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