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