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 79 49.4 %

          Line data    Source code
       1             : // SPDX-FileCopyrightText: 2022-2024 Paul Colby <git@colby.id.au>
       2             : // SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4             : /*!
       5             :  * \file
       6             :  * Defines the StatusService and StatusServicePrivate classes.
       7             :  */
       8             : 
       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         440 : QString StatusService::toString(const StatusService::DeviceStatus &status)
      33             : {
      34         440 :     switch (status) {
      35          60 :     case DeviceStatus::Idle:                 return  QLatin1String("Idle");
      36           6 :     case DeviceStatus::MultimeterDcVoltage:  return  QLatin1String("MultimeterDcVoltage");
      37           6 :     case DeviceStatus::MultimeterAcVoltage:  return  QLatin1String("MultimeterAcVoltage");
      38           6 :     case DeviceStatus::MultimeterDcCurrent:  return  QLatin1String("MultimeterDcCurrent");
      39           6 :     case DeviceStatus::MultimeterAcCurrent:  return  QLatin1String("MultimeterAcCurrent");
      40           6 :     case DeviceStatus::MultimeterResistance: return  QLatin1String("MultimeterResistance");
      41           6 :     case DeviceStatus::MultimeterDiode:      return  QLatin1String("MultimeterDiode");
      42           6 :     case DeviceStatus::MultimeterContinuity: return  QLatin1String("MultimeterContinuity");
      43           6 :     case DeviceStatus::MultimeterTemperature:return  QLatin1String("MultimeterTemperature");
      44           6 :     case DeviceStatus::DsoModeSampling:      return  QLatin1String("DsoModeSampling");
      45           6 :     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         260 : QString StatusService::toString(const StatusService::BatteryStatus &status)
      54             : {
      55         260 :     switch (status) {
      56          60 :     case BatteryStatus::Low:  return QLatin1String("Low");
      57           6 :     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         440 : QString StatusService::toString(const StatusService::SwitchPosition &position)
      74             : {
      75         440 :     switch (position) {
      76          18 :     case SwitchPosition::Voltage:     return QLatin1String("Voltage");
      77          48 :     case SwitchPosition::MultiMode:   return QLatin1String("MultiMode");
      78          12 :     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         100 : QString StatusService::toString(const StatusService::ChargingStatus &status)
      87             : {
      88         100 :     switch (status) {
      89           6 :     case ChargingStatus::Discharging: return QLatin1String("Discharging");
      90           6 :     case ChargingStatus::Charging:    return QLatin1String("Charging");
      91           6 :     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         318 : QString StatusService::toString(const StatusService::TorchStatus &status)
     100             : {
     101         318 :     switch (status) {
     102          36 :     case TorchStatus::Off: return QLatin1String("Off");
     103          18 :     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         202 : QString StatusService::toString(const StatusService::ButtonStatus &status)
     112             : {
     113         202 :     switch (status) {
     114          12 :     case ButtonStatus::Released: return QLatin1String("Released");
     115          12 :     case ButtonStatus::Pressed:  return QLatin1String("Pressed");
     116          12 :     case ButtonStatus::Held:     return QLatin1String("Held");
     117             :     }
     118             :     return QString();
     119             : }
     120             : 
     121             : /*!
     122             :  * Constructs a new Pokit service with \a parent.
     123             :  */
     124         700 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
     125         700 :     : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
     126             : {
     127             : 
     128         700 : }
     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         480 : StatusService::~StatusService()
     146             : {
     147             : 
     148         480 : }
     149             : 
     150          20 : bool StatusService::readCharacteristics()
     151             : {
     152          14 :     const bool r1 = readDeviceCharacteristics();
     153          14 :     const bool r2 = readStatusCharacteristic();
     154          14 :     const bool r3 = readNameCharacteristic();
     155          34 :     const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
     156          20 :         ? readTorchCharacteristic() : true;
     157          34 :     const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
     158          20 :         ? readButtonPressCharacteristic() : true;
     159          40 :     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          34 : bool StatusService::readDeviceCharacteristics()
     172             : {
     173             :     Q_D(StatusService);
     174          40 :     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          34 : bool StatusService::readStatusCharacteristic()
     187             : {
     188             :     Q_D(StatusService);
     189          40 :     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          34 : bool StatusService::readNameCharacteristic()
     202             : {
     203             :     Q_D(StatusService);
     204          40 :     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          20 : bool StatusService::readTorchCharacteristic()
     217             : {
     218             :     Q_D(StatusService);
     219          20 :     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          20 : bool StatusService::readButtonPressCharacteristic()
     232             : {
     233             :     Q_D(StatusService);
     234          20 :     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          40 : StatusService::DeviceCharacteristics StatusService::deviceCharacteristics() const
     253             : {
     254             :     Q_D(const StatusService);
     255             :     const QLowEnergyCharacteristic characteristic =
     256          40 :         d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
     257          40 :     return (characteristic.isValid())
     258          12 :         ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
     259          80 :         : StatusService::DeviceCharacteristics();
     260          40 : }
     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         200 : StatusService::Status StatusService::status() const
     281             : {
     282             :     Q_D(const StatusService);
     283             :     const QLowEnergyCharacteristic characteristic =
     284         200 :         d->getCharacteristic(CharacteristicUuids::status);
     285         200 :     return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
     286             :         : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
     287         400 :                                  BatteryStatus::Low, std::nullopt, std::nullopt };
     288         200 : }
     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          20 : bool StatusService::enableStatusNotifications()
     300             : {
     301             :     Q_D(StatusService);
     302          20 :     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          20 : bool StatusService::disableStatusNotifications()
     313             : {
     314             :     Q_D(StatusService);
     315          20 :     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         200 : QString StatusService::deviceName() const
     326             : {
     327             :     Q_D(const StatusService);
     328             :     const QLowEnergyCharacteristic characteristic =
     329         200 :         d->getCharacteristic(CharacteristicUuids::name);
     330         400 :     return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
     331         200 : }
     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          20 : bool StatusService::setDeviceName(const QString &name)
     341             : {
     342             :     Q_D(const StatusService);
     343             :     const QLowEnergyCharacteristic characteristic =
     344          20 :         d->getCharacteristic(CharacteristicUuids::name);
     345          20 :     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          20 : }
     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          20 : bool StatusService::flashLed()
     376             : {
     377             :     Q_D(const StatusService);
     378             :     const QLowEnergyCharacteristic characteristic =
     379          20 :         d->getCharacteristic(CharacteristicUuids::flashLed);
     380          20 :     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          20 : }
     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         200 : std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
     401             : {
     402             :     Q_D(const StatusService);
     403         200 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     404         400 :     return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
     405         200 : }
     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          20 : bool StatusService::setTorchStatus(const StatusService::TorchStatus status)
     417             : {
     418             :     Q_D(const StatusService);
     419          20 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     420          20 :     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          20 : }
     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          20 : bool StatusService::enableTorchStatusNotifications()
     439             : {
     440             :     Q_D(StatusService);
     441          20 :     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          20 : bool StatusService::disableTorchStatusNotifications()
     452             : {
     453             :     Q_D(StatusService);
     454          20 :     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          20 : bool StatusService::enableButtonPressedNotifications()
     467             : {
     468             :     Q_D(StatusService);
     469          20 :     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          20 : bool StatusService::disableButtonPressedNotifications()
     480             : {
     481             :     Q_D(StatusService);
     482          20 :     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         200 : std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
     493             : {
     494             :     Q_D(const StatusService);
     495         200 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
     496         400 :     return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
     497         200 : }
     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         700 : StatusServicePrivate::StatusServicePrivate(
     575         700 :     QLowEnergyController * controller, StatusService * const q)
     576         700 :     : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
     577             : {
     578             : 
     579         700 : }
     580             : 
     581             : /*!
     582             :  * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
     583             :  */
     584          80 : StatusService::DeviceCharacteristics StatusServicePrivate::parseDeviceCharacteristics(
     585             :     const QByteArray &value)
     586             : {
     587          80 :     StatusService::DeviceCharacteristics characteristics{
     588             :         QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
     589          80 :     };
     590             :     Q_ASSERT(characteristics.firmwareVersion.isNull());  // How we indicate failure.
     591             : 
     592         104 :     if (!checkSize(QLatin1String("Device Characteristics"), value, 20, 20)) {
     593             :         return characteristics;
     594             :     }
     595             : 
     596          52 :     characteristics.firmwareVersion = QVersionNumber(
     597          80 :                                           qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
     598          68 :                                           qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
     599          52 :     characteristics.maximumVoltage      = qFromLittleEndian<quint16>(value.mid(2,2).constData());
     600          52 :     characteristics.maximumCurrent      = qFromLittleEndian<quint16>(value.mid(4,2).constData());
     601          52 :     characteristics.maximumResistance   = qFromLittleEndian<quint16>(value.mid(6,2).constData());
     602          52 :     characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
     603          52 :     characteristics.samplingBufferSize  = qFromLittleEndian<quint16>(value.mid(10,2).constData());
     604          52 :     characteristics.capabilityMask      = qFromLittleEndian<quint16>(value.mid(12,2).constData());
     605          68 :     characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
     606         118 :                                                    ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
     607             : 
     608          44 :     qCDebug(lc).noquote() << tr("Firmware version:     ") << characteristics.firmwareVersion;
     609          44 :     qCDebug(lc).noquote() << tr("Maximum voltage:      ") << characteristics.maximumVoltage;
     610          44 :     qCDebug(lc).noquote() << tr("Maximum current:      ") << characteristics.maximumCurrent;
     611          44 :     qCDebug(lc).noquote() << tr("Maximum resistance:   ") << characteristics.maximumResistance;
     612          44 :     qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
     613          44 :     qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
     614          44 :     qCDebug(lc).noquote() << tr("Capability mask:      ") << characteristics.capabilityMask;
     615          44 :     qCDebug(lc).noquote() << tr("MAC address:          ") << characteristics.macAddress;
     616             : 
     617             :     Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
     618           4 :     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         260 : StatusService::Status StatusServicePrivate::parseStatus(const QByteArray &value)
     627             : {
     628         260 :     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. It appears that the first of those 2 extra bytes (ie the 7th byte) is used to
     641             :      * indicate the physical switch position, while the other extra byte (ie the 8th byte) indicates
     642             :      * the device's current charging status.
     643             :      */
     644             : 
     645         338 :     if (!checkSize(QLatin1String("Status"), value, 5, 8)) {
     646          40 :         return status;
     647             :     }
     648             : 
     649         220 :     status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
     650         286 :     status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
     651         220 :     if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
     652         200 :         status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
     653             :     }
     654         220 :     if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
     655         200 :         status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
     656             :     }
     657         220 :     if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
     658         200 :         status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
     659             :     }
     660         242 :     qCDebug(lc).noquote() << tr("Device status:   %1 (%2)")
     661           0 :         .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
     662         242 :     qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
     663         242 :     qCDebug(lc).noquote() << tr("Battery status:  %1 (%2)")
     664           0 :         .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
     665         220 :     if (status.switchPosition) {
     666         220 :         qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
     667           0 :             .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
     668             :     }
     669         220 :     if (status.chargingStatus) {
     670         220 :         qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
     671           0 :         .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
     672             :     }
     673         220 :     return status;
     674             : }
     675             : 
     676             : /*!
     677             :  * Parses the torch status \a value, and returns the corresponding TorchStatus.
     678             :  */
     679          60 : std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
     680             : {
     681          78 :     if (!checkSize(QLatin1String("Torch"), value, 1, 1)) {
     682          20 :         return std::nullopt;
     683             :     }
     684             : 
     685          40 :     const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
     686          44 :     qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     687          40 :     return status;
     688             : }
     689             : 
     690             : /*!
     691             :  * Parses the button press \a value, and returns the corresponding ButtonStatus.
     692             :  */
     693          80 : std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
     694             : {
     695         104 :     if (!checkSize(QLatin1String("Torch"), value, 2, 2)) {
     696          20 :         return std::nullopt;
     697             :     }
     698             : 
     699             :     /*!
     700             :      * \pokitApi The button event is the second byte, but no idea what the first byte is. In all examples
     701             :      * I've see it's always `0x02`. It appears that the Pokit Android app only ever looks at `bytes[1]`.
     702             :      *
     703             :      * \pokitApi Note, we can actually write to the Button Press characteristic too. If we do, then whatever
     704             :      * we set as the first byte persists, and (unsurprisingly) the second byte reverts to the current
     705             :      * button state. So still no idea what that first byte is for.
     706             :      */
     707             : 
     708          60 :     const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
     709          66 :     qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     710          60 :     return status;
     711             : }
     712             : 
     713             : /*!
     714             :  * Handles `QLowEnergyController::serviceDiscovered` events.
     715             :  *
     716             :  * Here we override the base implementation to detect if we're looking at a Pokit Meter, or Pokit
     717             :  * Pro device, as the two devices have very slightly different Status Service UUIDs.
     718             :  */
     719          80 : void StatusServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
     720             : {
     721          80 :     if (newService == StatusService::ServiceUuids::pokitMeter) {
     722          22 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
     723          20 :         serviceUuid = StatusService::ServiceUuids::pokitMeter;
     724          60 :     } else if (newService == StatusService::ServiceUuids::pokitPro) {
     725          22 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
     726          20 :         serviceUuid = StatusService::ServiceUuids::pokitPro;
     727             :     }
     728          80 :     AbstractPokitServicePrivate::serviceDiscovered(newService);
     729          80 : }
     730             : 
     731             : /*!
     732             :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     733             :  * specialised signal, for each supported \a characteristic.
     734             :  */
     735          20 : void StatusServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     736             :                                               const QByteArray &value)
     737             : {
     738          20 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     739             : 
     740             :     Q_Q(StatusService);
     741          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
     742           0 :         Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
     743           0 :         return;
     744             :     }
     745             : 
     746          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
     747           0 :         Q_EMIT q->deviceStatusRead(parseStatus(value));
     748           0 :         return;
     749             :     }
     750             : 
     751          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
     752           0 :         const QString deviceName = QString::fromUtf8(value);
     753           0 :         qCDebug(lc).noquote() << tr(R"(Device name: "%1")").arg(deviceName);
     754           0 :         Q_EMIT q->deviceNameRead(deviceName);
     755             :         return;
     756           0 :     }
     757             : 
     758          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
     759           0 :         qCWarning(lc).noquote() << tr("Flash LED characteristic is write-only, but somehow read")
     760           0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     761           0 :         return;
     762             :     }
     763             : 
     764          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
     765           0 :         if (!checkSize(QLatin1String("Torch"), value, 1, 1)) {
     766             :             return;
     767             :         }
     768           0 :         const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
     769           0 :         qCDebug(lc).noquote() << tr("Torch status:  %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     770           0 :         Q_EMIT q->torchStatusRead(status);
     771           0 :         return;
     772             :     }
     773             : 
     774          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::buttonPress) {
     775           0 :         if (!checkSize(QLatin1String("Torch"), value, 2, 2)) {
     776             :             return;
     777             :         }
     778           0 :         const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
     779           0 :         qCDebug(lc).noquote() << tr("Button status:  %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     780           0 :         Q_EMIT q->buttonPressRead(value.at(0), status);
     781           0 :         return;
     782             :     }
     783             : 
     784          62 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
     785          26 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     786             : }
     787             : 
     788             : /*!
     789             :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     790             :  * specialised signal, for each supported \a characteristic.
     791             :  */
     792          20 : void StatusServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     793             :                                                  const QByteArray &newValue)
     794             : {
     795          20 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     796             : 
     797             :     Q_Q(StatusService);
     798          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
     799           0 :         qCWarning(lc).noquote() << tr("Device Characteristics is read-only, but somehow written")
     800           0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     801           0 :         return;
     802             :     }
     803             : 
     804          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
     805           0 :         qCWarning(lc).noquote() << tr("Status characteristic is read-only, but somehow written")
     806           0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     807           0 :         return;
     808             :     }
     809             : 
     810          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
     811           0 :         Q_EMIT q->deviceNameWritten();
     812           0 :         return;
     813             :     }
     814             : 
     815          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
     816           0 :         Q_EMIT q->deviceLedFlashed();
     817           0 :         return;
     818             :     }
     819             : 
     820          20 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
     821           0 :         Q_EMIT q->torchStatusWritten();
     822           0 :         return;
     823             :     }
     824             : 
     825          62 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
     826          26 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     827             : }
     828             : 
     829             : /// \endcond

Generated by: LCOV version 1.14