LCOV - code coverage report
Current view: top level - src/lib - dsoservice.cpp (source / functions) Hit Total Coverage
Project: Dokit Lines: 111 143 77.6 %
Version: Functions: 26 30 86.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 DsoService and DsoServicePrivate classes.
       7             :  */
       8             : 
       9             : #include <qtpokit/dsoservice.h>
      10             : #include <qtpokit/pokitmeter.h>
      11             : #include <qtpokit/pokitpro.h>
      12             : #include "dsoservice_p.h"
      13             : #include "pokitproducts_p.h"
      14             : 
      15             : #include <QDataStream>
      16             : #include <QIODevice>
      17             : #include <QtEndian>
      18             : 
      19             : /*!
      20             :  * \class DsoService
      21             :  *
      22             :  * The DsoService class accesses the `DSO` (Digital Storage Oscilloscope) service of Pokit devices.
      23             :  */
      24             : 
      25             : /// UUID of the "DSO" service.
      26             : const QBluetoothUuid DsoService::
      27             :     serviceUuid(QLatin1String("1569801e-1425-4a7a-b617-a4f4ed719de6"));
      28             : 
      29             : /// \struct DsoService::CharacteristicUuids
      30             : /// \brief Characteristics available via the `DSO` service.
      31             : 
      32             : /// UUID of the `DSO` service's `Settings` characterstic.
      33             : const QBluetoothUuid DsoService::CharacteristicUuids::
      34             :     settings(QLatin1String("a81af1b6-b8b3-4244-8859-3da368d2be39"));
      35             : 
      36             : /// UUID of the `DSO` service's `Metadata` characterstic.
      37             : const QBluetoothUuid DsoService::CharacteristicUuids::
      38             :     metadata(QLatin1String("970f00ba-f46f-4825-96a8-153a5cd0cda9"));
      39             : 
      40             : /// UUID of the `DSO` service's `Reading` characterstic.
      41             : const QBluetoothUuid DsoService::CharacteristicUuids::
      42             :     reading(QLatin1String("98e14f8e-536e-4f24-b4f4-1debfed0a99e"));
      43             : 
      44             : /// \enum DsoService::Command
      45             : /// \brief Values supported by the `Command` attribute of the `Settings` characteristic.
      46             : 
      47             : /// \enum DsoService::Mode
      48             : /// \brief Values supported by the `Mode` attribute of the `Settings` and `Metadata` characteristics.
      49             : 
      50             : /// Returns \a mode as a user-friendly string.
      51        2214 : QString DsoService::toString(const Mode &mode)
      52             : {
      53        2214 :     switch (mode) {
      54          18 :     case Mode::Idle:        return tr("Idle");
      55         540 :     case Mode::DcVoltage:   return tr("DC voltage");
      56         540 :     case Mode::AcVoltage:   return tr("AC voltage");
      57         540 :     case Mode::DcCurrent:   return tr("DC current");
      58         540 :     case Mode::AcCurrent:   return tr("AC current");
      59             :     default:                return QString();
      60             :     }
      61             : }
      62             : 
      63             : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
      64        1476 : QString DsoService::toString(const PokitProduct product, const quint8 range, const Mode mode)
      65             : {
      66        1476 :     switch (mode) {
      67             :     case Mode::Idle:
      68             :         break;
      69         720 :     case Mode::DcVoltage:
      70             :     case Mode::AcVoltage:
      71         720 :         return VoltageRange::toString(product, range);
      72         720 :     case Mode::DcCurrent:
      73             :     case Mode::AcCurrent:
      74         720 :         return CurrentRange::toString(product, range);
      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        1386 : QString DsoService::toString(const quint8 range, const Mode mode) const
      81             : {
      82        1386 :     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         180 : QVariant DsoService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
      91             : {
      92         180 :     switch (mode) {
      93             :     case Mode::Idle:
      94             :         break;
      95          72 :     case Mode::DcVoltage:
      96             :     case Mode::AcVoltage:
      97          72 :         return VoltageRange::maxValue(product, range);
      98          72 :     case Mode::DcCurrent:
      99             :     case Mode::AcCurrent:
     100          72 :         return CurrentRange::maxValue(product, range);
     101             :     }
     102             :     return QVariant();
     103             : }
     104             : 
     105             : /*!
     106             :  *  Returns the maximum value for \a range, or the string "Auto".
     107             :  *
     108             :  *  If \a range is not a known valid enumeration value for the current \a product's \a mode,
     109             :  *  then a null QVariant is returned.
     110             :  */
     111          90 : QVariant DsoService::maxValue(const quint8 range, const Mode mode) const
     112             : {
     113          90 :     return maxValue(pokitProduct(), range, mode);
     114             : }
     115             : 
     116             : /// \struct DsoService::Settings
     117             : /// \brief Attributes included in the `Settings` characterstic.
     118             : 
     119             : /// \enum DsoService::DsoStatus
     120             : /// \brief Values supported by the `Status` attribute of the `Metadata` characteristic.
     121             : 
     122             : /// \struct DsoService::Metadata
     123             : /// \brief Attributes included in the `Metadata` characterstic.
     124             : 
     125             : /*!
     126             :  * \typedef DsoService::Samples
     127             :  *
     128             :  * Raw samples from the `Reading` characteristic. These raw samples are (supposedly) wihtin the
     129             :  * range -2048 to +2047, and need to be multiplied by the Metadata::scale value from the `Metadata`
     130             :  * characteristc to get the true values.
     131             :  *
     132             :  * Also supposedly, there should be no more than 10 samples at a time, according to Pokit's current
     133             :  * API docs. There is not artificial limitation imposed by QtPokit, so devices may begin batching
     134             :  * more samples in future.
     135             :  */
     136             : 
     137             : /*!
     138             :  * Constructs a new Pokit service with \a parent.
     139             :  */
     140        2034 : DsoService::DsoService(QLowEnergyController * const controller, QObject * parent)
     141        2034 :     : AbstractPokitService(new DsoServicePrivate(controller, this), parent)
     142             : {
     143             : 
     144        2034 : }
     145             : 
     146             : /*!
     147             :  * \cond internal
     148             :  * Constructs a new Pokit service with \a parent, and private implementation \a d.
     149             :  */
     150           0 : DsoService::DsoService(
     151           0 :     DsoServicePrivate * const d, QObject * const parent)
     152           0 :     : AbstractPokitService(d, parent)
     153             : {
     154             : 
     155           0 : }
     156             : /// \endcond
     157             : 
     158             : /*!
     159             :  * Destroys this DsoService object.
     160             :  */
     161         432 : DsoService::~DsoService()
     162             : {
     163             : 
     164         432 : }
     165             : 
     166          18 : bool DsoService::readCharacteristics()
     167             : {
     168          18 :     return readMetadataCharacteristic();
     169             : }
     170             : 
     171             : /*!
     172             :  * Reads the `DSO` service's `Metadata` characteristic.
     173             :  *
     174             :  * Returns `true` is the read request is succesfully queued, `false` otherwise (ie if the
     175             :  * underlying controller it not yet connected to the Pokit device, or the device's services have
     176             :  * not yet been discovered).
     177             :  *
     178             :  * Emits metadataRead() if/when the characteristic has been read successfully.
     179             :  */
     180          30 : bool DsoService::readMetadataCharacteristic()
     181             : {
     182             :     Q_D(DsoService);
     183          36 :     return d->readCharacteristic(CharacteristicUuids::metadata);
     184             : }
     185             : 
     186             : /*!
     187             :  * Configures the Pokit device's DSO mode.
     188             :  *
     189             :  * Returns `true` if the write request was successfully queued, `false` otherwise.
     190             :  *
     191             :  * Emits settingsWritten() if/when the \a settings have been writtem successfully.
     192             :  */
     193          90 : bool DsoService::setSettings(const Settings &settings)
     194             : {
     195             :     Q_D(const DsoService);
     196             :     const QLowEnergyCharacteristic characteristic =
     197          90 :         d->getCharacteristic(CharacteristicUuids::settings);
     198          90 :     if (!characteristic.isValid()) {
     199             :         return false;
     200             :     }
     201             : 
     202           0 :     const QByteArray value = DsoServicePrivate::encodeSettings(settings);
     203           0 :     if (value.isNull()) {
     204             :         return false;
     205             :     }
     206             : 
     207           0 :     d->service->writeCharacteristic(characteristic, value);
     208           0 :     return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
     209          90 : }
     210             : 
     211             : /*!
     212             :  * Start the DSO with \a settings.
     213             :  *
     214             :  * This is just a synonym for setSettings() except makes the caller's intention more explicit, and
     215             :  * sanity-checks that the settings's command is not DsoService::Command::ResendData.
     216             :  */
     217          72 : bool DsoService::startDso(const Settings &settings)
     218             : {
     219             :     Q_D(const DsoService);
     220             :     Q_ASSERT(settings.command != DsoService::Command::ResendData);
     221          72 :     if (settings.command == DsoService::Command::ResendData) {
     222          40 :         qCWarning(d->lc).noquote() << tr("Settings command must not be 'ResendData'.");
     223          18 :         return false;
     224             :     }
     225          54 :     return setSettings(settings);
     226             : }
     227             : 
     228             : /*!
     229             :  * Fetch DSO samples.
     230             :  *
     231             :  * This is just a convenience function equivalent to calling setSettings() with the command set to
     232             :  * DsoService::Command::Refresh.
     233             :  *
     234             :  * Once the Pokit device has processed this request succesffully, the device will begin notifying
     235             :  * the `Metadata` and `Reading` characteristic, resulting in emits of metadataRead and samplesRead
     236             :  * respectively.
     237             :  */
     238          18 : bool DsoService::fetchSamples()
     239             : {
     240             :     // Note, only the Settings::command member need be set, since the others are all ignored by the
     241             :     // Pokit device when the command is Refresh. However, we still explicitly initialise all other
     242             :     // members just to ensure we're never exposing uninitialised RAM to an external device.
     243          18 :     return setSettings({ DsoService::Command::ResendData, 0, DsoService::Mode::Idle, 0, 0, 0 });
     244             : }
     245             : 
     246             : /*!
     247             :  * Returns the most recent value of the `DSO` service's `Metadata` characteristic.
     248             :  *
     249             :  * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
     250             :  * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then the
     251             :  * returned DsoService::Metadata::scale member will be a quiet NaN, which can be checked like:
     252             :  *
     253             :  * ```
     254             :  * const DsoService::Metadata metadata = multimeterService->metadata();
     255             :  * if (qIsNaN(metadata.scale)) {
     256             :  *     // Handle failure.
     257             :  * }
     258             :  * ```
     259             :  */
     260          18 : DsoService::Metadata DsoService::metadata() const
     261             : {
     262             :     Q_D(const DsoService);
     263             :     const QLowEnergyCharacteristic characteristic =
     264          18 :         d->getCharacteristic(CharacteristicUuids::metadata);
     265          18 :     return (characteristic.isValid()) ? DsoServicePrivate::parseMetadata(characteristic.value())
     266          36 :         : Metadata{ DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
     267          18 : }
     268             : 
     269             : /*!
     270             :  * Enables client-side notifications of DSO metadata changes.
     271             :  *
     272             :  * This is an alternative to manually requesting individual reads via readMetadataCharacteristic().
     273             :  *
     274             :  * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
     275             :  *
     276             :  * Successfully read values (if any) will be emitted via the metadataRead() signal.
     277             :  */
     278          18 : bool DsoService::enableMetadataNotifications()
     279             : {
     280             :     Q_D(DsoService);
     281          18 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
     282             : }
     283             : 
     284             : /*!
     285             :  * Disables client-side notifications of DSO metadata changes.
     286             :  *
     287             :  * Instantaneous reads can still be fetched by readMetadataCharacteristic().
     288             :  *
     289             :  * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
     290             :  */
     291          18 : bool DsoService::disableMetadataNotifications()
     292             : {
     293             :     Q_D(DsoService);
     294          18 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
     295             : }
     296             : 
     297             : /*!
     298             :  * Enables client-side notifications of DSO readings.
     299             :  *
     300             :  * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
     301             :  *
     302             :  * Successfully read samples (if any) will be emitted via the samplesRead() signal.
     303             :  */
     304          18 : bool DsoService::enableReadingNotifications()
     305             : {
     306             :     Q_D(DsoService);
     307          18 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
     308             : }
     309             : 
     310             : /*!
     311             :  * Disables client-side notifications of DSO readings.
     312             :  *
     313             :  * Returns `true` is the request was successfully submited to the device queue, `false` otherwise.
     314             :  */
     315          18 : bool DsoService::disableReadingNotifications()
     316             : {
     317             :     Q_D(DsoService);
     318          18 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
     319             : }
     320             : 
     321             : /*!
     322             :  * \fn DsoService::settingsWritten
     323             :  *
     324             :  * This signal is emitted when the `Settings` characteristic has been written successfully.
     325             :  *
     326             :  * \see setSettings
     327             :  */
     328             : 
     329             : /*!
     330             :  * \fn DsoService::metadataRead
     331             :  *
     332             :  * This signal is emitted when the `Metadata` characteristic has been read successfully.
     333             :  *
     334             :  * \see readMetadataCharacteristic
     335             :  */
     336             : 
     337             : /*!
     338             :  * \fn DsoService::samplesRead
     339             :  *
     340             :  * This signal is emitted when the `Reading` characteristic has been notified.
     341             :  *
     342             :  * \see beginSampling
     343             :  * \see stopSampling
     344             :  */
     345             : 
     346             : 
     347             : /*!
     348             :  * \cond internal
     349             :  * \class DsoServicePrivate
     350             :  *
     351             :  * The DsoServicePrivate class provides private implementation for DsoService.
     352             :  */
     353             : 
     354             : /*!
     355             :  * \internal
     356             :  * Constructs a new DsoServicePrivate object with public implementation \a q.
     357             :  */
     358        1356 : DsoServicePrivate::DsoServicePrivate(
     359        2034 :     QLowEnergyController * controller, DsoService * const q)
     360        2034 :     : AbstractPokitServicePrivate(DsoService::serviceUuid, controller, q)
     361             : {
     362             : 
     363        1356 : }
     364             : 
     365             : /*!
     366             :  * Returns \a settings in the format Pokit devices expect.
     367             :  */
     368          54 : QByteArray DsoServicePrivate::encodeSettings(const DsoService::Settings &settings)
     369             : {
     370             :     static_assert(sizeof(settings.command)         == 1, "Expected to be 1 byte.");
     371             :     static_assert(sizeof(settings.triggerLevel)    == 4, "Expected to be 2 bytes.");
     372             :     static_assert(sizeof(settings.mode)            == 1, "Expected to be 1 byte.");
     373             :     static_assert(sizeof(settings.range)           == 1, "Expected to be 1 byte.");
     374             :     static_assert(sizeof(settings.samplingWindow)  == 4, "Expected to be 4 bytes.");
     375             :     static_assert(sizeof(settings.numberOfSamples) == 2, "Expected to be 2 bytes.");
     376             : 
     377          12 :     QByteArray value;
     378          54 :     QDataStream stream(&value, QIODevice::WriteOnly);
     379          54 :     stream.setByteOrder(QDataStream::LittleEndian);
     380          54 :     stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
     381          54 :     stream << (quint8)settings.command << settings.triggerLevel << (quint8)settings.mode
     382          54 :            << settings.range << settings.samplingWindow << settings.numberOfSamples;
     383             : 
     384             :     Q_ASSERT(value.size() == 13);
     385          54 :     return value;
     386          54 : }
     387             : 
     388             : /*!
     389             :  * Parses the `Metadata` \a value into a DsoService::Metatdata struct.
     390             :  */
     391          72 : DsoService::Metadata DsoServicePrivate::parseMetadata(const QByteArray &value)
     392             : {
     393          72 :     DsoService::Metadata metadata{
     394             :         DsoService::DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(),
     395             :         DsoService::Mode::Idle, 0, 0, 0, 0
     396             :     };
     397             : 
     398          88 :     if (!checkSize(QLatin1String("Metadata"), value, 17, 17)) {
     399             :         return metadata;
     400             :     }
     401             : 
     402          36 :     metadata.status          = static_cast<DsoService::DsoStatus>(value.at(0));
     403          44 :     metadata.scale           = qFromLittleEndian<float>(value.mid(1,4));
     404          36 :     metadata.mode            = static_cast<DsoService::Mode>(value.at(5));
     405          36 :     metadata.range           = static_cast<quint8>(value.at(6));
     406          44 :     metadata.samplingWindow  = qFromLittleEndian<quint32>(value.mid(7,4));
     407          44 :     metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2));
     408          44 :     metadata.samplingRate    = qFromLittleEndian<quint32>(value.mid(13,4));
     409          36 :     return metadata;
     410             : }
     411             : 
     412             : /*!
     413             :  * Parses the `Reading` \a value into a DsoService::Samples vector.
     414             :  */
     415          72 : DsoService::Samples DsoServicePrivate::parseSamples(const QByteArray &value)
     416             : {
     417          16 :     DsoService::Samples samples;
     418          72 :     if ((value.size()%2) != 0) {
     419          42 :         qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
     420          28 :             .arg(value.size()).arg(toHexString(value));
     421           3 :         return samples;
     422             :     }
     423         306 :     while ((samples.size()*2) < value.size()) {
     424         308 :         samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2)));
     425             :     }
     426          54 :     qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
     427           9 :     return samples;
     428           0 : }
     429             : 
     430             : /*!
     431             :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     432             :  * specialised signal, for each supported \a characteristic.
     433             :  */
     434          18 : void DsoServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     435             :                                               const QByteArray &value)
     436             : {
     437          18 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     438             : 
     439          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
     440           0 :         qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
     441           0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     442           0 :         return;
     443             :     }
     444             : 
     445             :     Q_Q(DsoService);
     446          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
     447           0 :         emit q->metadataRead(parseMetadata(value));
     448           0 :         return;
     449             :     }
     450             : 
     451          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
     452           0 :         qCWarning(lc).noquote() << tr("Reading characteristic is notify-only")
     453           0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     454           0 :         return;
     455             :     }
     456             : 
     457          54 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for DSO service")
     458          24 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     459             : }
     460             : 
     461             : /*!
     462             :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     463             :  * specialised signal, for each supported \a characteristic.
     464             :  */
     465          18 : void DsoServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     466             :                                                      const QByteArray &newValue)
     467             : {
     468          18 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     469             : 
     470             :     Q_Q(DsoService);
     471          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
     472           0 :         emit q->settingsWritten();
     473           0 :         return;
     474             :     }
     475             : 
     476          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
     477           0 :         qCWarning(lc).noquote() << tr("Metadata characteristic is read/notify, but somehow written")
     478           0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     479           0 :         return;
     480             :     }
     481             : 
     482          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
     483           0 :         qCWarning(lc).noquote() << tr("Reading characteristic is notify-only, but somehow written")
     484           0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     485           0 :         return;
     486             :     }
     487             : 
     488          60 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for DSO service")
     489          24 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     490             : }
     491             : 
     492             : /*!
     493             :  * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
     494             :  * specialised signal, for each supported \a characteristic.
     495             :  */
     496          18 : void DsoServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
     497             :                                                      const QByteArray &newValue)
     498             : {
     499          18 :     AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
     500             : 
     501             :     Q_Q(DsoService);
     502          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
     503           0 :         qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
     504           0 :             << serviceUuid << characteristic.name() << characteristic.uuid();
     505           0 :         return;
     506             :     }
     507             : 
     508          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
     509           0 :         emit q->metadataRead(parseMetadata(newValue));
     510           0 :         return;
     511             :     }
     512             : 
     513          18 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
     514           0 :         emit q->samplesRead(parseSamples(newValue));
     515           0 :         return;
     516             :     }
     517             : 
     518          54 :     qCWarning(lc).noquote() << tr("Unknown characteristic notified for DSO service")
     519          24 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     520             : }
     521             : 
     522             : /// \endcond

Generated by: LCOV version 1.14