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: 95.8 % 24 23

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

Generated by: LCOV version 2.3.1-1