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