Dokit
Internal development documentation
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Macros Pages
multimeterservice.cpp
Go to the documentation of this file.
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
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
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.
40{
41 switch (mode) {
42 case Mode::Idle: return tr("Idle");
43 case Mode::DcVoltage: return tr("DC voltage");
44 case Mode::AcVoltage: return tr("AC voltage");
45 case Mode::DcCurrent: return tr("DC current");
46 case Mode::AcCurrent: return tr("AC current");
47 case Mode::Resistance: return tr("Resistance");
48 case Mode::Diode: return tr("Diode");
49 case Mode::Continuity: return tr("Continuity");
50 case Mode::Temperature: return tr("Temperature");
51 case Mode::Capacitance: return tr("Capacitance");
52 case Mode::ExternalTemperature: return tr("External temperature");
53 }
54 return QString();
55}
56
57/// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
58QString MultimeterService::toString(const PokitProduct product, const quint8 range, const Mode mode)
59{
60 switch (mode) {
61 case Mode::Idle:
62 break;
63 case Mode::DcVoltage:
64 case Mode::AcVoltage:
65 return VoltageRange::toString(product, range);
66 case Mode::DcCurrent:
67 case Mode::AcCurrent:
68 return CurrentRange::toString(product, range);
70 return ResistanceRange::toString(product, range);
71 case Mode::Diode:
74 break;
76 return CapacitanceRange::toString(product, range);
78 break;
79 }
80 return QString();
81}
82
83/// Returns \a range as a user-friendly string, or a null QString if \a mode has no ranges.
84QString MultimeterService::toString(const quint8 range, const Mode mode) const
85{
86 return toString(*pokitProduct(), range, mode);
87}
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 */
92quint32 MultimeterService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
93{
94 switch (mode) {
95 case Mode::Idle:
96 break;
97 case Mode::DcVoltage:
98 case Mode::AcVoltage:
99 return VoltageRange::maxValue(product, range);
100 case Mode::DcCurrent:
101 case Mode::AcCurrent:
102 return CurrentRange::maxValue(product, range);
103 case Mode::Resistance:
104 return ResistanceRange::maxValue(product, range);
105 case Mode::Diode:
106 case Mode::Continuity:
108 break;
110 return CapacitanceRange::maxValue(product, range);
112 break;
113 }
114 return 0;
115}
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 */
120quint32 MultimeterService::maxValue(const quint8 range, const Mode mode) const
121{
122 return maxValue(*pokitProduct(), range, mode);
123}
124
125/*!
126 * Constructs a new Pokit service with \a parent.
127 */
129 : AbstractPokitService(new MultimeterServicePrivate(controller, this), parent)
130{
131
132}
133
134/*!
135 * \cond internal
136 * Constructs a new Pokit service with \a parent, and private implementation \a d.
137 */
139 MultimeterServicePrivate * const d, QObject * const parent)
140 : AbstractPokitService(d, parent)
141{
142
143}
144/// \endcond
145
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 */
161{
163 return d->readCharacteristic(CharacteristicUuids::reading);
164}
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 */
174{
175 Q_D(const MultimeterService);
176 const QLowEnergyCharacteristic characteristic =
177 d->getCharacteristic(CharacteristicUuids::settings);
178 if (!characteristic.isValid()) {
179 return false;
180 }
181
183 if (value.isNull()) {
184 return false;
185 }
186
187 d->service->writeCharacteristic(characteristic, value);
188 return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
189}
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 */
206{
207 Q_D(const MultimeterService);
208 const QLowEnergyCharacteristic characteristic =
209 d->getCharacteristic(CharacteristicUuids::reading);
210 return (characteristic.isValid()) ? MultimeterServicePrivate::parseReading(characteristic.value())
211 : Reading{ MeterStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0 };
212}
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 */
224{
226 return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
227}
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 */
237{
239 return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
240}
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 */
275
276/*!
277 * Returns \a settings in the format Pokit devices expect.
278 */
280{
281 static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
282 static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
283 static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
284
285 QByteArray value;
286 QDataStream stream(&value, QIODevice::WriteOnly);
288 stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
289 stream << (quint8)settings.mode << settings.range << settings.updateInterval;
290
291 Q_ASSERT(value.size() == 6);
292 return value;
293}
294
295/*!
296 * Parses the `Reading` \a value into a MultimeterService::Reading struct.
297 */
299{
302 std::numeric_limits<float>::quiet_NaN(),
304 };
305
306 if (!checkSize(u"Reading"_s, value, 7, 7)) {
307 return reading;
308 }
309
310 reading.status = MultimeterService::MeterStatus(value.at(0));
311 reading.value = qFromLittleEndian<float>(value.mid(1,4).constData());
312 reading.mode = static_cast<MultimeterService::Mode>(value.at(5));
313 reading.range = static_cast<quint8>(value.at(6));
314 return reading;
315}
316
317/*!
318 * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
319 * specialised signal, for each supported \a characteristic.
320 */
322 const QByteArray &value)
323{
325
328 Q_EMIT q->readingRead(parseReading(value));
329 return;
330 }
331
333 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
334 << serviceUuid << characteristic.name() << characteristic.uuid();
335 return;
336 }
337
338 qCWarning(lc).noquote() << tr("Unknown characteristic read for Multimeter service")
339 << serviceUuid << characteristic.name() << characteristic.uuid();
340}
341
342/*!
343 * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
344 * specialised signal, for each supported \a characteristic.
345 */
347 const QByteArray &newValue)
348{
350
353 Q_EMIT q->settingsWritten();
354 return;
355 }
356
358 qCWarning(lc).noquote() << tr("Reading characteristic is read/notify, but somehow written")
359 << serviceUuid << characteristic.name() << characteristic.uuid();
360 return;
361 }
362
363 qCWarning(lc).noquote() << tr("Unknown characteristic written for Multimeter service")
364 << serviceUuid << characteristic.name() << characteristic.uuid();
365}
366
367/*!
368 * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
369 * specialised signal, for each supported \a characteristic.
370 */
372 const QByteArray &newValue)
373{
375
378 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
379 << serviceUuid << characteristic.name() << characteristic.uuid();
380 return;
381 }
382
384 Q_EMIT q->readingRead(parseReading(newValue));
385 return;
386 }
387
388 qCWarning(lc).noquote() << tr("Unknown characteristic notified for Multimeter service")
389 << serviceUuid << characteristic.name() << characteristic.uuid();
390}
391
392/// \endcond
393
QBluetoothUuid serviceUuid
UUIDs for service.
virtual void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicChanged events.
AbstractPokitServicePrivate(const QBluetoothUuid &serviceUuid, QLowEnergyController *controller, AbstractPokitService *const q)
virtual void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
Handles QLowEnergyService::characteristicRead events.
virtual void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicWritten events.
QLowEnergyController * controller
BLE controller to fetch the service from.
static bool checkSize(const QString &label, const QByteArray &data, const int minSize, const int maxSize=-1, const bool failOnMax=false)
Returns false if data is smaller than minSize, otherwise returns failOnMax if data is bigger than max...
std::optional< PokitProduct > pokitProduct() const
Returns the Pokit product this service is attached to.
The MultimeterServicePrivate class provides private implementation for MultimeterService.
void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) override
Implements AbstractPokitServicePrivate::characteristicRead to parse value, then emit a specialised si...
MultimeterServicePrivate(QLowEnergyController *controller, MultimeterService *const q)
static QByteArray encodeSettings(const MultimeterService::Settings &settings)
Returns settings in the format Pokit devices expect.
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) override
Implements AbstractPokitServicePrivate::characteristicWritten to parse newValue, then emit a speciali...
static MultimeterService::Reading parseReading(const QByteArray &value)
Parses the Reading value into a MultimeterService::Reading struct.
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) override
Implements AbstractPokitServicePrivate::characteristicChanged to parse newValue, then emit a speciali...
The MultimeterService class accesses the Multimeter service of Pokit devices.
static quint32 maxValue(const PokitProduct product, const quint8 range, const Mode mode)
Returns the maximum value for range, or 0 if range is not a known value for product's mode.
bool enableReadingNotifications()
Enables client-side notifications of meter readings.
MeterStatus
Values supported by the Status attribute of the Settings characteristic.
Mode
Values supported by the Mode attribute of the Settings and Reading characteristics.
@ DcVoltage
Measure DC voltage.
@ Capacitance
Measure capacitance.
@ AcCurrent
Measure AC current.
@ ExternalTemperature
Measure temperature via an external temperature probe.
@ Resistance
Measure resistance.
@ AcVoltage
Measure AC voltage.
@ Idle
Make device idle.
@ Temperature
Measure temperature.
@ DcCurrent
Measure DC current.
@ Continuity
Measure continuity.
bool readCharacteristics() override
Read all characteristics.
static QString toString(const Mode &mode)
Returns mode as a user-friendly string.
MultimeterService(QLowEnergyController *const pokitDevice, QObject *parent=nullptr)
Constructs a new Pokit service with parent.
bool readReadingCharacteristic()
Read the Multimeter service's Reading characteristic.
bool setSettings(const Settings &settings)
Configures the Pokit device's multimeter mode.
Reading reading() const
Returns the most recent value of the Multimeter service's Reading characteristic.
bool disableReadingNotifications()
Disables client-side notifications of meter readings.
Declares the MultimeterService class.
Declares the MultimeterServicePrivate class.
QString toString(const PokitProduct product, const quint8 range)
Returns product's capacitance range as a human-friendly string.
quint32 maxValue(const PokitProduct product, const quint8 range)
Returns the maximum value for range in nanofarads, or 0 if range is not a known value for product.
QString toString(const PokitProduct product, const quint8 range)
Returns product's current range as a human-friendly string.
quint32 maxValue(const PokitProduct product, const quint8 range)
Returns the maximum value for range in microamps, or 0 if range is not a known value for product.
QString toString(const PokitProduct product, const quint8 range)
Returns product's current range as a human-friendly string.
quint32 maxValue(const PokitProduct product, const quint8 range)
Returns the maximum value for range in ohms, or 0 if range is not a known value for product.
quint32 maxValue(const PokitProduct product, const quint8 range)
Returns the maximum value for range in millivolts, or 0 if range is not a known value for product.
QString toString(const PokitProduct product, const quint8 range)
Returns product's current range as a human-friendly string.
PokitProduct
Pokit products known to, and supported by, the QtPokit library.
char at(int i) const const
const char * constData() const const
bool isNull() const const
QByteArray mid(int pos, int len) const const
int size() const const
void setByteOrder(QDataStream::ByteOrder bo)
void setFloatingPointPrecision(QDataStream::FloatingPointPrecision precision)
bool isValid() const const
QString name() const const
QBluetoothUuid uuid() const const
QByteArray value() const const
QObject(QObject *parent)
Q_EMITQ_EMIT
QObject * parent() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
#define QTPOKIT_BEGIN_NAMESPACE
Macro for starting the QtPokit library's top-most namespace (if one is defined).
#define QTPOKIT_END_NAMESPACE
Macro for ending the QtPokit library's top-most namespace (if one is defined).
Declares the DOKIT_USE_STRINGLITERALS macro, and related functions.
#define DOKIT_USE_STRINGLITERALS
Internal macro for using either official Qt string literals (added in Qt 6.4), or our own equivalent ...
static const QBluetoothUuid reading
UUID of the Multimeter service's Reading characteristic.
static const QBluetoothUuid settings
UUID of the Multimeter service's Settings characteristic.
Attributes included in the Reading characteristic.
MeterStatus status
Current multimeter status.
Attributes included in the Settings characteristic.
quint32 updateInterval
Desired update interval in milliseconds.
Mode mode
Desired operation mode.