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