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 "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 1840 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
23 : {
24 :
25 1840 : }
26 :
27 1180 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
28 : {
29 5074 : return DeviceCommand::requiredOptions(parser) + QStringList{
30 : QLatin1String("mode"),
31 : QLatin1String("range"),
32 4602 : };
33 0 : }
34 :
35 580 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
36 : {
37 3654 : return DeviceCommand::supportedOptions(parser) + QStringList{
38 : QLatin1String("interval"),
39 : QLatin1String("samples"),
40 : QLatin1String("trigger-level"),
41 : QLatin1String("trigger-mode"),
42 3422 : };
43 0 : }
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 560 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
52 : {
53 560 : QStringList errors = DeviceCommand::processOptions(parser);
54 560 : if (!errors.isEmpty()) {
55 : return errors;
56 : }
57 :
58 : // Parse the (required) mode option.
59 1000 : const QString mode = parser.value(QLatin1String("mode")).trimmed().toLower();
60 500 : if (mode.startsWith(QLatin1String("ac v")) || mode.startsWith(QLatin1String("vac"))) {
61 20 : settings.mode = DsoService::Mode::AcVoltage;
62 480 : } else if (mode.startsWith(QLatin1String("dc v")) || mode.startsWith(QLatin1String("vdc"))) {
63 420 : settings.mode = DsoService::Mode::DcVoltage;
64 60 : } else if (mode.startsWith(QLatin1String("ac c")) || mode.startsWith(QLatin1String("aac"))) {
65 20 : settings.mode = DsoService::Mode::AcCurrent;
66 40 : } else if (mode.startsWith(QLatin1String("dc c")) || mode.startsWith(QLatin1String("adc"))) {
67 20 : settings.mode = DsoService::Mode::DcCurrent;
68 : } else {
69 40 : errors.append(tr("Unknown DSO mode: %1").arg(parser.value(QLatin1String("mode"))));
70 20 : return errors;
71 : }
72 :
73 : // Parse the (required) range option.
74 144 : QString unit;
75 : {
76 480 : const QString value = parser.value(QLatin1String("range"));
77 : quint32 sensibleMinimum = 0;
78 480 : switch (settings.mode) {
79 : case DsoService::Mode::Idle:
80 : Q_ASSERT(false); // Not possible, since the mode parsing above never allows Idle.
81 : break;
82 440 : case DsoService::Mode::DcVoltage:
83 : case DsoService::Mode::AcVoltage:
84 440 : minRangeFunc = minVoltageRange;
85 440 : unit = QLatin1String("V");
86 : sensibleMinimum = 50; // mV.
87 440 : break;
88 40 : case DsoService::Mode::DcCurrent:
89 : case DsoService::Mode::AcCurrent:
90 40 : minRangeFunc = minCurrentRange;
91 40 : unit = QLatin1String("A");
92 : sensibleMinimum = 5; // mA.
93 40 : break;
94 : }
95 : Q_ASSERT(!unit.isEmpty());
96 480 : rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
97 480 : if (rangeOptionValue == 0) {
98 26 : errors.append(tr("Invalid range value: %1").arg(value));
99 : }
100 336 : }
101 :
102 : // Parse the trigger-level option.
103 624 : if (parser.isSet(QLatin1String("trigger-level"))) {
104 160 : const QString value = parser.value(QLatin1String("trigger-level"));
105 160 : const quint32 level = parseNumber<std::micro>(value, unit);
106 160 : if (level == 0) {
107 26 : errors.append(tr("Invalid trigger-level value: %1").arg(value));
108 : } else {
109 140 : settings.triggerLevel = (float)(level/1'000'000.0);
110 : }
111 112 : }
112 :
113 : // Parse the trigger-mode option.
114 624 : if (parser.isSet(QLatin1String("trigger-mode"))) {
115 320 : const QString triggerMode = parser.value(QLatin1String("trigger-mode")).trimmed().toLower();
116 160 : if (triggerMode.startsWith(QLatin1String("free"))) {
117 60 : settings.command = DsoService::Command::FreeRunning;
118 100 : } else if (triggerMode.startsWith(QLatin1String("ris"))) {
119 40 : settings.command = DsoService::Command::RisingEdgeTrigger;
120 60 : } else if (triggerMode.startsWith(QLatin1String("fall"))) {
121 40 : settings.command = DsoService::Command::FallingEdgeTrigger;
122 : } else {
123 60 : errors.append(tr("Unknown trigger mode: %1").arg(
124 40 : parser.value(QLatin1String("trigger-mode"))));
125 : }
126 112 : }
127 :
128 : // Ensure that if either trigger option is present, then both are.
129 960 : if (parser.isSet(QLatin1String("trigger-level")) !=
130 624 : parser.isSet(QLatin1String("trigger-mode"))) {
131 40 : errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
132 : }
133 :
134 : // Parse the interval option.
135 624 : if (parser.isSet(QLatin1String("interval"))) {
136 170 : const QString value = parser.value(QLatin1String("interval"));
137 100 : const quint32 interval = parseNumber<std::micro>(value, QLatin1String("s"), 500'000);
138 100 : if (interval == 0) {
139 52 : errors.append(tr("Invalid interval value: %1").arg(value));
140 : } else {
141 60 : settings.samplingWindow = interval;
142 : }
143 70 : }
144 :
145 : // Parse the samples option.
146 624 : if (parser.isSet(QLatin1String("samples"))) {
147 170 : const QString value = parser.value(QLatin1String("samples"));
148 100 : const quint32 samples = parseNumber<std::ratio<1>>(value, QLatin1String("S"));
149 100 : if (samples == 0) {
150 52 : errors.append(tr("Invalid samples value: %1").arg(value));
151 60 : } else if (samples > std::numeric_limits<quint16>::max()) {
152 28 : errors.append(tr("Samples value (%1) must be no greater than %2")
153 46 : .arg(value).arg(std::numeric_limits<quint16>::max()));
154 : } else {
155 40 : if (samples > 8192) {
156 48 : qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
157 : }
158 40 : settings.numberOfSamples = (quint16)samples;
159 : }
160 70 : }
161 : return errors;
162 350 : }
163 :
164 : /*!
165 : * \copybrief DeviceCommand::getService
166 : *
167 : * This override returns a pointer to a DsoService object.
168 : */
169 0 : AbstractPokitService * DsoCommand::getService()
170 : {
171 : Q_ASSERT(device);
172 0 : if (!service) {
173 0 : service = device->dso();
174 : Q_ASSERT(service);
175 0 : connect(service, &DsoService::settingsWritten,
176 0 : this, &DsoCommand::settingsWritten);
177 : }
178 0 : return service;
179 : }
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 : {
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 : {
216 : 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 1220 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
228 : {
229 1342 : qCDebug(lc) << "status:" << (int)(data.status);
230 1342 : qCDebug(lc) << "scale:" << data.scale;
231 1342 : qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
232 1342 : qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
233 1342 : qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
234 1342 : qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
235 1342 : qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
236 1220 : this->metadata = data;
237 1220 : this->samplesToGo = data.numberOfSamples;
238 1220 : }
239 :
240 : /*!
241 : * Outputs DSO \a samples in the selected ouput format.
242 : */
243 1440 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
244 : {
245 432 : QString unit;
246 1440 : switch (metadata.mode) {
247 360 : case DsoService::Mode::DcVoltage: unit = QLatin1String("Vdc"); break;
248 360 : case DsoService::Mode::AcVoltage: unit = QLatin1String("Vac"); break;
249 360 : case DsoService::Mode::DcCurrent: unit = QLatin1String("Adc"); break;
250 360 : 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 : }
255 2448 : const QString range = service->toString(metadata.range, metadata.mode);
256 :
257 8160 : for (const qint16 &sample: samples) {
258 6720 : static int sampleNumber = 0; ++sampleNumber;
259 6720 : const float value = sample * metadata.scale;
260 6720 : switch (format) {
261 : case OutputFormat::Csv:
262 2560 : for (; showCsvHeader; showCsvHeader = false) {
263 416 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
264 : }
265 5152 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
266 : .arg(sampleNumber).arg(value).arg(unit, range));
267 2240 : break;
268 672 : case OutputFormat::Json:
269 17696 : std::cout << QJsonDocument(QJsonObject{
270 672 : { QLatin1String("value"), value },
271 672 : { QLatin1String("unit"), unit },
272 672 : { QLatin1String("range"), range },
273 3808 : { QLatin1String("mode"), DsoService::toString(metadata.mode) },
274 13440 : }).toJson().toStdString();
275 2240 : break;
276 2240 : case OutputFormat::Text:
277 5152 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
278 2240 : break;
279 : }
280 6720 : --samplesToGo;
281 : }
282 1440 : if (samplesToGo <= 0) {
283 3456 : qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
284 2304 : nullptr, metadata.numberOfSamples).arg(samplesToGo);
285 1440 : if (device) disconnect(); // Will exit the application once disconnected.
286 : }
287 5472 : }
|