Dokit
Internal development documentation
Loading...
Searching...
No Matches
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
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.
36{
37 switch (mode) {
38 case Mode::Idle: return tr("Idle");
39 case Mode::DcVoltage: return tr("DC voltage");
40 case Mode::AcVoltage: return tr("AC voltage");
41 case Mode::DcCurrent: return tr("DC current");
42 case Mode::AcCurrent: return tr("AC current");
43 case Mode::Resistance: return tr("Resistance");
44 case Mode::Diode: return tr("Diode");
45 case Mode::Continuity: return tr("Continuity");
46 case Mode::Temperature: return tr("Temperature");
47 case Mode::Capacitance: return tr("Capacitance");
48 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.
54QString MultimeterService::toString(const PokitProduct product, const quint8 range, const Mode mode)
55{
56 switch (mode) {
57 case Mode::Idle:
58 break;
59 case Mode::DcVoltage:
60 case Mode::AcVoltage:
61 return VoltageRange::toString(product, range);
62 case Mode::DcCurrent:
63 case Mode::AcCurrent:
64 return CurrentRange::toString(product, range);
66 return ResistanceRange::toString(product, range);
67 case Mode::Diode:
70 break;
72 return CapacitanceRange::toString(product, range);
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.
80QString MultimeterService::toString(const quint8 range, const Mode mode) const
81{
82 return toString(*pokitProduct(), range, mode);
83}
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 */
88quint32 MultimeterService::maxValue(const PokitProduct product, const quint8 range, const Mode mode)
89{
90 switch (mode) {
91 case Mode::Idle:
92 break;
93 case Mode::DcVoltage:
94 case Mode::AcVoltage:
95 return VoltageRange::maxValue(product, range);
96 case Mode::DcCurrent:
97 case Mode::AcCurrent:
98 return CurrentRange::maxValue(product, range);
100 return ResistanceRange::maxValue(product, range);
101 case Mode::Diode:
102 case Mode::Continuity:
104 break;
106 return CapacitanceRange::maxValue(product, range);
108 break;
109 }
110 return 0;
111}
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 */
116quint32 MultimeterService::maxValue(const quint8 range, const Mode mode) const
117{
118 return maxValue(*pokitProduct(), range, mode);
119}
120
121/*!
122 * Constructs a new Pokit service with \a parent.
123 */
125 : AbstractPokitService(new MultimeterServicePrivate(controller, this), parent)
126{
127
128}
129
130/*!
131 * \cond internal
132 * Constructs a new Pokit service with \a parent, and private implementation \a d.
133 */
135 MultimeterServicePrivate * const d, QObject * const parent)
136 : AbstractPokitService(d, parent)
137{
138
139}
140/// \endcond
141
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 */
157{
159 return d->readCharacteristic(CharacteristicUuids::reading);
160}
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 */
170{
171 Q_D(const MultimeterService);
172 const QLowEnergyCharacteristic characteristic =
173 d->getCharacteristic(CharacteristicUuids::settings);
174 if (!characteristic.isValid()) {
175 return false;
176 }
177
179 if (value.isNull()) {
180 return false;
181 }
182
183 d->service->writeCharacteristic(characteristic, value);
184 return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError);
185}
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 */
202{
203 Q_D(const MultimeterService);
204 const QLowEnergyCharacteristic characteristic =
205 d->getCharacteristic(CharacteristicUuids::reading);
206 return (characteristic.isValid()) ? MultimeterServicePrivate::parseReading(characteristic.value())
207 : Reading{ MeterStatus::Error, std::numeric_limits<float>::quiet_NaN(), Mode::Idle, 0 };
208}
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 */
220{
222 return d->enableCharacteristicNotificatons(CharacteristicUuids::reading);
223}
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 */
233{
235 return d->disableCharacteristicNotificatons(CharacteristicUuids::reading);
236}
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 */
271
272/*!
273 * Returns \a settings in the format Pokit devices expect.
274 */
276{
277 static_assert(sizeof(settings.mode) == 1, "Expected to be 1 byte.");
278 static_assert(sizeof(settings.range) == 1, "Expected to be 1 byte.");
279 static_assert(sizeof(settings.updateInterval) == 4, "Expected to be 4 bytes.");
280
281 QByteArray value;
282 QDataStream stream(&value, QIODevice::WriteOnly);
284 stream.setFloatingPointPrecision(QDataStream::SinglePrecision); // 32-bit floats, not 64-bit.
285 stream << (quint8)settings.mode << settings.range << settings.updateInterval;
286
287 Q_ASSERT(value.size() == 6);
288 return value;
289}
290
291/*!
292 * Parses the `Reading` \a value into a MultimeterService::Reading struct.
293 */
295{
298 std::numeric_limits<float>::quiet_NaN(),
300 };
301
302 if (!checkSize(QLatin1String("Reading"), value, 7, 7)) {
303 return reading;
304 }
305
306 reading.status = MultimeterService::MeterStatus(value.at(0));
307 reading.value = qFromLittleEndian<float>(value.mid(1,4).constData());
308 reading.mode = static_cast<MultimeterService::Mode>(value.at(5));
309 reading.range = static_cast<quint8>(value.at(6));
310 return reading;
311}
312
313/*!
314 * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a
315 * specialised signal, for each supported \a characteristic.
316 */
318 const QByteArray &value)
319{
321
324 Q_EMIT q->readingRead(parseReading(value));
325 return;
326 }
327
329 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow read")
330 << serviceUuid << characteristic.name() << characteristic.uuid();
331 return;
332 }
333
334 qCWarning(lc).noquote() << tr("Unknown characteristic read for Multimeter service")
335 << serviceUuid << characteristic.name() << characteristic.uuid();
336}
337
338/*!
339 * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a
340 * specialised signal, for each supported \a characteristic.
341 */
343 const QByteArray &newValue)
344{
346
349 Q_EMIT q->settingsWritten();
350 return;
351 }
352
354 qCWarning(lc).noquote() << tr("Reading characteristic is read/notify, but somehow written")
355 << serviceUuid << characteristic.name() << characteristic.uuid();
356 return;
357 }
358
359 qCWarning(lc).noquote() << tr("Unknown characteristic written for Multimeter service")
360 << serviceUuid << characteristic.name() << characteristic.uuid();
361}
362
363/*!
364 * Implements AbstractPokitServicePrivate::characteristicChanged to parse \a newValue, then emit a
365 * specialised signal, for each supported \a characteristic.
366 */
368 const QByteArray &newValue)
369{
371
374 qCWarning(lc).noquote() << tr("Settings characteristic is write-only, but somehow updated")
375 << serviceUuid << characteristic.name() << characteristic.uuid();
376 return;
377 }
378
380 Q_EMIT q->readingRead(parseReading(newValue));
381 return;
382 }
383
384 qCWarning(lc).noquote() << tr("Unknown characteristic notified for Multimeter service")
385 << serviceUuid << characteristic.name() << characteristic.uuid();
386}
387
388/// \endcond
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)
static const QBluetoothUuid reading
UUID of the Multimeter service's Reading characterstic.
static const QBluetoothUuid settings
UUID of the Multimeter service's Settings characterstic.
Attributes included in the Reading characterstic.
MeterStatus status
Current multimeter status.
Attributes included in the Settings characterstic.
quint32 updateInterval
Desired update interval in milliseconds.
Mode mode
Desired operation mode.