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 55 47.3 %

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

Generated by: LCOV version 1.14