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