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: 94.7 % 19 18

            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         4500 : QString MultimeterService::toString(const Mode &mode)
      36         2590 : {
      37         7090 :     switch (mode) {
      38         1236 :     case Mode::Idle:        return tr("Idle");
      39          566 :     case Mode::DcVoltage:   return tr("DC voltage");
      40          566 :     case Mode::AcVoltage:   return tr("AC voltage");
      41          566 :     case Mode::DcCurrent:   return tr("DC current");
      42          566 :     case Mode::AcCurrent:   return tr("AC current");
      43          566 :     case Mode::Resistance:  return tr("Resistance");
      44          566 :     case Mode::Diode:       return tr("Diode");
      45         1035 :     case Mode::Continuity:  return tr("Continuity");
      46          566 :     case Mode::Temperature: return tr("Temperature");
      47          566 :     case Mode::Capacitance: return tr("Capacitance");
      48           97 :     case Mode::ExternalTemperature: return tr("External temperature");
      49         2590 :     }
      50          104 :     return QString();
      51         2590 : }
      52              : 
      53              : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
      54         4275 : QString MultimeterService::toString(const PokitProduct product, const quint8 range, const Mode mode)
      55         2690 : {
      56         6965 :     switch (mode) {
      57          434 :     case Mode::Idle:
      58          434 :         break;
      59          852 :     case Mode::DcVoltage:
      60          472 :     case Mode::AcVoltage:
      61         1192 :         return VoltageRange::toString(product, range);
      62         1060 :     case Mode::DcCurrent:
      63          472 :     case Mode::AcCurrent:
      64         1192 :         return CurrentRange::toString(product, range);
      65          790 :     case Mode::Resistance:
      66          790 :         return ResistanceRange::toString(product, range);
      67          132 :     case Mode::Diode:
      68          396 :     case Mode::Continuity:
      69          632 :     case Mode::Temperature:
      70          632 :         break;
      71          596 :     case Mode::Capacitance:
      72          596 :         return CapacitanceRange::toString(product, range);
      73          104 :     case Mode::ExternalTemperature:
      74          104 :         break;
      75         2690 :     }
      76         1170 :     return QString();
      77         2690 : }
      78              : 
      79              : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
      80         3825 : QString MultimeterService::toString(const quint8 range, const Mode mode) const
      81         2170 : {
      82         5995 :     return toString(*pokitProduct(), range, mode);
      83         2170 : }
      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          900 : quint32 MultimeterService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
      89         1040 : {
      90         1940 :     switch (mode) {
      91          104 :     case Mode::Idle:
      92          104 :         break;
      93          180 :     case Mode::DcVoltage:
      94          208 :     case Mode::AcVoltage:
      95          388 :         return VoltageRange::maxValue(product, range);
      96          388 :     case Mode::DcCurrent:
      97          208 :     case Mode::AcCurrent:
      98          388 :         return CurrentRange::maxValue(product, range);
      99          388 :     case Mode::Resistance:
     100          388 :         return ResistanceRange::maxValue(product, range);
     101            0 :     case Mode::Diode:
     102            0 :     case Mode::Continuity:
     103          104 :     case Mode::Temperature:
     104          104 :         break;
     105          194 :     case Mode::Capacitance:
     106          194 :         return CapacitanceRange::maxValue(product, range);
     107          104 :     case Mode::ExternalTemperature:
     108          104 :         break;
     109         1040 :     }
     110          312 :     return 0;
     111         1040 : }
     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          450 : quint32 MultimeterService::maxValue(const quint8 range, const Mode mode) const
     117          520 : {
     118          970 :     return maxValue(*pokitProduct(), range, mode);
     119          520 : }
     120              : 
     121              : /*!
     122              :  * Constructs a new Pokit service with \a parent.
     123              :  */
     124         3285 : MultimeterService::MultimeterService(QLowEnergyController * const controller, QObject * parent)
     125         4891 :     : AbstractPokitService(new MultimeterServicePrivate(controller, this), parent)
     126         2536 : {
     127              : 
     128         5821 : }
     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           45 : bool MultimeterService::readCharacteristics()
     143           52 : {
     144           97 :     return readReadingCharacteristic();
     145           52 : }
     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           67 : bool MultimeterService::readReadingCharacteristic()
     157          104 : {
     158          104 :     Q_D(MultimeterService);
     159          194 :     return d->readCharacteristic(CharacteristicUuids::reading);
     160          104 : }
     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           45 : bool MultimeterService::setSettings(const Settings &settings)
     170           52 : {
     171           52 :     Q_D(const MultimeterService);
     172           52 :     const QLowEnergyCharacteristic characteristic =
     173           97 :         d->getCharacteristic(CharacteristicUuids::settings);
     174           97 :     if (!characteristic.isValid()) {
     175           52 :         return false;
     176           52 :     }
     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           45 : }
     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           45 : MultimeterService::Reading MultimeterService::reading() const
     202           52 : {
     203           52 :     Q_D(const MultimeterService);
     204           52 :     const QLowEnergyCharacteristic characteristic =
     205           97 :         d->getCharacteristic(CharacteristicUuids::reading);
     206           97 :     return (characteristic.isValid()) ? MultimeterServicePrivate::parseReading(characteristic.value())
     207          142 :         : Reading{ MeterStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0 };
     208           97 : }
     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           45 : bool MultimeterService::enableReadingNotifications()
     220           52 : {
     221           52 :     Q_D(MultimeterService);
     222           97 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
     223           52 : }
     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           45 : bool MultimeterService::disableReadingNotifications()
     233           52 : {
     234           52 :     Q_D(MultimeterService);
     235           97 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
     236           52 : }
     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         1606 : MultimeterServicePrivate::MultimeterServicePrivate(
     266         3285 :     QLowEnergyController * controller, MultimeterService * const q)
     267         4891 :     : AbstractPokitServicePrivate(MultimeterService::serviceUuid, controller, q)
     268         2536 : {
     269              : 
     270         4142 : }
     271              : 
     272              : /*!
     273              :  * Returns \a settings in the format Pokit devices expect.
     274              :  */
     275          180 : QByteArray MultimeterServicePrivate::encodeSettings(const MultimeterService::Settings &settings)
     276          208 : {
     277          208 :     static_assert(sizeof(settings.mode)           == 1, "Expected to be 1 byte.");
     278          208 :     static_assert(sizeof(settings.range)          == 1, "Expected to be 1 byte.");
     279          208 :     static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
     280              : 
     281          292 :     QByteArray value;
     282          388 :     QDataStream stream(&value, QIODevice::WriteOnly);
     283          388 :     stream.setByteOrder(QDataStream::LittleEndian);
     284          388 :     stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
     285          388 :     stream << (quint8)settings.mode << settings.range << settings.updateInterval;
     286              : 
     287          208 :     Q_ASSERT(value.size() == 6);
     288          388 :     return value;
     289          388 : }
     290              : 
     291              : /*!
     292              :  * Parses the `Reading` \a value into a MultimeterService::Reading struct.
     293              :  */
     294          225 : MultimeterService::Reading MultimeterServicePrivate::parseReading(const QByteArray &value)
     295          260 : {
     296          355 :     MultimeterService::Reading reading{
     297          260 :         MultimeterService::MeterStatus::Error,
     298          260 :         std::numeric_limits<float>::quiet_NaN(),
     299          260 :         MultimeterService::Mode::Idle, 0
     300          260 :     };
     301              : 
     302          590 :     if (!checkSize(QLatin1String("Reading"), value, 7, 7)) {
     303          156 :         return reading;
     304          104 :     }
     305              : 
     306          276 :     reading.status = MultimeterService::MeterStatus(value.at(0));
     307          354 :     reading.value  = qFromLittleEndian<float>(value.mid(1,4).constData());
     308          276 :     reading.mode   = static_cast<MultimeterService::Mode>(value.at(5));
     309          276 :     reading.range  = static_cast<quint8>(value.at(6));
     310          291 :     return reading;
     311          260 : }
     312              : 
     313              : /*!
     314              :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     315              :  * specialised signal, for each supported \a characteristic.
     316              :  */
     317           45 : void MultimeterServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     318              :                                               const QByteArray &value)
     319           52 : {
     320           97 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     321              : 
     322           52 :     Q_Q(MultimeterService);
     323           97 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
     324            0 :         Q_EMIT q->readingRead(parseReading(value));
     325            0 :         return;
     326            0 :     }
     327              : 
     328           97 :     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          219 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for Multimeter service")
     335          163 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     336           52 : }
     337              : 
     338              : /*!
     339              :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     340              :  * specialised signal, for each supported \a characteristic.
     341              :  */
     342           45 : void MultimeterServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     343              :                                                      const QByteArray &newValue)
     344           52 : {
     345           97 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     346              : 
     347           52 :     Q_Q(MultimeterService);
     348           97 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
     349            0 :         Q_EMIT q->settingsWritten();
     350            0 :         return;
     351            0 :     }
     352              : 
     353           97 :     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          219 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for Multimeter service")
     360          163 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     361           52 : }
     362              : 
     363              : /*!
     364              :  * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
     365              :  * specialised signal, for each supported \a characteristic.
     366              :  */
     367           45 : void MultimeterServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
     368              :                                                      const QByteArray &newValue)
     369           52 : {
     370           97 :     AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
     371              : 
     372           52 :     Q_Q(MultimeterService);
     373           97 :     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           97 :     if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
     380            0 :         Q_EMIT q->readingRead(parseReading(newValue));
     381            0 :         return;
     382            0 :     }
     383              : 
     384          219 :     qCWarning(lc).noquote() << tr("Unknown characteristic notified for Multimeter service")
     385          163 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     386           52 : }
     387              : 
     388              : /// \endcond
        

Generated by: LCOV version 2.3-1