LCOV - code coverage report
Current view: top level - src/cli - dsocommand.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 82.1 % 201 165
Version: Functions: 70.0 % 10 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 "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         9632 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
      26         2336 : {
      27              : 
      28         9696 : }
      29              : 
      30         4720 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
      31         1512 : {
      32        22752 :     return DeviceCommand::requiredOptions(parser) + QStringList{
      33         1512 :         u"mode"_s,
      34         1512 :         u"range"_s,
      35        18681 :     };
      36         1512 : }
      37              : 
      38         2320 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
      39          728 : {
      40        15460 :     return DeviceCommand::supportedOptions(parser) + QStringList{
      41          728 :         u"interval"_s,
      42          728 :         u"samples"_s,
      43          728 :         u"trigger-level"_s,
      44          728 :         u"trigger-mode"_s,
      45        13807 :     };
      46          728 : }
      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         2240 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
      55          672 : {
      56         2912 :     QStringList errors = DeviceCommand::processOptions(parser);
      57         2912 :     if (!errors.isEmpty()) {
      58           72 :         return errors;
      59           72 :     }
      60              : 
      61              :     // Parse the (required) mode option.
      62         4600 :     if (const QString mode = parser.value(u"mode"_s).trimmed().toLower();
      63         5675 :         mode.startsWith(u"ac v"_s) || mode.startsWith(u"vac"_s)) {
      64          104 :         settings.mode = DsoService::Mode::AcVoltage;
      65         5448 :     } else if (mode.startsWith(u"dc v"_s) || mode.startsWith(u"vdc"_s)) {
      66         2184 :         settings.mode = DsoService::Mode::DcVoltage;
      67         1113 :     } else if (mode.startsWith(u"ac c"_s) || mode.startsWith(u"aac"_s)) {
      68          104 :         settings.mode = DsoService::Mode::AcCurrent;
      69          454 :     } else if (mode.startsWith(u"dc c"_s) || mode.startsWith(u"adc"_s)) {
      70          104 :         settings.mode = DsoService::Mode::DcCurrent;
      71           24 :     } else {
      72          197 :         errors.append(tr("Unknown DSO mode: %1").arg(parser.value(u"mode"_s)));
      73           24 :         return errors;
      74          949 :     }
      75              : 
      76              :     // Parse the (required) range option.
      77         1608 :     QString unit;
      78          576 :     {
      79         2496 :         const QString value = parser.value(u"range"_s);
      80          576 :         quint32 sensibleMinimum = 0;
      81         2496 :         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         2264 :         case DsoService::Mode::DcVoltage:
      86          528 :         case DsoService::Mode::AcVoltage:
      87         2288 :             minRangeFunc = minVoltageRange;
      88         2288 :             unit = u"V"_s;
      89          528 :             sensibleMinimum = 50; // mV.
      90         2288 :             break;
      91          184 :         case DsoService::Mode::DcCurrent:
      92           48 :         case DsoService::Mode::AcCurrent:
      93          208 :             minRangeFunc = minCurrentRange;
      94          208 :             unit = u"A"_s;
      95           48 :             sensibleMinimum = 5; // mA.
      96          208 :             break;
      97          576 :         }
      98          576 :         Q_ASSERT(!unit.isEmpty());
      99         2496 :         rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
     100         2496 :         if (rangeOptionValue == 0) {
     101          147 :             errors.append(tr("Invalid range value: %1").arg(value));
     102           24 :         }
     103         1464 :     }
     104              : 
     105              :     // Parse the trigger-level option.
     106         3528 :     if (parser.isSet(u"trigger-level"_s)) {
     107          832 :         const QString value = parser.value(u"trigger-level"_s);
     108          832 :         const quint32 level = parseNumber<std::micro>(value, unit);
     109          832 :         if (level == 0) {
     110          147 :             errors.append(tr("Invalid trigger-level value: %1").arg(value));
     111          168 :         } else {
     112          728 :             settings.triggerLevel = (float)(level/1'000'000.0);
     113          168 :         }
     114          488 :     }
     115              : 
     116              :     // Parse the trigger-mode option.
     117         3528 :     if (parser.isSet(u"trigger-mode"_s)) {
     118         1472 :         const QString triggerMode = parser.value(u"trigger-mode"_s).trimmed().toLower();
     119         1176 :         if (triggerMode.startsWith(u"free"_s)) {
     120          312 :             settings.command = DsoService::Command::FreeRunning;
     121          735 :         } else if (triggerMode.startsWith(u"ris"_s)) {
     122          208 :            settings.command = DsoService::Command::RisingEdgeTrigger;
     123          441 :         } else if (triggerMode.startsWith(u"fall"_s)) {
     124          208 :             settings.command = DsoService::Command::FallingEdgeTrigger;
     125           48 :         } else {
     126          197 :             errors.append(tr("Unknown trigger mode: %1").arg(parser.value(u"trigger-mode"_s)));
     127           24 :         }
     128          488 :     }
     129              : 
     130              :     // Ensure that if either trigger option is present, then both are.
     131         5448 :     if (parser.isSet(u"trigger-level"_s) != parser.isSet(u"trigger-mode"_s)) {
     132          208 :         errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
     133           48 :     }
     134              : 
     135              :     // Parse the interval option.
     136         3528 :     if (parser.isSet(u"interval"_s)) {
     137          780 :         const QString value = parser.value(u"interval"_s);
     138          520 :         const quint32 interval = parseNumber<std::micro>(value, u"s"_s, 500'000);
     139          520 :         if (interval == 0) {
     140          294 :             errors.append(tr("Invalid interval value: %1").arg(value));
     141           72 :         } else {
     142          312 :             settings.samplingWindow = interval;
     143           72 :         }
     144          305 :     }
     145              : 
     146              :     // Parse the samples option.
     147         3528 :     if (parser.isSet(u"samples"_s)) {
     148          780 :         const QString value = parser.value(u"samples"_s);
     149          520 :         const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
     150          520 :         if (samples == 0) {
     151          294 :             errors.append(tr("Invalid samples value: %1").arg(value));
     152          312 :         } else if (samples > std::numeric_limits<quint16>::max()) {
     153          113 :             errors.append(tr("Samples value (%1) must be no greater than %2")
     154          217 :                 .arg(value).arg(std::numeric_limits<quint16>::max()));
     155           48 :         } else {
     156          208 :             if (samples > 8192) {
     157          256 :                 qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
     158           24 :             }
     159          208 :             settings.numberOfSamples = (quint16)samples;
     160           48 :         }
     161          305 :     }
     162          576 :     return errors;
     163         1464 : }
     164              : 
     165              : /*!
     166              :  * \copybrief DeviceCommand::getService
     167              :  *
     168              :  * This override returns a pointer to a DsoService object.
     169              :  */
     170            0 : AbstractPokitService * DsoCommand::getService()
     171            0 : {
     172            0 :     Q_ASSERT(device);
     173            0 :     if (!service) {
     174            0 :         service = device->dso();
     175            0 :         Q_ASSERT(service);
     176            0 :         connect(service, &DsoService::settingsWritten,
     177            0 :                 this, &DsoCommand::settingsWritten);
     178            0 :     }
     179            0 :     return service;
     180            0 : }
     181              : 
     182              : /*!
     183              :  * \copybrief DeviceCommand::serviceDetailsDiscovered
     184              :  *
     185              :  * This override fetches the current device's status, and outputs it in the selected format.
     186              :  */
     187            0 : void DsoCommand::serviceDetailsDiscovered()
     188            0 : {
     189            0 :     DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
     190            0 :     settings.range = (minRangeFunc == nullptr) ? 0 : minRangeFunc(*service->pokitProduct(), rangeOptionValue);
     191            0 :     const QString range = service->toString(settings.range, settings.mode);
     192            0 :     qCInfo(lc).noquote() << tr("Sampling %1, with range %2, %Ln sample/s over %L3us", nullptr, settings.numberOfSamples)
     193            0 :         .arg(DsoService::toString(settings.mode), (range.isNull()) ? QString::fromLatin1("N/A") : range)
     194            0 :         .arg(settings.samplingWindow);
     195            0 :     service->setSettings(settings);
     196            0 : }
     197              : 
     198              : /*!
     199              :  * \var DsoCommand::minRangeFunc
     200              :  *
     201              :  * Pointer to function for converting #rangeOptionValue to a Pokit device's range enumerator. This function pointer
     202              :  * is assigned during the command line parsing, but is not invoked until after the device's services are discovered,
     203              :  * because prior to that discovery, we don't know which product (Meter vs Pro vs Clamp, etc) we're talking to and thus
     204              :  * which enumerator list to be using.
     205              :  *
     206              :  * If the current mode does not support ranges (eg diode, and continuity modes), then this member will be \c nullptr.
     207              :  *
     208              :  * \see processOptions
     209              :  * \see serviceDetailsDiscovered
     210              :  */
     211              : 
     212              : /*!
     213              :  * Invoked when the DSO settings have been written.
     214              :  */
     215            0 : void DsoCommand::settingsWritten()
     216            0 : {
     217            0 :     Q_ASSERT(service);
     218            0 :     qCDebug(lc).noquote() << tr("Settings written; DSO has started.");
     219            0 :     connect(service, &DsoService::metadataRead, this, &DsoCommand::metadataRead);
     220            0 :     connect(service, &DsoService::samplesRead, this, &DsoCommand::outputSamples);
     221            0 :     service->enableMetadataNotifications();
     222            0 :     service->enableReadingNotifications();
     223            0 : }
     224              : 
     225              : /*!
     226              :  * Invoked when \a metadata has been received from the DSO.
     227              :  */
     228         4880 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
     229         1496 : {
     230         8145 :     qCDebug(lc) << "status:" << (int)(data.status);
     231         8145 :     qCDebug(lc) << "scale:" << data.scale;
     232         8145 :     qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
     233         8145 :     qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
     234         8145 :     qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
     235         8145 :     qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
     236         8145 :     qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
     237         6376 :     this->metadata = data;
     238         6376 :     this->samplesToGo = data.numberOfSamples;
     239         6376 : }
     240              : 
     241              : /*!
     242              :  * Outputs DSO \a samples in the selected output format.
     243              :  */
     244         5760 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
     245         1728 : {
     246         4824 :     QString unit;
     247         7488 :     switch (metadata.mode) {
     248         2646 :     case DsoService::Mode::DcVoltage: unit = u"Vdc"_s; break;
     249         2646 :     case DsoService::Mode::AcVoltage: unit = u"Vac"_s; break;
     250         2646 :     case DsoService::Mode::DcCurrent: unit = u"Adc"_s; break;
     251         2646 :     case DsoService::Mode::AcCurrent: unit = u"Aac"_s; break;
     252            0 :     default:
     253            0 :         qCDebug(lc).noquote() << tr(R"(No known unit for mode %1 "%2".)").arg((int)metadata.mode)
     254            0 :             .arg(DsoService::toString(metadata.mode));
     255         1728 :     }
     256        10152 :     const QString range = service->toString(metadata.range, metadata.mode);
     257              : 
     258        40704 :     for (const qint16 &sample: samples) {
     259        34944 :         static int sampleNumber = 0; ++sampleNumber;
     260        34944 :         const float value = sample * metadata.scale;
     261        34944 :         switch (format) {
     262         2688 :         case OutputFormat::Csv:
     263        13312 :             for (; showCsvHeader; showCsvHeader = false) {
     264         2352 :                 std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
     265          384 :             }
     266        24528 :             std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
     267         2688 :                 .arg(sampleNumber).arg(value).arg(unit, range));
     268        11648 :             break;
     269        11648 :         case OutputFormat::Json:
     270        60144 :             std::cout << QJsonDocument(QJsonObject{
     271        14784 :                     { u"value"_s,  value },
     272        14784 :                     { u"unit"_s,   unit },
     273        14784 :                     { u"range"_s,  range },
     274        20608 :                     { u"mode"_s,   DsoService::toString(metadata.mode) },
     275        57008 :                 }).toJson().toStdString();
     276        11648 :             break;
     277        11648 :         case OutputFormat::Text:
     278        24976 :             std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
     279        11648 :             break;
     280         8064 :         }
     281        34944 :         --samplesToGo;
     282         8064 :     }
     283         7488 :     if (samplesToGo <= 0) {
     284        17352 :         qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
     285        12600 :             nullptr, metadata.numberOfSamples).arg(samplesToGo);
     286         7488 :         if (device) disconnect(); // Will exit the application once disconnected.
     287         1728 :     }
     288        38848 : }
        

Generated by: LCOV version 2.3.1-1