Dokit
Internal development documentation
Loading...
Searching...
No Matches
dataloggerservice.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 DataLoggerService and DataLoggerServicePrivate classes.
7 */
8
10#include "dataloggerservice_p.h"
11#include "pokitproducts_p.h"
12
14
15#include <QDataStream>
16#include <QIODevice>
17#include <QLowEnergyController>
18#include <QtEndian>
19
20/*!
21 * \class DataLoggerService
22 *
23 * The DataLoggerService class accesses the `Data Logger` service of Pokit devices.
24 */
25
26/// Returns \a mode as a user-friendly string.
28{
29 switch (mode) {
30 case Mode::Idle: return tr("Idle");
31 case Mode::DcVoltage: return tr("DC voltage");
32 case Mode::AcVoltage: return tr("AC voltage");
33 case Mode::DcCurrent: return tr("DC current");
34 case Mode::AcCurrent: return tr("AC current");
35 case Mode::Temperature: return tr("Temperature");
36 default: return QString();
37 }
38}
39
40/// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
41QString DataLoggerService::toString(const PokitProduct product, const quint8 range, const Mode mode)
42{
43 switch (mode) {
44 case Mode::Idle:
45 break;
46 case Mode::DcVoltage:
47 case Mode::AcVoltage:
48 return VoltageRange::toString(product, range);
49 case Mode::DcCurrent:
50 case Mode::AcCurrent:
51 return CurrentRange::toString(product, range);
53 break;
54 }
55 return QString();
56}
57
58/// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
59QString DataLoggerService::toString(const quint8 range, const Mode mode) const
60{
61 return toString(*pokitProduct(), range, mode);
62}
63
64/*!
65 * Returns the maximum value for \a range, or the string "Auto".
66 *
67 * If \a range is not a known valid enumeration value for \a product's \a mode, then a null QVariant is returned.
68 */
69QVariant DataLoggerService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
70{
71 switch (mode) {
72 case Mode::Idle:
73 break;
74 case Mode::DcVoltage:
75 case Mode::AcVoltage:
76 return VoltageRange::maxValue(product, range);
77 case Mode::DcCurrent:
78 case Mode::AcCurrent:
79 return CurrentRange::maxValue(product, range);
81 break;
82 }
83 return QVariant();
84}
85
86/*!
87 * Returns the maximum value for \a range, or the string "Auto".
88 *
89 * If \a range is not a known valid enumeration value for the current \a product's \a mode,
90 * then a null QVariant is returned.
91 */
92QVariant DataLoggerService::maxValue(const quint8 range, const Mode mode) const
93{
94 return maxValue(*pokitProduct(), range, mode);
95}
96
97/*!
98 * \typedef DataLoggerService::Samples
99 *
100 * Raw samples from the `Reading` characteristic. These raw samples are (supposedly) within the
101 * range -2048 to +2047, and need to be multiplied by the Metadata::scale value from the `Metadata`
102 * characteristc to get the true values.
103 *
104 * Also supposedly, there should be no more than 10 samples at a time, according to Pokit's current
105 * API docs. There is not artificial limitation imposed by QtPokit, so devices may begin batching
106 * more samples in future. Specifically, the Pokit Pro seems to send 88 samples (in 176 bytes) at a
107 * time.
108 */
109
110/*!
111 * Constructs a new Pokit service with \a parent.
112 */
114 : AbstractPokitService(new DataLoggerServicePrivate(controller, this), parent)
115{
116
117}
118
119/*!
120 * \cond internal
121 * Constructs a new Pokit service with \a parent, and private implementation \a d.
122 */
124 DataLoggerServicePrivate * const d, QObject * const parent)
125 : AbstractPokitService(d, parent)
126{
127
128}
129/// \endcond
130
131/*!
132 * Destroys this DataLoggerService object.
133 */
138
143
144/*!
145 * Reads the `DataLogger` service's `Metadata` characteristic.
146 *
147 * Returns `true` is the read request is succesfully queued, `false` otherwise (ie if the
148 * underlying controller it not yet connected to the Pokit device, or the device's services have
149 * not yet been discovered).
150 *
151 * Emits metadataRead() if/when the characteristic has been read successfully.
152 */
154{
156 return d->readCharacteristic(CharacteristicUuids::metadata);
157}
158
159/*!
160 * Configures the Pokit device's data logger mode.
161 *
162 * Returns `true` if the write request was successfully queued, `false` otherwise.
163 *
164 * Emits settingsWritten() if/when the \a settings have been writtem successfully.
165 */
167{
168 Q_D(const DataLoggerService);
169 const QLowEnergyCharacteristic characteristic =
170 d->getCharacteristic(CharacteristicUuids::settings);
171 if (!characteristic.isValid()) {
172 return false;
173 }
174
175 const bool updateIntervalIs32bit =
176 (d->getCharacteristic(CharacteristicUuids::metadata).value().size() >= 23);
177 const QByteArray value = DataLoggerServicePrivate::encodeSettings(settings, updateIntervalIs32bit);
178 if (value.isNull()) {
179 return false;
180 }
181
182 d->service->writeCharacteristic(characteristic, value);
183 return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
184}
185
186/*!
187 * Start the data logger with \a settings.
188 *
189 * This is just a synonym for setSettings() except makes the caller's intention more explicit, and
190 * sanity-checks that the settings's command is DataLoggerService::Command::Start.
191 */
193{
194 Q_D(const DataLoggerService);
195 Q_ASSERT(settings.command == DataLoggerService::Command::Start);
197 qCWarning(d->lc).noquote() << tr("Settings command must be 'Start'.");
198 return false;
199 }
200 return setSettings(settings);
201}
202
203/*!
204 * Stop the data logger.
205 *
206 * This is just a convenience function equivalent to calling setSettings() with the command set to
207 * DataLoggerService::Command::Stop.
208 */
210{
211 // Note, only the Settings::command member need be set, since the others are all ignored by the
212 // Pokit device when the command is Stop. However, we still explicitly initialise all other
213 // members just to ensure we're never exposing uninitialised RAM to an external device.
215}
216
217/*!
218 * Start the data logger.
219 *
220 * This is just a convenience function equivalent to calling setSettings() with the command set to
221 * DataLoggerService::Command::Refresh.
222 *
223 * Once the Pokit device has processed this request succesffully, the device will begin notifying
224 * the `Metadata` and `Reading` characteristic, resulting in emits of metadataRead and samplesRead
225 * respectively.
226 */
228{
229 // Note, only the Settings::command member need be set, since the others are all ignored by the
230 // Pokit device when the command is Refresh. However, we still explicitly initialise all other
231 // members just to ensure we're never exposing uninitialised RAM to an external device.
233}
234
235/*!
236 * Returns the most recent value of the `DataLogger` service's `Metadata` characteristic.
237 *
238 * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
239 * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then the
240 * returned DataLoggerService::Metadata::scale member will be a quiet NaN, which can be checked like:
241 *
242 * ```
243 * const DataLoggerService::Metadata metadata = multimeterService->metadata();
244 * if (qIsNaN(metadata.scale)) {
245 * // Handle failure.
246 * }
247 * ```
248 */
250{
251 Q_D(const DataLoggerService);
252 const QLowEnergyCharacteristic characteristic =
253 d->getCharacteristic(CharacteristicUuids::metadata);
254 return (characteristic.isValid()) ? DataLoggerServicePrivate::parseMetadata(characteristic.value())
255 : Metadata{ LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
256}
257
258/*!
259 * Enables client-side notifications of Data Logger metadata changes.
260 *
261 * This is an alternative to manually requesting individual reads via readMetadataCharacteristic().
262 *
263 * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
264 *
265 * Successfully read values (if any) will be emitted via the metadataRead() signal.
266 */
268{
270 return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
271}
272
273/*!
274 * Disables client-side notifications of Data Logger metadata changes.
275 *
276 * Instantaneous reads can still be fetched by readMetadataCharacteristic().
277 *
278 * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
279 */
281{
283 return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
284}
285
286/*!
287 * Enables client-side notifications of Data Logger readings.
288 *
289 * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
290 *
291 * Successfully read samples (if any) will be emitted via the samplesRead() signal.
292 */
294{
296 return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
297}
298
299/*!
300 * Disables client-side notifications of Data Logger readings.
301 *
302 * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
303 */
305{
307 return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
308}
309
310/*!
311 * \fn DataLoggerService::settingsWritten
312 *
313 * This signal is emitted when the `Settings` characteristic has been written successfully.
314 *
315 * \see setSettings
316 */
317
318/*!
319 * \fn DataLoggerService::metadataRead
320 *
321 * This signal is emitted when the `Metadata` characteristic has been read successfully.
322 *
323 * \see readMetadataCharacteristic
324 */
325
326/*!
327 * \fn DataLoggerService::samplesRead
328 *
329 * This signal is emitted when the `Reading` characteristic has been notified.
330 *
331 * \see beginSampling
332 * \see stopSampling
333 */
334
335
336/*!
337 * \cond internal
338 * \class DataLoggerServicePrivate
339 *
340 * The DataLoggerServicePrivate class provides private implementation for DataLoggerService.
341 */
342
343/*!
344 * \internal
345 * Constructs a new DataLoggerServicePrivate object with public implementation \a q.
346 */
353
354/*!
355 * Returns \a settings in the format Pokit devices expect. If \a updateIntervalIs32bit is \c true
356 * then the `Update Interval` field will be encoded in 32-bit instead of 16.
357 */
359 const bool updateIntervalIs32bit)
360{
361 static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
362 static_assert(sizeof(settings.arguments) == 2, "Expected to be 2 bytes.");
363 static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
364 static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
365 static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
366 static_assert(sizeof(settings.timestamp) == 4, "Expected to be 4 bytes.");
367
368 QByteArray value;
369 QDataStream stream(&value, QIODevice::WriteOnly);
371 stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
372 stream << (quint8)settings.command << settings.arguments << (quint8)settings.mode << settings.range;
373
374 /*!
375 * \pokitApi For Pokit Meter, `updateInterval` is `uint16` seconds (as per the Pokit API 1.00),
376 * however for Pokit Pro it's `uint32` milliseconds, even though that's not officially
377 * documented anywhere.
378 */
379
380 if (!updateIntervalIs32bit) {
381 stream << (quint16)((settings.updateInterval+500)/1000) << settings.timestamp;
382 Q_ASSERT(value.size() == 11); // According to Pokit API 1.00.
383 } else {
384 stream << settings.updateInterval << settings.timestamp;
385 Q_ASSERT(value.size() == 13); // According to testing / experimentation.
386 }
387 return value;
388}
389
390/*!
391 * Parses the `Metadata` \a value into a DataLoggerService::Metatdata struct.
392 */
394{
396 DataLoggerService::LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(),
398 };
399
400 // Pokit Meter: 15 bytes, Pokit Pro: 23 bytes.
401 if (!checkSize(QLatin1String("Metadata"), value, 15, 23)) {
402 return metadata;
403 }
404
405 qCDebug(lc) << value.mid(7,12).toHex(',');
406 metadata.status = static_cast<DataLoggerService::LoggerStatus>(value.at(0));
407 metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
408 metadata.mode = static_cast<DataLoggerService::Mode>(value.at(5));
409 metadata.range = static_cast<quint8>(value.at(6));
410
411 /*!
412 * \pokitApi For Pokit Meter, `updateInterval` is `uint16` (as per the Pokit API 1.00), however
413 * for Pokit Pro it's `uint32`, even though that's not officially documented anywhere.
414 * Also note, the doc claims 'microseconds' (ie 10^-6), but clearly the value is 'milliseconds'
415 * (ie 10^-3) for Pokit Pro, and whole seconds for Pokit Meter.
416 */
417
418 if (value.size() == 15) {
419 metadata.updateInterval = qFromLittleEndian<quint16>(value.mid(7,2).constData())*1000;
420 metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(9,2).constData());
421 metadata.timestamp = qFromLittleEndian<quint32>(value.mid(11,4).constData());
422 } else if (value.size() == 23) {
423 metadata.updateInterval = qFromLittleEndian<quint32>(value.mid(7,4).constData());
424 metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
425 metadata.timestamp = qFromLittleEndian<quint32>(value.mid(19,4).constData());
426 } else {
427 qCWarning(lc).noquote() << tr("Cannot decode metadata of %n byte/s: %1", nullptr, value.size())
428 .arg(toHexString(value));
429 }
430 return metadata;
431}
432
433/*!
434 * Parses the `Reading` \a value into a DataLoggerService::Samples vector.
435 */
437{
439 if ((value.size()%2) != 0) {
440 qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
441 .arg(value.size()).arg(toHexString(value));
442 return samples;
443 }
444 while ((samples.size()*2) < value.size()) {
445 samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
446 }
447 qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
448 return samples;
449}
450
451/*!
452 * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
453 * specialised signal, for each supported \a characteristic.
454 */
456 const QByteArray &value)
457{
459
461 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
462 << serviceUuid << characteristic.name() << characteristic.uuid();
463 return;
464 }
465
468 Q_EMIT q->metadataRead(parseMetadata(value));
469 return;
470 }
471
473 qCWarning(lc).noquote() << tr("Reading characteristic is notify-only")
474 << serviceUuid << characteristic.name() << characteristic.uuid();
475 return;
476 }
477
478 qCWarning(lc).noquote() << tr("Unknown characteristic read for Data Logger service")
479 << serviceUuid << characteristic.name() << characteristic.uuid();
480}
481
482/*!
483 * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
484 * specialised signal, for each supported \a characteristic.
485 */
487 const QByteArray &newValue)
488{
490
493 Q_EMIT q->settingsWritten();
494 return;
495 }
496
498 qCWarning(lc).noquote() << tr("Metadata characteristic is read/notify, but somehow written")
499 << serviceUuid << characteristic.name() << characteristic.uuid();
500 return;
501 }
502
504 qCWarning(lc).noquote() << tr("Reading characteristic is notify-only, but somehow written")
505 << serviceUuid << characteristic.name() << characteristic.uuid();
506 return;
507 }
508
509 qCWarning(lc).noquote() << tr("Unknown characteristic written for Data Logger service")
510 << serviceUuid << characteristic.name() << characteristic.uuid();
511}
512
513/*!
514 * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
515 * specialised signal, for each supported \a characteristic.
516 */
518 const QByteArray &newValue)
519{
521
524 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
525 << serviceUuid << characteristic.name() << characteristic.uuid();
526 return;
527 }
528
530 Q_EMIT q->metadataRead(parseMetadata(newValue));
531 return;
532 }
533
535 Q_EMIT q->samplesRead(parseSamples(newValue));
536 return;
537 }
538
539 qCWarning(lc).noquote() << tr("Unknown characteristic notified for Data Logger service")
540 << serviceUuid << characteristic.name() << characteristic.uuid();
541}
542
543/// \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 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.
static QString toString(const Mode &mode)
Returns mode as a user-friendly string.
bool fetchSamples()
Start the data logger.
bool readCharacteristics() override
Read all characteristics.
QString toString(const quint8 range, const Mode mode) const
Returns range as a user-friendly string, or a null QString if mode has no ranges.
@ 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.
~DataLoggerService() override
Destroys this DataLoggerService object.
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.
static QVariant maxValue(const PokitProduct product, const quint8 range, const Mode mode)
Returns the maximum value for range, or the string "Auto".
Declares the DataLoggerService class.
Declares the DataLoggerServicePrivate 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".
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
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
Declares the StatusService class.
static const QBluetoothUuid metadata
UUID of the DataLogger service's Metadata characterstic.
static const QBluetoothUuid settings
UUID of the DataLogger service's Settings characterstic.
static const QBluetoothUuid reading
UUID of the DataLogger service's Reading characterstic.
Attributes included in the Metadata characterstic.
Attributes included in the Settings characterstic.
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.