Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2024 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 4968 : DsoCommand::DsoCommand(QObject * const parent) : DeviceCommand(parent)
23 1584 : {
24 :
25 5080 : }
26 :
27 2242 : QStringList DsoCommand::requiredOptions(const QCommandLineParser &parser) const
28 1028 : {
29 11412 : return DeviceCommand::requiredOptions(parser) + QStringList{
30 1028 : QLatin1String("mode"),
31 1028 : QLatin1String("range"),
32 8875 : };
33 2326 : }
34 :
35 1102 : QStringList DsoCommand::supportedOptions(const QCommandLineParser &parser) const
36 492 : {
37 7568 : return DeviceCommand::supportedOptions(parser) + QStringList{
38 492 : QLatin1String("interval"),
39 492 : QLatin1String("samples"),
40 492 : QLatin1String("trigger-level"),
41 492 : QLatin1String("trigger-mode"),
42 6553 : };
43 1130 : }
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 1064 : QStringList DsoCommand::processOptions(const QCommandLineParser &parser)
52 448 : {
53 1512 : QStringList errors = DeviceCommand::processOptions(parser);
54 1512 : if (!errors.isEmpty()) {
55 48 : return errors;
56 48 : }
57 :
58 : // Parse the (required) mode option.
59 2300 : if (const QString mode = parser.value(QLatin1String("mode")).trimmed().toLower();
60 1450 : mode.startsWith(QLatin1String("ac v")) || mode.startsWith(QLatin1String("vac"))) {
61 54 : settings.mode = DsoService::Mode::AcVoltage;
62 1392 : } else if (mode.startsWith(QLatin1String("dc v")) || mode.startsWith(QLatin1String("vdc"))) {
63 1134 : settings.mode = DsoService::Mode::DcVoltage;
64 462 : } else if (mode.startsWith(QLatin1String("ac c")) || mode.startsWith(QLatin1String("aac"))) {
65 54 : settings.mode = DsoService::Mode::AcCurrent;
66 116 : } else if (mode.startsWith(QLatin1String("dc c")) || mode.startsWith(QLatin1String("adc"))) {
67 54 : settings.mode = DsoService::Mode::DcCurrent;
68 16 : } else {
69 92 : errors.append(tr("Unknown DSO mode: %1").arg(parser.value(QLatin1String("mode"))));
70 16 : return errors;
71 591 : }
72 :
73 : // Parse the (required) range option.
74 744 : QString unit;
75 384 : {
76 1296 : const QString value = parser.value(QLatin1String("range"));
77 384 : quint32 sensibleMinimum = 0;
78 1296 : 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 1172 : case DsoService::Mode::DcVoltage:
83 352 : case DsoService::Mode::AcVoltage:
84 1188 : minRangeFunc = minVoltageRange;
85 1188 : unit = QLatin1String("V");
86 352 : sensibleMinimum = 50; // mV.
87 1188 : break;
88 92 : case DsoService::Mode::DcCurrent:
89 32 : case DsoService::Mode::AcCurrent:
90 108 : minRangeFunc = minCurrentRange;
91 108 : unit = QLatin1String("A");
92 32 : sensibleMinimum = 5; // mA.
93 108 : break;
94 384 : }
95 384 : Q_ASSERT(!unit.isEmpty());
96 1296 : rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
97 1296 : if (rangeOptionValue == 0) {
98 69 : errors.append(tr("Invalid range value: %1").arg(value));
99 16 : }
100 936 : }
101 :
102 : // Parse the trigger-level option.
103 1656 : if (parser.isSet(QLatin1String("trigger-level"))) {
104 432 : const QString value = parser.value(QLatin1String("trigger-level"));
105 432 : const quint32 level = parseNumber<std::micro>(value, unit);
106 432 : if (level == 0) {
107 69 : errors.append(tr("Invalid trigger-level value: %1").arg(value));
108 112 : } else {
109 378 : settings.triggerLevel = (float)(level/1'000'000.0);
110 112 : }
111 312 : }
112 :
113 : // Parse the trigger-mode option.
114 1656 : if (parser.isSet(QLatin1String("trigger-mode"))) {
115 736 : const QString triggerMode = parser.value(QLatin1String("trigger-mode")).trimmed().toLower();
116 432 : if (triggerMode.startsWith(QLatin1String("free"))) {
117 162 : settings.command = DsoService::Command::FreeRunning;
118 270 : } else if (triggerMode.startsWith(QLatin1String("ris"))) {
119 108 : settings.command = DsoService::Command::RisingEdgeTrigger;
120 162 : } else if (triggerMode.startsWith(QLatin1String("fall"))) {
121 108 : settings.command = DsoService::Command::FallingEdgeTrigger;
122 32 : } else {
123 115 : errors.append(tr("Unknown trigger mode: %1").arg(
124 92 : parser.value(QLatin1String("trigger-mode"))));
125 16 : }
126 312 : }
127 :
128 : // Ensure that if either trigger option is present, then both are.
129 2208 : if (parser.isSet(QLatin1String("trigger-level")) !=
130 1656 : parser.isSet(QLatin1String("trigger-mode"))) {
131 108 : errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
132 32 : }
133 :
134 : // Parse the interval option.
135 1656 : if (parser.isSet(QLatin1String("interval"))) {
136 345 : const QString value = parser.value(QLatin1String("interval"));
137 270 : const quint32 interval = parseNumber<std::micro>(value, QLatin1String("s"), 500'000);
138 270 : if (interval == 0) {
139 138 : errors.append(tr("Invalid interval value: %1").arg(value));
140 48 : } else {
141 162 : settings.samplingWindow = interval;
142 48 : }
143 195 : }
144 :
145 : // Parse the samples option.
146 1656 : if (parser.isSet(QLatin1String("samples"))) {
147 345 : const QString value = parser.value(QLatin1String("samples"));
148 270 : const quint32 samples = parseNumber<std::ratio<1>>(value, QLatin1String("S"));
149 270 : if (samples == 0) {
150 138 : errors.append(tr("Invalid samples value: %1").arg(value));
151 162 : } else if (samples > std::numeric_limits<quint16>::max()) {
152 54 : errors.append(tr("Samples value (%1) must be no greater than %2")
153 107 : .arg(value).arg(std::numeric_limits<quint16>::max()));
154 32 : } else {
155 108 : if (samples > 8192) {
156 115 : qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
157 16 : }
158 108 : settings.numberOfSamples = (quint16)samples;
159 32 : }
160 195 : }
161 384 : return errors;
162 936 : }
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 2318 : void DsoCommand::metadataRead(const DsoService::Metadata &data)
228 1004 : {
229 3810 : qCDebug(lc) << "status:" << (int)(data.status);
230 3810 : qCDebug(lc) << "scale:" << data.scale;
231 3810 : qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
232 3810 : qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
233 3810 : qCDebug(lc) << "samplingWindow:" << (int)data.samplingWindow;
234 3810 : qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
235 3810 : qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
236 3322 : this->metadata = data;
237 3322 : this->samplesToGo = data.numberOfSamples;
238 3322 : }
239 :
240 : /*!
241 : * Outputs DSO \a samples in the selected ouput format.
242 : */
243 2736 : void DsoCommand::outputSamples(const DsoService::Samples &samples)
244 1152 : {
245 2232 : QString unit;
246 3888 : switch (metadata.mode) {
247 972 : case DsoService::Mode::DcVoltage: unit = QLatin1String("Vdc"); break;
248 972 : case DsoService::Mode::AcVoltage: unit = QLatin1String("Vac"); break;
249 972 : case DsoService::Mode::DcCurrent: unit = QLatin1String("Adc"); break;
250 972 : 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 1152 : }
255 5544 : const QString range = service->toString(metadata.range, metadata.mode);
256 :
257 20880 : for (const qint16 &sample: samples) {
258 18144 : static int sampleNumber = 0; ++sampleNumber;
259 18144 : const float value = sample * metadata.scale;
260 18144 : switch (format) {
261 1792 : case OutputFormat::Csv:
262 6912 : for (; showCsvHeader; showCsvHeader = false) {
263 1104 : std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
264 256 : }
265 11984 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
266 1792 : .arg(sampleNumber).arg(value).arg(unit, range));
267 6048 : break;
268 6048 : case OutputFormat::Json:
269 25088 : std::cout << QJsonDocument(QJsonObject{
270 3472 : { QLatin1String("value"), value },
271 3472 : { QLatin1String("unit"), unit },
272 3472 : { QLatin1String("range"), range },
273 8624 : { QLatin1String("mode"), DsoService::toString(metadata.mode) },
274 28336 : }).toJson().toStdString();
275 6048 : break;
276 6048 : case OutputFormat::Text:
277 11984 : std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
278 6048 : break;
279 5376 : }
280 18144 : --samplesToGo;
281 5376 : }
282 3888 : if (samplesToGo <= 0) {
283 8856 : qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
284 6624 : nullptr, metadata.numberOfSamples).arg(samplesToGo);
285 3888 : if (device) disconnect(); // Will exit the application once disconnected.
286 1152 : }
287 22704 : }
|