Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2026 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 9104 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
26 2732 : {
27 :
28 9172 : }
29 :
30 4130 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
31 1789 : {
32 20846 : return DeviceCommand::requiredOptions(parser) + QStringList{
33 1789 : u"mode"_s,
34 1789 : u"range"_s,
35 16303 : };
36 1789 : }
37 :
38 2030 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
39 839 : {
40 14150 : return DeviceCommand::supportedOptions(parser) + QStringList{
41 839 : u"interval"_s,
42 839 : u"samples"_s,
43 839 : u"trigger-level"_s,
44 839 : u"trigger-mode"_s,
45 12033 : };
46 839 : }
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 1960 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
55 728 : {
56 2688 : QStringList errors = DeviceCommand::processOptions(parser);
57 2688 : if (!errors.isEmpty()) {
58 78 : return errors;
59 78 : }
60 :
61 : // Parse the (required) mode option.
62 4150 : if (const QString mode = parser.value(u"mode"_s).trimmed().toLower();
63 5300 : mode.startsWith(u"ac v"_s) || mode.startsWith(u"vac"_s)) {
64 96 : settings.mode = DsoService::Mode::AcVoltage;
65 5088 : } else if (mode.startsWith(u"dc v"_s) || mode.startsWith(u"vdc"_s)) {
66 2016 : settings.mode = DsoService::Mode::DcVoltage;
67 1104 : } else if (mode.startsWith(u"ac c"_s) || mode.startsWith(u"aac"_s)) {
68 96 : settings.mode = DsoService::Mode::AcCurrent;
69 424 : } else if (mode.startsWith(u"dc c"_s) || mode.startsWith(u"adc"_s)) {
70 96 : settings.mode = DsoService::Mode::DcCurrent;
71 26 : } else {
72 184 : errors.append(tr("Unknown DSO mode: %1").arg(parser.value(u"mode"_s)));
73 26 : return errors;
74 626 : }
75 :
76 : // Parse the (required) range option.
77 1728 : QString unit;
78 624 : {
79 2304 : const QString value = parser.value(u"range"_s);
80 624 : quint32 sensibleMinimum = 0;
81 2304 : 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 2086 : case DsoService::Mode::DcVoltage:
86 572 : case DsoService::Mode::AcVoltage:
87 2112 : minRangeFunc = minVoltageRange;
88 2112 : unit = u"V"_s;
89 572 : sensibleMinimum = 50; // mV.
90 2112 : break;
91 166 : case DsoService::Mode::DcCurrent:
92 52 : case DsoService::Mode::AcCurrent:
93 192 : minRangeFunc = minCurrentRange;
94 192 : unit = u"A"_s;
95 52 : sensibleMinimum = 5; // mA.
96 192 : break;
97 624 : }
98 624 : Q_ASSERT(!unit.isEmpty());
99 2304 : rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
100 2304 : if (rangeOptionValue == 0) {
101 142 : errors.append(tr("Invalid range value: %1").arg(value));
102 26 : }
103 1200 : }
104 :
105 : // Parse the trigger-level option.
106 3408 : if (parser.isSet(u"trigger-level"_s)) {
107 208 : float sign = 1.0;
108 1208 : const QString rawValue = parser.value(u"trigger-level"_s);
109 208 : QString absValue = rawValue;
110 208 : DOKIT_STRING_INDEX_TYPE nonSpacePos;
111 768 : for (nonSpacePos = 0; (nonSpacePos < rawValue.length()) && (rawValue.at(nonSpacePos) == u' '); ++nonSpacePos);
112 768 : if ((nonSpacePos < rawValue.length()) && (rawValue.at(nonSpacePos) == u'-')) {
113 0 : absValue = rawValue.mid(nonSpacePos+1);
114 0 : sign = -1.0;
115 0 : }
116 768 : const float level = parseNumber<std::ratio<1>,float>(absValue, unit, 0.f);
117 1040 : qCDebug(lc) << "Trigger level" << rawValue << absValue << nonSpacePos << sign << level;
118 768 : if (qIsNaN(level)) {
119 142 : errors.append(tr("Invalid trigger-level value: %1").arg(rawValue));
120 182 : } else {
121 672 : settings.triggerLevel = sign * level;
122 910 : qCDebug(lc) << "Trigger level" << settings.triggerLevel;
123 : // Check the trigger level is within the Votage / Current range.
124 1162 : if ((rangeOptionValue != 0) && (qAbs(settings.triggerLevel) > (rangeOptionValue/1000.0))) {
125 0 : errors.append(tr("Trigger-level %1%2 is outside range ±%3%2").arg(
126 0 : appendSiPrefix(settings.triggerLevel), unit, appendSiPrefix(rangeOptionValue / 1000.0)));
127 0 : }
128 182 : }
129 400 : }
130 :
131 : // Parse the trigger-mode option.
132 3408 : if (parser.isSet(u"trigger-mode"_s)) {
133 1328 : const QString triggerMode = parser.value(u"trigger-mode"_s).trimmed().toLower();
134 1136 : if (triggerMode.startsWith(u"free"_s)) {
135 288 : settings.command = DsoService::Command::FreeRunning;
136 710 : } else if (triggerMode.startsWith(u"ris"_s)) {
137 192 : settings.command = DsoService::Command::RisingEdgeTrigger;
138 426 : } else if (triggerMode.startsWith(u"fall"_s)) {
139 192 : settings.command = DsoService::Command::FallingEdgeTrigger;
140 52 : } else {
141 184 : errors.append(tr("Unknown trigger mode: %1").arg(parser.value(u"trigger-mode"_s)));
142 26 : }
143 400 : }
144 :
145 : // Ensure that if either trigger option is present, then both are.
146 5088 : if (parser.isSet(u"trigger-level"_s) != parser.isSet(u"trigger-mode"_s)) {
147 192 : errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
148 52 : }
149 :
150 : // Parse the interval option.
151 3408 : if (parser.isSet(u"interval"_s)) {
152 755 : const QString value = parser.value(u"interval"_s);
153 480 : const quint32 interval = parseNumber<std::micro>(value, u"s"_s, (quint32)500'000);
154 480 : if (interval == 0) {
155 284 : errors.append(tr("Invalid interval value: %1").arg(value));
156 78 : } else {
157 288 : settings.samplingWindow = interval;
158 78 : }
159 250 : }
160 :
161 : // Parse the samples option.
162 3408 : if (parser.isSet(u"samples"_s)) {
163 755 : const QString value = parser.value(u"samples"_s);
164 480 : const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
165 480 : if (samples == 0) {
166 284 : errors.append(tr("Invalid samples value: %1").arg(value));
167 288 : } else if (samples > std::numeric_limits<quint16>::max()) {
168 105 : errors.append(tr("Samples value (%1) must be no greater than %2")
169 197 : .arg(value).arg(std::numeric_limits<quint16>::max()));
170 52 : } else {
171 192 : if (samples > 8192) {
172 246 : qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
173 26 : }
174 192 : settings.numberOfSamples = (quint16)samples;
175 52 : }
176 250 : }
177 624 : return errors;
178 1200 : }
179 :
180 : /*!
181 : * \copybrief DeviceCommand::getService
182 : *
183 : * This override returns a pointer to a DsoService object.
184 : */
185 0 : AbstractPokitService * DsoCommand::getService()
186 0 : {
187 0 : Q_ASSERT(device);
188 0 : if (!service) {
189 0 : service = device->dso();
190 0 : Q_ASSERT(service);
191 0 : connect(service, &DsoService::settingsWritten,
192 0 : this, &DsoCommand::settingsWritten);
193 0 : }
194 0 : return service;
195 0 : }
196 :
197 : /*!
198 : * \copybrief DeviceCommand::serviceDetailsDiscovered
199 : *
200 : * This override fetches the current device's status, and outputs it in the selected format.
201 : */
202 0 : void DsoCommand::serviceDetailsDiscovered()
203 0 : {
204 0 : DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
205 0 : settings.range = (minRangeFunc == nullptr) ? 0 : minRangeFunc(*service->pokitProduct(), rangeOptionValue);
206 0 : const QString range = service->toString(settings.range, settings.mode);
207 0 : const QString triggerInfo = (settings.command == DsoService::Command::FreeRunning) ? QString() :
208 0 : tr(", and a %1 at %2%3%4").arg(DsoService::toString(settings.command).toLower(),
209 0 : (settings.triggerLevel < 0.) ? u"-"_s : u""_s, appendSiPrefix(qAbs(settings.triggerLevel)),
210 0 : range.at(range.size()-1));
211 0 : qCInfo(lc).noquote() << tr("Sampling %1, with range %2, %Ln sample/s over %L3us%4.", nullptr, settings.numberOfSamples)
212 0 : .arg(DsoService::toString(settings.mode), (range.isNull()) ? QString::fromLatin1("N/A") : range)
213 0 : .arg(settings.samplingWindow).arg(triggerInfo);
214 0 : service->setSettings(settings);
215 0 : }
216 :
217 : /*!
218 : * \var DsoCommand::minRangeFunc
219 : *
220 : * Pointer to function for converting #rangeOptionValue to a Pokit device's range enumerator. This function pointer
221 : * is assigned during the command line parsing, but is not invoked until after the device's services are discovered,
222 : * because prior to that discovery, we don't know which product (Meter vs Pro vs Clamp, etc) we're talking to and thus
223 : * which enumerator list to be using.
224 : *
225 : * If the current mode does not support ranges (eg diode, and continuity modes), then this member will be \c nullptr.
226 : *
227 : * \see processOptions
228 : * \see serviceDetailsDiscovered
229 : */
230 :
231 : /*!
232 : * Invoked when the DSO settings have been written.
233 : */
234 0 : void DsoCommand::settingsWritten()
235 0 : {
236 0 : Q_ASSERT(service);
237 0 : qCDebug(lc).noquote() << tr("Settings written; DSO has started.");
238 0 : connect(service, &DsoService::metadataRead, this, &DsoCommand::metadataRead);
239 0 : connect(service, &DsoService::samplesRead, this, &DsoCommand::outputSamples);
240 0 : service->enableMetadataNotifications();
241 0 : service->enableReadingNotifications();
242 0 : }
243 :
244 : /*!
245 : * Invoked when \a metadata has been received from the DSO.
246 : */
247 4270 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
248 1671 : {
249 8015 : qCDebug(lc) << "status:" << (int)(data.status);
250 8015 : qCDebug(lc) << "scale:" << data.scale;
251 8015 : qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
252 8015 : qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
253 8015 : qCDebug(lc) << "samplingWindow:" << data.samplingWindow;
254 8015 : qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
255 8015 : qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
256 5941 : this->metadata = data;
257 5941 : this->samplesToGo = data.numberOfSamples;
258 5941 : }
259 :
260 : /*!
261 : * Outputs DSO \a samples in the selected output format.
262 : */
263 5040 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
264 1872 : {
265 5184 : QString unit;
266 6912 : switch (metadata.mode) {
267 2556 : case DsoService::Mode::DcVoltage: unit = u"Vdc"_s; break;
268 2556 : case DsoService::Mode::AcVoltage: unit = u"Vac"_s; break;
269 2556 : case DsoService::Mode::DcCurrent: unit = u"Adc"_s; break;
270 2556 : case DsoService::Mode::AcCurrent: unit = u"Aac"_s; break;
271 0 : default:
272 0 : qCDebug(lc).noquote() << tr(R"(No known unit for mode %1 "%2".)").arg((int)metadata.mode)
273 0 : .arg(DsoService::toString(metadata.mode));
274 1872 : }
275 8640 : const QString range = service->toString(metadata.range, metadata.mode);
276 :
277 37296 : for (const qint16 &sample: samples) {
278 32256 : static int sampleNumber = 0; ++sampleNumber;
279 32256 : const float value = sample * metadata.scale;
280 32256 : switch (format) {
281 2912 : case OutputFormat::Csv:
282 12288 : for (; showCsvHeader; showCsvHeader = false) {
283 2272 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
284 416 : }
285 22400 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
286 2912 : .arg(sampleNumber).arg(value).arg(unit, range));
287 10752 : break;
288 10752 : case OutputFormat::Json:
289 56224 : std::cout << QJsonDocument(QJsonObject{
290 12432 : { u"value"_s, value },
291 12432 : { u"unit"_s, unit },
292 12432 : { u"range"_s, range },
293 18592 : { u"mode"_s, DsoService::toString(metadata.mode) },
294 47600 : }).toJson().toStdString();
295 10752 : break;
296 10752 : case OutputFormat::Text:
297 23296 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
298 10752 : break;
299 8736 : }
300 32256 : --samplesToGo;
301 8736 : }
302 6912 : if (samplesToGo <= 0) {
303 15480 : qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
304 11304 : nullptr, metadata.numberOfSamples).arg(samplesToGo);
305 6912 : if (device) disconnect(); // Will exit the application once disconnected.
306 1872 : }
307 31328 : }
|