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