Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
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
18Q_DECLARE_METATYPE(QLowEnergyHandle)
19
20QT_BEGIN_NAMESPACE
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
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).
54 Q_ASSERT(type != OperationTimeout::none);
55 Q_UNUSED(type);
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
70QT_END_NAMESPACE
71
72QT_USE_NAMESPACE
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;
79- (void)stopWatchers;
80- (void)retrievePeripheralAndConnect;
81- (void)connectToPeripheral;
82- (void)discoverIncludedServices;
83- (void)readCharacteristics:(CBService *)service;
84- (void)serviceDetailsDiscoveryFinished:(CBService *)service;
85- (void)performNextRequest;
86- (void)performNextReadRequest;
87- (void)performNextWriteRequest;
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
106using DiscoveryMode = QLowEnergyService::DiscoveryMode;
107
108@implementation DarwinBTCentralManager
109{
110@private
111 CBCentralManager *manager;
112 DarwinBluetooth::CentralManagerState managerState;
113 bool disconnectPending;
114
115 QBluetoothUuid deviceUuid;
116
117 DarwinBluetooth::LECBManagerNotifier *notifier;
118
119 // Quite a verbose service discovery machinery
120 // (a "graph traversal").
121 DarwinBluetooth::ObjCStrongReference<NSMutableArray> servicesToVisit;
122 // The service we're discovering now (included services discovery):
123 NSUInteger currentService;
124 // Included services, we'll iterate through at the end of 'servicesToVisit':
125 DarwinBluetooth::ObjCStrongReference<NSMutableArray> servicesToVisitNext;
126 // We'd like to avoid loops in a services' topology:
127 DarwinBluetooth::ObjCStrongReference<NSMutableSet> visitedServices;
128
129 QMap<QBluetoothUuid, DiscoveryMode> servicesToDiscoverDetails;
130
131 DarwinBluetooth::ServiceHash serviceMap;
132 DarwinBluetooth::CharHash charMap;
133 DarwinBluetooth::DescHash descMap;
134
135 QLowEnergyHandle lastValidHandle;
136
137 bool requestPending;
138 DarwinBluetooth::RequestQueue requests;
139 QLowEnergyHandle currentReadHandle;
140
141 DarwinBluetooth::ValueHash valuesToWrite;
142
143 qint64 timeoutMS;
144 std::vector<DarwinBluetooth::GCDTimer> timeoutWatchdogs;
145
146 CBPeripheral *peripheral;
147 int lastKnownMtu;
148}
149
150- (id)initWith:(DarwinBluetooth::LECBManagerNotifier *)aNotifier
151{
152 using namespace DarwinBluetooth;
153
154 if (self = [super init]) {
155 manager = nil;
156 managerState = CentralManagerIdle;
157 disconnectPending = false;
158 peripheral = nil;
159 notifier = aNotifier;
160 currentService = 0;
161 lastValidHandle = 0;
162 requestPending = false;
163 currentReadHandle = 0;
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)
169 timeoutMS = value;
170 }
171
172 if (!timeoutMS)
173 timeoutMS = 20000;
174
175 lastKnownMtu = defaultMtu;
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)
199 notifier->deleteLater();
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{
222 return DarwinBluetooth::qt_find_watchdog(timeoutWatchdogs, object, type) != timeoutWatchdogs.end();
223}
224
225- (void)stopWatchingAfter:(id)object operation:(DarwinBluetooth::OperationTimeout)type
226{
227 auto pos = DarwinBluetooth::qt_find_watchdog(timeoutWatchdogs, object, type);
228 if (pos != timeoutWatchdogs.end()) {
229 [*pos cancelTimer];
230 timeoutWatchdogs.erase(pos);
231 }
232}
233
234- (void)stopWatchers
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()) {
299 managerState = DarwinBluetooth::CentralManagerUpdating;
300 manager = [[CBCentralManager alloc] initWithDelegate:self queue:leQueue];
301 }
302
303 if (!manager) {
304 managerState = DarwinBluetooth::CentralManagerIdle;
305 qCWarning(QT_BT_DARWIN) << "failed to allocate a central manager";
306 if (notifier)
307 emit notifier->CBManagerError(QLowEnergyController::ConnectionError);
308 }
309 } else if (managerState != DarwinBluetooth::CentralManagerUpdating) {
310 [self retrievePeripheralAndConnect];
311 }
312}
313
314- (void)retrievePeripheralAndConnect
315{
316 Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central manager (nil)");
317 Q_ASSERT_X(managerState == DarwinBluetooth::CentralManagerIdle,
318 Q_FUNC_INFO, "invalid state");
319
320 if ([self isConnected]) {
321 qCDebug(QT_BT_DARWIN) << "already connected";
322 if (notifier)
323 emit notifier->connected();
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)
339 emit notifier->CBManagerError(QLowEnergyController::ConnectionError);
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)
351 emit notifier->CBManagerError(QLowEnergyController::ConnectionError);
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)
362 emit notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError);
363 return;
364 }
365
366 peripheral = [static_cast<CBPeripheral *>([peripherals objectAtIndex:0]) retain];
367 [self connectToPeripheral];
368}
369
370- (void)connectToPeripheral
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)
382 emit notifier->connected();
383 } else {
384 [self setMtu:defaultMtu];
385 qCDebug(QT_BT_DARWIN) << "trying to connect";
386 managerState = CentralManagerConnecting;
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
403 if (managerState == DarwinBluetooth::CentralManagerUpdating) {
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.
411 emit notifier->disconnected();
412 }
413 } else {
414 disconnectPending = false;
415 if ([self isConnected])
416 managerState = DarwinBluetooth::CentralManagerDisconnecting;
417 else
418 managerState = DarwinBluetooth::CentralManagerIdle;
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:
443 managerState = CentralManagerDiscovering;
444 [self watchAfter:peripheral timeout:OperationTimeout::serviceDiscovery];
445 [peripheral discoverServices:nil];
446}
447
448- (void)discoverIncludedServices
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)
462 emit notifier->serviceDiscoveryFinished();
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];
472 managerState = CentralManagerDiscovering;
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)
511 emit notifier->CBManagerError(serviceUuid, QLowEnergyService::UnknownError);
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)
613 notifier->CBManagerError(serviceUuid, QLowEnergyService::OperationError);
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.
625 ++lastValidHandle;
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) {
635 ++lastValidHandle;
636 // Register this characteristic:
637 charMap[lastValidHandle] = c;
638 // Create a Qt's internal characteristic:
639 QLowEnergyServicePrivate::CharData newChar = {};
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:
652 ++lastValidHandle;
653 descMap[lastValidHandle] = d;
654 // Create a Qt's internal descriptor:
655 QLowEnergyServicePrivate::DescData newDesc = {};
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:
660 if (newDesc.uuid == QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration) {
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)
678 emit notifier->serviceDetailsDiscoveryFinished(qtService);
679}
680
681- (void)performNextRequest
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
702- (void)performNextReadRequest
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
740- (void)performNextWriteRequest
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) {
784 const QBluetoothUuid qtUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
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
842 Q_ASSERT(peripheral);
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,
872 QLowEnergyService::DescriptorWriteError);
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.
880 const QBluetoothUuid qtUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
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,
885 QLowEnergyService::DescriptorWriteError);
886 }
887 return;
888 }
889
890 LERequest request;
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,
912 QLowEnergyService::CharacteristicReadError);
913
914 }
915 return;
916 }
917
918 LERequest request;
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,
941 QLowEnergyService::CharacteristicWriteError);
942 }
943 return;
944 }
945
946 LERequest request;
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,
967 QLowEnergyService::DescriptorReadError);
968 }
969 return;
970 }
971
972 LERequest request;
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,
992 QLowEnergyService::DescriptorWriteError);
993 }
994 return;
995 }
996
997 LERequest request;
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();
1206 servicesToDiscoverDetails.clear();
1207 lastValidHandle = 0;
1208 serviceMap.clear();
1209 charMap.clear();
1210 descMap.clear();
1211 currentReadHandle = 0;
1212 [self stopWatchers];
1213 // TODO: also serviceToVisit/VisitNext and visitedServices ?
1214}
1215
1216- (void)handleReadWriteError:(NSError *)error
1217{
1218 Q_ASSERT(notifier);
1219
1220 switch (error.code) {
1221 case 0x05: // GATT_INSUFFICIENT_AUTHORIZATION
1222 case 0x0F: // GATT_INSUFFICIENT_ENCRYPTION
1223 emit notifier->CBManagerError(QLowEnergyController::AuthorizationError);
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) {
1252 managerState = CentralManagerIdle;
1253 // The LE is not supported or its usage was not authorized
1254 if (notifier) {
1255 if (state == CBManagerStateUnsupported)
1256 emit notifier->LEnotSupported();
1257 else
1258 emit notifier->CBManagerError(QLowEnergyController::MissingPermissionsError);
1259 }
1260 [self stopWatchers];
1261 return;
1262 }
1263
1264 if (state == CBManagerStatePoweredOff) {
1265 if (managerState == CentralManagerUpdating) {
1266 managerState = CentralManagerIdle;
1267 // I've seen this instead of Unsupported on OS X.
1268 if (notifier)
1269 emit notifier->LEnotSupported();
1270 } else {
1271 managerState = CentralManagerIdle;
1272 // TODO: we need a better error +
1273 // what will happen if later the state changes to PoweredOn???
1274 if (notifier)
1275 emit notifier->CBManagerError(QLowEnergyController::InvalidBluetoothAdapterError);
1276 }
1277 [self stopWatchers];
1278 return;
1279 }
1280
1281 if (state == CBManagerStatePoweredOn) {
1282 if (managerState == CentralManagerUpdating && !disconnectPending) {
1283 managerState = CentralManagerIdle;
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
1299 if (managerState != DarwinBluetooth::CentralManagerConnecting) {
1300 // We called cancel but before disconnected, managed to connect?
1301 return;
1302 }
1303
1304 void([self mtu]);
1305
1306 [peripheral setDelegate:self];
1307
1308 managerState = DarwinBluetooth::CentralManagerIdle;
1309 if (notifier)
1310 emit notifier->connected();
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
1320 if (managerState != DarwinBluetooth::CentralManagerConnecting) {
1321 // Canceled already.
1322 return;
1323 }
1324
1325 managerState = DarwinBluetooth::CentralManagerIdle;
1326 // TODO: better error mapping is required.
1327 if (notifier)
1328 notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError);
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
1340 if (error && managerState == DarwinBluetooth::CentralManagerDisconnecting) {
1341 managerState = DarwinBluetooth::CentralManagerIdle;
1342 qCWarning(QT_BT_DARWIN) << "failed to disconnect";
1343 if (notifier)
1344 emit notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError);
1345 } else {
1346 managerState = DarwinBluetooth::CentralManagerIdle;
1347 if (notifier)
1348 emit notifier->disconnected();
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
1372 managerState = CentralManagerIdle;
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)
1379 emit notifier->CBManagerError(QLowEnergyController::UnknownError);
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];
1406 managerState = DarwinBluetooth::CentralManagerIdle;
1407 if (notifier)
1408 emit notifier->servicesWereModified();
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];
1431 managerState = CentralManagerIdle;
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 ...
1438 if (!servicesToVisitNext)
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'?
1445 ++currentService;
1446
1447 for (const NSUInteger e = [servicesToVisit count]; currentService < e; ++currentService) {
1448 CBService *const s = [servicesToVisit objectAtIndex:currentService];
1449 if (![visitedServices containsObject:s]) {
1450 // Continue with discovery ...
1451 [visitedServices addObject:s];
1452 managerState = CentralManagerDiscovering;
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
1460 if (servicesToVisitNext && [servicesToVisitNext count]) {
1461 servicesToVisit.swap(servicesToVisitNext);
1462 servicesToVisitNext.reset();
1463
1464 currentService = 0;
1465 for (const NSUInteger e = [servicesToVisit count]; currentService < e; ++currentService) {
1466 CBService *const s = [servicesToVisit objectAtIndex:currentService];
1467 if (![visitedServices containsObject:s]) {
1468 [visitedServices addObject:s];
1469 managerState = CentralManagerDiscovering;
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)
1484 emit notifier->serviceDiscoveryFinished();
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).
1517 emit notifier->CBManagerError(qtUuid, QLowEnergyController::UnknownError);
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) {
1558 currentReadHandle = 0;
1559 requestPending = false;
1560 emit notifier->CBManagerError(qtUuid, QLowEnergyService::CharacteristicReadError);
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;
1597 currentReadHandle = 0;
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) {
1677 currentReadHandle = 0;
1678 requestPending = false;
1679 emit notifier->CBManagerError(qtUuid, QLowEnergyService::DescriptorReadError);
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) {
1722 currentReadHandle = 0;
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),
1769 QLowEnergyService::CharacteristicWriteError);
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),
1804 QLowEnergyService::DescriptorWriteError);
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
1830 const QBluetoothUuid qtUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
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),
1839 QLowEnergyService::DescriptorWriteError);
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);
1857 return emit notifier->CBManagerError(QLowEnergyController::RssiReadError);
1858 }
1859
1860 if (!RSSI) {
1861 qCWarning(QT_BT_DARWIN, "Reading RSSI returned no value");
1862 return emit notifier->CBManagerError(QLowEnergyController::RssiReadError);
1863 }
1864
1865 emit notifier->rssiUpdated(qint16([RSSI shortValue]));
1866}
1867
1868- (void)detach
1869{
1870 if (notifier) {
1871 notifier->disconnect();
1872 notifier->deleteLater();
1873 notifier = nullptr;
1874 }
1875
1876 [self stopWatchers];
1877 [self disconnectFromDevice];
1878}
1879
1880@end
#define QT_BT_MAC_AUTORELEASEPOOL
Definition btutility_p.h:78
ObjCStrongReference< NSError > qt_timeoutNSError(OperationTimeout type)
auto qt_find_watchdog(const std::vector< GCDTimer > &watchdogs, id object, OperationTimeout type)
NSUInteger qt_countGATTEntries(CBService *service)