Dokit
Internal development documentation
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Macros Pages
dataloggerservice.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 DataLoggerService and DataLoggerServicePrivate classes.
7 */
8
10#include "dataloggerservice_p.h"
11#include "pokitproducts_p.h"
12#include "../stringliterals_p.h"
13
15
16#include <QDataStream>
17#include <QIODevice>
18#include <QLowEnergyController>
19#include <QtEndian>
20
23
24/*!
25 * \class DataLoggerService
26 *
27 * The DataLoggerService class accesses the `Data Logger` service of Pokit devices.
28 */
29
30/// Returns \a mode as a user-friendly string.
32{
33 switch (mode) {
34 case Mode::Idle: return tr("Idle");
35 case Mode::DcVoltage: return tr("DC voltage");
36 case Mode::AcVoltage: return tr("AC voltage");
37 case Mode::DcCurrent: return tr("DC current");
38 case Mode::AcCurrent: return tr("AC current");
39 case Mode::Temperature: return tr("Temperature");
40 default: return QString();
41 }
42}
43
44/// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
45QString DataLoggerService::toString(const PokitProduct product, const quint8 range, const Mode mode)
46{
47 switch (mode) {
48 case Mode::Idle:
49 break;
50 case Mode::DcVoltage:
51 case Mode::AcVoltage:
52 return VoltageRange::toString(product, range);
53 case Mode::DcCurrent:
54 case Mode::AcCurrent:
55 return CurrentRange::toString(product, range);
57 break;
58 }
59 return QString();
60}
61
62/// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
63QString DataLoggerService::toString(const quint8 range, const Mode mode) const
64{
65 return toString(*pokitProduct(), range, mode);
66}
67
68/*!
69 * Returns the maximum value for \a range, or 0 if \a range is not a known value for \a product's \a mode.
70 */
71quint32 DataLoggerService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
72{
73 switch (mode) {
74 case Mode::Idle:
75 break;
76 case Mode::DcVoltage:
77 case Mode::AcVoltage:
78 return VoltageRange::maxValue(product, range);
79 case Mode::DcCurrent:
80 case Mode::AcCurrent:
81 return CurrentRange::maxValue(product, range);
83 break;
84 }
85 return 0;
86}
87
88/*!
89 * Returns the maximum value for \a range, or 0 \a range is not a known value for the current \a product's \a mode.
90 */
91quint32 DataLoggerService::maxValue(const quint8 range, const Mode mode) const
92{
93 return maxValue(*pokitProduct(), range, mode);
94}
95
96/*!
97 * \typedef DataLoggerService::Samples
98 *
99 * Raw samples from the `Reading` characteristic. These raw samples are (supposedly) within the
100 * range -2048 to +2047, and need to be multiplied by the Metadata::scale value from the `Metadata`
101 * characteristic to get the true values.
102 *
103 * Also supposedly, there should be no more than 10 samples at a time, according to Pokit's current
104 * API docs. There is not artificial limitation imposed by QtPokit, so devices may begin batching
105 * more samples in future. Specifically, the Pokit Pro seems to send 88 samples (in 176 bytes) at a
106 * time.
107 */
108
109/*!
110 * Constructs a new Pokit service with \a parent.
111 */
113 : AbstractPokitService(new DataLoggerServicePrivate(controller, this), parent)
114{
115
116}
117
118/*!
119 * \cond internal
120 * Constructs a new Pokit service with \a parent, and private implementation \a d.
121 */
123 DataLoggerServicePrivate * const d, QObject * const parent)
124 : AbstractPokitService(d, parent)
125{
126
127}
128/// \endcond
129
134
135/*!
136 * Reads the `DataLogger` service's `Metadata` characteristic.
137 *
138 * Returns `true` is the read request is successfully queued, `false` otherwise (ie if the
139 * underlying controller it not yet connected to the Pokit device, or the device's services have
140 * not yet been discovered).
141 *
142 * Emits metadataRead() if/when the characteristic has been read successfully.
143 */
145{
147 return d->readCharacteristic(CharacteristicUuids::metadata);
148}
149
150/*!
151 * Configures the Pokit device's data logger mode.
152 *
153 * Returns `true` if the write request was successfully queued, `false` otherwise.
154 *
155 * Emits settingsWritten() if/when the \a settings have been written successfully.
156 */
158{
159 Q_D(const DataLoggerService);
160 const QLowEnergyCharacteristic characteristic =
161 d->getCharacteristic(CharacteristicUuids::settings);
162 if (!characteristic.isValid()) {
163 return false;
164 }
165
166 const bool updateIntervalIs32bit =
167 (d->getCharacteristic(CharacteristicUuids::metadata).value().size() >= 23);
168 const QByteArray value = DataLoggerServicePrivate::encodeSettings(settings, updateIntervalIs32bit);
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 data logger 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 DataLoggerService::Command::Start.
182 */
184{
185 Q_D(const DataLoggerService);
186 Q_ASSERT(settings.command == DataLoggerService::Command::Start);
188 qCWarning(d->lc).noquote() << tr("Settings command must be 'Start'.");
189 return false;
190 }
191 return setSettings(settings);
192}
193
194/*!
195 * Stop the data logger.
196 *
197 * This is just a convenience function equivalent to calling setSettings() with the command set to
198 * DataLoggerService::Command::Stop.
199 */
201{
202 // Note, only the Settings::command member need be set, since the others are all ignored by the
203 // Pokit device when the command is Stop. However, we still explicitly initialise all other
204 // members just to ensure we're never exposing uninitialised RAM to an external device.
206}
207
208/*!
209 * Start the data logger.
210 *
211 * This is just a convenience function equivalent to calling setSettings() with the command set to
212 * DataLoggerService::Command::Refresh.
213 *
214 * Once the Pokit device has processed this request successfully, the device will begin notifying
215 * the `Metadata` and `Reading` characteristic, resulting in emits of metadataRead and samplesRead
216 * respectively.
217 */
219{
220 // Note, only the Settings::command member need be set, since the others are all ignored by the
221 // Pokit device when the command is Refresh. However, we still explicitly initialise all other
222 // members just to ensure we're never exposing uninitialised RAM to an external device.
224}
225
226/*!
227 * Returns the most recent value of the `DataLogger` service's `Metadata` characteristic.
228 *
229 * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
230 * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then the
231 * returned DataLoggerService::Metadata::scale member will be a quiet NaN, which can be checked like:
232 *
233 * ```
234 * const DataLoggerService::Metadata metadata = multimeterService->metadata();
235 * if (qIsNaN(metadata.scale)) {
236 * // Handle failure.
237 * }
238 * ```
239 */
241{
242 Q_D(const DataLoggerService);
243 const QLowEnergyCharacteristic characteristic =
244 d->getCharacteristic(CharacteristicUuids::metadata);
245 return (characteristic.isValid()) ? DataLoggerServicePrivate::parseMetadata(characteristic.value())
246 : Metadata{ LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
247}
248
249/*!
250 * Enables client-side notifications of Data Logger metadata changes.
251 *
252 * This is an alternative to manually requesting individual reads via readMetadataCharacteristic().
253 *
254 * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
255 *
256 * Successfully read values (if any) will be emitted via the metadataRead() signal.
257 */
259{
261 return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
262}
263
264/*!
265 * Disables client-side notifications of Data Logger metadata changes.
266 *
267 * Instantaneous reads can still be fetched by readMetadataCharacteristic().
268 *
269 * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
270 */
272{
274 return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
275}
276
277/*!
278 * Enables client-side notifications of Data Logger readings.
279 *
280 * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
281 *
282 * Successfully read samples (if any) will be emitted via the samplesRead() signal.
283 */
285{
287 return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
288}
289
290/*!
291 * Disables client-side notifications of Data Logger readings.
292 *
293 * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
294 */
296{
298 return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
299}
300
301/*!
302 * \fn DataLoggerService::settingsWritten
303 *
304 * This signal is emitted when the `Settings` characteristic has been written successfully.
305 *
306 * \see setSettings
307 */
308
309/*!
310 * \fn DataLoggerService::metadataRead
311 *
312 * This signal is emitted when the `Metadata` characteristic has been read successfully.
313 *
314 * \see readMetadataCharacteristic
315 */
316
317/*!
318 * \fn DataLoggerService::samplesRead
319 *
320 * This signal is emitted when the `Reading` characteristic has been notified.
321 *
322 * \see beginSampling
323 * \see stopSampling
324 */
325
326
327/*!
328 * \cond internal
329 * \class DataLoggerServicePrivate
330 *
331 * The DataLoggerServicePrivate class provides private implementation for DataLoggerService.
332 */
333
334/*!
335 * \internal
336 * Constructs a new DataLoggerServicePrivate object with public implementation \a q.
337 */
344
345/*!
346 * Returns \a settings in the format Pokit devices expect. If \a updateIntervalIs32bit is \c true
347 * then the `Update Interval` field will be encoded in 32-bit instead of 16.
348 */
350 const bool updateIntervalIs32bit)
351{
352 static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
353 static_assert(sizeof(settings.arguments) == 2, "Expected to be 2 bytes.");
354 static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
355 static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
356 static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
357 static_assert(sizeof(settings.timestamp) == 4, "Expected to be 4 bytes.");
358
359 QByteArray value;
360 QDataStream stream(&value, QIODevice::WriteOnly);
362 stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
363 stream << (quint8)settings.command << settings.arguments << (quint8)settings.mode << settings.range;
364
365 /*!
366 * \pokitApi For Pokit Meter, `updateInterval` is `uint16` seconds (as per the Pokit API 1.00),
367 * however for Pokit Pro it's `uint32` milliseconds, even though that's not officially
368 * documented anywhere.
369 */
370
371 if (!updateIntervalIs32bit) {
372 stream << (quint16)((settings.updateInterval+500)/1000) << settings.timestamp;
373 Q_ASSERT(value.size() == 11); // According to Pokit API 1.00.
374 } else {
375 stream << settings.updateInterval << settings.timestamp;
376 Q_ASSERT(value.size() == 13); // According to testing / experimentation.
377 }
378 return value;
379}
380
381/*!
382 * Parses the `Metadata` \a value into a DataLoggerService::Metatdata struct.
383 */
385{
387 DataLoggerService::LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(),
389 };
390
391 // Pokit Meter: 15 bytes, Pokit Pro: 23 bytes.
392 if (!checkSize(u"Metadata"_s, value, 15, 23)) {
393 return metadata;
394 }
395
396 qCDebug(lc) << value.mid(7,12).toHex(',');
397 metadata.status = static_cast<DataLoggerService::LoggerStatus>(value.at(0));
398 metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
399 metadata.mode = static_cast<DataLoggerService::Mode>(value.at(5));
400 metadata.range = static_cast<quint8>(value.at(6));
401
402 /*!
403 * \pokitApi For Pokit Meter, `updateInterval` is `uint16` (as per the Pokit API 1.00), however
404 * for Pokit Pro it's `uint32`, even though that's not officially documented anywhere.
405 * Also note, the doc claims 'microseconds' (ie 10^-6), but clearly the value is 'milliseconds'
406 * (ie 10^-3) for Pokit Pro, and whole seconds for Pokit Meter.
407 */
408
409 if (value.size() == 15) {
410 metadata.updateInterval = qFromLittleEndian<quint16>(value.mid(7,2).constData())*1000;
411 metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(9,2).constData());
412 metadata.timestamp = qFromLittleEndian<quint32>(value.mid(11,4).constData());
413 } else if (value.size() == 23) {
414 metadata.updateInterval = qFromLittleEndian<quint32>(value.mid(7,4).constData());
415 metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
416 metadata.timestamp = qFromLittleEndian<quint32>(value.mid(19,4).constData());
417 } else {
418 qCWarning(lc).noquote() << tr("Cannot decode metadata of %n byte/s: %1", nullptr, value.size())
419 .arg(toHexString(value));
420 }
421 return metadata;
422}
423
424/*!
425 * Parses the `Reading` \a value into a DataLoggerService::Samples vector.
426 */
428{
430 if ((value.size()%2) != 0) {
431 qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
432 .arg(value.size()).arg(toHexString(value));
433 return samples;
434 }
435 while ((samples.size()*2) < value.size()) {
436 samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
437 }
438 qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
439 return samples;
440}
441
442/*!
443 * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
444 * specialised signal, for each supported \a characteristic.
445 */
447 const QByteArray &value)
448{
450
452 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
453 << serviceUuid << characteristic.name() << characteristic.uuid();
454 return;
455 }
456
459 Q_EMIT q->metadataRead(parseMetadata(value));
460 return;
461 }
462
464 qCWarning(lc).noquote() << tr("Reading characteristic is notify-only")
465 << serviceUuid << characteristic.name() << characteristic.uuid();
466 return;
467 }
468
469 qCWarning(lc).noquote() << tr("Unknown characteristic read for Data Logger service")
470 << serviceUuid << characteristic.name() << characteristic.uuid();
471}
472
473/*!
474 * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
475 * specialised signal, for each supported \a characteristic.
476 */
478 const QByteArray &newValue)
479{
481
484 Q_EMIT q->settingsWritten();
485 return;
486 }
487
489 qCWarning(lc).noquote() << tr("Metadata characteristic is read/notify, but somehow written")
490 << serviceUuid << characteristic.name() << characteristic.uuid();
491 return;
492 }
493
495 qCWarning(lc).noquote() << tr("Reading characteristic is notify-only, but somehow written")
496 << serviceUuid << characteristic.name() << characteristic.uuid();
497 return;
498 }
499
500 qCWarning(lc).noquote() << tr("Unknown characteristic written for Data Logger service")
501 << serviceUuid << characteristic.name() << characteristic.uuid();
502}
503
504/*!
505 * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
506 * specialised signal, for each supported \a characteristic.
507 */
509 const QByteArray &newValue)
510{
512
515 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
516 << serviceUuid << characteristic.name() << characteristic.uuid();
517 return;
518 }
519
521 Q_EMIT q->metadataRead(parseMetadata(newValue));
522 return;
523 }
524
526 Q_EMIT q->samplesRead(parseSamples(newValue));
527 return;
528 }
529
530 qCWarning(lc).noquote() << tr("Unknown characteristic notified for Data Logger service")
531 << serviceUuid << characteristic.name() << characteristic.uuid();
532}
533
534/// \endcond
535
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 DataLoggerServicePrivate class provides private implementation for DataLoggerService.
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...
static QByteArray encodeSettings(const DataLoggerService::Settings &settings, const bool updateIntervalIs32bit)
Returns settings in the format Pokit devices expect.
void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) override
Implements AbstractPokitServicePrivate::characteristicRead to parse value, then emit a specialised si...
static DataLoggerService::Samples parseSamples(const QByteArray &value)
Parses the Reading value into a DataLoggerService::Samples vector.
static DataLoggerService::Metadata parseMetadata(const QByteArray &value)
Parses the Metadata value into a DataLoggerService::Metatdata struct.
DataLoggerServicePrivate(QLowEnergyController *controller, DataLoggerService *const q)
The DataLoggerService class accesses the Data Logger service of Pokit devices.
bool readMetadataCharacteristic()
Reads the DataLogger service's Metadata characteristic.
bool enableMetadataNotifications()
Enables client-side notifications of Data Logger metadata changes.
LoggerStatus
Values supported by the Status attribute of the Metadata characteristic.
@ Error
An error has occurred.
DataLoggerService(QLowEnergyController *const pokitDevice, QObject *parent=nullptr)
Constructs a new Pokit service with parent.
bool disableMetadataNotifications()
Disables client-side notifications of Data Logger metadata changes.
bool enableReadingNotifications()
Enables client-side notifications of Data Logger readings.
bool setSettings(const Settings &settings)
Configures the Pokit device's data logger mode.
bool startLogger(const Settings &settings)
Start the data logger with settings.
QVector< qint16 > Samples
Raw samples from the Reading characteristic.
static QString toString(const Mode &mode)
Returns mode as a user-friendly string.
bool fetchSamples()
Start the data logger.
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.
bool readCharacteristics() override
Read all characteristics.
@ Stop
Stop the Data Logger.
@ Refresh
Refresh the Data Logger.
@ Start
Start the Data Logger.
bool disableReadingNotifications()
Disables client-side notifications of Data Logger readings.
bool stopLogger()
Stop the data logger.
Metadata metadata() const
Returns the most recent value of the DataLogger service's Metadata characteristic.
Mode
Values supported by the Mode attribute of the Settings and Metadata characteristics.
@ DcVoltage
Measure DC voltage.
@ AcCurrent
Measure AC current.
@ AcVoltage
Measure AC voltage.
@ Idle
Make device idle.
@ Temperature
Measure temperature.
@ DcCurrent
Measure DC current.
Declares the DataLoggerService class.
Declares the DataLoggerServicePrivate 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.
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
QByteArray toHex() 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 StatusService class.
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 DataLogger service's Metadata characteristic.
static const QBluetoothUuid settings
UUID of the DataLogger service's Settings characteristic.
static const QBluetoothUuid reading
UUID of the DataLogger service's Reading characteristic.
Attributes included in the Metadata characteristic.
Attributes included in the Settings characteristic.
quint32 timestamp
Custom timestamp for start time in retrieved metadata.
quint16 arguments
Reserved to used along with command in future.
Command command
Custom operation request.
Mode mode
Desired operation mode.
quint32 updateInterval
Desired update interval in milliseconds.