Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2026 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 1540 : QString StatusService::toString(const StatusService::DeviceStatus &status)
37 1677 : {
38 3217 : switch (status) {
39 1045 : case DeviceStatus::Idle: return u"Idle"_s;
40 181 : case DeviceStatus::MultimeterDcVoltage: return u"MultimeterDcVoltage"_s;
41 181 : case DeviceStatus::MultimeterAcVoltage: return u"MultimeterAcVoltage"_s;
42 181 : case DeviceStatus::MultimeterDcCurrent: return u"MultimeterDcCurrent"_s;
43 181 : case DeviceStatus::MultimeterAcCurrent: return u"MultimeterAcCurrent"_s;
44 181 : case DeviceStatus::MultimeterResistance: return u"MultimeterResistance"_s;
45 181 : case DeviceStatus::MultimeterDiode: return u"MultimeterDiode"_s;
46 181 : case DeviceStatus::MultimeterContinuity: return u"MultimeterContinuity"_s;
47 181 : case DeviceStatus::MultimeterTemperature:return u"MultimeterTemperature"_s;
48 181 : case DeviceStatus::DsoModeSampling: return u"DsoModeSampling"_s;
49 181 : case DeviceStatus::LoggerModeSampling: return u"LoggerModeSampling"_s;
50 1677 : }
51 222 : return QString();
52 1677 : }
53 :
54 : /*!
55 : * Returns a string version of the \a status enum label.
56 : */
57 910 : QString StatusService::toString(const StatusService::BatteryStatus &status)
58 678 : {
59 1588 : switch (status) {
60 1045 : case BatteryStatus::Low: return u"Low"_s;
61 181 : case BatteryStatus::Good: return u"Good"_s;
62 678 : }
63 222 : return QString();
64 678 : }
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 950 : QString StatusService::toString(const StatusService::SwitchPosition &position)
78 1675 : {
79 2625 : switch (position) {
80 525 : case SwitchPosition::Voltage: return u"Voltage"_s;
81 1385 : case SwitchPosition::MultiMode: return u"MultiMode"_s;
82 353 : case SwitchPosition::HighCurrent: return u"HighCurrent"_s;
83 1675 : }
84 222 : return QString();
85 1675 : }
86 :
87 : /*!
88 : * Returns a string version of the \a status enum label.
89 : */
90 350 : QString StatusService::toString(const StatusService::ChargingStatus &status)
91 555 : {
92 905 : switch (status) {
93 181 : case ChargingStatus::Discharging: return u"Discharging"_s;
94 181 : case ChargingStatus::Charging: return u"Charging"_s;
95 181 : case ChargingStatus::Charged: return u"Charged"_s;
96 555 : }
97 222 : return QString();
98 555 : }
99 :
100 : /*!
101 : * Returns a string version of the \a status enum label.
102 : */
103 700 : QString StatusService::toString(const StatusService::TorchStatus &status)
104 1228 : {
105 1928 : switch (status) {
106 1041 : case TorchStatus::Off: return u"Off"_s;
107 525 : case TorchStatus::On: return u"On"_s;
108 1228 : }
109 222 : return QString();
110 1228 : }
111 :
112 : /*!
113 : * Returns a string version of the \a status enum label.
114 : */
115 530 : QString StatusService::toString(const StatusService::ButtonStatus &status)
116 891 : {
117 1421 : switch (status) {
118 353 : case ButtonStatus::Released: return u"Released"_s;
119 353 : case ButtonStatus::Pressed: return u"Pressed"_s;
120 353 : case ButtonStatus::Held: return u"Held"_s;
121 891 : }
122 222 : return QString();
123 891 : }
124 :
125 : /*!
126 : * Constructs a new Pokit service with \a parent.
127 : */
128 2450 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
129 5060 : : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
130 3035 : {
131 :
132 5485 : }
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 70 : bool StatusService::readCharacteristics()
147 111 : {
148 159 : const bool r1 = readDeviceCharacteristics();
149 159 : const bool r2 = readStatusCharacteristic();
150 159 : const bool r3 = readNameCharacteristic();
151 203 : const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
152 181 : ? readTorchCharacteristic() : true;
153 203 : const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
154 181 : ? readButtonPressCharacteristic() : true;
155 251 : return (r1 && r2 && r3 && r4 && r5);
156 111 : }
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 118 : bool StatusService::readDeviceCharacteristics()
168 222 : {
169 222 : Q_D(StatusService);
170 362 : return d->readCharacteristic(CharacteristicUuids::deviceCharacteristics);
171 222 : }
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 118 : bool StatusService::readStatusCharacteristic()
183 222 : {
184 222 : Q_D(StatusService);
185 362 : return d->readCharacteristic(CharacteristicUuids::status);
186 222 : }
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 118 : bool StatusService::readNameCharacteristic()
198 222 : {
199 222 : Q_D(StatusService);
200 362 : return d->readCharacteristic(CharacteristicUuids::name);
201 222 : }
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 70 : bool StatusService::readTorchCharacteristic()
213 111 : {
214 111 : Q_D(StatusService);
215 181 : return d->readCharacteristic(CharacteristicUuids::torch);
216 111 : }
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 70 : bool StatusService::readButtonPressCharacteristic()
228 111 : {
229 111 : Q_D(StatusService);
230 181 : return d->readCharacteristic(CharacteristicUuids::buttonPress);
231 111 : }
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 140 : StatusService::DeviceCharacteristics StatusService::deviceCharacteristics() const
249 137 : {
250 137 : Q_D(const StatusService);
251 137 : const QLowEnergyCharacteristic characteristic =
252 277 : d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
253 277 : return (characteristic.isValid())
254 233 : ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
255 417 : : StatusService::DeviceCharacteristics();
256 277 : }
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 700 : StatusService::Status StatusService::status() const
277 345 : {
278 345 : Q_D(const StatusService);
279 345 : const QLowEnergyCharacteristic characteristic =
280 1045 : d->getCharacteristic(CharacteristicUuids::status);
281 1045 : return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
282 345 : : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
283 1745 : BatteryStatus::Low, std::nullopt, std::nullopt };
284 1045 : }
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 70 : bool StatusService::enableStatusNotifications()
296 111 : {
297 111 : Q_D(StatusService);
298 181 : return d->enableCharacteristicNotificatons(CharacteristicUuids::status);
299 111 : }
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 70 : bool StatusService::disableStatusNotifications()
309 111 : {
310 111 : Q_D(StatusService);
311 181 : return d->disableCharacteristicNotificatons(CharacteristicUuids::status);
312 111 : }
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 700 : QString StatusService::deviceName() const
322 345 : {
323 345 : Q_D(const StatusService);
324 345 : const QLowEnergyCharacteristic characteristic =
325 1045 : d->getCharacteristic(CharacteristicUuids::name);
326 1745 : return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
327 1045 : }
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 70 : bool StatusService::setDeviceName(const QString &name)
337 111 : {
338 111 : Q_D(const StatusService);
339 111 : const QLowEnergyCharacteristic characteristic =
340 181 : d->getCharacteristic(CharacteristicUuids::name);
341 181 : if (!characteristic.isValid()) {
342 111 : return false;
343 111 : }
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 70 : }
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 70 : bool StatusService::flashLed()
372 111 : {
373 111 : Q_D(const StatusService);
374 111 : const QLowEnergyCharacteristic characteristic =
375 181 : d->getCharacteristic(CharacteristicUuids::flashLed);
376 181 : if (!characteristic.isValid()) {
377 111 : return false;
378 111 : }
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 181 : }
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 700 : std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
397 345 : {
398 345 : Q_D(const StatusService);
399 1045 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
400 1745 : return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
401 1045 : }
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 70 : bool StatusService::setTorchStatus(const StatusService::TorchStatus status)
413 111 : {
414 111 : Q_D(const StatusService);
415 181 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
416 181 : if (!characteristic.isValid()) {
417 111 : return false;
418 111 : }
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 181 : }
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 70 : bool StatusService::enableTorchStatusNotifications()
435 111 : {
436 111 : Q_D(StatusService);
437 181 : return d->enableCharacteristicNotificatons(CharacteristicUuids::torch);
438 111 : }
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 70 : bool StatusService::disableTorchStatusNotifications()
448 111 : {
449 111 : Q_D(StatusService);
450 181 : return d->disableCharacteristicNotificatons(CharacteristicUuids::torch);
451 111 : }
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 70 : bool StatusService::enableButtonPressedNotifications()
463 111 : {
464 111 : Q_D(StatusService);
465 181 : return d->enableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
466 111 : }
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 70 : bool StatusService::disableButtonPressedNotifications()
476 111 : {
477 111 : Q_D(StatusService);
478 181 : return d->disableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
479 111 : }
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 700 : std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
489 345 : {
490 345 : Q_D(const StatusService);
491 1045 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
492 1745 : return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
493 1045 : }
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 1890 : StatusServicePrivate::StatusServicePrivate(
571 2450 : QLowEnergyController * controller, StatusService * const q)
572 5060 : : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
573 3035 : {
574 :
575 4925 : }
576 :
577 : /*!
578 : * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
579 : */
580 280 : StatusService::DeviceCharacteristics StatusServicePrivate::parseDeviceCharacteristics(
581 : const QByteArray &value)
582 444 : {
583 724 : StatusService::DeviceCharacteristics characteristics{
584 444 : QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
585 592 : };
586 444 : Q_ASSERT(characteristics.firmwareVersion.isNull()); // How we indicate failure.
587 :
588 908 : if (!checkSize(u"Device Characteristics"_s, value, 20, 20)) {
589 222 : return characteristics;
590 222 : }
591 :
592 388 : characteristics.firmwareVersion = QVersionNumber(
593 478 : qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
594 426 : qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
595 454 : characteristics.maximumVoltage = qFromLittleEndian<quint16>(value.mid(2,2).constData());
596 454 : characteristics.maximumCurrent = qFromLittleEndian<quint16>(value.mid(4,2).constData());
597 454 : characteristics.maximumResistance = qFromLittleEndian<quint16>(value.mid(6,2).constData());
598 454 : characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
599 454 : characteristics.samplingBufferSize = qFromLittleEndian<quint16>(value.mid(10,2).constData());
600 454 : characteristics.capabilityMask = qFromLittleEndian<quint16>(value.mid(12,2).constData());
601 406 : characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
602 568 : ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
603 :
604 430 : qCDebug(lc).noquote() << tr("Firmware version: ") << characteristics.firmwareVersion;
605 430 : qCDebug(lc).noquote() << tr("Maximum voltage: ") << characteristics.maximumVoltage;
606 430 : qCDebug(lc).noquote() << tr("Maximum current: ") << characteristics.maximumCurrent;
607 430 : qCDebug(lc).noquote() << tr("Maximum resistance: ") << characteristics.maximumResistance;
608 430 : qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
609 430 : qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
610 430 : qCDebug(lc).noquote() << tr("Capability mask: ") << characteristics.capabilityMask;
611 430 : qCDebug(lc).noquote() << tr("MAC address: ") << characteristics.macAddress;
612 :
613 222 : Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
614 238 : return characteristics;
615 444 : }
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 910 : StatusService::Status StatusServicePrivate::parseStatus(const QByteArray &value)
623 1443 : {
624 2353 : StatusService::Status status{
625 1443 : static_cast<StatusService::DeviceStatus>
626 1443 : (std::numeric_limits<std::underlying_type_t<StatusService::DeviceStatus>>::max()),
627 1443 : std::numeric_limits<float>::quiet_NaN(),
628 1443 : static_cast<StatusService::BatteryStatus>
629 1443 : (std::numeric_limits<std::underlying_type_t<StatusService::BatteryStatus>>::max()),
630 1443 : std::nullopt, std::nullopt,
631 1443 : };
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 2951 : if (!checkSize(u"Status"_s, value, 5, 8)) {
642 318 : return status;
643 222 : }
644 :
645 1991 : status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
646 2497 : status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
647 1991 : if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
648 1810 : status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
649 1110 : }
650 1991 : if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
651 1810 : status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
652 1110 : }
653 1991 : if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
654 1810 : status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
655 1110 : }
656 2365 : qCDebug(lc).noquote() << tr("Device status: %1 (%2)")
657 0 : .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
658 2365 : qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
659 2365 : qCDebug(lc).noquote() << tr("Battery status: %1 (%2)")
660 0 : .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
661 1991 : if (status.switchPosition) {
662 2150 : qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
663 0 : .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
664 1110 : }
665 1991 : if (status.chargingStatus) {
666 2150 : qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
667 0 : .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
668 1110 : }
669 1749 : return status;
670 1443 : }
671 :
672 : /*!
673 : * Parses the torch status \a value, and returns the corresponding TorchStatus.
674 : */
675 210 : std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
676 333 : {
677 681 : if (!checkSize(u"Torch"_s, value, 1, 1)) {
678 181 : return std::nullopt;
679 111 : }
680 :
681 362 : const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
682 430 : qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
683 362 : return status;
684 333 : }
685 :
686 : /*!
687 : * Parses the button press \a value, and returns the corresponding ButtonStatus.
688 : */
689 280 : std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
690 444 : {
691 908 : if (!checkSize(u"Torch"_s, value, 2, 2)) {
692 181 : return std::nullopt;
693 111 : }
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 543 : const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
705 645 : qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
706 543 : return status;
707 444 : }
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 280 : void StatusServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
716 444 : {
717 724 : if (newService == StatusService::ServiceUuids::pokitMeter) {
718 215 : qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
719 181 : serviceUuid = StatusService::ServiceUuids::pokitMeter;
720 543 : } else if (newService == StatusService::ServiceUuids::pokitPro) {
721 215 : qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
722 181 : serviceUuid = StatusService::ServiceUuids::pokitPro;
723 111 : }
724 724 : AbstractPokitServicePrivate::serviceDiscovered(newService);
725 724 : }
726 :
727 : /*!
728 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
729 : * specialised signal, for each supported \a characteristic.
730 : */
731 70 : void StatusServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
732 : const QByteArray &value)
733 111 : {
734 181 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
735 :
736 111 : Q_Q(StatusService);
737 181 : if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
738 0 : Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
739 0 : return;
740 0 : }
741 :
742 181 : if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
743 0 : Q_EMIT q->deviceStatusRead(parseStatus(value));
744 0 : return;
745 0 : }
746 :
747 181 : 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 181 : 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 181 : 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 181 : 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 387 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
781 262 : << serviceUuid << characteristic.name() << characteristic.uuid();
782 111 : }
783 :
784 : /*!
785 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
786 : * specialised signal, for each supported \a characteristic.
787 : */
788 70 : void StatusServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
789 : const QByteArray &newValue)
790 111 : {
791 181 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
792 :
793 111 : Q_Q(StatusService);
794 181 : 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 181 : 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 181 : if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
807 0 : Q_EMIT q->deviceNameWritten();
808 0 : return;
809 0 : }
810 :
811 181 : if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
812 0 : Q_EMIT q->deviceLedFlashed();
813 0 : return;
814 0 : }
815 :
816 181 : if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
817 0 : Q_EMIT q->torchStatusWritten();
818 0 : return;
819 0 : }
820 :
821 387 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
822 262 : << serviceUuid << characteristic.name() << characteristic.uuid();
823 111 : }
824 :
825 : /// \endcond
826 :
827 : QTPOKIT_END_NAMESPACE
|