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 4056 : {
33 16296 : switch (mode) {
34 152 : case Mode::Idle: return tr("Idle");
35 3168 : case Mode::DcVoltage: return tr("DC voltage");
36 3168 : case Mode::AcVoltage: return tr("AC voltage");
37 3168 : case Mode::DcCurrent: return tr("DC current");
38 3168 : case Mode::AcCurrent: return tr("AC current");
39 3168 : case Mode::Temperature: return tr("Temperature");
40 144 : default: return QString();
41 4056 : }
42 4056 : }
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 3024 : {
47 11184 : switch (mode) {
48 144 : case Mode::Idle:
49 144 : break;
50 3632 : case Mode::DcVoltage:
51 1152 : case Mode::AcVoltage:
52 4352 : return VoltageRange::toString(product, range);
53 3920 : case Mode::DcCurrent:
54 1152 : case Mode::AcCurrent:
55 4352 : return CurrentRange::toString(product, range);
56 576 : case Mode::Temperature:
57 576 : break;
58 3024 : }
59 720 : return QString();
60 3024 : }
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 2592 : {
65 10272 : return toString(*pokitProduct(), range, mode);
66 2592 : }
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 864 : {
73 1824 : switch (mode) {
74 144 : case Mode::Idle:
75 144 : break;
76 320 : case Mode::DcVoltage:
77 288 : case Mode::AcVoltage:
78 608 : return VoltageRange::maxValue(product, range);
79 608 : case Mode::DcCurrent:
80 288 : case Mode::AcCurrent:
81 608 : return CurrentRange::maxValue(product, range);
82 144 : case Mode::Temperature:
83 144 : break;
84 864 : }
85 288 : return 0;
86 864 : }
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 432 : {
93 912 : return maxValue(*pokitProduct(), range, mode);
94 432 : }
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 11608 : : AbstractPokitService(new DataLoggerServicePrivate(controller, this), parent)
114 3816 : {
115 :
116 12056 : }
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 72 : {
132 152 : return readMetadataCharacteristic();
133 72 : }
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 144 : {
146 144 : Q_D(DataLoggerService);
147 304 : return d->readCharacteristic(CharacteristicUuids::metadata);
148 144 : }
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 288 : {
159 288 : Q_D(const DataLoggerService);
160 288 : const QLowEnergyCharacteristic characteristic =
161 608 : d->getCharacteristic(CharacteristicUuids::settings);
162 608 : if (!characteristic.isValid()) {
163 288 : return false;
164 288 : }
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 216 : {
185 216 : Q_D(const DataLoggerService);
186 216 : Q_ASSERT(settings.command == DataLoggerService::Command::Start);
187 456 : if (settings.command != DataLoggerService::Command::Start) {
188 608 : qCWarning(d->lc).noquote() << tr("Settings command must be 'Start'.");
189 246 : return false;
190 144 : }
191 152 : return setSettings(settings);
192 216 : }
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 72 : {
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 152 : return setSettings({ DataLoggerService::Command::Stop, 0, DataLoggerService::Mode::Idle, 0, 0, 0 });
206 72 : }
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 72 : {
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 152 : return setSettings({ DataLoggerService::Command::Refresh, 0, DataLoggerService::Mode::Idle, 0, 0, 0 });
224 72 : }
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 72 : {
242 72 : Q_D(const DataLoggerService);
243 72 : const QLowEnergyCharacteristic characteristic =
244 152 : d->getCharacteristic(CharacteristicUuids::metadata);
245 152 : return (characteristic.isValid()) ? DataLoggerServicePrivate::parseMetadata(characteristic.value())
246 232 : : Metadata{ LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
247 152 : }
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 72 : {
260 72 : Q_D(DataLoggerService);
261 152 : return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
262 72 : }
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 72 : {
273 72 : Q_D(DataLoggerService);
274 152 : return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
275 72 : }
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 72 : {
286 72 : Q_D(DataLoggerService);
287 152 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
288 72 : }
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 72 : {
297 72 : Q_D(DataLoggerService);
298 152 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
299 72 : }
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 11608 : : AbstractPokitServicePrivate(DataLoggerService::serviceUuid, controller, q)
341 3816 : {
342 :
343 8451 : }
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 504 : {
352 504 : static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
353 504 : static_assert(sizeof(settings.arguments) == 2, "Expected to be 2 bytes.");
354 504 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
355 504 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
356 504 : static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
357 504 : static_assert(sizeof(settings.timestamp) == 4, "Expected to be 4 bytes.");
358 :
359 805 : QByteArray value;
360 1064 : QDataStream stream(&value, QIODevice::WriteOnly);
361 1064 : stream.setByteOrder(QDataStream::LittleEndian);
362 1064 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
363 1064 : 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 1064 : if (!updateIntervalIs32bit) {
372 304 : stream << (quint16)((settings.updateInterval+500)/1000) << settings.timestamp;
373 144 : Q_ASSERT(value.size() == 11); // According to Pokit API 1.00.
374 360 : } else {
375 760 : stream << settings.updateInterval << settings.timestamp;
376 360 : Q_ASSERT(value.size() == 13); // According to testing / experimentation.
377 360 : }
378 1064 : return value;
379 1064 : }
380 :
381 : /*!
382 : * Parses the `Metadata` \a value into a DataLoggerService::Metatdata struct.
383 : */
384 400 : DataLoggerService::Metadata DataLoggerServicePrivate::parseMetadata(const QByteArray &value)
385 360 : {
386 760 : DataLoggerService::Metadata metadata{
387 360 : DataLoggerService::LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(),
388 360 : DataLoggerService::Mode::Idle, 0, 0, 0, 0
389 360 : };
390 :
391 : // Pokit Meter: 15 bytes, Pokit Pro: 23 bytes.
392 975 : if (!checkSize(u"Metadata"_s, value, 15, 23)) {
393 144 : return metadata;
394 144 : }
395 :
396 575 : qCDebug(lc) << value.mid(7,12).toHex(',');
397 456 : metadata.status = static_cast<DataLoggerService::LoggerStatus>(value.at(0));
398 585 : metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
399 456 : metadata.mode = static_cast<DataLoggerService::Mode>(value.at(5));
400 456 : 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 456 : if (value.size() == 15) {
410 195 : metadata.updateInterval = qFromLittleEndian<quint16>(value.mid(7,2).constData())*1000;
411 195 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(9,2).constData());
412 195 : metadata.timestamp = qFromLittleEndian<quint32>(value.mid(11,4).constData());
413 304 : } else if (value.size() == 23) {
414 195 : metadata.updateInterval = qFromLittleEndian<quint32>(value.mid(7,4).constData());
415 195 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
416 195 : metadata.timestamp = qFromLittleEndian<quint32>(value.mid(19,4).constData());
417 72 : } else {
418 265 : qCWarning(lc).noquote() << tr("Cannot decode metadata of %n byte/s: %1", nullptr, value.size())
419 205 : .arg(toHexString(value));
420 72 : }
421 216 : return metadata;
422 360 : }
423 :
424 : /*!
425 : * Parses the `Reading` \a value into a DataLoggerService::Samples vector.
426 : */
427 320 : DataLoggerService::Samples DataLoggerServicePrivate::parseSamples(const QByteArray &value)
428 288 : {
429 460 : DataLoggerService::Samples samples;
430 608 : if ((value.size()%2) != 0) {
431 289 : qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
432 241 : .arg(value.size()).arg(toHexString(value));
433 81 : return samples;
434 72 : }
435 1824 : while ((samples.size()*2) < value.size()) {
436 1755 : samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
437 648 : }
438 543 : qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
439 243 : return samples;
440 288 : }
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 72 : {
449 152 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
450 :
451 152 : 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 72 : Q_Q(DataLoggerService);
458 152 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::metadata) {
459 0 : Q_EMIT q->metadataRead(parseMetadata(value));
460 0 : return;
461 0 : }
462 :
463 152 : 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 370 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Data Logger service")
470 242 : << serviceUuid << characteristic.name() << characteristic.uuid();
471 72 : }
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 72 : {
480 152 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
481 :
482 72 : Q_Q(DataLoggerService);
483 152 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::settings) {
484 0 : Q_EMIT q->settingsWritten();
485 0 : return;
486 0 : }
487 :
488 152 : 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 152 : 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 370 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Data Logger service")
501 242 : << serviceUuid << characteristic.name() << characteristic.uuid();
502 72 : }
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 72 : {
511 152 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
512 :
513 72 : Q_Q(DataLoggerService);
514 152 : 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 152 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::metadata) {
521 0 : Q_EMIT q->metadataRead(parseMetadata(newValue));
522 0 : return;
523 0 : }
524 :
525 152 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::reading) {
526 0 : Q_EMIT q->samplesRead(parseSamples(newValue));
527 0 : return;
528 0 : }
529 :
530 370 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for Data Logger service")
531 242 : << serviceUuid << characteristic.name() << characteristic.uuid();
532 72 : }
533 :
534 : /// \endcond
535 :
536 : QTPOKIT_END_NAMESPACE
|