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