Dokit
Internal development documentation
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Macros Pages
metercommand.cpp
1// SPDX-FileCopyrightText: 2022-2025 Paul Colby <git@colby.id.au>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#include "metercommand.h"
6
8
9#include <QJsonDocument>
10#include <QJsonObject>
11
12#include <iostream>
13
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 */
29
31{
33 u"mode"_s,
34 };
35}
36
38{
40 u"interval"_s,
41 u"range"_s,
42 u"samples"_s,
43 };
44}
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 */
53{
55 if (!errors.isEmpty()) {
56 return errors;
57 }
58
59 // Parse the (required) mode option.
60 if (const QString mode = parser.value(u"mode"_s).trimmed().toLower();
61 mode.startsWith(u"ac v"_s) || mode.startsWith(u"vac"_s)) {
64 } else if (mode.startsWith(u"dc v"_s) || mode.startsWith(u"vdc"_s)) {
67 } else if (mode.startsWith(u"ac c"_s) || mode.startsWith(u"aac"_s)) {
70 } else if (mode.startsWith(u"dc c"_s) || mode.startsWith(u"adc"_s)) {
73 } else if (mode.startsWith(u"res"_s)) {
76 } else if (mode.startsWith(u"dio"_s)) {
78 minRangeFunc = nullptr;
79 } else if (mode.startsWith(u"cont"_s)) {
81 minRangeFunc = nullptr;
82 } else if (mode.startsWith(u"temp"_s)) {
84 minRangeFunc = nullptr;
85 } else if (mode.startsWith(u"cap"_s)) {
88 } else {
89 errors.append(tr("Unknown meter mode: %1").arg(parser.value(u"mode"_s)));
90 return errors;
91 }
92
93 // Parse the interval option.
94 if (parser.isSet(u"interval"_s)) {
95 const QString value = parser.value(u"interval"_s);
96 const quint32 interval = parseNumber<std::milli>(value, u"s"_s, 500);
97 if (interval == 0) {
98 errors.append(tr("Invalid interval value: %1").arg(value));
99 } else {
100 settings.updateInterval = interval;
101 }
102 }
103
104 // Parse the range option.
105 rangeOptionValue = 0; // Default to auto.
106 if (parser.isSet(u"range"_s)) {
107 const QString value = parser.value(u"range"_s);
108 if (value.trimmed().compare(u"auto"_s, Qt::CaseInsensitive) != 0) {
109 switch (settings.mode) {
112 rangeOptionValue = parseNumber<std::milli>(value, u"V"_s, 50); // mV.
113 break;
116 rangeOptionValue = parseNumber<std::milli>(value, u"A"_s, 5); // mA.
117 break;
119 rangeOptionValue = parseNumber<std::ratio<1>>(value, u"ohms"_s);
120 break;
122 rangeOptionValue = parseNumber<std::nano>(value, u"F"_s, 500); // pF.
123 break;
124 default:
125 qCInfo(lc).noquote() << tr("Ignoring range value: %1").arg(value);
126 }
127 if ((minRangeFunc != nullptr) && (rangeOptionValue == 0)) {
128 errors.append(tr("Invalid range value: %1").arg(value));
129 }
130 }
131 }
132
133 // Parse the samples option.
134 if (parser.isSet(u"samples"_s)) {
135 const QString value = parser.value(u"samples"_s);
136 const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
137 if (samples == 0) {
138 errors.append(tr("Invalid samples value: %1").arg(value));
139 } else {
140 samplesToGo = samples;
141 }
142 }
143 return errors;
144}
145
146/*!
147 * \copybrief DeviceCommand::getService
148 *
149 * This override returns a pointer to a MultimeterService object.
150 */
152{
153 Q_ASSERT(device);
154 if (!service) {
155 service = device->multimeter();
156 Q_ASSERT(service);
159 }
160 return service;
161}
162
163/*!
164 * \copybrief DeviceCommand::serviceDetailsDiscovered
165 *
166 * This override fetches the current device's status, and outputs it in the selected format.
167 */
169{
170 DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
171 settings.range = (minRangeFunc == nullptr) ? 0 : minRangeFunc(*service->pokitProduct(), rangeOptionValue);
172 const QString range = service->toString(settings.range, settings.mode);
173 qCInfo(lc).noquote() << tr("Measuring %1, with range %2, every %L3ms.").arg(
175 (range.isNull()) ? QString::fromLatin1("N/A") : range).arg(settings.updateInterval);
176 service->setSettings(settings);
177}
178
179/*!
180 * \var MeterCommand::minRangeFunc
181 *
182 * Pointer to function for converting #rangeOptionValue to a Pokit device's range enumerator. This function pointer
183 * is assigned during the command line parsing, but is not invoked until after the device's services are discovered,
184 * because prior to that discovery, we don't know which product (Meter vs Pro vs Clamp, etc) we're talking to and thus
185 * which enumerator list to be using.
186 *
187 * If the current mode does not support ranges (eg diode, and continuity modes), then this member will be \c nullptr.
188 *
189 * \see processOptions
190 * \see serviceDetailsDiscovered
191 */
192
193/*!
194 * Invoked when the multimeter settings have been written, to begin reading the meter values.
195 */
197{
198 qCDebug(lc).noquote() << tr("Settings written; starting meter readings...");
201 service->enableReadingNotifications();
202}
203
204/*!
205 * Outputs meter \a reading in the selected output format.
206 */
208{
209 QString status;
211 status = u"Error"_s;
212 } else switch (reading.mode) {
214 break;
222 ? tr("Auto Range On") : tr("Auto Range Off");
223 break;
226 ? tr("Continuity") : tr("No continuity");
227 break;
231 status = tr("Ok");
232 break;
233 }
234
235 QString unit;
236 switch (reading.mode) {
238 case MultimeterService::Mode::DcVoltage: unit = u"Vdc"_s; break;
239 case MultimeterService::Mode::AcVoltage: unit = u"Vac"_s; break;
240 case MultimeterService::Mode::DcCurrent: unit = u"Adc"_s; break;
241 case MultimeterService::Mode::AcCurrent: unit = u"Aac"_s; break;
248 }
249
250 const QString range = service->toString(reading.range, reading.mode);
251
252 switch (format) {
254 for (; showCsvHeader; showCsvHeader = false) {
255 std::cout << qUtf8Printable(tr("mode,value,unit,status,range\n"));
256 }
257 std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4,%5\n")
259 .arg(reading.value, 0, 'f').arg(unit, status, range)
260 );
261 break;
262 case OutputFormat::Json: {
263 QJsonObject object{
264 { u"status"_s, status },
265 { u"value"_s, qIsInf(reading.value) ?
266 QJsonValue(tr("Infinity")) : QJsonValue(reading.value) },
267 { u"mode"_s, MultimeterService::toString(reading.mode) },
268 };
269 if (!unit.isNull()) {
270 object.insert(u"unit"_s, unit);
271 }
272 if (!range.isNull()) {
273 object.insert(u"range"_s, range);
274 }
275 std::cout << QJsonDocument(object).toJson().toStdString();
276 } break;
278 std::cout << qUtf8Printable(tr("Mode: %1 (0x%2)\n").arg(MultimeterService::toString(reading.mode))
279 .arg((quint8)reading.mode,2,16,'0'_L1));
280 std::cout << qUtf8Printable(tr("Value: %1 %2\n").arg(reading.value,0,'f').arg(unit));
281 std::cout << qUtf8Printable(tr("Status: %1 (0x%2)\n").arg(status)
282 .arg((quint8)reading.status,2,16,'0'_L1));
283 std::cout << qUtf8Printable(tr("Range: %1 (0x%2)\n").arg(range)
284 .arg((quint8)reading.range,2,16,'0'_L1));
285 break;
286 }
287
288 if ((samplesToGo > 0) && (--samplesToGo == 0)) {
289 if (device) disconnect(); // Will exit the application once disconnected.
290 }
291}
virtual QStringList supportedOptions(const QCommandLineParser &parser) const
Returns a list of CLI option names supported by this command.
static quint32 parseNumber(const QString &value, const QString &unit, const quint32 sensibleMinimum=0)
Returns value as an integer multiple of the ratio R.
OutputFormat format
Selected output format.
@ Text
Plain unstructured text.
@ Csv
RFC 4180 compliant CSV text.
@ Json
RFC 8259 compliant JSON text.
virtual QStringList processOptions(const QCommandLineParser &parser)
Processes the relevant options from the command line parser.
static QString escapeCsvField(const QString &field)
Returns an RFC 4180 compliant version of field.
virtual QStringList requiredOptions(const QCommandLineParser &parser) const
Returns a list of CLI option names required by this command.
The AbstractPokitService class provides a common base for Pokit services classes.
static quint8 minResistanceRange(const PokitProduct product, const quint32 maxValue)
Returns the product's lowest resistance range that can measure at least up to maxValue (Ω),...
PokitDevice * device
Pokit Bluetooth device (if any) this command interacts with.
DeviceCommand(QObject *const parent=nullptr)
Construct a new DeviceCommand object with parent.
static quint8 minCapacitanceRange(const PokitProduct product, const quint32 maxValue)
Returns the product's lowest capacitance range that can measure at least up to maxValue (nF),...
virtual void serviceDetailsDiscovered()
Handles service detail discovery events.
static quint8 minVoltageRange(const PokitProduct product, const quint32 maxValue)
t Returns the product's lowest voltage range that can measure at least up to maxValue (mV),...
static quint8 minCurrentRange(const PokitProduct product, const quint32 maxValue)
Returns the product's lowest current range that can measure at least up to maxValue (µA),...
void disconnect(int exitCode=EXIT_SUCCESS)
Disconnects the underlying Pokit device, and sets exitCode to be return to the OS once the disconnect...
QStringList requiredOptions(const QCommandLineParser &parser) const override
Returns a list of CLI option names required by this command.
quint32 rangeOptionValue
The parsed value of range option, if one was supplied.
MultimeterService::Settings settings
< Settings for the Pokit device's multimeter mode.
bool showCsvHeader
Whether or not to show a header as the first line of CSV output.
void outputReading(const MultimeterService::Reading &reading)
Outputs meter reading in the selected output format.
QStringList processOptions(const QCommandLineParser &parser) override
Processes the relevant options from the command line parser.
void settingsWritten()
Invoked when the multimeter settings have been written, to begin reading the meter values.
QStringList supportedOptions(const QCommandLineParser &parser) const override
Returns a list of CLI option names supported by this command.
AbstractPokitService * getService() override
Returns a Pokit service object for the derived command class.
void serviceDetailsDiscovered() override
Handles service detail discovery events.
MeterCommand(QObject *const parent=nullptr)
Construct a new MeterCommand object with parent.
int samplesToGo
Number of samples to read, if specified on the CLI.
quint8(* minRangeFunc)(const PokitProduct product, const quint32 maxValue)
Pointer to function for converting rangeOptionValue to a Pokit device's range enumerator.
MultimeterService * service
Bluetooth service this command interacts with.
void settingsWritten()
This signal is emitted when the Settings characteristic has been written successfully.
@ AutoRangeOn
Auto-range is enabled (voltage, current and resistance modes only).
@ Continuity
Continuity (continuity mode only).
@ DcVoltage
Measure DC voltage.
@ Capacitance
Measure capacitance.
@ AcCurrent
Measure AC current.
@ ExternalTemperature
Measure temperature via an external temperature probe.
@ Resistance
Measure resistance.
@ AcVoltage
Measure AC voltage.
@ Idle
Make device idle.
@ Temperature
Measure temperature.
@ DcCurrent
Measure DC current.
@ Continuity
Measure continuity.
static QString toString(const Mode &mode)
Returns mode as a user-friendly string.
void readingRead(const MultimeterService::Reading &reading)
This signal is emitted when the Reading characteristic has been read successfully.
Declares the PokitDevice class.
std::string toStdString() const const
bool isSet(const QString &name) const const
QString value(const QString &optionName) const const
QByteArray toJson() const const
void append(const T &value)
bool isEmpty() const const
QObject(QObject *parent)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
int compare(const QString &other, Qt::CaseSensitivity cs) const const
QString fromLatin1(const char *str, int size)
QString fromUtf8(const char *str, int size)
bool isNull() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString toLower() const const
QString trimmed() const const
CaseInsensitive
Declares the DOKIT_USE_STRINGLITERALS macro, and related functions.
#define DOKIT_USE_STRINGLITERALS
Internal macro for using either official Qt string literals (added in Qt 6.4), or our own equivalent ...
Attributes included in the Reading characteristic.
MeterStatus status
Current multimeter status.
Mode mode
Current operation mode.
float value
Last acquired value.
quint8 range
Current range.