Dokit
Internal development documentation
Loading...
Searching...
No Matches
statusservice.cpp
Go to the documentation of this file.
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
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 */
33{
34 switch (status) {
35 case DeviceStatus::Idle: return QLatin1String("Idle");
36 case DeviceStatus::MultimeterDcVoltage: return QLatin1String("MultimeterDcVoltage");
37 case DeviceStatus::MultimeterAcVoltage: return QLatin1String("MultimeterAcVoltage");
38 case DeviceStatus::MultimeterDcCurrent: return QLatin1String("MultimeterDcCurrent");
39 case DeviceStatus::MultimeterAcCurrent: return QLatin1String("MultimeterAcCurrent");
40 case DeviceStatus::MultimeterResistance: return QLatin1String("MultimeterResistance");
41 case DeviceStatus::MultimeterDiode: return QLatin1String("MultimeterDiode");
42 case DeviceStatus::MultimeterContinuity: return QLatin1String("MultimeterContinuity");
43 case DeviceStatus::MultimeterTemperature:return QLatin1String("MultimeterTemperature");
44 case DeviceStatus::DsoModeSampling: return QLatin1String("DsoModeSampling");
45 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 */
54{
55 switch (status) {
56 case BatteryStatus::Low: return QLatin1String("Low");
57 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 */
74{
75 switch (position) {
76 case SwitchPosition::Voltage: return QLatin1String("Voltage");
77 case SwitchPosition::MultiMode: return QLatin1String("MultiMode");
78 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 */
87{
88 switch (status) {
89 case ChargingStatus::Discharging: return QLatin1String("Discharging");
90 case ChargingStatus::Charging: return QLatin1String("Charging");
91 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 */
100{
101 switch (status) {
102 case TorchStatus::Off: return QLatin1String("Off");
103 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 */
112{
113 switch (status) {
114 case ButtonStatus::Released: return QLatin1String("Released");
115 case ButtonStatus::Pressed: return QLatin1String("Pressed");
116 case ButtonStatus::Held: return QLatin1String("Held");
117 }
118 return QString();
119}
120
121/*!
122 * Constructs a new Pokit service with \a parent.
123 */
125 : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
126{
127
128}
129
130/*!
131 * \cond internal
132 * Constructs a new Pokit service with \a parent, and private implementation \a d.
133 */
135 StatusServicePrivate * const d, QObject * const parent)
136 : AbstractPokitService(d, parent)
137{
138
139}
140/// \endcond
141
142/*!
143 * Destroys this StatusService object.
144 */
149
151{
152 const bool r1 = readDeviceCharacteristics();
153 const bool r2 = readStatusCharacteristic();
154 const bool r3 = readNameCharacteristic();
155 const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
156 ? readTorchCharacteristic() : true;
157 const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
159 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 */
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 */
187{
188 Q_D(StatusService);
189 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 */
202{
203 Q_D(StatusService);
204 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 */
217{
218 Q_D(StatusService);
219 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 */
232{
233 Q_D(StatusService);
234 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 */
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 */
281{
282 Q_D(const StatusService);
283 const QLowEnergyCharacteristic characteristic =
284 d->getCharacteristic(CharacteristicUuids::status);
285 return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
286 : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
287 BatteryStatus::Low, std::nullopt, std::nullopt };
288}
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 */
300{
301 Q_D(StatusService);
302 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 */
313{
314 Q_D(StatusService);
315 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 */
326{
327 Q_D(const StatusService);
328 const QLowEnergyCharacteristic characteristic =
329 d->getCharacteristic(CharacteristicUuids::name);
330 return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
331}
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 */
341{
342 Q_D(const StatusService);
343 const QLowEnergyCharacteristic characteristic =
344 d->getCharacteristic(CharacteristicUuids::name);
345 if (!characteristic.isValid()) {
346 return false;
347 }
348
349 const QByteArray value = name.toUtf8();
350 if (value.length() > 11) {
351 qCWarning(d->lc).noquote() << tr(R"(Device name "%1" is too long (%2 > 11 bytes): 0x3)")
352 .arg(name).arg(value.length()).arg(QLatin1String(value.toHex()));
353 return false;
354 }
355
356 d->service->writeCharacteristic(characteristic, value);
357 return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
358}
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 */
376{
377 Q_D(const StatusService);
378 const QLowEnergyCharacteristic characteristic =
379 d->getCharacteristic(CharacteristicUuids::flashLed);
380 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 const QByteArray value(1, '\x01');
389 d->service->writeCharacteristic(characteristic, value);
390 return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
391}
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 */
400std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
401{
402 Q_D(const StatusService);
403 const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
404 return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
405}
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 */
417{
418 Q_D(const StatusService);
419 const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
420 if (!characteristic.isValid()) {
421 return false;
422 }
423
424 const QByteArray value(1, static_cast<char>(status));
425 d->service->writeCharacteristic(characteristic, value);
426 return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
427}
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 */
439{
440 Q_D(StatusService);
441 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 */
452{
453 Q_D(StatusService);
454 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 */
467{
468 Q_D(StatusService);
469 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 */
480{
481 Q_D(StatusService);
482 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 */
492std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
493{
494 Q_D(const StatusService);
495 const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
496 return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
497}
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 */
580
581/*!
582 * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
583 */
585 const QByteArray &value)
586{
588 QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
589 };
590 Q_ASSERT(characteristics.firmwareVersion.isNull()); // How we indicate failure.
591
592 if (!checkSize(QLatin1String("Device Characteristics"), value, 20, 20)) {
593 return characteristics;
594 }
595
596 characteristics.firmwareVersion = QVersionNumber(
597 qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
598 qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
599 characteristics.maximumVoltage = qFromLittleEndian<quint16>(value.mid(2,2).constData());
600 characteristics.maximumCurrent = qFromLittleEndian<quint16>(value.mid(4,2).constData());
601 characteristics.maximumResistance = qFromLittleEndian<quint16>(value.mid(6,2).constData());
602 characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
603 characteristics.samplingBufferSize = qFromLittleEndian<quint16>(value.mid(10,2).constData());
604 characteristics.capabilityMask = qFromLittleEndian<quint16>(value.mid(12,2).constData());
605 characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
606 ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
607
608 qCDebug(lc).noquote() << tr("Firmware version: ") << characteristics.firmwareVersion;
609 qCDebug(lc).noquote() << tr("Maximum voltage: ") << characteristics.maximumVoltage;
610 qCDebug(lc).noquote() << tr("Maximum current: ") << characteristics.maximumCurrent;
611 qCDebug(lc).noquote() << tr("Maximum resistance: ") << characteristics.maximumResistance;
612 qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
613 qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
614 qCDebug(lc).noquote() << tr("Capability mask: ") << characteristics.capabilityMask;
615 qCDebug(lc).noquote() << tr("MAC address: ") << characteristics.macAddress;
616
617 Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
618 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 */
627{
629 static_cast<StatusService::DeviceStatus>
630 (std::numeric_limits<std::underlying_type_t<StatusService::DeviceStatus>>::max()),
631 std::numeric_limits<float>::quiet_NaN(),
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 if (!checkSize(QLatin1String("Status"), value, 5, 8)) {
646 return status;
647 }
648
649 status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
650 status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
651 if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
652 status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
653 }
654 if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
655 status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
656 }
657 if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
658 status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
659 }
660 qCDebug(lc).noquote() << tr("Device status: %1 (%2)")
661 .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
662 qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
663 qCDebug(lc).noquote() << tr("Battery status: %1 (%2)")
664 .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
665 if (status.switchPosition) {
666 qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
667 .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
668 }
669 if (status.chargingStatus) {
670 qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
671 .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
672 }
673 return status;
674}
675
676/*!
677 * Parses the torch status \a value, and returns the corresponding TorchStatus.
678 */
679std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
680{
681 if (!checkSize(QLatin1String("Torch"), value, 1, 1)) {
682 return std::nullopt;
683 }
684
685 const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
686 qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
687 return status;
688}
689
690/*!
691 * Parses the button press \a value, and returns the corresponding ButtonStatus.
692 */
693std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
694{
695 if (!checkSize(QLatin1String("Torch"), value, 2, 2)) {
696 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 const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
709 qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
710 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 */
720{
721 if (newService == StatusService::ServiceUuids::pokitMeter) {
722 qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
724 } else if (newService == StatusService::ServiceUuids::pokitPro) {
725 qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
727 }
729}
730
731/*!
732 * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
733 * specialised signal, for each supported \a characteristic.
734 */
736 const QByteArray &value)
737{
739
740 Q_Q(StatusService);
742 Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
743 return;
744 }
745
746 if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
747 Q_EMIT q->deviceStatusRead(parseStatus(value));
748 return;
749 }
750
751 if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
752 const QString deviceName = QString::fromUtf8(value);
753 qCDebug(lc).noquote() << tr(R"(Device name: "%1")").arg(deviceName);
754 Q_EMIT q->deviceNameRead(deviceName);
755 return;
756 }
757
758 if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
759 qCWarning(lc).noquote() << tr("Flash LED characteristic is write-only, but somehow read")
760 << serviceUuid << characteristic.name() << characteristic.uuid();
761 return;
762 }
763
764 if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
765 if (!checkSize(QLatin1String("Torch"), value, 1, 1)) {
766 return;
767 }
768 const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
769 qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
770 Q_EMIT q->torchStatusRead(status);
771 return;
772 }
773
775 if (!checkSize(QLatin1String("Torch"), value, 2, 2)) {
776 return;
777 }
778 const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
779 qCDebug(lc).noquote() << tr("Button status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
780 Q_EMIT q->buttonPressRead(value.at(0), status);
781 return;
782 }
783
784 qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
785 << 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 */
793 const QByteArray &newValue)
794{
796
797 Q_Q(StatusService);
799 qCWarning(lc).noquote() << tr("Device Characteristics is read-only, but somehow written")
800 << serviceUuid << characteristic.name() << characteristic.uuid();
801 return;
802 }
803
804 if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
805 qCWarning(lc).noquote() << tr("Status characteristic is read-only, but somehow written")
806 << serviceUuid << characteristic.name() << characteristic.uuid();
807 return;
808 }
809
810 if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
811 Q_EMIT q->deviceNameWritten();
812 return;
813 }
814
815 if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
816 Q_EMIT q->deviceLedFlashed();
817 return;
818 }
819
820 if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
821 Q_EMIT q->torchStatusWritten();
822 return;
823 }
824
825 qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
826 << serviceUuid << characteristic.name() << characteristic.uuid();
827}
828
829/// \endcond
The AbstractPokitServicePrivate class provides private implementation for AbstractPokitService.
QBluetoothUuid serviceUuid
UUIDs for service.
virtual void serviceDiscovered(const QBluetoothUuid &newService)
Handles QLowEnergyController::serviceDiscovered events.
virtual void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
Handles QLowEnergyService::characteristicRead events.
virtual void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicWritten events.
static bool checkSize(const QString &label, const QByteArray &data, const int minSize, const int maxSize=-1, const bool failOnMax=false)
Returns false if data is smaller than minSize, otherwise returns failOnMax if data is bigger than max...
The AbstractPokitService class provides a common base for Pokit services classes.
QLowEnergyService * service()
Returns a non-const pointer to the internal service object, if any.
The StatusServicePrivate class provides private implementation for StatusService.
void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) override
Implements AbstractPokitServicePrivate::characteristicRead to parse value, then emit a specialised si...
StatusServicePrivate(QLowEnergyController *controller, StatusService *const q)
static StatusService::Status parseStatus(const QByteArray &value)
Parses the Status value into a Status struct.
void serviceDiscovered(const QBluetoothUuid &newService) override
Handles QLowEnergyController::serviceDiscovered events.
static StatusService::DeviceCharacteristics parseDeviceCharacteristics(const QByteArray &value)
Parses the Device Characteristics value into a DeviceCharacteristics struct.
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) override
Implements AbstractPokitServicePrivate::characteristicWritten to parse newValue, then emit a speciali...
static std::optional< StatusService::ButtonStatus > parseButtonPress(const QByteArray &value)
Parses the button press value, and returns the corresponding ButtonStatus.
static std::optional< StatusService::TorchStatus > parseTorchStatus(const QByteArray &value)
Parses the torch status value, and returns the corresponding TorchStatus.
The StatusService class accesses the Pokit Status service of Pokit devices.
DeviceCharacteristics deviceCharacteristics() const
Returns the most recent value of the Status service's Device Characteristics characteristic.
bool enableButtonPressedNotifications()
Enables client-side notifications of button presses.
bool readStatusCharacteristic()
Read the Status service's Status characteristic.
StatusService(QLowEnergyController *const pokitDevice, QObject *parent=nullptr)
Constructs a new Pokit service with parent.
QString deviceName() const
Returns the most recent value of the Status services's Device Name characteristic.
ButtonStatus
Values supported by the second byte of the attribute of the (undocumented) Button Press characteristi...
@ Pressed
Button was pressed.
@ Released
Button was released.
@ Held
Button was held down (for typically 1,500ms).
bool readNameCharacteristic()
Read the Status service's Name characteristic.
bool disableStatusNotifications()
Disables client-side notifications of device status changes.
BatteryStatus
Values supported by the Battery Status attribute of the Status characteristic.
@ Low
Low (replace battery).
bool readButtonPressCharacteristic()
Read the Status service's (undocumented) Button Press characteristic.
bool setTorchStatus(const TorchStatus status)
Set the Pokit device's torch to status.
bool flashLed()
Flash the Pokit device's LED.
DeviceStatus
Values supported by the Status attribute of the Status characteristic.
@ MultimeterContinuity
Multimeter is measuring continuity.
@ MultimeterAcCurrent
Multimeter is measuring AC current.
@ LoggerModeSampling
Data Logger is sampling.
@ MultimeterDcVoltage
Multimeter is measuring DC voltage.
@ DsoModeSampling
DSO is sampling.
@ MultimeterDcCurrent
Multimeter is measuring DC current.
@ MultimeterTemperature
Multimeter is measuring temperature.
@ MultimeterResistance
Multimeter is measuring resistance.
@ MultimeterDiode
Multimeter is measuring diode.
@ MultimeterAcVoltage
Multimeter is measuring AC voltage.
bool enableStatusNotifications()
Enables client-side notifications of device status changes.
~StatusService() override
Destroys this StatusService object.
std::optional< ButtonStatus > buttonPress() const
Returns the most recent value of the Status services's Button Press characteristic.
bool enableTorchStatusNotifications()
Enables client-side notifications of torch status changes.
SwitchPosition
Values supported by the (undocumented) Switch Position attribute of the Status characteristic.
@ HighCurrent
Device is switched to High Current position.
@ Voltage
Device is switched to Voltage position.
@ MultiMode
Device is switched to Resistance / Low Current / Capacitance / Diode position.
bool disableTorchStatusNotifications()
Disables client-side notifications of torch status changes.
static QString toString(const StatusService::DeviceStatus &status)
Returns a string version of the status enum label.
bool disableButtonPressedNotifications()
Disables client-side notifications of button presses.
TorchStatus
Values supported by the single byte of the attribute of the (undocumented) Torch characteristic.
std::optional< TorchStatus > torchStatus() const
Returns the most recent value of the Status services's Torch characteristic.
bool readCharacteristics() override
Read all characteristics.
bool setDeviceName(const QString &name)
Set's the Pokit device's name to name.
bool readDeviceCharacteristics()
Read the Status service's Device Characteristics characteristic.
Status status() const
Returns the most recent value of the Status service's Status characteristic.
ChargingStatus
Values supported by the (undocumented) Charging Statue attribute of the Status characteristic.
@ Discharging
Battery is dischargin.
@ Charged
Battery is fully charged.
@ Charging
Battery is being charged.
bool readTorchCharacteristic()
Read the Status service's (undocumented) Torch characteristic.
char at(int i) const const
const char * constData() const const
int length() const const
QByteArray mid(int pos, int len) const const
int size() const const
QByteArray toHex() const const
bool isValid() const const
QString name() const const
QBluetoothUuid uuid() const const
QByteArray value() const const
QLowEnergyCharacteristic characteristic(const QBluetoothUuid &uuid) const const
Q_EMITQ_EMIT
QString tr(const char *sourceText, const char *disambiguation, int n)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString fromUtf8(const char *str, int size)
QByteArray toUtf8() const const
Declares the StatusService class.
Declares the StatusServicePrivate class.
static const QBluetoothUuid name
UUID of the Pokit Status service's Device Name characterstic.
static const QBluetoothUuid torch
UUID of the Pokit Status service's (undocumented) Torch characterstic.
static const QBluetoothUuid deviceCharacteristics
UUID of the Pokit Status service's Device Characteristics characterstic.
static const QBluetoothUuid buttonPress
UUID of the Pokit Status service's (undocumented) Button Press characterstic.
static const QBluetoothUuid flashLed
UUID of the Pokit Status service's Flash LED characterstic.
static const QBluetoothUuid status
UUID of the Pokit Status service's Status characterstic.
Attributes included in the Device Characteristics characterstic.
static const QBluetoothUuid pokitPro
UUID of the Pokit Pro's Pokit Status service.
static const QBluetoothUuid pokitMeter
UUID of the Pokit Meter's Pokit Status service.
Attributes included in the Status characterstic.
DeviceStatus deviceStatus
Current Pokit device status.