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 45 46.7 %

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

Generated by: LCOV version 1.14