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