Dokit
Internal development documentation
Loading...
Searching...
No Matches
dsocommand.cpp
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"
6
8
9#include <QJsonDocument>
10#include <QJsonObject>
11
12#include <iostream>
13
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 */
29
31{
33 u"mode"_s,
34 u"range"_s,
35 };
36}
37
39{
41 u"interval"_s,
42 u"samples"_s,
43 u"trigger-level"_s,
44 u"trigger-mode"_s,
45 };
46}
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 */
55{
57 if (!errors.isEmpty()) {
58 return errors;
59 }
60
61 // Parse the (required) mode option.
62 if (const QString mode = parser.value(u"mode"_s).trimmed().toLower();
63 mode.startsWith(u"ac v"_s) || mode.startsWith(u"vac"_s)) {
65 } 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)) {
69 } else if (mode.startsWith(u"dc c"_s) || mode.startsWith(u"adc"_s)) {
71 } else {
72 errors.append(tr("Unknown DSO mode: %1").arg(parser.value(u"mode"_s)));
73 return errors;
74 }
75
76 // Parse the (required) range option.
77 QString unit;
78 {
79 const QString value = parser.value(u"range"_s);
80 quint32 sensibleMinimum = 0;
81 switch (settings.mode) {
83 Q_ASSERT(false); // Not possible, since the mode parsing above never allows Idle.
84 break;
88 unit = u"V"_s;
89 sensibleMinimum = 50; // mV.
90 break;
94 unit = u"A"_s;
95 sensibleMinimum = 5; // mA.
96 break;
97 }
98 Q_ASSERT(!unit.isEmpty());
99 rangeOptionValue = parseNumber<std::milli>(value, unit, sensibleMinimum);
100 if (rangeOptionValue == 0) {
101 errors.append(tr("Invalid range value: %1").arg(value));
102 }
103 }
104
105 // Parse the trigger-level option.
106 if (parser.isSet(u"trigger-level"_s)) {
107 float sign = 1.0;
108 const QString rawValue = parser.value(u"trigger-level"_s);
109 QString absValue = rawValue;
110 DOKIT_STRING_INDEX_TYPE nonSpacePos;
111 for (nonSpacePos = 0; (nonSpacePos < rawValue.length()) && (rawValue.at(nonSpacePos) == u' '); ++nonSpacePos);
112 if ((nonSpacePos < rawValue.length()) && (rawValue.at(nonSpacePos) == u'-')) {
113 absValue = rawValue.mid(nonSpacePos+1);
114 sign = -1.0;
115 }
116 const float level = parseNumber<std::ratio<1>,float>(absValue, unit, 0.f);
117 qCDebug(lc) << "Trigger level" << rawValue << absValue << nonSpacePos << sign << level;
118 if (qIsNaN(level)) {
119 errors.append(tr("Invalid trigger-level value: %1").arg(rawValue));
120 } else {
121 settings.triggerLevel = sign * level;
122 qCDebug(lc) << "Trigger level" << settings.triggerLevel;
123 // Check the trigger level is within the Votage / Current range.
124 if ((rangeOptionValue != 0) && (qAbs(settings.triggerLevel) > (rangeOptionValue/1000.0))) {
125 errors.append(tr("Trigger-level %1%2 is outside range ±%3%2").arg(
126 appendSiPrefix(settings.triggerLevel), unit, appendSiPrefix(rangeOptionValue / 1000.0)));
127 }
128 }
129 }
130
131 // Parse the trigger-mode option.
132 if (parser.isSet(u"trigger-mode"_s)) {
133 const QString triggerMode = parser.value(u"trigger-mode"_s).trimmed().toLower();
134 if (triggerMode.startsWith(u"free"_s)) {
136 } else if (triggerMode.startsWith(u"ris"_s)) {
138 } else if (triggerMode.startsWith(u"fall"_s)) {
140 } else {
141 errors.append(tr("Unknown trigger mode: %1").arg(parser.value(u"trigger-mode"_s)));
142 }
143 }
144
145 // Ensure that if either trigger option is present, then both are.
146 if (parser.isSet(u"trigger-level"_s) != parser.isSet(u"trigger-mode"_s)) {
147 errors.append(tr("If either option is provided, then both must be: trigger-level, trigger-mode"));
148 }
149
150 // Parse the interval option.
151 if (parser.isSet(u"interval"_s)) {
152 const QString value = parser.value(u"interval"_s);
153 const quint32 interval = parseNumber<std::micro>(value, u"s"_s, (quint32)500'000);
154 if (interval == 0) {
155 errors.append(tr("Invalid interval value: %1").arg(value));
156 } else {
157 settings.samplingWindow = interval;
158 }
159 }
160
161 // Parse the samples option.
162 if (parser.isSet(u"samples"_s)) {
163 const QString value = parser.value(u"samples"_s);
164 const quint32 samples = parseNumber<std::ratio<1>>(value, u"S"_s);
165 if (samples == 0) {
166 errors.append(tr("Invalid samples value: %1").arg(value));
167 } else if (samples > std::numeric_limits<quint16>::max()) {
168 errors.append(tr("Samples value (%1) must be no greater than %2")
169 .arg(value).arg(std::numeric_limits<quint16>::max()));
170 } else {
171 if (samples > 8192) {
172 qCWarning(lc).noquote() << tr("Pokit devices do not officially support great than 8192 samples");
173 }
174 settings.numberOfSamples = (quint16)samples;
175 }
176 }
177 return errors;
178}
179
180/*!
181 * \copybrief DeviceCommand::getService
182 *
183 * This override returns a pointer to a DsoService object.
184 */
186{
187 Q_ASSERT(device);
188 if (!service) {
189 service = device->dso();
190 Q_ASSERT(service);
193 }
194 return service;
195}
196
197/*!
198 * \copybrief DeviceCommand::serviceDetailsDiscovered
199 *
200 * This override fetches the current device's status, and outputs it in the selected format.
201 */
203{
204 DeviceCommand::serviceDetailsDiscovered(); // Just logs consistently.
205 settings.range = (minRangeFunc == nullptr) ? 0 : minRangeFunc(*service->pokitProduct(), rangeOptionValue);
206 const QString range = service->toString(settings.range, settings.mode);
207 const QString triggerInfo = (settings.command == DsoService::Command::FreeRunning) ? QString() :
208 tr(", and a %1 at %2%3%4").arg(DsoService::toString(settings.command).toLower(),
209 (settings.triggerLevel < 0.) ? u"-"_s : u""_s, appendSiPrefix(qAbs(settings.triggerLevel)),
210 range.at(range.size()-1));
211 qCInfo(lc).noquote() << tr("Sampling %1, with range %2, %Ln sample/s over %L3us%4.", nullptr, settings.numberOfSamples)
212 .arg(DsoService::toString(settings.mode), (range.isNull()) ? QString::fromLatin1("N/A") : range)
213 .arg(settings.samplingWindow).arg(triggerInfo);
214 service->setSettings(settings);
215}
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 */
235{
236 Q_ASSERT(service);
237 qCDebug(lc).noquote() << tr("Settings written; DSO has started.");
240 service->enableMetadataNotifications();
241 service->enableReadingNotifications();
242}
243
244/*!
245 * Invoked when \a metadata has been received from the DSO.
246 */
248{
249 qCDebug(lc) << "status:" << (int)(data.status);
250 qCDebug(lc) << "scale:" << data.scale;
251 qCDebug(lc) << "mode:" << DsoService::toString(data.mode);
252 qCDebug(lc) << "range:" << service->toString(data.range, data.mode);
253 qCDebug(lc) << "samplingWindow:" << data.samplingWindow;
254 qCDebug(lc) << "numberOfSamples:" << data.numberOfSamples;
255 qCDebug(lc) << "samplingRate:" << data.samplingRate << "Hz";
256 this->metadata = data;
257 this->samplesToGo = data.numberOfSamples;
258}
259
260/*!
261 * Outputs DSO \a samples in the selected output format.
262 */
264{
265 QString unit;
266 switch (metadata.mode) {
267 case DsoService::Mode::DcVoltage: unit = u"Vdc"_s; break;
268 case DsoService::Mode::AcVoltage: unit = u"Vac"_s; break;
269 case DsoService::Mode::DcCurrent: unit = u"Adc"_s; break;
270 case DsoService::Mode::AcCurrent: unit = u"Aac"_s; break;
271 default:
272 qCDebug(lc).noquote() << tr(R"(No known unit for mode %1 "%2".)").arg((int)metadata.mode)
274 }
275 const QString range = service->toString(metadata.range, metadata.mode);
276
277 for (const qint16 &sample: samples) {
278 static int sampleNumber = 0; ++sampleNumber;
279 const float value = sample * metadata.scale;
280 switch (format) {
282 for (; showCsvHeader; showCsvHeader = false) {
283 std::cout << qUtf8Printable(tr("sample_number,value,unit,range\n"));
284 }
285 std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4\n")
286 .arg(sampleNumber).arg(value).arg(unit, range));
287 break;
289 std::cout << QJsonDocument(QJsonObject{
290 { u"value"_s, value },
291 { u"unit"_s, unit },
292 { u"range"_s, range },
293 { u"mode"_s, DsoService::toString(metadata.mode) },
294 }).toJson().toStdString();
295 break;
297 std::cout << qUtf8Printable(tr("%1 %2 %3\n").arg(sampleNumber).arg(value).arg(unit));
298 break;
299 }
300 --samplesToGo;
301 }
302 if (samplesToGo <= 0) {
303 qCInfo(lc).noquote() << tr("Finished fetching %Ln sample/s (with %L2 to remaining).",
304 nullptr, metadata.numberOfSamples).arg(samplesToGo);
305 if (device) disconnect(); // Will exit the application once disconnected.
306 }
307}
virtual QStringList supportedOptions(const QCommandLineParser &parser) const
Returns a list of CLI option names supported by this command.
static QString appendSiPrefix(const double value, int precision=6)
Returns a human-readable version of value, with up-to precision decimal digits, and SI unit prefix wh...
static T parseNumber(const QString &value, const QString &unit, const T sensibleMinimum=0)
Returns value as an integer multiple of the ratio R, as number of type T.
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.
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.
PokitDevice * device
Pokit Bluetooth device (if any) this command interacts with.
DeviceCommand(QObject *const parent=nullptr)
Construct a new DeviceCommand object with parent.
virtual void serviceDetailsDiscovered()
Handles service detail discovery events.
static quint8 minVoltageRange(const PokitProduct product, const quint32 maxValue)
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...
DsoService::Settings settings
Settings for the Pokit device's DSO mode.
Definition dsocommand.h:33
quint8(* minRangeFunc)(const PokitProduct product, const quint32 maxValue)
Pointer to function for converting rangeOptionValue to a Pokit device's range enumerator.
Definition dsocommand.h:30
qint32 samplesToGo
Number of samples we're expecting in the current window.
Definition dsocommand.h:38
bool showCsvHeader
Whether or not to show a header as the first line of CSV output.
Definition dsocommand.h:39
void serviceDetailsDiscovered() override
Handles service detail discovery events.
void outputSamples(const DsoService::Samples &samples)
Outputs DSO samples in the selected output format.
QStringList supportedOptions(const QCommandLineParser &parser) const override
Returns a list of CLI option names supported by this command.
DsoCommand(QObject *const parent=nullptr)
Construct a new DsoCommand object with parent.
DsoService * service
Bluetooth service this command interacts with.
Definition dsocommand.h:32
QStringList processOptions(const QCommandLineParser &parser) override
Processes the relevant options from the command line parser.
void settingsWritten()
Invoked when the DSO settings have been written.
quint32 rangeOptionValue
The parsed value of range option.
Definition dsocommand.h:31
QStringList requiredOptions(const QCommandLineParser &parser) const override
Returns a list of CLI option names required by this command.
AbstractPokitService * getService() override
Returns a Pokit service object for the derived command class.
DsoService::Metadata metadata
Most recent DSO metadata.
Definition dsocommand.h:37
void metadataRead(const DsoService::Metadata &data)
Invoked when metadata has been received from the DSO.
void metadataRead(const DsoService::Metadata &meta)
This signal is emitted when the Metadata characteristic has been read successfully.
QVector< qint16 > Samples
Raw samples from the Reading characteristic.
Definition dsoservice.h:95
void settingsWritten()
This signal is emitted when the Settings characteristic has been written successfully.
static QString toString(const Command &command)
Returns command as a user-friendly string.
@ DcVoltage
Measure DC voltage.
Definition dsoservice.h:55
@ AcCurrent
Measure AC current.
Definition dsoservice.h:58
@ AcVoltage
Measure AC voltage.
Definition dsoservice.h:56
@ Idle
Make device idle.
Definition dsoservice.h:54
@ DcCurrent
Measure DC current.
Definition dsoservice.h:57
@ FreeRunning
Run free, without waiting for edge triggers.
Definition dsoservice.h:45
@ RisingEdgeTrigger
Trigger on a rising edge.
Definition dsoservice.h:46
@ FallingEdgeTrigger
Trigger on a falling edge.
Definition dsoservice.h:47
void samplesRead(const DsoService::Samples &samples)
This signal is emitted when the Reading characteristic has been notified.
Declares the PokitDevice class.
bool isSet(const QString &name) const const
QString value(const QString &optionName) 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
const QChar at(int position) const const
QString fromLatin1(const char *str, int size)
bool isEmpty() const const
bool isNull() const const
int length() const const
QString mid(int position, int n) const const
int size() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString toLower() const const
QString trimmed() const const
Declares the DOKIT_USE_STRINGLITERALS macro, and related functions.
#define DOKIT_STRING_INDEX_TYPE
Internal macro for matching the index type used by QString methods.
#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 Metadata characteristic.
Definition dsoservice.h:85
quint32 samplingRate
Sampling rate used during last acquisition (1 to 1MHz).
Definition dsoservice.h:92
DsoStatus status
Current DSO status.
Definition dsoservice.h:86
float scale
Scale to apply to read samples.
Definition dsoservice.h:87
quint16 numberOfSamples
Number of samples acquired (1 to 8192).
Definition dsoservice.h:91
quint8 range
Range used during last acquisition.
Definition dsoservice.h:89
Mode mode
Operation mode used during last acquisition.
Definition dsoservice.h:88
quint32 samplingWindow
Sampling window (microseconds) used during last acquisition.
Definition dsoservice.h:90