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

Generated by: LCOV version 2.3-1