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 % 39 37

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2022-2024 Paul Colby <git@colby.id.au>
       2              : // SPDX-License-Identifier: LGPL-3.0-or-later
       3              : 
       4              : /*!
       5              :  * \file
       6              :  * Defines the StatusService and StatusServicePrivate classes.
       7              :  */
       8              : 
       9              : #include <qtpokit/statusservice.h>
      10              : #include "statusservice_p.h"
      11              : 
      12              : #include <QtEndian>
      13              : 
      14              : /*!
      15              :  * \class StatusService
      16              :  *
      17              :  * The StatusService class accesses the `Pokit Status` service of Pokit devices.
      18              :  */
      19              : 
      20              : /*!
      21              :  * \cond internal
      22              :  * \struct StatusService::ServiceUuids
      23              :  * \pokitApi Pokit API 1.00 (and 0.02) states the Status Service UUID as
      24              :  * `57d3a771-267c-4394-8872-78223e92aec4` which is correct for the Pokit Meter, but Pokit Pro uses
      25              :  * `57d3a771-267c-4394-8872-78223e92aec5` instead, that is the last digit is a `5` not `4`.
      26              :  * \endcond
      27              :  */
      28              : 
      29              : /*!
      30              :  * Returns a string version of the \a status enum label.
      31              :  */
      32          836 : QString StatusService::toString(const StatusService::DeviceStatus &status)
      33          716 : {
      34         1552 :     switch (status) {
      35          298 :     case DeviceStatus::Idle:                 return  QLatin1String("Idle");
      36           55 :     case DeviceStatus::MultimeterDcVoltage:  return  QLatin1String("MultimeterDcVoltage");
      37           55 :     case DeviceStatus::MultimeterAcVoltage:  return  QLatin1String("MultimeterAcVoltage");
      38           55 :     case DeviceStatus::MultimeterDcCurrent:  return  QLatin1String("MultimeterDcCurrent");
      39           55 :     case DeviceStatus::MultimeterAcCurrent:  return  QLatin1String("MultimeterAcCurrent");
      40           55 :     case DeviceStatus::MultimeterResistance: return  QLatin1String("MultimeterResistance");
      41           55 :     case DeviceStatus::MultimeterDiode:      return  QLatin1String("MultimeterDiode");
      42           55 :     case DeviceStatus::MultimeterContinuity: return  QLatin1String("MultimeterContinuity");
      43           55 :     case DeviceStatus::MultimeterTemperature:return  QLatin1String("MultimeterTemperature");
      44           55 :     case DeviceStatus::DsoModeSampling:      return  QLatin1String("DsoModeSampling");
      45           55 :     case DeviceStatus::LoggerModeSampling:   return  QLatin1String("LoggerModeSampling");
      46          716 :     }
      47           88 :     return QString();
      48          716 : }
      49              : 
      50              : /*!
      51              :  * Returns a string version of the \a status enum label.
      52              :  */
      53          494 : QString StatusService::toString(const StatusService::BatteryStatus &status)
      54          320 : {
      55          814 :     switch (status) {
      56          298 :     case BatteryStatus::Low:  return QLatin1String("Low");
      57           55 :     case BatteryStatus::Good: return QLatin1String("Good");
      58          320 :     }
      59           88 :     return QString();
      60          320 : }
      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          730 : QString StatusService::toString(const StatusService::SwitchPosition &position)
      74          780 : {
      75         1510 :     switch (position) {
      76          183 :     case SwitchPosition::Voltage:     return QLatin1String("Voltage");
      77          503 :     case SwitchPosition::MultiMode:   return QLatin1String("MultiMode");
      78          119 :     case SwitchPosition::HighCurrent: return QLatin1String("HighCurrent");
      79          780 :     }
      80           88 :     return QString();
      81          780 : }
      82              : 
      83              : /*!
      84              :  * Returns a string version of the \a status enum label.
      85              :  */
      86          190 : QString StatusService::toString(const StatusService::ChargingStatus &status)
      87          220 : {
      88          410 :     switch (status) {
      89           55 :     case ChargingStatus::Discharging: return QLatin1String("Discharging");
      90           55 :     case ChargingStatus::Charging:    return QLatin1String("Charging");
      91           55 :     case ChargingStatus::Charged:     return QLatin1String("Charged");
      92          220 :     }
      93           88 :     return QString();
      94          220 : }
      95              : 
      96              : /*!
      97              :  * Returns a string version of the \a status enum label.
      98              :  */
      99          530 : QString StatusService::toString(const StatusService::TorchStatus &status)
     100          568 : {
     101         1098 :     switch (status) {
     102          375 :     case TorchStatus::Off: return QLatin1String("Off");
     103          183 :     case TorchStatus::On:  return QLatin1String("On");
     104          568 :     }
     105           88 :     return QString();
     106          568 : }
     107              : 
     108              : /*!
     109              :  * Returns a string version of the \a status enum label.
     110              :  */
     111          352 : QString StatusService::toString(const StatusService::ButtonStatus &status)
     112          388 : {
     113          740 :     switch (status) {
     114          119 :     case ButtonStatus::Released: return QLatin1String("Released");
     115          119 :     case ButtonStatus::Pressed:  return QLatin1String("Pressed");
     116          119 :     case ButtonStatus::Held:     return QLatin1String("Held");
     117          388 :     }
     118           88 :     return QString();
     119          388 : }
     120              : 
     121              : /*!
     122              :  * Constructs a new Pokit service with \a parent.
     123              :  */
     124         1330 : StatusService::StatusService(QLowEnergyController * const controller, QObject * parent)
     125         1890 :     : AbstractPokitService(new StatusServicePrivate(controller, this), parent)
     126         1260 : {
     127              : 
     128         2590 : }
     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           38 : bool StatusService::readCharacteristics()
     143           44 : {
     144           60 :     const bool r1 = readDeviceCharacteristics();
     145           60 :     const bool r2 = readStatusCharacteristic();
     146           60 :     const bool r3 = readNameCharacteristic();
     147           82 :     const bool r4 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::torch).isValid()))
     148           82 :         ? readTorchCharacteristic() : true;
     149           82 :     const bool r5 = ((service() != nullptr) && (service()->characteristic(CharacteristicUuids::buttonPress).isValid()))
     150           82 :         ? readButtonPressCharacteristic() : true;
     151          120 :     return (r1 && r2 && r3 && r4 && r5);
     152           44 : }
     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           54 : bool StatusService::readDeviceCharacteristics()
     164           88 : {
     165           88 :     Q_D(StatusService);
     166          164 :     return d->readCharacteristic(CharacteristicUuids::deviceCharacteristics);
     167           88 : }
     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           54 : bool StatusService::readStatusCharacteristic()
     179           88 : {
     180           88 :     Q_D(StatusService);
     181          164 :     return d->readCharacteristic(CharacteristicUuids::status);
     182           88 : }
     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           54 : bool StatusService::readNameCharacteristic()
     194           88 : {
     195           88 :     Q_D(StatusService);
     196          164 :     return d->readCharacteristic(CharacteristicUuids::name);
     197           88 : }
     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           38 : bool StatusService::readTorchCharacteristic()
     209           44 : {
     210           44 :     Q_D(StatusService);
     211           82 :     return d->readCharacteristic(CharacteristicUuids::torch);
     212           44 : }
     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           38 : bool StatusService::readButtonPressCharacteristic()
     224           44 : {
     225           44 :     Q_D(StatusService);
     226           82 :     return d->readCharacteristic(CharacteristicUuids::buttonPress);
     227           44 : }
     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           76 : StatusService::DeviceCharacteristics StatusService::deviceCharacteristics() const
     245           60 : {
     246           60 :     Q_D(const StatusService);
     247           60 :     const QLowEnergyCharacteristic characteristic =
     248          136 :         d->getCharacteristic(CharacteristicUuids::deviceCharacteristics);
     249          136 :     return (characteristic.isValid())
     250          136 :         ? StatusServicePrivate::parseDeviceCharacteristics(characteristic.value())
     251          212 :         : StatusService::DeviceCharacteristics();
     252          136 : }
     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          380 : StatusService::Status StatusService::status() const
     273          188 : {
     274          188 :     Q_D(const StatusService);
     275          188 :     const QLowEnergyCharacteristic characteristic =
     276          568 :         d->getCharacteristic(CharacteristicUuids::status);
     277          568 :     return (characteristic.isValid()) ? StatusServicePrivate::parseStatus(characteristic.value())
     278          188 :         : StatusService::Status{ DeviceStatus::Idle, std::numeric_limits<float>::quiet_NaN(),
     279          948 :                                  BatteryStatus::Low, std::nullopt, std::nullopt };
     280          568 : }
     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           38 : bool StatusService::enableStatusNotifications()
     292           44 : {
     293           44 :     Q_D(StatusService);
     294           82 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::status);
     295           44 : }
     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           38 : bool StatusService::disableStatusNotifications()
     305           44 : {
     306           44 :     Q_D(StatusService);
     307           82 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::status);
     308           44 : }
     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          380 : QString StatusService::deviceName() const
     318          188 : {
     319          188 :     Q_D(const StatusService);
     320          188 :     const QLowEnergyCharacteristic characteristic =
     321          568 :         d->getCharacteristic(CharacteristicUuids::name);
     322          948 :     return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString();
     323          568 : }
     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           38 : bool StatusService::setDeviceName(const QString &name)
     333           44 : {
     334           44 :     Q_D(const StatusService);
     335           44 :     const QLowEnergyCharacteristic characteristic =
     336           82 :         d->getCharacteristic(CharacteristicUuids::name);
     337           82 :     if (!characteristic.isValid()) {
     338           44 :         return false;
     339           44 :     }
     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           38 : }
     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           38 : bool StatusService::flashLed()
     368           44 : {
     369           44 :     Q_D(const StatusService);
     370           44 :     const QLowEnergyCharacteristic characteristic =
     371           82 :         d->getCharacteristic(CharacteristicUuids::flashLed);
     372           82 :     if (!characteristic.isValid()) {
     373           44 :         return false;
     374           44 :     }
     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           82 : }
     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          380 : std::optional<StatusService::TorchStatus> StatusService::torchStatus() const
     393          188 : {
     394          188 :     Q_D(const StatusService);
     395          568 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     396          948 :     return (characteristic.isValid()) ? StatusServicePrivate::parseTorchStatus(characteristic.value()) : std::nullopt;
     397          568 : }
     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           38 : bool StatusService::setTorchStatus(const StatusService::TorchStatus status)
     409           44 : {
     410           44 :     Q_D(const StatusService);
     411           82 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::torch);
     412           82 :     if (!characteristic.isValid()) {
     413           44 :         return false;
     414           44 :     }
     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           82 : }
     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           38 : bool StatusService::enableTorchStatusNotifications()
     431           44 : {
     432           44 :     Q_D(StatusService);
     433           82 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::torch);
     434           44 : }
     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           38 : bool StatusService::disableTorchStatusNotifications()
     444           44 : {
     445           44 :     Q_D(StatusService);
     446           82 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::torch);
     447           44 : }
     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           38 : bool StatusService::enableButtonPressedNotifications()
     459           44 : {
     460           44 :     Q_D(StatusService);
     461           82 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
     462           44 : }
     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           38 : bool StatusService::disableButtonPressedNotifications()
     472           44 : {
     473           44 :     Q_D(StatusService);
     474           82 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::buttonPress);
     475           44 : }
     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          380 : std::optional<StatusService::ButtonStatus> StatusService::buttonPress() const
     485          188 : {
     486          188 :     Q_D(const StatusService);
     487          568 :     const QLowEnergyCharacteristic characteristic = d->getCharacteristic(CharacteristicUuids::buttonPress);
     488          948 :     return (characteristic.isValid()) ? StatusServicePrivate::parseButtonPress(characteristic.value()) : std::nullopt;
     489          568 : }
     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         1085 : StatusServicePrivate::StatusServicePrivate(
     567         1330 :     QLowEnergyController * controller, StatusService * const q)
     568         1890 :     : AbstractPokitServicePrivate(QBluetoothUuid(), controller, q)
     569         1260 : {
     570              : 
     571         2345 : }
     572              : 
     573              : /*!
     574              :  * Parses the `Device Characteristics` \a value into a DeviceCharacteristics struct.
     575              :  */
     576          152 : StatusService::DeviceCharacteristics StatusServicePrivate::parseDeviceCharacteristics(
     577              :     const QByteArray &value)
     578          176 : {
     579          328 :     StatusService::DeviceCharacteristics characteristics{
     580          176 :         QVersionNumber(), 0, 0, 0, 0, 0, 0, QBluetoothAddress()
     581          276 :     };
     582          176 :     Q_ASSERT(characteristics.firmwareVersion.isNull());  // How we indicate failure.
     583              : 
     584          388 :     if (!checkSize(QLatin1String("Device Characteristics"), value, 20, 20)) {
     585           88 :         return characteristics;
     586           88 :     }
     587              : 
     588          164 :     characteristics.firmwareVersion = QVersionNumber(
     589          232 :                                           qFromLittleEndian<quint8 >(value.mid(0,1).constData()),
     590          210 :                                           qFromLittleEndian<quint8 >(value.mid(1,1).constData()));
     591          194 :     characteristics.maximumVoltage      = qFromLittleEndian<quint16>(value.mid(2,2).constData());
     592          194 :     characteristics.maximumCurrent      = qFromLittleEndian<quint16>(value.mid(4,2).constData());
     593          194 :     characteristics.maximumResistance   = qFromLittleEndian<quint16>(value.mid(6,2).constData());
     594          194 :     characteristics.maximumSamplingRate = qFromLittleEndian<quint16>(value.mid(8,2).constData());
     595          194 :     characteristics.samplingBufferSize  = qFromLittleEndian<quint16>(value.mid(10,2).constData());
     596          194 :     characteristics.capabilityMask      = qFromLittleEndian<quint16>(value.mid(12,2).constData());
     597          202 :     characteristics.macAddress = QBluetoothAddress(qFromBigEndian<quint64>
     598          296 :                                                    ((QByteArray(2, '\0') + value.mid(14,6)).constData()));
     599              : 
     600          180 :     qCDebug(lc).noquote() << tr("Firmware version:     ") << characteristics.firmwareVersion;
     601          180 :     qCDebug(lc).noquote() << tr("Maximum voltage:      ") << characteristics.maximumVoltage;
     602          180 :     qCDebug(lc).noquote() << tr("Maximum current:      ") << characteristics.maximumCurrent;
     603          180 :     qCDebug(lc).noquote() << tr("Maximum resistance:   ") << characteristics.maximumResistance;
     604          180 :     qCDebug(lc).noquote() << tr("Maximum sampling rate:") << characteristics.maximumSamplingRate;
     605          180 :     qCDebug(lc).noquote() << tr("Sampling buffer size: ") << characteristics.samplingBufferSize;
     606          180 :     qCDebug(lc).noquote() << tr("Capability mask:      ") << characteristics.capabilityMask;
     607          180 :     qCDebug(lc).noquote() << tr("MAC address:          ") << characteristics.macAddress;
     608              : 
     609           88 :     Q_ASSERT(!characteristics.firmwareVersion.isNull()); // How we indicate success.
     610           98 :     return characteristics;
     611          176 : }
     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          494 : StatusService::Status StatusServicePrivate::parseStatus(const QByteArray &value)
     619          572 : {
     620         1066 :     StatusService::Status status{
     621          572 :         static_cast<StatusService::DeviceStatus>
     622          572 :             (std::numeric_limits<std::underlying_type_t<StatusService::DeviceStatus>>::max()),
     623          572 :         std::numeric_limits<float>::quiet_NaN(),
     624          572 :         static_cast<StatusService::BatteryStatus>
     625          572 :             (std::numeric_limits<std::underlying_type_t<StatusService::BatteryStatus>>::max()),
     626          572 :         std::nullopt, std::nullopt,
     627          572 :     };
     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         1261 :     if (!checkSize(QLatin1String("Status"), value, 5, 8)) {
     638          128 :         return status;
     639           88 :     }
     640              : 
     641          902 :     status.deviceStatus = static_cast<StatusService::DeviceStatus>(value.at(0));
     642         1067 :     status.batteryVoltage = qFromLittleEndian<float>(value.mid(1,4).constData());
     643          902 :     if (value.size() >= 6) { // Battery Status added to Pokit API docs v1.00.
     644          820 :         status.batteryStatus = static_cast<StatusService::BatteryStatus>(value.at(5));
     645          440 :     }
     646          902 :     if (value.size() >= 7) { // Switch Position - as yet, undocumented by Pokit Innovations.
     647          820 :         status.switchPosition = static_cast<StatusService::SwitchPosition>(value.at(6));
     648          440 :     }
     649          902 :     if (value.size() >= 8) { // Charging Status - as yet, undocumented by Pokit Innovations.
     650          820 :         status.chargingStatus = static_cast<StatusService::ChargingStatus>(value.at(7));
     651          440 :     }
     652          990 :     qCDebug(lc).noquote() << tr("Device status:   %1 (%2)")
     653            0 :         .arg((quint8)status.deviceStatus).arg(StatusService::toString(status.deviceStatus));
     654          990 :     qCDebug(lc).noquote() << tr("Battery voltage: %1 volts").arg(status.batteryVoltage);
     655          990 :     qCDebug(lc).noquote() << tr("Battery status:  %1 (%2)")
     656            0 :         .arg((quint8)status.batteryStatus).arg(StatusService::toString(status.batteryStatus));
     657          902 :     if (status.switchPosition) {
     658          900 :         qCDebug(lc).noquote() << tr("Switch position: %1 (%2)")
     659            0 :             .arg((quint8)*status.switchPosition).arg(StatusService::toString(*status.switchPosition));
     660          440 :     }
     661          902 :     if (status.chargingStatus) {
     662          900 :         qCDebug(lc).noquote() << tr("Charging status: %1 (%2)")
     663            0 :         .arg((quint8)*status.chargingStatus).arg(StatusService::toString(*status.chargingStatus));
     664          440 :     }
     665          704 :     return status;
     666          572 : }
     667              : 
     668              : /*!
     669              :  * Parses the torch status \a value, and returns the corresponding TorchStatus.
     670              :  */
     671          114 : std::optional<StatusService::TorchStatus> StatusServicePrivate::parseTorchStatus(const QByteArray &value)
     672          132 : {
     673          291 :     if (!checkSize(QLatin1String("Torch"), value, 1, 1)) {
     674           82 :         return std::nullopt;
     675           44 :     }
     676              : 
     677          164 :     const StatusService::TorchStatus status = static_cast<StatusService::TorchStatus>(value.at(0));
     678          180 :     qCDebug(lc).noquote() << tr("Torch status: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     679          164 :     return status;
     680          132 : }
     681              : 
     682              : /*!
     683              :  * Parses the button press \a value, and returns the corresponding ButtonStatus.
     684              :  */
     685          152 : std::optional<StatusService::ButtonStatus> StatusServicePrivate::parseButtonPress(const QByteArray &value)
     686          176 : {
     687          388 :     if (!checkSize(QLatin1String("Torch"), value, 2, 2)) {
     688           82 :         return std::nullopt;
     689           44 :     }
     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          246 :     const StatusService::ButtonStatus status = static_cast<StatusService::ButtonStatus>(value.at(1));
     701          270 :     qCDebug(lc).noquote() << tr("Button: %1 (%2)").arg((quint8)status).arg(StatusService::toString(status));
     702          246 :     return status;
     703          176 : }
     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          152 : void StatusServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
     712          176 : {
     713          328 :     if (newService == StatusService::ServiceUuids::pokitMeter) {
     714           90 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Meter device.");
     715           82 :         serviceUuid = StatusService::ServiceUuids::pokitMeter;
     716          246 :     } else if (newService == StatusService::ServiceUuids::pokitPro) {
     717           90 :         qCDebug(lc).noquote() << tr("Found Status Service for a Pokit Pro device.");
     718           82 :         serviceUuid = StatusService::ServiceUuids::pokitPro;
     719           44 :     }
     720          328 :     AbstractPokitServicePrivate::serviceDiscovered(newService);
     721          328 : }
     722              : 
     723              : /*!
     724              :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     725              :  * specialised signal, for each supported \a characteristic.
     726              :  */
     727           38 : void StatusServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     728              :                                               const QByteArray &value)
     729           44 : {
     730           82 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     731              : 
     732           44 :     Q_Q(StatusService);
     733           82 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::deviceCharacteristics) {
     734            0 :         Q_EMIT q->deviceCharacteristicsRead(parseDeviceCharacteristics(value));
     735            0 :         return;
     736            0 :     }
     737              : 
     738           82 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::status) {
     739            0 :         Q_EMIT q->deviceStatusRead(parseStatus(value));
     740            0 :         return;
     741            0 :     }
     742              : 
     743           82 :     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           82 :     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           82 :     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           82 :     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          179 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for Status service")
     777          135 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     778           44 : }
     779              : 
     780              : /*!
     781              :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     782              :  * specialised signal, for each supported \a characteristic.
     783              :  */
     784           38 : void StatusServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     785              :                                                  const QByteArray &newValue)
     786           44 : {
     787           82 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     788              : 
     789           44 :     Q_Q(StatusService);
     790           82 :     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           82 :     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           82 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::name) {
     803            0 :         Q_EMIT q->deviceNameWritten();
     804            0 :         return;
     805            0 :     }
     806              : 
     807           82 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::flashLed) {
     808            0 :         Q_EMIT q->deviceLedFlashed();
     809            0 :         return;
     810            0 :     }
     811              : 
     812           82 :     if (characteristic.uuid() == StatusService::CharacteristicUuids::torch) {
     813            0 :         Q_EMIT q->torchStatusWritten();
     814            0 :         return;
     815            0 :     }
     816              : 
     817          179 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for Status service")
     818          135 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     819           44 : }
     820              : 
     821              : /// \endcond
        

Generated by: LCOV version 2.0-1