LCOV - code coverage report
Current view: top level - src/cli - main.cpp (source / functions) Hit Total Coverage
Project: Dokit Lines: 110 186 59.1 %
Version: Functions: 6 16 37.5 %

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

Generated by: LCOV version 1.14