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