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