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