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 5244 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
23 1684 : {
24 :
25 5364 : }
26 :
27 2360 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
28 1093 : {
29 12067 : return DeviceCommand::requiredOptions(parser) + QStringList{
30 1093 : QLatin1String("mode"),
31 1093 : QLatin1String("range"),
32 9294 : };
33 2391 : }
34 :
35 1160 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
36 523 : {
37 8005 : return DeviceCommand::supportedOptions(parser) + QStringList{
38 523 : QLatin1String("interval"),
39 523 : QLatin1String("samples"),
40 523 : QLatin1String("trigger-level"),
41 523 : QLatin1String("trigger-mode"),
42 6874 : };
43 1161 : }
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 1120 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
52 476 : {
53 1596 : QStringList errors = DeviceCommand::processOptions(parser);
54 1596 : if (!errors.isEmpty()) {
55 51 : return errors;
56 51 : }
57 :
58 : // Parse the (required) mode option.
59 2425 : if (const QString mode = parser.value(QLatin1String("mode")).trimmed().toLower();
60 1575 : mode.startsWith(QLatin1String("ac v")) || mode.startsWith(QLatin1String("vac"))) {
61 57 : settings.mode = DsoService::Mode::AcVoltage;
62 1512 : } else if (mode.startsWith(QLatin1String("dc v")) || mode.startsWith(QLatin1String("vdc"))) {
63 1197 : settings.mode = DsoService::Mode::DcVoltage;
64 495 : } else if (mode.startsWith(QLatin1String("ac c")) || mode.startsWith(QLatin1String("aac"))) {
65 57 : settings.mode = DsoService::Mode::AcCurrent;
66 126 : } else if (mode.startsWith(QLatin1String("dc c")) || mode.startsWith(QLatin1String("adc"))) {
67 57 : settings.mode = DsoService::Mode::DcCurrent;
68 17 : } else {
69 99 : errors.append(tr("Unknown DSO mode: %1").arg(parser.value(QLatin1String("mode"))));
70 17 : return errors;
71 592 : }
72 :
73 : // Parse the (required) range option.
74 816 : QString unit;
75 408 : {
76 1368 : const QString value = parser.value(QLatin1String("range"));
77 408 : quint32 sensibleMinimum = 0;
78 1368 : 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 1237 : case DsoService::Mode::DcVoltage:
83 374 : case DsoService::Mode::AcVoltage:
84 1254 : minRangeFunc = minVoltageRange;
85 1254 : unit = QLatin1String("V");
86 374 : sensibleMinimum = 50; // mV.
87 1254 : break;
88 97 : case DsoService::Mode::DcCurrent:
89 34 : case DsoService::Mode::AcCurrent:
90 114 : minRangeFunc = minCurrentRange;
91 114 : unit = QLatin1String("A");
92 34 : sensibleMinimum = 5; // mA.
93 114 : break;
94 408 : }
95 408 : Q_ASSERT(!unit.isEmpty());
96 1368 : rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
97 1368 : if (rangeOptionValue == 0) {
98 74 : errors.append(tr("Invalid range value: %1").arg(value));
99 17 : }
100 960 : }
101 :
102 : // Parse the trigger-level option.
103 1776 : if (parser.isSet(QLatin1String("trigger-level"))) {
104 456 : const QString value = parser.value(QLatin1String("trigger-level"));
105 456 : const quint32 level = parseNumber<std::micro>(value, unit);
106 456 : if (level == 0) {
107 74 : errors.append(tr("Invalid trigger-level value: %1").arg(value));
108 119 : } else {
109 399 : settings.triggerLevel = (float)(level/1'000'000.0);
110 119 : }
111 320 : }
112 :
113 : // Parse the trigger-mode option.
114 1776 : if (parser.isSet(QLatin1String("trigger-mode"))) {
115 776 : const QString triggerMode = parser.value(QLatin1String("trigger-mode")).trimmed().toLower();
116 456 : if (triggerMode.startsWith(QLatin1String("free"))) {
117 171 : settings.command = DsoService::Command::FreeRunning;
118 285 : } else if (triggerMode.startsWith(QLatin1String("ris"))) {
119 114 : settings.command = DsoService::Command::RisingEdgeTrigger;
120 171 : } else if (triggerMode.startsWith(QLatin1String("fall"))) {
121 114 : settings.command = DsoService::Command::FallingEdgeTrigger;
122 34 : } else {
123 120 : errors.append(tr("Unknown trigger mode: %1").arg(
124 97 : parser.value(QLatin1String("trigger-mode"))));
125 17 : }
126 320 : }
127 :
128 : // Ensure that if either trigger option is present, then both are.
129 2328 : if (parser.isSet(QLatin1String("trigger-level")) !=
130 1776 : parser.isSet(QLatin1String("trigger-mode"))) {
131 114 : errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
132 34 : }
133 :
134 : // Parse the interval option.
135 1776 : if (parser.isSet(QLatin1String("interval"))) {
136 370 : const QString value = parser.value(QLatin1String("interval"));
137 285 : const quint32 interval = parseNumber<std::micro>(value, QLatin1String("s"), 500'000);
138 285 : if (interval == 0) {
139 148 : errors.append(tr("Invalid interval value: %1").arg(value));
140 51 : } else {
141 171 : settings.samplingWindow = interval;
142 51 : }
143 200 : }
144 :
145 : // Parse the samples option.
146 1776 : if (parser.isSet(QLatin1String("samples"))) {
147 370 : const QString value = parser.value(QLatin1String("samples"));
148 285 : const quint32 samples = parseNumber<std::ratio<1>>(value, QLatin1String("S"));
149 285 : if (samples == 0) {
150 148 : errors.append(tr("Invalid samples value: %1").arg(value));
151 171 : } else if (samples > std::numeric_limits<quint16>::max()) {
152 57 : errors.append(tr("Samples value (%1) must be no greater than %2")
153 112 : .arg(value).arg(std::numeric_limits<quint16>::max()));
154 34 : } else {
155 114 : if (samples > 8192) {
156 124 : qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
157 17 : }
158 114 : settings.numberOfSamples = (quint16)samples;
159 34 : }
160 200 : }
161 408 : return errors;
162 960 : }
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 2440 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
228 1067 : {
229 4117 : qCDebug(lc) << "status:" << (int)(data.status);
230 4117 : qCDebug(lc) << "scale:" << data.scale;
231 4117 : qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
232 4117 : qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
233 4117 : qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
234 4117 : qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
235 4117 : qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
236 3507 : this->metadata = data;
237 3507 : this->samplesToGo = data.numberOfSamples;
238 3507 : }
239 :
240 : /*!
241 : * Outputs DSO \a samples in the selected ouput format.
242 : */
243 2880 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
244 1224 : {
245 2448 : QString unit;
246 4104 : switch (metadata.mode) {
247 1026 : case DsoService::Mode::DcVoltage: unit = QLatin1String("Vdc"); break;
248 1026 : case DsoService::Mode::AcVoltage: unit = QLatin1String("Vac"); break;
249 1026 : case DsoService::Mode::DcCurrent: unit = QLatin1String("Adc"); break;
250 1026 : 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 1224 : }
255 5760 : const QString range = service->toString(metadata.range, metadata.mode);
256 :
257 22032 : for (const qint16 &sample: samples) {
258 19152 : static int sampleNumber = 0; ++sampleNumber;
259 19152 : const float value = sample * metadata.scale;
260 19152 : switch (format) {
261 1904 : case OutputFormat::Csv:
262 7296 : for (; showCsvHeader; showCsvHeader = false) {
263 1184 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
264 272 : }
265 12544 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
266 1904 : .arg(sampleNumber).arg(value).arg(unit, range));
267 6384 : break;
268 6384 : case OutputFormat::Json:
269 26544 : std::cout << QJsonDocument(QJsonObject{
270 3808 : { QLatin1String("value"), value },
271 3808 : { QLatin1String("unit"), unit },
272 3808 : { QLatin1String("range"), range },
273 8960 : { QLatin1String("mode"), DsoService::toString(metadata.mode) },
274 29568 : }).toJson().toStdString();
275 6384 : break;
276 6384 : case OutputFormat::Text:
277 12544 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
278 6384 : break;
279 5712 : }
280 19152 : --samplesToGo;
281 5712 : }
282 4104 : if (samplesToGo <= 0) {
283 9360 : qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
284 6984 : nullptr, metadata.numberOfSamples).arg(samplesToGo);
285 4104 : if (device) disconnect(); // Will exit the application once disconnected.
286 1224 : }
287 23816 : }
|