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 420 : QString DsoService::toString(const Command &command)
31 666 : {
32 1086 : switch (command) {
33 181 : case Command::FreeRunning: return tr("Free running");
34 181 : case Command::RisingEdgeTrigger: return tr("Rising edge trigger");
35 181 : case Command::FallingEdgeTrigger: return tr("Falling edge trigger");
36 181 : case Command::ResendData: return tr("Resend data");
37 222 : default: return QString();
38 666 : }
39 666 : }
40 :
41 : /// Returns \a mode as a user-friendly string.
42 8610 : QString DsoService::toString(const Mode &mode)
43 3793 : {
44 12403 : switch (mode) {
45 181 : case Mode::Idle: return tr("Idle");
46 2965 : case Mode::DcVoltage: return tr("DC voltage");
47 2965 : case Mode::AcVoltage: return tr("AC voltage");
48 2965 : case Mode::DcCurrent: return tr("DC current");
49 2965 : case Mode::AcCurrent: return tr("AC current");
50 222 : default: return QString();
51 3793 : }
52 3793 : }
53 :
54 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
55 5740 : QString DsoService::toString(const PokitProduct product, const quint8 range, const Mode mode)
56 2982 : {
57 8722 : switch (mode) {
58 222 : case Mode::Idle:
59 222 : break;
60 3268 : case Mode::DcVoltage:
61 1380 : case Mode::AcVoltage:
62 4180 : return VoltageRange::toString(product, range);
63 3712 : case Mode::DcCurrent:
64 1380 : case Mode::AcCurrent:
65 4180 : return CurrentRange::toString(product, range);
66 2982 : }
67 222 : return QString();
68 2982 : }
69 :
70 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
71 5390 : QString DsoService::toString(const quint8 range, const Mode mode) const
72 2427 : {
73 7817 : return toString(*pokitProduct(), range, mode);
74 2427 : }
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 700 : quint32 DsoService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
80 1110 : {
81 1810 : switch (mode) {
82 222 : case Mode::Idle:
83 222 : break;
84 280 : case Mode::DcVoltage:
85 444 : case Mode::AcVoltage:
86 724 : return VoltageRange::maxValue(product, range);
87 724 : case Mode::DcCurrent:
88 444 : case Mode::AcCurrent:
89 724 : return CurrentRange::maxValue(product, range);
90 1110 : }
91 222 : return 0;
92 1110 : }
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 350 : quint32 DsoService::maxValue(const quint8 range, const Mode mode) const
98 555 : {
99 905 : return maxValue(*pokitProduct(), range, mode);
100 555 : }
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 8890 : DsoService::DsoService(QLowEnergyController * const controller, QObject * parent)
118 13892 : : AbstractPokitService(new DsoServicePrivate(controller, this), parent)
119 5427 : {
120 :
121 14317 : }
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 70 : bool DsoService::readCharacteristics()
136 111 : {
137 181 : return readMetadataCharacteristic();
138 111 : }
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 118 : bool DsoService::readMetadataCharacteristic()
150 222 : {
151 222 : Q_D(DsoService);
152 362 : return d->readCharacteristic(CharacteristicUuids::metadata);
153 222 : }
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 350 : bool DsoService::setSettings(const Settings &settings)
165 555 : {
166 555 : Q_ASSERT_X(settings.range != 255, "DsoService::setSettings", "Pokit devices do not allow AutoRange in DSO service");
167 :
168 555 : Q_D(const DsoService);
169 555 : const QLowEnergyCharacteristic characteristic =
170 905 : d->getCharacteristic(CharacteristicUuids::settings);
171 905 : if (!characteristic.isValid()) {
172 555 : return false;
173 555 : }
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 350 : }
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 280 : bool DsoService::startDso(const Settings &settings)
191 444 : {
192 444 : Q_D(const DsoService);
193 444 : Q_ASSERT(settings.command != DsoService::Command::ResendData);
194 724 : if (settings.command == DsoService::Command::ResendData) {
195 331 : qCWarning(d->lc).noquote() << tr("Settings command must not be 'ResendData'.");
196 147 : return false;
197 111 : }
198 543 : return setSettings(settings);
199 444 : }
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 70 : bool DsoService::fetchSamples()
212 111 : {
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 181 : return setSettings({ DsoService::Command::ResendData, 0, DsoService::Mode::Idle, 0, 0, 0 });
217 111 : }
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 70 : DsoService::Metadata DsoService::metadata() const
234 111 : {
235 111 : Q_D(const DsoService);
236 111 : const QLowEnergyCharacteristic characteristic =
237 181 : d->getCharacteristic(CharacteristicUuids::metadata);
238 181 : return (characteristic.isValid()) ? DsoServicePrivate::parseMetadata(characteristic.value())
239 251 : : Metadata{ DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
240 181 : }
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 70 : bool DsoService::enableMetadataNotifications()
252 111 : {
253 111 : Q_D(DsoService);
254 181 : return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
255 111 : }
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 70 : bool DsoService::disableMetadataNotifications()
265 111 : {
266 111 : Q_D(DsoService);
267 181 : return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
268 111 : }
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 70 : bool DsoService::enableReadingNotifications()
278 111 : {
279 111 : Q_D(DsoService);
280 181 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
281 111 : }
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 70 : bool DsoService::disableReadingNotifications()
289 111 : {
290 111 : Q_D(DsoService);
291 181 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
292 111 : }
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 6096 : DsoServicePrivate::DsoServicePrivate(
332 8890 : QLowEnergyController * controller, DsoService * const q)
333 13892 : : AbstractPokitServicePrivate(DsoService::serviceUuid, controller, q)
334 5427 : {
335 :
336 11523 : }
337 :
338 : /*!
339 : * Returns \a settings in the format Pokit devices expect.
340 : */
341 210 : QByteArray DsoServicePrivate::encodeSettings(const DsoService::Settings &settings)
342 333 : {
343 333 : static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
344 333 : static_assert(sizeof(settings.triggerLevel) == 4, "Expected to be 2 bytes.");
345 333 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
346 333 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
347 333 : static_assert(sizeof(settings.samplingWindow) == 4, "Expected to be 4 bytes.");
348 333 : static_assert(sizeof(settings.numberOfSamples) == 2, "Expected to be 2 bytes.");
349 :
350 471 : QByteArray value;
351 543 : QDataStream stream(&value, QIODevice::WriteOnly);
352 543 : stream.setByteOrder(QDataStream::LittleEndian);
353 543 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
354 549 : stream << (quint8)settings.command << settings.triggerLevel << (quint8)settings.mode
355 549 : << settings.range << settings.samplingWindow << settings.numberOfSamples;
356 :
357 333 : Q_ASSERT(value.size() == 13);
358 543 : return value;
359 543 : }
360 :
361 : /*!
362 : * Parses the `Metadata` \a value into a DsoService::Metatdata struct.
363 : */
364 280 : DsoService::Metadata DsoServicePrivate::parseMetadata(const QByteArray &value)
365 444 : {
366 724 : DsoService::Metadata metadata{
367 444 : DsoService::DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(),
368 444 : DsoService::Mode::Idle, 0, 0, 0, 0
369 444 : };
370 :
371 908 : if (!checkSize(u"Metadata"_s, value, 17, 17)) {
372 222 : return metadata;
373 222 : }
374 :
375 362 : metadata.status = static_cast<DsoService::DsoStatus>(value.at(0));
376 454 : metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
377 362 : metadata.mode = static_cast<DsoService::Mode>(value.at(5));
378 362 : metadata.range = static_cast<quint8>(value.at(6));
379 454 : metadata.samplingWindow = qFromLittleEndian<quint32>(value.mid(7,4).constData());
380 454 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
381 454 : metadata.samplingRate = qFromLittleEndian<quint32>(value.mid(13,4).constData());
382 362 : return metadata;
383 444 : }
384 :
385 : /*!
386 : * Parses the `Reading` \a value into a DsoService::Samples vector.
387 : */
388 280 : DsoService::Samples DsoServicePrivate::parseSamples(const QByteArray &value)
389 444 : {
390 628 : DsoService::Samples samples;
391 724 : if ((value.size()%2) != 0) {
392 300 : qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
393 269 : .arg(value.size()).arg(toHexString(value));
394 119 : return samples;
395 111 : }
396 3077 : while ((samples.size()*2) < value.size()) {
397 3178 : samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
398 1554 : }
399 645 : qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
400 357 : return samples;
401 444 : }
402 :
403 : /*!
404 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
405 : * specialised signal, for each supported \a characteristic.
406 : */
407 70 : void DsoServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
408 : const QByteArray &value)
409 111 : {
410 181 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
411 :
412 181 : 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 111 : Q_Q(DsoService);
419 181 : if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
420 0 : Q_EMIT q->metadataRead(parseMetadata(value));
421 0 : return;
422 0 : }
423 :
424 181 : 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 387 : qCWarning(lc).noquote() << tr("Unknown characteristic read for DSO service")
431 262 : << serviceUuid << characteristic.name() << characteristic.uuid();
432 111 : }
433 :
434 : /*!
435 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
436 : * specialised signal, for each supported \a characteristic.
437 : */
438 70 : void DsoServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
439 : const QByteArray &newValue)
440 111 : {
441 181 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
442 :
443 111 : Q_Q(DsoService);
444 181 : if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
445 0 : Q_EMIT q->settingsWritten();
446 0 : return;
447 0 : }
448 :
449 181 : 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 181 : 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 387 : qCWarning(lc).noquote() << tr("Unknown characteristic written for DSO service")
462 262 : << serviceUuid << characteristic.name() << characteristic.uuid();
463 111 : }
464 :
465 : /*!
466 : * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
467 : * specialised signal, for each supported \a characteristic.
468 : */
469 70 : void DsoServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
470 : const QByteArray &newValue)
471 111 : {
472 181 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
473 :
474 111 : Q_Q(DsoService);
475 181 : 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 181 : if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
482 0 : Q_EMIT q->metadataRead(parseMetadata(newValue));
483 0 : return;
484 0 : }
485 :
486 181 : if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
487 0 : Q_EMIT q->samplesRead(parseSamples(newValue));
488 0 : return;
489 0 : }
490 :
491 387 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for DSO service")
492 262 : << serviceUuid << characteristic.name() << characteristic.uuid();
493 111 : }
494 :
495 : /// \endcond
496 :
497 : QTPOKIT_END_NAMESPACE
|