LCOV - code coverage report
Current view: top level - src/cli - dsocommand.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 79.0 % 219 173
Version: Functions: 70.0 % 10 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 "dsocommand.h"
       5              : #include "../stringliterals_p.h"
       6              : 
       7              : #include <qtpokit/pokitdevice.h>
       8              : 
       9              : #include <QJsonDocument>
      10              : #include <QJsonObject>
      11              : 
      12              : #include <iostream>
      13              : 
      14              : DOKIT_USE_STRINGLITERALS
      15              : 
      16              : /*!
      17              :  * \class DsoCommand
      18              :  *
      19              :  * The DsoCommand class implements the `dso` CLI command.
      20              :  */
      21              : 
      22              : /*!
      23              :  * Construct a new DsoCommand object with \a parent.
      24              :  */
      25         9104 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
      26         2732 : {
      27              : 
      28         9172 : }
      29              : 
      30         4130 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
      31         1789 : {
      32        20846 :     return DeviceCommand::requiredOptions(parser) + QStringList{
      33         1789 :         u"mode"_s,
      34         1789 :         u"range"_s,
      35        16303 :     };
      36         1789 : }
      37              : 
      38         2030 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
      39          839 : {
      40        14150 :     return DeviceCommand::supportedOptions(parser) + QStringList{
      41          839 :         u"interval"_s,
      42          839 :         u"samples"_s,
      43          839 :         u"trigger-level"_s,
      44          839 :         u"trigger-mode"_s,
      45        12033 :     };
      46          839 : }
      47              : 
      48              : /*!
      49              :  * \copybrief DeviceCommand::processOptions
      50              :  *
      51              :  * This implementation extends DeviceCommand::processOptions to process additional CLI options
      52              :  * supported (or required) by this command.
      53              :  */
      54         1960 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
      55          728 : {
      56         2688 :     QStringList errors = DeviceCommand::processOptions(parser);
      57         2688 :     if (!errors.isEmpty()) {
      58           78 :         return errors;
      59           78 :     }
      60              : 
      61              :     // Parse the (required) mode option.
      62         4150 :     if (const QString mode = parser.value(u"mode"_s).trimmed().toLower();
      63         5300 :         mode.startsWith(u"ac v"_s) || mode.startsWith(u"vac"_s)) {
      64           96 :         settings.mode = DsoService::Mode::AcVoltage;
      65         5088 :     } else if (mode.startsWith(u"dc v"_s) || mode.startsWith(u"vdc"_s)) {
      66         2016 :         settings.mode = DsoService::Mode::DcVoltage;
      67         1104 :     } else if (mode.startsWith(u"ac c"_s) || mode.startsWith(u"aac"_s)) {
      68           96 :         settings.mode = DsoService::Mode::AcCurrent;
      69          424 :     } else if (mode.startsWith(u"dc c"_s) || mode.startsWith(u"adc"_s)) {
      70           96 :         settings.mode = DsoService::Mode::DcCurrent;
      71           26 :     } else {
      72          184 :         errors.append(tr("Unknown DSO mode: %1").arg(parser.value(u"mode"_s)));
      73           26 :         return errors;
      74          626 :     }
      75              : 
      76              :     // Parse the (required) range option.
      77         1728 :     QString unit;
      78          624 :     {
      79         2304 :         const QString value = parser.value(u"range"_s);
      80          624 :         quint32 sensibleMinimum = 0;
      81         2304 :         switch (settings.mode) {
      82            0 :         case DsoService::Mode::Idle:
      83            0 :             Q_ASSERT(false); // Not possible, since the mode parsing above never allows Idle.
      84            0 :             break;
      85         2086 :         case DsoService::Mode::DcVoltage:
      86          572 :         case DsoService::Mode::AcVoltage:
      87         2112 :             minRangeFunc = minVoltageRange;
      88         2112 :             unit = u"V"_s;
      89          572 :             sensibleMinimum = 50; // mV.
      90         2112 :             break;
      91          166 :         case DsoService::Mode::DcCurrent:
      92           52 :         case DsoService::Mode::AcCurrent:
      93          192 :             minRangeFunc = minCurrentRange;
      94          192 :             unit = u"A"_s;
      95           52 :             sensibleMinimum = 5; // mA.
      96          192 :             break;
      97          624 :         }
      98          624 :         Q_ASSERT(!unit.isEmpty());
      99         2304 :         rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
     100         2304 :         if (rangeOptionValue == 0) {
     101          142 :             errors.append(tr("Invalid range value: %1").arg(value));
     102           26 :         }
     103         1200 :     }
     104              : 
     105              :     // Parse the trigger-level option.
     106         3408 :     if (parser.isSet(u"trigger-level"_s)) {
     107          208 :         float sign = 1.0;
     108         1208 :         const QString rawValue = parser.value(u"trigger-level"_s);
     109          208 :         QString absValue = rawValue;
     110          208 :         DOKIT_STRING_INDEX_TYPE nonSpacePos;
     111          768 :         for (nonSpacePos = 0; (nonSpacePos < rawValue.length()) && (rawValue.at(nonSpacePos) == u' '); ++nonSpacePos);
     112          768 :         if ((nonSpacePos < rawValue.length()) && (rawValue.at(nonSpacePos) == u'-')) {
     113            0 :             absValue = rawValue.mid(nonSpacePos+1);
     114            0 :             sign = -1.0;
     115            0 :         }
     116          768 :         const float level = parseNumber<std::ratio<1>,float>(absValue, unit, 0.f);
     117         1040 :         qCDebug(lc) << "Trigger level" << rawValue << absValue << nonSpacePos << sign << level;
     118          768 :         if (qIsNaN(level)) {
     119          142 :             errors.append(tr("Invalid trigger-level value: %1").arg(rawValue));
     120          182 :         } else {
     121          672 :             settings.triggerLevel = sign * level;
     122          910 :             qCDebug(lc) << "Trigger level" << settings.triggerLevel;
     123              :             // Check the trigger level is within the Votage / Current range.
     124         1162 :             if ((rangeOptionValue != 0) && (qAbs(settings.triggerLevel) > (rangeOptionValue/1000.0))) {
     125            0 :                 errors.append(tr("Trigger-level %1%2 is outside range ±%3%2").arg(
     126            0 :                     appendSiPrefix(settings.triggerLevel), unit, appendSiPrefix(rangeOptionValue / 1000.0)));
     127            0 :             }
     128          182 :         }
     129          400 :     }
     130              : 
     131              :     // Parse the trigger-mode option.
     132         3408 :     if (parser.isSet(u"trigger-mode"_s)) {
     133         1328 :         const QString triggerMode = parser.value(u"trigger-mode"_s).trimmed().toLower();
     134         1136 :         if (triggerMode.startsWith(u"free"_s)) {
     135          288 :             settings.command = DsoService::Command::FreeRunning;
     136          710 :         } else if (triggerMode.startsWith(u"ris"_s)) {
     137          192 :            settings.command = DsoService::Command::RisingEdgeTrigger;
     138          426 :         } else if (triggerMode.startsWith(u"fall"_s)) {
     139          192 :             settings.command = DsoService::Command::FallingEdgeTrigger;
     140           52 :         } else {
     141          184 :             errors.append(tr("Unknown trigger mode: %1").arg(parser.value(u"trigger-mode"_s)));
     142           26 :         }
     143          400 :     }
     144              : 
     145              :     // Ensure that if either trigger option is present, then both are.
     146         5088 :     if (parser.isSet(u"trigger-level"_s) != parser.isSet(u"trigger-mode"_s)) {
     147          192 :         errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
     148           52 :     }
     149              : 
     150              :     // Parse the interval option.
     151         3408 :     if (parser.isSet(u"interval"_s)) {
     152          755 :         const QString value = parser.value(u"interval"_s);
     153          480 :         const quint32 interval = parseNumber<std::micro>(value, u"s"_s, (quint32)500'000);
     154          480 :         if (interval == 0) {
     155          284 :             errors.append(tr("Invalid interval value: %1").arg(value));
     156           78 :         } else {
     157          288 :             settings.samplingWindow = interval;
     158           78 :         }
     159          250 :     }
     160              : 
     161              :     // Parse the samples option.
     162         3408 :     if (parser.isSet(u"samples"_s)) {
     163          755 :         const QString value = parser.value(u"samples"_s);
     164          480 :         const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
     165          480 :         if (samples == 0) {
     166          284 :             errors.append(tr("Invalid samples value: %1").arg(value));
     167          288 :         } else if (samples > std::numeric_limits<quint16>::max()) {
     168          105 :             errors.append(tr("Samples value (%1) must be no greater than %2")
     169          197 :                 .arg(value).arg(std::numeric_limits<quint16>::max()));
     170           52 :         } else {
     171          192 :             if (samples > 8192) {
     172          246 :                 qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
     173           26 :             }
     174          192 :             settings.numberOfSamples = (quint16)samples;
     175           52 :         }
     176          250 :     }
     177          624 :     return errors;
     178         1200 : }
     179              : 
     180              : /*!
     181              :  * \copybrief DeviceCommand::getService
     182              :  *
     183              :  * This override returns a pointer to a DsoService object.
     184              :  */
     185            0 : AbstractPokitService * DsoCommand::getService()
     186            0 : {
     187            0 :     Q_ASSERT(device);
     188            0 :     if (!service) {
     189            0 :         service = device->dso();
     190            0 :         Q_ASSERT(service);
     191            0 :         connect(service, &DsoService::settingsWritten,
     192            0 :                 this, &DsoCommand::settingsWritten);
     193            0 :     }
     194            0 :     return service;
     195            0 : }
     196              : 
     197              : /*!
     198              :  * \copybrief DeviceCommand::serviceDetailsDiscovered
     199              :  *
     200              :  * This override fetches the current device's status, and outputs it in the selected format.
     201              :  */
     202            0 : void DsoCommand::serviceDetailsDiscovered()
     203            0 : {
     204            0 :     DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
     205            0 :     settings.range = (minRangeFunc == nullptr) ? 0 : minRangeFunc(*service->pokitProduct(), rangeOptionValue);
     206            0 :     const QString range = service->toString(settings.range, settings.mode);
     207            0 :     const QString triggerInfo = (settings.command == DsoService::Command::FreeRunning) ? QString() :
     208            0 :         tr(", and a %1 at %2%3%4").arg(DsoService::toString(settings.command).toLower(),
     209            0 :             (settings.triggerLevel < 0.) ? u"-"_s : u""_s, appendSiPrefix(qAbs(settings.triggerLevel)),
     210            0 :             range.at(range.size()-1));
     211            0 :     qCInfo(lc).noquote() << tr("Sampling %1, with range %2, %Ln sample/s over %L3us%4.", nullptr, settings.numberOfSamples)
     212            0 :         .arg(DsoService::toString(settings.mode), (range.isNull()) ? QString::fromLatin1("N/A") : range)
     213            0 :         .arg(settings.samplingWindow).arg(triggerInfo);
     214            0 :     service->setSettings(settings);
     215            0 : }
     216              : 
     217              : /*!
     218              :  * \var DsoCommand::minRangeFunc
     219              :  *
     220              :  * Pointer to function for converting #rangeOptionValue to a Pokit device's range enumerator. This function pointer
     221              :  * is assigned during the command line parsing, but is not invoked until after the device's services are discovered,
     222              :  * because prior to that discovery, we don't know which product (Meter vs Pro vs Clamp, etc) we're talking to and thus
     223              :  * which enumerator list to be using.
     224              :  *
     225              :  * If the current mode does not support ranges (eg diode, and continuity modes), then this member will be \c nullptr.
     226              :  *
     227              :  * \see processOptions
     228              :  * \see serviceDetailsDiscovered
     229              :  */
     230              : 
     231              : /*!
     232              :  * Invoked when the DSO settings have been written.
     233              :  */
     234            0 : void DsoCommand::settingsWritten()
     235            0 : {
     236            0 :     Q_ASSERT(service);
     237            0 :     qCDebug(lc).noquote() << tr("Settings written; DSO has started.");
     238            0 :     connect(service, &DsoService::metadataRead, this, &DsoCommand::metadataRead);
     239            0 :     connect(service, &DsoService::samplesRead, this, &DsoCommand::outputSamples);
     240            0 :     service->enableMetadataNotifications();
     241            0 :     service->enableReadingNotifications();
     242            0 : }
     243              : 
     244              : /*!
     245              :  * Invoked when \a metadata has been received from the DSO.
     246              :  */
     247         4270 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
     248         1671 : {
     249         8015 :     qCDebug(lc) << "status:" << (int)(data.status);
     250         8015 :     qCDebug(lc) << "scale:" << data.scale;
     251         8015 :     qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
     252         8015 :     qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
     253         8015 :     qCDebug(lc) << "samplingWindow:" << data.samplingWindow;
     254         8015 :     qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
     255         8015 :     qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
     256         5941 :     this->metadata = data;
     257         5941 :     this->samplesToGo = data.numberOfSamples;
     258         5941 : }
     259              : 
     260              : /*!
     261              :  * Outputs DSO \a samples in the selected output format.
     262              :  */
     263         5040 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
     264         1872 : {
     265         5184 :     QString unit;
     266         6912 :     switch (metadata.mode) {
     267         2556 :     case DsoService::Mode::DcVoltage: unit = u"Vdc"_s; break;
     268         2556 :     case DsoService::Mode::AcVoltage: unit = u"Vac"_s; break;
     269         2556 :     case DsoService::Mode::DcCurrent: unit = u"Adc"_s; break;
     270         2556 :     case DsoService::Mode::AcCurrent: unit = u"Aac"_s; break;
     271            0 :     default:
     272            0 :         qCDebug(lc).noquote() << tr(R"(No known unit for mode %1 "%2".)").arg((int)metadata.mode)
     273            0 :             .arg(DsoService::toString(metadata.mode));
     274         1872 :     }
     275         8640 :     const QString range = service->toString(metadata.range, metadata.mode);
     276              : 
     277        37296 :     for (const qint16 &sample: samples) {
     278        32256 :         static int sampleNumber = 0; ++sampleNumber;
     279        32256 :         const float value = sample * metadata.scale;
     280        32256 :         switch (format) {
     281         2912 :         case OutputFormat::Csv:
     282        12288 :             for (; showCsvHeader; showCsvHeader = false) {
     283         2272 :                 std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
     284          416 :             }
     285        22400 :             std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
     286         2912 :                 .arg(sampleNumber).arg(value).arg(unit, range));
     287        10752 :             break;
     288        10752 :         case OutputFormat::Json:
     289        56224 :             std::cout << QJsonDocument(QJsonObject{
     290        12432 :                     { u"value"_s,  value },
     291        12432 :                     { u"unit"_s,   unit },
     292        12432 :                     { u"range"_s,  range },
     293        18592 :                     { u"mode"_s,   DsoService::toString(metadata.mode) },
     294        47600 :                 }).toJson().toStdString();
     295        10752 :             break;
     296        10752 :         case OutputFormat::Text:
     297        23296 :             std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
     298        10752 :             break;
     299         8736 :         }
     300        32256 :         --samplesToGo;
     301         8736 :     }
     302         6912 :     if (samplesToGo <= 0) {
     303        15480 :         qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
     304        11304 :             nullptr, metadata.numberOfSamples).arg(samplesToGo);
     305         6912 :         if (device) disconnect(); // Will exit the application once disconnected.
     306         1872 :     }
     307        31328 : }
        

Generated by: LCOV version 2.4-0