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          618 : QString DsoService::toString(const Command &command)
      31          768 : {
      32         1386 :     switch (command) {
      33          231 :     case Command::FreeRunning:        return tr("Free running");
      34          231 :     case Command::RisingEdgeTrigger:  return tr("Rising edge trigger");
      35          231 :     case Command::FallingEdgeTrigger: return tr("Falling edge trigger");
      36          231 :     case Command::ResendData:         return tr("Resend data");
      37          294 :     default:                          return QString();
      38          768 :     }
      39          768 : }
      40              : 
      41              : /// Returns \a mode as a user-friendly string.
      42        12669 : QString DsoService::toString(const Mode &mode)
      43         7392 : {
      44        20061 :     switch (mode) {
      45          231 :     case Mode::Idle:        return tr("Idle");
      46         4842 :     case Mode::DcVoltage:   return tr("DC voltage");
      47         4842 :     case Mode::AcVoltage:   return tr("AC voltage");
      48         4842 :     case Mode::DcCurrent:   return tr("DC current");
      49         4842 :     case Mode::AcCurrent:   return tr("AC current");
      50          294 :     default:                return QString();
      51         7392 :     }
      52         7392 : }
      53              : 
      54              : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
      55         8446 : QString DsoService::toString(const PokitProduct product, const quint8 range, const Mode mode)
      56         5312 : {
      57        13758 :     switch (mode) {
      58          256 :     case Mode::Idle:
      59          256 :         break;
      60         5128 :     case Mode::DcVoltage:
      61         2528 :     case Mode::AcVoltage:
      62         6648 :         return VoltageRange::toString(product, range);
      63         5640 :     case Mode::DcCurrent:
      64         2528 :     case Mode::AcCurrent:
      65         6648 :         return CurrentRange::toString(product, range);
      66         5312 :     }
      67          294 :     return QString();
      68         5312 : }
      69              : 
      70              : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
      71         7931 : QString DsoService::toString(const quint8 range, const Mode mode) const
      72         4672 : {
      73        12603 :     return toString(*pokitProduct(), range, mode);
      74         4672 : }
      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         1030 : quint32 DsoService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
      80         1280 : {
      81         2310 :     switch (mode) {
      82          256 :     case Mode::Idle:
      83          256 :         break;
      84          412 :     case Mode::DcVoltage:
      85          512 :     case Mode::AcVoltage:
      86          924 :         return VoltageRange::maxValue(product, range);
      87          924 :     case Mode::DcCurrent:
      88          512 :     case Mode::AcCurrent:
      89          924 :         return CurrentRange::maxValue(product, range);
      90         1280 :     }
      91          256 :     return 0;
      92         1280 : }
      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          515 : quint32 DsoService::maxValue(const quint8 range, const Mode mode) const
      98          640 : {
      99         1155 :     return maxValue(*pokitProduct(), range, mode);
     100          640 : }
     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        13081 : DsoService::DsoService(QLowEnergyController * const controller, QObject * parent)
     118        21993 :     : AbstractPokitService(new DsoServicePrivate(controller, this), parent)
     119         8912 : {
     120              : 
     121        21993 : }
     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          103 : bool DsoService::readCharacteristics()
     136          128 : {
     137          231 :     return readMetadataCharacteristic();
     138          128 : }
     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          182 : bool DsoService::readMetadataCharacteristic()
     150          256 : {
     151          256 :     Q_D(DsoService);
     152          462 :     return d->readCharacteristic(CharacteristicUuids::metadata);
     153          256 : }
     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          515 : bool DsoService::setSettings(const Settings &settings)
     165          640 : {
     166          640 :     Q_ASSERT_X(settings.range != 255, "DsoService::setSettings", "Pokit devices do not allow AutoRange in DSO service");
     167              : 
     168          640 :     Q_D(const DsoService);
     169          640 :     const QLowEnergyCharacteristic characteristic =
     170         1155 :         d->getCharacteristic(CharacteristicUuids::settings);
     171         1155 :     if (!characteristic.isValid()) {
     172          640 :         return false;
     173          640 :     }
     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          515 : }
     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          412 : bool DsoService::startDso(const Settings &settings)
     191          512 : {
     192          512 :     Q_D(const DsoService);
     193          512 :     Q_ASSERT(settings.command != DsoService::Command::ResendData);
     194          924 :     if (settings.command == DsoService::Command::ResendData) {
     195          459 :         qCWarning(d->lc).noquote() << tr("Settings command must not be 'ResendData'.");
     196          176 :         return false;
     197          128 :     }
     198          693 :     return setSettings(settings);
     199          512 : }
     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          103 : bool DsoService::fetchSamples()
     212          128 : {
     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          231 :     return setSettings({ DsoService::Command::ResendData, 0, DsoService::Mode::Idle, 0, 0, 0 });
     217          128 : }
     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          103 : DsoService::Metadata DsoService::metadata() const
     234          128 : {
     235          128 :     Q_D(const DsoService);
     236          128 :     const QLowEnergyCharacteristic characteristic =
     237          231 :         d->getCharacteristic(CharacteristicUuids::metadata);
     238          231 :     return (characteristic.isValid()) ? DsoServicePrivate::parseMetadata(characteristic.value())
     239          334 :         : Metadata{ DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
     240          231 : }
     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          103 : bool DsoService::enableMetadataNotifications()
     252          128 : {
     253          128 :     Q_D(DsoService);
     254          231 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
     255          128 : }
     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          103 : bool DsoService::disableMetadataNotifications()
     265          128 : {
     266          128 :     Q_D(DsoService);
     267          231 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
     268          128 : }
     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          103 : bool DsoService::enableReadingNotifications()
     278          128 : {
     279          128 :     Q_D(DsoService);
     280          231 :     return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
     281          128 : }
     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          103 : bool DsoService::disableReadingNotifications()
     289          128 : {
     290          128 :     Q_D(DsoService);
     291          231 :     return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
     292          128 : }
     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        10033 : DsoServicePrivate::DsoServicePrivate(
     332        13081 :     QLowEnergyController * controller, DsoService * const q)
     333        21993 :     : AbstractPokitServicePrivate(DsoService::serviceUuid, controller, q)
     334         8912 : {
     335              : 
     336        18945 : }
     337              : 
     338              : /*!
     339              :  * Returns \a settings in the format Pokit devices expect.
     340              :  */
     341          309 : QByteArray DsoServicePrivate::encodeSettings(const DsoService::Settings &settings)
     342          384 : {
     343          384 :     static_assert(sizeof(settings.command)         == 1, "Expected to be 1 byte.");
     344          384 :     static_assert(sizeof(settings.triggerLevel)    == 4, "Expected to be 2 bytes.");
     345          384 :     static_assert(sizeof(settings.mode)            == 1, "Expected to be 1 byte.");
     346          384 :     static_assert(sizeof(settings.range)           == 1, "Expected to be 1 byte.");
     347          384 :     static_assert(sizeof(settings.samplingWindow)  == 4, "Expected to be 4 bytes.");
     348          384 :     static_assert(sizeof(settings.numberOfSamples) == 2, "Expected to be 2 bytes.");
     349              : 
     350          594 :     QByteArray value;
     351          693 :     QDataStream stream(&value, QIODevice::WriteOnly);
     352          693 :     stream.setByteOrder(QDataStream::LittleEndian);
     353          693 :     stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
     354          702 :     stream << (quint8)settings.command << settings.triggerLevel << (quint8)settings.mode
     355          702 :            << settings.range << settings.samplingWindow << settings.numberOfSamples;
     356              : 
     357          384 :     Q_ASSERT(value.size() == 13);
     358          693 :     return value;
     359          693 : }
     360              : 
     361              : /*!
     362              :  * Parses the `Metadata` \a value into a DsoService::Metatdata struct.
     363              :  */
     364          412 : DsoService::Metadata DsoServicePrivate::parseMetadata(const QByteArray &value)
     365          512 : {
     366          924 :     DsoService::Metadata metadata{
     367          512 :         DsoService::DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(),
     368          512 :         DsoService::Mode::Idle, 0, 0, 0, 0
     369          512 :     };
     370              : 
     371         1204 :     if (!checkSize(u"Metadata"_s, value, 17, 17)) {
     372          256 :         return metadata;
     373          256 :     }
     374              : 
     375          462 :     metadata.status          = static_cast<DsoService::DsoStatus>(value.at(0));
     376          602 :     metadata.scale           = qFromLittleEndian<float>(value.mid(1,4).constData());
     377          462 :     metadata.mode            = static_cast<DsoService::Mode>(value.at(5));
     378          462 :     metadata.range           = static_cast<quint8>(value.at(6));
     379          602 :     metadata.samplingWindow  = qFromLittleEndian<quint32>(value.mid(7,4).constData());
     380          602 :     metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
     381          602 :     metadata.samplingRate    = qFromLittleEndian<quint32>(value.mid(13,4).constData());
     382          462 :     return metadata;
     383          512 : }
     384              : 
     385              : /*!
     386              :  * Parses the `Reading` \a value into a DsoService::Samples vector.
     387              :  */
     388          412 : DsoService::Samples DsoServicePrivate::parseSamples(const QByteArray &value)
     389          512 : {
     390          792 :     DsoService::Samples samples;
     391          924 :     if ((value.size()%2) != 0) {
     392          413 :         qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
     393          381 :             .arg(value.size()).arg(toHexString(value));
     394          138 :         return samples;
     395          128 :     }
     396         3927 :     while ((samples.size()*2) < value.size()) {
     397         4214 :         samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
     398         1792 :     }
     399          858 :     qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
     400          414 :     return samples;
     401          512 : }
     402              : 
     403              : /*!
     404              :  * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
     405              :  * specialised signal, for each supported \a characteristic.
     406              :  */
     407          103 : void DsoServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
     408              :                                               const QByteArray &value)
     409          128 : {
     410          231 :     AbstractPokitServicePrivate::characteristicRead(characteristic, value);
     411              : 
     412          231 :     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          128 :     Q_Q(DsoService);
     419          231 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
     420            0 :         Q_EMIT q->metadataRead(parseMetadata(value));
     421            0 :         return;
     422            0 :     }
     423              : 
     424          231 :     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          546 :     qCWarning(lc).noquote() << tr("Unknown characteristic read for DSO service")
     431          367 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     432          128 : }
     433              : 
     434              : /*!
     435              :  * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
     436              :  * specialised signal, for each supported \a characteristic.
     437              :  */
     438          103 : void DsoServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
     439              :                                                      const QByteArray &newValue)
     440          128 : {
     441          231 :     AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
     442              : 
     443          128 :     Q_Q(DsoService);
     444          231 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
     445            0 :         Q_EMIT q->settingsWritten();
     446            0 :         return;
     447            0 :     }
     448              : 
     449          231 :     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          231 :     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          546 :     qCWarning(lc).noquote() << tr("Unknown characteristic written for DSO service")
     462          367 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     463          128 : }
     464              : 
     465              : /*!
     466              :  * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
     467              :  * specialised signal, for each supported \a characteristic.
     468              :  */
     469          103 : void DsoServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
     470              :                                                      const QByteArray &newValue)
     471          128 : {
     472          231 :     AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
     473              : 
     474          128 :     Q_Q(DsoService);
     475          231 :     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          231 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
     482            0 :         Q_EMIT q->metadataRead(parseMetadata(newValue));
     483            0 :         return;
     484            0 :     }
     485              : 
     486          231 :     if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
     487            0 :         Q_EMIT q->samplesRead(parseSamples(newValue));
     488            0 :         return;
     489            0 :     }
     490              : 
     491          546 :     qCWarning(lc).noquote() << tr("Unknown characteristic notified for DSO service")
     492          367 :         << serviceUuid << characteristic.name() << characteristic.uuid();
     493          128 : }
     494              : 
     495              : /// \endcond
     496              : 
     497              : QTPOKIT_END_NAMESPACE
        

Generated by: LCOV version 2.5-0