LCOV - code coverage report
Current view: top level - src/lib - multimeterservice.cpp (source / functions) Hit Total Coverage
Project: Dokit Lines: 97 120 80.8 %
Version: Functions: 21 25 84.0 %

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

Generated by: LCOV version 1.14