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

Generated by: LCOV version 2.3.1-1