Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2026 Paul Colby <git@colby.id.au>
2 : // SPDX-License-Identifier: LGPL-3.0-or-later
3 :
4 : /*!
5 : * \file
6 : * Defines the MultimeterService and MultimeterServicePrivate classes.
7 : */
8 :
9 : #include <qtpokit/multimeterservice.h>
10 : #include "multimeterservice_p.h"
11 : #include "pokitproducts_p.h"
12 : #include "../stringliterals_p.h"
13 :
14 : #include <QDataStream>
15 : #include <QIODevice>
16 : #include <QtEndian>
17 :
18 : QTPOKIT_BEGIN_NAMESPACE
19 : DOKIT_USE_STRINGLITERALS
20 :
21 : /*!
22 : * \class MultimeterService
23 : *
24 : * The MultimeterService class accesses the `Multimeter` service of Pokit devices.
25 : */
26 :
27 : /*!
28 : * \cond internal
29 : * \enum MultimeterService::Mode
30 : * \pokitApi The following enumeration values are as-yet undocumented by Pokit Innovations.
31 : * [\@pcolby](https://github.com/pcolby) reverse-engineered them as part of the
32 : * [dokit](https://github.com/pcolby/dokit) project.
33 : * * Mode::Capacitance
34 : * * Mode::ExternalTemperature
35 : * \endcond
36 : */
37 :
38 : /// Returns \a mode as a user-friendly string.
39 10300 : QString MultimeterService::toString(const Mode &mode)
40 6536 : {
41 16836 : switch (mode) {
42 2934 : case Mode::Idle: return tr("Idle");
43 1344 : case Mode::DcVoltage: return tr("DC voltage");
44 1344 : case Mode::AcVoltage: return tr("AC voltage");
45 1344 : case Mode::DcCurrent: return tr("DC current");
46 1344 : case Mode::AcCurrent: return tr("AC current");
47 1344 : case Mode::Resistance: return tr("Resistance");
48 1344 : case Mode::Diode: return tr("Diode");
49 2457 : case Mode::Continuity: return tr("Continuity");
50 1344 : case Mode::Temperature: return tr("Temperature");
51 1344 : case Mode::Capacitance: return tr("Capacitance");
52 231 : case Mode::ExternalTemperature: return tr("External temperature");
53 6536 : }
54 294 : return QString();
55 6536 : }
56 :
57 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
58 9785 : QString MultimeterService::toString(const PokitProduct product, const quint8 range, const Mode mode)
59 6760 : {
60 16545 : switch (mode) {
61 1096 : case Mode::Idle:
62 1096 : break;
63 1984 : case Mode::DcVoltage:
64 1184 : case Mode::AcVoltage:
65 2832 : return VoltageRange::toString(product, range);
66 2496 : case Mode::DcCurrent:
67 1184 : case Mode::AcCurrent:
68 2832 : return CurrentRange::toString(product, range);
69 1878 : case Mode::Resistance:
70 1878 : return ResistanceRange::toString(product, range);
71 336 : case Mode::Diode:
72 1008 : case Mode::Continuity:
73 1600 : case Mode::Temperature:
74 1600 : break;
75 1416 : case Mode::Capacitance:
76 1416 : return CapacitanceRange::toString(product, range);
77 256 : case Mode::ExternalTemperature:
78 256 : break;
79 6760 : }
80 3807 : return QString();
81 6760 : }
82 :
83 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
84 8755 : QString MultimeterService::toString(const quint8 range, const Mode mode) const
85 5480 : {
86 14235 : return toString(*pokitProduct(), range, mode);
87 5480 : }
88 :
89 : /*!
90 : * Returns the maximum value for \a range, or 0 if \a range is not a known value for \a product's \a mode.
91 : */
92 2060 : quint32 MultimeterService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
93 2560 : {
94 4620 : switch (mode) {
95 256 : case Mode::Idle:
96 256 : break;
97 412 : case Mode::DcVoltage:
98 512 : case Mode::AcVoltage:
99 924 : return VoltageRange::maxValue(product, range);
100 924 : case Mode::DcCurrent:
101 512 : case Mode::AcCurrent:
102 924 : return CurrentRange::maxValue(product, range);
103 924 : case Mode::Resistance:
104 924 : return ResistanceRange::maxValue(product, range);
105 0 : case Mode::Diode:
106 0 : case Mode::Continuity:
107 256 : case Mode::Temperature:
108 256 : break;
109 462 : case Mode::Capacitance:
110 462 : return CapacitanceRange::maxValue(product, range);
111 256 : case Mode::ExternalTemperature:
112 256 : break;
113 2560 : }
114 768 : return 0;
115 2560 : }
116 :
117 : /*!
118 : * Returns the maximum value for \a range, or 0 \a range is not a known value for the current \a product's \a mode.
119 : */
120 1030 : quint32 MultimeterService::maxValue(const quint8 range, const Mode mode) const
121 1280 : {
122 2310 : return maxValue(*pokitProduct(), range, mode);
123 1280 : }
124 :
125 : /*!
126 : * Constructs a new Pokit service with \a parent.
127 : */
128 7519 : MultimeterService::MultimeterService(QLowEnergyController * const controller, QObject * parent)
129 13839 : : AbstractPokitService(new MultimeterServicePrivate(controller, this), parent)
130 6320 : {
131 :
132 13839 : }
133 :
134 : /*!
135 : * \cond internal
136 : * Constructs a new Pokit service with \a parent, and private implementation \a d.
137 : */
138 0 : MultimeterService::MultimeterService(
139 0 : MultimeterServicePrivate * const d, QObject * const parent)
140 0 : : AbstractPokitService(d, parent)
141 0 : {
142 :
143 0 : }
144 : /// \endcond
145 :
146 103 : bool MultimeterService::readCharacteristics()
147 128 : {
148 231 : return readReadingCharacteristic();
149 128 : }
150 :
151 : /*!
152 : * Read the `Multimeter` service's `Reading` characteristic.
153 : *
154 : * Returns `true` is the read request is successfully queued, `false` otherwise (ie if the
155 : * underlying controller it not yet connected to the Pokit device, or the device's services have
156 : * not yet been discovered).
157 : *
158 : * Emits readingRead() if/when the characteristic has been read successfully.
159 : */
160 182 : bool MultimeterService::readReadingCharacteristic()
161 256 : {
162 256 : Q_D(MultimeterService);
163 462 : return d->readCharacteristic(CharacteristicUuids::reading);
164 256 : }
165 :
166 : /*!
167 : * Configures the Pokit device's multimeter mode.
168 : *
169 : * Returns `true` if the write request was successfully queued, `false` otherwise.
170 : *
171 : * Emits settingsWritten() if/when the \a settings have been written successfully.
172 : */
173 103 : bool MultimeterService::setSettings(const Settings &settings)
174 128 : {
175 128 : Q_D(const MultimeterService);
176 128 : const QLowEnergyCharacteristic characteristic =
177 231 : d->getCharacteristic(CharacteristicUuids::settings);
178 231 : if (!characteristic.isValid()) {
179 128 : return false;
180 128 : }
181 :
182 0 : const QByteArray value = MultimeterServicePrivate::encodeSettings(settings);
183 0 : if (value.isNull()) {
184 0 : return false;
185 0 : }
186 :
187 0 : d->service->writeCharacteristic(characteristic, value);
188 0 : return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
189 103 : }
190 :
191 : /*!
192 : * Returns the most recent value of the `Multimeter` service's `Reading` characteristic.
193 : *
194 : * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is
195 : * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then the
196 : * returned MultimeterService::Reading::value member will be a quiet NaN, which can be checked like:
197 : *
198 : * ```
199 : * const MultimeterService::Reading reading = multimeterService->reading();
200 : * if (qIsNaN(reading.value)) {
201 : * // Handle failure.
202 : * }
203 : * ```
204 : */
205 103 : MultimeterService::Reading MultimeterService::reading() const
206 128 : {
207 128 : Q_D(const MultimeterService);
208 128 : const QLowEnergyCharacteristic characteristic =
209 231 : d->getCharacteristic(CharacteristicUuids::reading);
210 231 : return (characteristic.isValid()) ? MultimeterServicePrivate::parseReading(characteristic.value())
211 334 : : Reading{ MeterStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0 };
212 231 : }
213 :
214 : /*!
215 : * Enables client-side notifications of meter readings.
216 : *
217 : * This is an alternative to manually requesting individual reads via readReadingCharacteristic().
218 : *
219 : * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
220 : *
221 : * Successfully read values (if any) will be emitted via the readingRead() signal.
222 : */
223 103 : bool MultimeterService::enableReadingNotifications()
224 128 : {
225 128 : Q_D(MultimeterService);
226 231 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
227 128 : }
228 :
229 : /*!
230 : * Disables client-side notifications of meter readings.
231 : *
232 : * Instantaneous reads can still be fetched by readReadingCharacteristic().
233 : *
234 : * Returns `true` is the request was successfully submitted to the device queue, `false` otherwise.
235 : */
236 103 : bool MultimeterService::disableReadingNotifications()
237 128 : {
238 128 : Q_D(MultimeterService);
239 231 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
240 128 : }
241 :
242 : /*!
243 : * \fn MultimeterService::readingRead
244 : *
245 : * This signal is emitted when the `Reading` characteristic has been read successfully.
246 : *
247 : * \see readReadingCharacteristic
248 : */
249 :
250 : /*!
251 : * \fn MultimeterService::settingsWritten
252 : *
253 : * This signal is emitted when the `Settings` characteristic has been written successfully.
254 : *
255 : * \see setSettings
256 : */
257 :
258 : /*!
259 : * \cond internal
260 : * \class MultimeterServicePrivate
261 : *
262 : * The MultimeterServicePrivate class provides private implementation for MultimeterService.
263 : */
264 :
265 : /*!
266 : * \internal
267 : * Constructs a new MultimeterServicePrivate object with public implementation \a q.
268 : */
269 5767 : MultimeterServicePrivate::MultimeterServicePrivate(
270 7519 : QLowEnergyController * controller, MultimeterService * const q)
271 13839 : : AbstractPokitServicePrivate(MultimeterService::serviceUuid, controller, q)
272 6320 : {
273 :
274 12087 : }
275 :
276 : /*!
277 : * Returns \a settings in the format Pokit devices expect.
278 : */
279 412 : QByteArray MultimeterServicePrivate::encodeSettings(const MultimeterService::Settings &settings)
280 512 : {
281 512 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
282 512 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
283 512 : static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
284 :
285 792 : QByteArray value;
286 924 : QDataStream stream(&value, QIODevice::WriteOnly);
287 924 : stream.setByteOrder(QDataStream::LittleEndian);
288 924 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
289 924 : stream << (quint8)settings.mode << settings.range << settings.updateInterval;
290 :
291 512 : Q_ASSERT(value.size() == 6);
292 924 : return value;
293 924 : }
294 :
295 : /*!
296 : * Parses the `Reading` \a value into a MultimeterService::Reading struct.
297 : */
298 515 : MultimeterService::Reading MultimeterServicePrivate::parseReading(const QByteArray &value)
299 640 : {
300 760 : MultimeterService::Reading reading{
301 640 : MultimeterService::MeterStatus::Error,
302 640 : std::numeric_limits<float>::quiet_NaN(),
303 640 : MultimeterService::Mode::Idle, 0
304 640 : };
305 :
306 1505 : if (!checkSize(u"Reading"_s, value, 7, 7)) {
307 414 : return reading;
308 256 : }
309 :
310 636 : reading.status = MultimeterService::MeterStatus(value.at(0));
311 903 : reading.value = qFromLittleEndian<float>(value.mid(1,4).constData());
312 636 : reading.mode = static_cast<MultimeterService::Mode>(value.at(5));
313 636 : reading.range = static_cast<quint8>(value.at(6));
314 693 : return reading;
315 640 : }
316 :
317 : /*!
318 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
319 : * specialised signal, for each supported \a characteristic.
320 : */
321 103 : void MultimeterServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
322 : const QByteArray &value)
323 128 : {
324 231 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
325 :
326 128 : Q_Q(MultimeterService);
327 231 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
328 0 : Q_EMIT q->readingRead(parseReading(value));
329 0 : return;
330 0 : }
331 :
332 231 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
333 0 : qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
334 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
335 0 : return;
336 0 : }
337 :
338 546 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Multimeter service")
339 367 : << serviceUuid << characteristic.name() << characteristic.uuid();
340 128 : }
341 :
342 : /*!
343 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
344 : * specialised signal, for each supported \a characteristic.
345 : */
346 103 : void MultimeterServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
347 : const QByteArray &newValue)
348 128 : {
349 231 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
350 :
351 128 : Q_Q(MultimeterService);
352 231 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
353 0 : Q_EMIT q->settingsWritten();
354 0 : return;
355 0 : }
356 :
357 231 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
358 0 : qCWarning(lc).noquote() << tr("Reading characteristic is read/notify, but somehow written")
359 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
360 0 : return;
361 0 : }
362 :
363 546 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Multimeter service")
364 367 : << serviceUuid << characteristic.name() << characteristic.uuid();
365 128 : }
366 :
367 : /*!
368 : * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
369 : * specialised signal, for each supported \a characteristic.
370 : */
371 103 : void MultimeterServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
372 : const QByteArray &newValue)
373 128 : {
374 231 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
375 :
376 128 : Q_Q(MultimeterService);
377 231 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
378 0 : qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
379 0 : << serviceUuid << characteristic.name() << characteristic.uuid();
380 0 : return;
381 0 : }
382 :
383 231 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
384 0 : Q_EMIT q->readingRead(parseReading(newValue));
385 0 : return;
386 0 : }
387 :
388 546 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for Multimeter service")
389 367 : << serviceUuid << characteristic.name() << characteristic.uuid();
390 128 : }
391 :
392 : /// \endcond
393 :
394 : QTPOKIT_END_NAMESPACE
|