LCOV - code coverage report
Current view: top level - src/lib - dsoservice.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 82.7 % 249 206
Version: Functions: 96.0 % 25 24

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

Generated by: LCOV version 2.4-0