LCOV - code coverage report
Current view: top level - src/lib - statusservice.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 81.7 % 389 318
Version: Functions: 97.2 % 36 35

            Line data    Source code
       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              : 
       9              : #include <qtpokit/statusservice.h>
      10              : #include "statusservice_p.h"
      11              : #include "../stringliterals_p.h"
      12              : 
      13              : #include <QtEndian>
      14              : 
      15              : QTPOKIT_BEGIN_NAMESPACE
      16              : DOKIT_USE_STRINGLITERALS
      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              :  */
      36         1760 : QString StatusService::toString(const StatusService::DeviceStatus &status)
      37          944 : {
      38         2704 :     switch (status) {
      39         1072 :     case DeviceStatus::Idle:                 return  u"Idle"_s;
      40          136 :     case DeviceStatus::MultimeterDcVoltage:  return  u"MultimeterDcVoltage"_s;
      41          136 :     case DeviceStatus::MultimeterAcVoltage:  return  u"MultimeterAcVoltage"_s;
      42          136 :     case DeviceStatus::MultimeterDcCurrent:  return  u"MultimeterDcCurrent"_s;
      43          136 :     case DeviceStatus::MultimeterAcCurrent:  return  u"MultimeterAcCurrent"_s;
      44          136 :     case DeviceStatus::MultimeterResistance: return  u"MultimeterResistance"_s;
      45          136 :     case DeviceStatus::MultimeterDiode:      return  u"MultimeterDiode"_s;
      46          136 :     case DeviceStatus::MultimeterContinuity: return  u"MultimeterContinuity"_s;
      47          136 :     case DeviceStatus::MultimeterTemperature:return  u"MultimeterTemperature"_s;
      48          136 :     case DeviceStatus::DsoModeSampling:      return  u"DsoModeSampling"_s;
      49          136 :     case DeviceStatus::LoggerModeSampling:   return  u"LoggerModeSampling"_s;
      50          944 :     }
      51          112 :     return QString();
      52          944 : }
      53              : 
      54              : /*!
      55              :  * Returns a string version of the \a status enum label.
      56              :  */
      57         1040 : QString StatusService::toString(const StatusService::BatteryStatus &status)
      58          440 : {
      59         1480 :     switch (status) {
      60         1072 :     case BatteryStatus::Low:  return u"Low"_s;
      61          136 :     case BatteryStatus::Good: return u"Good"_s;
      62          440 :     }
      63          112 :     return QString();
      64          440 : }
      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              :  */
      77         1300 : QString StatusService::toString(const StatusService::SwitchPosition &position)
      78          860 : {
      79         2160 :     switch (position) {
      80          432 :     case SwitchPosition::Voltage:     return u"Voltage"_s;
      81         1172 :     case SwitchPosition::MultiMode:   return u"MultiMode"_s;
      82          284 :     case SwitchPosition::HighCurrent: return u"HighCurrent"_s;
      83          860 :     }
      84          112 :     return QString();
      85          860 : }
      86              : 
      87              : /*!
      88              :  * Returns a string version of the \a status enum label.
      89              :  */
      90          400 : QString StatusService::toString(const StatusService::ChargingStatus &status)
      91          280 : {
      92          680 :     switch (status) {
      93          136 :     case ChargingStatus::Discharging: return u"Discharging"_s;
      94          136 :     case ChargingStatus::Charging:    return u"Charging"_s;
      95          136 :     case ChargingStatus::Charged:     return u"Charged"_s;
      96          280 :     }
      97          112 :     return QString();
      98          280 : }
      99              : 
     100              : /*!
     101              :  * Returns a string version of the \a status enum label.
     102              :  */
     103          950 : QString StatusService::toString(const StatusService::TorchStatus &status)
     104          630 : {
     105         1580 :     switch (status) {
     106          876 :     case TorchStatus::Off: return u"Off"_s;
     107          432 :     case TorchStatus::On:  return u"On"_s;
     108          630 :     }
     109          112 :     return QString();
     110          630 : }
     111              : 
     112              : /*!
     113              :  * Returns a string version of the \a status enum label.
     114              :  */
     115          670 : QString StatusService::toString(const StatusService::ButtonStatus &status)
     116          454 : {
     117         1124 :     switch (status) {
     118          284 :     case ButtonStatus::Released: return u"Released"_s;
     119          284 :     case ButtonStatus::Pressed:  return u"Pressed"_s;
     120          284 :     case ButtonStatus::Held:     return u"Held"_s;
     121          454 :     }
     122          112 :     return QString();
     123          454 : }
     124              : 
     125              : /*!
     126              :  * Constructs a new Pokit service with \a parent.
     127              :  */
     128         2800 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
     129         4040 :     : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
     130         1640 : {
     131              : 
     132         4440 : }
     133              : 
     134              : /*!
     135              :  * \cond internal
     136              :  * Constructs a new Pokit service with \a parent, and private implementation \a d.
     137              :  */
     138            0 : StatusService::StatusService(
     139            0 :     StatusServicePrivate * const d, QObject * const parent)
     140            0 :     : AbstractPokitService(d, parent)
     141            0 : {
     142              : 
     143            0 : }
     144              : /// \endcond
     145              : 
     146           80 : bool StatusService::readCharacteristics()
     147           56 : {
     148          101 :     const bool r1 = readDeviceCharacteristics();
     149          101 :     const bool r2 = readStatusCharacteristic();
     150          101 :     const bool r3 = readNameCharacteristic();
     151          157 :     const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
     152          136 :         ? readTorchCharacteristic() : true;
     153          157 :     const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
     154          136 :         ? readButtonPressCharacteristic() : true;
     155          216 :     return (r1 && r2 && r3 && r4 && r5);
     156           56 : }
     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              :  */
     167          125 : bool StatusService::readDeviceCharacteristics()
     168          112 : {
     169          112 :     Q_D(StatusService);
     170          272 :     return d->readCharacteristic(CharacteristicUuids::deviceCharacteristics);
     171          112 : }
     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              :  */
     182          125 : bool StatusService::readStatusCharacteristic()
     183          112 : {
     184          112 :     Q_D(StatusService);
     185          272 :     return d->readCharacteristic(CharacteristicUuids::status);
     186          112 : }
     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              :  */
     197          125 : bool StatusService::readNameCharacteristic()
     198          112 : {
     199          112 :     Q_D(StatusService);
     200          272 :     return d->readCharacteristic(CharacteristicUuids::name);
     201          112 : }
     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              :  */
     212           80 : bool StatusService::readTorchCharacteristic()
     213           56 : {
     214           56 :     Q_D(StatusService);
     215          136 :     return d->readCharacteristic(CharacteristicUuids::torch);
     216           56 : }
     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              :  */
     227           80 : bool StatusService::readButtonPressCharacteristic()
     228           56 : {
     229           56 :     Q_D(StatusService);
     230          136 :     return d->readCharacteristic(CharacteristicUuids::buttonPress);
     231           56 : }
     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              :  */
     248          160 : StatusService::DeviceCharacteristics StatusService::deviceCharacteristics() const
     249           80 : {
     250           80 :     Q_D(const StatusService);
     251           80 :     const QLowEnergyCharacteristic characteristic =
     252          240 :         d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
     253          240 :     return (characteristic.isValid())
     254          198 :         ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
     255          400 :         : StatusService::DeviceCharacteristics();
     256          240 : }
     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              :  */
     276          800 : StatusService::Status StatusService::status() const
     277          272 : {
     278          272 :     Q_D(const StatusService);
     279          272 :     const QLowEnergyCharacteristic characteristic =
     280         1072 :         d->getCharacteristic(CharacteristicUuids::status);
     281         1072 :     return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
     282          272 :         : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
     283         1872 :                                  BatteryStatus::Low, std::nullopt, std::nullopt };
     284         1072 : }
     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              :  */
     295           80 : bool StatusService::enableStatusNotifications()
     296           56 : {
     297           56 :     Q_D(StatusService);
     298          136 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::status);
     299           56 : }
     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              :  */
     308           80 : bool StatusService::disableStatusNotifications()
     309           56 : {
     310           56 :     Q_D(StatusService);
     311          136 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::status);
     312           56 : }
     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              :  */
     321          800 : QString StatusService::deviceName() const
     322          272 : {
     323          272 :     Q_D(const StatusService);
     324          272 :     const QLowEnergyCharacteristic characteristic =
     325         1072 :         d->getCharacteristic(CharacteristicUuids::name);
     326         1872 :     return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
     327         1072 : }
     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              :  */
     336           80 : bool StatusService::setDeviceName(const QString &name)
     337           56 : {
     338           56 :     Q_D(const StatusService);
     339           56 :     const QLowEnergyCharacteristic characteristic =
     340          136 :         d->getCharacteristic(CharacteristicUuids::name);
     341          136 :     if (!characteristic.isValid()) {
     342           56 :         return false;
     343           56 :     }
     344              : 
     345            0 :     const QByteArray value = name.toUtf8();
     346            0 :     if (value.length() > 11) {
     347            0 :         qCWarning(d->lc).noquote() << tr(R"(Device name "%1" is too long (%2 > 11 bytes): 0x3)")
     348            0 :             .arg(name).arg(value.length()).arg(QLatin1String(value.toHex()));
     349            0 :         return false;
     350            0 :     }
     351              : 
     352            0 :     d->service->writeCharacteristic(characteristic, value);
     353            0 :     return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
     354           80 : }
     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              :  */
     371           80 : bool StatusService::flashLed()
     372           56 : {
     373           56 :     Q_D(const StatusService);
     374           56 :     const QLowEnergyCharacteristic characteristic =
     375          136 :         d->getCharacteristic(CharacteristicUuids::flashLed);
     376          136 :     if (!characteristic.isValid()) {
     377           56 :         return false;
     378           56 :     }
     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            0 :     const QByteArray value(1, '\x01');
     385            0 :     d->service->writeCharacteristic(characteristic, value);
     386            0 :     return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
     387          136 : }
     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              :  */
     396          800 : std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
     397          272 : {
     398          272 :     Q_D(const StatusService);
     399         1072 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     400         1872 :     return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
     401         1072 : }
     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              :  */
     412           80 : bool StatusService::setTorchStatus(const StatusService::TorchStatus status)
     413           56 : {
     414           56 :     Q_D(const StatusService);
     415          136 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     416          136 :     if (!characteristic.isValid()) {
     417           56 :         return false;
     418           56 :     }
     419              : 
     420            0 :     const QByteArray value(1, static_cast<char>(status));
     421            0 :     d->service->writeCharacteristic(characteristic, value);
     422            0 :     return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
     423          136 : }
     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              :  */
     434           80 : bool StatusService::enableTorchStatusNotifications()
     435           56 : {
     436           56 :     Q_D(StatusService);
     437          136 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::torch);
     438           56 : }
     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              :  */
     447           80 : bool StatusService::disableTorchStatusNotifications()
     448           56 : {
     449           56 :     Q_D(StatusService);
     450          136 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::torch);
     451           56 : }
     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              :  */
     462           80 : bool StatusService::enableButtonPressedNotifications()
     463           56 : {
     464           56 :     Q_D(StatusService);
     465          136 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
     466           56 : }
     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              :  */
     475           80 : bool StatusService::disableButtonPressedNotifications()
     476           56 : {
     477           56 :     Q_D(StatusService);
     478          136 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
     479           56 : }
     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              :  */
     488          800 : std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
     489          272 : {
     490          272 :     Q_D(const StatusService);
     491         1072 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
     492         1872 :     return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
     493         1072 : }
     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              :  */
     570         2240 : StatusServicePrivate::StatusServicePrivate(
     571         2800 :     QLowEnergyController * controller, StatusService * const q)
     572         4040 :     : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
     573         1640 : {
     574              : 
     575         3880 : }
     576              : 
     577              : /*!
     578              :  * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
     579              :  */
     580          320 : StatusService::DeviceCharacteristics StatusServicePrivate::parseDeviceCharacteristics(
     581              :     const QByteArray &value)
     582          224 : {
     583          544 :     StatusService::DeviceCharacteristics characteristics{
     584          224 :         QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
     585          428 :     };
     586          224 :     Q_ASSERT(characteristics.firmwareVersion.isNull());  // How we indicate failure.
     587              : 
     588          716 :     if (!checkSize(u"Device Characteristics"_s, value, 20, 20)) {
     589          112 :         return characteristics;
     590          112 :     }
     591              : 
     592          296 :     characteristics.firmwareVersion = QVersionNumber(
     593          402 :                                           qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
     594          360 :                                           qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
     595          358 :     characteristics.maximumVoltage      = qFromLittleEndian<quint16>(value.mid(2,2).constData());
     596          358 :     characteristics.maximumCurrent      = qFromLittleEndian<quint16>(value.mid(4,2).constData());
     597          358 :     characteristics.maximumResistance   = qFromLittleEndian<quint16>(value.mid(6,2).constData());
     598          358 :     characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
     599          358 :     characteristics.samplingBufferSize  = qFromLittleEndian<quint16>(value.mid(10,2).constData());
     600          358 :     characteristics.capabilityMask      = qFromLittleEndian<quint16>(value.mid(12,2).constData());
     601          334 :     characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
     602          520 :                                                    ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
     603              : 
     604          330 :     qCDebug(lc).noquote() << tr("Firmware version:     ") << characteristics.firmwareVersion;
     605          330 :     qCDebug(lc).noquote() << tr("Maximum voltage:      ") << characteristics.maximumVoltage;
     606          330 :     qCDebug(lc).noquote() << tr("Maximum current:      ") << characteristics.maximumCurrent;
     607          330 :     qCDebug(lc).noquote() << tr("Maximum resistance:   ") << characteristics.maximumResistance;
     608          330 :     qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
     609          330 :     qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
     610          330 :     qCDebug(lc).noquote() << tr("Capability mask:      ") << characteristics.capabilityMask;
     611          330 :     qCDebug(lc).noquote() << tr("MAC address:          ") << characteristics.macAddress;
     612              : 
     613          112 :     Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
     614          130 :     return characteristics;
     615          224 : }
     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              :  */
     622         1040 : StatusService::Status StatusServicePrivate::parseStatus(const QByteArray &value)
     623          728 : {
     624         1768 :     StatusService::Status status{
     625          728 :         static_cast<StatusService::DeviceStatus>
     626          728 :             (std::numeric_limits<std::underlying_type_t<StatusService::DeviceStatus>>::max()),
     627          728 :         std::numeric_limits<float>::quiet_NaN(),
     628          728 :         static_cast<StatusService::BatteryStatus>
     629          728 :             (std::numeric_limits<std::underlying_type_t<StatusService::BatteryStatus>>::max()),
     630          728 :         std::nullopt, std::nullopt,
     631          728 :     };
     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         2327 :     if (!checkSize(u"Status"_s, value, 5, 8)) {
     642          210 :         return status;
     643          112 :     }
     644              : 
     645         1496 :     status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
     646         1969 :     status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
     647         1496 :     if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
     648         1360 :         status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
     649          560 :     }
     650         1496 :     if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
     651         1360 :         status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
     652          560 :     }
     653         1496 :     if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
     654         1360 :         status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
     655          560 :     }
     656         1815 :     qCDebug(lc).noquote() << tr("Device status:   %1 (%2)")
     657            0 :         .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
     658         1815 :     qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
     659         1815 :     qCDebug(lc).noquote() << tr("Battery status:  %1 (%2)")
     660            0 :         .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
     661         1496 :     if (status.switchPosition) {
     662         1650 :         qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
     663            0 :             .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
     664          560 :     }
     665         1496 :     if (status.chargingStatus) {
     666         1650 :         qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
     667            0 :         .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
     668          560 :     }
     669         1155 :     return status;
     670          728 : }
     671              : 
     672              : /*!
     673              :  * Parses the torch status \a value, and returns the corresponding TorchStatus.
     674              :  */
     675          240 : std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
     676          168 : {
     677          537 :     if (!checkSize(u"Torch"_s, value, 1, 1)) {
     678          136 :         return std::nullopt;
     679           56 :     }
     680              : 
     681          272 :     const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
     682          330 :     qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     683          272 :     return status;
     684          168 : }
     685              : 
     686              : /*!
     687              :  * Parses the button press \a value, and returns the corresponding ButtonStatus.
     688              :  */
     689          320 : std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
     690          224 : {
     691          716 :     if (!checkSize(u"Torch"_s, value, 2, 2)) {
     692          136 :         return std::nullopt;
     693           56 :     }
     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          408 :     const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
     705          495 :     qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     706          408 :     return status;
     707          224 : }
     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              :  */
     715          320 : void StatusServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
     716          224 : {
     717          544 :     if (newService == StatusService::ServiceUuids::pokitMeter) {
     718          165 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
     719          136 :         serviceUuid = StatusService::ServiceUuids::pokitMeter;
     720          408 :     } else if (newService == StatusService::ServiceUuids::pokitPro) {
     721          165 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
     722          136 :         serviceUuid = StatusService::ServiceUuids::pokitPro;
     723           56 :     }
     724          544 :     AbstractPokitServicePrivate::serviceDiscovered(newService);
     725          544 : }
     726              : 
     727              : /*!
     728              :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     729              :  * specialised signal, for each supported \a characteristic.
     730              :  */
     731           80 : void StatusServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     732              :                                               const QByteArray &value)
     733           56 : {
     734          136 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     735              : 
     736           56 :     Q_Q(StatusService);
     737          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
     738            0 :         Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
     739            0 :         return;
     740            0 :     }
     741              : 
     742          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
     743            0 :         Q_EMIT q->deviceStatusRead(parseStatus(value));
     744            0 :         return;
     745            0 :     }
     746              : 
     747          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
     748            0 :         const QString deviceName = QString::fromUtf8(value);
     749            0 :         qCDebug(lc).noquote() << tr(R"(Device name: "%1")").arg(deviceName);
     750            0 :         Q_EMIT q->deviceNameRead(deviceName);
     751            0 :         return;
     752            0 :     }
     753              : 
     754          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
     755            0 :         qCWarning(lc).noquote() << tr("Flash LED characteristic is write-only, but somehow read")
     756            0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     757            0 :         return;
     758            0 :     }
     759              : 
     760          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
     761            0 :         if (!checkSize(u"Torch"_s, value, 1, 1)) {
     762            0 :             return;
     763            0 :         }
     764            0 :         const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
     765            0 :         qCDebug(lc).noquote() << tr("Torch status:  %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     766            0 :         Q_EMIT q->torchStatusRead(status);
     767            0 :         return;
     768            0 :     }
     769              : 
     770          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::buttonPress) {
     771            0 :         if (!checkSize(u"Torch"_s, value, 2, 2)) {
     772            0 :             return;
     773            0 :         }
     774            0 :         const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
     775            0 :         qCDebug(lc).noquote() << tr("Button status:  %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     776            0 :         Q_EMIT q->buttonPressRead(value.at(0), status);
     777            0 :         return;
     778            0 :     }
     779              : 
     780          354 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
     781          226 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     782           56 : }
     783              : 
     784              : /*!
     785              :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     786              :  * specialised signal, for each supported \a characteristic.
     787              :  */
     788           80 : void StatusServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     789              :                                                  const QByteArray &newValue)
     790           56 : {
     791          136 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     792              : 
     793           56 :     Q_Q(StatusService);
     794          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
     795            0 :         qCWarning(lc).noquote() << tr("Device Characteristics is read-only, but somehow written")
     796            0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     797            0 :         return;
     798            0 :     }
     799              : 
     800          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
     801            0 :         qCWarning(lc).noquote() << tr("Status characteristic is read-only, but somehow written")
     802            0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     803            0 :         return;
     804            0 :     }
     805              : 
     806          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
     807            0 :         Q_EMIT q->deviceNameWritten();
     808            0 :         return;
     809            0 :     }
     810              : 
     811          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
     812            0 :         Q_EMIT q->deviceLedFlashed();
     813            0 :         return;
     814            0 :     }
     815              : 
     816          136 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
     817            0 :         Q_EMIT q->torchStatusWritten();
     818            0 :         return;
     819            0 :     }
     820              : 
     821          354 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
     822          226 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     823           56 : }
     824              : 
     825              : /// \endcond
     826              : 
     827              : QTPOKIT_END_NAMESPACE
        

Generated by: LCOV version 2.3.1-1