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 7000 : QString MultimeterService::toString(const Mode &mode)
40 3705 : {
41 10705 : switch (mode) {
42 1813 : case Mode::Idle: return tr("Idle");
43 853 : case Mode::DcVoltage: return tr("DC voltage");
44 853 : case Mode::AcVoltage: return tr("AC voltage");
45 853 : case Mode::DcCurrent: return tr("DC current");
46 853 : case Mode::AcCurrent: return tr("AC current");
47 853 : case Mode::Resistance: return tr("Resistance");
48 853 : case Mode::Diode: return tr("Diode");
49 1525 : case Mode::Continuity: return tr("Continuity");
50 853 : case Mode::Temperature: return tr("Temperature");
51 853 : case Mode::Capacitance: return tr("Capacitance");
52 181 : case Mode::ExternalTemperature: return tr("External temperature");
53 3705 : }
54 222 : return QString();
55 3705 : }
56 :
57 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
58 6650 : QString MultimeterService::toString(const PokitProduct product, const quint8 range, const Mode mode)
59 4170 : {
60 10820 : switch (mode) {
61 612 : case Mode::Idle:
62 612 : break;
63 1276 : case Mode::DcVoltage:
64 756 : case Mode::AcVoltage:
65 1876 : return VoltageRange::toString(product, range);
66 1720 : case Mode::DcCurrent:
67 756 : case Mode::AcCurrent:
68 1876 : return CurrentRange::toString(product, range);
69 1300 : case Mode::Resistance:
70 1300 : return ResistanceRange::toString(product, range);
71 156 : case Mode::Diode:
72 468 : case Mode::Continuity:
73 846 : case Mode::Temperature:
74 846 : break;
75 938 : case Mode::Capacitance:
76 938 : return CapacitanceRange::toString(product, range);
77 222 : case Mode::ExternalTemperature:
78 222 : break;
79 4170 : }
80 1680 : return QString();
81 4170 : }
82 :
83 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
84 5950 : QString MultimeterService::toString(const quint8 range, const Mode mode) const
85 3060 : {
86 9010 : return toString(*pokitProduct(), range, mode);
87 3060 : }
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 1400 : quint32 MultimeterService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
93 2220 : {
94 3620 : switch (mode) {
95 222 : case Mode::Idle:
96 222 : break;
97 280 : case Mode::DcVoltage:
98 444 : case Mode::AcVoltage:
99 724 : return VoltageRange::maxValue(product, range);
100 724 : case Mode::DcCurrent:
101 444 : case Mode::AcCurrent:
102 724 : return CurrentRange::maxValue(product, range);
103 724 : case Mode::Resistance:
104 724 : return ResistanceRange::maxValue(product, range);
105 0 : case Mode::Diode:
106 0 : case Mode::Continuity:
107 222 : case Mode::Temperature:
108 222 : break;
109 362 : case Mode::Capacitance:
110 362 : return CapacitanceRange::maxValue(product, range);
111 222 : case Mode::ExternalTemperature:
112 222 : break;
113 2220 : }
114 666 : return 0;
115 2220 : }
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 700 : quint32 MultimeterService::maxValue(const quint8 range, const Mode mode) const
121 1110 : {
122 1810 : return maxValue(*pokitProduct(), range, mode);
123 1110 : }
124 :
125 : /*!
126 : * Constructs a new Pokit service with \a parent.
127 : */
128 5110 : MultimeterService::MultimeterService(QLowEnergyController * const controller, QObject * parent)
129 9116 : : AbstractPokitService(new MultimeterServicePrivate(controller, this), parent)
130 4533 : {
131 :
132 9643 : }
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 70 : bool MultimeterService::readCharacteristics()
147 111 : {
148 181 : return readReadingCharacteristic();
149 111 : }
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 118 : bool MultimeterService::readReadingCharacteristic()
161 222 : {
162 222 : Q_D(MultimeterService);
163 362 : return d->readCharacteristic(CharacteristicUuids::reading);
164 222 : }
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 70 : bool MultimeterService::setSettings(const Settings &settings)
174 111 : {
175 111 : Q_D(const MultimeterService);
176 111 : const QLowEnergyCharacteristic characteristic =
177 181 : d->getCharacteristic(CharacteristicUuids::settings);
178 181 : if (!characteristic.isValid()) {
179 111 : return false;
180 111 : }
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 70 : }
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 70 : MultimeterService::Reading MultimeterService::reading() const
206 111 : {
207 111 : Q_D(const MultimeterService);
208 111 : const QLowEnergyCharacteristic characteristic =
209 181 : d->getCharacteristic(CharacteristicUuids::reading);
210 181 : return (characteristic.isValid()) ? MultimeterServicePrivate::parseReading(characteristic.value())
211 251 : : Reading{ MeterStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0 };
212 181 : }
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 70 : bool MultimeterService::enableReadingNotifications()
224 111 : {
225 111 : Q_D(MultimeterService);
226 181 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
227 111 : }
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 70 : bool MultimeterService::disableReadingNotifications()
237 111 : {
238 111 : Q_D(MultimeterService);
239 181 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
240 111 : }
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 3504 : MultimeterServicePrivate::MultimeterServicePrivate(
270 5110 : QLowEnergyController * controller, MultimeterService * const q)
271 9116 : : AbstractPokitServicePrivate(MultimeterService::serviceUuid, controller, q)
272 4533 : {
273 :
274 8037 : }
275 :
276 : /*!
277 : * Returns \a settings in the format Pokit devices expect.
278 : */
279 280 : QByteArray MultimeterServicePrivate::encodeSettings(const MultimeterService::Settings &settings)
280 444 : {
281 444 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
282 444 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
283 444 : static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
284 :
285 628 : QByteArray value;
286 724 : QDataStream stream(&value, QIODevice::WriteOnly);
287 724 : stream.setByteOrder(QDataStream::LittleEndian);
288 724 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
289 724 : stream << (quint8)settings.mode << settings.range << settings.updateInterval;
290 :
291 444 : Q_ASSERT(value.size() == 6);
292 724 : return value;
293 724 : }
294 :
295 : /*!
296 : * Parses the `Reading` \a value into a MultimeterService::Reading struct.
297 : */
298 350 : MultimeterService::Reading MultimeterServicePrivate::parseReading(const QByteArray &value)
299 555 : {
300 665 : MultimeterService::Reading reading{
301 555 : MultimeterService::MeterStatus::Error,
302 555 : std::numeric_limits<float>::quiet_NaN(),
303 555 : MultimeterService::Mode::Idle, 0
304 555 : };
305 :
306 1135 : if (!checkSize(u"Reading"_s, value, 7, 7)) {
307 318 : return reading;
308 222 : }
309 :
310 513 : reading.status = MultimeterService::MeterStatus(value.at(0));
311 681 : reading.value = qFromLittleEndian<float>(value.mid(1,4).constData());
312 513 : reading.mode = static_cast<MultimeterService::Mode>(value.at(5));
313 513 : reading.range = static_cast<quint8>(value.at(6));
314 543 : return reading;
315 555 : }
316 :
317 : /*!
318 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
319 : * specialised signal, for each supported \a characteristic.
320 : */
321 70 : void MultimeterServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
322 : const QByteArray &value)
323 111 : {
324 181 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
325 :
326 111 : Q_Q(MultimeterService);
327 181 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
328 0 : Q_EMIT q->readingRead(parseReading(value));
329 0 : return;
330 0 : }
331 :
332 181 : 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 387 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Multimeter service")
339 262 : << serviceUuid << characteristic.name() << characteristic.uuid();
340 111 : }
341 :
342 : /*!
343 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
344 : * specialised signal, for each supported \a characteristic.
345 : */
346 70 : void MultimeterServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
347 : const QByteArray &newValue)
348 111 : {
349 181 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
350 :
351 111 : Q_Q(MultimeterService);
352 181 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
353 0 : Q_EMIT q->settingsWritten();
354 0 : return;
355 0 : }
356 :
357 181 : 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 387 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Multimeter service")
364 262 : << serviceUuid << characteristic.name() << characteristic.uuid();
365 111 : }
366 :
367 : /*!
368 : * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
369 : * specialised signal, for each supported \a characteristic.
370 : */
371 70 : void MultimeterServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
372 : const QByteArray &newValue)
373 111 : {
374 181 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
375 :
376 111 : Q_Q(MultimeterService);
377 181 : 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 181 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
384 0 : Q_EMIT q->readingRead(parseReading(newValue));
385 0 : return;
386 0 : }
387 :
388 387 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for Multimeter service")
389 262 : << serviceUuid << characteristic.name() << characteristic.uuid();
390 111 : }
391 :
392 : /// \endcond
393 :
394 : QTPOKIT_END_NAMESPACE
|