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

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

Generated by: LCOV version 2.3-1