Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2023 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 1530 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent),
23 1530 : service(nullptr), settings{
24 : DsoService::Command::FreeRunning, 0.0f, DsoService::Mode::DcVoltage,
25 1530 : DsoService::VoltageRange::_30V_to_60V, 1000*1000, 1000}, showCsvHeader(true)
26 : {
27 :
28 1530 : }
29 :
30 935 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
31 : {
32 3740 : return DeviceCommand::requiredOptions(parser) + QStringList{
33 : QLatin1String("mode"),
34 : QLatin1String("range"),
35 3740 : };
36 : }
37 :
38 459 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
39 : {
40 2754 : return DeviceCommand::supportedOptions(parser) + QStringList{
41 : QLatin1String("interval"),
42 : QLatin1String("samples"),
43 : QLatin1String("trigger-level"),
44 : QLatin1String("trigger-mode"),
45 2754 : };
46 : }
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 442 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
55 : {
56 442 : QStringList errors = DeviceCommand::processOptions(parser);
57 442 : if (!errors.isEmpty()) {
58 : return errors;
59 : }
60 :
61 : // Parse the (required) mode option.
62 782 : const QString mode = parser.value(QLatin1String("mode")).trimmed().toLower();
63 391 : if (mode.startsWith(QLatin1String("ac v")) || mode.startsWith(QLatin1String("vac"))) {
64 17 : settings.mode = DsoService::Mode::AcVoltage;
65 374 : } else if (mode.startsWith(QLatin1String("dc v")) || mode.startsWith(QLatin1String("vdc"))) {
66 323 : settings.mode = DsoService::Mode::DcVoltage;
67 51 : } else if (mode.startsWith(QLatin1String("ac c")) || mode.startsWith(QLatin1String("aac"))) {
68 17 : settings.mode = DsoService::Mode::AcCurrent;
69 34 : } else if (mode.startsWith(QLatin1String("dc c")) || mode.startsWith(QLatin1String("adc"))) {
70 17 : settings.mode = DsoService::Mode::DcCurrent;
71 : } else {
72 34 : errors.append(tr("Unknown DSO mode: %1").arg(parser.value(QLatin1String("mode"))));
73 17 : return errors;
74 : }
75 :
76 : // Parse the (required) range option.
77 88 : QString unit;
78 : {
79 374 : const QString value = parser.value(QLatin1String("range"));
80 : quint32 sensibleMinimum = 0;
81 374 : switch (settings.mode) {
82 : case DsoService::Mode::Idle:
83 : Q_ASSERT(false); // Not possible, since the mode parsing above never allows Idle.
84 : break;
85 : case DsoService::Mode::DcVoltage:
86 : case DsoService::Mode::AcVoltage:
87 340 : unit = QLatin1String("V");
88 : sensibleMinimum = 50; // mV.
89 340 : break;
90 : case DsoService::Mode::DcCurrent:
91 : case DsoService::Mode::AcCurrent:
92 34 : unit = QLatin1String("A");
93 : sensibleMinimum = 5; // mA.
94 34 : break;
95 : }
96 : Q_ASSERT(!unit.isEmpty());
97 374 : const quint32 rangeMax = parseMilliValue(value, unit, sensibleMinimum);
98 374 : if (rangeMax == 0) {
99 20 : errors.append(tr("Invalid range value: %1").arg(value));
100 : } else {
101 357 : settings.range = lowestRange(settings.mode, rangeMax);
102 : }
103 308 : }
104 :
105 : // Parse the trigger-level option.
106 440 : if (parser.isSet(QLatin1String("trigger-level"))) {
107 136 : const QString value = parser.value(QLatin1String("trigger-level"));
108 136 : const quint32 level = parseMicroValue(value, unit);
109 136 : if (level == 0) {
110 20 : errors.append(tr("Invalid trigger-level value: %1").arg(value));
111 : } else {
112 119 : settings.triggerLevel = level/1000.0/1000.0;
113 : }
114 112 : }
115 :
116 : // Parse the trigger-mode option.
117 440 : if (parser.isSet(QLatin1String("trigger-mode"))) {
118 272 : const QString triggerMode = parser.value(QLatin1String("trigger-mode")).trimmed().toLower();
119 136 : if (triggerMode.startsWith(QLatin1String("free"))) {
120 51 : settings.command = DsoService::Command::FreeRunning;
121 85 : } else if (triggerMode.startsWith(QLatin1String("ris"))) {
122 34 : settings.command = DsoService::Command::RisingEdgeTrigger;
123 51 : } else if (triggerMode.startsWith(QLatin1String("fall"))) {
124 34 : settings.command = DsoService::Command::FallingEdgeTrigger;
125 : } else {
126 51 : errors.append(tr("Unknown trigger mode: %1").arg(
127 34 : parser.value(QLatin1String("trigger-mode"))));
128 : }
129 112 : }
130 :
131 : // Ensure that if either trigger option is present, then both are.
132 748 : if (parser.isSet(QLatin1String("trigger-level")) !=
133 440 : parser.isSet(QLatin1String("trigger-mode"))) {
134 34 : errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
135 : }
136 :
137 : // Parse the interval option.
138 440 : if (parser.isSet(QLatin1String("interval"))) {
139 170 : const QString value = parser.value(QLatin1String("interval"));
140 85 : const quint32 interval = parseMicroValue(value, QLatin1String("s"), 500*1000);
141 85 : if (interval == 0) {
142 40 : errors.append(tr("Invalid interval value: %1").arg(value));
143 : } else {
144 51 : settings.samplingWindow = interval;
145 : }
146 70 : }
147 :
148 : // Parse the samples option.
149 440 : if (parser.isSet(QLatin1String("samples"))) {
150 102 : const QString value = parser.value(QLatin1String("samples"));
151 51 : const quint32 samples = parseWholeValue(value, QLatin1String("S"));
152 51 : if (samples == 0) {
153 40 : errors.append(tr("Invalid samples value: %1").arg(value));
154 : } else {
155 17 : settings.numberOfSamples = samples;
156 : }
157 42 : }
158 : return errors;
159 322 : }
160 :
161 : /*!
162 : * \copybrief DeviceCommand::getService
163 : *
164 : * This override returns a pointer to a DsoService object.
165 : */
166 0 : AbstractPokitService * DsoCommand::getService()
167 : {
168 : Q_ASSERT(device);
169 0 : if (!service) {
170 0 : service = device->dso();
171 : Q_ASSERT(service);
172 0 : connect(service, &DsoService::settingsWritten,
173 : this, &DsoCommand::settingsWritten);
174 : }
175 0 : return service;
176 : }
177 :
178 : /*!
179 : * \copybrief DeviceCommand::serviceDetailsDiscovered
180 : *
181 : * This override fetches the current device's status, and outputs it in the selected format.
182 : */
183 0 : void DsoCommand::serviceDetailsDiscovered()
184 : {
185 0 : DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
186 0 : const QString range = DsoService::toString(settings.range, settings.mode);
187 0 : qCInfo(lc).noquote() << tr("Sampling %1, with range %2, %L3 samples over %L4us").arg(
188 0 : DsoService::toString(settings.mode), (range.isNull()) ? QString::fromLatin1("N/A") : range)
189 0 : .arg(settings.numberOfSamples).arg(settings.samplingWindow);
190 0 : service->setSettings(settings);
191 0 : }
192 :
193 : /*!
194 : * Returns the lowest \a mode range that can measure at least up to \a desired max, or AutoRange
195 : * if no such range is available.
196 : */
197 1411 : DsoService::Range DsoCommand::lowestRange(
198 : const DsoService::Mode mode, const quint32 desiredMax)
199 : {
200 1411 : switch (mode) {
201 0 : case DsoService::Mode::Idle:
202 0 : qCWarning(lc).noquote() << tr("Idle has no defined ranges.");
203 : Q_ASSERT(false); // Should never have been called with this Idle mode.
204 0 : break;
205 901 : case DsoService::Mode::DcVoltage:
206 : case DsoService::Mode::AcVoltage:
207 901 : return lowestVoltageRange(desiredMax);
208 510 : case DsoService::Mode::DcCurrent:
209 : case DsoService::Mode::AcCurrent:
210 510 : return lowestCurrentRange(desiredMax);
211 0 : default:
212 0 : qCWarning(lc).noquote() << tr("No defined ranges for mode %1.").arg((quint8)mode);
213 : Q_ASSERT(false); // Should never have been called with this invalid mode.
214 : }
215 0 : return DsoService::Range();
216 : }
217 :
218 : #define POKIT_APP_IF_LESS_THAN_RETURN(value, label) \
219 : if (value <= DsoService::maxValue(DsoService::label).toUInt()) { \
220 : return DsoService::label; \
221 : }
222 :
223 : /*!
224 : * Returns the lowest current range that can measure at least up to \a desired max, or AutoRange
225 : * if no such range is available.
226 : */
227 748 : DsoService::CurrentRange DsoCommand::lowestCurrentRange(const quint32 desiredMax)
228 : {
229 748 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_0_to_10mA)
230 595 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_10mA_to_30mA)
231 442 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_30mA_to_150mA)
232 272 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_150mA_to_300mA)
233 119 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_300mA_to_3A)
234 : return DsoService::CurrentRange::_300mA_to_3A; // Out of range, so go with the biggest.
235 : }
236 :
237 : /*!
238 : * Returns the lowest voltage range that can measure at least up to \a desired max, or AutoRange
239 : * if no such range is available.
240 : */
241 1190 : DsoService::VoltageRange DsoCommand::lowestVoltageRange(const quint32 desiredMax)
242 : {
243 1190 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_0_to_300mV)
244 1037 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_300mV_to_2V)
245 578 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_2V_to_6V)
246 408 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_6V_to_12V)
247 255 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_12V_to_30V)
248 102 : POKIT_APP_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_30V_to_60V)
249 : return DsoService::VoltageRange::_30V_to_60V; // Out of range, so go with the biggest.
250 : }
251 :
252 : #undef POKIT_APP_IF_LESS_THAN_RETURN
253 :
254 : /*!
255 : * Invoked when the DSO settings have been written.
256 : */
257 0 : void DsoCommand::settingsWritten()
258 : {
259 : Q_ASSERT(service);
260 0 : qCDebug(lc).noquote() << tr("Settings written; DSO has started.");
261 0 : connect(service, &DsoService::metadataRead, this, &DsoCommand::metadataRead);
262 0 : connect(service, &DsoService::samplesRead, this, &DsoCommand::outputSamples);
263 0 : service->enableMetadataNotifications();
264 0 : service->enableReadingNotifications();
265 0 : }
266 :
267 : /*!
268 : * Invoked when \a metadata has been received from the DSO.
269 : */
270 1037 : void DsoCommand::metadataRead(const DsoService::Metadata &metadata)
271 : {
272 1037 : qCDebug(lc) << "status:" << (int)(metadata.status);
273 1037 : qCDebug(lc) << "scale:" << metadata.scale;
274 1037 : qCDebug(lc) << "mode:" << DsoService::toString(metadata.mode);
275 1037 : qCDebug(lc) << "range:" << DsoService::toString(metadata.range.voltageRange);
276 1037 : qCDebug(lc) << "samplingWindow:" << (int)metadata.samplingWindow;
277 1037 : qCDebug(lc) << "numberOfSamples:" << metadata.numberOfSamples;
278 1037 : qCDebug(lc) << "samplingRate:" << metadata.samplingRate << "Hz";
279 1037 : this->metadata = metadata;
280 1037 : this->samplesToGo = metadata.numberOfSamples;
281 1037 : }
282 :
283 : /*!
284 : * Outputs DSO \a samples in the selected ouput format.
285 : */
286 1224 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
287 : {
288 216 : QString unit;
289 1224 : switch (metadata.mode) {
290 306 : case DsoService::Mode::DcVoltage: unit = QLatin1String("Vdc"); break;
291 306 : case DsoService::Mode::AcVoltage: unit = QLatin1String("Vac"); break;
292 306 : case DsoService::Mode::DcCurrent: unit = QLatin1String("Adc"); break;
293 306 : case DsoService::Mode::AcCurrent: unit = QLatin1String("Aac"); break;
294 0 : default:
295 0 : qCDebug(lc).noquote() << tr("No known unit for mode %1 \"%2\".").arg((int)metadata.mode)
296 0 : .arg(DsoService::toString(metadata.mode));
297 : }
298 2232 : const QString range = DsoService::toString(metadata.range, metadata.mode);
299 :
300 6936 : for (const qint16 &sample: samples) {
301 5712 : static int sampleNumber = 0; ++sampleNumber;
302 5712 : const float value = sample * metadata.scale;
303 5712 : switch (format) {
304 : case OutputFormat::Csv:
305 2176 : for (; showCsvHeader; showCsvHeader = false) {
306 320 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
307 : }
308 4144 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
309 : .arg(sampleNumber).arg(value).arg(unit, range));
310 1904 : break;
311 : case OutputFormat::Json:
312 18032 : std::cout << QJsonDocument(QJsonObject{
313 336 : { QLatin1String("value"), value },
314 336 : { QLatin1String("unit"), unit },
315 336 : { QLatin1String("range"), range },
316 3472 : { QLatin1String("mode"), DsoService::toString(metadata.mode) },
317 11424 : }).toJson().toStdString();
318 1904 : break;
319 1904 : case OutputFormat::Text:
320 4144 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
321 1904 : break;
322 : }
323 5712 : --samplesToGo;
324 : }
325 1224 : if (samplesToGo <= 0) {
326 2448 : qCInfo(lc).noquote() << tr("Finished fetching %L1 samples (with %L3 to remaining).")
327 1440 : .arg(metadata.numberOfSamples).arg(samplesToGo);
328 1224 : if (device) disconnect(); // Will exit the application once disconnected.
329 : }
330 1224 : }
|