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