Line data Source code
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 :
9 : #include <qtpokit/dataloggerservice.h>
10 : #include "dataloggerservice_p.h"
11 : #include "pokitproducts_p.h"
12 :
13 : #include <qtpokit/statusservice.h>
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.
27 5814 : QString DataLoggerService::toString(const Mode &mode)
28 2812 : {
29 8626 : switch (mode) {
30 82 : case Mode::Idle: return tr("Idle");
31 1676 : case Mode::DcVoltage: return tr("DC voltage");
32 1676 : case Mode::AcVoltage: return tr("AC voltage");
33 1676 : case Mode::DcCurrent: return tr("DC current");
34 1676 : case Mode::AcCurrent: return tr("AC current");
35 1676 : case Mode::Temperature: return tr("Temperature");
36 88 : default: return QString();
37 2812 : }
38 2812 : }
39 :
40 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
41 3876 : QString DataLoggerService::toString(const PokitProduct product, const quint8 range, const Mode mode)
42 1968 : {
43 5844 : switch (mode) {
44 88 : case Mode::Idle:
45 88 : break;
46 1808 : case Mode::DcVoltage:
47 752 : case Mode::AcVoltage:
48 2272 : return VoltageRange::toString(product, range);
49 1984 : case Mode::DcCurrent:
50 752 : case Mode::AcCurrent:
51 2272 : return CurrentRange::toString(product, range);
52 376 : case Mode::Temperature:
53 376 : break;
54 1968 : }
55 464 : return QString();
56 1968 : }
57 :
58 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
59 3648 : QString DataLoggerService::toString(const quint8 range, const Mode mode) const
60 1704 : {
61 5352 : return toString(*pokitProduct(), range, mode);
62 1704 : }
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 : */
69 456 : QVariant DataLoggerService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
70 528 : {
71 984 : switch (mode) {
72 88 : case Mode::Idle:
73 88 : break;
74 152 : case Mode::DcVoltage:
75 176 : case Mode::AcVoltage:
76 328 : return VoltageRange::maxValue(product, range);
77 328 : case Mode::DcCurrent:
78 176 : case Mode::AcCurrent:
79 328 : return CurrentRange::maxValue(product, range);
80 88 : case Mode::Temperature:
81 88 : break;
82 528 : }
83 176 : return QVariant();
84 528 : }
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 : */
92 228 : QVariant DataLoggerService::maxValue(const quint8 range, const Mode mode) const
93 264 : {
94 492 : return maxValue(*pokitProduct(), range, mode);
95 264 : }
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 : */
113 3914 : DataLoggerService::DataLoggerService(QLowEnergyController * const controller, QObject * parent)
114 5562 : : AbstractPokitService(new DataLoggerServicePrivate(controller, this), parent)
115 2432 : {
116 :
117 6346 : }
118 :
119 : /*!
120 : * \cond internal
121 : * Constructs a new Pokit service with \a parent, and private implementation \a d.
122 : */
123 0 : DataLoggerService::DataLoggerService(
124 0 : DataLoggerServicePrivate * const d, QObject * const parent)
125 0 : : AbstractPokitService(d, parent)
126 0 : {
127 :
128 0 : }
129 : /// \endcond
130 :
131 38 : bool DataLoggerService::readCharacteristics()
132 44 : {
133 82 : return readMetadataCharacteristic();
134 44 : }
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 : */
145 54 : bool DataLoggerService::readMetadataCharacteristic()
146 88 : {
147 88 : Q_D(DataLoggerService);
148 164 : return d->readCharacteristic(CharacteristicUuids::metadata);
149 88 : }
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 : */
158 152 : bool DataLoggerService::setSettings(const Settings &settings)
159 176 : {
160 176 : Q_D(const DataLoggerService);
161 176 : const QLowEnergyCharacteristic characteristic =
162 328 : d->getCharacteristic(CharacteristicUuids::settings);
163 328 : if (!characteristic.isValid()) {
164 176 : return false;
165 176 : }
166 :
167 0 : const bool updateIntervalIs32bit =
168 0 : (d->getCharacteristic(CharacteristicUuids::metadata).value().size() >= 23);
169 0 : const QByteArray value = DataLoggerServicePrivate::encodeSettings(settings, updateIntervalIs32bit);
170 0 : if (value.isNull()) {
171 0 : return false;
172 0 : }
173 :
174 0 : d->service->writeCharacteristic(characteristic, value);
175 0 : return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
176 152 : }
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 : */
184 114 : bool DataLoggerService::startLogger(const Settings &settings)
185 132 : {
186 132 : Q_D(const DataLoggerService);
187 132 : Q_ASSERT(settings.command == DataLoggerService::Command::Start);
188 246 : if (settings.command != DataLoggerService::Command::Start) {
189 286 : qCWarning(d->lc).noquote() << tr("Settings command must be 'Start'.");
190 148 : return false;
191 88 : }
192 82 : return setSettings(settings);
193 132 : }
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 : */
201 38 : bool DataLoggerService::stopLogger()
202 44 : {
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.
206 82 : return setSettings({ DataLoggerService::Command::Stop, 0, DataLoggerService::Mode::Idle, 0, 0, 0 });
207 44 : }
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 : */
219 38 : bool DataLoggerService::fetchSamples()
220 44 : {
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.
224 82 : return setSettings({ DataLoggerService::Command::Refresh, 0, DataLoggerService::Mode::Idle, 0, 0, 0 });
225 44 : }
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 : */
241 38 : DataLoggerService::Metadata DataLoggerService::metadata() const
242 44 : {
243 44 : Q_D(const DataLoggerService);
244 44 : const QLowEnergyCharacteristic characteristic =
245 82 : d->getCharacteristic(CharacteristicUuids::metadata);
246 82 : return (characteristic.isValid()) ? DataLoggerServicePrivate::parseMetadata(characteristic.value())
247 120 : : Metadata{ LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
248 82 : }
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 : */
259 38 : bool DataLoggerService::enableMetadataNotifications()
260 44 : {
261 44 : Q_D(DataLoggerService);
262 82 : return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
263 44 : }
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 : */
272 38 : bool DataLoggerService::disableMetadataNotifications()
273 44 : {
274 44 : Q_D(DataLoggerService);
275 82 : return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
276 44 : }
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 : */
285 38 : bool DataLoggerService::enableReadingNotifications()
286 44 : {
287 44 : Q_D(DataLoggerService);
288 82 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
289 44 : }
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 : */
296 38 : bool DataLoggerService::disableReadingNotifications()
297 44 : {
298 44 : Q_D(DataLoggerService);
299 82 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
300 44 : }
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 : */
339 1648 : DataLoggerServicePrivate::DataLoggerServicePrivate(
340 3914 : QLowEnergyController * controller, DataLoggerService * const q)
341 5562 : : AbstractPokitServicePrivate(DataLoggerService::serviceUuid, controller, q)
342 2432 : {
343 :
344 4080 : }
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 : */
350 266 : QByteArray DataLoggerServicePrivate::encodeSettings(const DataLoggerService::Settings &settings,
351 : const bool updateIntervalIs32bit)
352 308 : {
353 308 : static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
354 308 : static_assert(sizeof(settings.arguments) == 2, "Expected to be 2 bytes.");
355 308 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
356 308 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
357 308 : static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
358 308 : static_assert(sizeof(settings.timestamp) == 4, "Expected to be 4 bytes.");
359 :
360 413 : QByteArray value;
361 574 : QDataStream stream(&value, QIODevice::WriteOnly);
362 574 : stream.setByteOrder(QDataStream::LittleEndian);
363 574 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
364 574 : 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 574 : if (!updateIntervalIs32bit) {
373 164 : stream << (quint16)((settings.updateInterval+500)/1000) << settings.timestamp;
374 88 : Q_ASSERT(value.size() == 11); // According to Pokit API 1.00.
375 220 : } else {
376 410 : stream << settings.updateInterval << settings.timestamp;
377 220 : Q_ASSERT(value.size() == 13); // According to testing / experimentation.
378 220 : }
379 574 : return value;
380 574 : }
381 :
382 : /*!
383 : * Parses the `Metadata` \a value into a DataLoggerService::Metatdata struct.
384 : */
385 190 : DataLoggerService::Metadata DataLoggerServicePrivate::parseMetadata(const QByteArray &value)
386 220 : {
387 410 : DataLoggerService::Metadata metadata{
388 220 : DataLoggerService::LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(),
389 220 : DataLoggerService::Mode::Idle, 0, 0, 0, 0
390 220 : };
391 :
392 : // Pokit Meter: 15 bytes, Pokit Pro: 23 bytes.
393 485 : if (!checkSize(QLatin1String("Metadata"), value, 15, 23)) {
394 88 : return metadata;
395 88 : }
396 :
397 270 : qCDebug(lc) << value.mid(7,12).toHex(',');
398 246 : metadata.status = static_cast<DataLoggerService::LoggerStatus>(value.at(0));
399 291 : metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
400 246 : metadata.mode = static_cast<DataLoggerService::Mode>(value.at(5));
401 246 : 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 246 : if (value.size() == 15) {
411 97 : metadata.updateInterval = qFromLittleEndian<quint16>(value.mid(7,2).constData())*1000;
412 97 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(9,2).constData());
413 97 : metadata.timestamp = qFromLittleEndian<quint32>(value.mid(11,4).constData());
414 164 : } else if (value.size() == 23) {
415 97 : metadata.updateInterval = qFromLittleEndian<quint32>(value.mid(7,4).constData());
416 97 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
417 97 : metadata.timestamp = qFromLittleEndian<quint32>(value.mid(19,4).constData());
418 44 : } else {
419 128 : qCWarning(lc).noquote() << tr("Cannot decode metadata of %n byte/s: %1", nullptr, value.size())
420 97 : .arg(toHexString(value));
421 44 : }
422 132 : return metadata;
423 220 : }
424 :
425 : /*!
426 : * Parses the `Reading` \a value into a DataLoggerService::Samples vector.
427 : */
428 152 : DataLoggerService::Samples DataLoggerServicePrivate::parseSamples(const QByteArray &value)
429 176 : {
430 236 : DataLoggerService::Samples samples;
431 328 : if ((value.size()%2) != 0) {
432 151 : qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
433 120 : .arg(value.size()).arg(toHexString(value));
434 49 : return samples;
435 44 : }
436 1020 : while ((samples.size()*2) < value.size()) {
437 873 : samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
438 396 : }
439 270 : qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
440 147 : return samples;
441 176 : }
442 :
443 : /*!
444 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
445 : * specialised signal, for each supported \a characteristic.
446 : */
447 38 : void DataLoggerServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
448 : const QByteArray &value)
449 44 : {
450 82 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
451 :
452 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::settings) {
453 0 : qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
454 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
455 0 : return;
456 0 : }
457 :
458 44 : Q_Q(DataLoggerService);
459 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::metadata) {
460 0 : Q_EMIT q->metadataRead(parseMetadata(value));
461 0 : return;
462 0 : }
463 :
464 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::reading) {
465 0 : qCWarning(lc).noquote() << tr("Reading characteristic is notify-only")
466 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
467 0 : return;
468 0 : }
469 :
470 179 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Data Logger service")
471 135 : << serviceUuid << characteristic.name() << characteristic.uuid();
472 44 : }
473 :
474 : /*!
475 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
476 : * specialised signal, for each supported \a characteristic.
477 : */
478 38 : void DataLoggerServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
479 : const QByteArray &newValue)
480 44 : {
481 82 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
482 :
483 44 : Q_Q(DataLoggerService);
484 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::settings) {
485 0 : Q_EMIT q->settingsWritten();
486 0 : return;
487 0 : }
488 :
489 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::metadata) {
490 0 : qCWarning(lc).noquote() << tr("Metadata characteristic is read/notify, but somehow written")
491 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
492 0 : return;
493 0 : }
494 :
495 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::reading) {
496 0 : qCWarning(lc).noquote() << tr("Reading characteristic is notify-only, but somehow written")
497 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
498 0 : return;
499 0 : }
500 :
501 183 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Data Logger service")
502 135 : << serviceUuid << characteristic.name() << characteristic.uuid();
503 44 : }
504 :
505 : /*!
506 : * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
507 : * specialised signal, for each supported \a characteristic.
508 : */
509 38 : void DataLoggerServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
510 : const QByteArray &newValue)
511 44 : {
512 82 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
513 :
514 44 : Q_Q(DataLoggerService);
515 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::settings) {
516 0 : qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
517 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
518 0 : return;
519 0 : }
520 :
521 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::metadata) {
522 0 : Q_EMIT q->metadataRead(parseMetadata(newValue));
523 0 : return;
524 0 : }
525 :
526 82 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::reading) {
527 0 : Q_EMIT q->samplesRead(parseSamples(newValue));
528 0 : return;
529 0 : }
530 :
531 179 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for Data Logger service")
532 135 : << serviceUuid << characteristic.name() << characteristic.uuid();
533 44 : }
534 :
535 : /// \endcond
|