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