LCOV - code coverage report
Current view: top level - src/lib - dsoservice.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 81.9 % 238 195
Version: Functions: 90.7 % 54 49

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

Generated by: LCOV version 2.2-1