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

Generated by: LCOV version 1.14