Dokit
Internal development documentation
Loading...
Searching...
No Matches
dsoservice.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2022-2026 Paul Colby <git@colby.id.au>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4/*!
5 * \file
6 * Defines the DsoService and DsoServicePrivate classes.
7 */
8
10#include <qtpokit/pokitmeter.h>
11#include <qtpokit/pokitpro.h>
12#include "dsoservice_p.h"
13#include "pokitproducts_p.h"
14#include "../stringliterals_p.h"
15
16#include <QDataStream>
17#include <QIODevice>
18#include <QtEndian>
19
22
23/*!
24 * \class DsoService
25 *
26 * The DsoService class accesses the `DSO` (Digital Storage Oscilloscope) service of Pokit devices.
27 */
28
29/// Returns \a command as a user-friendly string.
31{
32 switch (command) {
33 case Command::FreeRunning: return tr("Free running");
34 case Command::RisingEdgeTrigger: return tr("Rising edge trigger");
35 case Command::FallingEdgeTrigger: return tr("Falling edge trigger");
36 case Command::ResendData: return tr("Resend data");
37 default: return QString();
38 }
39}
40
41/// Returns \a mode as a user-friendly string.
43{
44 switch (mode) {
45 case Mode::Idle: return tr("Idle");
46 case Mode::DcVoltage: return tr("DC voltage");
47 case Mode::AcVoltage: return tr("AC voltage");
48 case Mode::DcCurrent: return tr("DC current");
49 case Mode::AcCurrent: return tr("AC current");
50 default: return QString();
51 }
52}
53
54/// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
55QString DsoService::toString(const PokitProduct product, const quint8 range, const Mode mode)
56{
57 switch (mode) {
58 case Mode::Idle:
59 break;
60 case Mode::DcVoltage:
61 case Mode::AcVoltage:
62 return VoltageRange::toString(product, range);
63 case Mode::DcCurrent:
64 case Mode::AcCurrent:
65 return CurrentRange::toString(product, range);
66 }
67 return QString();
68}
69
70/// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
71QString DsoService::toString(const quint8 range, const Mode mode) const
72{
73 return toString(*pokitProduct(), range, mode);
74}
75
76/*!
77 * Returns the maximum value for \a range, or 0 if \a range is not a known value for \a product's \a mode.
78 */
79quint32 DsoService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
80{
81 switch (mode) {
82 case Mode::Idle:
83 break;
84 case Mode::DcVoltage:
85 case Mode::AcVoltage:
86 return VoltageRange::maxValue(product, range);
87 case Mode::DcCurrent:
88 case Mode::AcCurrent:
89 return CurrentRange::maxValue(product, range);
90 }
91 return 0;
92}
93
94/*!
95 * Returns the maximum value for \a range, or 0 \a range is not a known value for the current \a product's \a mode.
96 */
97quint32 DsoService::maxValue(const quint8 range, const Mode mode) const
98{
99 return maxValue(*pokitProduct(), range, mode);
100}
101
102/*!
103 * \typedef DsoService::Samples
104 *
105 * Raw samples from the `Reading` characteristic. These raw samples are (supposedly) within the
106 * range -2048 to +2047, and need to be multiplied by the Metadata::scale value from the `Metadata`
107 * characteristic to get the true values.
108 *
109 * Also supposedly, there should be no more than 10 samples at a time, according to Pokit's current
110 * API docs. There is not artificial limitation imposed by QtPokit, so devices may begin batching
111 * more samples in future.
112 */
113
114/*!
115 * Constructs a new Pokit service with \a parent.
116 */
118 : AbstractPokitService(new DsoServicePrivate(controller, this), parent)
119{
120
121}
122
123/*!
124 * \cond internal
125 * Constructs a new Pokit service with \a parent, and private implementation \a d.
126 */
128 DsoServicePrivate * const d, QObject * const parent)
129 : AbstractPokitService(d, parent)
130{
131
132}
133/// \endcond
134
139
140/*!
141 * Reads the `DSO` service's `Metadata` characteristic.
142 *
143 * Returns `true` is the read request is successfully queued, `false` otherwise (ie if the
144 * underlying controller it not yet connected to the Pokit device, or the device's services have
145 * not yet been discovered).
146 *
147 * Emits metadataRead() if/when the characteristic has been read successfully.
148 */
150{
151 Q_D(DsoService);
152 return d->readCharacteristic(CharacteristicUuids::metadata);
153}
154
155/*!
156 * Configures the Pokit device's DSO mode.
157 *
158 * Note, Pokit devices do not allow AutoRange in DSO service, so \a settings.range should not be \c AutoRange.
159 *
160 * Returns `true` if the write request was successfully queued, `false` otherwise.
161 *
162 * Emits settingsWritten() if/when the \a settings have been written successfully.
163 */
165{
166 Q_ASSERT_X(settings.range != 255, "DsoService::setSettings", "Pokit devices do not allow AutoRange in DSO service");
167
168 Q_D(const DsoService);
169 const QLowEnergyCharacteristic characteristic =
170 d->getCharacteristic(CharacteristicUuids::settings);
171 if (!characteristic.isValid()) {
172 return false;
173 }
174
175 const QByteArray value = DsoServicePrivate::encodeSettings(settings);
176 if (value.isNull()) {
177 return false;
178 }
179
180 d->service->writeCharacteristic(characteristic, value);
181 return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
182}
183
184/*!
185 * Start the DSO with \a settings.
186 *
187 * This is just a synonym for setSettings() except makes the caller's intention more explicit, and
188 * sanity-checks that the settings's command is not DsoService::Command::ResendData.
189 */
190bool DsoService::startDso(const Settings &settings)
191{
192 Q_D(const DsoService);
193 Q_ASSERT(settings.command != DsoService::Command::ResendData);
195 qCWarning(d->lc).noquote() << tr("Settings command must not be 'ResendData'.");
196 return false;
197 }
198 return setSettings(settings);
199}
200
201/*!
202 * Fetch DSO samples.
203 *
204 * This is just a convenience function equivalent to calling setSettings() with the command set to
205 * DsoService::Command::Refresh.
206 *
207 * Once the Pokit device has processed this request successfully, the device will begin notifying
208 * the `Metadata` and `Reading` characteristic, resulting in emits of metadataRead and samplesRead
209 * respectively.
210 */
212{
213 // Note, only the Settings::command member need be set, since the others are all ignored by the
214 // Pokit device when the command is Refresh. However, we still explicitly initialise all other
215 // members just to ensure we're never exposing uninitialised RAM to an external device.
217}
218
219/*!
220 * Returns the most recent value of the `DSO` service's `Metadata` characteristic.
221 *
222 * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
223 * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then the
224 * returned DsoService::Metadata::scale member will be a quiet NaN, which can be checked like:
225 *
226 * ```
227 * const DsoService::Metadata metadata = multimeterService->metadata();
228 * if (qIsNaN(metadata.scale)) {
229 * // Handle failure.
230 * }
231 * ```
232 */
234{
235 Q_D(const DsoService);
236 const QLowEnergyCharacteristic characteristic =
237 d->getCharacteristic(CharacteristicUuids::metadata);
238 return (characteristic.isValid()) ? DsoServicePrivate::parseMetadata(characteristic.value())
239 : Metadata{ DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
240}
241
242/*!
243 * Enables client-side notifications of DSO metadata changes.
244 *
245 * This is an alternative to manually requesting individual reads via readMetadataCharacteristic().
246 *
247 * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
248 *
249 * Successfully read values (if any) will be emitted via the metadataRead() signal.
250 */
252{
253 Q_D(DsoService);
254 return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
255}
256
257/*!
258 * Disables client-side notifications of DSO metadata changes.
259 *
260 * Instantaneous reads can still be fetched by readMetadataCharacteristic().
261 *
262 * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
263 */
265{
266 Q_D(DsoService);
267 return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
268}
269
270/*!
271 * Enables client-side notifications of DSO readings.
272 *
273 * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
274 *
275 * Successfully read samples (if any) will be emitted via the samplesRead() signal.
276 */
278{
279 Q_D(DsoService);
280 return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
281}
282
283/*!
284 * Disables client-side notifications of DSO readings.
285 *
286 * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
287 */
289{
290 Q_D(DsoService);
291 return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
292}
293
294/*!
295 * \fn DsoService::settingsWritten
296 *
297 * This signal is emitted when the `Settings` characteristic has been written successfully.
298 *
299 * \see setSettings
300 */
301
302/*!
303 * \fn DsoService::metadataRead
304 *
305 * This signal is emitted when the `Metadata` characteristic has been read successfully.
306 *
307 * \see readMetadataCharacteristic
308 */
309
310/*!
311 * \fn DsoService::samplesRead
312 *
313 * This signal is emitted when the `Reading` characteristic has been notified.
314 *
315 * \see beginSampling
316 * \see stopSampling
317 */
318
319
320/*!
321 * \cond internal
322 * \class DsoServicePrivate
323 *
324 * The DsoServicePrivate class provides private implementation for DsoService.
325 */
326
327/*!
328 * \internal
329 * Constructs a new DsoServicePrivate object with public implementation \a q.
330 */
337
338/*!
339 * Returns \a settings in the format Pokit devices expect.
340 */
342{
343 static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
344 static_assert(sizeof(settings.triggerLevel) == 4, "Expected to be 2 bytes.");
345 static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
346 static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
347 static_assert(sizeof(settings.samplingWindow) == 4, "Expected to be 4 bytes.");
348 static_assert(sizeof(settings.numberOfSamples) == 2, "Expected to be 2 bytes.");
349
350 QByteArray value;
351 QDataStream stream(&value, QIODevice::WriteOnly);
353 stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
354 stream << (quint8)settings.command << settings.triggerLevel << (quint8)settings.mode
355 << settings.range << settings.samplingWindow << settings.numberOfSamples;
356
357 Q_ASSERT(value.size() == 13);
358 return value;
359}
360
361/*!
362 * Parses the `Metadata` \a value into a DsoService::Metatdata struct.
363 */
365{
366 DsoService::Metadata metadata{
367 DsoService::DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(),
368 DsoService::Mode::Idle, 0, 0, 0, 0
369 };
370
371 if (!checkSize(u"Metadata"_s, value, 17, 17)) {
372 return metadata;
373 }
374
375 metadata.status = static_cast<DsoService::DsoStatus>(value.at(0));
376 metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
377 metadata.mode = static_cast<DsoService::Mode>(value.at(5));
378 metadata.range = static_cast<quint8>(value.at(6));
379 metadata.samplingWindow = qFromLittleEndian<quint32>(value.mid(7,4).constData());
380 metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
381 metadata.samplingRate = qFromLittleEndian<quint32>(value.mid(13,4).constData());
382 return metadata;
383}
384
385/*!
386 * Parses the `Reading` \a value into a DsoService::Samples vector.
387 */
389{
390 DsoService::Samples samples;
391 if ((value.size()%2) != 0) {
392 qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
393 .arg(value.size()).arg(toHexString(value));
394 return samples;
395 }
396 while ((samples.size()*2) < value.size()) {
397 samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
398 }
399 qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
400 return samples;
401}
402
403/*!
404 * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
405 * specialised signal, for each supported \a characteristic.
406 */
408 const QByteArray &value)
409{
411
412 if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
413 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
414 << serviceUuid << characteristic.name() << characteristic.uuid();
415 return;
416 }
417
418 Q_Q(DsoService);
419 if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
420 Q_EMIT q->metadataRead(parseMetadata(value));
421 return;
422 }
423
424 if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
425 qCWarning(lc).noquote() << tr("Reading characteristic is notify-only")
426 << serviceUuid << characteristic.name() << characteristic.uuid();
427 return;
428 }
429
430 qCWarning(lc).noquote() << tr("Unknown characteristic read for DSO service")
431 << serviceUuid << characteristic.name() << characteristic.uuid();
432}
433
434/*!
435 * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
436 * specialised signal, for each supported \a characteristic.
437 */
439 const QByteArray &newValue)
440{
442
443 Q_Q(DsoService);
444 if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
445 Q_EMIT q->settingsWritten();
446 return;
447 }
448
449 if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
450 qCWarning(lc).noquote() << tr("Metadata characteristic is read/notify, but somehow written")
451 << serviceUuid << characteristic.name() << characteristic.uuid();
452 return;
453 }
454
455 if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
456 qCWarning(lc).noquote() << tr("Reading characteristic is notify-only, but somehow written")
457 << serviceUuid << characteristic.name() << characteristic.uuid();
458 return;
459 }
460
461 qCWarning(lc).noquote() << tr("Unknown characteristic written for DSO service")
462 << serviceUuid << characteristic.name() << characteristic.uuid();
463}
464
465/*!
466 * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
467 * specialised signal, for each supported \a characteristic.
468 */
470 const QByteArray &newValue)
471{
473
474 Q_Q(DsoService);
475 if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
476 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
477 << serviceUuid << characteristic.name() << characteristic.uuid();
478 return;
479 }
480
481 if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
482 Q_EMIT q->metadataRead(parseMetadata(newValue));
483 return;
484 }
485
486 if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
487 Q_EMIT q->samplesRead(parseSamples(newValue));
488 return;
489 }
490
491 qCWarning(lc).noquote() << tr("Unknown characteristic notified for DSO service")
492 << serviceUuid << characteristic.name() << characteristic.uuid();
493}
494
495/// \endcond
496
QBluetoothUuid serviceUuid
UUIDs for service.
virtual void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicChanged events.
AbstractPokitServicePrivate(const QBluetoothUuid &serviceUuid, QLowEnergyController *controller, AbstractPokitService *const q)
virtual void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
Handles QLowEnergyService::characteristicRead events.
virtual void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicWritten events.
QLowEnergyController * controller
BLE controller to fetch the service from.
static QString toHexString(const QByteArray &data, const int maxSize=20)
Returns up to maxSize bytes of data as a human readable hexadecimal string.
static bool checkSize(const QString &label, const QByteArray &data, const int minSize, const int maxSize=-1, const bool failOnMax=false)
Returns false if data is smaller than minSize, otherwise returns failOnMax if data is bigger than max...
std::optional< PokitProduct > pokitProduct() const
Returns the Pokit product this service is attached to.
The DsoServicePrivate class provides private implementation for DsoService.
void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) override
Implements AbstractPokitServicePrivate::characteristicRead to parse value, then emit a specialised si...
static DsoService::Samples parseSamples(const QByteArray &value)
Parses the Reading value into a DsoService::Samples vector.
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) override
Implements AbstractPokitServicePrivate::characteristicChanged to parse newValue, then emit a speciali...
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) override
Implements AbstractPokitServicePrivate::characteristicWritten to parse newValue, then emit a speciali...
DsoServicePrivate(QLowEnergyController *controller, DsoService *const q)
static QByteArray encodeSettings(const DsoService::Settings &settings)
Returns settings in the format Pokit devices expect.
static DsoService::Metadata parseMetadata(const QByteArray &value)
Parses the Metadata value into a DsoService::Metatdata struct.
The DsoService class accesses the DSO (Digital Storage Oscilloscope) service of Pokit devices.
Definition dsoservice.h:24
DsoService(QLowEnergyController *const pokitDevice, QObject *parent=nullptr)
Constructs a new Pokit service with parent.
bool disableMetadataNotifications()
Disables client-side notifications of DSO metadata changes.
QVector< qint16 > Samples
Raw samples from the Reading characteristic.
Definition dsoservice.h:95
bool startDso(const Settings &settings)
Start the DSO with settings.
bool setSettings(const Settings &settings)
Configures the Pokit device's DSO mode.
bool fetchSamples()
Fetch DSO samples.
bool enableMetadataNotifications()
Enables client-side notifications of DSO metadata changes.
static quint32 maxValue(const PokitProduct product, const quint8 range, const Mode mode)
Returns the maximum value for range, or 0 if range is not a known value for product's mode.
DsoStatus
Values supported by the Status attribute of the Metadata characteristic.
Definition dsoservice.h:78
@ Error
An error has occurred.
Definition dsoservice.h:81
bool readCharacteristics() override
Read all characteristics.
static QString toString(const Command &command)
Returns command as a user-friendly string.
bool enableReadingNotifications()
Enables client-side notifications of DSO readings.
bool readMetadataCharacteristic()
Reads the DSO service's Metadata characteristic.
Mode
Values supported by the Mode attribute of the Settings and Metadata characteristics.
Definition dsoservice.h:53
@ 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
Command
Values supported by the Command attribute of the Settings characteristic.
Definition dsoservice.h:44
@ ResendData
Resend the last acquired data.
Definition dsoservice.h:48
@ 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
bool disableReadingNotifications()
Disables client-side notifications of DSO readings.
Metadata metadata() const
Returns the most recent value of the DSO service's Metadata characteristic.
Declares the DsoService class.
Declares the DsoServicePrivate class.
QString toString(const PokitProduct product, const quint8 range)
Returns product's current range as a human-friendly string.
quint32 maxValue(const PokitProduct product, const quint8 range)
Returns the maximum value for range in microamps, or 0 if range is not a known value for product.
quint32 maxValue(const PokitProduct product, const quint8 range)
Returns the maximum value for range in millivolts, or 0 if range is not a known value for product.
QString toString(const PokitProduct product, const quint8 range)
Returns product's current range as a human-friendly string.
Declares the PokitMeter namespace.
Declares the PokitPro namespace.
PokitProduct
Pokit products known to, and supported by, the QtPokit library.
char at(int i) const const
const char * constData() const const
bool isNull() const const
QByteArray mid(int pos, int len) const const
int size() const const
void setByteOrder(QDataStream::ByteOrder bo)
void setFloatingPointPrecision(QDataStream::FloatingPointPrecision precision)
bool isValid() const const
QString name() const const
QBluetoothUuid uuid() const const
QByteArray value() const const
QObject(QObject *parent)
Q_EMITQ_EMIT
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
#define QTPOKIT_BEGIN_NAMESPACE
Macro for starting the QtPokit library's top-most namespace (if one is defined).
#define QTPOKIT_END_NAMESPACE
Macro for ending the QtPokit library's top-most namespace (if one is defined).
void append(const T &value)
int size() const const
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 ...
static const QBluetoothUuid metadata
UUID of the DSO service's Metadata characteristic.
Definition dsoservice.h:37
static const QBluetoothUuid reading
UUID of the DSO service's Reading characteristic.
Definition dsoservice.h:40
static const QBluetoothUuid settings
UUID of the DSO service's Settings characteristic.
Definition dsoservice.h:34
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 8,192|16,384 for Pokit Meter|Pro).
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
Attributes included in the Settings characteristic.
Definition dsoservice.h:68
Mode mode
Desired operation mode.
Definition dsoservice.h:71
quint8 range
Desired range, eg settings.range = +PokitPro::CurrentRange::AutoRange;.
Definition dsoservice.h:72
Command command
Custom operation request.
Definition dsoservice.h:69
quint32 samplingWindow
Desired sampling window in microseconds.
Definition dsoservice.h:73
float triggerLevel
Trigger threshold level in Volts or Amps, depending on mode.
Definition dsoservice.h:70
quint16 numberOfSamples
Desired number of samples to acquire.
Definition dsoservice.h:74