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