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 4000 : QString MultimeterService::toString(const Mode &mode)
36 2090 : {
37 6090 : switch (mode) {
38 1056 : case Mode::Idle: return tr("Idle");
39 486 : case Mode::DcVoltage: return tr("DC voltage");
40 486 : case Mode::AcVoltage: return tr("AC voltage");
41 486 : case Mode::DcCurrent: return tr("DC current");
42 486 : case Mode::AcCurrent: return tr("AC current");
43 486 : case Mode::Resistance: return tr("Resistance");
44 486 : case Mode::Diode: return tr("Diode");
45 885 : case Mode::Continuity: return tr("Continuity");
46 486 : case Mode::Temperature: return tr("Temperature");
47 486 : case Mode::Capacitance: return tr("Capacitance");
48 87 : case Mode::ExternalTemperature: return tr("External temperature");
49 2090 : }
50 94 : return QString();
51 2090 : }
52 :
53 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
54 3800 : QString MultimeterService::toString(const PokitProduct product, const quint8 range, const Mode mode)
55 2215 : {
56 6015 : switch (mode) {
57 349 : case Mode::Idle:
58 349 : break;
59 742 : case Mode::DcVoltage:
60 392 : case Mode::AcVoltage:
61 1032 : return VoltageRange::toString(product, range);
62 930 : case Mode::DcCurrent:
63 392 : case Mode::AcCurrent:
64 1032 : return CurrentRange::toString(product, range);
65 690 : case Mode::Resistance:
66 690 : return ResistanceRange::toString(product, range);
67 102 : case Mode::Diode:
68 306 : case Mode::Continuity:
69 502 : case Mode::Temperature:
70 502 : break;
71 516 : case Mode::Capacitance:
72 516 : return CapacitanceRange::toString(product, range);
73 94 : case Mode::ExternalTemperature:
74 94 : break;
75 2215 : }
76 945 : return QString();
77 2215 : }
78 :
79 : /// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
80 3400 : QString MultimeterService::toString(const quint8 range, const Mode mode) const
81 1745 : {
82 5145 : return toString(*pokitProduct(), range, mode);
83 1745 : }
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 800 : quint32 MultimeterService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
89 940 : {
90 1740 : switch (mode) {
91 94 : case Mode::Idle:
92 94 : break;
93 160 : case Mode::DcVoltage:
94 188 : case Mode::AcVoltage:
95 348 : return VoltageRange::maxValue(product, range);
96 348 : case Mode::DcCurrent:
97 188 : case Mode::AcCurrent:
98 348 : return CurrentRange::maxValue(product, range);
99 348 : case Mode::Resistance:
100 348 : return ResistanceRange::maxValue(product, range);
101 0 : case Mode::Diode:
102 0 : case Mode::Continuity:
103 94 : case Mode::Temperature:
104 94 : break;
105 174 : case Mode::Capacitance:
106 174 : return CapacitanceRange::maxValue(product, range);
107 94 : case Mode::ExternalTemperature:
108 94 : break;
109 940 : }
110 282 : return 0;
111 940 : }
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 400 : quint32 MultimeterService::maxValue(const quint8 range, const Mode mode) const
117 470 : {
118 870 : return maxValue(*pokitProduct(), range, mode);
119 470 : }
120 :
121 : /*!
122 : * Constructs a new Pokit service with \a parent.
123 : */
124 2920 : MultimeterService::MultimeterService(QLowEnergyController * const controller, QObject * parent)
125 4161 : : AbstractPokitService(new MultimeterServicePrivate(controller, this), parent)
126 2171 : {
127 :
128 5091 : }
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 40 : bool MultimeterService::readCharacteristics()
143 47 : {
144 87 : return readReadingCharacteristic();
145 47 : }
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 57 : bool MultimeterService::readReadingCharacteristic()
157 94 : {
158 94 : Q_D(MultimeterService);
159 174 : return d->readCharacteristic(CharacteristicUuids::reading);
160 94 : }
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 40 : bool MultimeterService::setSettings(const Settings &settings)
170 47 : {
171 47 : Q_D(const MultimeterService);
172 47 : const QLowEnergyCharacteristic characteristic =
173 87 : d->getCharacteristic(CharacteristicUuids::settings);
174 87 : if (!characteristic.isValid()) {
175 47 : return false;
176 47 : }
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 40 : }
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 40 : MultimeterService::Reading MultimeterService::reading() const
202 47 : {
203 47 : Q_D(const MultimeterService);
204 47 : const QLowEnergyCharacteristic characteristic =
205 87 : d->getCharacteristic(CharacteristicUuids::reading);
206 87 : return (characteristic.isValid()) ? MultimeterServicePrivate::parseReading(characteristic.value())
207 127 : : Reading{ MeterStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0 };
208 87 : }
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 40 : bool MultimeterService::enableReadingNotifications()
220 47 : {
221 47 : Q_D(MultimeterService);
222 87 : return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
223 47 : }
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 40 : bool MultimeterService::disableReadingNotifications()
233 47 : {
234 47 : Q_D(MultimeterService);
235 87 : return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
236 47 : }
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 1241 : MultimeterServicePrivate::MultimeterServicePrivate(
266 2920 : QLowEnergyController * controller, MultimeterService * const q)
267 4161 : : AbstractPokitServicePrivate(MultimeterService::serviceUuid, controller, q)
268 2171 : {
269 :
270 3412 : }
271 :
272 : /*!
273 : * Returns \a settings in the format Pokit devices expect.
274 : */
275 160 : QByteArray MultimeterServicePrivate::encodeSettings(const MultimeterService::Settings &settings)
276 188 : {
277 188 : static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
278 188 : static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
279 188 : static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
280 :
281 256 : QByteArray value;
282 348 : QDataStream stream(&value, QIODevice::WriteOnly);
283 348 : stream.setByteOrder(QDataStream::LittleEndian);
284 348 : stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
285 348 : stream << (quint8)settings.mode << settings.range << settings.updateInterval;
286 :
287 188 : Q_ASSERT(value.size() == 6);
288 348 : return value;
289 348 : }
290 :
291 : /*!
292 : * Parses the `Reading` \a value into a MultimeterService::Reading struct.
293 : */
294 200 : MultimeterService::Reading MultimeterServicePrivate::parseReading(const QByteArray &value)
295 235 : {
296 330 : MultimeterService::Reading reading{
297 235 : MultimeterService::MeterStatus::Error,
298 235 : std::numeric_limits<float>::quiet_NaN(),
299 235 : MultimeterService::Mode::Idle, 0
300 235 : };
301 :
302 520 : if (!checkSize(QLatin1String("Reading"), value, 7, 7)) {
303 136 : return reading;
304 94 : }
305 :
306 261 : reading.status = MultimeterService::MeterStatus(value.at(0));
307 312 : reading.value = qFromLittleEndian<float>(value.mid(1,4).constData());
308 261 : reading.mode = static_cast<MultimeterService::Mode>(value.at(5));
309 261 : reading.range = static_cast<quint8>(value.at(6));
310 261 : return reading;
311 235 : }
312 :
313 : /*!
314 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
315 : * specialised signal, for each supported \a characteristic.
316 : */
317 40 : void MultimeterServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic,
318 : const QByteArray &value)
319 47 : {
320 87 : AbstractPokitServicePrivate::characteristicRead(characteristic, value);
321 :
322 47 : Q_Q(MultimeterService);
323 87 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
324 0 : Q_EMIT q->readingRead(parseReading(value));
325 0 : return;
326 0 : }
327 :
328 87 : 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 192 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Multimeter service")
335 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
336 47 : }
337 :
338 : /*!
339 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
340 : * specialised signal, for each supported \a characteristic.
341 : */
342 40 : void MultimeterServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic,
343 : const QByteArray &newValue)
344 47 : {
345 87 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue);
346 :
347 47 : Q_Q(MultimeterService);
348 87 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::settings) {
349 0 : Q_EMIT q->settingsWritten();
350 0 : return;
351 0 : }
352 :
353 87 : 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 192 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Multimeter service")
360 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
361 47 : }
362 :
363 : /*!
364 : * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
365 : * specialised signal, for each supported \a characteristic.
366 : */
367 40 : void MultimeterServicePrivate::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
368 : const QByteArray &newValue)
369 47 : {
370 87 : AbstractPokitServicePrivate::characteristicChanged(characteristic, newValue);
371 :
372 47 : Q_Q(MultimeterService);
373 87 : 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 87 : if (characteristic.uuid() == MultimeterService::CharacteristicUuids::reading) {
380 0 : Q_EMIT q->readingRead(parseReading(newValue));
381 0 : return;
382 0 : }
383 :
384 192 : qCWarning(lc).noquote() << tr("Unknown characteristic notified for Multimeter service")
385 144 : << serviceUuid << characteristic.name() << characteristic.uuid();
386 47 : }
387 :
388 : /// \endcond
|