Dokit
Internal development documentation
Loading...
Searching...
No Matches
abstractpokitservice.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2022-2024 Paul Colby <git@colby.id.au>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4/*!
5 * \file
6 * Defines the AbstractPokitService and AbstractPokitServicePrivate classes.
7 */
8
11#include "pokitproducts_p.h"
12
13#include <qtpokit/pokitdevice.h>
14
15#include <QLowEnergyController>
16
17/*!
18 * \class AbstractPokitService
19 *
20 * The AbstractPokitService class provides a common base for Pokit services classes.
21 */
22
23/*!
24 * \cond internal
25 * Constructs a new Pokit service with \a parent, and private implementation \a d.
26 */
27AbstractPokitService::AbstractPokitService(
28 AbstractPokitServicePrivate * const d, QObject * const parent)
29 : QObject(parent), d_ptr(d)
30{
31
32}
33/// \endcond
34
35/*!
36 * Destroys this AbstractPokitService object.
37 */
42
43/*!
44 * \fn virtual bool AbstractPokitService::readCharacteristics() = 0
45 *
46 * Read all characteristics.
47 *
48 * This convenience function will queue refresh requests of all characteristics supported by this
49 * service.
50 *
51 * Relevant `*Service::*Read` signals will be emitted by derived class objects as each
52 * characteristic is successfully read.
53 */
54
55/*!
56 * Returns `true` if autodiscovery of services and service details is enabled, `false` otherwise.
57 *
58 * \see setAutoDiscover for more information on what autodiscovery provides.
59 */
61{
62 Q_D(const AbstractPokitService);
63 return d->autoDiscover;
64}
65
66/*!
67 * If \a discover is \c true, autodiscovery will be attempted.
68 *
69 * Specifically, this may resulting in automatic invocation of:
70 * * QLowEnergyController::discoverServices if/when the internal controller is connected; and
71 * * QLowEnergyService::discoverDetails if/when an internal service object is created.
72 *
73 * \see autoDiscover
74 */
76{
78 d->autoDiscover = discover;
79}
80
81/*!
82 * Returns the Pokit product this service is attached to.
83 *
84 * \see setPokitProduct
85 */
86std::optional<PokitProduct> AbstractPokitService::pokitProduct() const
87{
88 Q_D(const AbstractPokitService);
89 return d->pokitProduct;
90}
91
92/*!
93 * Sets the current Pokit \a product.
94 *
95 * This must be called to set the product before this object's BLE controller's services are discovered. If
96 * autoDiscover() is enabled, then this should be done before the controller's \c connectToDevice() is called.
97 *
98 * For example:
99 * ```
100 * Q_ASSERT(isPokitProduct(deviceInfo));
101 * auto controller = QLowEnergyController::createCentral(deviceInfo);
102 * auto service = new DsoService(controller);
103 * service->setPokitProduct(pokitProduct(deviceInfo));
104 * controller->connectToDevice();
105 * ```
106 *
107 * \see autoDiscover
108 * \see pokitProduct
109 */
111{
113 d->pokitProduct = product;
114}
115
116/*!
117 * Returns a non-const pointer to the internal service object, if any.
118 */
120{
122 return d->service;
123}
124
125/*!
126 * Returns a const pointer to the internal service object, if any.
127 */
129{
130 Q_D(const AbstractPokitService);
131 return d->service;
132}
133
134/*!
135 * \fn void AbstractPokitService::serviceDetailsDiscovered()
136 *
137 * This signal is emitted when the Pokit service details have been discovered.
138 *
139 * Once this signal has been emitted, cached characteristics values should be immediately available
140 * via derived classes' accessor functions, and refreshes can be queued via readCharacteristics()
141 * and any related read functions provided by derived classes.
142 */
143
144/*!
145 * \fn void AbstractPokitService::serviceErrorOccurred(QLowEnergyService::ServiceError newError)
146 *
147 * This signal is emitted whenever an error occurs on the underlying QLowEnergyService.
148 */
149
150/*!
151 * \cond internal
152 * \class AbstractPokitServicePrivate
153 *
154 * The AbstractPokitServicePrivate class provides private implementation for AbstractPokitService.
155 */
156
157/*!
158 * \internal
159 * Constructs a new AbstractPokitServicePrivate object with public implementation \a q.
160 *
161 * Note, typically the \a serviceUuid should be set validly, however, in the rare case that a
162 * service's UUID can vary (ie the Status Service), \a serviceUuid may be set to a `null`
163 * QBluetoothUuid here, and updated when the correct service UUID is known.
164 *
165 * \see StatusService::ServiceUuids
166 * \see StatusServicePrivate::serviceDiscovered
167 */
185
186/*!
187 * Creates an internal service object from the internal controller.
188 *
189 * Any existing service object will *not* be replaced.
190 *
191 * Returns \c true if a service was created successfully, either now, or sometime previously.
192 */
194{
195 if (!controller) {
196 return false;
197 }
198
199 if (service) {
200 qCDebug(lc).noquote() << tr("Already have service object:") << service;
201 return true;
202 }
203
204 if (serviceUuid.isNull()) {
205 qCDebug(lc).noquote() << tr("Service UUID not assigned yet.");
206 return false;
207 }
208
210 if (!service) {
211 return false;
212 }
213 qCDebug(lc).noquote() << tr("Service object created for %1 device:").arg(toString(*this->pokitProduct)) << service;
214
223
225 [](const QLowEnergyDescriptor &descriptor, const QByteArray &value){
226 qCDebug(lc).noquote() << tr(R"(Descriptor "%1" (%2) read.)")
227 .arg(descriptor.name(), descriptor.uuid().toString());
228 Q_UNUSED(value)
229 });
230
232 [](const QLowEnergyDescriptor &descriptor, const QByteArray &newValue){
233 qCDebug(lc).noquote() << tr(R"(Descriptor "%1" (%2) written.)")
234 .arg(descriptor.name(), descriptor.uuid().toString());
235 Q_UNUSED(newValue)
236 });
237
239 #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 0))
240 QOverload<QLowEnergyService::ServiceError>::of(&QLowEnergyService::error),
241 #else
242 &QLowEnergyService::errorOccurred,
243 #endif
245
246 if (autoDiscover) {
248 }
249 return true;
250}
251
252/*!
253 * Get \a uuid characteristc from the underlying service. This helper function is equivalent to
254 *
255 * ```
256 * return service->characteristic(uuid);
257 * ```
258 *
259 * except that it performs some sanity checks, such as checking the service object pointer has been
260 * assigned first, and also logs failures in a consistent manner.
261 *
262 * \param uuid
263 * \return
264 */
266{
267 if (!service) {
268 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" requested before service assigned.)")
271 }
272
273 const QLowEnergyCharacteristic characteristic = service->characteristic(uuid);
274 if (characteristic.isValid()) {
275 return characteristic;
276 }
277
279 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
280 ServiceDiscovered
281 #else
282 RemoteServiceDiscovered
283 #endif
284 ) {
285 qCWarning(lc).noquote() << tr(R"(Characteristic %1 "%2" requested before service %3 "%4" discovered.)")
288 qCInfo(lc).noquote() << tr("Current service state:") << service->state();
290 }
291
292 qCWarning(lc).noquote() << tr(R"(Characteristic %1 "%2" not found in service %3 "%4".)")
296}
297
298/*!
299 * Read the \a uuid characteristic.
300 *
301 * If succesful, the `QLowEnergyService::characteristicRead` signal will be emitted by the internal
302 * service object. For convenience, derived classes should implement the characteristicRead()
303 * virtual function to handle the read value.
304 *
305 * Returns \c true if the characteristic read request was successfully queued, \c false otherwise.
306 *
307 * \see AbstractPokitService::readCharacteristics()
308 * \see AbstractPokitServicePrivate::characteristicRead()
309 */
311{
312 const QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
313 if (!characteristic.isValid()) {
314 return false;
315 }
316 qCDebug(lc).noquote() << tr(R"(Reading characteristic %1 "%2".)")
318 service->readCharacteristic(characteristic);
319 return true;
320}
321
322/*!
323 * Enables client (Pokit device) side notification for characteristic \a uuid.
324 *
325 * Returns \c true if the notication enable request was successfully queued, \c false otherwise.
326 *
327 * \see AbstractPokitServicePrivate::characteristicChanged
328 * \see AbstractPokitServicePrivate::disableCharacteristicNotificatons
329 */
331{
332 qCDebug(lc).noquote() << tr(R"(Enabling CCCD for characteristic %1 "%2".)")
334 QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
335 if (!characteristic.isValid()) {
336 return false;
337 }
338
339 QLowEnergyDescriptor descriptor = characteristic.descriptor(
340 QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
341 if (!descriptor.isValid()) {
342 qCWarning(lc).noquote() << tr(R"(Characteristic %1 "%2" has no client configuration descriptor.)")
344 return false;
345 }
346
347 service->writeDescriptor(descriptor,
348 #if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0))
349 QLowEnergyCharacteristic::CCCDEnableNotification
350 #else
351 QByteArray::fromHex("0100") // See Qt6's QLowEnergyCharacteristic::CCCDEnableNotification.
352 #endif
353 );
354 return true;
355}
356
357/*!
358 * Disables client (Pokit device) side notification for characteristic \a uuid.
359 *
360 * Returns \c true if the notication disable request was successfully queued, \c false otherwise.
361 *
362 * \see AbstractPokitServicePrivate::characteristicChanged
363 * \see AbstractPokitServicePrivate::enableCharacteristicNotificatons
364 */
366{
367 qCDebug(lc).noquote() << tr(R"(Disabling CCCD for characteristic %1 "%2".)")
369 QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
370 if (!characteristic.isValid()) {
371 return false;
372 }
373
374 QLowEnergyDescriptor descriptor = characteristic.descriptor(
375 QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
376 if (!descriptor.isValid()) {
377 qCWarning(lc).noquote() << tr(R"(Characteristic %1 "%2" has no client configuration descriptor.)")
379 return false;
380 }
381
382 service->writeDescriptor(descriptor,
383 #if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0))
384 QLowEnergyCharacteristic::CCCDDisable
385 #else
386 QByteArray::fromHex("0000") // See Qt6's QLowEnergyCharacteristic::CCCDDisable.
387 #endif
388 );
389 return true;
390}
391
392/*!
393 * Returns `false` if \a data is smaller than \a minSize, otherwise returns \a failOnMax if \a data
394 * is bigger than \a maxSize, otherwise returns `true`.
395 *
396 * A warning is logged if either \a minSize or \a maxSize is violated, regardless of the returned
397 * value; ie this funcion can be used to simply warn if \a data is too big, or it can be used to
398 * failed (return `false`) in that case.
399 */
401 const int minSize, const int maxSize,
402 const bool failOnMax)
403{
404 if (data.size() < minSize) {
405 qCWarning(lc).noquote() << tr("%1 requires %n byte/s, but only %2 present: %3", nullptr, minSize)
406 .arg(label).arg(data.size()).arg(toHexString(data));
407 return false;
408 }
409 if ((maxSize >= 0) && (data.size() > maxSize)) {
410 qCWarning(lc).noquote() << tr("%1 has %n extraneous byte/s: %2", nullptr, data.size()-maxSize)
411 .arg(label, toHexString(data.mid(maxSize)));
412 return (!failOnMax);
413 }
414 return true;
415}
416
417/*!
418 * Returns up to \a maxSize bytes of \a data as a human readable hexadecimal string. If \a data
419 * exceeds \a maxSize, then \a data is elided in the middle. For example:
420 *
421 * ```
422 * toHex(QBytArray("\x1\x2\x3\x4\x5\x6", 4); // "0x01,02,...,05,06"
423 * ```
424 */
426{
427 return (data.size() <= maxSize)
428 ? QString::fromLatin1("0x%1").arg(QLatin1String(data.toHex(',')))
429 : QString::fromLatin1("0x%1,...,%2").arg(
430 QLatin1String(data.left(maxSize/2-1).toHex(',')),
431 QLatin1String(data.right(maxSize/2-1).toHex(',')));
432}
433
434/*!
435 * Handles `QLowEnergyController::connected` events.
436 *
437 * If `autoDiscover` is enabled, this will begin service discovery on the newly connected contoller.
438 *
439 * \see AbstractPokitService::autoDiscover()
440 */
442{
443 if (!controller) {
444 qCWarning(lc).noquote() << tr("Connected with no controller set") << sender();
445 return;
446 }
447
448 qCDebug(lc).noquote() << tr(R"(Connected to "%1" (%2) at %3.)").arg(
451 if (autoDiscover) {
453 }
454}
455
456/*!
457 * Handles `QLowEnergyController::discoveryFinished` events.
458 *
459 * As this event indicates that the conroller has finished discovering services, this function will
460 * invoke createServiceObject() to create the internal service object (if not already created).
461 */
463{
464 if (!controller) {
465 qCWarning(lc).noquote() << tr("Discovery finished with no controller set") << sender();
466 return;
467 }
468
469 qCDebug(lc).noquote() << tr(R"(Discovery finished for "%1" (%2) at %3.)").arg(
472
473 if (!createServiceObject()) {
474 qCWarning(lc).noquote() << tr("Discovery finished, but service not found.");
476 Q_EMIT q->serviceErrorOccurred(QLowEnergyService::ServiceError::UnknownError);
477 }
478}
479
480/*!
481 * Handles `QLowEnergyController::errorOccurred` events.
482 *
483 * This function simply re-emits \a newError as AbstractPokitService::serviceErrorOccurred.
484 */
486{
488 qCDebug(lc).noquote() << tr("Service error") << newError;
489 Q_EMIT q->serviceErrorOccurred(newError);
490}
491
492/*!
493 * Handles `QLowEnergyController::serviceDiscovered` events.
494 *
495 * If the discovered service is the one this (or rather the derived) class wraps, then
496 * createServiceObject() will be invoked immediately (otherwise it will be invoked after full
497 * service discovery has completed, ie in discoveryFinished()).
498 */
500{
501 if ((!service) && (newService == serviceUuid)) {
502 qCDebug(lc).noquote() << tr("Service discovered") << newService;
504 }
505}
506
507/*!
508 * Handles `QLowEnergyController::stateChanged` events.
509 *
510 * If \a newState indicates that service details have now been discovered, then
511 * AbstractPokitService::serviceDetailsDiscovered will be emitted.
512 *
513 * \see AbstractPokitService::autoDiscover()
514 */
516{
517 qCDebug(lc).noquote() << tr("State changed to") << newState;
518
519 if (lc().isDebugEnabled()) {
520 for (const auto &characteristic: service->characteristics()) {
521 QStringList properties;
522 /// \cond no-doxygen
523 #define QTPOKIT_INTERNAL_TEST_AND_APPEND(property) \
524 if (characteristic.properties().testFlag(QLowEnergyCharacteristic::property)) { \
525 properties.append(QStringLiteral(#property).toLower());\
526 }
527 /// \endcond
528 QTPOKIT_INTERNAL_TEST_AND_APPEND(Broadcasting)
529 QTPOKIT_INTERNAL_TEST_AND_APPEND(Read)
530 QTPOKIT_INTERNAL_TEST_AND_APPEND(WriteNoResponse)
531 QTPOKIT_INTERNAL_TEST_AND_APPEND(Write)
532 QTPOKIT_INTERNAL_TEST_AND_APPEND(Notify)
533 QTPOKIT_INTERNAL_TEST_AND_APPEND(Indicate)
534 QTPOKIT_INTERNAL_TEST_AND_APPEND(WriteSigned)
535 QTPOKIT_INTERNAL_TEST_AND_APPEND(ExtendedProperty)
536 #undef QTPOKIT_INTERNAL_TEST_AND_APPEND
537 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" supports %3.)").arg(characteristic.uuid().toString(),
538 PokitDevice::charcteristicToString(characteristic.uuid()), properties.join(QStringLiteral(", ")));
539 }
540 }
541
542 if (newState == QLowEnergyService::
543 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
544 ServiceDiscovered
545 #else
546 RemoteServiceDiscovered
547 #endif
548 ) {
550 qCDebug(lc).noquote() << tr("Service details discovered.");
551 Q_EMIT q->serviceDetailsDiscovered();
552 }
553}
554
555/*!
556 * Handles `QLowEnergyService::characteristicRead` events. This base implementation simply debug
557 * logs the event.
558 *
559 * Derived classes should implement this function to handle the successful reads of
560 * \a characteristic, typically by parsing \a value, then emitting a speciailised signal.
561 */
563 const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
564{
565 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" read %n byte/s: %3)", nullptr, value.size()).arg(
566 characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()), toHexString(value));
567}
568
569/*!
570 * Handles `QLowEnergyService::characteristicWritten` events. This base implementation simply debug
571 * logs the event.
572 *
573 * Derived classes should implement this function to handle the successful writes of
574 * \a characteristic, typically by parsing \a newValue, then emitting a speciailised signal.
575 */
577 const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
578{
579 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" written with %Ln byte/s: %3)", nullptr, newValue.size())
580 .arg(characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()), toHexString(newValue));
581}
582
583/*!
584 * Handles `QLowEnergyService::characteristicChanged` events. This base implementation simply debug
585 * logs the event.
586 *
587 * If derived classes support characteristics with client-side notification (ie Notify, as opposed
588 * to Read or Write operations), they should implement this function to handle the successful reads of
589 * \a characteristic, typically by parsing \a value, then emitting a speciailised signal.
590 */
592 const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
593{
594 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" changed to %Ln byte/s: %3)", nullptr, newValue.size())
595 .arg(characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()), toHexString(newValue));
596}
597
598/// \endcond
Declares the AbstractPokitService class.
Declares the AbstractPokitServicePrivate class.
The AbstractPokitServicePrivate class provides private implementation for AbstractPokitService.
bool autoDiscover
Whether autodiscovery is enabled or not.
bool disableCharacteristicNotificatons(const QBluetoothUuid &uuid)
Disables client (Pokit device) side notification for characteristic uuid.
bool createServiceObject()
Creates an internal service object from the internal controller.
void discoveryFinished()
Handles QLowEnergyController::discoveryFinished events.
QBluetoothUuid serviceUuid
UUIDs for service.
virtual void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicChanged events.
virtual void serviceDiscovered(const QBluetoothUuid &newService)
Handles QLowEnergyController::serviceDiscovered events.
AbstractPokitServicePrivate(const QBluetoothUuid &serviceUuid, QLowEnergyController *controller, AbstractPokitService *const q)
bool enableCharacteristicNotificatons(const QBluetoothUuid &uuid)
Enables client (Pokit device) side notification for characteristic uuid.
virtual void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
Handles QLowEnergyService::characteristicRead events.
void connected()
Handles QLowEnergyController::connected events.
QLowEnergyCharacteristic getCharacteristic(const QBluetoothUuid &uuid) const
Get uuid characteristc from the underlying service.
virtual void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicWritten events.
void errorOccurred(const QLowEnergyService::ServiceError newError)
Handles QLowEnergyController::errorOccurred events.
void stateChanged(QLowEnergyService::ServiceState newState)
Handles QLowEnergyController::stateChanged events.
bool readCharacteristic(const QBluetoothUuid &uuid)
Read the uuid characteristic.
QLowEnergyService * service
BLE service to read/write characteristics.
QLowEnergyController * controller
BLE controller to fetch the service from.
static QString toHexString(const QByteArray &data, const int maxSize=20)
Returns up to maxSize bytes of data as a human readable hexadecimal string.
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
The Pokit product controller is connected to.
The AbstractPokitService class provides a common base for Pokit services classes.
std::optional< PokitProduct > pokitProduct() const
Returns the Pokit product this service is attached to.
bool autoDiscover() const
Returns true if autodiscovery of services and service details is enabled, false otherwise.
QLowEnergyService * service()
Returns a non-const pointer to the internal service object, if any.
void setAutoDiscover(const bool discover=true)
If discover is true, autodiscovery will be attempted.
void setPokitProduct(const PokitProduct product)
Sets the current Pokit product.
AbstractPokitServicePrivate * d_ptr
Internal d-pointer.
virtual ~AbstractPokitService()
Destroys this AbstractPokitService object.
static QString charcteristicToString(const QBluetoothUuid &uuid)
Returns a human-readable name for the uuid characteristic, or a null QString if unknown.
static QString serviceToString(const QBluetoothUuid &uuid)
Returns a human-readable name for the uuid service, or a null QString if unknonw.
Declares the PokitDevice class.
PokitProduct
Pokit products known to, and supported by, the QtPokit library.
QTPOKIT_EXPORT QString toString(const PokitProduct product)
Returns product as user-friendly string.
QString toString() const const
QByteArray fromHex(const QByteArray &hexEncoded)
QByteArray left(int len) const const
QByteArray mid(int pos, int len) const const
QByteArray right(int len) const const
int size() const const
QByteArray toHex() const const
QLowEnergyDescriptor descriptor(const QBluetoothUuid &uuid) const const
bool isValid() const const
QBluetoothUuid uuid() const const
QLowEnergyService * createServiceObject(const QBluetoothUuid &serviceUuid, QObject *parent)
QBluetoothAddress remoteAddress() const const
QBluetoothUuid remoteDeviceUuid() const const
QString remoteName() const const
void serviceDiscovered(const QBluetoothUuid &newService)
bool isValid() const const
QString name() const const
QBluetoothUuid uuid() const const
QLowEnergyCharacteristic characteristic(const QBluetoothUuid &uuid) const const
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
QList< QLowEnergyCharacteristic > characteristics() const const
void descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &value)
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
QLowEnergyService::ServiceError error() const const
void readCharacteristic(const QLowEnergyCharacteristic &characteristic)
QBluetoothUuid serviceUuid() const const
QLowEnergyService::ServiceState state() const const
void stateChanged(QLowEnergyService::ServiceState newState)
void writeDescriptor(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * sender() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString fromLatin1(const char *str, int size)
QString join(const QString &separator) const const
bool isNull() const const
QString toString() const const