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 768 : const QString value = parser.value(u"trigger-level"_s);
108 768 : const quint32 level = parseNumber<std::micro>(value, unit);
109 768 : if (level == 0) {
110 142 : errors.append(tr("Invalid trigger-level value: %1").arg(value));
111 182 : } else {
112 672 : settings.triggerLevel = (float)(level/1'000'000.0);
113 182 : }
114 400 : }
115 :
116 : // Parse the trigger-mode option.
117 3408 : if (parser.isSet(u"trigger-mode"_s)) {
118 1328 : const QString triggerMode = parser.value(u"trigger-mode"_s).trimmed().toLower();
119 1136 : if (triggerMode.startsWith(u"free"_s)) {
120 288 : settings.command = DsoService::Command::FreeRunning;
121 710 : } else if (triggerMode.startsWith(u"ris"_s)) {
122 192 : settings.command = DsoService::Command::RisingEdgeTrigger;
123 426 : } else if (triggerMode.startsWith(u"fall"_s)) {
124 192 : settings.command = DsoService::Command::FallingEdgeTrigger;
125 52 : } else {
126 184 : errors.append(tr("Unknown trigger mode: %1").arg(parser.value(u"trigger-mode"_s)));
127 26 : }
128 400 : }
129 :
130 : // Ensure that if either trigger option is present, then both are.
131 5088 : if (parser.isSet(u"trigger-level"_s) != parser.isSet(u"trigger-mode"_s)) {
132 192 : errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
133 52 : }
134 :
135 : // Parse the interval option.
136 3408 : if (parser.isSet(u"interval"_s)) {
137 755 : const QString value = parser.value(u"interval"_s);
138 480 : const quint32 interval = parseNumber<std::micro>(value, u"s"_s, 500'000);
139 480 : if (interval == 0) {
140 284 : errors.append(tr("Invalid interval value: %1").arg(value));
141 78 : } else {
142 288 : settings.samplingWindow = interval;
143 78 : }
144 250 : }
145 :
146 : // Parse the samples option.
147 3408 : if (parser.isSet(u"samples"_s)) {
148 755 : const QString value = parser.value(u"samples"_s);
149 480 : const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
150 480 : if (samples == 0) {
151 284 : errors.append(tr("Invalid samples value: %1").arg(value));
152 288 : } else if (samples > std::numeric_limits<quint16>::max()) {
153 105 : errors.append(tr("Samples value (%1) must be no greater than %2")
154 197 : .arg(value).arg(std::numeric_limits<quint16>::max()));
155 52 : } else {
156 192 : if (samples > 8192) {
157 246 : qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
158 26 : }
159 192 : settings.numberOfSamples = (quint16)samples;
160 52 : }
161 250 : }
162 624 : return errors;
163 1200 : }
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 4270 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
229 1671 : {
230 8015 : qCDebug(lc) << "status:" << (int)(data.status);
231 8015 : qCDebug(lc) << "scale:" << data.scale;
232 8015 : qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
233 8015 : qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
234 8015 : qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
235 8015 : qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
236 8015 : qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
237 5941 : this->metadata = data;
238 5941 : this->samplesToGo = data.numberOfSamples;
239 5941 : }
240 :
241 : /*!
242 : * Outputs DSO \a samples in the selected output format.
243 : */
244 5040 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
245 1872 : {
246 5184 : QString unit;
247 6912 : switch (metadata.mode) {
248 2556 : case DsoService::Mode::DcVoltage: unit = u"Vdc"_s; break;
249 2556 : case DsoService::Mode::AcVoltage: unit = u"Vac"_s; break;
250 2556 : case DsoService::Mode::DcCurrent: unit = u"Adc"_s; break;
251 2556 : 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 1872 : }
256 8640 : const QString range = service->toString(metadata.range, metadata.mode);
257 :
258 37296 : for (const qint16 &sample: samples) {
259 32256 : static int sampleNumber = 0; ++sampleNumber;
260 32256 : const float value = sample * metadata.scale;
261 32256 : switch (format) {
262 2912 : case OutputFormat::Csv:
263 12288 : for (; showCsvHeader; showCsvHeader = false) {
264 2272 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
265 416 : }
266 22400 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
267 2912 : .arg(sampleNumber).arg(value).arg(unit, range));
268 10752 : break;
269 10752 : case OutputFormat::Json:
270 56224 : std::cout << QJsonDocument(QJsonObject{
271 12432 : { u"value"_s, value },
272 12432 : { u"unit"_s, unit },
273 12432 : { u"range"_s, range },
274 18592 : { u"mode"_s, DsoService::toString(metadata.mode) },
275 47600 : }).toJson().toStdString();
276 10752 : break;
277 10752 : case OutputFormat::Text:
278 23296 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
279 10752 : break;
280 8736 : }
281 32256 : --samplesToGo;
282 8736 : }
283 6912 : if (samplesToGo <= 0) {
284 15480 : qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
285 11304 : nullptr, metadata.numberOfSamples).arg(samplesToGo);
286 6912 : if (device) disconnect(); // Will exit the application once disconnected.
287 1872 : }
288 31328 : }
|