Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
btcentralmanager.mm
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
8#include "btnotifier_p.h"
9
10#include <QtCore/qloggingcategory.h>
11#include <QtCore/qdebug.h>
12#include <QtCore/qmap.h>
13
14#include <algorithm>
15#include <vector>
16#include <limits>
17
19
21
22namespace DarwinBluetooth {
23
25{
26 // Identify, how many characteristics/descriptors we have on a given service,
27 // +1 for the service itself.
28 // No checks if NSUInteger is big enough :)
29 // Let's assume such number of entries is not possible :)
30
31 Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)");
32
34
35 NSArray *const cs = service.characteristics;
36 if (!cs || !cs.count)
37 return 1;
38
39 NSUInteger n = 1 + cs.count;
40 for (CBCharacteristic *c in cs) {
41 NSArray *const ds = c.descriptors;
42 if (ds)
43 n += ds.count;
44 }
45
46 return n;
47}
48
49ObjCStrongReference<NSError> qt_timeoutNSError(OperationTimeout type)
50{
51 // For now we do not provide details, since nobody is using this NSError
52 // after all (except callbacks checking if an operation was successful
53 // or not).
56 NSError *nsError = [[NSError alloc] initWithDomain:CBErrorDomain
57 code:CBErrorOperationCancelled
58 userInfo:nil];
59 return ObjCStrongReference<NSError>(nsError, RetainPolicy::noInitialRetain);
60}
61
62auto qt_find_watchdog(const std::vector<GCDTimer> &watchdogs, id object, OperationTimeout type)
63{
64 return std::find_if(watchdogs.begin(), watchdogs.end(), [object, type](const GCDTimer &other){
65 return [other objectUnderWatch] == object && [other timeoutType] == type;});
66}
67
68} // namespace DarwinBluetooth
69
71
73
74@interface DarwinBTCentralManager (PrivateAPI)
75
76- (void)watchAfter:(id)object timeout:(DarwinBluetooth::OperationTimeout)type;
77- (bool)objectIsUnderWatch:(id)object operation:(DarwinBluetooth::OperationTimeout)type;
78- (void)stopWatchingAfter:(id)object operation:(DarwinBluetooth::OperationTimeout)type;
83- (void)readCharacteristics:(CBService *)service;
84- (void)serviceDetailsDiscoveryFinished:(CBService *)service;
88
89// Aux. functions.
90- (CBService *)serviceForUUID:(const QBluetoothUuid &)qtUuid;
91- (CBCharacteristic *)nextCharacteristicForService:(CBService*)service
92 startingFrom:(CBCharacteristic *)from;
93- (CBCharacteristic *)nextCharacteristicForService:(CBService*)service
94 startingFrom:(CBCharacteristic *)from
95 withProperties:(CBCharacteristicProperties)properties;
96- (CBDescriptor *)nextDescriptorForCharacteristic:(CBCharacteristic *)characteristic
97 startingFrom:(CBDescriptor *)descriptor;
98- (CBDescriptor *)descriptor:(const QBluetoothUuid &)dUuid
99 forCharacteristic:(CBCharacteristic *)ch;
100- (bool)cacheWriteValue:(const QByteArray &)value for:(NSObject *)obj;
101- (void)handleReadWriteError:(NSError *)error;
102- (void)reset;
103
104@end
105
107
109{
110@private
111 CBCentralManager *manager;
114
116
118
119 // Quite a verbose service discovery machinery
120 // (a "graph traversal").
122 // The service we're discovering now (included services discovery):
124 // Included services, we'll iterate through at the end of 'servicesToVisit':
126 // We'd like to avoid loops in a services' topology:
128
129 QMap<QBluetoothUuid, DiscoveryMode> servicesToDiscoverDetails;
130
134
136
140
142
144 std::vector<DarwinBluetooth::GCDTimer> timeoutWatchdogs;
145
146 CBPeripheral *peripheral;
148}
149
150- (id)initWith:(DarwinBluetooth::LECBManagerNotifier *)aNotifier
151{
152 using namespace DarwinBluetooth;
153
154 if (self = [super init]) {
155 manager = nil;
157 disconnectPending = false;
158 peripheral = nil;
159 notifier = aNotifier;
160 currentService = 0;
161 lastValidHandle = 0;
162 requestPending = false;
164
165 if (Q_UNLIKELY(!qEnvironmentVariableIsEmpty("BLUETOOTH_GATT_TIMEOUT"))) {
166 bool ok = false;
167 const int value = qEnvironmentVariableIntValue("BLUETOOTH_GATT_TIMEOUT", &ok);
168 if (ok && value >= 0)
170 }
171
172 if (!timeoutMS)
173 timeoutMS = 20000;
174
176 }
177
178 return self;
179}
180
181- (void)dealloc
182{
183 // In the past I had a 'transient delegate': I've seen some crashes
184 // while deleting a manager _before_ its state updated.
185 // Strangely enough, I can not reproduce this anymore, so this
186 // part is simplified now. To be investigated though.
187
188 visitedServices.reset();
189 servicesToVisit.reset();
190 servicesToVisitNext.reset();
191
192 [manager setDelegate:nil];
193 [manager release];
194
195 [peripheral setDelegate:nil];
196 [peripheral release];
197
198 if (notifier)
200
201 [self stopWatchers];
202 [super dealloc];
203}
204
205- (CBPeripheral *)peripheral
206{
207 return peripheral;
208}
209
210- (void)watchAfter:(id)object timeout:(DarwinBluetooth::OperationTimeout)type
211{
212 using namespace DarwinBluetooth;
213
214 GCDTimer newWatcher([[DarwinBTGCDTimer alloc] initWithDelegate:self], RetainPolicy::noInitialRetain);
215 [newWatcher watchAfter:object withTimeoutType:type];
216 timeoutWatchdogs.push_back(newWatcher);
217 [newWatcher startWithTimeout:timeoutMS step:200];
218}
219
220- (bool)objectIsUnderWatch:(id)object operation:(DarwinBluetooth::OperationTimeout)type
221{
223}
224
225- (void)stopWatchingAfter:(id)object operation:(DarwinBluetooth::OperationTimeout)type
226{
228 if (pos != timeoutWatchdogs.end()) {
229 [*pos cancelTimer];
230 timeoutWatchdogs.erase(pos);
231 }
232}
233
235{
236 for (auto &watchdog : timeoutWatchdogs)
237 [watchdog cancelTimer];
238 timeoutWatchdogs.clear();
239}
240
241- (void)timeout:(id)sender
242{
243 Q_UNUSED(sender);
244
245 using namespace DarwinBluetooth;
246
247 DarwinBTGCDTimer *watcher = static_cast<DarwinBTGCDTimer *>(sender);
248 id cbObject = [watcher objectUnderWatch];
249 const OperationTimeout type = [watcher timeoutType];
250
251 Q_ASSERT([self objectIsUnderWatch:cbObject operation:type]);
252
253 NSLog(@"Timeout caused by: %@", cbObject);
254
255 // Note that after this switch the 'watcher' is released (we don't
256 // own it anymore), though GCD is probably still holding a reference.
257 const ObjCStrongReference<NSError> nsError(qt_timeoutNSError(type));
258 switch (type) {
259 case OperationTimeout::serviceDiscovery:
260 qCWarning(QT_BT_DARWIN, "Timeout in services discovery");
261 [self peripheral:peripheral didDiscoverServices:nsError];
262 break;
263 case OperationTimeout::includedServicesDiscovery:
264 qCWarning(QT_BT_DARWIN, "Timeout in included services discovery");
265 [self peripheral:peripheral didDiscoverIncludedServicesForService:cbObject error:nsError];
266 break;
267 case OperationTimeout::characteristicsDiscovery:
268 qCWarning(QT_BT_DARWIN, "Timeout in characteristics discovery");
269 [self peripheral:peripheral didDiscoverCharacteristicsForService:cbObject error:nsError];
270 break;
271 case OperationTimeout::characteristicRead:
272 qCWarning(QT_BT_DARWIN, "Timeout while reading a characteristic");
273 [self peripheral:peripheral didUpdateValueForCharacteristic:cbObject error:nsError];
274 break;
275 case OperationTimeout::descriptorsDiscovery:
276 qCWarning(QT_BT_DARWIN, "Timeout in descriptors discovery");
277 [self peripheral:peripheral didDiscoverDescriptorsForCharacteristic:cbObject error:nsError];
278 break;
279 case OperationTimeout::descriptorRead:
280 qCWarning(QT_BT_DARWIN, "Timeout while reading a descriptor");
281 [self peripheral:peripheral didUpdateValueForDescriptor:cbObject error:nsError];
282 break;
283 case OperationTimeout::characteristicWrite:
284 qCWarning(QT_BT_DARWIN, "Timeout while writing a characteristic with response");
285 [self peripheral:peripheral didWriteValueForCharacteristic:cbObject error:nsError];
286 default:;
287 }
288}
289
290- (void)connectToDevice:(const QBluetoothUuid &)aDeviceUuid
291{
292 disconnectPending = false; // Cancel the previous disconnect if any.
293 deviceUuid = aDeviceUuid;
294
295 if (!manager) {
296 // The first time we try to connect, no manager created yet,
297 // no status update received.
298 if (const dispatch_queue_t leQueue = DarwinBluetooth::qt_LE_queue()) {
300 manager = [[CBCentralManager alloc] initWithDelegate:self queue:leQueue];
301 }
302
303 if (!manager) {
305 qCWarning(QT_BT_DARWIN) << "failed to allocate a central manager";
306 if (notifier)
308 }
310 [self retrievePeripheralAndConnect];
311 }
312}
313
315{
316 Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central manager (nil)");
318 Q_FUNC_INFO, "invalid state");
319
320 if ([self isConnected]) {
321 qCDebug(QT_BT_DARWIN) << "already connected";
322 if (notifier)
324 return;
325 } else if (peripheral) {
326 // Was retrieved already, but not connected
327 // or disconnected.
328 [self connectToPeripheral];
329 return;
330 }
331
332 using namespace DarwinBluetooth;
333
334 // Retrieve a peripheral first ...
335 const ObjCScopedPointer<NSMutableArray> uuids([[NSMutableArray alloc] init], RetainPolicy::noInitialRetain);
336 if (!uuids) {
337 qCWarning(QT_BT_DARWIN) << "failed to allocate identifiers";
338 if (notifier)
340 return;
341 }
342
343
344 const QUuid::Id128Bytes qtUuidData(deviceUuid.toBytes());
345 uuid_t uuidData = {};
346 std::copy(qtUuidData.data, qtUuidData.data + 16, uuidData);
347 const ObjCScopedPointer<NSUUID> nsUuid([[NSUUID alloc] initWithUUIDBytes:uuidData], RetainPolicy::noInitialRetain);
348 if (!nsUuid) {
349 qCWarning(QT_BT_DARWIN) << "failed to allocate NSUUID identifier";
350 if (notifier)
352 return;
353 }
354
355 [uuids addObject:nsUuid];
356 // With the latest CoreBluetooth, we can synchronously retrieve peripherals:
358 NSArray *const peripherals = [manager retrievePeripheralsWithIdentifiers:uuids];
359 if (!peripherals || peripherals.count != 1) {
360 qCWarning(QT_BT_DARWIN) << "failed to retrieve a peripheral";
361 if (notifier)
363 return;
364 }
365
366 peripheral = [static_cast<CBPeripheral *>([peripherals objectAtIndex:0]) retain];
367 [self connectToPeripheral];
368}
369
371{
372 using namespace DarwinBluetooth;
373
374 Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central manager (nil)");
375 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
376 Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state");
377
378 // The state is still the same - connecting.
379 if ([self isConnected]) {
380 qCDebug(QT_BT_DARWIN) << "already connected";
381 if (notifier)
383 } else {
384 [self setMtu:defaultMtu];
385 qCDebug(QT_BT_DARWIN) << "trying to connect";
387 [manager connectPeripheral:peripheral options:nil];
388 }
389}
390
391- (bool)isConnected
392{
393 if (!peripheral)
394 return false;
395
396 return peripheral.state == CBPeripheralStateConnected;
397}
398
399- (void)disconnectFromDevice
400{
401 [self reset];
402
404 disconnectPending = true; // this is for 'didUpdate' method.
405 if (notifier) {
406 // We were waiting for the first update
407 // with 'PoweredOn' status, when suddenly got disconnected called.
408 // Since we have not attempted to connect yet, emit now.
409 // Note: we do not change the state, since we still maybe interested
410 // in the status update before the next connect attempt.
412 }
413 } else {
414 disconnectPending = false;
415 if ([self isConnected])
417 else
419
420 // We have to call -cancelPeripheralConnection: even
421 // if not connected (to cancel a pending connect attempt).
422 // Unfortunately, didDisconnect callback is not always called
423 // (despite of Apple's docs saying it _must_ be).
424 if (peripheral)
425 [manager cancelPeripheralConnection:peripheral];
426 }
427}
428
429- (void)discoverServices
430{
431 using namespace DarwinBluetooth;
432
433 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
434 Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state");
435
436 // From Apple's docs:
437 //
438 //"If the servicesUUIDs parameter is nil, all the available
439 //services of the peripheral are returned; setting the
440 //parameter to nil is considerably slower and is not recommended."
441 //
442 // ... but we'd like to have them all:
444 [self watchAfter:peripheral timeout:OperationTimeout::serviceDiscovery];
445 [peripheral discoverServices:nil];
446}
447
449{
450 using namespace DarwinBluetooth;
451
452 Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state");
453 Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid manager (nil)");
454 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
455
457
458 NSArray *const services = peripheral.services;
459 if (!services || !services.count) {
460 // A peripheral without any services at all.
461 if (notifier)
463 } else {
464 // 'reset' also calls retain on a parameter.
465 servicesToVisitNext.reset();
466 servicesToVisit.reset([NSMutableArray arrayWithArray:services], RetainPolicy::doInitialRetain);
467 currentService = 0;
468 visitedServices.reset([NSMutableSet setWithCapacity:peripheral.services.count], RetainPolicy::doInitialRetain);
469
470 CBService *const s = [services objectAtIndex:currentService];
471 [visitedServices addObject:s];
473 [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery];
474 [peripheral discoverIncludedServices:nil forService:s];
475 }
476}
477
478- (void)discoverServiceDetails:(const QBluetoothUuid &)serviceUuid
479 readValues:(bool) read
480{
481 // This function does not change 'managerState', since it
482 // can be called concurrently (not waiting for the previous
483 // discovery to finish).
484
485 using namespace DarwinBluetooth;
486
487 Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state");
488 Q_ASSERT_X(!serviceUuid.isNull(), Q_FUNC_INFO, "invalid service UUID");
489 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
490
491 if (servicesToDiscoverDetails.contains(serviceUuid)) {
492 qCWarning(QT_BT_DARWIN) << "already discovering for"
493 << serviceUuid;
494 return;
495 }
496
498
499 if (CBService *const service = [self serviceForUUID:serviceUuid]) {
500 const auto mode = read ? DiscoveryMode::FullDiscovery : DiscoveryMode::SkipValueDiscovery;
501 servicesToDiscoverDetails[serviceUuid] = mode;
502 [self watchAfter:service timeout:OperationTimeout::characteristicsDiscovery];
503 [peripheral discoverCharacteristics:nil forService:service];
504 return;
505 }
506
507 qCWarning(QT_BT_DARWIN) << "unknown service uuid"
508 << serviceUuid;
509
510 if (notifier)
512}
513
514- (void)readCharacteristics:(CBService *)service
515{
516 // This method does not change 'managerState', we can
517 // have several 'detail discoveries' active.
518 Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)");
519
520 using namespace DarwinBluetooth;
521
523
524 Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state");
525 Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid manager (nil)");
526 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
527
528 if (!service.characteristics || !service.characteristics.count)
529 return [self serviceDetailsDiscoveryFinished:service];
530
531 NSArray *const cs = service.characteristics;
532 for (CBCharacteristic *c in cs) {
533 if (c.properties & CBCharacteristicPropertyRead) {
534 [self watchAfter:c timeout:OperationTimeout::characteristicRead];
535 return [peripheral readValueForCharacteristic:c];
536 }
537 }
538
539 // No readable properties? Discover descriptors then:
540 [self discoverDescriptors:service];
541}
542
543- (void)discoverDescriptors:(CBService *)service
544{
545 // This method does not change 'managerState', we can have
546 // several discoveries active.
547 Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)");
548
549 using namespace DarwinBluetooth;
550
552
553 Q_ASSERT_X(managerState != CentralManagerUpdating,
554 Q_FUNC_INFO, "invalid state");
555 Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid manager (nil)");
556 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
557
558 if (!service.characteristics || !service.characteristics.count) {
559 [self serviceDetailsDiscoveryFinished:service];
560 } else {
561 // Start from 0 and continue in the callback.
562 CBCharacteristic *ch = [service.characteristics objectAtIndex:0];
563 [self watchAfter:ch timeout:OperationTimeout::descriptorsDiscovery];
564 [peripheral discoverDescriptorsForCharacteristic:ch];
565 }
566}
567
568- (void)readDescriptors:(CBService *)service
569{
570 using namespace DarwinBluetooth;
571
572 Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)");
573 Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state");
574 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
575
577
578 NSArray *const cs = service.characteristics;
579 // We can never be here if we have no characteristics.
580 Q_ASSERT_X(cs && cs.count, Q_FUNC_INFO, "invalid service");
581 for (CBCharacteristic *c in cs) {
582 if (c.descriptors && c.descriptors.count) {
583 CBDescriptor *desc = [c.descriptors objectAtIndex:0];
584 [self watchAfter:desc timeout:OperationTimeout::descriptorRead];
585 return [peripheral readValueForDescriptor:desc];
586 }
587 }
588
589 // No descriptors to read, done.
590 [self serviceDetailsDiscoveryFinished:service];
591}
592
593- (void)serviceDetailsDiscoveryFinished:(CBService *)service
594{
595 Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)");
596
597 using namespace DarwinBluetooth;
598
600
601 const QBluetoothUuid serviceUuid(qt_uuid(service.UUID));
602 const bool skipValues = servicesToDiscoverDetails[serviceUuid] == DiscoveryMode::SkipValueDiscovery;
603 servicesToDiscoverDetails.remove(serviceUuid);
604
605 const NSUInteger nHandles = qt_countGATTEntries(service);
606 Q_ASSERT_X(nHandles, Q_FUNC_INFO, "unexpected number of GATT entires");
607
608 const QLowEnergyHandle maxHandle = std::numeric_limits<QLowEnergyHandle>::max();
609 if (nHandles >= maxHandle || lastValidHandle > maxHandle - nHandles) {
610 // Well, that's unlikely :) But we must be sure.
611 qCWarning(QT_BT_DARWIN) << "can not allocate more handles";
612 if (notifier)
614 return;
615 }
616
617 // A temporary service object to pass the details.
618 // Set only uuid, characteristics and descriptors (and probably values),
619 // nothing else is needed.
620 QSharedPointer<QLowEnergyServicePrivate> qtService(new QLowEnergyServicePrivate);
621 qtService->uuid = serviceUuid;
622 // We 'register' handles/'CBentities' even if qlowenergycontroller (delegate)
623 // later fails to do this with some error. Otherwise, if we try to implement
624 // rollback/transaction logic interface is getting too ugly/complicated.
626 serviceMap[lastValidHandle] = service;
627 qtService->startHandle = lastValidHandle;
628
629 NSArray *const cs = service.characteristics;
630 // Now map chars/descriptors and handles.
631 if (cs && cs.count) {
632 QHash<QLowEnergyHandle, QLowEnergyServicePrivate::CharData> charList;
633
634 for (CBCharacteristic *c in cs) {
636 // Register this characteristic:
637 charMap[lastValidHandle] = c;
638 // Create a Qt's internal characteristic:
640 newChar.uuid = qt_uuid(c.UUID);
641 const int cbProps = c.properties & 0xff;
642 newChar.properties = static_cast<QLowEnergyCharacteristic::PropertyTypes>(cbProps);
643 if (!skipValues)
644 newChar.value = qt_bytearray(c.value);
645 newChar.valueHandle = lastValidHandle;
646
647 NSArray *const ds = c.descriptors;
648 if (ds && ds.count) {
649 QHash<QLowEnergyHandle, QLowEnergyServicePrivate::DescData> descList;
650 for (CBDescriptor *d in ds) {
651 // Register this descriptor:
653 descMap[lastValidHandle] = d;
654 // Create a Qt's internal descriptor:
656 newDesc.uuid = qt_uuid(d.UUID);
657 newDesc.value = qt_bytearray(static_cast<NSObject *>(d.value));
658 descList[lastValidHandle] = newDesc;
659 // Check, if it's client characteristic configuration descriptor:
661 if (newDesc.value.size() && (newDesc.value[0] & 3))
662 [peripheral setNotifyValue:YES forCharacteristic:c];
663 }
664 }
665
666 newChar.descriptorList = descList;
667 }
668
669 charList[newChar.valueHandle] = newChar;
670 }
671
672 qtService->characteristicList = charList;
673 }
674
675 qtService->endHandle = lastValidHandle;
676
677 if (notifier)
679}
680
682{
683 using namespace DarwinBluetooth;
684
685 if (requestPending || !requests.size())
686 return;
687
688 switch (requests.head().type) {
689 case LERequest::CharRead:
690 case LERequest::DescRead:
691 return [self performNextReadRequest];
692 case LERequest::CharWrite:
693 case LERequest::DescWrite:
694 case LERequest::ClientConfiguration:
695 return [self performNextWriteRequest];
696 default:
697 // Should never happen.
698 Q_ASSERT(0);
699 }
700}
701
703{
704 using namespace DarwinBluetooth;
705
706 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
707 Q_ASSERT_X(!requestPending, Q_FUNC_INFO, "processing another request");
708 Q_ASSERT_X(requests.size(), Q_FUNC_INFO, "no requests to handle");
709 Q_ASSERT_X(requests.head().type == LERequest::CharRead
710 || requests.head().type == LERequest::DescRead,
711 Q_FUNC_INFO, "not a read request");
712
713 const LERequest request(requests.dequeue());
714 if (request.type == LERequest::CharRead) {
715 if (!charMap.contains(request.handle)) {
716 qCWarning(QT_BT_DARWIN) << "characteristic with handle"
717 << request.handle << "not found";
718 return [self performNextRequest];
719 }
720
721 requestPending = true;
722 currentReadHandle = request.handle;
723 // Timeouts: for now, we do not alert timeoutWatchdog - never had such
724 // bug reports and after all a read timeout can be handled externally.
725 [peripheral readValueForCharacteristic:charMap[request.handle]];
726 } else {
727 if (!descMap.contains(request.handle)) {
728 qCWarning(QT_BT_DARWIN) << "descriptor with handle"
729 << request.handle << "not found";
730 return [self performNextRequest];
731 }
732
733 requestPending = true;
734 currentReadHandle = request.handle;
735 // Timeouts: see the comment above (CharRead).
736 [peripheral readValueForDescriptor:descMap[request.handle]];
737 }
738}
739
741{
742 using namespace DarwinBluetooth;
743
744 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
745 Q_ASSERT_X(!requestPending, Q_FUNC_INFO, "processing another request");
746 Q_ASSERT_X(requests.size(), Q_FUNC_INFO, "no requests to handle");
747 Q_ASSERT_X(requests.head().type == LERequest::CharWrite
748 || requests.head().type == LERequest::DescWrite
749 || requests.head().type == LERequest::ClientConfiguration,
750 Q_FUNC_INFO, "not a write request");
751
752 const LERequest request(requests.dequeue());
753
754 if (request.type == LERequest::DescWrite) {
755 if (!descMap.contains(request.handle)) {
756 qCWarning(QT_BT_DARWIN) << "handle:" << request.handle
757 << "not found";
758 return [self performNextRequest];
759 }
760
761 CBDescriptor *const descriptor = descMap[request.handle];
762 ObjCStrongReference<NSData> data(data_from_bytearray(request.value));
763 if (!data) {
764 // Even if qtData.size() == 0, we still need NSData object.
765 qCWarning(QT_BT_DARWIN) << "failed to allocate an NSData object";
766 return [self performNextRequest];
767 }
768
769 if (![self cacheWriteValue:request.value for:descriptor])
770 return [self performNextRequest];
771
772 requestPending = true;
773 return [peripheral writeValue:data.data() forDescriptor:descriptor];
774 } else {
775 if (!charMap.contains(request.handle)) {
776 qCWarning(QT_BT_DARWIN) << "characteristic with handle:"
777 << request.handle << "not found";
778 return [self performNextRequest];
779 }
780
781 CBCharacteristic *const characteristic = charMap[request.handle];
782
783 if (request.type == LERequest::ClientConfiguration) {
785 CBDescriptor *const descriptor = [self descriptor:qtUuid forCharacteristic:characteristic];
786 Q_ASSERT_X(descriptor, Q_FUNC_INFO, "no client characteristic "
787 "configuration descriptor found");
788
789 if (![self cacheWriteValue:request.value for:descriptor])
790 return [self performNextRequest];
791
792 bool enable = false;
793 if (request.value.size())
794 enable = request.value[0] & 3;
795
796 requestPending = true;
797 [peripheral setNotifyValue:enable forCharacteristic:characteristic];
798 } else {
799 ObjCStrongReference<NSData> data(data_from_bytearray(request.value));
800 if (!data) {
801 // Even if qtData.size() == 0, we still need NSData object.
802 qCWarning(QT_BT_DARWIN) << "failed to allocate NSData object";
803 return [self performNextRequest];
804 }
805
806 // TODO: check what happens if I'm using NSData with length 0.
807 if (request.withResponse) {
808 if (![self cacheWriteValue:request.value for:characteristic])
809 return [self performNextRequest];
810
811 requestPending = true;
812 [self watchAfter:characteristic timeout:OperationTimeout::characteristicWrite];
813 [peripheral writeValue:data.data() forCharacteristic:characteristic
814 type:CBCharacteristicWriteWithResponse];
815 } else {
816 [peripheral writeValue:data.data() forCharacteristic:characteristic
817 type:CBCharacteristicWriteWithoutResponse];
818 [self performNextRequest];
819 }
820 }
821 }
822}
823
824- (void)setMtu:(int)newMtu
825{
826 if (lastKnownMtu == newMtu)
827 return;
828 lastKnownMtu = newMtu;
829 if (notifier)
830 emit notifier->mtuChanged(newMtu);
831}
832
833- (int)mtu
834{
835 using namespace DarwinBluetooth;
836
837 if (![self isConnected]) {
838 [self setMtu:defaultMtu];
839 return defaultMtu;
840 }
841
843 const NSUInteger maxLen = [peripheral maximumWriteValueLengthForType:
844 CBCharacteristicWriteWithoutResponse];
845 if (maxLen > std::numeric_limits<int>::max() - 3)
846 [self setMtu:defaultMtu];
847 else
848 [self setMtu:int(maxLen) + 3];
849
850 return lastKnownMtu;
851}
852
853- (void)readRssi
854{
855 Q_ASSERT([self isConnected]);
856 [peripheral readRSSI];
857}
858
859- (void)setNotifyValue:(const QByteArray &)value
860 forCharacteristic:(QLowEnergyHandle)charHandle
861 onService:(const QBluetoothUuid &)serviceUuid
862{
863 using namespace DarwinBluetooth;
864
865 Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)");
866
867 if (!charMap.contains(charHandle)) {
868 qCWarning(QT_BT_DARWIN) << "unknown characteristic handle"
869 << charHandle;
870 if (notifier) {
871 emit notifier->CBManagerError(serviceUuid,
873 }
874 return;
875 }
876
877 // At the moment we call setNotifyValue _only_ from 'writeDescriptor';
878 // from Qt's API POV it's a descriptor write operation and we must report
879 // it back, so check _now_ that we really have this descriptor.
881 if (![self descriptor:qtUuid forCharacteristic:charMap[charHandle]]) {
882 qCWarning(QT_BT_DARWIN) << "no client characteristic configuration found";
883 if (notifier) {
884 emit notifier->CBManagerError(serviceUuid,
886 }
887 return;
888 }
889
891 request.type = LERequest::ClientConfiguration;
892 request.handle = charHandle;
893 request.value = value;
894
895 requests.enqueue(request);
896 [self performNextRequest];
897}
898
899- (void)readCharacteristic:(QLowEnergyHandle)charHandle
900 onService:(const QBluetoothUuid &)serviceUuid
901{
902 using namespace DarwinBluetooth;
903
904 Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)");
905
907
908 if (!charMap.contains(charHandle)) {
909 qCWarning(QT_BT_DARWIN) << "characteristic:" << charHandle << "not found";
910 if (notifier) {
911 emit notifier->CBManagerError(serviceUuid,
913
914 }
915 return;
916 }
917
919 request.type = LERequest::CharRead;
920 request.handle = charHandle;
921
922 requests.enqueue(request);
923 [self performNextRequest];
924}
925
926- (void)write:(const QByteArray &)value
927 charHandle:(QLowEnergyHandle)charHandle
928 onService:(const QBluetoothUuid &)serviceUuid
929 withResponse:(bool)withResponse
930{
931 using namespace DarwinBluetooth;
932
933 Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)");
934
936
937 if (!charMap.contains(charHandle)) {
938 qCWarning(QT_BT_DARWIN) << "characteristic:" << charHandle << "not found";
939 if (notifier) {
940 emit notifier->CBManagerError(serviceUuid,
942 }
943 return;
944 }
945
947 request.type = LERequest::CharWrite;
948 request.withResponse = withResponse;
949 request.handle = charHandle;
950 request.value = value;
951
952 requests.enqueue(request);
953 [self performNextRequest];
954}
955
956- (void)readDescriptor:(QLowEnergyHandle)descHandle
957 onService:(const QBluetoothUuid &)serviceUuid
958{
959 using namespace DarwinBluetooth;
960
961 Q_ASSERT_X(descHandle, Q_FUNC_INFO, "invalid descriptor handle (0)");
962
963 if (!descMap.contains(descHandle)) {
964 qCWarning(QT_BT_DARWIN) << "handle:" << descHandle << "not found";
965 if (notifier) {
966 emit notifier->CBManagerError(serviceUuid,
968 }
969 return;
970 }
971
973 request.type = LERequest::DescRead;
974 request.handle = descHandle;
975
976 requests.enqueue(request);
977 [self performNextRequest];
978}
979
980- (void)write:(const QByteArray &)value
981 descHandle:(QLowEnergyHandle)descHandle
982 onService:(const QBluetoothUuid &)serviceUuid
983{
984 using namespace DarwinBluetooth;
985
986 Q_ASSERT_X(descHandle, Q_FUNC_INFO, "invalid descriptor handle (0)");
987
988 if (!descMap.contains(descHandle)) {
989 qCWarning(QT_BT_DARWIN) << "handle:" << descHandle << "not found";
990 if (notifier) {
991 emit notifier->CBManagerError(serviceUuid,
993 }
994 return;
995 }
996
998 request.type = LERequest::DescWrite;
999 request.handle = descHandle;
1000 request.value = value;
1001
1002 requests.enqueue(request);
1003 [self performNextRequest];
1004}
1005
1006// Aux. methods:
1007
1008- (CBService *)serviceForUUID:(const QBluetoothUuid &)qtUuid
1009{
1010 using namespace DarwinBluetooth;
1011
1012 Q_ASSERT_X(!qtUuid.isNull(), Q_FUNC_INFO, "invalid uuid");
1013 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
1014
1015 ObjCStrongReference<NSMutableArray> toVisit([NSMutableArray arrayWithArray:peripheral.services], RetainPolicy::doInitialRetain);
1016 ObjCStrongReference<NSMutableArray> toVisitNext([[NSMutableArray alloc] init], RetainPolicy::noInitialRetain);
1017 ObjCStrongReference<NSMutableSet> visitedNodes([[NSMutableSet alloc] init], RetainPolicy::noInitialRetain);
1018
1019 while (true) {
1020 for (NSUInteger i = 0, e = [toVisit count]; i < e; ++i) {
1021 CBService *const s = [toVisit objectAtIndex:i];
1022 if (equal_uuids(s.UUID, qtUuid))
1023 return s;
1024 if (![visitedNodes containsObject:s] && s.includedServices && s.includedServices.count) {
1025 [visitedNodes addObject:s];
1026 [toVisitNext addObjectsFromArray:s.includedServices];
1027 }
1028 }
1029
1030 if (![toVisitNext count])
1031 return nil;
1032
1033 toVisit.swap(toVisitNext);
1034 toVisitNext.reset([[NSMutableArray alloc] init], RetainPolicy::noInitialRetain);
1035 }
1036
1037 return nil;
1038}
1039
1040- (CBCharacteristic *)nextCharacteristicForService:(CBService*)service
1041 startingFrom:(CBCharacteristic *)characteristic
1042{
1043 Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)");
1044 Q_ASSERT_X(characteristic, Q_FUNC_INFO, "invalid characteristic (nil)");
1045 Q_ASSERT_X(service.characteristics, Q_FUNC_INFO, "invalid service");
1046 Q_ASSERT_X(service.characteristics.count, Q_FUNC_INFO, "invalid service");
1047
1049
1050 // TODO: test that we NEVER have the same characteristic twice in array!
1051 // At the moment I just protect against this by iterating in a reverse
1052 // order (at least avoiding a potential inifite loop with '-indexOfObject:').
1053 NSArray *const cs = service.characteristics;
1054 if (cs.count == 1)
1055 return nil;
1056
1057 for (NSUInteger index = cs.count - 1; index != 0; --index) {
1058 if ([cs objectAtIndex:index] == characteristic) {
1059 if (index + 1 == cs.count)
1060 return nil;
1061 else
1062 return [cs objectAtIndex:index + 1];
1063 }
1064 }
1065
1066 Q_ASSERT_X([cs objectAtIndex:0] == characteristic, Q_FUNC_INFO,
1067 "characteristic was not found in service.characteristics");
1068
1069 return [cs objectAtIndex:1];
1070}
1071
1072- (CBCharacteristic *)nextCharacteristicForService:(CBService*)service
1073 startingFrom:(CBCharacteristic *)characteristic
1074 properties:(CBCharacteristicProperties)properties
1075{
1076 Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)");
1077 Q_ASSERT_X(characteristic, Q_FUNC_INFO, "invalid characteristic (nil)");
1078 Q_ASSERT_X(service.characteristics, Q_FUNC_INFO, "invalid service");
1079 Q_ASSERT_X(service.characteristics.count, Q_FUNC_INFO, "invalid service");
1080
1082
1083 // TODO: test that we NEVER have the same characteristic twice in array!
1084 // At the moment I just protect against this by iterating in a reverse
1085 // order (at least avoiding a potential inifite loop with '-indexOfObject:').
1086 NSArray *const cs = service.characteristics;
1087 if (cs.count == 1)
1088 return nil;
1089
1090 NSUInteger index = cs.count - 1;
1091 for (; index != 0; --index) {
1092 if ([cs objectAtIndex:index] == characteristic) {
1093 if (index + 1 == cs.count) {
1094 return nil;
1095 } else {
1096 index += 1;
1097 break;
1098 }
1099 }
1100 }
1101
1102 if (!index) {
1103 Q_ASSERT_X([cs objectAtIndex:0] == characteristic, Q_FUNC_INFO,
1104 "characteristic not found in service.characteristics");
1105 index = 1;
1106 }
1107
1108 for (const NSUInteger e = cs.count; index < e; ++index) {
1109 CBCharacteristic *const c = [cs objectAtIndex:index];
1110 if (c.properties & properties)
1111 return c;
1112 }
1113
1114 return nil;
1115}
1116
1117- (CBDescriptor *)nextDescriptorForCharacteristic:(CBCharacteristic *)characteristic
1118 startingFrom:(CBDescriptor *)descriptor
1119{
1120 Q_ASSERT_X(characteristic, Q_FUNC_INFO, "invalid characteristic (nil)");
1121 Q_ASSERT_X(descriptor, Q_FUNC_INFO, "invalid descriptor (nil)");
1122 Q_ASSERT_X(characteristic.descriptors, Q_FUNC_INFO, "invalid characteristic");
1123 Q_ASSERT_X(characteristic.descriptors.count, Q_FUNC_INFO, "invalid characteristic");
1124
1126
1127 NSArray *const ds = characteristic.descriptors;
1128 if (ds.count == 1)
1129 return nil;
1130
1131 for (NSUInteger index = ds.count - 1; index != 0; --index) {
1132 if ([ds objectAtIndex:index] == descriptor) {
1133 if (index + 1 == ds.count)
1134 return nil;
1135 else
1136 return [ds objectAtIndex:index + 1];
1137 }
1138 }
1139
1140 Q_ASSERT_X([ds objectAtIndex:0] == descriptor, Q_FUNC_INFO,
1141 "descriptor was not found in characteristic.descriptors");
1142
1143 return [ds objectAtIndex:1];
1144}
1145
1146- (CBDescriptor *)descriptor:(const QBluetoothUuid &)qtUuid
1147 forCharacteristic:(CBCharacteristic *)ch
1148{
1149 if (qtUuid.isNull() || !ch)
1150 return nil;
1151
1153
1154 CBDescriptor *descriptor = nil;
1155 NSArray *const ds = ch.descriptors;
1156 if (ds && ds.count) {
1157 for (CBDescriptor *d in ds) {
1158 if (DarwinBluetooth::equal_uuids(d.UUID, qtUuid)) {
1159 descriptor = d;
1160 break;
1161 }
1162 }
1163 }
1164
1165 return descriptor;
1166}
1167
1168- (bool)cacheWriteValue:(const QByteArray &)value for:(NSObject *)obj
1169{
1170 Q_ASSERT_X(obj, Q_FUNC_INFO, "invalid object (nil)");
1171
1172 if ([obj isKindOfClass:[CBCharacteristic class]]) {
1173 CBCharacteristic *const ch = static_cast<CBCharacteristic *>(obj);
1174 if (!charMap.key(ch)) {
1175 qCWarning(QT_BT_DARWIN) << "unexpected characteristic, no handle found";
1176 return false;
1177 }
1178 } else if ([obj isKindOfClass:[CBDescriptor class]]) {
1179 CBDescriptor *const d = static_cast<CBDescriptor *>(obj);
1180 if (!descMap.key(d)) {
1181 qCWarning(QT_BT_DARWIN) << "unexpected descriptor, no handle found";
1182 return false;
1183 }
1184 } else {
1185 qCWarning(QT_BT_DARWIN) << "invalid object, characteristic "
1186 "or descriptor required";
1187 return false;
1188 }
1189
1190 if (valuesToWrite.contains(obj)) {
1191 // It can be a result of some previous errors - for example,
1192 // we never got a callback from a previous write.
1193 qCWarning(QT_BT_DARWIN) << "already has a cached value for this "
1194 "object, the value will be replaced";
1195 }
1196
1197 valuesToWrite[obj] = value;
1198 return true;
1199}
1200
1201- (void)reset
1202{
1203 requestPending = false;
1204 valuesToWrite.clear();
1205 requests.clear();
1207 lastValidHandle = 0;
1208 serviceMap.clear();
1209 charMap.clear();
1210 descMap.clear();
1212 [self stopWatchers];
1213 // TODO: also serviceToVisit/VisitNext and visitedServices ?
1214}
1215
1216- (void)handleReadWriteError:(NSError *)error
1217{
1219
1220 switch (error.code) {
1221 case 0x05: // GATT_INSUFFICIENT_AUTHORIZATION
1222 case 0x0F: // GATT_INSUFFICIENT_ENCRYPTION
1224 [self detach];
1225 break;
1226 default:
1227 break;
1228 }
1229}
1230
1231// CBCentralManagerDelegate (the real one).
1232
1233- (void)centralManagerDidUpdateState:(CBCentralManager *)central
1234{
1235 using namespace DarwinBluetooth;
1236
1237#pragma clang diagnostic push
1238#pragma clang diagnostic ignored "-Wunguarded-availability-new"
1239
1240 const auto state = central.state;
1241 if (state == CBManagerStateUnknown || state == CBManagerStateResetting) {
1242 // We still have to wait, docs say:
1243 // "The current state of the central manager is unknown;
1244 // an update is imminent." or
1245 // "The connection with the system service was momentarily
1246 // lost; an update is imminent."
1247 return;
1248 }
1249
1250 // Let's check some states we do not like first:
1251 if (state == CBManagerStateUnsupported || state == CBManagerStateUnauthorized) {
1253 // The LE is not supported or its usage was not authorized
1254 if (notifier) {
1255 if (state == CBManagerStateUnsupported)
1257 else
1259 }
1260 [self stopWatchers];
1261 return;
1262 }
1263
1264 if (state == CBManagerStatePoweredOff) {
1265 if (managerState == CentralManagerUpdating) {
1267 // I've seen this instead of Unsupported on OS X.
1268 if (notifier)
1270 } else {
1272 // TODO: we need a better error +
1273 // what will happen if later the state changes to PoweredOn???
1274 if (notifier)
1276 }
1277 [self stopWatchers];
1278 return;
1279 }
1280
1281 if (state == CBManagerStatePoweredOn) {
1282 if (managerState == CentralManagerUpdating && !disconnectPending) {
1284 [self retrievePeripheralAndConnect];
1285 }
1286 } else {
1287 // We actually handled all known states, but .. Core Bluetooth can change?
1288 Q_ASSERT_X(0, Q_FUNC_INFO, "invalid central's state");
1289 }
1290
1291#pragma clang diagnostic pop
1292}
1293
1294- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral
1295{
1296 Q_UNUSED(central);
1297 Q_UNUSED(aPeripheral);
1298
1300 // We called cancel but before disconnected, managed to connect?
1301 return;
1302 }
1303
1304 void([self mtu]);
1305
1306 [peripheral setDelegate:self];
1307
1309 if (notifier)
1311}
1312
1313- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)aPeripheral
1314 error:(NSError *)error
1315{
1316 Q_UNUSED(central);
1317 Q_UNUSED(aPeripheral);
1318 Q_UNUSED(error);
1319
1321 // Canceled already.
1322 return;
1323 }
1324
1326 // TODO: better error mapping is required.
1327 if (notifier)
1329}
1330
1331- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)aPeripheral
1332 error:(NSError *)error
1333{
1334 Q_UNUSED(central);
1335 Q_UNUSED(aPeripheral);
1336
1337 // Clear internal caches/data.
1338 [self reset];
1339
1342 qCWarning(QT_BT_DARWIN) << "failed to disconnect";
1343 if (notifier)
1345 } else {
1347 if (notifier)
1349 }
1350}
1351
1352// CBPeripheralDelegate.
1353
1354- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error
1355{
1356 Q_UNUSED(aPeripheral);
1357
1358 using namespace DarwinBluetooth;
1359
1360 if (managerState != CentralManagerDiscovering) {
1361 // Canceled by -disconnectFromDevice, or as a result of a timeout.
1362 return;
1363 }
1364
1365 if (![self objectIsUnderWatch:aPeripheral operation:OperationTimeout::serviceDiscovery]) // Timed out already
1366 return;
1367
1369
1370 [self stopWatchingAfter:aPeripheral operation:OperationTimeout::serviceDiscovery];
1371
1373
1374 if (error) {
1375 NSLog(@"%s failed with error %@", Q_FUNC_INFO, error);
1376 // TODO: better error mapping required.
1377 // Emit an error which also causes the service discovery finished() signal
1378 if (notifier)
1380 return;
1381 }
1382
1383 [self discoverIncludedServices];
1384}
1385
1386- (void)peripheral:(CBPeripheral *)aPeripheral
1387 didModifyServices:(NSArray<CBService *> *)invalidatedServices
1388{
1389 Q_UNUSED(aPeripheral);
1390 Q_UNUSED(invalidatedServices);
1391
1392 qCWarning(QT_BT_DARWIN) << "The peripheral has modified its services.";
1393 // "This method is invoked whenever one or more services of a peripheral have changed.
1394 // A peripheral’s services have changed if:
1395 // * A service is removed from the peripheral’s database
1396 // * A new service is added to the peripheral’s database
1397 // * A service that was previously removed from the peripheral’s
1398 // database is readded to the database at a different location"
1399
1400 // In case new services were added - we have to discover them.
1401 // In case some were removed - we can end up with dangling pointers
1402 // (see our 'watchdogs', for example). To handle the situation
1403 // we stop all current operations here, report to QLowEnergyController
1404 // so that it can trigger re-discovery.
1405 [self reset];
1407 if (notifier)
1409}
1410
1411- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverIncludedServicesForService:(CBService *)service
1412 error:(NSError *)error
1413{
1414 Q_UNUSED(aPeripheral);
1415
1416 using namespace DarwinBluetooth;
1417
1418 if (managerState != CentralManagerDiscovering) {
1419 // Canceled by disconnectFromDevice or -peripheralDidDisconnect...
1420 return;
1421 }
1422
1423 if (![self objectIsUnderWatch:service operation:OperationTimeout::includedServicesDiscovery])
1424 return;
1425
1427
1428 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
1429
1430 [self stopWatchingAfter:service operation:OperationTimeout::includedServicesDiscovery];
1432
1433 if (error) {
1434 NSLog(@"%s: finished with error %@ for service %@",
1435 Q_FUNC_INFO, error, service.UUID);
1436 } else if (service.includedServices && service.includedServices.count) {
1437 // Now we have even more services to do included services discovery ...
1439 servicesToVisitNext.reset([NSMutableArray arrayWithArray:service.includedServices], RetainPolicy::doInitialRetain);
1440 else
1441 [servicesToVisitNext addObjectsFromArray:service.includedServices];
1442 }
1443
1444 // Do we have something else to discover on this 'level'?
1446
1448 CBService *const s = [servicesToVisit objectAtIndex:currentService];
1449 if (![visitedServices containsObject:s]) {
1450 // Continue with discovery ...
1451 [visitedServices addObject:s];
1453 [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery];
1454 return [peripheral discoverIncludedServices:nil forService:s];
1455 }
1456 }
1457
1458 // No services to visit more on this 'level'.
1459
1462 servicesToVisitNext.reset();
1463
1464 currentService = 0;
1466 CBService *const s = [servicesToVisit objectAtIndex:currentService];
1467 if (![visitedServices containsObject:s]) {
1468 [visitedServices addObject:s];
1470 [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery];
1471 return [peripheral discoverIncludedServices:nil forService:s];
1472 }
1473 }
1474 }
1475
1476 // Finally, if we're here, the service discovery is done!
1477
1478 // Release all these things now, no need to prolong their lifetime.
1479 visitedServices.reset();
1480 servicesToVisit.reset();
1481 servicesToVisitNext.reset();
1482
1483 if (notifier)
1485}
1486
1487- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service
1488 error:(NSError *)error
1489{
1490 // This method does not change 'managerState', we can have several
1491 // discoveries active.
1492 Q_UNUSED(aPeripheral);
1493
1494 if (!notifier) {
1495 // Detached.
1496 return;
1497 }
1498
1499 using namespace DarwinBluetooth;
1500
1501 if (![self objectIsUnderWatch:service operation:OperationTimeout::characteristicsDiscovery])
1502 return;
1503
1505
1506 [self stopWatchingAfter:service operation:OperationTimeout::characteristicsDiscovery];
1507
1508 const auto qtUuid = qt_uuid(service.UUID);
1509 const bool skipRead = servicesToDiscoverDetails[qtUuid] == DiscoveryMode::SkipValueDiscovery;
1510
1511 Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state");
1512
1513 if (error) {
1514 NSLog(@"%s failed with error: %@", Q_FUNC_INFO, error);
1515 // We did not discover any characteristics and can not discover descriptors,
1516 // inform our delegate (it will set a service state also).
1518 }
1519
1520 if (skipRead) {
1521 [self serviceDetailsDiscoveryFinished:service];
1522 return;
1523 }
1524 [self readCharacteristics:service];
1525}
1526
1527- (void)peripheral:(CBPeripheral *)aPeripheral
1528 didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
1529 error:(NSError *)error
1530{
1531 Q_UNUSED(aPeripheral);
1532
1533 if (!notifier) // Detached.
1534 return;
1535
1536 using namespace DarwinBluetooth;
1537
1539
1540 const bool readMatch = [self objectIsUnderWatch:characteristic operation:OperationTimeout::characteristicRead];
1541 if (readMatch)
1542 [self stopWatchingAfter:characteristic operation:OperationTimeout::characteristicRead];
1543
1544 Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state");
1545 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
1546
1547
1548 // First, let's check if we're discovering a service details now.
1549 CBService *const service = characteristic.service;
1550 const QBluetoothUuid qtUuid(qt_uuid(service.UUID));
1551 const bool isDetailsDiscovery = servicesToDiscoverDetails.contains(qtUuid);
1552 const QLowEnergyHandle chHandle = charMap.key(characteristic);
1553
1554 if (error) {
1555 NSLog(@"%s failed with error %@", Q_FUNC_INFO, error);
1556 if (!isDetailsDiscovery) {
1557 if (chHandle && chHandle == currentReadHandle) {
1559 requestPending = false;
1561 [self handleReadWriteError:error];
1562 [self performNextRequest];
1563 }
1564 return;
1565 }
1566 }
1567
1568 if (isDetailsDiscovery) {
1569 if (readMatch) {
1570 // Test if we have any other characteristic to read yet.
1571 CBCharacteristic *const next = [self nextCharacteristicForService:characteristic.service
1572 startingFrom:characteristic properties:CBCharacteristicPropertyRead];
1573 if (next) {
1574 [self watchAfter:next timeout:OperationTimeout::characteristicRead];
1575 [peripheral readValueForCharacteristic:next];
1576 } else {
1577 [self discoverDescriptors:characteristic.service];
1578 }
1579 }
1580 } else {
1581 // This is (probably) the result of update notification.
1582 // It's very possible we can have an invalid handle here (0) -
1583 // if something esle is wrong (we subscribed for a notification),
1584 // disconnected (but other application is connected) and still receiveing
1585 // updated values ...
1586 // TODO: this must be properly tested.
1587 if (!chHandle) {
1588 qCCritical(QT_BT_DARWIN) << "unexpected update notification, "
1589 "no characteristic handle found";
1590 return;
1591 }
1592
1593 if (currentReadHandle == chHandle) {
1594 // Even if it was not a reply to our read request (no way to test it)
1595 // report it.
1596 requestPending = false;
1598 //
1599 emit notifier->characteristicRead(chHandle, qt_bytearray(characteristic.value));
1600 [self performNextRequest];
1601 } else {
1602 emit notifier->characteristicUpdated(chHandle, qt_bytearray(characteristic.value));
1603 }
1604 }
1605}
1606
1607- (void)peripheral:(CBPeripheral *)aPeripheral
1608 didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic
1609 error:(NSError *)error
1610{
1611 // This method does not change 'managerState', we can
1612 // have several discoveries active at the same time.
1613 Q_UNUSED(aPeripheral);
1614
1615 if (!notifier) {
1616 // Detached, no need to continue ...
1617 return;
1618 }
1619
1621
1622 using namespace DarwinBluetooth;
1623
1624 if (![self objectIsUnderWatch:characteristic operation:OperationTimeout::descriptorsDiscovery])
1625 return;
1626
1627 [self stopWatchingAfter:characteristic operation:OperationTimeout::descriptorsDiscovery];
1628
1629 if (error) {
1630 NSLog(@"%s failed with error %@", Q_FUNC_INFO, error);
1631 // We can continue though ...
1632 }
1633
1634 // Do we have more characteristics on this service to discover descriptors?
1635 CBCharacteristic *const next = [self nextCharacteristicForService:characteristic.service
1636 startingFrom:characteristic];
1637 if (next) {
1638 [self watchAfter:next timeout:OperationTimeout::descriptorsDiscovery];
1639 [peripheral discoverDescriptorsForCharacteristic:next];
1640 } else {
1641 [self readDescriptors:characteristic.service];
1642 }
1643}
1644
1645- (void)peripheral:(CBPeripheral *)aPeripheral
1646 didUpdateValueForDescriptor:(CBDescriptor *)descriptor
1647 error:(NSError *)error
1648{
1649 Q_UNUSED(aPeripheral);
1650
1651 Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
1652
1653 if (!notifier) {
1654 // Detached ...
1655 return;
1656 }
1657
1659
1660 using namespace DarwinBluetooth;
1661
1662 if (![self objectIsUnderWatch:descriptor operation:OperationTimeout::descriptorRead])
1663 return;
1664
1665 [self stopWatchingAfter:descriptor operation:OperationTimeout::descriptorRead];
1666
1667 CBService *const service = descriptor.characteristic.service;
1668 const QBluetoothUuid qtUuid(qt_uuid(service.UUID));
1669 const bool isDetailsDiscovery = servicesToDiscoverDetails.contains(qtUuid);
1670 const QLowEnergyHandle dHandle = descMap.key(descriptor);
1671
1672 if (error) {
1673 NSLog(@"%s failed with error %@", Q_FUNC_INFO, error);
1674
1675 if (!isDetailsDiscovery) {
1676 if (dHandle && dHandle == currentReadHandle) {
1678 requestPending = false;
1680 [self handleReadWriteError:error];
1681 [self performNextRequest];
1682 }
1683 return;
1684 }
1685 }
1686
1687 if (isDetailsDiscovery) {
1688 // Test if we have any other characteristic to read yet.
1689 CBDescriptor *const next = [self nextDescriptorForCharacteristic:descriptor.characteristic
1690 startingFrom:descriptor];
1691 if (next) {
1692 [self watchAfter:next timeout:OperationTimeout::descriptorRead];
1693 [peripheral readValueForDescriptor:next];
1694 } else {
1695 // We either have to read a value for a next descriptor
1696 // on a given characteristic, or continue with the
1697 // next characteristic in a given service (if any).
1698 CBCharacteristic *const ch = descriptor.characteristic;
1699 CBCharacteristic *nextCh = [self nextCharacteristicForService:ch.service
1700 startingFrom:ch];
1701 while (nextCh) {
1702 if (nextCh.descriptors && nextCh.descriptors.count) {
1703 CBDescriptor *desc = [nextCh.descriptors objectAtIndex:0];
1704 [self watchAfter:desc timeout:OperationTimeout::descriptorRead];
1705 return [peripheral readValueForDescriptor:desc];
1706 }
1707
1708 nextCh = [self nextCharacteristicForService:ch.service
1709 startingFrom:nextCh];
1710 }
1711
1712 [self serviceDetailsDiscoveryFinished:service];
1713 }
1714 } else {
1715 if (!dHandle) {
1716 qCCritical(QT_BT_DARWIN) << "unexpected value update notification, "
1717 "no descriptor handle found";
1718 return;
1719 }
1720
1721 if (dHandle == currentReadHandle) {
1723 requestPending = false;
1724 emit notifier->descriptorRead(dHandle, qt_bytearray(static_cast<NSObject *>(descriptor.value)));
1725 [self performNextRequest];
1726 }
1727 }
1728}
1729
1730- (void)peripheral:(CBPeripheral *)aPeripheral
1731 didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
1732 error:(NSError *)error
1733{
1734 Q_UNUSED(aPeripheral);
1735 Q_UNUSED(characteristic);
1736
1737 if (!notifier) {
1738 // Detached.
1739 return;
1740 }
1741
1742 // From docs:
1743 //
1744 // "This method is invoked only when your app calls the writeValue:forCharacteristic:type:
1745 // method with the CBCharacteristicWriteWithResponse constant specified as the write type.
1746 // If successful, the error parameter is nil. If unsuccessful,
1747 // the error parameter returns the cause of the failure."
1748
1749 using namespace DarwinBluetooth;
1750
1752
1753 if (![self objectIsUnderWatch:characteristic operation:OperationTimeout::characteristicWrite])
1754 return;
1755
1756 [self stopWatchingAfter:characteristic operation:OperationTimeout::characteristicWrite];
1757 requestPending = false;
1758
1759 // Error or not, but the cached value has to be deleted ...
1760 const QByteArray valueToReport(valuesToWrite.value(characteristic, QByteArray()));
1761 if (!valuesToWrite.remove(characteristic)) {
1762 qCWarning(QT_BT_DARWIN) << "no updated value found "
1763 "for characteristic";
1764 }
1765
1766 if (error) {
1767 NSLog(@"%s failed with error %@", Q_FUNC_INFO, error);
1768 emit notifier->CBManagerError(qt_uuid(characteristic.service.UUID),
1770 [self handleReadWriteError:error];
1771 } else {
1772 const QLowEnergyHandle cHandle = charMap.key(characteristic);
1773 emit notifier->characteristicWritten(cHandle, valueToReport);
1774 }
1775
1776 [self performNextRequest];
1777}
1778
1779- (void)peripheral:(CBPeripheral *)aPeripheral
1780 didWriteValueForDescriptor:(CBDescriptor *)descriptor
1781 error:(NSError *)error
1782{
1783 Q_UNUSED(aPeripheral);
1784
1785 if (!notifier) {
1786 // Detached already.
1787 return;
1788 }
1789
1790 using namespace DarwinBluetooth;
1791
1793
1794 requestPending = false;
1795
1796 // Error or not, a value (if any) must be removed.
1797 const QByteArray valueToReport(valuesToWrite.value(descriptor, QByteArray()));
1798 if (!valuesToWrite.remove(descriptor))
1799 qCWarning(QT_BT_DARWIN) << "no updated value found";
1800
1801 if (error) {
1802 NSLog(@"%s failed with error %@", Q_FUNC_INFO, error);
1803 emit notifier->CBManagerError(qt_uuid(descriptor.characteristic.service.UUID),
1805 [self handleReadWriteError:error];
1806 } else {
1807 const QLowEnergyHandle dHandle = descMap.key(descriptor);
1808 Q_ASSERT_X(dHandle, Q_FUNC_INFO, "descriptor not found in the descriptors map");
1809 emit notifier->descriptorWritten(dHandle, valueToReport);
1810 }
1811
1812 [self performNextRequest];
1813}
1814
1815- (void)peripheral:(CBPeripheral *)aPeripheral
1816 didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
1817 error:(NSError *)error
1818{
1819 Q_UNUSED(aPeripheral);
1820
1821 if (!notifier)
1822 return;
1823
1824 using namespace DarwinBluetooth;
1825
1827
1828 requestPending = false;
1829
1831 CBDescriptor *const descriptor = [self descriptor:qtUuid forCharacteristic:characteristic];
1832 const QByteArray valueToReport(valuesToWrite.value(descriptor, QByteArray()));
1833 const int nRemoved = valuesToWrite.remove(descriptor);
1834
1835 if (error) {
1836 NSLog(@"%s failed with error %@", Q_FUNC_INFO, error);
1837 // In Qt's API it's a descriptor write actually.
1838 emit notifier->CBManagerError(qt_uuid(characteristic.service.UUID),
1840 } else if (nRemoved) {
1841 const QLowEnergyHandle dHandle = descMap.key(descriptor);
1842 emit notifier->descriptorWritten(dHandle, valueToReport);
1843 }
1844
1845 [self performNextRequest];
1846}
1847
1848- (void)peripheral:(CBPeripheral *)aPeripheral didReadRSSI:(NSNumber *)RSSI error:(nullable NSError *)error
1849{
1850 Q_UNUSED(aPeripheral);
1851
1852 if (!notifier) // This controller was detached.
1853 return;
1854
1855 if (error) {
1856 NSLog(@"Reading RSSI finished with error: %@", error);
1858 }
1859
1860 if (!RSSI) {
1861 qCWarning(QT_BT_DARWIN, "Reading RSSI returned no value");
1863 }
1864
1865 emit notifier->rssiUpdated(qint16([RSSI shortValue]));
1866}
1867
1868- (void)detach
1869{
1870 if (notifier) {
1873 notifier = nullptr;
1874 }
1875
1876 [self stopWatchers];
1877 [self disconnectFromDevice];
1878}
1879
1880@end
QBluetoothUuid deviceUuid
DarwinBluetooth::ObjCStrongReference< NSMutableArray > servicesToVisit
DarwinBluetooth::ServiceHash serviceMap
DarwinBluetooth::ObjCStrongReference< NSMutableSet > visitedServices
DarwinBluetooth::CharHash charMap
QLowEnergyHandle currentReadHandle
CBPeripheral * peripheral
QLowEnergyHandle lastValidHandle
DarwinBluetooth::RequestQueue requests
DarwinBluetooth::CentralManagerState managerState
DarwinBluetooth::LECBManagerNotifier * notifier
bool requestPending
int lastKnownMtu
std::vector< DarwinBluetooth::GCDTimer > timeoutWatchdogs
bool disconnectPending
DarwinBluetooth::ValueHash valuesToWrite
QMap< QBluetoothUuid, DiscoveryMode > servicesToDiscoverDetails
qint64 timeoutMS
NSUInteger currentService
DarwinBluetooth::DescHash descMap
DarwinBluetooth::ObjCStrongReference< NSMutableArray > servicesToVisitNext
std::vector< ObjCStrongReference< CBMutableService > > services
#define QT_BT_MAC_AUTORELEASEPOOL
Definition btutility_p.h:78
void characteristicUpdated(QLowEnergyHandle charHandle, const QByteArray &value)
void rssiUpdated(qint16 newValue)
void serviceDetailsDiscoveryFinished(QSharedPointer< QLowEnergyServicePrivate > service)
void characteristicRead(QLowEnergyHandle charHandle, const QByteArray &value)
void descriptorRead(QLowEnergyHandle descHandle, const QByteArray &value)
void characteristicWritten(QLowEnergyHandle charHandle, const QByteArray &value)
void CBManagerError(QBluetoothDeviceDiscoveryAgent::Error error)
void descriptorWritten(QLowEnergyHandle descHandle, const QByteArray &value)
\inmodule QtBluetooth
\inmodule QtCore
Definition qbytearray.h:57
DiscoveryMode
This enum lists service discovery modes.
static bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *member)
\threadsafe
Definition qobject.cpp:3236
void deleteLater()
\threadsafe
Definition qobject.cpp:2435
Id128Bytes toBytes(QSysInfo::Endian order=QSysInfo::BigEndian) const noexcept
Definition quuid.h:232
bool isNull() const noexcept
Returns true if this is the null UUID {00000000-0000-0000-0000-000000000000}; otherwise returns false...
Definition quuid.cpp:818
for(qsizetype i=0;i< list.size();++i)
else opt state
[0]
short next
Definition keywords.cpp:445
ObjCStrongReference< NSError > qt_timeoutNSError(OperationTimeout type)
QQueue< LERequest > RequestQueue
QHash< NSObject *, QByteArray > ValueHash
bool equal_uuids(const QBluetoothUuid &qtUuid, CBUUID *cbUuid)
Definition btutility.mm:190
auto qt_find_watchdog(const std::vector< GCDTimer > &watchdogs, id object, OperationTimeout type)
QByteArray qt_bytearray(NSData *data)
Definition btutility.mm:201
QHash< QLowEnergyHandle, CBDescriptor * > DescHash
ObjCStrongReference< DarwinBTGCDTimer > GCDTimer
QBluetoothUuid qt_uuid(NSUUID *nsUuid)
QHash< QLowEnergyHandle, CBService * > ServiceHash
NSUInteger qt_countGATTEntries(CBService *service)
ObjCStrongReference< NSData > data_from_bytearray(const QByteArray &qtData)
Definition btutility.mm:272
dispatch_queue_t qt_LE_queue()
Definition btutility.mm:324
const int defaultMtu
Definition btutility.mm:34
QHash< QLowEnergyHandle, CBCharacteristic * > CharHash
Combined button and popup list for selecting options.
Q_CORE_EXPORT QtJniTypes::Service service()
QString self
Definition language.cpp:58
quint16 QLowEnergyHandle
Definition qbluetooth.h:42
unsigned long NSUInteger
#define Q_UNLIKELY(x)
#define Q_FUNC_INFO
static const QCssKnownValue properties[NumProperties - 1]
DBusConnection const char DBusError DBusBusType DBusError return DBusConnection DBusHandleMessageFunction void DBusFreeFunction return DBusConnection return DBusConnection return const char DBusError return DBusConnection DBusMessage dbus_uint32_t return DBusConnection dbus_bool_t DBusConnection DBusAddWatchFunction DBusRemoveWatchFunction DBusWatchToggledFunction void DBusFreeFunction return DBusConnection DBusDispatchStatusFunction void DBusFreeFunction DBusTimeout return DBusTimeout return DBusWatch return DBusWatch unsigned int return DBusError const DBusError return const DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessageIter int const void return DBusMessageIter DBusMessageIter return DBusMessageIter void DBusMessageIter void int return DBusMessage DBusMessageIter return DBusMessageIter return DBusMessageIter DBusMessageIter const char const char const char const char return DBusMessage return DBusMessage const char return DBusMessage dbus_bool_t return DBusMessage dbus_uint32_t return DBusMessage void
DBusConnection const char DBusError * error
EGLOutputLayerEXT EGLint EGLAttrib value
[5]
#define qCCritical(category,...)
#define qCWarning(category,...)
#define qCDebug(category,...)
#define Q_DECLARE_METATYPE(TYPE)
Definition qmetatype.h:1525
GLenum mode
GLuint index
[2]
GLenum GLuint id
[7]
GLenum GLenum GLsizei count
GLbitfield GLuint64 timeout
[4]
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLenum type
GLboolean enable
GLfloat n
GLhandleARB obj
[2]
GLdouble s
[6]
Definition qopenglext.h:235
GLboolean reset
const GLubyte * c
GLuint in
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define Q_ASSERT_X(cond, x, msg)
Definition qrandom.cpp:48
Q_CORE_EXPORT bool qEnvironmentVariableIsEmpty(const char *varName) noexcept
Q_CORE_EXPORT int qEnvironmentVariableIntValue(const char *varName, bool *ok=nullptr) noexcept
static QT_BEGIN_NAMESPACE void init(QTextBoundaryFinder::BoundaryType type, QStringView str, QCharAttributes *attributes)
#define emit
#define Q_UNUSED(x)
short qint16
Definition qtypes.h:47
long long qint64
Definition qtypes.h:60
ReturnedValue read(const char *data)
QFutureWatcher< int > watcher
gzip write("uncompressed data")
QSharedPointer< T > other(t)
[5]
QNetworkAccessManager manager
QNetworkRequest request(url)
\inmodule QtCore
Definition quuid.h:58