LCOV - code coverage report
Current view: top level - src/lib - multimeterservice.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 83.5 % 206 172
Version: Functions: 88.6 % 44 39

            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 MultimeterService and MultimeterServicePrivate classes.
       7              :  */
       8              : 
       9              : #include <qtpokit/multimeterservice.h>
      10              : #include "multimeterservice_p.h"
      11              : #include "pokitproducts_p.h"
      12              : 
      13              : #include <QDataStream>
      14              : #include <QIODevice>
      15              : #include <QtEndian>
      16              : 
      17              : /*!
      18              :  * \class MultimeterService
      19              :  *
      20              :  * The MultimeterService class accesses the `Multimeter` service of Pokit devices.
      21              :  */
      22              : 
      23              : /*!
      24              :  * \cond internal
      25              :  * \enum MultimeterService::Mode
      26              :  * \pokitApi The following enumeration values are as-yet undocumented by Pokit Innovations.
      27              :  * [\@pcolby](https://github.com/pcolby) reverse-engineered them as part of the
      28              :  * [dokit](https://github.com/pcolby/dokit) project.
      29              :  *   * Mode::Capacitance
      30              :  *   * Mode::ExternalTemperature
      31              :  * \endcond
      32              :  */
      33              : 
      34              : /// Returns \a mode as a user-friendly string.
      35         4000 : QString MultimeterService::toString(const Mode &mode)
      36         2090 : {
      37         6090 :     switch (mode) {
      38         1056 :     case Mode::Idle:        return tr("Idle");
      39          486 :     case Mode::DcVoltage:   return tr("DC voltage");
      40          486 :     case Mode::AcVoltage:   return tr("AC voltage");
      41          486 :     case Mode::DcCurrent:   return tr("DC current");
      42          486 :     case Mode::AcCurrent:   return tr("AC current");
      43          486 :     case Mode::Resistance:  return tr("Resistance");
      44          486 :     case Mode::Diode:       return tr("Diode");
      45          885 :     case Mode::Continuity:  return tr("Continuity");
      46          486 :     case Mode::Temperature: return tr("Temperature");
      47          486 :     case Mode::Capacitance: return tr("Capacitance");
      48           87 :     case Mode::ExternalTemperature: return tr("External temperature");
      49         2090 :     }
      50           94 :     return QString();
      51         2090 : }
      52              : 
      53              : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
      54         3800 : QString MultimeterService::toString(const PokitProduct product, const quint8 range, const Mode mode)
      55         2215 : {
      56         6015 :     switch (mode) {
      57          349 :     case Mode::Idle:
      58          349 :         break;
      59          742 :     case Mode::DcVoltage:
      60          392 :     case Mode::AcVoltage:
      61         1032 :         return VoltageRange::toString(product, range);
      62          930 :     case Mode::DcCurrent:
      63          392 :     case Mode::AcCurrent:
      64         1032 :         return CurrentRange::toString(product, range);
      65          690 :     case Mode::Resistance:
      66          690 :         return ResistanceRange::toString(product, range);
      67          102 :     case Mode::Diode:
      68          306 :     case Mode::Continuity:
      69          502 :     case Mode::Temperature:
      70          502 :         break;
      71          516 :     case Mode::Capacitance:
      72          516 :         return CapacitanceRange::toString(product, range);
      73           94 :     case Mode::ExternalTemperature:
      74           94 :         break;
      75         2215 :     }
      76          945 :     return QString();
      77         2215 : }
      78              : 
      79              : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
      80         3400 : QString MultimeterService::toString(const quint8 range, const Mode mode) const
      81         1745 : {
      82         5145 :     return toString(*pokitProduct(), range, mode);
      83         1745 : }
      84              : 
      85              : /*!
      86              :  * Returns the maximum value for \a range, or 0 if \a range is not a known value for \a product's \a mode.
      87              :  */
      88          800 : quint32 MultimeterService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
      89          940 : {
      90         1740 :     switch (mode) {
      91           94 :     case Mode::Idle:
      92           94 :         break;
      93          160 :     case Mode::DcVoltage:
      94          188 :     case Mode::AcVoltage:
      95          348 :         return VoltageRange::maxValue(product, range);
      96          348 :     case Mode::DcCurrent:
      97          188 :     case Mode::AcCurrent:
      98          348 :         return CurrentRange::maxValue(product, range);
      99          348 :     case Mode::Resistance:
     100          348 :         return ResistanceRange::maxValue(product, range);
     101            0 :     case Mode::Diode:
     102            0 :     case Mode::Continuity:
     103           94 :     case Mode::Temperature:
     104           94 :         break;
     105          174 :     case Mode::Capacitance:
     106          174 :         return CapacitanceRange::maxValue(product, range);
     107           94 :     case Mode::ExternalTemperature:
     108           94 :         break;
     109          940 :     }
     110          282 :     return 0;
     111          940 : }
     112              : 
     113              : /*!
     114              :  * Returns the maximum value for \a range, or 0 \a range is not a known value for the current \a product's \a mode.
     115              :  */
     116          400 : quint32 MultimeterService::maxValue(const quint8 range, const Mode mode) const
     117          470 : {
     118          870 :     return maxValue(*pokitProduct(), range, mode);
     119          470 : }
     120              : 
     121              : /*!
     122              :  * Constructs a new Pokit service with \a parent.
     123              :  */
     124         2920 : MultimeterService::MultimeterService(QLowEnergyController * const controller, QObject * parent)
     125         4161 :     : AbstractPokitService(new MultimeterServicePrivate(controller, this), parent)
     126         2171 : {
     127              : 
     128         5091 : }
     129              : 
     130              : /*!
     131              :  * \cond internal
     132              :  * Constructs a new Pokit service with \a parent, and private implementation \a d.
     133              :  */
     134            0 : MultimeterService::MultimeterService(
     135            0 :     MultimeterServicePrivate * const d, QObject * const parent)
     136            0 :     : AbstractPokitService(d, parent)
     137            0 : {
     138              : 
     139            0 : }
     140              : /// \endcond
     141              : 
     142           40 : bool MultimeterService::readCharacteristics()
     143           47 : {
     144           87 :     return readReadingCharacteristic();
     145           47 : }
     146              : 
     147              : /*!
     148              :  * Read the `Multimeter` service's `Reading` characteristic.
     149              :  *
     150              :  * Returns `true` is the read request is succesfully queued, `false` otherwise (ie if the
     151              :  * underlying controller it not yet connected to the Pokit device, or the device's services have
     152              :  * not yet been discovered).
     153              :  *
     154              :  * Emits readingRead() if/when the characteristic has been read successfully.
     155              :  */
     156           57 : bool MultimeterService::readReadingCharacteristic()
     157           94 : {
     158           94 :     Q_D(MultimeterService);
     159          174 :     return d->readCharacteristic(CharacteristicUuids::reading);
     160           94 : }
     161              : 
     162              : /*!
     163              :  * Configures the Pokit device's multimeter mode.
     164              :  *
     165              :  * Returns `true` if the write request was successfully queued, `false` otherwise.
     166              :  *
     167              :  * Emits settingsWritten() if/when the \a settings have been writtem successfully.
     168              :  */
     169           40 : bool MultimeterService::setSettings(const Settings &settings)
     170           47 : {
     171           47 :     Q_D(const MultimeterService);
     172           47 :     const QLowEnergyCharacteristic characteristic =
     173           87 :         d->getCharacteristic(CharacteristicUuids::settings);
     174           87 :     if (!characteristic.isValid()) {
     175           47 :         return false;
     176           47 :     }
     177              : 
     178            0 :     const QByteArray value = MultimeterServicePrivate::encodeSettings(settings);
     179            0 :     if (value.isNull()) {
     180            0 :         return false;
     181            0 :     }
     182              : 
     183            0 :     d->service->writeCharacteristic(characteristic, value);
     184            0 :     return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
     185           40 : }
     186              : 
     187              : /*!
     188              :  * Returns the most recent value of the `Multimeter` service's `Reading` characteristic.
     189              :  *
     190              :  * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
     191              :  * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then the
     192              :  * returned MultimeterService::Reading::value member will be a quiet NaN, which can be checked like:
     193              :  *
     194              :  * ```
     195              :  * const MultimeterService::Reading reading = multimeterService->reading();
     196              :  * if (qIsNaN(reading.value)) {
     197              :  *     // Handle failure.
     198              :  * }
     199              :  * ```
     200              :  */
     201           40 : MultimeterService::Reading MultimeterService::reading() const
     202           47 : {
     203           47 :     Q_D(const MultimeterService);
     204           47 :     const QLowEnergyCharacteristic characteristic =
     205           87 :         d->getCharacteristic(CharacteristicUuids::reading);
     206           87 :     return (characteristic.isValid()) ? MultimeterServicePrivate::parseReading(characteristic.value())
     207          127 :         : Reading{ MeterStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0 };
     208           87 : }
     209              : 
     210              : /*!
     211              :  * Enables client-side notifications of meter readings.
     212              :  *
     213              :  * This is an alternative to manually requesting individual reads via readReadingCharacteristic().
     214              :  *
     215              :  * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
     216              :  *
     217              :  * Successfully read values (if any) will be emitted via the readingRead() signal.
     218              :  */
     219           40 : bool MultimeterService::enableReadingNotifications()
     220           47 : {
     221           47 :     Q_D(MultimeterService);
     222           87 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
     223           47 : }
     224              : 
     225              : /*!
     226              :  * Disables client-side notifications of meter readings.
     227              :  *
     228              :  * Instantaneous reads can still be fetched by readReadingCharacteristic().
     229              :  *
     230              :  * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
     231              :  */
     232           40 : bool MultimeterService::disableReadingNotifications()
     233           47 : {
     234           47 :     Q_D(MultimeterService);
     235           87 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
     236           47 : }
     237              : 
     238              : /*!
     239              :  * \fn MultimeterService::readingRead
     240              :  *
     241              :  * This signal is emitted when the `Reading` characteristic has been read successfully.
     242              :  *
     243              :  * \see readReadingCharacteristic
     244              :  */
     245              : 
     246              : /*!
     247              :  * \fn MultimeterService::settingsWritten
     248              :  *
     249              :  * This signal is emitted when the `Settings` characteristic has been written successfully.
     250              :  *
     251              :  * \see setSettings
     252              :  */
     253              : 
     254              : /*!
     255              :  * \cond internal
     256              :  * \class MultimeterServicePrivate
     257              :  *
     258              :  * The MultimeterServicePrivate class provides private implementation for MultimeterService.
     259              :  */
     260              : 
     261              : /*!
     262              :  * \internal
     263              :  * Constructs a new MultimeterServicePrivate object with public implementation \a q.
     264              :  */
     265         1241 : MultimeterServicePrivate::MultimeterServicePrivate(
     266         2920 :     QLowEnergyController * controller, MultimeterService * const q)
     267         4161 :     : AbstractPokitServicePrivate(MultimeterService::serviceUuid, controller, q)
     268         2171 : {
     269              : 
     270         3412 : }
     271              : 
     272              : /*!
     273              :  * Returns \a settings in the format Pokit devices expect.
     274              :  */
     275          160 : QByteArray MultimeterServicePrivate::encodeSettings(const MultimeterService::Settings &settings)
     276          188 : {
     277          188 :     static_assert(sizeof(settings.mode)           == 1, "Expected to be 1 byte.");
     278          188 :     static_assert(sizeof(settings.range)          == 1, "Expected to be 1 byte.");
     279          188 :     static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
     280              : 
     281          256 :     QByteArray value;
     282          348 :     QDataStream stream(&value, QIODevice::WriteOnly);
     283          348 :     stream.setByteOrder(QDataStream::LittleEndian);
     284          348 :     stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
     285          348 :     stream << (quint8)settings.mode << settings.range << settings.updateInterval;
     286              : 
     287          188 :     Q_ASSERT(value.size() == 6);
     288          348 :     return value;
     289          348 : }
     290              : 
     291              : /*!
     292              :  * Parses the `Reading` \a value into a MultimeterService::Reading struct.
     293              :  */
     294          200 : MultimeterService::Reading MultimeterServicePrivate::parseReading(const QByteArray &value)
     295          235 : {
     296          330 :     MultimeterService::Reading reading{
     297          235 :         MultimeterService::MeterStatus::Error,
     298          235 :         std::numeric_limits<float>::quiet_NaN(),
     299          235 :         MultimeterService::Mode::Idle, 0
     300          235 :     };
     301              : 
     302          520 :     if (!checkSize(QLatin1String("Reading"), value, 7, 7)) {
     303          136 :         return reading;
     304           94 :     }
     305              : 
     306          261 :     reading.status = MultimeterService::MeterStatus(value.at(0));
     307          312 :     reading.value  = qFromLittleEndian<float>(value.mid(1,4).constData());
     308          261 :     reading.mode   = static_cast<MultimeterService::Mode>(value.at(5));
     309          261 :     reading.range  = static_cast<quint8>(value.at(6));
     310          261 :     return reading;
     311          235 : }
     312              : 
     313              : /*!
     314              :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     315              :  * specialised signal, for each supported \a characteristic.
     316              :  */
     317           40 : void MultimeterServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     318              :                                               const QByteArray &value)
     319           47 : {
     320           87 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     321              : 
     322           47 :     Q_Q(MultimeterService);
     323           87 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
     324            0 :         Q_EMIT q->readingRead(parseReading(value));
     325            0 :         return;
     326            0 :     }
     327              : 
     328           87 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
     329            0 :         qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
     330            0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     331            0 :         return;
     332            0 :     }
     333              : 
     334          192 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for Multimeter service")
     335          144 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     336           47 : }
     337              : 
     338              : /*!
     339              :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     340              :  * specialised signal, for each supported \a characteristic.
     341              :  */
     342           40 : void MultimeterServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     343              :                                                      const QByteArray &newValue)
     344           47 : {
     345           87 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     346              : 
     347           47 :     Q_Q(MultimeterService);
     348           87 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
     349            0 :         Q_EMIT q->settingsWritten();
     350            0 :         return;
     351            0 :     }
     352              : 
     353           87 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
     354            0 :         qCWarning(lc).noquote() << tr("Reading characteristic is read/notify, but somehow written")
     355            0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     356            0 :         return;
     357            0 :     }
     358              : 
     359          192 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for Multimeter service")
     360          144 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     361           47 : }
     362              : 
     363              : /*!
     364              :  * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
     365              :  * specialised signal, for each supported \a characteristic.
     366              :  */
     367           40 : void MultimeterServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
     368              :                                                      const QByteArray &newValue)
     369           47 : {
     370           87 :     AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
     371              : 
     372           47 :     Q_Q(MultimeterService);
     373           87 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
     374            0 :         qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
     375            0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     376            0 :         return;
     377            0 :     }
     378              : 
     379           87 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
     380            0 :         Q_EMIT q->readingRead(parseReading(newValue));
     381            0 :         return;
     382            0 :     }
     383              : 
     384          192 :     qCWarning(lc).noquote() << tr("Unknown characteristic notified for Multimeter service")
     385          144 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     386           47 : }
     387              : 
     388              : /// \endcond
        

Generated by: LCOV version 2.2-1