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