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: 94.9 % 78 74

            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          880 : QString StatusService::toString(const StatusService::DeviceStatus &status)
      33          764 : {
      34         1644 :     switch (status) {
      35          310 :     case DeviceStatus::Idle:                 return  QLatin1String("Idle");
      36           58 :     case DeviceStatus::MultimeterDcVoltage:  return  QLatin1String("MultimeterDcVoltage");
      37           58 :     case DeviceStatus::MultimeterAcVoltage:  return  QLatin1String("MultimeterAcVoltage");
      38           58 :     case DeviceStatus::MultimeterDcCurrent:  return  QLatin1String("MultimeterDcCurrent");
      39           58 :     case DeviceStatus::MultimeterAcCurrent:  return  QLatin1String("MultimeterAcCurrent");
      40           58 :     case DeviceStatus::MultimeterResistance: return  QLatin1String("MultimeterResistance");
      41           58 :     case DeviceStatus::MultimeterDiode:      return  QLatin1String("MultimeterDiode");
      42           58 :     case DeviceStatus::MultimeterContinuity: return  QLatin1String("MultimeterContinuity");
      43           58 :     case DeviceStatus::MultimeterTemperature:return  QLatin1String("MultimeterTemperature");
      44           58 :     case DeviceStatus::DsoModeSampling:      return  QLatin1String("DsoModeSampling");
      45           58 :     case DeviceStatus::LoggerModeSampling:   return  QLatin1String("LoggerModeSampling");
      46          764 :     }
      47           94 :     return QString();
      48          764 : }
      49              : 
      50              : /*!
      51              :  * Returns a string version of the \a status enum label.
      52              :  */
      53          520 : QString StatusService::toString(const StatusService::BatteryStatus &status)
      54          341 : {
      55          861 :     switch (status) {
      56          310 :     case BatteryStatus::Low:  return QLatin1String("Low");
      57           58 :     case BatteryStatus::Good: return QLatin1String("Good");
      58          341 :     }
      59           94 :     return QString();
      60          341 : }
      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          740 : QString StatusService::toString(const StatusService::SwitchPosition &position)
      74          795 : {
      75         1535 :     switch (position) {
      76          186 :     case SwitchPosition::Voltage:     return QLatin1String("Voltage");
      77          506 :     case SwitchPosition::MultiMode:   return QLatin1String("MultiMode");
      78          122 :     case SwitchPosition::HighCurrent: return QLatin1String("HighCurrent");
      79          795 :     }
      80           94 :     return QString();
      81          795 : }
      82              : 
      83              : /*!
      84              :  * Returns a string version of the \a status enum label.
      85              :  */
      86          200 : QString StatusService::toString(const StatusService::ChargingStatus &status)
      87          235 : {
      88          435 :     switch (status) {
      89           58 :     case ChargingStatus::Discharging: return QLatin1String("Discharging");
      90           58 :     case ChargingStatus::Charging:    return QLatin1String("Charging");
      91           58 :     case ChargingStatus::Charged:     return QLatin1String("Charged");
      92          235 :     }
      93           94 :     return QString();
      94          235 : }
      95              : 
      96              : /*!
      97              :  * Returns a string version of the \a status enum label.
      98              :  */
      99          538 : QString StatusService::toString(const StatusService::TorchStatus &status)
     100          580 : {
     101         1118 :     switch (status) {
     102          378 :     case TorchStatus::Off: return QLatin1String("Off");
     103          186 :     case TorchStatus::On:  return QLatin1String("On");
     104          580 :     }
     105           94 :     return QString();
     106          580 : }
     107              : 
     108              : /*!
     109              :  * Returns a string version of the \a status enum label.
     110              :  */
     111          362 : QString StatusService::toString(const StatusService::ButtonStatus &status)
     112          403 : {
     113          765 :     switch (status) {
     114          122 :     case ButtonStatus::Released: return QLatin1String("Released");
     115          122 :     case ButtonStatus::Pressed:  return QLatin1String("Pressed");
     116          122 :     case ButtonStatus::Held:     return QLatin1String("Held");
     117          403 :     }
     118           94 :     return QString();
     119          403 : }
     120              : 
     121              : /*!
     122              :  * Constructs a new Pokit service with \a parent.
     123              :  */
     124         1400 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
     125         1995 :     : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
     126         1345 : {
     127              : 
     128         2745 : }
     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           40 : bool StatusService::readCharacteristics()
     143           47 : {
     144           64 :     const bool r1 = readDeviceCharacteristics();
     145           64 :     const bool r2 = readStatusCharacteristic();
     146           64 :     const bool r3 = readNameCharacteristic();
     147           87 :     const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
     148           87 :         ? readTorchCharacteristic() : true;
     149           87 :     const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
     150           87 :         ? readButtonPressCharacteristic() : true;
     151          127 :     return (r1 && r2 && r3 && r4 && r5);
     152           47 : }
     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           57 : bool StatusService::readDeviceCharacteristics()
     164           94 : {
     165           94 :     Q_D(StatusService);
     166          174 :     return d->readCharacteristic(CharacteristicUuids::deviceCharacteristics);
     167           94 : }
     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           57 : bool StatusService::readStatusCharacteristic()
     179           94 : {
     180           94 :     Q_D(StatusService);
     181          174 :     return d->readCharacteristic(CharacteristicUuids::status);
     182           94 : }
     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          114 : bool StatusService::readNameCharacteristic()
     194           94 : {
     195           94 :     Q_D(StatusService);
     196          174 :     return d->readCharacteristic(CharacteristicUuids::name);
     197           94 : }
     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           40 : bool StatusService::readTorchCharacteristic()
     209           47 : {
     210           47 :     Q_D(StatusService);
     211           87 :     return d->readCharacteristic(CharacteristicUuids::torch);
     212           47 : }
     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           40 : bool StatusService::readButtonPressCharacteristic()
     224           47 : {
     225           47 :     Q_D(StatusService);
     226           87 :     return d->readCharacteristic(CharacteristicUuids::buttonPress);
     227           47 : }
     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           80 : StatusService::DeviceCharacteristics StatusService::deviceCharacteristics() const
     245           64 : {
     246           64 :     Q_D(const StatusService);
     247           64 :     const QLowEnergyCharacteristic characteristic =
     248          144 :         d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
     249          144 :     return (characteristic.isValid())
     250          144 :         ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
     251          224 :         : StatusService::DeviceCharacteristics();
     252          144 : }
     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          400 : StatusService::Status StatusService::status() const
     273          200 : {
     274          200 :     Q_D(const StatusService);
     275          200 :     const QLowEnergyCharacteristic characteristic =
     276          600 :         d->getCharacteristic(CharacteristicUuids::status);
     277          600 :     return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
     278          200 :         : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
     279         1000 :                                  BatteryStatus::Low, std::nullopt, std::nullopt };
     280          600 : }
     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           40 : bool StatusService::enableStatusNotifications()
     292           47 : {
     293           47 :     Q_D(StatusService);
     294           87 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::status);
     295           47 : }
     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           40 : bool StatusService::disableStatusNotifications()
     305           47 : {
     306           47 :     Q_D(StatusService);
     307           87 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::status);
     308           47 : }
     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          400 : QString StatusService::deviceName() const
     318          200 : {
     319          200 :     Q_D(const StatusService);
     320          200 :     const QLowEnergyCharacteristic characteristic =
     321          600 :         d->getCharacteristic(CharacteristicUuids::name);
     322         1000 :     return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
     323          600 : }
     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           40 : bool StatusService::setDeviceName(const QString &name)
     333           47 : {
     334           47 :     Q_D(const StatusService);
     335           47 :     const QLowEnergyCharacteristic characteristic =
     336           87 :         d->getCharacteristic(CharacteristicUuids::name);
     337           87 :     if (!characteristic.isValid()) {
     338           47 :         return false;
     339           47 :     }
     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           40 : }
     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           40 : bool StatusService::flashLed()
     368           47 : {
     369           47 :     Q_D(const StatusService);
     370           47 :     const QLowEnergyCharacteristic characteristic =
     371           87 :         d->getCharacteristic(CharacteristicUuids::flashLed);
     372           87 :     if (!characteristic.isValid()) {
     373           47 :         return false;
     374           47 :     }
     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           87 : }
     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          400 : std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
     393          200 : {
     394          200 :     Q_D(const StatusService);
     395          600 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     396         1000 :     return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
     397          600 : }
     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           40 : bool StatusService::setTorchStatus(const StatusService::TorchStatus status)
     409           47 : {
     410           47 :     Q_D(const StatusService);
     411           87 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     412           87 :     if (!characteristic.isValid()) {
     413           47 :         return false;
     414           47 :     }
     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           87 : }
     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           40 : bool StatusService::enableTorchStatusNotifications()
     431           47 : {
     432           47 :     Q_D(StatusService);
     433           87 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::torch);
     434           47 : }
     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           40 : bool StatusService::disableTorchStatusNotifications()
     444           47 : {
     445           47 :     Q_D(StatusService);
     446           87 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::torch);
     447           47 : }
     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           40 : bool StatusService::enableButtonPressedNotifications()
     459           47 : {
     460           47 :     Q_D(StatusService);
     461           87 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
     462           47 : }
     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           40 : bool StatusService::disableButtonPressedNotifications()
     472           47 : {
     473           47 :     Q_D(StatusService);
     474           87 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
     475           47 : }
     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          400 : std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
     485          200 : {
     486          200 :     Q_D(const StatusService);
     487          600 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
     488         1000 :     return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
     489          600 : }
     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         1120 : StatusServicePrivate::StatusServicePrivate(
     567         1400 :     QLowEnergyController * controller, StatusService * const q)
     568         1995 :     : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
     569         1345 : {
     570              : 
     571         2465 : }
     572              : 
     573              : /*!
     574              :  * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
     575              :  */
     576          160 : StatusService::DeviceCharacteristics StatusServicePrivate::parseDeviceCharacteristics(
     577              :     const QByteArray &value)
     578          188 : {
     579          348 :     StatusService::DeviceCharacteristics characteristics{
     580          188 :         QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
     581          288 :     };
     582          188 :     Q_ASSERT(characteristics.firmwareVersion.isNull());  // How we indicate failure.
     583              : 
     584          416 :     if (!checkSize(QLatin1String("Device Characteristics"), value, 20, 20)) {
     585           94 :         return characteristics;
     586           94 :     }
     587              : 
     588          174 :     characteristics.firmwareVersion = QVersionNumber(
     589          246 :                                           qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
     590          220 :                                           qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
     591          208 :     characteristics.maximumVoltage      = qFromLittleEndian<quint16>(value.mid(2,2).constData());
     592          208 :     characteristics.maximumCurrent      = qFromLittleEndian<quint16>(value.mid(4,2).constData());
     593          208 :     characteristics.maximumResistance   = qFromLittleEndian<quint16>(value.mid(6,2).constData());
     594          208 :     characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
     595          208 :     characteristics.samplingBufferSize  = qFromLittleEndian<quint16>(value.mid(10,2).constData());
     596          208 :     characteristics.capabilityMask      = qFromLittleEndian<quint16>(value.mid(12,2).constData());
     597          212 :     characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
     598          310 :                                                    ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
     599              : 
     600          194 :     qCDebug(lc).noquote() << tr("Firmware version:     ") << characteristics.firmwareVersion;
     601          194 :     qCDebug(lc).noquote() << tr("Maximum voltage:      ") << characteristics.maximumVoltage;
     602          194 :     qCDebug(lc).noquote() << tr("Maximum current:      ") << characteristics.maximumCurrent;
     603          194 :     qCDebug(lc).noquote() << tr("Maximum resistance:   ") << characteristics.maximumResistance;
     604          194 :     qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
     605          194 :     qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
     606          194 :     qCDebug(lc).noquote() << tr("Capability mask:      ") << characteristics.capabilityMask;
     607          194 :     qCDebug(lc).noquote() << tr("MAC address:          ") << characteristics.macAddress;
     608              : 
     609           94 :     Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
     610          104 :     return characteristics;
     611          188 : }
     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          520 : StatusService::Status StatusServicePrivate::parseStatus(const QByteArray &value)
     619          611 : {
     620         1131 :     StatusService::Status status{
     621          611 :         static_cast<StatusService::DeviceStatus>
     622          611 :             (std::numeric_limits<std::underlying_type_t<StatusService::DeviceStatus>>::max()),
     623          611 :         std::numeric_limits<float>::quiet_NaN(),
     624          611 :         static_cast<StatusService::BatteryStatus>
     625          611 :             (std::numeric_limits<std::underlying_type_t<StatusService::BatteryStatus>>::max()),
     626          611 :         std::nullopt, std::nullopt,
     627          611 :     };
     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         1352 :     if (!checkSize(QLatin1String("Status"), value, 5, 8)) {
     638          136 :         return status;
     639           94 :     }
     640              : 
     641          957 :     status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
     642         1144 :     status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
     643          957 :     if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
     644          870 :         status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
     645          470 :     }
     646          957 :     if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
     647          870 :         status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
     648          470 :     }
     649          957 :     if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
     650          870 :         status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
     651          470 :     }
     652         1067 :     qCDebug(lc).noquote() << tr("Device status:   %1 (%2)")
     653            0 :         .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
     654         1067 :     qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
     655         1067 :     qCDebug(lc).noquote() << tr("Battery status:  %1 (%2)")
     656            0 :         .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
     657          957 :     if (status.switchPosition) {
     658          970 :         qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
     659            0 :             .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
     660          470 :     }
     661          957 :     if (status.chargingStatus) {
     662          970 :         qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
     663            0 :         .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
     664          470 :     }
     665          748 :     return status;
     666          611 : }
     667              : 
     668              : /*!
     669              :  * Parses the torch status \a value, and returns the corresponding TorchStatus.
     670              :  */
     671          120 : std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
     672          141 : {
     673          312 :     if (!checkSize(QLatin1String("Torch"), value, 1, 1)) {
     674           87 :         return std::nullopt;
     675           47 :     }
     676              : 
     677          174 :     const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
     678          194 :     qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     679          174 :     return status;
     680          141 : }
     681              : 
     682              : /*!
     683              :  * Parses the button press \a value, and returns the corresponding ButtonStatus.
     684              :  */
     685          160 : std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
     686          188 : {
     687          416 :     if (!checkSize(QLatin1String("Torch"), value, 2, 2)) {
     688           87 :         return std::nullopt;
     689           47 :     }
     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          261 :     const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
     701          291 :     qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     702          261 :     return status;
     703          188 : }
     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          160 : void StatusServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
     712          188 : {
     713          348 :     if (newService == StatusService::ServiceUuids::pokitMeter) {
     714           97 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
     715           87 :         serviceUuid = StatusService::ServiceUuids::pokitMeter;
     716          261 :     } else if (newService == StatusService::ServiceUuids::pokitPro) {
     717           97 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
     718           87 :         serviceUuid = StatusService::ServiceUuids::pokitPro;
     719           47 :     }
     720          348 :     AbstractPokitServicePrivate::serviceDiscovered(newService);
     721          348 : }
     722              : 
     723              : /*!
     724              :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     725              :  * specialised signal, for each supported \a characteristic.
     726              :  */
     727           40 : void StatusServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     728              :                                               const QByteArray &value)
     729           47 : {
     730           87 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     731              : 
     732           47 :     Q_Q(StatusService);
     733           87 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
     734            0 :         Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
     735            0 :         return;
     736            0 :     }
     737              : 
     738           87 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
     739            0 :         Q_EMIT q->deviceStatusRead(parseStatus(value));
     740            0 :         return;
     741            0 :     }
     742              : 
     743           87 :     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           87 :     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           87 :     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           87 :     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          192 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
     777          144 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     778           47 : }
     779              : 
     780              : /*!
     781              :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     782              :  * specialised signal, for each supported \a characteristic.
     783              :  */
     784           40 : void StatusServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     785              :                                                  const QByteArray &newValue)
     786           47 : {
     787           87 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     788              : 
     789           47 :     Q_Q(StatusService);
     790           87 :     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           87 :     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           87 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
     803            0 :         Q_EMIT q->deviceNameWritten();
     804            0 :         return;
     805            0 :     }
     806              : 
     807           87 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
     808            0 :         Q_EMIT q->deviceLedFlashed();
     809            0 :         return;
     810            0 :     }
     811              : 
     812           87 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
     813            0 :         Q_EMIT q->torchStatusWritten();
     814            0 :         return;
     815            0 :     }
     816              : 
     817          192 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
     818          144 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     819           47 : }
     820              : 
     821              : /// \endcond
        

Generated by: LCOV version 2.2-1