LCOV - code coverage report
Current view: top level - src/lib - statusservice.cpp (source / functions) Hit Total Coverage
Project: Dokit Lines: 195 248 78.6 %
Version: Functions: 39 42 92.9 %

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

Generated by: LCOV version 1.14