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