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