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 StatusService and StatusServicePrivate classes.
7 : */
8 :
9 : #include <qtpokit/statusservice.h>
10 : #include "statusservice_p.h"
11 : #include "../stringliterals_p.h"
12 :
13 : #include <QtEndian>
14 :
15 : QTPOKIT_BEGIN_NAMESPACE
16 : DOKIT_USE_STRINGLITERALS
17 :
18 : /*!
19 : * \class StatusService
20 : *
21 : * The StatusService class accesses the `Pokit Status` service of Pokit devices.
22 : */
23 :
24 : /*!
25 : * \cond internal
26 : * \struct StatusService::ServiceUuids
27 : * \pokitApi Pokit API 1.00 (and 0.02) states the Status Service UUID as
28 : * `57d3a771-267c-4394-8872-78223e92aec4` which is correct for the Pokit Meter, but Pokit Pro uses
29 : * `57d3a771-267c-4394-8872-78223e92aec5` instead, that is the last digit is a `5` not `4`.
30 : * \endcond
31 : */
32 :
33 : /*!
34 : * Returns a string version of the \a status enum label.
35 : */
36 1760 : QString StatusService::toString(const StatusService::DeviceStatus &status)
37 944 : {
38 2704 : switch (status) {
39 1072 : case DeviceStatus::Idle: return u"Idle"_s;
40 136 : case DeviceStatus::MultimeterDcVoltage: return u"MultimeterDcVoltage"_s;
41 136 : case DeviceStatus::MultimeterAcVoltage: return u"MultimeterAcVoltage"_s;
42 136 : case DeviceStatus::MultimeterDcCurrent: return u"MultimeterDcCurrent"_s;
43 136 : case DeviceStatus::MultimeterAcCurrent: return u"MultimeterAcCurrent"_s;
44 136 : case DeviceStatus::MultimeterResistance: return u"MultimeterResistance"_s;
45 136 : case DeviceStatus::MultimeterDiode: return u"MultimeterDiode"_s;
46 136 : case DeviceStatus::MultimeterContinuity: return u"MultimeterContinuity"_s;
47 136 : case DeviceStatus::MultimeterTemperature:return u"MultimeterTemperature"_s;
48 136 : case DeviceStatus::DsoModeSampling: return u"DsoModeSampling"_s;
49 136 : case DeviceStatus::LoggerModeSampling: return u"LoggerModeSampling"_s;
50 944 : }
51 112 : return QString();
52 944 : }
53 :
54 : /*!
55 : * Returns a string version of the \a status enum label.
56 : */
57 1040 : QString StatusService::toString(const StatusService::BatteryStatus &status)
58 440 : {
59 1480 : switch (status) {
60 1072 : case BatteryStatus::Low: return u"Low"_s;
61 136 : case BatteryStatus::Good: return u"Good"_s;
62 440 : }
63 112 : return QString();
64 440 : }
65 :
66 : /*!
67 : * \cond internal
68 : * \enum StatusService::SwitchPosition
69 : * \pokitApi These enum values are undocumented, but easily testable with a physical Pokit Pro device.
70 : * Internally, Pokit's Android app calls these: `SWITCH_MODE_VOLTAGE`, `SWITCH_MODE_ALL` and `SWITCH_MODE_CURRENT`.
71 : * \endcond
72 : */
73 :
74 : /*!
75 : * Returns a string version of the \a position enum label.
76 : */
77 1300 : QString StatusService::toString(const StatusService::SwitchPosition &position)
78 860 : {
79 2160 : switch (position) {
80 432 : case SwitchPosition::Voltage: return u"Voltage"_s;
81 1172 : case SwitchPosition::MultiMode: return u"MultiMode"_s;
82 284 : case SwitchPosition::HighCurrent: return u"HighCurrent"_s;
83 860 : }
84 112 : return QString();
85 860 : }
86 :
87 : /*!
88 : * Returns a string version of the \a status enum label.
89 : */
90 400 : QString StatusService::toString(const StatusService::ChargingStatus &status)
91 280 : {
92 680 : switch (status) {
93 136 : case ChargingStatus::Discharging: return u"Discharging"_s;
94 136 : case ChargingStatus::Charging: return u"Charging"_s;
95 136 : case ChargingStatus::Charged: return u"Charged"_s;
96 280 : }
97 112 : return QString();
98 280 : }
99 :
100 : /*!
101 : * Returns a string version of the \a status enum label.
102 : */
103 950 : QString StatusService::toString(const StatusService::TorchStatus &status)
104 630 : {
105 1580 : switch (status) {
106 876 : case TorchStatus::Off: return u"Off"_s;
107 432 : case TorchStatus::On: return u"On"_s;
108 630 : }
109 112 : return QString();
110 630 : }
111 :
112 : /*!
113 : * Returns a string version of the \a status enum label.
114 : */
115 670 : QString StatusService::toString(const StatusService::ButtonStatus &status)
116 454 : {
117 1124 : switch (status) {
118 284 : case ButtonStatus::Released: return u"Released"_s;
119 284 : case ButtonStatus::Pressed: return u"Pressed"_s;
120 284 : case ButtonStatus::Held: return u"Held"_s;
121 454 : }
122 112 : return QString();
123 454 : }
124 :
125 : /*!
126 : * Constructs a new Pokit service with \a parent.
127 : */
128 2800 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
129 4040 : : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
130 1640 : {
131 :
132 4440 : }
133 :
134 : /*!
135 : * \cond internal
136 : * Constructs a new Pokit service with \a parent, and private implementation \a d.
137 : */
138 0 : StatusService::StatusService(
139 0 : StatusServicePrivate * const d, QObject * const parent)
140 0 : : AbstractPokitService(d, parent)
141 0 : {
142 :
143 0 : }
144 : /// \endcond
145 :
146 80 : bool StatusService::readCharacteristics()
147 56 : {
148 101 : const bool r1 = readDeviceCharacteristics();
149 101 : const bool r2 = readStatusCharacteristic();
150 101 : const bool r3 = readNameCharacteristic();
151 157 : const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
152 136 : ? readTorchCharacteristic() : true;
153 157 : const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
154 136 : ? readButtonPressCharacteristic() : true;
155 216 : return (r1 && r2 && r3 && r4 && r5);
156 56 : }
157 :
158 : /*!
159 : * Read the `Status` service's `Device Characteristics` characteristic.
160 : *
161 : * Returns `true` is the read request is successfully queued, `false` otherwise (ie if the
162 : * underlying controller it not yet connected to the Pokit device, or the device's services have
163 : * not yet been discovered).
164 : *
165 : * Emits deviceCharacteristicsRead() if/when the characteristic has been read successfully.
166 : */
167 125 : bool StatusService::readDeviceCharacteristics()
168 112 : {
169 112 : Q_D(StatusService);
170 272 : return d->readCharacteristic(CharacteristicUuids::deviceCharacteristics);
171 112 : }
172 :
173 : /*!
174 : * Read the `Status` service's `Status` characteristic.
175 : *
176 : * Returns `true` is the read request is successfully queued, `false` otherwise (ie if the
177 : * underlying controller it not yet connected to the Pokit device, or the device's services have
178 : * not yet been discovered).
179 : *
180 : * Emits deviceStatusRead() if/when the characteristic has been read successfully.
181 : */
182 125 : bool StatusService::readStatusCharacteristic()
183 112 : {
184 112 : Q_D(StatusService);
185 272 : return d->readCharacteristic(CharacteristicUuids::status);
186 112 : }
187 :
188 : /*!
189 : * Read the `Status` service's `Name` characteristic.
190 : *
191 : * Returns `true` is the read request is successfully queued, `false` otherwise (ie if the
192 : * underlying controller it not yet connected to the Pokit device, or the device's services have
193 : * not yet been discovered).
194 : *
195 : * Emits deviceNameRead() if/when the characteristic has been read successfully.
196 : */
197 125 : bool StatusService::readNameCharacteristic()
198 112 : {
199 112 : Q_D(StatusService);
200 272 : return d->readCharacteristic(CharacteristicUuids::name);
201 112 : }
202 :
203 : /*!
204 : * Read the `Status` service's (undocumented) `Torch` characteristic.
205 : *
206 : * Returns `true` is the read request is successfully queued, `false` otherwise (ie if the
207 : * underlying controller it not yet connected to the Pokit device, or the device's services have
208 : * not yet been discovered).
209 : *
210 : * Emits torchStatusRead() if/when the characteristic has been read successfully.
211 : */
212 80 : bool StatusService::readTorchCharacteristic()
213 56 : {
214 56 : Q_D(StatusService);
215 136 : return d->readCharacteristic(CharacteristicUuids::torch);
216 56 : }
217 :
218 : /*!
219 : * Read the `Status` service's (undocumented) `Button Press` characteristic.
220 : *
221 : * Returns `true` is the read request is successfully queued, `false` otherwise (ie if the
222 : * underlying controller it not yet connected to the Pokit device, or the device's services have
223 : * not yet been discovered).
224 : *
225 : * Emits buttonPressRead() if/when the characteristic has been read successfully.
226 : */
227 80 : bool StatusService::readButtonPressCharacteristic()
228 56 : {
229 56 : Q_D(StatusService);
230 136 : return d->readCharacteristic(CharacteristicUuids::buttonPress);
231 56 : }
232 :
233 : /*!
234 : * Returns the most recent value of the `Status` service's `Device Characteristics` characteristic.
235 : *
236 : * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
237 : * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then a
238 : * null result is returned, which can be checked via the returned
239 : * DeviceCharacteristics::firmwareVersion, like:
240 : *
241 : * ```
242 : * const DeviceCharacteristics characteristics = service->deviceCharacteristics();
243 : * if (!characteristics.firmwareVersion.isNull()) {
244 : * ...
245 : * }
246 : * ```
247 : */
248 160 : StatusService::DeviceCharacteristics StatusService::deviceCharacteristics() const
249 80 : {
250 80 : Q_D(const StatusService);
251 80 : const QLowEnergyCharacteristic characteristic =
252 240 : d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
253 240 : return (characteristic.isValid())
254 198 : ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
255 400 : : StatusService::DeviceCharacteristics();
256 240 : }
257 :
258 : /*!
259 : * Returns the most recent value of the `Status` service's `Status` characteristic.
260 : *
261 : * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
262 : * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then the
263 : * returned StatusService::Status::batteryLevel member will be a quiet NaN, which can be checked
264 : * like:
265 : *
266 : * ```
267 : * const StatusService::Status status = statusService->status();
268 : * if (qIsNaN(status.batteryVoltage)) {
269 : * // Handle failure.
270 : * }
271 : * ```
272 : *
273 : * Not all Pokit devices support the Status::batteryStatus member, in which case the member will be
274 : * initilialised to the maximum value supported by the underlying type (ie `255`) to indicate "not set"
275 : */
276 800 : StatusService::Status StatusService::status() const
277 272 : {
278 272 : Q_D(const StatusService);
279 272 : const QLowEnergyCharacteristic characteristic =
280 1072 : d->getCharacteristic(CharacteristicUuids::status);
281 1072 : return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
282 272 : : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
283 1872 : BatteryStatus::Low, std::nullopt, std::nullopt };
284 1072 : }
285 :
286 : /*!
287 : * Enables client-side notifications of device status changes.
288 : *
289 : * This is an alternative to manually requesting individual reads via readStatusCharacteristic().
290 : *
291 : * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
292 : *
293 : * Successfully read values (if any) will be emitted via the deviceStatusRead() signal.
294 : */
295 80 : bool StatusService::enableStatusNotifications()
296 56 : {
297 56 : Q_D(StatusService);
298 136 : return d->enableCharacteristicNotificatons(CharacteristicUuids::status);
299 56 : }
300 :
301 : /*!
302 : * Disables client-side notifications of device status changes.
303 : *
304 : * Instantaneous status can still be fetched by readStatusCharacteristic().
305 : *
306 : * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
307 : */
308 80 : bool StatusService::disableStatusNotifications()
309 56 : {
310 56 : Q_D(StatusService);
311 136 : return d->disableCharacteristicNotificatons(CharacteristicUuids::status);
312 56 : }
313 :
314 : /*!
315 : * Returns the most recent value of the `Status` services's `Device Name` characteristic.
316 : *
317 : * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
318 : * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then a
319 : * null QString is returned.
320 : */
321 800 : QString StatusService::deviceName() const
322 272 : {
323 272 : Q_D(const StatusService);
324 272 : const QLowEnergyCharacteristic characteristic =
325 1072 : d->getCharacteristic(CharacteristicUuids::name);
326 1872 : return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
327 1072 : }
328 :
329 : /*!
330 : * Set's the Pokit device's name to \a name.
331 : *
332 : * Returns `true` if the write request was successfully queued, `false` otherwise.
333 : *
334 : * Emits deviceNameWritten() if/when the \a name has been set.
335 : */
336 80 : bool StatusService::setDeviceName(const QString &name)
337 56 : {
338 56 : Q_D(const StatusService);
339 56 : const QLowEnergyCharacteristic characteristic =
340 136 : d->getCharacteristic(CharacteristicUuids::name);
341 136 : if (!characteristic.isValid()) {
342 56 : return false;
343 56 : }
344 :
345 0 : const QByteArray value = name.toUtf8();
346 0 : if (value.length() > 11) {
347 0 : qCWarning(d->lc).noquote() << tr(R"(Device name "%1" is too long (%2 > 11 bytes): 0x3)")
348 0 : .arg(name).arg(value.length()).arg(QLatin1String(value.toHex()));
349 0 : return false;
350 0 : }
351 :
352 0 : d->service->writeCharacteristic(characteristic, value);
353 0 : return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
354 80 : }
355 :
356 : /*!
357 : * Flash the Pokit device's LED.
358 : *
359 : * Returns `true` if the flash request was successfully queued, `false` otherwise.
360 : *
361 : * Emits deviceLedFlashed() if/when the LED has flashed successfully.
362 : *
363 : * \note This operation is only supported by Pokit Meter devices. Pokit Pro devices will report an
364 : * Bluetooth ATT error `0x80`.
365 : *
366 : * \cond internal
367 : * \pokitApi The Android app can turn Pokit Pro LEDs on/off. Perhaps that is handled by an
368 : * undocumented use of this characteristic. Or perhaps its via some other service.
369 : * \endcond
370 : */
371 80 : bool StatusService::flashLed()
372 56 : {
373 56 : Q_D(const StatusService);
374 56 : const QLowEnergyCharacteristic characteristic =
375 136 : d->getCharacteristic(CharacteristicUuids::flashLed);
376 136 : if (!characteristic.isValid()) {
377 56 : return false;
378 56 : }
379 :
380 : // The Flash LED characteristic is write-only, and takes a single uint8 "LED" parameter, which
381 : // must always be 1. Presumably this is an index for which LED to flash, but the Pokit API docs
382 : // say that "any value other than 1 will be ignored", which makes sense given that all current
383 : // Pokit devices have only one LED.
384 0 : const QByteArray value(1, '\x01');
385 0 : d->service->writeCharacteristic(characteristic, value);
386 0 : return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
387 136 : }
388 :
389 : /*!
390 : * Returns the most recent value of the `Status` services's `Torch` characteristic.
391 : *
392 : * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
393 : * currently available (eg if the device does not support the Torch characteristic), then `nullopt`
394 : * is returned.
395 : */
396 800 : std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
397 272 : {
398 272 : Q_D(const StatusService);
399 1072 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
400 1872 : return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
401 1072 : }
402 :
403 : /*!
404 : * Set the Pokit device's torch to \a status.
405 : *
406 : * Returns `true` if the request was successfully queued, `false` otherwise.
407 : *
408 : * Emits torchStatusWritten() if/when the LED has flashed successfully.
409 : *
410 : * \note This operation is only supported by Pokit Pro devices, and not Pokit Meter devices.
411 : */
412 80 : bool StatusService::setTorchStatus(const StatusService::TorchStatus status)
413 56 : {
414 56 : Q_D(const StatusService);
415 136 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
416 136 : if (!characteristic.isValid()) {
417 56 : return false;
418 56 : }
419 :
420 0 : const QByteArray value(1, static_cast<char>(status));
421 0 : d->service->writeCharacteristic(characteristic, value);
422 0 : return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
423 136 : }
424 :
425 : /*!
426 : * Enables client-side notifications of torch status changes.
427 : *
428 : * This is an alternative to manually requesting individual reads via readTorchCharacteristic().
429 : *
430 : * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
431 : *
432 : * Successfully read values (if any) will be emitted via the torchStatusRead() signal.
433 : */
434 80 : bool StatusService::enableTorchStatusNotifications()
435 56 : {
436 56 : Q_D(StatusService);
437 136 : return d->enableCharacteristicNotificatons(CharacteristicUuids::torch);
438 56 : }
439 :
440 : /*!
441 : * Disables client-side notifications of torch status changes.
442 : *
443 : * Instantaneous torch status can still be fetched by readTorchCharacteristic().
444 : *
445 : * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
446 : */
447 80 : bool StatusService::disableTorchStatusNotifications()
448 56 : {
449 56 : Q_D(StatusService);
450 136 : return d->disableCharacteristicNotificatons(CharacteristicUuids::torch);
451 56 : }
452 :
453 : /*!
454 : * Enables client-side notifications of button presses.
455 : *
456 : * This is an alternative to manually requesting individual reads via readButtonPressCharacteristic().
457 : *
458 : * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
459 : *
460 : * Successfully read values (if any) will be emitted via the torchStatusRead() signal.
461 : */
462 80 : bool StatusService::enableButtonPressedNotifications()
463 56 : {
464 56 : Q_D(StatusService);
465 136 : return d->enableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
466 56 : }
467 :
468 : /*!
469 : * Disables client-side notifications of button presses.
470 : *
471 : * Instantaneous button press statusses can still be fetched by readButtonPressCharacteristic().
472 : *
473 : * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
474 : */
475 80 : bool StatusService::disableButtonPressedNotifications()
476 56 : {
477 56 : Q_D(StatusService);
478 136 : return d->disableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
479 56 : }
480 :
481 : /*!
482 : * Returns the most recent value of the `Status` services's `Button Press` characteristic.
483 : *
484 : * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
485 : * currently available (eg if the device does not support the Torch characteristic), then `nullopt`
486 : * is returned.
487 : */
488 800 : std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
489 272 : {
490 272 : Q_D(const StatusService);
491 1072 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
492 1872 : return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
493 1072 : }
494 :
495 : /*!
496 : * \fn StatusService::deviceCharacteristicsRead
497 : *
498 : * This signal is emitted when the `Device Characteristics` characteristic has been read
499 : * successfully.
500 : *
501 : * \see readDeviceCharacteristics
502 : */
503 :
504 : /*!
505 : * \fn StatusService::deviceNameRead
506 : *
507 : * This signal is emitted when the `Device Name` characteristic has been read successfully.
508 : *
509 : * \see readDeviceName
510 : */
511 :
512 : /*!
513 : * \fn StatusService::deviceNameWritten
514 : *
515 : * This signal is emitted when the `Device Name` characteristic has been written successfully.
516 : *
517 : * \see setDeviceName
518 : */
519 :
520 : /*!
521 : * \fn StatusService::deviceStatusRead
522 : *
523 : * This signal is emitted when the `Status` characteristic has been read successfully.
524 : *
525 : * \see readDeviceStatus
526 : */
527 :
528 : /*!
529 : * \fn StatusService::deviceLedFlashed
530 : *
531 : * This signal is emitted when device's LED has flashed in response to a write of the `Flash LED`
532 : * characteristic.
533 : */
534 :
535 : /*!
536 : * \fn StatusService::torchStatusRead
537 : *
538 : * This signal is emitted when the `Torch` characteristic has been read successfully.
539 : *
540 : * \see setTorchStatus
541 : */
542 :
543 : /*!
544 : * \fn StatusService::torchStatusWritten
545 : *
546 : * This signal is emitted when the `Torch` characteristic has been written successfully.
547 : *
548 : * \see readTorchCharacteristic
549 : */
550 :
551 : /*!
552 : * \fn StatusService::buttonPressRead
553 : *
554 : * This signal is emitted when the `Button Press` characteristic has been read successfully.
555 : *
556 : * \see readButtonPressCharacteristic
557 : */
558 :
559 : /*!
560 : * \cond internal
561 : * \class StatusServicePrivate
562 : *
563 : * The StatusServicePrivate class provides private implementation for StatusService.
564 : */
565 :
566 : /*!
567 : * \internal
568 : * Constructs a new StatusServicePrivate object with public implementation \a q.
569 : */
570 2240 : StatusServicePrivate::StatusServicePrivate(
571 2800 : QLowEnergyController * controller, StatusService * const q)
572 4040 : : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
573 1640 : {
574 :
575 3880 : }
576 :
577 : /*!
578 : * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
579 : */
580 320 : StatusService::DeviceCharacteristics StatusServicePrivate::parseDeviceCharacteristics(
581 : const QByteArray &value)
582 224 : {
583 544 : StatusService::DeviceCharacteristics characteristics{
584 224 : QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
585 428 : };
586 224 : Q_ASSERT(characteristics.firmwareVersion.isNull()); // How we indicate failure.
587 :
588 716 : if (!checkSize(u"Device Characteristics"_s, value, 20, 20)) {
589 112 : return characteristics;
590 112 : }
591 :
592 296 : characteristics.firmwareVersion = QVersionNumber(
593 402 : qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
594 360 : qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
595 358 : characteristics.maximumVoltage = qFromLittleEndian<quint16>(value.mid(2,2).constData());
596 358 : characteristics.maximumCurrent = qFromLittleEndian<quint16>(value.mid(4,2).constData());
597 358 : characteristics.maximumResistance = qFromLittleEndian<quint16>(value.mid(6,2).constData());
598 358 : characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
599 358 : characteristics.samplingBufferSize = qFromLittleEndian<quint16>(value.mid(10,2).constData());
600 358 : characteristics.capabilityMask = qFromLittleEndian<quint16>(value.mid(12,2).constData());
601 334 : characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
602 520 : ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
603 :
604 330 : qCDebug(lc).noquote() << tr("Firmware version: ") << characteristics.firmwareVersion;
605 330 : qCDebug(lc).noquote() << tr("Maximum voltage: ") << characteristics.maximumVoltage;
606 330 : qCDebug(lc).noquote() << tr("Maximum current: ") << characteristics.maximumCurrent;
607 330 : qCDebug(lc).noquote() << tr("Maximum resistance: ") << characteristics.maximumResistance;
608 330 : qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
609 330 : qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
610 330 : qCDebug(lc).noquote() << tr("Capability mask: ") << characteristics.capabilityMask;
611 330 : qCDebug(lc).noquote() << tr("MAC address: ") << characteristics.macAddress;
612 :
613 112 : Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
614 130 : return characteristics;
615 224 : }
616 :
617 : /*!
618 : * Parses the `Status` \a value into a Status struct. Note, not all Pokit devices support all members
619 : * in Status. Specifically, the batteryStatus member is not usually set by Pokit Meter devices, so
620 : * will be an invalid BatteryStatus enum value (`255`) in that case.
621 : */
622 1040 : StatusService::Status StatusServicePrivate::parseStatus(const QByteArray &value)
623 728 : {
624 1768 : StatusService::Status status{
625 728 : static_cast<StatusService::DeviceStatus>
626 728 : (std::numeric_limits<std::underlying_type_t<StatusService::DeviceStatus>>::max()),
627 728 : std::numeric_limits<float>::quiet_NaN(),
628 728 : static_cast<StatusService::BatteryStatus>
629 728 : (std::numeric_limits<std::underlying_type_t<StatusService::BatteryStatus>>::max()),
630 728 : std::nullopt, std::nullopt,
631 728 : };
632 :
633 : /*!
634 : * \pokitApi Pokit API 0.02 says the `Status` characteristic is 5 bytes. API 1.00 then added an
635 : * additional byte for `Battery Status`, for 6 bytes in total. However, Pokit Pro devices return
636 : * 8 bytes here. It appears that the first of those 2 extra bytes (ie the 7th byte) is used to
637 : * indicate the physical switch position, while the other extra byte (ie the 8th byte) indicates
638 : * the device's current charging status.
639 : */
640 :
641 2327 : if (!checkSize(u"Status"_s, value, 5, 8)) {
642 210 : return status;
643 112 : }
644 :
645 1496 : status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
646 1969 : status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
647 1496 : if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
648 1360 : status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
649 560 : }
650 1496 : if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
651 1360 : status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
652 560 : }
653 1496 : if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
654 1360 : status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
655 560 : }
656 1815 : qCDebug(lc).noquote() << tr("Device status: %1 (%2)")
657 0 : .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
658 1815 : qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
659 1815 : qCDebug(lc).noquote() << tr("Battery status: %1 (%2)")
660 0 : .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
661 1496 : if (status.switchPosition) {
662 1650 : qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
663 0 : .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
664 560 : }
665 1496 : if (status.chargingStatus) {
666 1650 : qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
667 0 : .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
668 560 : }
669 1155 : return status;
670 728 : }
671 :
672 : /*!
673 : * Parses the torch status \a value, and returns the corresponding TorchStatus.
674 : */
675 240 : std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
676 168 : {
677 537 : if (!checkSize(u"Torch"_s, value, 1, 1)) {
678 136 : return std::nullopt;
679 56 : }
680 :
681 272 : const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
682 330 : qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
683 272 : return status;
684 168 : }
685 :
686 : /*!
687 : * Parses the button press \a value, and returns the corresponding ButtonStatus.
688 : */
689 320 : std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
690 224 : {
691 716 : if (!checkSize(u"Torch"_s, value, 2, 2)) {
692 136 : return std::nullopt;
693 56 : }
694 :
695 : /*!
696 : * \pokitApi The button event is the second byte, but no idea what the first byte is. In all examples
697 : * I've see it's always `0x02`. It appears that the Pokit Android app only ever looks at `bytes[1]`.
698 : *
699 : * \pokitApi Note, we can actually write to the Button Press characteristic too. If we do, then whatever
700 : * we set as the first byte persists, and (unsurprisingly) the second byte reverts to the current
701 : * button state. So still no idea what that first byte is for.
702 : */
703 :
704 408 : const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
705 495 : qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
706 408 : return status;
707 224 : }
708 :
709 : /*!
710 : * Handles `QLowEnergyController::serviceDiscovered` events.
711 : *
712 : * Here we override the base implementation to detect if we're looking at a Pokit Meter, or Pokit
713 : * Pro device, as the two devices have very slightly different Status Service UUIDs.
714 : */
715 320 : void StatusServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
716 224 : {
717 544 : if (newService == StatusService::ServiceUuids::pokitMeter) {
718 165 : qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
719 136 : serviceUuid = StatusService::ServiceUuids::pokitMeter;
720 408 : } else if (newService == StatusService::ServiceUuids::pokitPro) {
721 165 : qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
722 136 : serviceUuid = StatusService::ServiceUuids::pokitPro;
723 56 : }
724 544 : AbstractPokitServicePrivate::serviceDiscovered(newService);
725 544 : }
726 :
727 : /*!
728 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
729 : * specialised signal, for each supported \a characteristic.
730 : */
731 80 : void StatusServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
732 : const QByteArray &value)
733 56 : {
734 136 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
735 :
736 56 : Q_Q(StatusService);
737 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
738 0 : Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
739 0 : return;
740 0 : }
741 :
742 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
743 0 : Q_EMIT q->deviceStatusRead(parseStatus(value));
744 0 : return;
745 0 : }
746 :
747 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
748 0 : const QString deviceName = QString::fromUtf8(value);
749 0 : qCDebug(lc).noquote() << tr(R"(Device name: "%1")").arg(deviceName);
750 0 : Q_EMIT q->deviceNameRead(deviceName);
751 0 : return;
752 0 : }
753 :
754 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
755 0 : qCWarning(lc).noquote() << tr("Flash LED characteristic is write-only, but somehow read")
756 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
757 0 : return;
758 0 : }
759 :
760 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
761 0 : if (!checkSize(u"Torch"_s, value, 1, 1)) {
762 0 : return;
763 0 : }
764 0 : const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
765 0 : qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
766 0 : Q_EMIT q->torchStatusRead(status);
767 0 : return;
768 0 : }
769 :
770 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::buttonPress) {
771 0 : if (!checkSize(u"Torch"_s, value, 2, 2)) {
772 0 : return;
773 0 : }
774 0 : const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
775 0 : qCDebug(lc).noquote() << tr("Button status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
776 0 : Q_EMIT q->buttonPressRead(value.at(0), status);
777 0 : return;
778 0 : }
779 :
780 354 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
781 226 : << serviceUuid << characteristic.name() << characteristic.uuid();
782 56 : }
783 :
784 : /*!
785 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
786 : * specialised signal, for each supported \a characteristic.
787 : */
788 80 : void StatusServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
789 : const QByteArray &newValue)
790 56 : {
791 136 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
792 :
793 56 : Q_Q(StatusService);
794 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
795 0 : qCWarning(lc).noquote() << tr("Device Characteristics is read-only, but somehow written")
796 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
797 0 : return;
798 0 : }
799 :
800 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
801 0 : qCWarning(lc).noquote() << tr("Status characteristic is read-only, but somehow written")
802 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
803 0 : return;
804 0 : }
805 :
806 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
807 0 : Q_EMIT q->deviceNameWritten();
808 0 : return;
809 0 : }
810 :
811 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
812 0 : Q_EMIT q->deviceLedFlashed();
813 0 : return;
814 0 : }
815 :
816 136 : if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
817 0 : Q_EMIT q->torchStatusWritten();
818 0 : return;
819 0 : }
820 :
821 354 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
822 226 : << serviceUuid << characteristic.name() << characteristic.uuid();
823 56 : }
824 :
825 : /// \endcond
826 :
827 : QTPOKIT_END_NAMESPACE
|