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: 88.9 % 27 24

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

Generated by: LCOV version 2.0-1