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 1656 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
23 : {
24 :
25 1656 : }
26 :
27 1062 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
28 : {
29 4602 : return DeviceCommand::requiredOptions(parser) + QStringList{
30 : QLatin1String("mode"),
31 : QLatin1String("range"),
32 4189 : };
33 0 : }
34 :
35 522 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
36 : {
37 3306 : return DeviceCommand::supportedOptions(parser) + QStringList{
38 : QLatin1String("interval"),
39 : QLatin1String("samples"),
40 : QLatin1String("trigger-level"),
41 : QLatin1String("trigger-mode"),
42 3103 : };
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 504 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
52 : {
53 504 : QStringList errors = DeviceCommand::processOptions(parser);
54 504 : if (!errors.isEmpty()) {
55 : return errors;
56 : }
57 :
58 : // Parse the (required) mode option.
59 900 : const QString mode = parser.value(QLatin1String("mode")).trimmed().toLower();
60 450 : if (mode.startsWith(QLatin1String("ac v")) || mode.startsWith(QLatin1String("vac"))) {
61 18 : settings.mode = DsoService::Mode::AcVoltage;
62 432 : } else if (mode.startsWith(QLatin1String("dc v")) || mode.startsWith(QLatin1String("vdc"))) {
63 378 : settings.mode = DsoService::Mode::DcVoltage;
64 54 : } else if (mode.startsWith(QLatin1String("ac c")) || mode.startsWith(QLatin1String("aac"))) {
65 18 : settings.mode = DsoService::Mode::AcCurrent;
66 36 : } else if (mode.startsWith(QLatin1String("dc c")) || mode.startsWith(QLatin1String("adc"))) {
67 18 : settings.mode = DsoService::Mode::DcCurrent;
68 : } else {
69 36 : errors.append(tr("Unknown DSO mode: %1").arg(parser.value(QLatin1String("mode"))));
70 18 : return errors;
71 : }
72 :
73 : // Parse the (required) range option.
74 96 : QString unit;
75 : {
76 432 : const QString value = parser.value(QLatin1String("range"));
77 : quint32 sensibleMinimum = 0;
78 432 : 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 : case DsoService::Mode::DcVoltage:
83 : case DsoService::Mode::AcVoltage:
84 396 : unit = QLatin1String("V");
85 : sensibleMinimum = 50; // mV.
86 396 : break;
87 : case DsoService::Mode::DcCurrent:
88 : case DsoService::Mode::AcCurrent:
89 36 : unit = QLatin1String("A");
90 : sensibleMinimum = 5; // mA.
91 36 : break;
92 : }
93 : Q_ASSERT(!unit.isEmpty());
94 432 : const quint32 rangeMax = parseMilliValue(value, unit, sensibleMinimum);
95 432 : if (rangeMax == 0) {
96 22 : errors.append(tr("Invalid range value: %1").arg(value));
97 : } else {
98 414 : settings.range = lowestRange(settings.mode, rangeMax);
99 : }
100 336 : }
101 :
102 : // Parse the trigger-level option.
103 528 : if (parser.isSet(QLatin1String("trigger-level"))) {
104 144 : const QString value = parser.value(QLatin1String("trigger-level"));
105 144 : const quint32 level = parseMicroValue(value, unit);
106 144 : if (level == 0) {
107 22 : errors.append(tr("Invalid trigger-level value: %1").arg(value));
108 : } else {
109 126 : settings.triggerLevel = (float)(level/1000.0/1000.0);
110 : }
111 112 : }
112 :
113 : // Parse the trigger-mode option.
114 528 : if (parser.isSet(QLatin1String("trigger-mode"))) {
115 288 : const QString triggerMode = parser.value(QLatin1String("trigger-mode")).trimmed().toLower();
116 144 : if (triggerMode.startsWith(QLatin1String("free"))) {
117 54 : settings.command = DsoService::Command::FreeRunning;
118 90 : } else if (triggerMode.startsWith(QLatin1String("ris"))) {
119 36 : settings.command = DsoService::Command::RisingEdgeTrigger;
120 54 : } else if (triggerMode.startsWith(QLatin1String("fall"))) {
121 36 : settings.command = DsoService::Command::FallingEdgeTrigger;
122 : } else {
123 54 : errors.append(tr("Unknown trigger mode: %1").arg(
124 36 : parser.value(QLatin1String("trigger-mode"))));
125 : }
126 112 : }
127 :
128 : // Ensure that if either trigger option is present, then both are.
129 864 : if (parser.isSet(QLatin1String("trigger-level")) !=
130 528 : parser.isSet(QLatin1String("trigger-mode"))) {
131 36 : errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
132 : }
133 :
134 : // Parse the interval option.
135 528 : if (parser.isSet(QLatin1String("interval"))) {
136 150 : const QString value = parser.value(QLatin1String("interval"));
137 90 : const quint32 interval = parseMicroValue(value, QLatin1String("s"), 500*1000);
138 90 : if (interval == 0) {
139 44 : errors.append(tr("Invalid interval value: %1").arg(value));
140 : } else {
141 54 : settings.samplingWindow = interval;
142 : }
143 70 : }
144 :
145 : // Parse the samples option.
146 528 : if (parser.isSet(QLatin1String("samples"))) {
147 150 : const QString value = parser.value(QLatin1String("samples"));
148 90 : const quint32 samples = parseWholeValue(value, QLatin1String("S"));
149 90 : if (samples == 0) {
150 44 : errors.append(tr("Invalid samples value: %1").arg(value));
151 54 : } else if (samples > std::numeric_limits<quint16>::max()) {
152 26 : errors.append(tr("Samples value (%1) must be no greater than %2")
153 40 : .arg(value).arg(std::numeric_limits<quint16>::max()));
154 : } else {
155 36 : if (samples > 8192) {
156 40 : qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
157 : }
158 36 : 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 : 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 : const QString range = DsoService::toString(settings.range, settings.mode);
190 0 : qCInfo(lc).noquote() << tr("Sampling %1, with range %2, %Ln sample/s over %L3us", nullptr, settings.numberOfSamples)
191 0 : .arg(DsoService::toString(settings.mode), (range.isNull()) ? QString::fromLatin1("N/A") : range)
192 0 : .arg(settings.samplingWindow);
193 0 : service->setSettings(settings);
194 0 : }
195 :
196 : /*!
197 : * Returns the lowest \a mode range that can measure at least up to \a desired max, or AutoRange
198 : * if no such range is available.
199 : */
200 1530 : DsoService::Range DsoCommand::lowestRange(
201 : const DsoService::Mode mode, const quint32 desiredMax)
202 : {
203 1530 : switch (mode) {
204 0 : case DsoService::Mode::Idle:
205 0 : qCWarning(lc).noquote() << tr("Idle has no defined ranges.");
206 : Q_ASSERT(false); // Should never have been called with this Idle mode.
207 0 : break;
208 990 : case DsoService::Mode::DcVoltage:
209 : case DsoService::Mode::AcVoltage:
210 990 : return lowestVoltageRange(desiredMax);
211 540 : case DsoService::Mode::DcCurrent:
212 : case DsoService::Mode::AcCurrent:
213 540 : return lowestCurrentRange(desiredMax);
214 0 : default:
215 0 : qCWarning(lc).noquote() << tr("No defined ranges for mode %1.").arg((quint8)mode);
216 : Q_ASSERT(false); // Should never have been called with this invalid mode.
217 : }
218 0 : return DsoService::Range();
219 : }
220 :
221 : #define DOKIT_CLI_IF_LESS_THAN_RETURN(value, label) \
222 : if (value <= DsoService::maxValue(DsoService::label).toUInt()) { \
223 : return DsoService::label; \
224 : }
225 :
226 : /*!
227 : * Returns the lowest current range that can measure at least up to \a desired max, or AutoRange
228 : * if no such range is available.
229 : */
230 792 : DsoService::CurrentRange DsoCommand::lowestCurrentRange(const quint32 desiredMax)
231 : {
232 792 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_0_to_10mA)
233 630 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_10mA_to_30mA)
234 468 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_30mA_to_150mA)
235 288 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_150mA_to_300mA)
236 126 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, CurrentRange::_300mA_to_3A)
237 : return DsoService::CurrentRange::_300mA_to_3A; // Out of range, so go with the biggest.
238 : }
239 :
240 : /*!
241 : * Returns the lowest voltage range that can measure at least up to \a desired max, or AutoRange
242 : * if no such range is available.
243 : */
244 1296 : DsoService::VoltageRange DsoCommand::lowestVoltageRange(const quint32 desiredMax)
245 : {
246 1296 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_0_to_300mV)
247 1134 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_300mV_to_2V)
248 612 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_2V_to_6V)
249 432 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_6V_to_12V)
250 270 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_12V_to_30V)
251 108 : DOKIT_CLI_IF_LESS_THAN_RETURN(desiredMax, VoltageRange::_30V_to_60V)
252 : return DsoService::VoltageRange::_30V_to_60V; // Out of range, so go with the biggest.
253 : }
254 :
255 : #undef DOKIT_CLI_IF_LESS_THAN_RETURN
256 :
257 : /*!
258 : * Invoked when the DSO settings have been written.
259 : */
260 0 : void DsoCommand::settingsWritten()
261 : {
262 : Q_ASSERT(service);
263 0 : qCDebug(lc).noquote() << tr("Settings written; DSO has started.");
264 0 : connect(service, &DsoService::metadataRead, this, &DsoCommand::metadataRead);
265 0 : connect(service, &DsoService::samplesRead, this, &DsoCommand::outputSamples);
266 0 : service->enableMetadataNotifications();
267 0 : service->enableReadingNotifications();
268 0 : }
269 :
270 : /*!
271 : * Invoked when \a metadata has been received from the DSO.
272 : */
273 1098 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
274 : {
275 1098 : qCDebug(lc) << "status:" << (int)(data.status);
276 1098 : qCDebug(lc) << "scale:" << data.scale;
277 1098 : qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
278 1098 : qCDebug(lc) << "range:" << DsoService::toString(data.range.voltageRange);
279 1098 : qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
280 1098 : qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
281 1098 : qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
282 1098 : this->metadata = data;
283 1098 : this->samplesToGo = data.numberOfSamples;
284 1098 : }
285 :
286 : /*!
287 : * Outputs DSO \a samples in the selected ouput format.
288 : */
289 1296 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
290 : {
291 288 : QString unit;
292 1296 : switch (metadata.mode) {
293 324 : case DsoService::Mode::DcVoltage: unit = QLatin1String("Vdc"); break;
294 324 : case DsoService::Mode::AcVoltage: unit = QLatin1String("Vac"); break;
295 324 : case DsoService::Mode::DcCurrent: unit = QLatin1String("Adc"); break;
296 324 : case DsoService::Mode::AcCurrent: unit = QLatin1String("Aac"); break;
297 0 : default:
298 0 : qCDebug(lc).noquote() << tr("No known unit for mode %1 \"%2\".").arg((int)metadata.mode)
299 0 : .arg(DsoService::toString(metadata.mode));
300 : }
301 2304 : const QString range = DsoService::toString(metadata.range, metadata.mode);
302 :
303 7344 : for (const qint16 &sample: samples) {
304 6048 : static int sampleNumber = 0; ++sampleNumber;
305 6048 : const float value = sample * metadata.scale;
306 6048 : switch (format) {
307 : case OutputFormat::Csv:
308 2304 : for (; showCsvHeader; showCsvHeader = false) {
309 352 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
310 : }
311 4480 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
312 : .arg(sampleNumber).arg(value).arg(unit, range));
313 2016 : break;
314 672 : case OutputFormat::Json:
315 16128 : std::cout << QJsonDocument(QJsonObject{
316 448 : { QLatin1String("value"), value },
317 448 : { QLatin1String("unit"), unit },
318 448 : { QLatin1String("range"), range },
319 3584 : { QLatin1String("mode"), DsoService::toString(metadata.mode) },
320 12096 : }).toJson().toStdString();
321 2016 : break;
322 2016 : case OutputFormat::Text:
323 4480 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
324 2016 : break;
325 : }
326 6048 : --samplesToGo;
327 : }
328 1296 : if (samplesToGo <= 0) {
329 3024 : qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
330 2016 : nullptr, metadata.numberOfSamples).arg(samplesToGo);
331 1296 : if (device) disconnect(); // Will exit the application once disconnected.
332 : }
333 5328 : }
|