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