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 : #include "../stringliterals_p.h"
6 :
7 : #include <qtpokit/pokitdevice.h>
8 :
9 : #include <QJsonDocument>
10 : #include <QJsonObject>
11 :
12 : #include <iostream>
13 :
14 : DOKIT_USE_STRINGLITERALS
15 :
16 : /*!
17 : * \class DsoCommand
18 : *
19 : * The DsoCommand class implements the `dso` CLI command.
20 : */
21 :
22 : /*!
23 : * Construct a new DsoCommand object with \a parent.
24 : */
25 8316 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
26 2400 : {
27 :
28 8380 : }
29 :
30 3835 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
31 1560 : {
32 19201 : return DeviceCommand::requiredOptions(parser) + QStringList{
33 1560 : u"mode"_s,
34 1560 : u"range"_s,
35 15189 : };
36 1560 : }
37 :
38 1885 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
39 744 : {
40 13069 : return DeviceCommand::supportedOptions(parser) + QStringList{
41 744 : u"interval"_s,
42 744 : u"samples"_s,
43 744 : u"trigger-level"_s,
44 744 : u"trigger-mode"_s,
45 11213 : };
46 744 : }
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 1820 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
55 672 : {
56 2492 : QStringList errors = DeviceCommand::processOptions(parser);
57 2492 : if (!errors.isEmpty()) {
58 72 : return errors;
59 72 : }
60 :
61 : // Parse the (required) mode option.
62 3850 : if (const QString mode = parser.value(u"mode"_s).trimmed().toLower();
63 4875 : mode.startsWith(u"ac v"_s) || mode.startsWith(u"vac"_s)) {
64 89 : settings.mode = DsoService::Mode::AcVoltage;
65 4680 : } else if (mode.startsWith(u"dc v"_s) || mode.startsWith(u"vdc"_s)) {
66 1869 : settings.mode = DsoService::Mode::DcVoltage;
67 1017 : } else if (mode.startsWith(u"ac c"_s) || mode.startsWith(u"aac"_s)) {
68 89 : settings.mode = DsoService::Mode::AcCurrent;
69 390 : } else if (mode.startsWith(u"dc c"_s) || mode.startsWith(u"adc"_s)) {
70 89 : settings.mode = DsoService::Mode::DcCurrent;
71 24 : } else {
72 167 : errors.append(tr("Unknown DSO mode: %1").arg(parser.value(u"mode"_s)));
73 24 : return errors;
74 624 : }
75 :
76 : // Parse the (required) range option.
77 1560 : QString unit;
78 576 : {
79 2136 : const QString value = parser.value(u"range"_s);
80 576 : quint32 sensibleMinimum = 0;
81 2136 : switch (settings.mode) {
82 0 : case DsoService::Mode::Idle:
83 0 : Q_ASSERT(false); // Not possible, since the mode parsing above never allows Idle.
84 0 : break;
85 1934 : case DsoService::Mode::DcVoltage:
86 528 : case DsoService::Mode::AcVoltage:
87 1958 : minRangeFunc = minVoltageRange;
88 1958 : unit = u"V"_s;
89 528 : sensibleMinimum = 50; // mV.
90 1958 : break;
91 154 : case DsoService::Mode::DcCurrent:
92 48 : case DsoService::Mode::AcCurrent:
93 178 : minRangeFunc = minCurrentRange;
94 178 : unit = u"A"_s;
95 48 : sensibleMinimum = 5; // mA.
96 178 : break;
97 576 : }
98 576 : Q_ASSERT(!unit.isEmpty());
99 2136 : rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
100 2136 : if (rangeOptionValue == 0) {
101 130 : errors.append(tr("Invalid range value: %1").arg(value));
102 24 : }
103 1152 : }
104 :
105 : // Parse the trigger-level option.
106 3120 : if (parser.isSet(u"trigger-level"_s)) {
107 712 : const QString value = parser.value(u"trigger-level"_s);
108 712 : const quint32 level = parseNumber<std::micro>(value, unit);
109 712 : if (level == 0) {
110 130 : errors.append(tr("Invalid trigger-level value: %1").arg(value));
111 168 : } else {
112 623 : settings.triggerLevel = (float)(level/1'000'000.0);
113 168 : }
114 384 : }
115 :
116 : // Parse the trigger-mode option.
117 3120 : if (parser.isSet(u"trigger-mode"_s)) {
118 1232 : const QString triggerMode = parser.value(u"trigger-mode"_s).trimmed().toLower();
119 1040 : if (triggerMode.startsWith(u"free"_s)) {
120 267 : settings.command = DsoService::Command::FreeRunning;
121 650 : } else if (triggerMode.startsWith(u"ris"_s)) {
122 178 : settings.command = DsoService::Command::RisingEdgeTrigger;
123 390 : } else if (triggerMode.startsWith(u"fall"_s)) {
124 178 : settings.command = DsoService::Command::FallingEdgeTrigger;
125 48 : } else {
126 167 : errors.append(tr("Unknown trigger mode: %1").arg(parser.value(u"trigger-mode"_s)));
127 24 : }
128 384 : }
129 :
130 : // Ensure that if either trigger option is present, then both are.
131 4680 : if (parser.isSet(u"trigger-level"_s) != parser.isSet(u"trigger-mode"_s)) {
132 178 : errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
133 48 : }
134 :
135 : // Parse the interval option.
136 3120 : if (parser.isSet(u"interval"_s)) {
137 695 : const QString value = parser.value(u"interval"_s);
138 445 : const quint32 interval = parseNumber<std::micro>(value, u"s"_s, 500'000);
139 445 : if (interval == 0) {
140 260 : errors.append(tr("Invalid interval value: %1").arg(value));
141 72 : } else {
142 267 : settings.samplingWindow = interval;
143 72 : }
144 240 : }
145 :
146 : // Parse the samples option.
147 3120 : if (parser.isSet(u"samples"_s)) {
148 695 : const QString value = parser.value(u"samples"_s);
149 445 : const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
150 445 : if (samples == 0) {
151 260 : errors.append(tr("Invalid samples value: %1").arg(value));
152 267 : } else if (samples > std::numeric_limits<quint16>::max()) {
153 98 : errors.append(tr("Samples value (%1) must be no greater than %2")
154 185 : .arg(value).arg(std::numeric_limits<quint16>::max()));
155 48 : } else {
156 178 : if (samples > 8192) {
157 224 : qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
158 24 : }
159 178 : settings.numberOfSamples = (quint16)samples;
160 48 : }
161 240 : }
162 576 : return errors;
163 1152 : }
164 :
165 : /*!
166 : * \copybrief DeviceCommand::getService
167 : *
168 : * This override returns a pointer to a DsoService object.
169 : */
170 0 : AbstractPokitService * DsoCommand::getService()
171 0 : {
172 0 : Q_ASSERT(device);
173 0 : if (!service) {
174 0 : service = device->dso();
175 0 : Q_ASSERT(service);
176 0 : connect(service, &DsoService::settingsWritten,
177 0 : this, &DsoCommand::settingsWritten);
178 0 : }
179 0 : return service;
180 0 : }
181 :
182 : /*!
183 : * \copybrief DeviceCommand::serviceDetailsDiscovered
184 : *
185 : * This override fetches the current device's status, and outputs it in the selected format.
186 : */
187 0 : void DsoCommand::serviceDetailsDiscovered()
188 0 : {
189 0 : DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
190 0 : settings.range = (minRangeFunc == nullptr) ? 0 : minRangeFunc(*service->pokitProduct(), rangeOptionValue);
191 0 : const QString range = service->toString(settings.range, settings.mode);
192 0 : qCInfo(lc).noquote() << tr("Sampling %1, with range %2, %Ln sample/s over %L3us", nullptr, settings.numberOfSamples)
193 0 : .arg(DsoService::toString(settings.mode), (range.isNull()) ? QString::fromLatin1("N/A") : range)
194 0 : .arg(settings.samplingWindow);
195 0 : service->setSettings(settings);
196 0 : }
197 :
198 : /*!
199 : * \var DsoCommand::minRangeFunc
200 : *
201 : * Pointer to function for converting #rangeOptionValue to a Pokit device's range enumerator. This function pointer
202 : * is assigned during the command line parsing, but is not invoked until after the device's services are discovered,
203 : * because prior to that discovery, we don't know which product (Meter vs Pro vs Clamp, etc) we're talking to and thus
204 : * which enumerator list to be using.
205 : *
206 : * If the current mode does not support ranges (eg diode, and continuity modes), then this member will be \c nullptr.
207 : *
208 : * \see processOptions
209 : * \see serviceDetailsDiscovered
210 : */
211 :
212 : /*!
213 : * Invoked when the DSO settings have been written.
214 : */
215 0 : void DsoCommand::settingsWritten()
216 0 : {
217 0 : Q_ASSERT(service);
218 0 : qCDebug(lc).noquote() << tr("Settings written; DSO has started.");
219 0 : connect(service, &DsoService::metadataRead, this, &DsoCommand::metadataRead);
220 0 : connect(service, &DsoService::samplesRead, this, &DsoCommand::outputSamples);
221 0 : service->enableMetadataNotifications();
222 0 : service->enableReadingNotifications();
223 0 : }
224 :
225 : /*!
226 : * Invoked when \a metadata has been received from the DSO.
227 : */
228 3965 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
229 1512 : {
230 7246 : qCDebug(lc) << "status:" << (int)(data.status);
231 7246 : qCDebug(lc) << "scale:" << data.scale;
232 7246 : qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
233 7246 : qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
234 7246 : qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
235 7246 : qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
236 7246 : qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
237 5477 : this->metadata = data;
238 5477 : this->samplesToGo = data.numberOfSamples;
239 5477 : }
240 :
241 : /*!
242 : * Outputs DSO \a samples in the selected output format.
243 : */
244 4680 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
245 1728 : {
246 4680 : QString unit;
247 6408 : switch (metadata.mode) {
248 2340 : case DsoService::Mode::DcVoltage: unit = u"Vdc"_s; break;
249 2340 : case DsoService::Mode::AcVoltage: unit = u"Vac"_s; break;
250 2340 : case DsoService::Mode::DcCurrent: unit = u"Adc"_s; break;
251 2340 : case DsoService::Mode::AcCurrent: unit = u"Aac"_s; break;
252 0 : default:
253 0 : qCDebug(lc).noquote() << tr(R"(No known unit for mode %1 "%2".)").arg((int)metadata.mode)
254 0 : .arg(DsoService::toString(metadata.mode));
255 1728 : }
256 8136 : const QString range = service->toString(metadata.range, metadata.mode);
257 :
258 34584 : for (const qint16 &sample: samples) {
259 29904 : static int sampleNumber = 0; ++sampleNumber;
260 29904 : const float value = sample * metadata.scale;
261 29904 : switch (format) {
262 2688 : case OutputFormat::Csv:
263 11392 : for (; showCsvHeader; showCsvHeader = false) {
264 2080 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
265 384 : }
266 20944 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
267 2688 : .arg(sampleNumber).arg(value).arg(unit, range));
268 9968 : break;
269 9968 : case OutputFormat::Json:
270 52304 : std::cout << QJsonDocument(QJsonObject{
271 11648 : { u"value"_s, value },
272 11648 : { u"unit"_s, unit },
273 11648 : { u"range"_s, range },
274 17248 : { u"mode"_s, DsoService::toString(metadata.mode) },
275 44464 : }).toJson().toStdString();
276 9968 : break;
277 9968 : case OutputFormat::Text:
278 21392 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
279 9968 : break;
280 8064 : }
281 29904 : --samplesToGo;
282 8064 : }
283 6408 : if (samplesToGo <= 0) {
284 14256 : qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
285 10440 : nullptr, metadata.numberOfSamples).arg(samplesToGo);
286 6408 : if (device) disconnect(); // Will exit the application once disconnected.
287 1728 : }
288 29032 : }
|