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

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

Generated by: LCOV version 2.4-0