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-2026 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         2266 : QString StatusService::toString(const StatusService::DeviceStatus &status)
      37         2168 : {
      38         4434 :     switch (status) {
      39         1662 :     case DeviceStatus::Idle:                 return  u"Idle"_s;
      40          231 :     case DeviceStatus::MultimeterDcVoltage:  return  u"MultimeterDcVoltage"_s;
      41          231 :     case DeviceStatus::MultimeterAcVoltage:  return  u"MultimeterAcVoltage"_s;
      42          231 :     case DeviceStatus::MultimeterDcCurrent:  return  u"MultimeterDcCurrent"_s;
      43          231 :     case DeviceStatus::MultimeterAcCurrent:  return  u"MultimeterAcCurrent"_s;
      44          231 :     case DeviceStatus::MultimeterResistance: return  u"MultimeterResistance"_s;
      45          231 :     case DeviceStatus::MultimeterDiode:      return  u"MultimeterDiode"_s;
      46          231 :     case DeviceStatus::MultimeterContinuity: return  u"MultimeterContinuity"_s;
      47          231 :     case DeviceStatus::MultimeterTemperature:return  u"MultimeterTemperature"_s;
      48          231 :     case DeviceStatus::DsoModeSampling:      return  u"DsoModeSampling"_s;
      49          231 :     case DeviceStatus::LoggerModeSampling:   return  u"LoggerModeSampling"_s;
      50         2168 :     }
      51          294 :     return QString();
      52         2168 : }
      53              : 
      54              : /*!
      55              :  * Returns a string version of the \a status enum label.
      56              :  */
      57         1339 : QString StatusService::toString(const StatusService::BatteryStatus &status)
      58         1016 : {
      59         2355 :     switch (status) {
      60         1662 :     case BatteryStatus::Low:  return u"Low"_s;
      61          231 :     case BatteryStatus::Good: return u"Good"_s;
      62         1016 :     }
      63          294 :     return QString();
      64         1016 : }
      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         1335 : QString StatusService::toString(const StatusService::SwitchPosition &position)
      78         1800 : {
      79         3135 :     switch (position) {
      80          627 :     case SwitchPosition::Voltage:     return u"Voltage"_s;
      81         1617 :     case SwitchPosition::MultiMode:   return u"MultiMode"_s;
      82          429 :     case SwitchPosition::HighCurrent: return u"HighCurrent"_s;
      83         1800 :     }
      84          294 :     return QString();
      85         1800 : }
      86              : 
      87              : /*!
      88              :  * Returns a string version of the \a status enum label.
      89              :  */
      90          515 : QString StatusService::toString(const StatusService::ChargingStatus &status)
      91          640 : {
      92         1155 :     switch (status) {
      93          231 :     case ChargingStatus::Discharging: return u"Discharging"_s;
      94          231 :     case ChargingStatus::Charging:    return u"Charging"_s;
      95          231 :     case ChargingStatus::Charged:     return u"Charged"_s;
      96          640 :     }
      97          294 :     return QString();
      98          640 : }
      99              : 
     100              : /*!
     101              :  * Returns a string version of the \a status enum label.
     102              :  */
     103          986 : QString StatusService::toString(const StatusService::TorchStatus &status)
     104         1324 : {
     105         2310 :     switch (status) {
     106         1221 :     case TorchStatus::Off: return u"Off"_s;
     107          627 :     case TorchStatus::On:  return u"On"_s;
     108         1324 :     }
     109          294 :     return QString();
     110         1324 : }
     111              : 
     112              : /*!
     113              :  * Returns a string version of the \a status enum label.
     114              :  */
     115          761 : QString StatusService::toString(const StatusService::ButtonStatus &status)
     116          988 : {
     117         1749 :     switch (status) {
     118          429 :     case ButtonStatus::Released: return u"Released"_s;
     119          429 :     case ButtonStatus::Pressed:  return u"Pressed"_s;
     120          429 :     case ButtonStatus::Held:     return u"Held"_s;
     121          988 :     }
     122          294 :     return QString();
     123          988 : }
     124              : 
     125              : /*!
     126              :  * Constructs a new Pokit service with \a parent.
     127              :  */
     128         3605 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
     129         7365 :     : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
     130         3760 : {
     131              : 
     132         7365 : }
     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          103 : bool StatusService::readCharacteristics()
     147          128 : {
     148          207 :     const bool r1 = readDeviceCharacteristics();
     149          207 :     const bool r2 = readStatusCharacteristic();
     150          207 :     const bool r3 = readNameCharacteristic();
     151          254 :     const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
     152          231 :         ? readTorchCharacteristic() : true;
     153          254 :     const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
     154          231 :         ? readButtonPressCharacteristic() : true;
     155          334 :     return (r1 && r2 && r3 && r4 && r5);
     156          128 : }
     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          182 : bool StatusService::readDeviceCharacteristics()
     168          256 : {
     169          256 :     Q_D(StatusService);
     170          462 :     return d->readCharacteristic(CharacteristicUuids::deviceCharacteristics);
     171          256 : }
     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          182 : bool StatusService::readStatusCharacteristic()
     183          256 : {
     184          256 :     Q_D(StatusService);
     185          462 :     return d->readCharacteristic(CharacteristicUuids::status);
     186          256 : }
     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          182 : bool StatusService::readNameCharacteristic()
     198          256 : {
     199          256 :     Q_D(StatusService);
     200          462 :     return d->readCharacteristic(CharacteristicUuids::name);
     201          256 : }
     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          103 : bool StatusService::readTorchCharacteristic()
     213          128 : {
     214          128 :     Q_D(StatusService);
     215          231 :     return d->readCharacteristic(CharacteristicUuids::torch);
     216          128 : }
     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          103 : bool StatusService::readButtonPressCharacteristic()
     228          128 : {
     229          128 :     Q_D(StatusService);
     230          231 :     return d->readCharacteristic(CharacteristicUuids::buttonPress);
     231          128 : }
     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          206 : StatusService::DeviceCharacteristics StatusService::deviceCharacteristics() const
     249          184 : {
     250          184 :     Q_D(const StatusService);
     251          184 :     const QLowEnergyCharacteristic characteristic =
     252          390 :         d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
     253          390 :     return (characteristic.isValid())
     254          344 :         ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
     255          596 :         : StatusService::DeviceCharacteristics();
     256          390 : }
     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         1030 : StatusService::Status StatusService::status() const
     277          632 : {
     278          632 :     Q_D(const StatusService);
     279          632 :     const QLowEnergyCharacteristic characteristic =
     280         1662 :         d->getCharacteristic(CharacteristicUuids::status);
     281         1662 :     return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
     282          632 :         : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
     283         2692 :                                  BatteryStatus::Low, std::nullopt, std::nullopt };
     284         1662 : }
     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          103 : bool StatusService::enableStatusNotifications()
     296          128 : {
     297          128 :     Q_D(StatusService);
     298          231 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::status);
     299          128 : }
     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          103 : bool StatusService::disableStatusNotifications()
     309          128 : {
     310          128 :     Q_D(StatusService);
     311          231 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::status);
     312          128 : }
     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         1030 : QString StatusService::deviceName() const
     322          632 : {
     323          632 :     Q_D(const StatusService);
     324          632 :     const QLowEnergyCharacteristic characteristic =
     325         1662 :         d->getCharacteristic(CharacteristicUuids::name);
     326         2692 :     return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
     327         1662 : }
     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          103 : bool StatusService::setDeviceName(const QString &name)
     337          128 : {
     338          128 :     Q_D(const StatusService);
     339          128 :     const QLowEnergyCharacteristic characteristic =
     340          231 :         d->getCharacteristic(CharacteristicUuids::name);
     341          231 :     if (!characteristic.isValid()) {
     342          128 :         return false;
     343          128 :     }
     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          103 : }
     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          103 : bool StatusService::flashLed()
     372          128 : {
     373          128 :     Q_D(const StatusService);
     374          128 :     const QLowEnergyCharacteristic characteristic =
     375          231 :         d->getCharacteristic(CharacteristicUuids::flashLed);
     376          231 :     if (!characteristic.isValid()) {
     377          128 :         return false;
     378          128 :     }
     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          231 : }
     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         1030 : std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
     397          632 : {
     398          632 :     Q_D(const StatusService);
     399         1662 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     400         2692 :     return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
     401         1662 : }
     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          103 : bool StatusService::setTorchStatus(const StatusService::TorchStatus status)
     413          128 : {
     414          128 :     Q_D(const StatusService);
     415          231 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     416          231 :     if (!characteristic.isValid()) {
     417          128 :         return false;
     418          128 :     }
     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          231 : }
     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          103 : bool StatusService::enableTorchStatusNotifications()
     435          128 : {
     436          128 :     Q_D(StatusService);
     437          231 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::torch);
     438          128 : }
     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          103 : bool StatusService::disableTorchStatusNotifications()
     448          128 : {
     449          128 :     Q_D(StatusService);
     450          231 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::torch);
     451          128 : }
     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          103 : bool StatusService::enableButtonPressedNotifications()
     463          128 : {
     464          128 :     Q_D(StatusService);
     465          231 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
     466          128 : }
     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          103 : bool StatusService::disableButtonPressedNotifications()
     476          128 : {
     477          128 :     Q_D(StatusService);
     478          231 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
     479          128 : }
     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         1030 : std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
     489          632 : {
     490          632 :     Q_D(const StatusService);
     491         1662 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
     492         2692 :     return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
     493         1662 : }
     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         2975 : StatusServicePrivate::StatusServicePrivate(
     571         3605 :     QLowEnergyController * controller, StatusService * const q)
     572         7365 :     : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
     573         3760 : {
     574              : 
     575         6735 : }
     576              : 
     577              : /*!
     578              :  * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
     579              :  */
     580          412 : StatusService::DeviceCharacteristics StatusServicePrivate::parseDeviceCharacteristics(
     581              :     const QByteArray &value)
     582          512 : {
     583          924 :     StatusService::DeviceCharacteristics characteristics{
     584          512 :         QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
     585          700 :     };
     586          512 :     Q_ASSERT(characteristics.firmwareVersion.isNull());  // How we indicate failure.
     587              : 
     588         1204 :     if (!checkSize(u"Device Characteristics"_s, value, 20, 20)) {
     589          256 :         return characteristics;
     590          256 :     }
     591              : 
     592          490 :     characteristics.firmwareVersion = QVersionNumber(
     593          586 :                                           qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
     594          622 :                                           qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
     595          602 :     characteristics.maximumVoltage      = qFromLittleEndian<quint16>(value.mid(2,2).constData());
     596          602 :     characteristics.maximumCurrent      = qFromLittleEndian<quint16>(value.mid(4,2).constData());
     597          602 :     characteristics.maximumResistance   = qFromLittleEndian<quint16>(value.mid(6,2).constData());
     598          602 :     characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
     599          602 :     characteristics.samplingBufferSize  = qFromLittleEndian<quint16>(value.mid(10,2).constData());
     600          602 :     characteristics.capabilityMask      = qFromLittleEndian<quint16>(value.mid(12,2).constData());
     601          506 :     characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
     602          738 :                                                    ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
     603              : 
     604          572 :     qCDebug(lc).noquote() << tr("Firmware version:     ") << characteristics.firmwareVersion;
     605          572 :     qCDebug(lc).noquote() << tr("Maximum voltage:      ") << characteristics.maximumVoltage;
     606          572 :     qCDebug(lc).noquote() << tr("Maximum current:      ") << characteristics.maximumCurrent;
     607          572 :     qCDebug(lc).noquote() << tr("Maximum resistance:   ") << characteristics.maximumResistance;
     608          572 :     qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
     609          572 :     qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
     610          572 :     qCDebug(lc).noquote() << tr("Capability mask:      ") << characteristics.capabilityMask;
     611          572 :     qCDebug(lc).noquote() << tr("MAC address:          ") << characteristics.macAddress;
     612              : 
     613          256 :     Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
     614          276 :     return characteristics;
     615          512 : }
     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         1339 : StatusService::Status StatusServicePrivate::parseStatus(const QByteArray &value)
     623         1664 : {
     624         3003 :     StatusService::Status status{
     625         1664 :         static_cast<StatusService::DeviceStatus>
     626         1664 :             (std::numeric_limits<std::underlying_type_t<StatusService::DeviceStatus>>::max()),
     627         1664 :         std::numeric_limits<float>::quiet_NaN(),
     628         1664 :         static_cast<StatusService::BatteryStatus>
     629         1664 :             (std::numeric_limits<std::underlying_type_t<StatusService::BatteryStatus>>::max()),
     630         1664 :         std::nullopt, std::nullopt,
     631         1664 :     };
     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         3913 :     if (!checkSize(u"Status"_s, value, 5, 8)) {
     642          414 :         return status;
     643          256 :     }
     644              : 
     645         2464 :     status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
     646         3311 :     status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
     647         2541 :     if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
     648         2310 :         status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
     649         1280 :     }
     650         2541 :     if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
     651         2310 :         status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
     652         1280 :     }
     653         2541 :     if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
     654         2310 :         status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
     655         1280 :     }
     656         3146 :     qCDebug(lc).noquote() << tr("Device status:   %1 (%2)")
     657            0 :         .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
     658         3146 :     qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
     659         3146 :     qCDebug(lc).noquote() << tr("Battery status:  %1 (%2)")
     660            0 :         .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
     661         2541 :     if (status.switchPosition) {
     662         2860 :         qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
     663            0 :             .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
     664         1280 :     }
     665         2541 :     if (status.chargingStatus) {
     666         2860 :         qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
     667            0 :         .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
     668         1280 :     }
     669         2277 :     return status;
     670         1664 : }
     671              : 
     672              : /*!
     673              :  * Parses the torch status \a value, and returns the corresponding TorchStatus.
     674              :  */
     675          309 : std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
     676          384 : {
     677          903 :     if (!checkSize(u"Torch"_s, value, 1, 1)) {
     678          231 :         return std::nullopt;
     679          128 :     }
     680              : 
     681          462 :     const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
     682          572 :     qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     683          462 :     return status;
     684          384 : }
     685              : 
     686              : /*!
     687              :  * Parses the button press \a value, and returns the corresponding ButtonStatus.
     688              :  */
     689          412 : std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
     690          512 : {
     691         1204 :     if (!checkSize(u"Torch"_s, value, 2, 2)) {
     692          231 :         return std::nullopt;
     693          128 :     }
     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          693 :     const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
     705          858 :     qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     706          693 :     return status;
     707          512 : }
     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          412 : void StatusServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
     716          512 : {
     717          924 :     if (newService == StatusService::ServiceUuids::pokitMeter) {
     718          286 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
     719          231 :         serviceUuid = StatusService::ServiceUuids::pokitMeter;
     720          693 :     } else if (newService == StatusService::ServiceUuids::pokitPro) {
     721          286 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
     722          231 :         serviceUuid = StatusService::ServiceUuids::pokitPro;
     723          128 :     }
     724          924 :     AbstractPokitServicePrivate::serviceDiscovered(newService);
     725          924 : }
     726              : 
     727              : /*!
     728              :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     729              :  * specialised signal, for each supported \a characteristic.
     730              :  */
     731          103 : void StatusServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     732              :                                               const QByteArray &value)
     733          128 : {
     734          231 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     735              : 
     736          128 :     Q_Q(StatusService);
     737          231 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
     738            0 :         Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
     739            0 :         return;
     740            0 :     }
     741              : 
     742          231 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
     743            0 :         Q_EMIT q->deviceStatusRead(parseStatus(value));
     744            0 :         return;
     745            0 :     }
     746              : 
     747          231 :     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          231 :     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          231 :     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          231 :     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          546 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
     781          367 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     782          128 : }
     783              : 
     784              : /*!
     785              :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     786              :  * specialised signal, for each supported \a characteristic.
     787              :  */
     788          103 : void StatusServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     789              :                                                  const QByteArray &newValue)
     790          128 : {
     791          231 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     792              : 
     793          128 :     Q_Q(StatusService);
     794          231 :     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          231 :     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          231 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
     807            0 :         Q_EMIT q->deviceNameWritten();
     808            0 :         return;
     809            0 :     }
     810              : 
     811          231 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
     812            0 :         Q_EMIT q->deviceLedFlashed();
     813            0 :         return;
     814            0 :     }
     815              : 
     816          231 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
     817            0 :         Q_EMIT q->torchStatusWritten();
     818            0 :         return;
     819            0 :     }
     820              : 
     821          546 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
     822          367 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     823          128 : }
     824              : 
     825              : /// \endcond
     826              : 
     827              : QTPOKIT_END_NAMESPACE
        

Generated by: LCOV version 2.5-0