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 "metercommand.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 MeterCommand
18 : *
19 : * The MeterCommand class implements the `meter` CLI command.
20 : */
21 :
22 : /*!
23 : * Construct a new MeterCommand object with \a parent.
24 : */
25 13146 : MeterCommand::MeterCommand(QObject * const parent) : DeviceCommand(parent)
26 5936 : {
27 :
28 13146 : }
29 :
30 5459 : QStringList MeterCommand::requiredOptions(const QCommandLineParser &parser) const
31 6784 : {
32 23585 : return DeviceCommand::requiredOptions(parser) + QStringList{
33 6784 : u"mode"_s,
34 21253 : };
35 6784 : }
36 :
37 2678 : QStringList MeterCommand::supportedOptions(const QCommandLineParser &parser) const
38 3328 : {
39 15366 : return DeviceCommand::supportedOptions(parser) + QStringList{
40 3328 : u"interval"_s,
41 3328 : u"range"_s,
42 3328 : u"samples"_s,
43 15782 : };
44 3328 : }
45 :
46 : /*!
47 : * \copybrief DeviceCommand::processOptions
48 : *
49 : * This implementation extends DeviceCommand::processOptions to process additional CLI options
50 : * supported (or required) by this command.
51 : */
52 2575 : QStringList MeterCommand::processOptions(const QCommandLineParser &parser)
53 3200 : {
54 5775 : QStringList errors = DeviceCommand::processOptions(parser);
55 5775 : if (!errors.isEmpty()) {
56 128 : return errors;
57 128 : }
58 :
59 : // Parse the (required) mode option.
60 8016 : if (const QString mode = parser.value(u"mode"_s).trimmed().toLower();
61 9696 : mode.startsWith(u"ac v"_s) || mode.startsWith(u"vac"_s)) {
62 231 : settings.mode = MultimeterService::Mode::AcVoltage;
63 231 : minRangeFunc = minVoltageRange;
64 9292 : } else if (mode.startsWith(u"dc v"_s) || mode.startsWith(u"vdc"_s)) {
65 2541 : settings.mode = MultimeterService::Mode::DcVoltage;
66 2541 : minRangeFunc = minVoltageRange;
67 4848 : } else if (mode.startsWith(u"ac c"_s) || mode.startsWith(u"aac"_s)) {
68 231 : settings.mode = MultimeterService::Mode::AcCurrent;
69 231 : minRangeFunc = minCurrentRange;
70 4444 : } else if (mode.startsWith(u"dc c"_s) || mode.startsWith(u"adc"_s)) {
71 462 : settings.mode = MultimeterService::Mode::DcCurrent;
72 462 : minRangeFunc = minCurrentRange;
73 2709 : } else if (mode.startsWith(u"res"_s)) {
74 462 : settings.mode = MultimeterService::Mode::Resistance;
75 462 : minRangeFunc = minResistanceRange;
76 2107 : } else if (mode.startsWith(u"dio"_s)) {
77 462 : settings.mode = MultimeterService::Mode::Diode;
78 462 : minRangeFunc = nullptr;
79 1505 : } else if (mode.startsWith(u"cont"_s)) {
80 231 : settings.mode = MultimeterService::Mode::Continuity;
81 231 : minRangeFunc = nullptr;
82 1204 : } else if (mode.startsWith(u"temp"_s)) {
83 231 : settings.mode = MultimeterService::Mode::Temperature;
84 231 : minRangeFunc = nullptr;
85 903 : } else if (mode.startsWith(u"cap"_s)) {
86 462 : settings.mode = MultimeterService::Mode::Capacitance;
87 462 : minRangeFunc = minCapacitanceRange;
88 256 : } else {
89 361 : errors.append(tr("Unknown meter mode: %1").arg(parser.value(u"mode"_s)));
90 128 : return errors;
91 920 : }
92 :
93 : // Parse the interval option.
94 6923 : if (parser.isSet(u"interval"_s)) {
95 1550 : const QString value = parser.value(u"interval"_s);
96 1155 : const quint32 interval = parseNumber<std::milli>(value, u"s"_s, 500);
97 1155 : if (interval == 0) {
98 602 : errors.append(tr("Invalid interval value: %1").arg(value));
99 384 : } else {
100 693 : settings.updateInterval = interval;
101 384 : }
102 805 : }
103 :
104 : // Parse the range option.
105 5313 : rangeOptionValue = 0; // Default to auto.
106 6923 : if (parser.isSet(u"range"_s)) {
107 3410 : const QString value = parser.value(u"range"_s);
108 3311 : if (value.trimmed().compare(u"auto"_s, Qt::CaseInsensitive) != 0) {
109 1617 : switch (settings.mode) {
110 565 : case MultimeterService::Mode::DcVoltage:
111 384 : case MultimeterService::Mode::AcVoltage:
112 693 : rangeOptionValue = parseNumber<std::milli>(value, u"V"_s, 50); // mV.
113 693 : break;
114 334 : case MultimeterService::Mode::DcCurrent:
115 256 : case MultimeterService::Mode::AcCurrent:
116 462 : rangeOptionValue = parseNumber<std::milli>(value, u"A"_s, 5); // mA.
117 462 : break;
118 0 : case MultimeterService::Mode::Resistance:
119 0 : rangeOptionValue = parseNumber<std::ratio<1>>(value, u"ohms"_s);
120 0 : break;
121 231 : case MultimeterService::Mode::Capacitance:
122 231 : rangeOptionValue = parseNumber<std::nano>(value, u"F"_s, (quint32)500); // pF.
123 231 : break;
124 231 : default:
125 459 : qCInfo(lc).noquote() << tr("Ignoring range value: %1").arg(value);
126 896 : }
127 1617 : if ((minRangeFunc != nullptr) && (rangeOptionValue == 0)) {
128 301 : errors.append(tr("Invalid range value: %1").arg(value));
129 128 : }
130 896 : }
131 1771 : }
132 :
133 : // Parse the samples option.
134 6923 : if (parser.isSet(u"samples"_s)) {
135 930 : const QString value = parser.value(u"samples"_s);
136 693 : const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
137 693 : if (samples == 0) {
138 602 : errors.append(tr("Invalid samples value: %1").arg(value));
139 256 : } else {
140 231 : samplesToGo = samples;
141 128 : }
142 483 : }
143 2944 : return errors;
144 2944 : }
145 :
146 : /*!
147 : * \copybrief DeviceCommand::getService
148 : *
149 : * This override returns a pointer to a MultimeterService object.
150 : */
151 0 : AbstractPokitService * MeterCommand::getService()
152 0 : {
153 0 : Q_ASSERT(device);
154 0 : if (!service) {
155 0 : service = device->multimeter();
156 0 : Q_ASSERT(service);
157 0 : connect(service, &MultimeterService::settingsWritten,
158 0 : this, &MeterCommand::settingsWritten);
159 0 : }
160 0 : return service;
161 0 : }
162 :
163 : /*!
164 : * \copybrief DeviceCommand::serviceDetailsDiscovered
165 : *
166 : * This override fetches the current device's status, and outputs it in the selected format.
167 : */
168 0 : void MeterCommand::serviceDetailsDiscovered()
169 0 : {
170 0 : DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
171 0 : settings.range = (minRangeFunc == nullptr) ? 0 : minRangeFunc(*service->pokitProduct(), rangeOptionValue);
172 0 : const QString range = service->toString(settings.range, settings.mode);
173 0 : qCInfo(lc).noquote() << tr("Measuring %1, with range %2, every %L3ms.").arg(
174 0 : MultimeterService::toString(settings.mode),
175 0 : (range.isNull()) ? QString::fromLatin1("N/A") : range).arg(settings.updateInterval);
176 0 : if (!service->enableReadingNotifications()) {
177 0 : qCCritical(lc).noquote() << tr("Failed to enable reading notifications");
178 0 : disconnect(EXIT_FAILURE);
179 0 : return;
180 0 : }
181 0 : service->setSettings(settings);
182 0 : }
183 :
184 : /*!
185 : * \var MeterCommand::minRangeFunc
186 : *
187 : * Pointer to function for converting #rangeOptionValue to a Pokit device's range enumerator. This function pointer
188 : * is assigned during the command line parsing, but is not invoked until after the device's services are discovered,
189 : * because prior to that discovery, we don't know which product (Meter vs Pro vs Clamp, etc) we're talking to and thus
190 : * which enumerator list to be using.
191 : *
192 : * If the current mode does not support ranges (eg diode, and continuity modes), then this member will be \c nullptr.
193 : *
194 : * \see processOptions
195 : * \see serviceDetailsDiscovered
196 : */
197 :
198 : /*!
199 : * Invoked when the multimeter settings have been written, to begin reading the meter values.
200 : */
201 0 : void MeterCommand::settingsWritten()
202 0 : {
203 0 : qCDebug(lc).noquote() << tr("Settings written; starting meter readings...");
204 0 : connect(service, &MultimeterService::readingRead,
205 0 : this, &MeterCommand::outputReading);
206 0 : }
207 :
208 : /*!
209 : * Outputs meter \a reading in the selected output format.
210 : */
211 7725 : void MeterCommand::outputReading(const MultimeterService::Reading &reading)
212 4200 : {
213 9450 : QString status;
214 11925 : if (reading.status == MultimeterService::MeterStatus::Error) {
215 954 : status = u"Error"_s;
216 10971 : } else switch (reading.mode) {
217 504 : case MultimeterService::Mode::Idle:
218 504 : break;
219 4044 : case MultimeterService::Mode::DcVoltage:
220 672 : case MultimeterService::Mode::AcVoltage:
221 1008 : case MultimeterService::Mode::DcCurrent:
222 1344 : case MultimeterService::Mode::AcCurrent:
223 1680 : case MultimeterService::Mode::Resistance:
224 2016 : case MultimeterService::Mode::Capacitance:
225 2016 : status = (reading.status == MultimeterService::MeterStatus::AutoRangeOn)
226 6912 : ? tr("Auto Range On") : tr("Auto Range Off");
227 5724 : break;
228 1908 : case MultimeterService::Mode::Continuity:
229 672 : status = (reading.status == MultimeterService::MeterStatus::Continuity)
230 2304 : ? tr("Continuity") : tr("No continuity");
231 1908 : break;
232 1572 : case MultimeterService::Mode::Temperature:
233 336 : case MultimeterService::Mode::ExternalTemperature:
234 672 : case MultimeterService::Mode::Diode:
235 1236 : status = tr("Ok");
236 1908 : break;
237 3864 : }
238 :
239 11925 : QString unit;
240 11925 : switch (reading.mode) {
241 840 : case MultimeterService::Mode::Idle: break;
242 1374 : case MultimeterService::Mode::DcVoltage: unit = u"Vdc"_s; break;
243 1374 : case MultimeterService::Mode::AcVoltage: unit = u"Vac"_s; break;
244 1374 : case MultimeterService::Mode::DcCurrent: unit = u"Adc"_s; break;
245 1374 : case MultimeterService::Mode::AcCurrent: unit = u"Aac"_s; break;
246 1374 : case MultimeterService::Mode::Resistance: unit = QString::fromUtf8("Ω"); break;
247 336 : case MultimeterService::Mode::Diode: break;
248 672 : case MultimeterService::Mode::Continuity: break;
249 1374 : case MultimeterService::Mode::Temperature: unit = QString::fromUtf8("°C"); break;
250 1374 : case MultimeterService::Mode::Capacitance: unit = QString::fromUtf8("F"); break;
251 0 : case MultimeterService::Mode::ExternalTemperature: unit = QString::fromUtf8("°C"); break;
252 4200 : }
253 :
254 14400 : const QString range = service->toString(reading.range, reading.mode);
255 :
256 11925 : switch (format) {
257 1400 : case OutputFormat::Csv:
258 6201 : for (; showCsvHeader; showCsvHeader = false) {
259 3206 : std::cout << qUtf8Printable(tr("mode,value,unit,status,range\n"));
260 784 : }
261 9800 : std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4,%5\n")
262 1400 : .arg(escapeCsvField(MultimeterService::toString(reading.mode)))
263 1400 : .arg(reading.value, 0, 'f').arg(unit, status, range)
264 1400 : );
265 3975 : break;
266 3975 : case OutputFormat::Json: {
267 1400 : QJsonObject object{
268 4575 : { u"status"_s, status },
269 6175 : { u"value"_s, qIsInf(reading.value) ?
270 4050 : QJsonValue(tr("Infinity")) : QJsonValue(reading.value) },
271 6925 : { u"mode"_s, MultimeterService::toString(reading.mode) },
272 15475 : };
273 3975 : if (!unit.isNull()) {
274 4158 : object.insert(u"unit"_s, unit);
275 784 : }
276 3975 : if (!range.isNull()) {
277 3267 : object.insert(u"range"_s, range);
278 616 : }
279 6550 : std::cout << QJsonDocument(object).toJson().toStdString();
280 3975 : } break;
281 3975 : case OutputFormat::Text:
282 8225 : std::cout << qUtf8Printable(tr("Mode: %1 (0x%2)\n").arg(MultimeterService::toString(reading.mode))
283 1400 : .arg((quint8)reading.mode,2,16,'0'_L1));
284 8800 : std::cout << qUtf8Printable(tr("Value: %1 %2\n").arg(reading.value,0,'f').arg(unit));
285 7475 : std::cout << qUtf8Printable(tr("Status: %1 (0x%2)\n").arg(status)
286 1400 : .arg((quint8)reading.status,2,16,'0'_L1));
287 6825 : std::cout << qUtf8Printable(tr("Range: %1 (0x%2)\n").arg(range)
288 1400 : .arg((quint8)reading.range,2,16,'0'_L1));
289 3975 : break;
290 4200 : }
291 :
292 11925 : if ((samplesToGo > 0) && (--samplesToGo == 0)) {
293 0 : if (device) disconnect(); // Will exit the application once disconnected.
294 0 : }
295 17500 : }
|