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 4920 : QString DsoService::toString(const Mode &mode)
27 2301 : {
28 7221 : switch (mode) {
29 87 : case Mode::Idle: return tr("Idle");
30 1740 : case Mode::DcVoltage: return tr("DC voltage");
31 1740 : case Mode::AcVoltage: return tr("AC voltage");
32 1740 : case Mode::DcCurrent: return tr("DC current");
33 1740 : case Mode::AcCurrent: return tr("AC current");
34 94 : default: return QString();
35 2301 : }
36 2301 : }
37 :
38 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
39 3280 : QString DsoService::toString(const PokitProduct product, const quint8 range, const Mode mode)
40 1694 : {
41 4974 : switch (mode) {
42 94 : case Mode::Idle:
43 94 : break;
44 1906 : case Mode::DcVoltage:
45 800 : case Mode::AcVoltage:
46 2400 : return VoltageRange::toString(product, range);
47 2094 : case Mode::DcCurrent:
48 800 : case Mode::AcCurrent:
49 2400 : return CurrentRange::toString(product, range);
50 1694 : }
51 94 : return QString();
52 1694 : }
53 :
54 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
55 3080 : QString DsoService::toString(const quint8 range, const Mode mode) const
56 1459 : {
57 4539 : return toString(*pokitProduct(), range, mode);
58 1459 : }
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 400 : quint32 DsoService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
64 470 : {
65 870 : switch (mode) {
66 94 : case Mode::Idle:
67 94 : break;
68 160 : case Mode::DcVoltage:
69 188 : case Mode::AcVoltage:
70 348 : return VoltageRange::maxValue(product, range);
71 348 : case Mode::DcCurrent:
72 188 : case Mode::AcCurrent:
73 348 : return CurrentRange::maxValue(product, range);
74 470 : }
75 94 : return 0;
76 470 : }
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 200 : quint32 DsoService::maxValue(const quint8 range, const Mode mode) const
82 235 : {
83 435 : return maxValue(*pokitProduct(), range, mode);
84 235 : }
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 4520 : DsoService::DsoService(QLowEnergyController * const controller, QObject * parent)
102 6441 : : AbstractPokitService(new DsoServicePrivate(controller, this), parent)
103 2671 : {
104 :
105 7191 : }
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 40 : bool DsoService::readCharacteristics()
120 47 : {
121 87 : return readMetadataCharacteristic();
122 47 : }
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 57 : bool DsoService::readMetadataCharacteristic()
134 94 : {
135 94 : Q_D(DsoService);
136 174 : return d->readCharacteristic(CharacteristicUuids::metadata);
137 94 : }
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 200 : bool DsoService::setSettings(const Settings &settings)
147 235 : {
148 235 : Q_D(const DsoService);
149 235 : const QLowEnergyCharacteristic characteristic =
150 435 : d->getCharacteristic(CharacteristicUuids::settings);
151 435 : if (!characteristic.isValid()) {
152 235 : return false;
153 235 : }
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 200 : }
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 160 : bool DsoService::startDso(const Settings &settings)
171 188 : {
172 188 : Q_D(const DsoService);
173 188 : Q_ASSERT(settings.command != DsoService::Command::ResendData);
174 348 : if (settings.command == DsoService::Command::ResendData) {
175 154 : qCWarning(d->lc).noquote() << tr("Settings command must not be 'ResendData'.");
176 77 : return false;
177 47 : }
178 261 : return setSettings(settings);
179 188 : }
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 40 : bool DsoService::fetchSamples()
192 47 : {
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 87 : return setSettings({ DsoService::Command::ResendData, 0, DsoService::Mode::Idle, 0, 0, 0 });
197 47 : }
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 40 : DsoService::Metadata DsoService::metadata() const
214 47 : {
215 47 : Q_D(const DsoService);
216 47 : const QLowEnergyCharacteristic characteristic =
217 87 : d->getCharacteristic(CharacteristicUuids::metadata);
218 87 : return (characteristic.isValid()) ? DsoServicePrivate::parseMetadata(characteristic.value())
219 127 : : Metadata{ DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0, 0, 0, 0 };
220 87 : }
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 40 : bool DsoService::enableMetadataNotifications()
232 47 : {
233 47 : Q_D(DsoService);
234 87 : return d->enableCharacteristicNotificatons(CharacteristicUuids::metadata);
235 47 : }
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 40 : bool DsoService::disableMetadataNotifications()
245 47 : {
246 47 : Q_D(DsoService);
247 87 : return d->disableCharacteristicNotificatons(CharacteristicUuids::metadata);
248 47 : }
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 40 : bool DsoService::enableReadingNotifications()
258 47 : {
259 47 : Q_D(DsoService);
260 87 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
261 47 : }
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 40 : bool DsoService::disableReadingNotifications()
269 47 : {
270 47 : Q_D(DsoService);
271 87 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
272 47 : }
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 1921 : DsoServicePrivate::DsoServicePrivate(
312 4520 : QLowEnergyController * controller, DsoService * const q)
313 6441 : : AbstractPokitServicePrivate(DsoService::serviceUuid, controller, q)
314 2671 : {
315 :
316 4592 : }
317 :
318 : /*!
319 : * Returns \a settings in the format Pokit devices expect.
320 : */
321 120 : QByteArray DsoServicePrivate::encodeSettings(const DsoService::Settings &settings)
322 141 : {
323 141 : static_assert(sizeof(settings.command) == 1, "Expected to be 1 byte.");
324 141 : static_assert(sizeof(settings.triggerLevel) == 4, "Expected to be 2 bytes.");
325 141 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
326 141 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
327 141 : static_assert(sizeof(settings.samplingWindow) == 4, "Expected to be 4 bytes.");
328 141 : static_assert(sizeof(settings.numberOfSamples) == 2, "Expected to be 2 bytes.");
329 :
330 192 : QByteArray value;
331 261 : QDataStream stream(&value, QIODevice::WriteOnly);
332 261 : stream.setByteOrder(QDataStream::LittleEndian);
333 261 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
334 261 : stream << (quint8)settings.command << settings.triggerLevel << (quint8)settings.mode
335 261 : << settings.range << settings.samplingWindow << settings.numberOfSamples;
336 :
337 141 : Q_ASSERT(value.size() == 13);
338 261 : return value;
339 261 : }
340 :
341 : /*!
342 : * Parses the `Metadata` \a value into a DsoService::Metatdata struct.
343 : */
344 160 : DsoService::Metadata DsoServicePrivate::parseMetadata(const QByteArray &value)
345 188 : {
346 348 : DsoService::Metadata metadata{
347 188 : DsoService::DsoStatus::Error, std::numeric_limits<float>::quiet_NaN(),
348 188 : DsoService::Mode::Idle, 0, 0, 0, 0
349 188 : };
350 :
351 416 : if (!checkSize(QLatin1String("Metadata"), value, 17, 17)) {
352 94 : return metadata;
353 94 : }
354 :
355 174 : metadata.status = static_cast<DsoService::DsoStatus>(value.at(0));
356 208 : metadata.scale = qFromLittleEndian<float>(value.mid(1,4).constData());
357 174 : metadata.mode = static_cast<DsoService::Mode>(value.at(5));
358 174 : metadata.range = static_cast<quint8>(value.at(6));
359 208 : metadata.samplingWindow = qFromLittleEndian<quint32>(value.mid(7,4).constData());
360 208 : metadata.numberOfSamples = qFromLittleEndian<quint16>(value.mid(11,2).constData());
361 208 : metadata.samplingRate = qFromLittleEndian<quint32>(value.mid(13,4).constData());
362 174 : return metadata;
363 188 : }
364 :
365 : /*!
366 : * Parses the `Reading` \a value into a DsoService::Samples vector.
367 : */
368 160 : DsoService::Samples DsoServicePrivate::parseSamples(const QByteArray &value)
369 188 : {
370 256 : DsoService::Samples samples;
371 348 : if ((value.size()%2) != 0) {
372 160 : qCWarning(lc).noquote() << tr("Samples value has odd size %1 (should be even): %2")
373 131 : .arg(value.size()).arg(toHexString(value));
374 52 : return samples;
375 47 : }
376 1479 : while ((samples.size()*2) < value.size()) {
377 1456 : samples.append(qFromLittleEndian<qint16>(value.mid(samples.size()*2,2).constData()));
378 658 : }
379 291 : qCDebug(lc).noquote() << tr("Read %n sample/s from %1-bytes.", nullptr, samples.size()).arg(value.size());
380 156 : return samples;
381 188 : }
382 :
383 : /*!
384 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
385 : * specialised signal, for each supported \a characteristic.
386 : */
387 40 : void DsoServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
388 : const QByteArray &value)
389 47 : {
390 87 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
391 :
392 87 : 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 47 : Q_Q(DsoService);
399 87 : if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
400 0 : Q_EMIT q->metadataRead(parseMetadata(value));
401 0 : return;
402 0 : }
403 :
404 87 : 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 192 : qCWarning(lc).noquote() << tr("Unknown characteristic read for DSO service")
411 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
412 47 : }
413 :
414 : /*!
415 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
416 : * specialised signal, for each supported \a characteristic.
417 : */
418 40 : void DsoServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
419 : const QByteArray &newValue)
420 47 : {
421 87 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
422 :
423 47 : Q_Q(DsoService);
424 87 : if (characteristic.uuid() == DsoService::CharacteristicUuids::settings) {
425 0 : Q_EMIT q->settingsWritten();
426 0 : return;
427 0 : }
428 :
429 87 : 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 87 : 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 192 : qCWarning(lc).noquote() << tr("Unknown characteristic written for DSO service")
442 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
443 47 : }
444 :
445 : /*!
446 : * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
447 : * specialised signal, for each supported \a characteristic.
448 : */
449 40 : void DsoServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
450 : const QByteArray &newValue)
451 47 : {
452 87 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
453 :
454 47 : Q_Q(DsoService);
455 87 : 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 87 : if (characteristic.uuid() == DsoService::CharacteristicUuids::metadata) {
462 0 : Q_EMIT q->metadataRead(parseMetadata(newValue));
463 0 : return;
464 0 : }
465 :
466 87 : if (characteristic.uuid() == DsoService::CharacteristicUuids::reading) {
467 0 : Q_EMIT q->samplesRead(parseSamples(newValue));
468 0 : return;
469 0 : }
470 :
471 192 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for DSO service")
472 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
473 47 : }
474 :
475 : /// \endcond
|