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