LCOV - code coverage report
Current view: top level - src/cli - dsocommand.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 82.3 % 203 167
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              : 
       6              : #include <qtpokit/pokitdevice.h>
       7              : 
       8              : #include <QJsonDocument>
       9              : #include <QJsonObject>
      10              : 
      11              : #include <iostream>
      12              : 
      13              : /*!
      14              :  * \class DsoCommand
      15              :  *
      16              :  * The DsoCommand class implements the `dso` CLI command.
      17              :  */
      18              : 
      19              : /*!
      20              :  * Construct a new DsoCommand object with \a parent.
      21              :  */
      22         6164 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
      23         2144 : {
      24              : 
      25         6284 : }
      26              : 
      27         2655 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
      28         1388 : {
      29        13778 :     return DeviceCommand::requiredOptions(parser) + QStringList{
      30         1388 :         QLatin1String("mode"),
      31         1388 :         QLatin1String("range"),
      32        10592 :     };
      33         2804 : }
      34              : 
      35         1305 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
      36          668 : {
      37         9136 :     return DeviceCommand::supportedOptions(parser) + QStringList{
      38          668 :         QLatin1String("interval"),
      39          668 :         QLatin1String("samples"),
      40          668 :         QLatin1String("trigger-level"),
      41          668 :         QLatin1String("trigger-mode"),
      42         7802 :     };
      43         1364 : }
      44              : 
      45              : /*!
      46              :  * \copybrief DeviceCommand::processOptions
      47              :  *
      48              :  * This implementation extends DeviceCommand::processOptions to process additional CLI options
      49              :  * supported (or required) by this command.
      50              :  */
      51         1260 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
      52          616 : {
      53         1876 :     QStringList errors = DeviceCommand::processOptions(parser);
      54         1876 :     if (!errors.isEmpty()) {
      55           66 :         return errors;
      56           66 :     }
      57              : 
      58              :     // Parse the (required) mode option.
      59         2800 :     if (const QString mode = parser.value(QLatin1String("mode")).trimmed().toLower();
      60         1900 :         mode.startsWith(QLatin1String("ac v")) || mode.startsWith(QLatin1String("vac"))) {
      61           67 :         settings.mode = DsoService::Mode::AcVoltage;
      62         1824 :     } else if (mode.startsWith(QLatin1String("dc v")) || mode.startsWith(QLatin1String("vdc"))) {
      63         1407 :         settings.mode = DsoService::Mode::DcVoltage;
      64          624 :     } else if (mode.startsWith(QLatin1String("ac c")) || mode.startsWith(QLatin1String("aac"))) {
      65           67 :         settings.mode = DsoService::Mode::AcCurrent;
      66          152 :     } else if (mode.startsWith(QLatin1String("dc c")) || mode.startsWith(QLatin1String("adc"))) {
      67           67 :         settings.mode = DsoService::Mode::DcCurrent;
      68           22 :     } else {
      69          118 :         errors.append(tr("Unknown DSO mode: %1").arg(parser.value(QLatin1String("mode"))));
      70           22 :         return errors;
      71          622 :     }
      72              : 
      73              :     // Parse the (required) range option.
      74         1032 :     QString unit;
      75          528 :     {
      76         1608 :         const QString value = parser.value(QLatin1String("range"));
      77          528 :         quint32 sensibleMinimum = 0;
      78         1608 :         switch (settings.mode) {
      79            0 :         case DsoService::Mode::Idle:
      80            0 :             Q_ASSERT(false); // Not possible, since the mode parsing above never allows Idle.
      81            0 :             break;
      82         1452 :         case DsoService::Mode::DcVoltage:
      83          484 :         case DsoService::Mode::AcVoltage:
      84         1474 :             minRangeFunc = minVoltageRange;
      85         1474 :             unit = QLatin1String("V");
      86          484 :             sensibleMinimum = 50; // mV.
      87         1474 :             break;
      88          112 :         case DsoService::Mode::DcCurrent:
      89           44 :         case DsoService::Mode::AcCurrent:
      90          134 :             minRangeFunc = minCurrentRange;
      91          134 :             unit = QLatin1String("A");
      92           44 :             sensibleMinimum = 5; // mA.
      93          134 :             break;
      94          528 :         }
      95          528 :         Q_ASSERT(!unit.isEmpty());
      96         1608 :         rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
      97         1608 :         if (rangeOptionValue == 0) {
      98           88 :             errors.append(tr("Invalid range value: %1").arg(value));
      99           22 :         }
     100         1104 :     }
     101              : 
     102              :     // Parse the trigger-level option.
     103         2112 :     if (parser.isSet(QLatin1String("trigger-level"))) {
     104          536 :         const QString value = parser.value(QLatin1String("trigger-level"));
     105          536 :         const quint32 level = parseNumber<std::micro>(value, unit);
     106          536 :         if (level == 0) {
     107           88 :             errors.append(tr("Invalid trigger-level value: %1").arg(value));
     108          154 :         } else {
     109          469 :             settings.triggerLevel = (float)(level/1'000'000.0);
     110          154 :         }
     111          368 :     }
     112              : 
     113              :     // Parse the trigger-mode option.
     114         2112 :     if (parser.isSet(QLatin1String("trigger-mode"))) {
     115          896 :         const QString triggerMode = parser.value(QLatin1String("trigger-mode")).trimmed().toLower();
     116          536 :         if (triggerMode.startsWith(QLatin1String("free"))) {
     117          201 :             settings.command = DsoService::Command::FreeRunning;
     118          335 :         } else if (triggerMode.startsWith(QLatin1String("ris"))) {
     119          134 :            settings.command = DsoService::Command::RisingEdgeTrigger;
     120          201 :         } else if (triggerMode.startsWith(QLatin1String("fall"))) {
     121          134 :             settings.command = DsoService::Command::FallingEdgeTrigger;
     122           44 :         } else {
     123          139 :             errors.append(tr("Unknown trigger mode: %1").arg(
     124          112 :                 parser.value(QLatin1String("trigger-mode"))));
     125           22 :         }
     126          368 :     }
     127              : 
     128              :     // Ensure that if either trigger option is present, then both are.
     129         2688 :     if (parser.isSet(QLatin1String("trigger-level")) !=
     130         2112 :         parser.isSet(QLatin1String("trigger-mode"))) {
     131          134 :         errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
     132           44 :     }
     133              : 
     134              :     // Parse the interval option.
     135         2112 :     if (parser.isSet(QLatin1String("interval"))) {
     136          440 :         const QString value = parser.value(QLatin1String("interval"));
     137          335 :         const quint32 interval = parseNumber<std::micro>(value, QLatin1String("s"), 500'000);
     138          335 :         if (interval == 0) {
     139          176 :             errors.append(tr("Invalid interval value: %1").arg(value));
     140           66 :         } else {
     141          201 :             settings.samplingWindow = interval;
     142           66 :         }
     143          230 :     }
     144              : 
     145              :     // Parse the samples option.
     146         2112 :     if (parser.isSet(QLatin1String("samples"))) {
     147          440 :         const QString value = parser.value(QLatin1String("samples"));
     148          335 :         const quint32 samples = parseNumber<std::ratio<1>>(value, QLatin1String("S"));
     149          335 :         if (samples == 0) {
     150          176 :             errors.append(tr("Invalid samples value: %1").arg(value));
     151          201 :         } else if (samples > std::numeric_limits<quint16>::max()) {
     152           67 :             errors.append(tr("Samples value (%1) must be no greater than %2")
     153          130 :                 .arg(value).arg(std::numeric_limits<quint16>::max()));
     154           44 :         } else {
     155          134 :             if (samples > 8192) {
     156          146 :                 qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
     157           22 :             }
     158          134 :             settings.numberOfSamples = (quint16)samples;
     159           44 :         }
     160          230 :     }
     161          528 :     return errors;
     162         1104 : }
     163              : 
     164              : /*!
     165              :  * \copybrief DeviceCommand::getService
     166              :  *
     167              :  * This override returns a pointer to a DsoService object.
     168              :  */
     169            0 : AbstractPokitService * DsoCommand::getService()
     170            0 : {
     171            0 :     Q_ASSERT(device);
     172            0 :     if (!service) {
     173            0 :         service = device->dso();
     174            0 :         Q_ASSERT(service);
     175            0 :         connect(service, &DsoService::settingsWritten,
     176            0 :                 this, &DsoCommand::settingsWritten);
     177            0 :     }
     178            0 :     return service;
     179            0 : }
     180              : 
     181              : /*!
     182              :  * \copybrief DeviceCommand::serviceDetailsDiscovered
     183              :  *
     184              :  * This override fetches the current device's status, and outputs it in the selected format.
     185              :  */
     186            0 : void DsoCommand::serviceDetailsDiscovered()
     187            0 : {
     188            0 :     DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
     189            0 :     settings.range = (minRangeFunc == nullptr) ? 0 : minRangeFunc(*service->pokitProduct(), rangeOptionValue);
     190            0 :     const QString range = service->toString(settings.range, settings.mode);
     191            0 :     qCInfo(lc).noquote() << tr("Sampling %1, with range %2, %Ln sample/s over %L3us", nullptr, settings.numberOfSamples)
     192            0 :         .arg(DsoService::toString(settings.mode), (range.isNull()) ? QString::fromLatin1("N/A") : range)
     193            0 :         .arg(settings.samplingWindow);
     194            0 :     service->setSettings(settings);
     195            0 : }
     196              : 
     197              : /*!
     198              :  * \var DsoCommand::minRangeFunc
     199              :  *
     200              :  * Pointer to function for converting #rangeOptionValue to a Pokit device's range enumerator. This function pointer
     201              :  * is assigned during the command line parsing, but is not invoked until after the device's services are discovere,
     202              :  * because prior to that discovery, we don't know which product (Meter vs Pro vs Clamp, etc) we're talking to and thus
     203              :  * which enumerator list to be using.
     204              :  *
     205              :  * If the current mode does not support ranges (eg diode, and continuity modes), then this member will be \c nullptr.
     206              :  *
     207              :  * \see processOptions
     208              :  * \see serviceDetailsDiscovered
     209              :  */
     210              : 
     211              : /*!
     212              :  * Invoked when the DSO settings have been written.
     213              :  */
     214            0 : void DsoCommand::settingsWritten()
     215            0 : {
     216            0 :     Q_ASSERT(service);
     217            0 :     qCDebug(lc).noquote() << tr("Settings written; DSO has started.");
     218            0 :     connect(service, &DsoService::metadataRead, this, &DsoCommand::metadataRead);
     219            0 :     connect(service, &DsoService::samplesRead, this, &DsoCommand::outputSamples);
     220            0 :     service->enableMetadataNotifications();
     221            0 :     service->enableReadingNotifications();
     222            0 : }
     223              : 
     224              : /*!
     225              :  * Invoked when \a metadata has been received from the DSO.
     226              :  */
     227         2745 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
     228         1372 : {
     229         4910 :     qCDebug(lc) << "status:" << (int)(data.status);
     230         4910 :     qCDebug(lc) << "scale:" << data.scale;
     231         4910 :     qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
     232         4910 :     qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
     233         4910 :     qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
     234         4910 :     qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
     235         4910 :     qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
     236         4117 :     this->metadata = data;
     237         4117 :     this->samplesToGo = data.numberOfSamples;
     238         4117 : }
     239              : 
     240              : /*!
     241              :  * Outputs DSO \a samples in the selected ouput format.
     242              :  */
     243         3240 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
     244         1584 : {
     245         3096 :     QString unit;
     246         4824 :     switch (metadata.mode) {
     247         1206 :     case DsoService::Mode::DcVoltage: unit = QLatin1String("Vdc"); break;
     248         1206 :     case DsoService::Mode::AcVoltage: unit = QLatin1String("Vac"); break;
     249         1206 :     case DsoService::Mode::DcCurrent: unit = QLatin1String("Adc"); break;
     250         1206 :     case DsoService::Mode::AcCurrent: unit = QLatin1String("Aac"); break;
     251            0 :     default:
     252            0 :         qCDebug(lc).noquote() << tr(R"(No known unit for mode %1 "%2".)").arg((int)metadata.mode)
     253            0 :             .arg(DsoService::toString(metadata.mode));
     254         1584 :     }
     255         6552 :     const QString range = service->toString(metadata.range, metadata.mode);
     256              : 
     257        25752 :     for (const qint16 &sample: samples) {
     258        22512 :         static int sampleNumber = 0; ++sampleNumber;
     259        22512 :         const float value = sample * metadata.scale;
     260        22512 :         switch (format) {
     261         2464 :         case OutputFormat::Csv:
     262         8576 :             for (; showCsvHeader; showCsvHeader = false) {
     263         1408 :                 std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
     264          352 :             }
     265        14560 :             std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
     266         2464 :                 .arg(sampleNumber).arg(value).arg(unit, range));
     267         7504 :             break;
     268         7504 :         case OutputFormat::Json:
     269        30464 :             std::cout << QJsonDocument(QJsonObject{
     270         4816 :                     { QLatin1String("value"),  value },
     271         4816 :                     { QLatin1String("unit"),   unit },
     272         4816 :                     { QLatin1String("range"),  range },
     273        10192 :                     { QLatin1String("mode"),   DsoService::toString(metadata.mode) },
     274        33040 :                 }).toJson().toStdString();
     275         7504 :             break;
     276         7504 :         case OutputFormat::Text:
     277        14560 :             std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
     278         7504 :             break;
     279         7392 :         }
     280        22512 :         --samplesToGo;
     281         7392 :     }
     282         4824 :     if (samplesToGo <= 0) {
     283        10728 :         qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
     284         8064 :             nullptr, metadata.numberOfSamples).arg(samplesToGo);
     285         4824 :         if (device) disconnect(); // Will exit the application once disconnected.
     286         1584 :     }
     287        26776 : }
        

Generated by: LCOV version 2.3-1