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