Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2023 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 396 : QString StatusService::toString(const StatusService::DeviceStatus &status)
33 : {
34 396 : switch (status) {
35 40 : case DeviceStatus::Idle: return QLatin1String("Idle");
36 4 : case DeviceStatus::MultimeterDcVoltage: return QLatin1String("MultimeterDcVoltage");
37 4 : case DeviceStatus::MultimeterAcVoltage: return QLatin1String("MultimeterAcVoltage");
38 4 : case DeviceStatus::MultimeterDcCurrent: return QLatin1String("MultimeterDcCurrent");
39 4 : case DeviceStatus::MultimeterAcCurrent: return QLatin1String("MultimeterAcCurrent");
40 4 : case DeviceStatus::MultimeterResistance: return QLatin1String("MultimeterResistance");
41 4 : case DeviceStatus::MultimeterDiode: return QLatin1String("MultimeterDiode");
42 4 : case DeviceStatus::MultimeterContinuity: return QLatin1String("MultimeterContinuity");
43 4 : case DeviceStatus::MultimeterTemperature:return QLatin1String("MultimeterTemperature");
44 4 : case DeviceStatus::DsoModeSampling: return QLatin1String("DsoModeSampling");
45 4 : 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 234 : QString StatusService::toString(const StatusService::BatteryStatus &status)
54 : {
55 234 : switch (status) {
56 40 : case BatteryStatus::Low: return QLatin1String("Low");
57 4 : 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 410 : QString StatusService::toString(const StatusService::SwitchPosition &position)
74 : {
75 410 : switch (position) {
76 12 : case SwitchPosition::Voltage: return QLatin1String("Voltage");
77 32 : case SwitchPosition::MultiMode: return QLatin1String("MultiMode");
78 8 : 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 90 : QString StatusService::toString(const StatusService::ChargingStatus &status)
87 : {
88 90 : switch (status) {
89 4 : case ChargingStatus::Discharging: return QLatin1String("Discharging");
90 4 : case ChargingStatus::Charging: return QLatin1String("Charging");
91 4 : 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 296 : QString StatusService::toString(const StatusService::TorchStatus &status)
100 : {
101 296 : switch (status) {
102 24 : case TorchStatus::Off: return QLatin1String("Off");
103 12 : 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 186 : QString StatusService::toString(const StatusService::ButtonStatus &status)
112 : {
113 186 : switch (status) {
114 8 : case ButtonStatus::Released: return QLatin1String("Released");
115 8 : case ButtonStatus::Pressed: return QLatin1String("Pressed");
116 8 : case ButtonStatus::Held: return QLatin1String("Held");
117 : }
118 : return QString();
119 : }
120 :
121 : /*!
122 : * Constructs a new Pokit service with \a parent.
123 : */
124 630 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
125 630 : : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
126 : {
127 :
128 630 : }
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 432 : StatusService::~StatusService()
146 : {
147 :
148 432 : }
149 :
150 18 : bool StatusService::readCharacteristics()
151 : {
152 12 : const bool r1 = readDeviceCharacteristics();
153 12 : const bool r2 = readStatusCharacteristic();
154 12 : const bool r3 = readNameCharacteristic();
155 30 : const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
156 18 : ? readTorchCharacteristic() : true;
157 30 : const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
158 18 : ? readButtonPressCharacteristic() : true;
159 36 : 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 30 : bool StatusService::readDeviceCharacteristics()
172 : {
173 : Q_D(StatusService);
174 36 : 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 30 : bool StatusService::readStatusCharacteristic()
187 : {
188 : Q_D(StatusService);
189 36 : 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 30 : bool StatusService::readNameCharacteristic()
202 : {
203 : Q_D(StatusService);
204 36 : 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 18 : bool StatusService::readTorchCharacteristic()
217 : {
218 : Q_D(StatusService);
219 18 : 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 18 : bool StatusService::readButtonPressCharacteristic()
232 : {
233 : Q_D(StatusService);
234 18 : 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 36 : StatusService::DeviceCharacteristics StatusService::deviceCharacteristics() const
253 : {
254 : Q_D(const StatusService);
255 : const QLowEnergyCharacteristic characteristic =
256 36 : d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
257 36 : return (characteristic.isValid())
258 12 : ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
259 72 : : StatusService::DeviceCharacteristics();
260 36 : }
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 180 : StatusService::Status StatusService::status() const
281 : {
282 : Q_D(const StatusService);
283 : const QLowEnergyCharacteristic characteristic =
284 180 : d->getCharacteristic(CharacteristicUuids::status);
285 180 : return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
286 : : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
287 360 : BatteryStatus::Low, std::nullopt, std::nullopt };
288 180 : }
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 18 : bool StatusService::enableStatusNotifications()
300 : {
301 : Q_D(StatusService);
302 18 : 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 18 : bool StatusService::disableStatusNotifications()
313 : {
314 : Q_D(StatusService);
315 18 : 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 180 : QString StatusService::deviceName() const
326 : {
327 : Q_D(const StatusService);
328 : const QLowEnergyCharacteristic characteristic =
329 180 : d->getCharacteristic(CharacteristicUuids::name);
330 360 : return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
331 180 : }
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 18 : bool StatusService::setDeviceName(const QString &name)
341 : {
342 : Q_D(const StatusService);
343 : const QLowEnergyCharacteristic characteristic =
344 18 : d->getCharacteristic(CharacteristicUuids::name);
345 18 : 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 18 : }
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 18 : bool StatusService::flashLed()
376 : {
377 : Q_D(const StatusService);
378 : const QLowEnergyCharacteristic characteristic =
379 18 : d->getCharacteristic(CharacteristicUuids::flashLed);
380 18 : 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 18 : }
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 180 : std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
401 : {
402 : Q_D(const StatusService);
403 180 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
404 360 : return (characteristic.isValid()) ? d->parseTorchStatus(characteristic.value()) : std::nullopt;
405 180 : }
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 18 : bool StatusService::setTorchStatus(const StatusService::TorchStatus status)
417 : {
418 : Q_D(const StatusService);
419 18 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
420 18 : 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 18 : }
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 18 : bool StatusService::enableTorchStatusNotifications()
439 : {
440 : Q_D(StatusService);
441 18 : 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 18 : bool StatusService::disableTorchStatusNotifications()
452 : {
453 : Q_D(StatusService);
454 18 : 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 18 : bool StatusService::enableButtonPressedNotifications()
467 : {
468 : Q_D(StatusService);
469 18 : 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 18 : bool StatusService::disableButtonPressedNotifications()
480 : {
481 : Q_D(StatusService);
482 18 : 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 180 : std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
493 : {
494 : Q_D(const StatusService);
495 180 : const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
496 360 : return (characteristic.isValid()) ? d->parseButtonPress(characteristic.value()) : std::nullopt;
497 180 : }
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 630 : StatusServicePrivate::StatusServicePrivate(
575 630 : QLowEnergyController * controller, StatusService * const q)
576 630 : : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
577 : {
578 :
579 630 : }
580 :
581 : /*!
582 : * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
583 : */
584 72 : StatusService::DeviceCharacteristics StatusServicePrivate::parseDeviceCharacteristics(
585 : const QByteArray &value)
586 : {
587 72 : StatusService::DeviceCharacteristics characteristics{
588 : QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
589 72 : };
590 : Q_ASSERT(characteristics.firmwareVersion.isNull()); // How we indicate failure.
591 :
592 88 : if (!checkSize(QLatin1String("Device Characterisitcs"), value, 20, 20)) {
593 : return characteristics;
594 : }
595 :
596 44 : characteristics.firmwareVersion = QVersionNumber(
597 72 : qFromLittleEndian<quint8 >(value.mid(0,1)),
598 64 : qFromLittleEndian<quint8 >(value.mid(1,1)));
599 44 : characteristics.maximumVoltage = qFromLittleEndian<quint16>(value.mid(2,2));
600 44 : characteristics.maximumCurrent = qFromLittleEndian<quint16>(value.mid(4,2));
601 44 : characteristics.maximumResistance = qFromLittleEndian<quint16>(value.mid(6,2));
602 44 : characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2));
603 44 : characteristics.samplingBufferSize = qFromLittleEndian<quint16>(value.mid(10,2));
604 44 : characteristics.capabilityMask = qFromLittleEndian<quint16>(value.mid(12,2));
605 64 : characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
606 108 : (QByteArray(2, '\0') + value.mid(14,6)));
607 :
608 36 : qCDebug(lc).noquote() << tr("Firmware version: ") << characteristics.firmwareVersion;
609 36 : qCDebug(lc).noquote() << tr("Maximum voltage: ") << characteristics.maximumVoltage;
610 36 : qCDebug(lc).noquote() << tr("Maximum current: ") << characteristics.maximumCurrent;
611 36 : qCDebug(lc).noquote() << tr("Maximum resistance: ") << characteristics.maximumResistance;
612 36 : qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
613 36 : qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
614 36 : qCDebug(lc).noquote() << tr("Capability mask: ") << characteristics.capabilityMask;
615 36 : qCDebug(lc).noquote() << tr("MAC address: ") << characteristics.macAddress;
616 :
617 : Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
618 6 : 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 234 : StatusService::Status StatusServicePrivate::parseStatus(const QByteArray &value)
627 : {
628 234 : 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. The purpose of those last 2 bytes are not currently known. Note also, Pokit
641 : * Meter only uses the first 5 bytes - ie `Battery Status` is not present.
642 : *
643 : * Update: it appears that the first of those 2 extra bytes is used to indicate the phycical switch
644 : * position.
645 : */
646 :
647 286 : if (!checkSize(QLatin1String("Status"), value, 5, 6)) {
648 36 : return status;
649 : }
650 :
651 198 : status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
652 242 : status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4));
653 198 : if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
654 180 : status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
655 : }
656 198 : if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
657 180 : status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
658 : }
659 198 : if (value.size() >= 8) { // Switch Position - as yet, undocumented by Pokit Innovations.
660 180 : status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
661 : }
662 198 : qCDebug(lc).noquote() << tr("Device status: %1 (%2)")
663 0 : .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
664 198 : qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
665 198 : qCDebug(lc).noquote() << tr("Battery status: %1 (%2)")
666 0 : .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
667 198 : if (status.switchPosition) {
668 180 : qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
669 0 : .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
670 : }
671 198 : if (status.chargingStatus) {
672 180 : qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
673 0 : .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
674 : }
675 198 : return status;
676 : }
677 :
678 : /*!
679 : * Parses the torch status \a value, and returns the corresponding TorchStatus.
680 : */
681 54 : std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
682 : {
683 66 : if (!checkSize(QLatin1String("Torch"), value, 1, 1)) {
684 18 : return std::nullopt;
685 : }
686 :
687 36 : const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
688 36 : qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
689 36 : return status;
690 : }
691 :
692 : /*!
693 : * Parses the button press \a value, and returns the corresponding ButtonStatus.
694 : */
695 72 : std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
696 : {
697 88 : if (!checkSize(QLatin1String("Torch"), value, 2, 2)) {
698 18 : return std::nullopt;
699 : }
700 :
701 : /*!
702 : * \pokitApi The button event is the second byte, but no idea what the first byte is. In all examples
703 : * I've see it's always `0x02`. It appears that the Pokit Android app only ever looks at `bytes[1]`.
704 : *
705 : * \pokitApi Note, we can actually write to the Button Press characteristic too. If we do, the whatever
706 : * we set as the first byte persists, and (unsurprisingly) the second byte reverts to the current
707 : * button state. So still no idea what that first byte is for.
708 : */
709 :
710 54 : const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
711 54 : qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
712 54 : return status;
713 : }
714 :
715 : /*!
716 : * Handles `QLowEnergyController::serviceDiscovered` events.
717 : *
718 : * Here we override the base implementation to detect if we're looking at a Pokit Meter, or Pokit
719 : * Pro device, as the two devices have very slightly different Status Service UUIDs.
720 : */
721 72 : void StatusServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
722 : {
723 72 : if (newService == StatusService::ServiceUuids::pokitMeter) {
724 18 : qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
725 18 : serviceUuid = StatusService::ServiceUuids::pokitMeter;
726 54 : } else if (newService == StatusService::ServiceUuids::pokitPro) {
727 18 : qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
728 18 : serviceUuid = StatusService::ServiceUuids::pokitPro;
729 : }
730 72 : AbstractPokitServicePrivate::serviceDiscovered(newService);
731 72 : }
732 :
733 : /*!
734 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
735 : * specialised signal, for each supported \a characteristic.
736 : */
737 18 : void StatusServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
738 : const QByteArray &value)
739 : {
740 18 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
741 :
742 : Q_Q(StatusService);
743 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
744 0 : emit q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
745 0 : return;
746 : }
747 :
748 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
749 0 : emit q->deviceStatusRead(parseStatus(value));
750 0 : return;
751 : }
752 :
753 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
754 0 : const QString deviceName = QString::fromUtf8(value);
755 0 : qCDebug(lc).noquote() << tr(R"(Device name: "%1")").arg(deviceName);
756 0 : emit q->deviceNameRead(deviceName);
757 : return;
758 0 : }
759 :
760 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
761 0 : qCWarning(lc).noquote() << tr("Flash LED characteristic is write-only, but somehow read")
762 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
763 0 : return;
764 : }
765 :
766 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
767 0 : if (!checkSize(QLatin1String("Torch"), value, 1, 1)) {
768 : return;
769 : }
770 0 : const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
771 0 : qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
772 0 : emit q->torchStatusRead(status);
773 0 : return;
774 : }
775 :
776 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::buttonPress) {
777 0 : if (!checkSize(QLatin1String("Torch"), value, 2, 2)) {
778 : return;
779 : }
780 0 : const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
781 0 : qCDebug(lc).noquote() << tr("Button status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
782 0 : emit q->buttonPressRead(value.at(0), status);
783 0 : return;
784 : }
785 :
786 54 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
787 24 : << serviceUuid << characteristic.name() << characteristic.uuid();
788 : }
789 :
790 : /*!
791 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
792 : * specialised signal, for each supported \a characteristic.
793 : */
794 18 : void StatusServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
795 : const QByteArray &newValue)
796 : {
797 18 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
798 :
799 : Q_Q(StatusService);
800 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
801 0 : qCWarning(lc).noquote() << tr("Device Characteristics is read-only, but somehow written")
802 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
803 0 : return;
804 : }
805 :
806 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
807 0 : qCWarning(lc).noquote() << tr("Status characteristic is read-only, but somehow written")
808 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
809 0 : return;
810 : }
811 :
812 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
813 0 : emit q->deviceNameWritten();
814 0 : return;
815 : }
816 :
817 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
818 0 : emit q->deviceLedFlashed();
819 0 : return;
820 : }
821 :
822 18 : if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
823 0 : emit q->torchStatusWritten();
824 0 : return;
825 : }
826 :
827 54 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
828 24 : << serviceUuid << characteristic.name() << characteristic.uuid();
829 : }
830 :
831 : /// \endcond
|