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: 86.4 % 22 19

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

Generated by: LCOV version 2.0-1