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 9632 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
26 2336 : {
27 :
28 9696 : }
29 :
30 4720 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
31 1512 : {
32 22752 : return DeviceCommand::requiredOptions(parser) + QStringList{
33 1512 : u"mode"_s,
34 1512 : u"range"_s,
35 18681 : };
36 1512 : }
37 :
38 2320 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
39 728 : {
40 15460 : return DeviceCommand::supportedOptions(parser) + QStringList{
41 728 : u"interval"_s,
42 728 : u"samples"_s,
43 728 : u"trigger-level"_s,
44 728 : u"trigger-mode"_s,
45 13807 : };
46 728 : }
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 2240 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
55 672 : {
56 2912 : QStringList errors = DeviceCommand::processOptions(parser);
57 2912 : if (!errors.isEmpty()) {
58 72 : return errors;
59 72 : }
60 :
61 : // Parse the (required) mode option.
62 4600 : if (const QString mode = parser.value(u"mode"_s).trimmed().toLower();
63 5675 : mode.startsWith(u"ac v"_s) || mode.startsWith(u"vac"_s)) {
64 104 : settings.mode = DsoService::Mode::AcVoltage;
65 5448 : } else if (mode.startsWith(u"dc v"_s) || mode.startsWith(u"vdc"_s)) {
66 2184 : settings.mode = DsoService::Mode::DcVoltage;
67 1113 : } else if (mode.startsWith(u"ac c"_s) || mode.startsWith(u"aac"_s)) {
68 104 : settings.mode = DsoService::Mode::AcCurrent;
69 454 : } else if (mode.startsWith(u"dc c"_s) || mode.startsWith(u"adc"_s)) {
70 104 : settings.mode = DsoService::Mode::DcCurrent;
71 24 : } else {
72 197 : errors.append(tr("Unknown DSO mode: %1").arg(parser.value(u"mode"_s)));
73 24 : return errors;
74 949 : }
75 :
76 : // Parse the (required) range option.
77 1608 : QString unit;
78 576 : {
79 2496 : const QString value = parser.value(u"range"_s);
80 576 : quint32 sensibleMinimum = 0;
81 2496 : 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 2264 : case DsoService::Mode::DcVoltage:
86 528 : case DsoService::Mode::AcVoltage:
87 2288 : minRangeFunc = minVoltageRange;
88 2288 : unit = u"V"_s;
89 528 : sensibleMinimum = 50; // mV.
90 2288 : break;
91 184 : case DsoService::Mode::DcCurrent:
92 48 : case DsoService::Mode::AcCurrent:
93 208 : minRangeFunc = minCurrentRange;
94 208 : unit = u"A"_s;
95 48 : sensibleMinimum = 5; // mA.
96 208 : break;
97 576 : }
98 576 : Q_ASSERT(!unit.isEmpty());
99 2496 : rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
100 2496 : if (rangeOptionValue == 0) {
101 147 : errors.append(tr("Invalid range value: %1").arg(value));
102 24 : }
103 1464 : }
104 :
105 : // Parse the trigger-level option.
106 3528 : if (parser.isSet(u"trigger-level"_s)) {
107 832 : const QString value = parser.value(u"trigger-level"_s);
108 832 : const quint32 level = parseNumber<std::micro>(value, unit);
109 832 : if (level == 0) {
110 147 : errors.append(tr("Invalid trigger-level value: %1").arg(value));
111 168 : } else {
112 728 : settings.triggerLevel = (float)(level/1'000'000.0);
113 168 : }
114 488 : }
115 :
116 : // Parse the trigger-mode option.
117 3528 : if (parser.isSet(u"trigger-mode"_s)) {
118 1472 : const QString triggerMode = parser.value(u"trigger-mode"_s).trimmed().toLower();
119 1176 : if (triggerMode.startsWith(u"free"_s)) {
120 312 : settings.command = DsoService::Command::FreeRunning;
121 735 : } else if (triggerMode.startsWith(u"ris"_s)) {
122 208 : settings.command = DsoService::Command::RisingEdgeTrigger;
123 441 : } else if (triggerMode.startsWith(u"fall"_s)) {
124 208 : settings.command = DsoService::Command::FallingEdgeTrigger;
125 48 : } else {
126 197 : errors.append(tr("Unknown trigger mode: %1").arg(parser.value(u"trigger-mode"_s)));
127 24 : }
128 488 : }
129 :
130 : // Ensure that if either trigger option is present, then both are.
131 5448 : if (parser.isSet(u"trigger-level"_s) != parser.isSet(u"trigger-mode"_s)) {
132 208 : 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 3528 : if (parser.isSet(u"interval"_s)) {
137 780 : const QString value = parser.value(u"interval"_s);
138 520 : const quint32 interval = parseNumber<std::micro>(value, u"s"_s, 500'000);
139 520 : if (interval == 0) {
140 294 : errors.append(tr("Invalid interval value: %1").arg(value));
141 72 : } else {
142 312 : settings.samplingWindow = interval;
143 72 : }
144 305 : }
145 :
146 : // Parse the samples option.
147 3528 : if (parser.isSet(u"samples"_s)) {
148 780 : const QString value = parser.value(u"samples"_s);
149 520 : const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
150 520 : if (samples == 0) {
151 294 : errors.append(tr("Invalid samples value: %1").arg(value));
152 312 : } else if (samples > std::numeric_limits<quint16>::max()) {
153 113 : errors.append(tr("Samples value (%1) must be no greater than %2")
154 217 : .arg(value).arg(std::numeric_limits<quint16>::max()));
155 48 : } else {
156 208 : if (samples > 8192) {
157 256 : qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
158 24 : }
159 208 : settings.numberOfSamples = (quint16)samples;
160 48 : }
161 305 : }
162 576 : return errors;
163 1464 : }
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 4880 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
229 1496 : {
230 8145 : qCDebug(lc) << "status:" << (int)(data.status);
231 8145 : qCDebug(lc) << "scale:" << data.scale;
232 8145 : qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
233 8145 : qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
234 8145 : qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
235 8145 : qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
236 8145 : qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
237 6376 : this->metadata = data;
238 6376 : this->samplesToGo = data.numberOfSamples;
239 6376 : }
240 :
241 : /*!
242 : * Outputs DSO \a samples in the selected output format.
243 : */
244 5760 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
245 1728 : {
246 4824 : QString unit;
247 7488 : switch (metadata.mode) {
248 2646 : case DsoService::Mode::DcVoltage: unit = u"Vdc"_s; break;
249 2646 : case DsoService::Mode::AcVoltage: unit = u"Vac"_s; break;
250 2646 : case DsoService::Mode::DcCurrent: unit = u"Adc"_s; break;
251 2646 : 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 10152 : const QString range = service->toString(metadata.range, metadata.mode);
257 :
258 40704 : for (const qint16 &sample: samples) {
259 34944 : static int sampleNumber = 0; ++sampleNumber;
260 34944 : const float value = sample * metadata.scale;
261 34944 : switch (format) {
262 2688 : case OutputFormat::Csv:
263 13312 : for (; showCsvHeader; showCsvHeader = false) {
264 2352 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
265 384 : }
266 24528 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
267 2688 : .arg(sampleNumber).arg(value).arg(unit, range));
268 11648 : break;
269 11648 : case OutputFormat::Json:
270 60144 : std::cout << QJsonDocument(QJsonObject{
271 14784 : { u"value"_s, value },
272 14784 : { u"unit"_s, unit },
273 14784 : { u"range"_s, range },
274 20608 : { u"mode"_s, DsoService::toString(metadata.mode) },
275 57008 : }).toJson().toStdString();
276 11648 : break;
277 11648 : case OutputFormat::Text:
278 24976 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
279 11648 : break;
280 8064 : }
281 34944 : --samplesToGo;
282 8064 : }
283 7488 : if (samplesToGo <= 0) {
284 17352 : qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
285 12600 : nullptr, metadata.numberOfSamples).arg(samplesToGo);
286 7488 : if (device) disconnect(); // Will exit the application once disconnected.
287 1728 : }
288 38848 : }
|