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 AbstractPokitService and AbstractPokitServicePrivate classes.
7 : */
8 :
9 : #include <qtpokit/abstractpokitservice.h>
10 : #include "abstractpokitservice_p.h"
11 :
12 : #include <qtpokit/pokitdevice.h>
13 :
14 : #include <QLowEnergyController>
15 :
16 : /*!
17 : * \class AbstractPokitService
18 : *
19 : * The AbstractPokitService class provides a common base for Pokit services classes.
20 : */
21 :
22 : /*!
23 : * \cond internal
24 : * Constructs a new Pokit service with \a parent, and private implementation \a d.
25 : */
26 2209 : AbstractPokitService::AbstractPokitService(
27 2209 : AbstractPokitServicePrivate * const d, QObject * const parent)
28 2209 : : QObject(parent), d_ptr(d)
29 : {
30 :
31 2209 : }
32 : /// \endcond
33 :
34 : /*!
35 : * Destroys this AbstractPokitService object.
36 : */
37 1632 : AbstractPokitService::~AbstractPokitService()
38 : {
39 1632 : delete d_ptr;
40 1632 : }
41 :
42 : /*!
43 : * \fn virtual bool AbstractPokitService::readCharacteristics() = 0
44 : *
45 : * Read all characteristics.
46 : *
47 : * This convenience function will queue refresh requests of all characteristics supported by this
48 : * service.
49 : *
50 : * Relevant `*Service::*Read` signals will be emitted by derived class objects as each
51 : * characteristic is successfully read.
52 : */
53 :
54 : /*!
55 : * Returns `true` if autodiscovery of services and service details is enabled, `false` otherwise.
56 : *
57 : * \see setAutoDiscover for more information on what autodiscovery provides.
58 : */
59 51 : bool AbstractPokitService::autoDiscover() const
60 : {
61 : Q_D(const AbstractPokitService);
62 51 : return d->autoDiscover;
63 : }
64 :
65 : /*!
66 : * If \a discover is \c true, autodiscovery will be attempted.
67 : *
68 : * Specifically, this may resulting in automatic invocation of:
69 : * * QLowEnergyController::discoverServices if/when the internal controller is connected; and
70 : * * QLowEnergyService::discoverDetails if/when an internal service object is created.
71 : *
72 : * \see autoDiscover
73 : */
74 34 : void AbstractPokitService::setAutoDiscover(const bool discover)
75 : {
76 : Q_D(AbstractPokitService);
77 34 : d->autoDiscover = discover;
78 34 : }
79 :
80 : /*!
81 : * Returns a non-const pointer to the internal service object, if any.
82 : */
83 102 : QLowEnergyService * AbstractPokitService::service()
84 : {
85 : Q_D(AbstractPokitService);
86 102 : return d->service;
87 : }
88 :
89 : /*!
90 : * Returns a const pointer to the internal service object, if any.
91 : */
92 17 : const QLowEnergyService * AbstractPokitService::service() const
93 : {
94 : Q_D(const AbstractPokitService);
95 17 : return d->service;
96 : }
97 :
98 : /*!
99 : * \fn void AbstractPokitService::serviceDetailsDiscovered()
100 : *
101 : * This signal is emitted when the Pokit service details have been discovered.
102 : *
103 : * Once this signal has been emitted, cached characteristics values should be immediately available
104 : * via derived classes' accessor functions, and refreshes can be queued via readCharacteristics()
105 : * and any related read functions provided by derived classes.
106 : */
107 :
108 : /*!
109 : * \fn void AbstractPokitService::serviceErrorOccurred(QLowEnergyService::ServiceError newError)
110 : *
111 : * This signal is emitted whenever an error occurs on the underlying QLowEnergyService.
112 : */
113 :
114 : /*!
115 : * \cond internal
116 : * \class AbstractPokitServicePrivate
117 : *
118 : * The AbstractPokitServicePrivate class provides private implementation for AbstractPokitService.
119 : */
120 :
121 : /*!
122 : * \internal
123 : * Constructs a new AbstractPokitServicePrivate object with public implementation \a q.
124 : *
125 : * Note, typically the \a serviceUuid should be set validly, however, in the rare case that a
126 : * service's UUID can vary (ie the Status Service), \a serviceUuid may be set to a `null`
127 : * QBluetoothUuid here, and updated when the correct service UUID is known.
128 : *
129 : * \see StatusService::ServiceUuids
130 : * \see StatusServicePrivate::serviceDiscovered
131 : */
132 2209 : AbstractPokitServicePrivate::AbstractPokitServicePrivate(const QBluetoothUuid &serviceUuid,
133 2209 : QLowEnergyController * controller, AbstractPokitService * const q)
134 2209 : : autoDiscover(true), controller(controller), service(nullptr), serviceUuid(serviceUuid),
135 2209 : q_ptr(q)
136 : {
137 2209 : if (controller) {
138 492 : connect(controller, &QLowEnergyController::connected,
139 : this, &AbstractPokitServicePrivate::connected);
140 :
141 492 : connect(controller, &QLowEnergyController::discoveryFinished,
142 : this, &AbstractPokitServicePrivate::discoveryFinished);
143 :
144 492 : connect(controller, &QLowEnergyController::serviceDiscovered,
145 : this, &AbstractPokitServicePrivate::serviceDiscovered);
146 :
147 492 : createServiceObject();
148 : }
149 :
150 2209 : }
151 :
152 : /*!
153 : * Creates an internal service object from the internal controller.
154 : *
155 : * Any existing service object will *not* be replaced.
156 : *
157 : * Returns \c true if a service was created successfully, either now, or sometime previously.
158 : */
159 611 : bool AbstractPokitServicePrivate::createServiceObject()
160 : {
161 611 : if (!controller) {
162 : return false;
163 : }
164 :
165 543 : if (service) {
166 17 : qCDebug(lc).noquote() << tr("Already have service object:") << service;
167 17 : return true;
168 : }
169 :
170 526 : if (serviceUuid.isNull()) {
171 170 : qCDebug(lc).noquote() << tr("Service UUID not assigned yet.");
172 170 : return false;
173 : }
174 :
175 356 : service = controller->createServiceObject(serviceUuid, this);
176 356 : if (!service) {
177 : return false;
178 : }
179 0 : qCDebug(lc).noquote() << tr("Service object created") << service;
180 :
181 0 : connect(service, &QLowEnergyService::stateChanged,
182 : this, &AbstractPokitServicePrivate::stateChanged);
183 0 : connect(service, &QLowEnergyService::characteristicRead,
184 : this, &AbstractPokitServicePrivate::characteristicRead);
185 0 : connect(service, &QLowEnergyService::characteristicWritten,
186 : this, &AbstractPokitServicePrivate::characteristicWritten);
187 0 : connect(service, &QLowEnergyService::characteristicChanged,
188 : this, &AbstractPokitServicePrivate::characteristicChanged);
189 :
190 0 : connect(service, &QLowEnergyService::descriptorRead,
191 0 : [](const QLowEnergyDescriptor &descriptor, const QByteArray &value){
192 0 : qCDebug(lc).noquote() << tr("Descriptor \"%1\" (%2) read.")
193 0 : .arg(descriptor.name(), descriptor.uuid().toString());
194 : Q_UNUSED(value);
195 0 : });
196 :
197 0 : connect(service, &QLowEnergyService::descriptorWritten,
198 0 : [](const QLowEnergyDescriptor &descriptor, const QByteArray &newValue){
199 0 : qCDebug(lc).noquote() << tr("Descriptor \"%1\" (%2) written.")
200 0 : .arg(descriptor.name(), descriptor.uuid().toString());
201 : Q_UNUSED(newValue);
202 0 : });
203 :
204 0 : connect(service,
205 : #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 0))
206 : QOverload<QLowEnergyService::ServiceError>::of(&QLowEnergyService::error),
207 : #else
208 : &QLowEnergyService::errorOccurred,
209 : #endif
210 : this, &AbstractPokitServicePrivate::errorOccurred);
211 :
212 0 : if (autoDiscover) {
213 0 : service->discoverDetails();
214 : }
215 : return true;
216 : }
217 :
218 : /*!
219 : * Get \a uuid characteristc from the underlying service. This helper function is equivalent to
220 : *
221 : * ```
222 : * return service->characteristic(uuid);
223 : * ```
224 : *
225 : * except that it performs some sanity checks, such as checking the service object pointer has been
226 : * assigned first, and also logs failures in a consistent manner.
227 : *
228 : * \param uuid
229 : * \return
230 : */
231 2987 : QLowEnergyCharacteristic AbstractPokitServicePrivate::getCharacteristic(const QBluetoothUuid &uuid) const
232 : {
233 2987 : if (!service) {
234 2987 : qCDebug(lc).noquote() << tr("Characterisitc %1 \"%2\" requested before service assigned.")
235 0 : .arg(uuid.toString(), PokitDevice::charcteristicToString(uuid));
236 2987 : return QLowEnergyCharacteristic();
237 : }
238 :
239 0 : const QLowEnergyCharacteristic characteristic = service->characteristic(uuid);
240 0 : if (characteristic.isValid()) {
241 0 : return characteristic;
242 : }
243 :
244 0 : if (service->state() != QLowEnergyService::
245 : #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
246 : ServiceDiscovered
247 : #else
248 : RemoteServiceDiscovered
249 : #endif
250 : ) {
251 0 : qCWarning(lc).noquote() << tr("Characterisitc %1 \"%2\" requested before service %3 \"%4\" discovered.")
252 0 : .arg(uuid.toString(), PokitDevice::charcteristicToString(uuid),
253 0 : service->serviceUuid().toString(), PokitDevice::serviceToString(service->serviceUuid()));
254 0 : qCInfo(lc).noquote() << tr("Current service state:") << service->state();
255 0 : return QLowEnergyCharacteristic();
256 : }
257 :
258 0 : qCWarning(lc).noquote() << tr("Characterisitc %1 \"%2\" not found in service %3 \"%4\".")
259 0 : .arg(uuid.toString(), PokitDevice::charcteristicToString(uuid),
260 0 : service->serviceUuid().toString(), PokitDevice::serviceToString(service->serviceUuid()));
261 0 : return QLowEnergyCharacteristic();
262 0 : }
263 :
264 : /*!
265 : * Read the \a uuid characteristic.
266 : *
267 : * If succesful, the `QLowEnergyService::characteristicRead` signal will be emitted by the internal
268 : * service object. For convenience, derived classes should implement the characteristicRead()
269 : * virtual function to handle the read value.
270 : *
271 : * Returns \c true if the characteristic read request was successfully queued, \c false otherwise.
272 : *
273 : * \see AbstractPokitService::readCharacteristics()
274 : * \see AbstractPokitServicePrivate::characteristicRead()
275 : */
276 459 : bool AbstractPokitServicePrivate::readCharacteristic(const QBluetoothUuid &uuid)
277 : {
278 459 : const QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
279 459 : if (!characteristic.isValid()) {
280 : return false;
281 : }
282 0 : qCDebug(lc).noquote() << tr("Reading characteristic %1 \"%2\".")
283 0 : .arg(uuid.toString(), PokitDevice::charcteristicToString(uuid));
284 0 : service->readCharacteristic(characteristic);
285 : return true;
286 459 : }
287 :
288 : /*!
289 : * Enables client (Pokit device) side notification for characteristic \a uuid.
290 : *
291 : * Returns \c true if the notication enable request was successfully queued, \c false otherwise.
292 : *
293 : * \see AbstractPokitServicePrivate::characteristicChanged
294 : * \see AbstractPokitServicePrivate::disableCharacteristicNotificatons
295 : */
296 102 : bool AbstractPokitServicePrivate::enableCharacteristicNotificatons(const QBluetoothUuid &uuid)
297 : {
298 102 : qCDebug(lc).noquote() << tr("Enabling CCCD for characteristic %1 \"%2\".")
299 0 : .arg(uuid.toString(), PokitDevice::charcteristicToString(uuid));
300 102 : QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
301 102 : if (!characteristic.isValid()) {
302 : return false;
303 : }
304 :
305 : QLowEnergyDescriptor descriptor = characteristic.descriptor(
306 0 : QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
307 0 : if (!descriptor.isValid()) {
308 0 : qCWarning(lc).noquote() << tr("Characterisitc %1 \"%2\" has no client configuration descriptor.")
309 0 : .arg(uuid.toString(), PokitDevice::charcteristicToString(uuid));
310 0 : return false;
311 : }
312 :
313 0 : service->writeDescriptor(descriptor,
314 : #if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0))
315 : QLowEnergyCharacteristic::CCCDEnableNotification
316 : #else
317 0 : QByteArray::fromHex("0100") // See Qt6's QLowEnergyCharacteristic::CCCDEnableNotification.
318 : #endif
319 : );
320 0 : return true;
321 102 : }
322 :
323 : /*!
324 : * Disables client (Pokit device) side notification for characteristic \a uuid.
325 : *
326 : * Returns \c true if the notication disable request was successfully queued, \c false otherwise.
327 : *
328 : * \see AbstractPokitServicePrivate::characteristicChanged
329 : * \see AbstractPokitServicePrivate::enableCharacteristicNotificatons
330 : */
331 102 : bool AbstractPokitServicePrivate::disableCharacteristicNotificatons(const QBluetoothUuid &uuid)
332 : {
333 102 : qCDebug(lc).noquote() << tr("Disabling CCCD for characteristic %1 \"%2\".")
334 0 : .arg(uuid.toString(), PokitDevice::charcteristicToString(uuid));
335 102 : QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
336 102 : if (!characteristic.isValid()) {
337 : return false;
338 : }
339 :
340 : QLowEnergyDescriptor descriptor = characteristic.descriptor(
341 0 : QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
342 0 : if (!descriptor.isValid()) {
343 0 : qCWarning(lc).noquote() << tr("Characterisitc %1 \"%2\" has no client configuration descriptor.")
344 0 : .arg(uuid.toString(), PokitDevice::charcteristicToString(uuid));
345 0 : return false;
346 : }
347 :
348 0 : service->writeDescriptor(descriptor,
349 : #if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0))
350 : QLowEnergyCharacteristic::CCCDDisable
351 : #else
352 0 : QByteArray::fromHex("0000") // See Qt6's QLowEnergyCharacteristic::CCCDDisable.
353 : #endif
354 : );
355 0 : return true;
356 102 : }
357 :
358 : /*!
359 : * Returns `false` if \a data is smaller than \a minSize, otherwise returns \a failOnMax if \a data
360 : * is bigger than \a maxSize, otherwise returns `true`.
361 : *
362 : * A warning is logged if either \a minSize or \a maxSize is violated, regardless of the returned
363 : * value; ie this funcion can be used to simply warn if \a data is too big, or it can be used to
364 : * failed (return `false`) in that case.
365 : */
366 578 : bool AbstractPokitServicePrivate::checkSize(const QString &label, const QByteArray &data,
367 : const int minSize, const int maxSize,
368 : const bool failOnMax)
369 : {
370 578 : if (data.size() < minSize) {
371 442 : qCWarning(lc).noquote() << tr("%1 requires %2 bytes, but only %3 present: %4")
372 481 : .arg(label).arg(minSize).arg(data.size()).arg(toHexString(data));
373 221 : return false;
374 : }
375 357 : if ((maxSize >= 0) && (data.size() > maxSize)) {
376 238 : qCWarning(lc).noquote() << tr("%1 has %2 extraneous bytes: %3")
377 140 : .arg(label).arg(data.size()-maxSize).arg(toHexString(data.mid(maxSize)));
378 119 : return (!failOnMax);
379 : }
380 : return true;
381 : }
382 :
383 : /*!
384 : * Returns up to \a maxSize bytes of \a data as a human readable hexadecimal string. If \a data
385 : * exceeds \a maxSize, then \a data is elided in the middle. For example:
386 : *
387 : * ```
388 : * toHex(QBytArray("\x1\x2\x3\x4\x5\x6", 4); // "0x01,02,...,05,06"
389 : * ```
390 : */
391 476 : QString AbstractPokitServicePrivate::toHexString(const QByteArray &data, const int maxSize)
392 : {
393 84 : return (data.size() <= maxSize)
394 1125 : ? QString::fromLatin1("0x%1").arg(QLatin1String(data.toHex(',')))
395 51 : : QString::fromLatin1("0x%1,...,%2").arg(
396 536 : QLatin1String(data.left(maxSize/2-1).toHex(',')),
397 2398 : QLatin1String(data.right(maxSize/2-1).toHex(',')));
398 : }
399 :
400 : /*!
401 : * Handles `QLowEnergyController::connected` events.
402 : *
403 : * If `autoDiscover` is enabled, this will begin service discovery on the newly connected contoller.
404 : *
405 : * \see AbstractPokitService::autoDiscover()
406 : */
407 17 : void AbstractPokitServicePrivate::connected()
408 : {
409 17 : if (!controller) {
410 34 : qCWarning(lc).noquote() << tr("Connected with no controller set") << sender();
411 17 : return;
412 : }
413 :
414 0 : qCDebug(lc).noquote() << tr("Connected to \"%1\" (%2) at %3.").arg(
415 0 : controller->remoteName(), controller->remoteDeviceUuid().toString(),
416 0 : controller->remoteAddress().toString());
417 0 : if (autoDiscover) {
418 0 : controller->discoverServices();
419 : }
420 : }
421 :
422 : /*!
423 : * Handles `QLowEnergyController::discoveryFinished` events.
424 : *
425 : * As this event indicates that the conroller has finished discovering services, this function will
426 : * invoke createServiceObject() to create the internal service object (if not already created).
427 : */
428 17 : void AbstractPokitServicePrivate::discoveryFinished()
429 : {
430 17 : if (!controller) {
431 34 : qCWarning(lc).noquote() << tr("Discovery finished with no controller set") << sender();
432 17 : return;
433 : }
434 :
435 0 : qCDebug(lc).noquote() << tr("Discovery finished for \"%1\" (%2) at %3.").arg(
436 0 : controller->remoteName(), controller->remoteDeviceUuid().toString(),
437 0 : controller->remoteAddress().toString());
438 :
439 0 : if (!createServiceObject()) {
440 0 : qCWarning(lc).noquote() << tr("Discovery finished, but service not found.");
441 : Q_Q(AbstractPokitService);
442 0 : emit q->serviceErrorOccurred(QLowEnergyService::ServiceError::UnknownError);
443 : }
444 : }
445 :
446 : /*!
447 : * Handles `QLowEnergyController::errorOccurred` events.
448 : *
449 : * This function simply re-emits \a newError as AbstractPokitService::serviceErrorOccurred.
450 : */
451 17 : void AbstractPokitServicePrivate::errorOccurred(const QLowEnergyService::ServiceError newError)
452 : {
453 : Q_Q(AbstractPokitService);
454 17 : qCDebug(lc).noquote() << tr("Service error") << newError;
455 17 : emit q->serviceErrorOccurred(newError);
456 17 : }
457 :
458 : /*!
459 : * Handles `QLowEnergyController::serviceDiscovered` events.
460 : *
461 : * If the discovered service is the one this (or rather the derived) class wraps, then
462 : * createServiceObject() will be invoked immediately (otherwise it will be invoked after full
463 : * service discovery has completed, ie in discoveryFinished()).
464 : */
465 102 : void AbstractPokitServicePrivate::serviceDiscovered(const QBluetoothUuid &newService)
466 : {
467 102 : if ((!service) && (newService == serviceUuid)) {
468 51 : qCDebug(lc).noquote() << tr("Service discovered") << newService;
469 51 : createServiceObject();
470 : }
471 102 : }
472 :
473 : /*!
474 : * Handles `QLowEnergyController::stateChanged` events.
475 : *
476 : * If \a newState indicates that service details have now been discovered, then
477 : * AbstractPokitService::serviceDetailsDiscovered will be emitted.
478 : *
479 : * \see AbstractPokitService::autoDiscover()
480 : */
481 51 : void AbstractPokitServicePrivate::stateChanged(QLowEnergyService::ServiceState newState)
482 : {
483 51 : qCDebug(lc).noquote() << tr("State changed to") << newState;
484 :
485 51 : if (newState == QLowEnergyService::
486 : #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
487 : ServiceDiscovered
488 : #else
489 : RemoteServiceDiscovered
490 : #endif
491 : ) {
492 : Q_Q(AbstractPokitService);
493 17 : qCDebug(lc).noquote() << tr("Service details discovered.");
494 17 : emit q->serviceDetailsDiscovered();
495 : }
496 51 : }
497 :
498 : /*!
499 : * Handles `QLowEnergyService::characteristicRead` events. This base implementation simply debug
500 : * logs the event.
501 : *
502 : * Derived classes should implement this function to handle the successful reads of
503 : * \a characteristic, typically by parsing \a value, then emitting a speciailised signal.
504 : */
505 119 : void AbstractPokitServicePrivate::characteristicRead(
506 : const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
507 : {
508 119 : qCDebug(lc).noquote() << tr("Characteristic %1 \"%2\" read %3 bytes: %4").arg(
509 0 : characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()))
510 0 : .arg(value.size()).arg(toHexString(value));
511 119 : }
512 :
513 : /*!
514 : * Handles `QLowEnergyService::characteristicWritten` events. This base implementation simply debug
515 : * logs the event.
516 : *
517 : * Derived classes should implement this function to handle the successful writes of
518 : * \a characteristic, typically by parsing \a newValue, then emitting a speciailised signal.
519 : */
520 119 : void AbstractPokitServicePrivate::characteristicWritten(
521 : const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
522 : {
523 119 : qCDebug(lc).noquote() << tr("Characteristic %1 \"%2\" written with %L3 bytes: %4").arg(
524 0 : characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()))
525 0 : .arg(newValue.size()).arg(toHexString(newValue));
526 119 : }
527 :
528 : /*!
529 : * Handles `QLowEnergyService::characteristicChanged` events. This base implementation simply debug
530 : * logs the event.
531 : *
532 : * If derived classes support characteristics with client-side notification (ie Notify, as opposed
533 : * to Read or Write operations), they should implement this function to handle the successful reads of
534 : * \a characteristic, typically by parsing \a value, then emitting a speciailised signal.
535 : */
536 68 : void AbstractPokitServicePrivate::characteristicChanged(
537 : const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
538 : {
539 68 : qCDebug(lc).noquote() << tr("Characteristic %1 \"%2\" changed to %L3 bytes: %4").arg(
540 0 : characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()))
541 0 : .arg(newValue.size()).arg(toHexString(newValue));
542 68 : }
543 :
544 : /// \endcond
|