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 6120 : QString DataLoggerService::toString(const Mode &mode)
28 2841 : {
29 8961 : switch (mode) {
30 87 : case Mode::Idle: return tr("Idle");
31 1740 : case Mode::DcVoltage: return tr("DC voltage");
32 1740 : case Mode::AcVoltage: return tr("AC voltage");
33 1740 : case Mode::DcCurrent: return tr("DC current");
34 1740 : case Mode::AcCurrent: return tr("AC current");
35 1740 : case Mode::Temperature: return tr("Temperature");
36 94 : default: return QString();
37 2841 : }
38 2841 : }
39 :
40 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
41 4080 : QString DataLoggerService::toString(const PokitProduct product, const quint8 range, const Mode mode)
42 2094 : {
43 6174 : switch (mode) {
44 94 : case Mode::Idle:
45 94 : break;
46 1906 : case Mode::DcVoltage:
47 800 : case Mode::AcVoltage:
48 2400 : return VoltageRange::toString(product, range);
49 2094 : case Mode::DcCurrent:
50 800 : case Mode::AcCurrent:
51 2400 : return CurrentRange::toString(product, range);
52 400 : case Mode::Temperature:
53 400 : break;
54 2094 : }
55 494 : return QString();
56 2094 : }
57 :
58 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
59 3840 : QString DataLoggerService::toString(const quint8 range, const Mode mode) const
60 1812 : {
61 5652 : return toString(*pokitProduct(), range, mode);
62 1812 : }
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 480 : quint32 DataLoggerService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
68 564 : {
69 1044 : switch (mode) {
70 94 : case Mode::Idle:
71 94 : break;
72 160 : case Mode::DcVoltage:
73 188 : case Mode::AcVoltage:
74 348 : return VoltageRange::maxValue(product, range);
75 348 : case Mode::DcCurrent:
76 188 : case Mode::AcCurrent:
77 348 : return CurrentRange::maxValue(product, range);
78 94 : case Mode::Temperature:
79 94 : break;
80 564 : }
81 188 : return 0;
82 564 : }
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 240 : quint32 DataLoggerService::maxValue(const quint8 range, const Mode mode) const
88 282 : {
89 522 : return maxValue(*pokitProduct(), range, mode);
90 282 : }
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 4120 : DataLoggerService::DataLoggerService(QLowEnergyController * const controller, QObject * parent)
109 5871 : : AbstractPokitService(new DataLoggerServicePrivate(controller, this), parent)
110 2591 : {
111 :
112 6711 : }
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 40 : bool DataLoggerService::readCharacteristics()
127 47 : {
128 87 : return readMetadataCharacteristic();
129 47 : }
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 57 : bool DataLoggerService::readMetadataCharacteristic()
141 94 : {
142 94 : Q_D(DataLoggerService);
143 174 : return d->readCharacteristic(CharacteristicUuids::metadata);
144 94 : }
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 160 : bool DataLoggerService::setSettings(const Settings &settings)
154 188 : {
155 188 : Q_D(const DataLoggerService);
156 188 : const QLowEnergyCharacteristic characteristic =
157 348 : d->getCharacteristic(CharacteristicUuids::settings);
158 348 : if (!characteristic.isValid()) {
159 188 : return false;
160 188 : }
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 160 : }
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 120 : bool DataLoggerService::startLogger(const Settings &settings)
180 141 : {
181 141 : Q_D(const DataLoggerService);
182 141 : Q_ASSERT(settings.command == DataLoggerService::Command::Start);
183 261 : if (settings.command != DataLoggerService::Command::Start) {
184 308 : qCWarning(d->lc).noquote() << tr("Settings command must be 'Start'.");
185 154 : return false;
186 94 : }
187 87 : return setSettings(settings);
188 141 : }
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 40 : bool DataLoggerService::stopLogger()
197 47 : {
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 87 : return setSettings({ DataLoggerService::Command::Stop, 0, DataLoggerService::Mode::Idle, 0, 0, 0 });
202 47 : }
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 40 : bool DataLoggerService::fetchSamples()
215 47 : {
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 87 : return setSettings({ DataLoggerService::Command::Refresh, 0, DataLoggerService::Mode::Idle, 0, 0, 0 });
220 47 : }
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 40 : DataLoggerService::Metadata DataLoggerService::metadata() const
237 47 : {
238 47 : Q_D(const DataLoggerService);
239 47 : const QLowEnergyCharacteristic characteristic =
240 87 : d->getCharacteristic(CharacteristicUuids::metadata);
241 87 : return (characteristic.isValid()) ? DataLoggerServicePrivate::parseMetadata(characteristic.value())
242 127 : : Metadata{ LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
243 87 : }
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 40 : bool DataLoggerService::enableMetadataNotifications()
255 47 : {
256 47 : Q_D(DataLoggerService);
257 87 : return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
258 47 : }
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 40 : bool DataLoggerService::disableMetadataNotifications()
268 47 : {
269 47 : Q_D(DataLoggerService);
270 87 : return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
271 47 : }
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 40 : bool DataLoggerService::enableReadingNotifications()
281 47 : {
282 47 : Q_D(DataLoggerService);
283 87 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
284 47 : }
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 40 : bool DataLoggerService::disableReadingNotifications()
292 47 : {
293 47 : Q_D(DataLoggerService);
294 87 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
295 47 : }
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 1751 : DataLoggerServicePrivate::DataLoggerServicePrivate(
335 4120 : QLowEnergyController * controller, DataLoggerService * const q)
336 5871 : : AbstractPokitServicePrivate(DataLoggerService::serviceUuid, controller, q)
337 2591 : {
338 :
339 4342 : }
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 280 : QByteArray DataLoggerServicePrivate::encodeSettings(const DataLoggerService::Settings &settings,
346 : const bool updateIntervalIs32bit)
347 329 : {
348 329 : static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
349 329 : static_assert(sizeof(settings.arguments) == 2, "Expected to be 2 bytes.");
350 329 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
351 329 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
352 329 : static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
353 329 : static_assert(sizeof(settings.timestamp) == 4, "Expected to be 4 bytes.");
354 :
355 448 : QByteArray value;
356 609 : QDataStream stream(&value, QIODevice::WriteOnly);
357 609 : stream.setByteOrder(QDataStream::LittleEndian);
358 609 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
359 609 : 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 609 : if (!updateIntervalIs32bit) {
368 174 : stream << (quint16)((settings.updateInterval+500)/1000) << settings.timestamp;
369 94 : Q_ASSERT(value.size() == 11); // According to Pokit API 1.00.
370 235 : } else {
371 435 : stream << settings.updateInterval << settings.timestamp;
372 235 : Q_ASSERT(value.size() == 13); // According to testing / experimentation.
373 235 : }
374 609 : return value;
375 609 : }
376 :
377 : /*!
378 : * Parses the `Metadata` \a value into a DataLoggerService::Metatdata struct.
379 : */
380 200 : DataLoggerService::Metadata DataLoggerServicePrivate::parseMetadata(const QByteArray &value)
381 235 : {
382 435 : DataLoggerService::Metadata metadata{
383 235 : DataLoggerService::LoggerStatus::Error, std::numeric_limits<float>::quiet_NaN(),
384 235 : DataLoggerService::Mode::Idle, 0, 0, 0, 0
385 235 : };
386 :
387 : // Pokit Meter: 15 bytes, Pokit Pro: 23 bytes.
388 520 : if (!checkSize(QLatin1String("Metadata"), value, 15, 23)) {
389 94 : return metadata;
390 94 : }
391 :
392 291 : qCDebug(lc) << value.mid(7,12).toHex(',');
393 261 : metadata.status = static_cast<DataLoggerService::LoggerStatus>(value.at(0));
394 312 : metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
395 261 : metadata.mode = static_cast<DataLoggerService::Mode>(value.at(5));
396 261 : 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 261 : if (value.size() == 15) {
406 104 : metadata.updateInterval = qFromLittleEndian<quint16>(value.mid(7,2).constData())*1000;
407 104 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(9,2).constData());
408 104 : metadata.timestamp = qFromLittleEndian<quint32>(value.mid(11,4).constData());
409 174 : } else if (value.size() == 23) {
410 104 : metadata.updateInterval = qFromLittleEndian<quint32>(value.mid(7,4).constData());
411 104 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
412 104 : metadata.timestamp = qFromLittleEndian<quint32>(value.mid(19,4).constData());
413 47 : } else {
414 137 : qCWarning(lc).noquote() << tr("Cannot decode metadata of %n byte/s: %1", nullptr, value.size())
415 106 : .arg(toHexString(value));
416 47 : }
417 141 : return metadata;
418 235 : }
419 :
420 : /*!
421 : * Parses the `Reading` \a value into a DataLoggerService::Samples vector.
422 : */
423 160 : DataLoggerService::Samples DataLoggerServicePrivate::parseSamples(const QByteArray &value)
424 188 : {
425 256 : DataLoggerService::Samples samples;
426 348 : if ((value.size()%2) != 0) {
427 160 : qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
428 131 : .arg(value.size()).arg(toHexString(value));
429 52 : return samples;
430 47 : }
431 1044 : while ((samples.size()*2) < value.size()) {
432 936 : samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
433 423 : }
434 291 : qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
435 156 : return samples;
436 188 : }
437 :
438 : /*!
439 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
440 : * specialised signal, for each supported \a characteristic.
441 : */
442 40 : void DataLoggerServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
443 : const QByteArray &value)
444 47 : {
445 87 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
446 :
447 87 : 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 47 : Q_Q(DataLoggerService);
454 87 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::metadata) {
455 0 : Q_EMIT q->metadataRead(parseMetadata(value));
456 0 : return;
457 0 : }
458 :
459 87 : 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 192 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Data Logger service")
466 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
467 47 : }
468 :
469 : /*!
470 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
471 : * specialised signal, for each supported \a characteristic.
472 : */
473 40 : void DataLoggerServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
474 : const QByteArray &newValue)
475 47 : {
476 87 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
477 :
478 47 : Q_Q(DataLoggerService);
479 87 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::settings) {
480 0 : Q_EMIT q->settingsWritten();
481 0 : return;
482 0 : }
483 :
484 87 : 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 87 : 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 192 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Data Logger service")
497 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
498 47 : }
499 :
500 : /*!
501 : * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
502 : * specialised signal, for each supported \a characteristic.
503 : */
504 40 : void DataLoggerServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
505 : const QByteArray &newValue)
506 47 : {
507 87 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
508 :
509 47 : Q_Q(DataLoggerService);
510 87 : 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 87 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::metadata) {
517 0 : Q_EMIT q->metadataRead(parseMetadata(newValue));
518 0 : return;
519 0 : }
520 :
521 87 : if (characteristic.uuid() == DataLoggerService::CharacteristicUuids::reading) {
522 0 : Q_EMIT q->samplesRead(parseSamples(newValue));
523 0 : return;
524 0 : }
525 :
526 192 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for Data Logger service")
527 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
528 47 : }
529 :
530 : /// \endcond
|