LCOV - code coverage report
Current view: top level - src/cli - main.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 57.6 % 170 98
Version: Functions: 77.8 % 9 7

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2022-2025 Paul Colby <git@colby.id.au>
       2              : // SPDX-License-Identifier: LGPL-3.0-or-later
       3              : 
       4              : #include "calibratecommand.h"
       5              : #include "dsocommand.h"
       6              : #include "flashledcommand.h"
       7              : #include "infocommand.h"
       8              : #include "loggerfetchcommand.h"
       9              : #include "loggerstartcommand.h"
      10              : #include "loggerstopcommand.h"
      11              : #include "metercommand.h"
      12              : #include "scancommand.h"
      13              : #include "setnamecommand.h"
      14              : #include "settorchcommand.h"
      15              : #include "statuscommand.h"
      16              : 
      17              : #include <QCommandLineParser>
      18              : #include <QCoreApplication>
      19              : #include <QLocale>
      20              : #include <QLoggingCategory>
      21              : #include <QTranslator>
      22              : 
      23              : #include <iostream>
      24              : 
      25              : #if defined(Q_OS_UNIX)
      26              : #include <unistd.h>
      27              : #elif defined(Q_OS_WIN)
      28              : #include <Windows.h>
      29              : #endif
      30              : 
      31           67 : static Q_LOGGING_CATEGORY(lc, "dokit.cli.main", QtInfoMsg);
      32              : 
      33              : class Private
      34              : {
      35          546 :     Q_DECLARE_TR_FUNCTIONS(cli_main)
      36              : };
      37              : 
      38              : inline bool haveConsole()
      39              : {
      40              :     #if defined(Q_OS_UNIX)
      41           21 :     return isatty(STDERR_FILENO);
      42              :     #elif defined(Q_OS_WIN)
      43              :     return GetConsoleWindow();
      44              :     #else
      45              :     return false;
      46              :     #endif
      47              : }
      48              : 
      49           21 : void configureLogging(const QCommandLineParser &parser)
      50              : {
      51              :     // Start with the Qt default message pattern (see qtbase:::qlogging.cpp:defaultPattern)
      52            9 :     QString messagePattern = QStringLiteral("%{if-category}%{category}: %{endif}%{message}");
      53              : 
      54           30 :     if (parser.isSet(QStringLiteral("debug"))) {
      55              :         #ifdef QT_MESSAGELOGCONTEXT
      56              :         // %{file}, %{line} and %{function} are only available when QT_MESSAGELOGCONTEXT is set.
      57              :         messagePattern.prepend(QStringLiteral("%{function} "));
      58              :         #endif
      59            0 :         messagePattern.prepend(QStringLiteral("%{time process} %{threadid} %{type} "));
      60            0 :         QLoggingCategory::setFilterRules(QStringLiteral("dokit.*.debug=true\npokit.*.debug=true"));
      61              :     }
      62              : 
      63           30 :     if (const QString color = parser.value(QStringLiteral("color"));
      64           72 :         (color == QStringLiteral("yes")) || (color == QStringLiteral("auto") && haveConsole())) {
      65            0 :         messagePattern.prepend(QStringLiteral(
      66              :         "%{if-debug}\x1b[37m%{endif}"      // White
      67              :         "%{if-info}\x1b[32m%{endif}"       // Green
      68              :         "%{if-warning}\x1b[35m%{endif}"    // Magenta
      69              :         "%{if-critical}\x1b[31m%{endif}"   // Red
      70              :         "%{if-fatal}\x1b[31;1m%{endif}")); // Red and bold
      71            0 :         messagePattern.append(QStringLiteral("\x1b[0m")); // Reset.
      72           12 :     }
      73              : 
      74           21 :     qSetMessagePattern(messagePattern);
      75           21 : }
      76              : 
      77              : enum class Command {
      78              :     None,
      79              :     Info,
      80              :     Status,
      81              :     Meter,
      82              :     DSO,
      83              :     LoggerStart,
      84              :     LoggerStop,
      85              :     LoggerFetch,
      86              :     Scan,
      87              :     SetName,
      88              :     SetTorch,
      89              :     FlashLed,
      90              :     Calibrate
      91              : };
      92              : 
      93            0 : void showCliError(const QString &errorText)
      94              : {
      95              :     // Output the same way QCommandLineParser does (qcommandlineparser.cpp::showParserMessage).
      96            0 :     const QString message = QCoreApplication::applicationName() + QLatin1String(": ")
      97            0 :         + errorText + QLatin1Char('\n');
      98            0 :     std::cerr << qUtf8Printable(message);
      99            0 : }
     100              : 
     101           21 : Command getCliCommand(const QStringList &posArguments)
     102              : {
     103           21 :     if (posArguments.isEmpty()) {
     104              :         return Command::None;
     105              :     }
     106            0 :     if (posArguments.size() > 1) {
     107            0 :         showCliError(Private::tr("More than one command: %1").arg(posArguments.join(QStringLiteral(", "))));
     108            0 :         ::exit(EXIT_FAILURE);
     109              :     }
     110              : 
     111              :     const QMap<QString, Command> supportedCommands {
     112            0 :         { QStringLiteral("info"),         Command::Info },
     113            0 :         { QStringLiteral("status"),       Command::Status },
     114            0 :         { QStringLiteral("meter"),        Command::Meter },
     115            0 :         { QStringLiteral("dso"),          Command::DSO },
     116            0 :         { QStringLiteral("logger-start"), Command::LoggerStart },
     117            0 :         { QStringLiteral("logger-stop"),  Command::LoggerStop },
     118            0 :         { QStringLiteral("logger-fetch"), Command::LoggerFetch },
     119            0 :         { QStringLiteral("scan"),         Command::Scan },
     120            0 :         { QStringLiteral("set-name"),     Command::SetName },
     121            0 :         { QStringLiteral("set-torch"),    Command::SetTorch },
     122            0 :         { QStringLiteral("flash-led"),    Command::FlashLed },
     123            0 :         { QStringLiteral("calibrate"),    Command::Calibrate },
     124            0 :     };
     125            0 :     const Command command = supportedCommands.value(posArguments.first().toLower(), Command::None);
     126            0 :     if (command == Command::None) {
     127            0 :         showCliError(Private::tr("Unknown command: %1").arg(posArguments.first()));
     128            0 :         ::exit(EXIT_FAILURE);
     129              :     }
     130              :     return command;
     131            0 : }
     132              : 
     133           21 : Command parseCommandLine(const QStringList &appArguments, QCommandLineParser &parser)
     134              : {
     135              :     // Setup the command line options.
     136          141 :     parser.addOptions({
     137           33 :         { QStringLiteral("color"),
     138           21 :           Private::tr("Colors the console output. Valid options are: yes, no and auto. The default is auto."),
     139           21 :           QStringLiteral("yes|no|auto"), QStringLiteral("auto")},
     140           21 :         {{QStringLiteral("debug")},
     141           21 :           Private::tr("Enable debug output.")},
     142            9 :         {{QStringLiteral("d"), QStringLiteral("device")},
     143           21 :           Private::tr("Set the name, hardware address or macOS UUID of Pokit device to use. If not specified, "
     144              :           "the first discovered Pokit device will be used."),
     145           21 :           Private::tr("device")},
     146              :     });
     147           21 :     parser.addHelpOption();
     148          261 :     parser.addOptions({
     149           33 :         {{QStringLiteral("interval")},
     150           21 :           Private::tr("Set the update interval for DOS, meter and "
     151              :           "logger modes. Suffixes such as 's' and 'ms' (for seconds and milliseconds) may be used. "
     152              :           "If no suffix is present, the units will be inferred from the magnitide of the given "
     153              :           "interval. If the option itself is not specified, a sensible default will be chosen "
     154              :           "according to the selected command."),
     155           21 :           Private::tr("interval")},
     156           29 :         {{QStringLiteral("mode")},
     157           21 :           Private::tr("Set the desired operation mode. For "
     158              :           "meter, dso, and logger commands, the supported modes are: AC Voltage, DC Voltage, AC Current, "
     159              :           "DC Current, Resistance, Diode, Continuity, and Temperature. All are case insensitive. "
     160              :           "Only the first four options are available for dso and logger commands; the rest are "
     161              :           "available in meter mode only. Temperature is also available for logger commands, but "
     162              :           "requires firmware v1.5 or later for Pokit devices to support it. For the set-torch command "
     163              :           "supported modes are On and Off."),
     164           21 :           Private::tr("mode")},
     165           29 :         {{QStringLiteral("new-name")},
     166           21 :           Private::tr("Give the desired new name for the set-name command."), Private::tr("name")},
     167           29 :         {{QStringLiteral("output")},
     168           21 :           Private::tr("Set the format for output. Supported "
     169              :           "formats are: CSV, JSON and Text. All are case insenstitve. The default is Text."),
     170           21 :           Private::tr("format"),
     171           21 :           Private::tr("text")},
     172           21 :         {{QStringLiteral("range")},
     173           21 :           Private::tr("Set the desired measurement range. Pokit "
     174              :           "devices support specific ranges, such as 0 to 300mV. Specify the desired upper limit, "
     175              :           "and the best range will be selected, or use 'auto' to enable the Pokit device's auto-"
     176              :           "range feature. The default is 'auto'."),
     177           21 :           Private::tr("range"), QStringLiteral("auto")},
     178           21 :         {{QStringLiteral("samples")}, Private::tr("Set the number of samples to acquire."), Private::tr("count")},
     179           29 :         {{QStringLiteral("temperature")},
     180           21 :           Private::tr("Set the current ambient temperature for the calibration command."), Private::tr("degrees")},
     181           29 :         {{QStringLiteral("timeout")},
     182           21 :           Private::tr("Set the device discovery scan timeout. "
     183              :           "Suffixes such as 's' and 'ms' (for seconds and milliseconds) may be used. "
     184              :           "If no suffix is present, the units will be inferred from the magnitide of the given "
     185              :           "interval. The default behaviour is no timeout."),
     186           21 :           Private::tr("period")},
     187           29 :         {{QStringLiteral("timestamp")},
     188           21 :           Private::tr("Set the optional starting timestamp for data logging. Default to 'now'."),
     189           21 :           Private::tr("period")},
     190           21 :         {{QStringLiteral("trigger-level")}, Private::tr("Set the DSO trigger level."), Private::tr("level")},
     191           29 :         {{QStringLiteral("trigger-mode")},
     192           21 :           Private::tr("Set the DSO trigger mode. Supported modes are: free, rising and falling. The default is free."),
     193           21 :           Private::tr("mode"), QStringLiteral("free")},
     194              :     });
     195           21 :     parser.addVersionOption();
     196              : 
     197              :     // Add supported 'commands' (as positional arguments, so they'll appear in the help text).
     198           30 :     parser.addPositionalArgument(QStringLiteral("info"),
     199           21 :         Private::tr("Get Pokit device information"), QStringLiteral(" "));
     200           30 :     parser.addPositionalArgument(QStringLiteral("status"),
     201           21 :         Private::tr("Get Pokit device status"), QStringLiteral(" "));
     202           30 :     parser.addPositionalArgument(QStringLiteral("meter"),
     203           21 :         Private::tr("Access Pokit device's multimeter mode"), QStringLiteral(" "));
     204           30 :     parser.addPositionalArgument(QStringLiteral("dso"),
     205           21 :         Private::tr("Access Pokit device's DSO mode"), QStringLiteral(" "));
     206           30 :     parser.addPositionalArgument(QStringLiteral("logger-start"),
     207           21 :         Private::tr("Start Pokit device's data logger mode"), QStringLiteral(" "));
     208           30 :     parser.addPositionalArgument(QStringLiteral("logger-stop"),
     209           21 :         Private::tr("Stop Pokit device's data logger mode"), QStringLiteral(" "));
     210           30 :     parser.addPositionalArgument(QStringLiteral("logger-fetch"),
     211           21 :         Private::tr("Fetch Pokit device's data logger samples"), QStringLiteral(" "));
     212           30 :     parser.addPositionalArgument(QStringLiteral("scan"),
     213           21 :         Private::tr("Scan Bluetooth for Pokit devices"), QStringLiteral(" "));
     214           30 :     parser.addPositionalArgument(QStringLiteral("set-name"),
     215           21 :         Private::tr("Set Pokit device's name"), QStringLiteral(" "));
     216           30 :     parser.addPositionalArgument(QStringLiteral("set-torch"),
     217           21 :         Private::tr("Set Pokit device's torch on or off"), QStringLiteral(" "));
     218           30 :     parser.addPositionalArgument(QStringLiteral("flash-led"),
     219           21 :         Private::tr("Flash Pokit device's LED (Pokit Meter only)"), QStringLiteral(" "));
     220           30 :     parser.addPositionalArgument(QStringLiteral("calibrate"),
     221           21 :         Private::tr("Calibrate Pokit device temperature"), QStringLiteral(" "));
     222              : 
     223              :     // Do the initial parse, the see if we have a command specified yet.
     224           21 :     parser.parse(appArguments);
     225           21 :     configureLogging(parser);
     226           26 :     qCDebug(lc).noquote() << QCoreApplication::applicationName() << QCoreApplication::applicationVersion();
     227           26 :     qCDebug(lc).noquote() << "Qt " QT_VERSION_STR " compile-time";
     228           26 :     qCDebug(lc).noquote() << "Qt" << qVersion() << "runtime";
     229           21 :     const Command command = getCliCommand(parser.positionalArguments());
     230              : 
     231              :     // If we have a (single, valid) command, then remove the commands list from the help text.
     232           21 :     if (command != Command::None) {
     233            0 :         parser.clearPositionalArguments();
     234              :     }
     235              : 
     236              :     // Handle -h|--help explicitly, so we can tweak the output to include the <command> info.
     237           30 :     if (parser.isSet(QStringLiteral("help"))) {
     238            0 :         const QString commandString = (command == Command::None) ? QStringLiteral("<command>")
     239            0 :                 : parser.positionalArguments().constFirst();
     240            0 :         std::cout << qUtf8Printable(parser.helpText()
     241              :             .replace(QStringLiteral("[options]"), commandString + QStringLiteral(" [options]"))
     242              :             .replace(QStringLiteral("Arguments:"),  QStringLiteral("Command:"))
     243              :         );
     244            0 :         ::exit(EXIT_SUCCESS);
     245            0 :     }
     246              : 
     247              :     // Process the command for real (ie throw errors for unknown options, etc).
     248           21 :     parser.process(appArguments);
     249            0 :     return command;
     250          512 : }
     251              : 
     252            0 : AbstractCommand * getCommandObject(const Command command, QObject * const parent)
     253              : {
     254            0 :     switch (command) {
     255            0 :     case Command::None:
     256            0 :         showCliError(Private::tr("Missing argument: <command>\nSee --help for usage information."));
     257            0 :         return nullptr;
     258            0 :     case Command::Calibrate:   return new CalibrateCommand(parent);
     259            0 :     case Command::DSO:         return new DsoCommand(parent);
     260            0 :     case Command::FlashLed:    return new FlashLedCommand(parent);
     261            0 :     case Command::Info:        return new InfoCommand(parent);
     262            0 :     case Command::LoggerStart: return new LoggerStartCommand(parent);
     263            0 :     case Command::LoggerStop:  return new LoggerStopCommand(parent);
     264            0 :     case Command::LoggerFetch: return new LoggerFetchCommand(parent);
     265            0 :     case Command::Meter:       return new MeterCommand(parent);
     266            0 :     case Command::Scan:        return new ScanCommand(parent);
     267            0 :     case Command::Status:      return new StatusCommand(parent);
     268            0 :     case Command::SetName:     return new SetNameCommand(parent);
     269            0 :     case Command::SetTorch:    return new SetTorchCommand(parent);
     270              :     }
     271            0 :     showCliError(Private::tr("Unknown command (%1)").arg((int)command));
     272            0 :     return nullptr;
     273              : }
     274              : 
     275           21 : int main(int argc, char *argv[])
     276              : {
     277              :     // Setup the core application.
     278           21 :     QCoreApplication app(argc, argv);
     279           30 :     QCoreApplication::setApplicationName(QStringLiteral(PROJECT_NAME));
     280           21 :     QCoreApplication::setApplicationVersion(QString::fromLatin1(PROJECT_VERSION
     281              :         #ifdef PROJECT_PRE_RELEASE
     282              :         "-" PROJECT_PRE_RELEASE
     283              :         #endif
     284              :         #ifdef PROJECT_BUILD_ID
     285              :         "+" PROJECT_BUILD_ID
     286              :         #endif
     287              :     ));
     288              : 
     289              :     // Install localised translators, if we have translations for the current locale.
     290           21 :     QTranslator appTranslator, libTranslator;
     291           21 :     if (appTranslator.load(QLocale(), QStringLiteral("cli"), QStringLiteral("/"), QStringLiteral(":/i18n"))) {
     292            0 :         QCoreApplication::installTranslator(&appTranslator);
     293              :     }
     294           21 :     if (libTranslator.load(QLocale(), QStringLiteral("lib"), QStringLiteral("/"), QStringLiteral(":/i18n"))) {
     295            0 :         QCoreApplication::installTranslator(&libTranslator);
     296              :     }
     297              : 
     298              :     // Parse the command line.
     299           21 :     const QStringList appArguments = QCoreApplication::arguments();
     300           21 :     QCommandLineParser parser;
     301           21 :     const Command commandType = parseCommandLine(appArguments, parser);
     302              : #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) // QTranslator::filePath() added in Qt 5.15.
     303            0 :     qCDebug(lc).noquote() << "App translations:" <<
     304            0 :         (appTranslator.filePath().isEmpty() ? QStringLiteral("<none>") : appTranslator.filePath());
     305            0 :     qCDebug(lc).noquote() << "Library translations:" <<
     306            0 :         (libTranslator.filePath().isEmpty() ? QStringLiteral("<none>") : libTranslator.filePath());
     307              : #else
     308            0 :     qCDebug(lc).noquote() << "App translations:" << (!appTranslator.isEmpty());
     309            0 :     qCDebug(lc).noquote() << "Lib translations:" << (!libTranslator.isEmpty());
     310              : #endif
     311              : 
     312              :     // Handle the given command.
     313            0 :     AbstractCommand * const command = getCommandObject(commandType, &app);
     314            0 :     if (command == nullptr) {
     315              :         return EXIT_FAILURE; // getCommandObject will have logged the reason already.
     316              :     }
     317            0 :     const QStringList cliErrors = command->processOptions(parser);
     318            0 :     for (const QString &error: cliErrors) {
     319            0 :         showCliError(error);
     320              :     }
     321            0 :     const int result = ((cliErrors.isEmpty()) && (command->start())) ? QCoreApplication::exec() : EXIT_FAILURE;
     322            0 :     delete command; // We don't strictly need to do this, but it does fix QTBUG-119063, and is probably good practice.
     323              :     return result;
     324            0 : }
        

Generated by: LCOV version 2.2-1