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 1152 : {
38 2912 : switch (status) {
39 1088 : case DeviceStatus::Idle: return u"Idle"_s;
40 152 : case DeviceStatus::MultimeterDcVoltage: return u"MultimeterDcVoltage"_s;
41 152 : case DeviceStatus::MultimeterAcVoltage: return u"MultimeterAcVoltage"_s;
42 152 : case DeviceStatus::MultimeterDcCurrent: return u"MultimeterDcCurrent"_s;
43 152 : case DeviceStatus::MultimeterAcCurrent: return u"MultimeterAcCurrent"_s;
44 152 : case DeviceStatus::MultimeterResistance: return u"MultimeterResistance"_s;
45 152 : case DeviceStatus::MultimeterDiode: return u"MultimeterDiode"_s;
46 152 : case DeviceStatus::MultimeterContinuity: return u"MultimeterContinuity"_s;
47 152 : case DeviceStatus::MultimeterTemperature:return u"MultimeterTemperature"_s;
48 152 : case DeviceStatus::DsoModeSampling: return u"DsoModeSampling"_s;
49 152 : case DeviceStatus::LoggerModeSampling: return u"LoggerModeSampling"_s;
50 1152 : }
51 144 : return QString();
52 1152 : }
53 :
54 : /*!
55 : * Returns a string version of the \a status enum label.
56 : */
57 1040 : QString StatusService::toString(const StatusService::BatteryStatus &status)
58 504 : {
59 1544 : switch (status) {
60 1088 : case BatteryStatus::Low: return u"Low"_s;
61 152 : case BatteryStatus::Good: return u"Good"_s;
62 504 : }
63 144 : return QString();
64 504 : }
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 1120 : {
79 2420 : switch (position) {
80 484 : case SwitchPosition::Voltage: return u"Voltage"_s;
81 1314 : case SwitchPosition::MultiMode: return u"MultiMode"_s;
82 318 : case SwitchPosition::HighCurrent: return u"HighCurrent"_s;
83 1120 : }
84 144 : return QString();
85 1120 : }
86 :
87 : /*!
88 : * Returns a string version of the \a status enum label.
89 : */
90 400 : QString StatusService::toString(const StatusService::ChargingStatus &status)
91 360 : {
92 760 : switch (status) {
93 152 : case ChargingStatus::Discharging: return u"Discharging"_s;
94 152 : case ChargingStatus::Charging: return u"Charging"_s;
95 152 : case ChargingStatus::Charged: return u"Charged"_s;
96 360 : }
97 144 : return QString();
98 360 : }
99 :
100 : /*!
101 : * Returns a string version of the \a status enum label.
102 : */
103 950 : QString StatusService::toString(const StatusService::TorchStatus &status)
104 820 : {
105 1770 : switch (status) {
106 982 : case TorchStatus::Off: return u"Off"_s;
107 484 : case TorchStatus::On: return u"On"_s;
108 820 : }
109 144 : return QString();
110 820 : }
111 :
112 : /*!
113 : * Returns a string version of the \a status enum label.
114 : */
115 670 : QString StatusService::toString(const StatusService::ButtonStatus &status)
116 588 : {
117 1258 : switch (status) {
118 318 : case ButtonStatus::Released: return u"Released"_s;
119 318 : case ButtonStatus::Pressed: return u"Pressed"_s;
120 318 : case ButtonStatus::Held: return u"Held"_s;
121 588 : }
122 144 : return QString();
123 588 : }
124 :
125 : /*!
126 : * Constructs a new Pokit service with \a parent.
127 : */
128 2800 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
129 4440 : : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
130 2040 : {
131 :
132 4840 : }
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 72 : {
148 117 : const bool r1 = readDeviceCharacteristics();
149 117 : const bool r2 = readStatusCharacteristic();
150 117 : const bool r3 = readNameCharacteristic();
151 173 : const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
152 152 : ? readTorchCharacteristic() : true;
153 173 : const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
154 152 : ? readButtonPressCharacteristic() : true;
155 232 : return (r1 && r2 && r3 && r4 && r5);
156 72 : }
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 144 : {
169 144 : Q_D(StatusService);
170 304 : return d->readCharacteristic(CharacteristicUuids::deviceCharacteristics);
171 144 : }
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 144 : {
184 144 : Q_D(StatusService);
185 304 : return d->readCharacteristic(CharacteristicUuids::status);
186 144 : }
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 144 : {
199 144 : Q_D(StatusService);
200 304 : return d->readCharacteristic(CharacteristicUuids::name);
201 144 : }
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 72 : {
214 72 : Q_D(StatusService);
215 152 : return d->readCharacteristic(CharacteristicUuids::torch);
216 72 : }
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 72 : {
229 72 : Q_D(StatusService);
230 152 : return d->readCharacteristic(CharacteristicUuids::buttonPress);
231 72 : }
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 96 : {
250 96 : Q_D(const StatusService);
251 96 : const QLowEnergyCharacteristic characteristic =
252 256 : d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
253 256 : return (characteristic.isValid())
254 214 : ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
255 416 : : StatusService::DeviceCharacteristics();
256 256 : }
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 288 : {
278 288 : Q_D(const StatusService);
279 288 : const QLowEnergyCharacteristic characteristic =
280 1088 : d->getCharacteristic(CharacteristicUuids::status);
281 1088 : return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
282 288 : : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
283 1888 : BatteryStatus::Low, std::nullopt, std::nullopt };
284 1088 : }
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 72 : {
297 72 : Q_D(StatusService);
298 152 : return d->enableCharacteristicNotificatons(CharacteristicUuids::status);
299 72 : }
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 72 : {
310 72 : Q_D(StatusService);
311 152 : return d->disableCharacteristicNotificatons(CharacteristicUuids::status);
312 72 : }
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 288 : {
323 288 : Q_D(const StatusService);
324 288 : const QLowEnergyCharacteristic characteristic =
325 1088 : d->getCharacteristic(CharacteristicUuids::name);
326 1888 : return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
327 1088 : }
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 72 : {
338 72 : Q_D(const StatusService);
339 72 : const QLowEnergyCharacteristic characteristic =
340 152 : d->getCharacteristic(CharacteristicUuids::name);
341 152 : if (!characteristic.isValid()) {
342 72 : return false;
343 72 : }
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 72 : {
373 72 : Q_D(const StatusService);
374 72 : const QLowEnergyCharacteristic characteristic =
375 152 : d->getCharacteristic(CharacteristicUuids::flashLed);
376 152 : if (!characteristic.isValid()) {
377 72 : return false;
378 72 : }
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 152 : }
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 288 : {
398 288 : Q_D(const StatusService);
399 1088 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
400 1888 : return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
401 1088 : }
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 72 : {
414 72 : Q_D(const StatusService);
415 152 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
416 152 : if (!characteristic.isValid()) {
417 72 : return false;
418 72 : }
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 152 : }
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 72 : {
436 72 : Q_D(StatusService);
437 152 : return d->enableCharacteristicNotificatons(CharacteristicUuids::torch);
438 72 : }
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 72 : {
449 72 : Q_D(StatusService);
450 152 : return d->disableCharacteristicNotificatons(CharacteristicUuids::torch);
451 72 : }
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 72 : {
464 72 : Q_D(StatusService);
465 152 : return d->enableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
466 72 : }
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 72 : {
477 72 : Q_D(StatusService);
478 152 : return d->disableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
479 72 : }
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 288 : {
490 288 : Q_D(const StatusService);
491 1088 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
492 1888 : return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
493 1088 : }
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 4440 : : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
573 2040 : {
574 :
575 4280 : }
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 288 : {
583 608 : StatusService::DeviceCharacteristics characteristics{
584 288 : QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
585 492 : };
586 288 : Q_ASSERT(characteristics.firmwareVersion.isNull()); // How we indicate failure.
587 :
588 780 : if (!checkSize(u"Device Characteristics"_s, value, 20, 20)) {
589 144 : return characteristics;
590 144 : }
591 :
592 328 : characteristics.firmwareVersion = QVersionNumber(
593 434 : qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
594 392 : qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
595 390 : characteristics.maximumVoltage = qFromLittleEndian<quint16>(value.mid(2,2).constData());
596 390 : characteristics.maximumCurrent = qFromLittleEndian<quint16>(value.mid(4,2).constData());
597 390 : characteristics.maximumResistance = qFromLittleEndian<quint16>(value.mid(6,2).constData());
598 390 : characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
599 390 : characteristics.samplingBufferSize = qFromLittleEndian<quint16>(value.mid(10,2).constData());
600 390 : characteristics.capabilityMask = qFromLittleEndian<quint16>(value.mid(12,2).constData());
601 366 : characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
602 552 : ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
603 :
604 362 : qCDebug(lc).noquote() << tr("Firmware version: ") << characteristics.firmwareVersion;
605 362 : qCDebug(lc).noquote() << tr("Maximum voltage: ") << characteristics.maximumVoltage;
606 362 : qCDebug(lc).noquote() << tr("Maximum current: ") << characteristics.maximumCurrent;
607 362 : qCDebug(lc).noquote() << tr("Maximum resistance: ") << characteristics.maximumResistance;
608 362 : qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
609 362 : qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
610 362 : qCDebug(lc).noquote() << tr("Capability mask: ") << characteristics.capabilityMask;
611 362 : qCDebug(lc).noquote() << tr("MAC address: ") << characteristics.macAddress;
612 :
613 144 : Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
614 162 : return characteristics;
615 288 : }
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 936 : {
624 1976 : StatusService::Status status{
625 936 : static_cast<StatusService::DeviceStatus>
626 936 : (std::numeric_limits<std::underlying_type_t<StatusService::DeviceStatus>>::max()),
627 936 : std::numeric_limits<float>::quiet_NaN(),
628 936 : static_cast<StatusService::BatteryStatus>
629 936 : (std::numeric_limits<std::underlying_type_t<StatusService::BatteryStatus>>::max()),
630 936 : std::nullopt, std::nullopt,
631 936 : };
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 2535 : if (!checkSize(u"Status"_s, value, 5, 8)) {
642 242 : return status;
643 144 : }
644 :
645 1672 : status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
646 2145 : status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
647 1672 : if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
648 1520 : status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
649 720 : }
650 1672 : if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
651 1520 : status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
652 720 : }
653 1672 : if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
654 1520 : status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
655 720 : }
656 1991 : qCDebug(lc).noquote() << tr("Device status: %1 (%2)")
657 0 : .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
658 1991 : qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
659 1991 : qCDebug(lc).noquote() << tr("Battery status: %1 (%2)")
660 0 : .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
661 1672 : if (status.switchPosition) {
662 1810 : qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
663 0 : .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
664 720 : }
665 1672 : if (status.chargingStatus) {
666 1810 : qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
667 0 : .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
668 720 : }
669 1331 : return status;
670 936 : }
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 216 : {
677 585 : if (!checkSize(u"Torch"_s, value, 1, 1)) {
678 152 : return std::nullopt;
679 72 : }
680 :
681 304 : const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
682 362 : qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
683 304 : return status;
684 216 : }
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 288 : {
691 780 : if (!checkSize(u"Torch"_s, value, 2, 2)) {
692 152 : return std::nullopt;
693 72 : }
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 456 : const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
705 543 : qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
706 456 : return status;
707 288 : }
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 288 : {
717 608 : if (newService == StatusService::ServiceUuids::pokitMeter) {
718 181 : qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
719 152 : serviceUuid = StatusService::ServiceUuids::pokitMeter;
720 456 : } else if (newService == StatusService::ServiceUuids::pokitPro) {
721 181 : qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
722 152 : serviceUuid = StatusService::ServiceUuids::pokitPro;
723 72 : }
724 608 : AbstractPokitServicePrivate::serviceDiscovered(newService);
725 608 : }
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 72 : {
734 152 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
735 :
736 72 : Q_Q(StatusService);
737 152 : if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
738 0 : Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
739 0 : return;
740 0 : }
741 :
742 152 : if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
743 0 : Q_EMIT q->deviceStatusRead(parseStatus(value));
744 0 : return;
745 0 : }
746 :
747 152 : 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 152 : 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 152 : 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 152 : 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 370 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
781 242 : << serviceUuid << characteristic.name() << characteristic.uuid();
782 72 : }
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 72 : {
791 152 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
792 :
793 72 : Q_Q(StatusService);
794 152 : 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 152 : 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 152 : if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
807 0 : Q_EMIT q->deviceNameWritten();
808 0 : return;
809 0 : }
810 :
811 152 : if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
812 0 : Q_EMIT q->deviceLedFlashed();
813 0 : return;
814 0 : }
815 :
816 152 : if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
817 0 : Q_EMIT q->torchStatusWritten();
818 0 : return;
819 0 : }
820 :
821 370 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
822 242 : << serviceUuid << characteristic.name() << characteristic.uuid();
823 72 : }
824 :
825 : /// \endcond
826 :
827 : QTPOKIT_END_NAMESPACE
|