LCOV - code coverage report
Current view: top level - src/app - abstractcommand.cpp (source / functions) Hit Total Coverage
Project: QtPokit Lines: 105 106 99.1 %
Version: Functions: 10 11 90.9 %

          Line data    Source code
       1             : // SPDX-FileCopyrightText: 2022 Paul Colby <git@colby.id.au>
       2             : // SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4             : #include "abstractcommand.h"
       5             : 
       6             : #include <qtpokit/pokitdevice.h>
       7             : #include <qtpokit/pokitdiscoveryagent.h>
       8             : 
       9             : #include <QLocale>
      10             : 
      11             : /*!
      12             :  * \class AbstractCommand
      13             :  *
      14             :  * The AbstractCommand class provides a consistent base for the classes that implement CLI commands.
      15             :  */
      16             : 
      17             : /// \enum AbstractCommand::OutputFormat
      18             : /// \brief Supported output formats.
      19             : 
      20             : /*!
      21             :  * Constructs a new command with \a parent.
      22             :  */
      23        7230 : AbstractCommand::AbstractCommand(QObject * const parent) : QObject(parent),
      24        7230 :     discoveryAgent(new PokitDiscoveryAgent(this)), format(OutputFormat::Text)
      25             : {
      26        7230 :     connect(discoveryAgent, &PokitDiscoveryAgent::pokitDeviceDiscovered,
      27             :             this, &AbstractCommand::deviceDiscovered);
      28        7230 :     connect(discoveryAgent, &PokitDiscoveryAgent::finished,
      29             :             this, &AbstractCommand::deviceDiscoveryFinished);
      30        7230 :     connect(discoveryAgent,
      31             :         #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 0))
      32             :         QOverload<PokitDiscoveryAgent::Error>::of(&PokitDiscoveryAgent::error),
      33             :         #else
      34             :         &PokitDiscoveryAgent::errorOccurred,
      35             :         #endif
      36          45 :     [](const PokitDiscoveryAgent::Error &error) {
      37          90 :         qCWarning(lc).noquote() << tr("Bluetooth controller error:") << error;
      38          45 :         QCoreApplication::exit(EXIT_FAILURE);
      39          45 :     });
      40        7230 : }
      41             : 
      42             : /*!
      43             :  * Returns a list of CLI option names required by this command. The main console appication may
      44             :  * use this list to output an eror (and exit) if any of the returned names are not found in the
      45             :  * parsed CLI options.
      46             :  *
      47             :  * The (already parsed) \a parser may be used adjust the returned required options depending on the
      48             :  * value of other options. For example, the `logger` command only requires the `--mode` option if
      49             :  * the `--command` option is `start`.
      50             :  *
      51             :  * This base implementation simply returns an empty list. Derived classes should override this
      52             :  * function to include any required options.
      53             :  */
      54        4637 : QStringList AbstractCommand::requiredOptions(const QCommandLineParser &parser) const
      55             : {
      56             :     Q_UNUSED(parser);
      57        4637 :     return QStringList();
      58             : }
      59             : 
      60             : /*!
      61             :  * Returns a list of CLI option names supported by this command. The main console appication may
      62             :  * use this list to output a warning for any parsed CLI options not included in the returned list.
      63             :  *
      64             :  * The (already parsed) \a parser may be used adjust the returned supported options depending on the
      65             :  * value of other options. For example, the `logger` command only supported the `--timestamp` option
      66             :  * if the `--command` option is `start`.
      67             :  *
      68             :  * This base implementation simply returns requiredOptions(). Derived classes should override this
      69             :  * function to include optional options, such as:
      70             :  *
      71             :  * ```
      72             :  * QStringList Derived::supportedOptions(const QCommandLineParser &parser) const
      73             :  * {
      74             :  *     const QStringList list = AbstractCommand::supportedOptions(parser) + QStringList{ ... };
      75             :  *     list.sort();
      76             :  *     list.removeDuplicates(); // Optional, recommended.
      77             :  *     return list;
      78             :  * }
      79             :  * ```
      80             :  */
      81        2259 : QStringList AbstractCommand::supportedOptions(const QCommandLineParser &parser) const
      82             : {
      83       13554 :     return requiredOptions(parser) + QStringList{
      84             :         QLatin1String("debug"),
      85             :         QLatin1String("device"),
      86             :         QLatin1String("output"),
      87             :         QLatin1String("timeout"),
      88       11886 :     };
      89             : }
      90             : 
      91             : /*!
      92             :  * Returns an RFC 4180 compliant version of \a field. That is, if \a field contains any of the
      93             :  * the below four characters, than any double quotes are escaped (by addition double-quotes), and
      94             :  * the string itself surrounded in double-quotes. Otherwise, \a field is returned verbatim.
      95             :  *
      96             :  * Some examples:
      97             :  * ```
      98             :  * QCOMPARE(escapeCsvField("abc"), "abc");       // Returned unchanged.
      99             :  * QCOMPARE(escapeCsvField("a,c"), "\"a,c\"");   // Wrapped in double-quotes.
     100             :  * QCOMPARE(escapeCsvField("a\"c"), "\"a""c\""); // Existing double-quotes doubles, then wrapped.
     101             :  * ```
     102             :  */
     103        1497 : QString AbstractCommand::escapeCsvField(const QString &field)
     104             : {
     105        1480 :     if (field.contains(QLatin1Char(','))||field.contains(QLatin1Char('\r'))||
     106        4440 :         field.contains(QLatin1Char('"'))||field.contains(QLatin1Char('\n')))
     107             :     {
     108          40 :         return QString::fromLatin1("\"%1\"").arg(
     109          34 :             QString(field).replace(QLatin1Char('"'), QLatin1String("\"\"")));
     110             :     } else return field;
     111             : }
     112             : 
     113             : /*!
     114             :  * Returns \a value as a number of micros, such as microseconds, or microvolts. The string \a value
     115             :  * may end with the optional \a unit, such as `V` or `s`, which may also be preceded with a SI unit
     116             :  * prefix such as `m` for `milli`. If \a value contains no SI unit prefix, then the result will be
     117             :  * multiplied by 1,000 enough times to be greater than \a sensibleMinimum. This allows for
     118             :  * convenient use like:
     119             :  *
     120             :  * ```
     121             :  * const quin32t timeout = parseMicroValue(parser.value("window"), 's', 500*1000);
     122             :  * ```
     123             :  *
     124             :  * So that an unqalified period like "300" will be assumed to be 300 milliseconds, and not 300
     125             :  * microseconds, while a period like "1000" will be assume to be 1 second.
     126             :  *
     127             :  * If conversion fails for any reason, 0 is returned.
     128             :  */
     129         459 : quint32 AbstractCommand::parseMicroValue(const QString &value, const QString &unit,
     130             :                                          const quint32 sensibleMinimum)
     131             : {
     132             :     // Remove the optional (whole) unit suffix.
     133             :     quint32 scale = 0;
     134         216 :     QString number = value.trimmed();
     135         459 :     if ((!unit.isEmpty()) && (number.endsWith(unit, Qt::CaseInsensitive))) {
     136         204 :         number.chop(unit.length());
     137             :         scale = 1000 * 1000;
     138             :     }
     139             : 
     140             :     // Parse, and remove, the optional SI unit prefix.
     141         459 :     if (number.endsWith(QLatin1String("m"))) {
     142         136 :         number.chop(1);
     143             :         scale = 1000;
     144             :     }
     145             : 
     146             :     // Parse, and remove, the optional SI unit prefix.
     147         459 :     if (number.endsWith(QLatin1String("u"))) {
     148          17 :         number.chop(1);
     149             :         scale = 1;
     150             :     }
     151             : 
     152             :     // Parse the number as an (unsigned) integer.
     153         756 :     QLocale locale; bool ok;
     154         378 :     const quint32 integer = locale.toUInt(number, &ok);
     155         459 :     if (ok) {
     156         272 :         if ((scale == 0) && (integer != 0)) {
     157         170 :             for (scale = 1; (integer * scale) < sensibleMinimum; scale *= 1000);
     158             :         }
     159         272 :         return integer * scale;
     160             :     }
     161             : 
     162             :     // Parse the number as a (double) floating point number, and check that it is positive.
     163         154 :     const double dbl = locale.toDouble(number, &ok);
     164         187 :     if ((ok) && (dbl > 0)) {
     165          68 :         if ((scale == 0) && (dbl > 0.0)) {
     166          51 :             for (scale = 1; (dbl * scale) < sensibleMinimum; scale *= 1000);
     167             :         }
     168          68 :         return dbl * scale;
     169             :     }
     170             : 
     171             :     return 0; // Failed to parse as either integer, or float.
     172         162 : }
     173             : 
     174             : /*!
     175             :  * Returns \a value as a number of millis, such as milliseconds, or millivolts. The string \a value
     176             :  * may end with the optional \a unit, such as `V` or `s`, which may also be preceded with a SI unit
     177             :  * prefix such as `m` for `milli`. If \a value contains no SI unit prefix, then the result will be
     178             :  * multiplied by 1,000 enough times to be greater than \a sensibleMinimum. This allows for
     179             :  * convenient use like:
     180             :  *
     181             :  * ```
     182             :  * const quin32t timeout = parseMilliValue(parser.value("timeout"), 's', 600);
     183             :  * ```
     184             :  *
     185             :  * So that an unqalified period like "300" will be assumed to be 300 seconds, and not 300
     186             :  * milliseconds, while a period like "1000" will be assume to be 1 second.
     187             :  *
     188             :  * If conversion fails for any reason, 0 is returned.
     189             :  */
     190        1222 : quint32 AbstractCommand::parseMilliValue(const QString &value, const QString &unit,
     191             :                                          const quint32 sensibleMinimum)
     192             : {
     193             :     // Remove the optional (whole) unit suffix.
     194             :     quint32 scale = 0;
     195         520 :     QString number = value.trimmed();
     196        1222 :     if ((!unit.isEmpty()) && (number.endsWith(unit, Qt::CaseInsensitive))) {
     197         767 :         number.chop(unit.length());
     198             :         scale = 1000;
     199             :     }
     200             : 
     201             :     // Parse, and remove, the optional SI unit prefix.
     202        1222 :     if (number.endsWith(QLatin1String("m"))) {
     203         605 :         number.chop(1);
     204             :         scale = 1;
     205             :     }
     206             : 
     207             :     // Parse the number as an (unsigned) integer.
     208        1976 :     QLocale locale; bool ok;
     209         988 :     const quint32 integer = locale.toUInt(number, &ok);
     210        1222 :     if (ok) {
     211         922 :         if ((scale == 0) && (integer != 0)) {
     212         283 :             for (scale = 1; (integer * scale) < sensibleMinimum; scale *= 1000);
     213             :         }
     214         922 :         return integer * scale;
     215             :     }
     216             : 
     217             :     // Parse the number as a (double) floating point number, and check that it is positive.
     218         240 :     const double dbl = locale.toDouble(number, &ok);
     219         300 :     if ((ok) && (dbl > 0)) {
     220          68 :         if ((scale == 0) && (dbl > 0.0)) {
     221          51 :             for (scale = 1; (dbl * scale) < sensibleMinimum; scale *= 1000);
     222             :         }
     223          68 :         return dbl * scale;
     224             :     }
     225             : 
     226             :     return 0; // Failed to parse as either integer, or float.
     227         468 : }
     228             : 
     229             : /*!
     230             :  * Returns \a value as a number, with optional SI unit prefix, and optional \a unit suffix. For
     231             :  * example:
     232             :  *
     233             :  * ```
     234             :  * QCOMPARE(parseWholeValue("1.2Mohm", "ohm"), 1200000);
     235             :  * ```
     236             :  *
     237             :  * If conversion fails for any reason, 0 is returned.
     238             :  */
     239         323 : quint32 AbstractCommand::parseWholeValue(const QString &value, const QString &unit)
     240             : {
     241             :     // Remove the optional unit suffix.
     242         152 :     QString number = value.trimmed();
     243         323 :     if (number.endsWith(unit, Qt::CaseInsensitive)) {
     244         170 :         number.chop(unit.length());
     245             :     }
     246             : 
     247             :     // Parse, and remove, the optional SI unit prefix.
     248             :     quint32 scale = 1;
     249         323 :     if (number.endsWith(QLatin1String("k"), Qt::CaseInsensitive)) {
     250          51 :         number.chop(1);
     251             :         scale = 1000;
     252         272 :     } else if (number.endsWith(QLatin1String("M"))) {
     253          51 :         number.chop(1);
     254             :         scale = 1000 * 1000;
     255             :     }
     256             : 
     257             :     // Parse the number as an (unsigned) integer.
     258         532 :     QLocale locale; bool ok;
     259         266 :     const quint16 integer = locale.toUInt(number, &ok);
     260         323 :     if (ok) {
     261         136 :         return integer * scale;
     262             :     }
     263             : 
     264             :     // Parse the number as a (double) floating point number, and check that it is positive.
     265         154 :     const double dbl = locale.toDouble(number, &ok);
     266         187 :     if ((ok) && (dbl > 0)) {
     267          51 :         return dbl * scale;
     268             :     }
     269             : 
     270             :     return 0; // Failed to parse as either integer, or float.
     271         114 : }
     272             : 
     273             : /*!
     274             :  * Processes the relevant options from the command line \a parser.
     275             :  *
     276             :  * On success, returns an empty QStringList, otherwise returns a list of CLI errors that the caller
     277             :  * should report appropriately before exiting.
     278             :  *
     279             :  * This base implementations performs some common checks, such as ensuring that required options are
     280             :  * present. Derived classes should override this function to perform further processing, typically
     281             :  * inovking this base implementation as a first step, such as:
     282             :  *
     283             :  * ```
     284             :  * QStringList CustomCommand::processOptions(const QCommandLineParser &parser)
     285             :  * {
     286             :  *     QStringList errors = AbstractCommand::processOptions(parser);
     287             :  *     if (!errors.isEmpty()) {
     288             :  *         return errors;
     289             :  *     }
     290             :  *
     291             :  *     // Do further procession of options.
     292             :  *
     293             :  *     return errors;
     294             :  * }
     295             :  * ```
     296             :  */
     297        1936 : QStringList AbstractCommand::processOptions(const QCommandLineParser &parser)
     298             : {
     299             :     // Report any supplied options that are not supported by this command.
     300        1936 :     const QStringList suppliedOptions = parser.optionNames();
     301        1936 :     const QStringList supportedOptions = this->supportedOptions(parser);
     302        5638 :     for (const QString &option: suppliedOptions) {
     303        3702 :         if (!supportedOptions.contains(option)) {
     304          37 :             qCInfo(lc).noquote() << tr("Ignoring option: %1").arg(option);
     305             :         }
     306             :     }
     307         360 :     QStringList errors;
     308             : 
     309             :     // Parse the device (name/addr/uuid) option.
     310        2296 :     if (parser.isSet(QLatin1String("device"))) {
     311          40 :         deviceToScanFor = parser.value(QLatin1String("device"));
     312             :     }
     313             : 
     314             :     // Parse the output format options (if supported, and supplied).
     315        4099 :     if ((supportedOptions.contains(QLatin1String("output"))) && // Derived classes may have removed.
     316        4193 :         (parser.isSet(QLatin1String("output"))))
     317             :     {
     318         546 :         const QString output = parser.value(QLatin1String("output")).toLower();
     319         221 :         if (output == QLatin1String("csv")) {
     320          51 :             format = OutputFormat::Csv;
     321         170 :         } else if (output == QLatin1String("json")) {
     322          51 :             format = OutputFormat::Json;
     323         119 :         } else if (output == QLatin1String("text")) {
     324          51 :             format = OutputFormat::Text;
     325             :         } else {
     326          80 :             errors.append(tr("Unknown output format: %1").arg(output));
     327             :         }
     328          78 :     }
     329             : 
     330             :     // Parse the device scan timeout option.
     331        2296 :     if (parser.isSet(QLatin1String("timeout"))) {
     332         156 :         const quint32 timeout = parseMilliValue(parser.value(QLatin1String("timeout")),
     333             :                                                 QLatin1String("s"), 500);
     334         117 :         if (timeout == 0) {
     335         108 :             errors.append(tr("Invalid timeout: %1").arg(parser.value(QLatin1String("timeout"))));
     336             :         } else {
     337          63 :             discoveryAgent->setLowEnergyDiscoveryTimeout(timeout);
     338          63 :             qCDebug(lc).noquote() << tr("Set scan timeout to %1").arg(
     339           0 :                 discoveryAgent->lowEnergyDiscoveryTimeout());
     340             :         }
     341             :     }
     342             : 
     343             :     // Return errors for any required options that are absent.
     344        1936 :     const QStringList requiredOptions = this->requiredOptions(parser);
     345        4569 :     for (const QString &option: requiredOptions) {
     346        2633 :         if (!parser.isSet(option)) {
     347         240 :             errors.append(tr("Missing required option: %1").arg(option));
     348             :         }
     349             :     }
     350        1936 :     return errors;
     351             : }
     352             : 
     353             : /*!
     354             :  * \fn virtual bool AbstractCommand::start()
     355             :  *
     356             :  * Begins the functionality of this command, and returns `true` if begun successfully, `false`
     357             :  * otherwise.
     358             :  */
     359             : 
     360             : /*!
     361             :  * \fn virtual void AbstractCommand::deviceDiscovered(const QBluetoothDeviceInfo &info) = 0
     362             :  *
     363             :  * Handles PokitDiscoveryAgent::pokitDeviceDiscovered signal. Derived classes must
     364             :  * implement this slot to begin whatever actions are relevant when a Pokit device has been
     365             :  * discovered. For example, the 'scan' command would simply output the \a info details, whereas
     366             :  * most other commands would begin connecting if \a info is the device they're after.
     367             :  */
     368             : 
     369             : /*!
     370             :  * \fn virtual void AbstractCommand::deviceDiscoveryFinished() = 0
     371             :  *
     372             :  * Handles PokitDiscoveryAgent::deviceDiscoveryFinished signal. Derived classes must
     373             :  * implement this slot to perform whatever actions are appropraite when discovery is finished.
     374             :  * For example, the 'scan' command would simply exit, whereas most other commands would verify that
     375             :  * an appropriate device was found.
     376             :  */

Generated by: LCOV version 1.14