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
135
136/*!
137 * Reads the `DataLogger` service's `Metadata` characteristic.
138 *
139 * Returns `true` is the read request is succesfully queued, `false` otherwise (ie if the
140 * underlying controller it not yet connected to the Pokit device, or the device's services have
141 * not yet been discovered).
142 *
143 * Emits metadataRead() if/when the characteristic has been read successfully.
144 */
146{
148 return d->readCharacteristic(CharacteristicUuids::metadata);
149}
150
151/*!
152 * Configures the Pokit device's data logger mode.
153 *
154 * Returns `true` if the write request was successfully queued, `false` otherwise.
155 *
156 * Emits settingsWritten() if/when the \a settings have been writtem successfully.
157 */
159{
160 Q_D(const DataLoggerService);
161 const QLowEnergyCharacteristic characteristic =
162 d->getCharacteristic(CharacteristicUuids::settings);
163 if (!characteristic.isValid()) {
164 return false;
165 }
166
167 const bool updateIntervalIs32bit =
168 (d->getCharacteristic(CharacteristicUuids::metadata).value().size() >= 23);
169 const QByteArray value = DataLoggerServicePrivate::encodeSettings(settings, updateIntervalIs32bit);
170 if (value.isNull()) {
171 return false;
172 }
173
174 d->service->writeCharacteristic(characteristic, value);
175 return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
176}
177
178/*!
179 * Start the data logger with \a settings.
180 *
181 * This is just a synonym for setSettings() except makes the caller's intention more explicit, and
182 * sanity-checks that the settings's command is DataLoggerService::Command::Start.
183 */
185{
186 Q_D(const DataLoggerService);
187 Q_ASSERT(settings.command == DataLoggerService::Command::Start);
189 qCWarning(d->lc).noquote() << tr("Settings command must be 'Start'.");
190 return false;
191 }
192 return setSettings(settings);
193}
194
195/*!
196 * Stop the data logger.
197 *
198 * This is just a convenience function equivalent to calling setSettings() with the command set to
199 * DataLoggerService::Command::Stop.
200 */
202{
203 // Note, only the Settings::command member need be set, since the others are all ignored by the
204 // Pokit device when the command is Stop. However, we still explicitly initialise all other
205 // members just to ensure we're never exposing uninitialised RAM to an external device.
207}
208
209/*!
210 * Start the data logger.
211 *
212 * This is just a convenience function equivalent to calling setSettings() with the command set to
213 * DataLoggerService::Command::Refresh.
214 *
215 * Once the Pokit device has processed this request succesffully, the device will begin notifying
216 * the `Metadata` and `Reading` characteristic, resulting in emits of metadataRead and samplesRead
217 * respectively.
218 */
220{
221 // Note, only the Settings::command member need be set, since the others are all ignored by the
222 // Pokit device when the command is Refresh. However, we still explicitly initialise all other
223 // members just to ensure we're never exposing uninitialised RAM to an external device.
225}
226
227/*!
228 * Returns the most recent value of the `DataLogger` service's `Metadata` characteristic.
229 *
230 * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
231 * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then the
232 * returned DataLoggerService::Metadata::scale member will be a quiet NaN, which can be checked like:
233 *
234 * ```
235 * const DataLoggerService::Metadata metadata = multimeterService->metadata();
236 * if (qIsNaN(metadata.scale)) {
237 * // Handle failure.
238 * }
239 * ```
240 */
242{
243 Q_D(const DataLoggerService);
244 const QLowEnergyCharacteristic characteristic =
245 d->getCharacteristic(CharacteristicUuids::metadata);
246 return (characteristic.isValid()) ? DataLoggerServicePrivate::parseMetadata(characteristic.value())
247 : Metadata{ LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
248}
249
250/*!
251 * Enables client-side notifications of Data Logger metadata changes.
252 *
253 * This is an alternative to manually requesting individual reads via readMetadataCharacteristic().
254 *
255 * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
256 *
257 * Successfully read values (if any) will be emitted via the metadataRead() signal.
258 */
260{
262 return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
263}
264
265/*!
266 * Disables client-side notifications of Data Logger metadata changes.
267 *
268 * Instantaneous reads can still be fetched by readMetadataCharacteristic().
269 *
270 * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
271 */
273{
275 return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
276}
277
278/*!
279 * Enables client-side notifications of Data Logger readings.
280 *
281 * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
282 *
283 * Successfully read samples (if any) will be emitted via the samplesRead() signal.
284 */
286{
288 return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
289}
290
291/*!
292 * Disables client-side notifications of Data Logger readings.
293 *
294 * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
295 */
297{
299 return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
300}
301
302/*!
303 * \fn DataLoggerService::settingsWritten
304 *
305 * This signal is emitted when the `Settings` characteristic has been written successfully.
306 *
307 * \see setSettings
308 */
309
310/*!
311 * \fn DataLoggerService::metadataRead
312 *
313 * This signal is emitted when the `Metadata` characteristic has been read successfully.
314 *
315 * \see readMetadataCharacteristic
316 */
317
318/*!
319 * \fn DataLoggerService::samplesRead
320 *
321 * This signal is emitted when the `Reading` characteristic has been notified.
322 *
323 * \see beginSampling
324 * \see stopSampling
325 */
326
327
328/*!
329 * \cond internal
330 * \class DataLoggerServicePrivate
331 *
332 * The DataLoggerServicePrivate class provides private implementation for DataLoggerService.
333 */
334
335/*!
336 * \internal
337 * Constructs a new DataLoggerServicePrivate object with public implementation \a q.
338 */
345
346/*!
347 * Returns \a settings in the format Pokit devices expect. If \a updateIntervalIs32bit is \c true
348 * then the `Update Interval` field will be encoded in 32-bit instead of 16.
349 */
351 const bool updateIntervalIs32bit)
352{
353 static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
354 static_assert(sizeof(settings.arguments) == 2, "Expected to be 2 bytes.");
355 static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
356 static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
357 static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
358 static_assert(sizeof(settings.timestamp) == 4, "Expected to be 4 bytes.");
359
360 QByteArray value;
361 QDataStream stream(&value, QIODevice::WriteOnly);
363 stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
364 stream << (quint8)settings.command << settings.arguments << (quint8)settings.mode << settings.range;
365
366 /*!
367 * \pokitApi For Pokit Meter, `updateInterval` is `uint16` seconds (as per the Pokit API 1.00),
368 * however for Pokit Pro it's `uint32` milliseconds, even though that's not officially
369 * documented anywhere.
370 */
371
372 if (!updateIntervalIs32bit) {
373 stream << (quint16)((settings.updateInterval+500)/1000) << settings.timestamp;
374 Q_ASSERT(value.size() == 11); // According to Pokit API 1.00.
375 } else {
376 stream << settings.updateInterval << settings.timestamp;
377 Q_ASSERT(value.size() == 13); // According to testing / experimentation.
378 }
379 return value;
380}
381
382/*!
383 * Parses the `Metadata` \a value into a DataLoggerService::Metatdata struct.
384 */
386{
388 DataLoggerService::LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(),
390 };
391
392 // Pokit Meter: 15 bytes, Pokit Pro: 23 bytes.
393 if (!checkSize(QLatin1String("Metadata"), value, 15, 23)) {
394 return metadata;
395 }
396
397 qCDebug(lc) << value.mid(7,12).toHex(',');
398 metadata.status = static_cast<DataLoggerService::LoggerStatus>(value.at(0));
399 metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
400 metadata.mode = static_cast<DataLoggerService::Mode>(value.at(5));
401 metadata.range = static_cast<quint8>(value.at(6));
402
403 /*!
404 * \pokitApi For Pokit Meter, `updateInterval` is `uint16` (as per the Pokit API 1.00), however
405 * for Pokit Pro it's `uint32`, even though that's not officially documented anywhere.
406 * Also note, the doc claims 'microseconds' (ie 10^-6), but clearly the value is 'milliseconds'
407 * (ie 10^-3) for Pokit Pro, and whole seconds for Pokit Meter.
408 */
409
410 if (value.size() == 15) {
411 metadata.updateInterval = qFromLittleEndian<quint16>(value.mid(7,2).constData())*1000;
412 metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(9,2).constData());
413 metadata.timestamp = qFromLittleEndian<quint32>(value.mid(11,4).constData());
414 } else if (value.size() == 23) {
415 metadata.updateInterval = qFromLittleEndian<quint32>(value.mid(7,4).constData());
416 metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
417 metadata.timestamp = qFromLittleEndian<quint32>(value.mid(19,4).constData());
418 } else {
419 qCWarning(lc).noquote() << tr("Cannot decode metadata of %n byte/s: %1", nullptr, value.size())
420 .arg(toHexString(value));
421 }
422 return metadata;
423}
424
425/*!
426 * Parses the `Reading` \a value into a DataLoggerService::Samples vector.
427 */
429{
431 if ((value.size()%2) != 0) {
432 qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
433 .arg(value.size()).arg(toHexString(value));
434 return samples;
435 }
436 while ((samples.size()*2) < value.size()) {
437 samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
438 }
439 qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
440 return samples;
441}
442
443/*!
444 * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
445 * specialised signal, for each supported \a characteristic.
446 */
448 const QByteArray &value)
449{
451
453 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
454 << serviceUuid << characteristic.name() << characteristic.uuid();
455 return;
456 }
457
460 Q_EMIT q->metadataRead(parseMetadata(value));
461 return;
462 }
463
465 qCWarning(lc).noquote() << tr("Reading characteristic is notify-only")
466 << serviceUuid << characteristic.name() << characteristic.uuid();
467 return;
468 }
469
470 qCWarning(lc).noquote() << tr("Unknown characteristic read for Data Logger service")
471 << serviceUuid << characteristic.name() << characteristic.uuid();
472}
473
474/*!
475 * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
476 * specialised signal, for each supported \a characteristic.
477 */
479 const QByteArray &newValue)
480{
482
485 Q_EMIT q->settingsWritten();
486 return;
487 }
488
490 qCWarning(lc).noquote() << tr("Metadata characteristic is read/notify, but somehow written")
491 << serviceUuid << characteristic.name() << characteristic.uuid();
492 return;
493 }
494
496 qCWarning(lc).noquote() << tr("Reading characteristic is notify-only, but somehow written")
497 << serviceUuid << characteristic.name() << characteristic.uuid();
498 return;
499 }
500
501 qCWarning(lc).noquote() << tr("Unknown characteristic written for Data Logger service")
502 << serviceUuid << characteristic.name() << characteristic.uuid();
503}
504
505/*!
506 * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
507 * specialised signal, for each supported \a characteristic.
508 */
510 const QByteArray &newValue)
511{
513
516 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
517 << serviceUuid << characteristic.name() << characteristic.uuid();
518 return;
519 }
520
522 Q_EMIT q->metadataRead(parseMetadata(newValue));
523 return;
524 }
525
527 Q_EMIT q->samplesRead(parseSamples(newValue));
528 return;
529 }
530
531 qCWarning(lc).noquote() << tr("Unknown characteristic notified for Data Logger service")
532 << serviceUuid << characteristic.name() << characteristic.uuid();
533}
534
535/// \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.
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.