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 5535 : QString DsoService::toString(const Mode &mode)
31 2916 : {
32 8451 : switch (mode) {
33 97 : case Mode::Idle: return tr("Idle");
34 2040 : case Mode::DcVoltage: return tr("DC voltage");
35 2040 : case Mode::AcVoltage: return tr("AC voltage");
36 2040 : case Mode::DcCurrent: return tr("DC current");
37 2040 : case Mode::AcCurrent: return tr("AC current");
38 104 : default: return QString();
39 2916 : }
40 2916 : }
41 :
42 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
43 3690 : QString DsoService::toString(const PokitProduct product, const quint8 range, const Mode mode)
44 2104 : {
45 5794 : switch (mode) {
46 104 : case Mode::Idle:
47 104 : break;
48 2196 : case Mode::DcVoltage:
49 1000 : case Mode::AcVoltage:
50 2800 : return VoltageRange::toString(product, range);
51 2404 : case Mode::DcCurrent:
52 1000 : case Mode::AcCurrent:
53 2800 : return CurrentRange::toString(product, range);
54 2104 : }
55 104 : return QString();
56 2104 : }
57 :
58 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
59 3465 : QString DsoService::toString(const quint8 range, const Mode mode) const
60 1844 : {
61 5309 : return toString(*pokitProduct(), range, mode);
62 1844 : }
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 450 : quint32 DsoService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
68 520 : {
69 970 : switch (mode) {
70 104 : case Mode::Idle:
71 104 : break;
72 180 : case Mode::DcVoltage:
73 208 : case Mode::AcVoltage:
74 388 : return VoltageRange::maxValue(product, range);
75 388 : case Mode::DcCurrent:
76 208 : case Mode::AcCurrent:
77 388 : return CurrentRange::maxValue(product, range);
78 520 : }
79 104 : return 0;
80 520 : }
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 225 : quint32 DsoService::maxValue(const quint8 range, const Mode mode) const
86 260 : {
87 485 : return maxValue(*pokitProduct(), range, mode);
88 260 : }
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 5085 : DsoService::DsoService(QLowEnergyController * const controller, QObject * parent)
106 7946 : : AbstractPokitService(new DsoServicePrivate(controller, this), parent)
107 3236 : {
108 :
109 8321 : }
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 45 : bool DsoService::readCharacteristics()
124 52 : {
125 97 : return readMetadataCharacteristic();
126 52 : }
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 67 : bool DsoService::readMetadataCharacteristic()
138 104 : {
139 104 : Q_D(DsoService);
140 194 : return d->readCharacteristic(CharacteristicUuids::metadata);
141 104 : }
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 225 : bool DsoService::setSettings(const Settings &settings)
151 260 : {
152 260 : Q_D(const DsoService);
153 260 : const QLowEnergyCharacteristic characteristic =
154 485 : d->getCharacteristic(CharacteristicUuids::settings);
155 485 : if (!characteristic.isValid()) {
156 260 : return false;
157 260 : }
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 225 : }
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 180 : bool DsoService::startDso(const Settings &settings)
175 208 : {
176 208 : Q_D(const DsoService);
177 208 : Q_ASSERT(settings.command != DsoService::Command::ResendData);
178 388 : if (settings.command == DsoService::Command::ResendData) {
179 176 : qCWarning(d->lc).noquote() << tr("Settings command must not be 'ResendData'.");
180 84 : return false;
181 52 : }
182 291 : return setSettings(settings);
183 208 : }
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 45 : bool DsoService::fetchSamples()
196 52 : {
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 97 : return setSettings({ DsoService::Command::ResendData, 0, DsoService::Mode::Idle, 0, 0, 0 });
201 52 : }
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 45 : DsoService::Metadata DsoService::metadata() const
218 52 : {
219 52 : Q_D(const DsoService);
220 52 : const QLowEnergyCharacteristic characteristic =
221 97 : d->getCharacteristic(CharacteristicUuids::metadata);
222 97 : return (characteristic.isValid()) ? DsoServicePrivate::parseMetadata(characteristic.value())
223 142 : : Metadata{ DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
224 97 : }
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 45 : bool DsoService::enableMetadataNotifications()
236 52 : {
237 52 : Q_D(DsoService);
238 97 : return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
239 52 : }
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 45 : bool DsoService::disableMetadataNotifications()
249 52 : {
250 52 : Q_D(DsoService);
251 97 : return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
252 52 : }
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 45 : bool DsoService::enableReadingNotifications()
262 52 : {
263 52 : Q_D(DsoService);
264 97 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
265 52 : }
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 45 : bool DsoService::disableReadingNotifications()
273 52 : {
274 52 : Q_D(DsoService);
275 97 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
276 52 : }
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 2486 : DsoServicePrivate::DsoServicePrivate(
316 5085 : QLowEnergyController * controller, DsoService * const q)
317 7946 : : AbstractPokitServicePrivate(DsoService::serviceUuid, controller, q)
318 3236 : {
319 :
320 5722 : }
321 :
322 : /*!
323 : * Returns \a settings in the format Pokit devices expect.
324 : */
325 135 : QByteArray DsoServicePrivate::encodeSettings(const DsoService::Settings &settings)
326 156 : {
327 156 : static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
328 156 : static_assert(sizeof(settings.triggerLevel) == 4, "Expected to be 2 bytes.");
329 156 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
330 156 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
331 156 : static_assert(sizeof(settings.samplingWindow) == 4, "Expected to be 4 bytes.");
332 156 : static_assert(sizeof(settings.numberOfSamples) == 2, "Expected to be 2 bytes.");
333 :
334 219 : QByteArray value;
335 291 : QDataStream stream(&value, QIODevice::WriteOnly);
336 291 : stream.setByteOrder(QDataStream::LittleEndian);
337 291 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
338 291 : stream << (quint8)settings.command << settings.triggerLevel << (quint8)settings.mode
339 291 : << settings.range << settings.samplingWindow << settings.numberOfSamples;
340 :
341 156 : Q_ASSERT(value.size() == 13);
342 291 : return value;
343 291 : }
344 :
345 : /*!
346 : * Parses the `Metadata` \a value into a DsoService::Metatdata struct.
347 : */
348 180 : DsoService::Metadata DsoServicePrivate::parseMetadata(const QByteArray &value)
349 208 : {
350 388 : DsoService::Metadata metadata{
351 208 : DsoService::DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(),
352 208 : DsoService::Mode::Idle, 0, 0, 0, 0
353 208 : };
354 :
355 472 : if (!checkSize(u"Metadata"_s, value, 17, 17)) {
356 104 : return metadata;
357 104 : }
358 :
359 194 : metadata.status = static_cast<DsoService::DsoStatus>(value.at(0));
360 236 : metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
361 194 : metadata.mode = static_cast<DsoService::Mode>(value.at(5));
362 194 : metadata.range = static_cast<quint8>(value.at(6));
363 236 : metadata.samplingWindow = qFromLittleEndian<quint32>(value.mid(7,4).constData());
364 236 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
365 236 : metadata.samplingRate = qFromLittleEndian<quint32>(value.mid(13,4).constData());
366 194 : return metadata;
367 208 : }
368 :
369 : /*!
370 : * Parses the `Reading` \a value into a DsoService::Samples vector.
371 : */
372 180 : DsoService::Samples DsoServicePrivate::parseSamples(const QByteArray &value)
373 208 : {
374 292 : DsoService::Samples samples;
375 388 : if ((value.size()%2) != 0) {
376 179 : qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
377 148 : .arg(value.size()).arg(toHexString(value));
378 58 : return samples;
379 52 : }
380 1649 : while ((samples.size()*2) < value.size()) {
381 1652 : samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
382 728 : }
383 330 : qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
384 174 : return samples;
385 208 : }
386 :
387 : /*!
388 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
389 : * specialised signal, for each supported \a characteristic.
390 : */
391 45 : void DsoServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
392 : const QByteArray &value)
393 52 : {
394 97 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
395 :
396 97 : 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 52 : Q_Q(DsoService);
403 97 : if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
404 0 : Q_EMIT q->metadataRead(parseMetadata(value));
405 0 : return;
406 0 : }
407 :
408 97 : 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 219 : qCWarning(lc).noquote() << tr("Unknown characteristic read for DSO service")
415 163 : << serviceUuid << characteristic.name() << characteristic.uuid();
416 52 : }
417 :
418 : /*!
419 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
420 : * specialised signal, for each supported \a characteristic.
421 : */
422 45 : void DsoServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
423 : const QByteArray &newValue)
424 52 : {
425 97 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
426 :
427 52 : Q_Q(DsoService);
428 97 : if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
429 0 : Q_EMIT q->settingsWritten();
430 0 : return;
431 0 : }
432 :
433 97 : 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 97 : 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 219 : qCWarning(lc).noquote() << tr("Unknown characteristic written for DSO service")
446 163 : << serviceUuid << characteristic.name() << characteristic.uuid();
447 52 : }
448 :
449 : /*!
450 : * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
451 : * specialised signal, for each supported \a characteristic.
452 : */
453 45 : void DsoServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
454 : const QByteArray &newValue)
455 52 : {
456 97 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
457 :
458 52 : Q_Q(DsoService);
459 97 : 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 97 : if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
466 0 : Q_EMIT q->metadataRead(parseMetadata(newValue));
467 0 : return;
468 0 : }
469 :
470 97 : if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
471 0 : Q_EMIT q->samplesRead(parseSamples(newValue));
472 0 : return;
473 0 : }
474 :
475 219 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for DSO service")
476 163 : << serviceUuid << characteristic.name() << characteristic.uuid();
477 52 : }
478 :
479 : /// \endcond
480 :
481 : QTPOKIT_END_NAMESPACE
|