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
btledeviceinquiry.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
7#include "btnotifier_p.h"
8#include "btutility_p.h"
9
10#include <QtCore/qloggingcategory.h>
11#include <QtCore/qdebug.h>
12#include <QtCore/qendian.h>
13#include <QtCore/qlist.h>
14
15#include <algorithm>
16
17QT_BEGIN_NAMESPACE
18
19namespace DarwinBluetooth {
20
21QBluetoothUuid qt_uuid(NSUUID *nsUuid)
22{
23 if (!nsUuid)
24 return QBluetoothUuid();
25
26 uuid_t uuidData = {};
27 [nsUuid getUUIDBytes:uuidData];
28 QUuid::Id128Bytes qtUuidData = {};
29 std::copy(uuidData, uuidData + 16, qtUuidData.data);
30 return QBluetoothUuid(qtUuidData);
31}
32
33const int timeStepMS = 100;
34const int powerOffTimeoutMS = 30000;
35
37 // That's what CoreBluetooth has:
38 // CBAdvertisementDataLocalNameKey
39 // CBAdvertisementDataTxPowerLevelKey
40 // CBAdvertisementDataServiceUUIDsKey
41 // CBAdvertisementDataServiceDataKey
42 // CBAdvertisementDataManufacturerDataKey
43 // CBAdvertisementDataOverflowServiceUUIDsKey
44 // CBAdvertisementDataIsConnectable
45 // CBAdvertisementDataSolicitedServiceUUIDsKey
46
47 // For now, we "parse":
48 QString localName;
52 // TODO: other keys probably?
53 AdvertisementData(NSDictionary *AdvertisementData);
54};
55
56AdvertisementData::AdvertisementData(NSDictionary *advertisementData)
57{
58 if (!advertisementData)
59 return;
60
61 // ... constant CBAdvertisementDataLocalNameKey ...
62 // NSString containing the local name of a peripheral.
63 NSObject *value = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
64 if (value && [value isKindOfClass:[NSString class]])
65 localName = QString::fromNSString(static_cast<NSString *>(value));
66
67 // ... constant CBAdvertisementDataServiceUUIDsKey ...
68 // A list of one or more CBUUID objects, representing CBService UUIDs.
69
70 value = [advertisementData objectForKey:CBAdvertisementDataServiceUUIDsKey];
71 if (value && [value isKindOfClass:[NSArray class]]) {
72 NSArray *uuids = static_cast<NSArray *>(value);
73 for (CBUUID *cbUuid in uuids)
74 serviceUuids << qt_uuid(cbUuid);
75 }
76
78 if (advdict) {
81 }];
82 }
83
85 if (value && [value isKindOfClass:[NSData class]]) {
86 QByteArray data = QByteArray::fromNSData(static_cast<NSData *>(value));
88 }
89}
90
91}
92
93QT_END_NAMESPACE
94
95QT_USE_NAMESPACE
96
97@interface DarwinBTLEDeviceInquiry (PrivateAPI)
98- (void)stopScanSafe;
99- (void)stopNotifier;
100@end
101
102@implementation DarwinBTLEDeviceInquiry
103{
104 LECBManagerNotifier *notifier;
105 ObjCScopedPointer<CBCentralManager> manager;
106
107 QList<QBluetoothDeviceInfo> devices;
108 LEInquiryState internalState;
109 int inquiryTimeoutMS;
110
111 QT_PREPEND_NAMESPACE(DarwinBluetooth)::GCDTimer elapsedTimer;
112}
113
114-(id)initWithNotifier:(LECBManagerNotifier *)aNotifier
115{
116 if (self = [super init]) {
117 Q_ASSERT(aNotifier);
118 notifier = aNotifier;
119 internalState = InquiryStarting;
120 inquiryTimeoutMS = DarwinBluetooth::defaultLEScanTimeoutMS;
121 }
122
123 return self;
124}
125
126- (void)dealloc
127{
128 [self stopScanSafe];
129 [manager setDelegate:nil];
130 [elapsedTimer cancelTimer];
131 [self stopNotifier];
132 [super dealloc];
133}
134
135- (void)timeout:(id)sender
136{
137 Q_UNUSED(sender);
138
139 if (internalState == InquiryActive) {
140 [self stopScanSafe];
141 [manager setDelegate:nil];
142 internalState = InquiryFinished;
143 Q_ASSERT(notifier);
144 emit notifier->discoveryFinished();
145 } else if (internalState == InquiryStarting) {
146 // This is interesting on iOS only, where the system shows an alert
147 // asking to enable Bluetooth in the 'Settings' app. If not done yet
148 // (after 30 seconds) - we consider this as an error.
149 [manager setDelegate:nil];
150 internalState = ErrorPoweredOff;
151 Q_ASSERT(notifier);
152 emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
153 }
154}
155
156- (void)startWithTimeout:(int)timeout
157{
158 dispatch_queue_t leQueue(DarwinBluetooth::qt_LE_queue());
159 Q_ASSERT(leQueue);
160 inquiryTimeoutMS = timeout;
161 manager.reset([[CBCentralManager alloc] initWithDelegate:self queue:leQueue],
162 DarwinBluetooth::RetainPolicy::noInitialRetain);
163}
164
165- (void)centralManagerDidUpdateState:(CBCentralManager *)central
166{
167#pragma clang diagnostic push
168#pragma clang diagnostic ignored "-Wunguarded-availability-new"
169
170 if (central != manager)
171 return;
172
173 if (internalState != InquiryActive && internalState != InquiryStarting)
174 return;
175
176 Q_ASSERT(notifier);
177
178 using namespace DarwinBluetooth;
179
180 const auto state = central.state;
181 if (state == CBManagerStatePoweredOn) {
182 if (internalState == InquiryStarting) {
183 internalState = InquiryActive;
184
185 if (inquiryTimeoutMS > 0) {
186 [elapsedTimer cancelTimer];
187 elapsedTimer.reset([[DarwinBTGCDTimer alloc] initWithDelegate:self], RetainPolicy::noInitialRetain);
188 [elapsedTimer startWithTimeout:inquiryTimeoutMS step:timeStepMS];
189 }
190
191 // ### Qt 6.x: remove the use of env. variable, as soon as a proper public API is in place.
192 bool envOk = false;
193 const int env = qEnvironmentVariableIntValue("QT_BLUETOOTH_SCAN_ENABLE_DUPLICATES", &envOk);
194 if (envOk && env) {
195 [manager scanForPeripheralsWithServices:nil
196 options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}];
197 } else {
198 [manager scanForPeripheralsWithServices:nil options:nil];
199 }
200 } // Else we ignore.
201 } else if (state == CBManagerStateUnsupported) {
202 if (internalState == InquiryActive) {
203 [self stopScanSafe];
204 // Not sure how this is possible at all,
205 // probably, can never happen.
206 internalState = ErrorPoweredOff;
207 emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
208 } else {
209 internalState = ErrorLENotSupported;
210 emit notifier->LEnotSupported();
211 }
212 [manager setDelegate:nil];
213 } else if (state == CBManagerStateUnauthorized) {
214 if (internalState == InquiryActive)
215 [self stopScanSafe];
216 internalState = ErrorNotAuthorized;
217 emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::MissingPermissionsError);
218 [manager setDelegate:nil];
219 } else if (state == CBManagerStatePoweredOff) {
220
221#ifndef Q_OS_MACOS
222 if (internalState == InquiryStarting) {
223 // On iOS a user can see at this point an alert asking to
224 // enable Bluetooth in the "Settings" app. If a user does so,
225 // we'll receive 'PoweredOn' state update later.
226 // No change in internalState. Wait for 30 seconds.
227 [elapsedTimer cancelTimer];
228 elapsedTimer.reset([[DarwinBTGCDTimer alloc] initWithDelegate:self], RetainPolicy::noInitialRetain);
229 [elapsedTimer startWithTimeout:powerOffTimeoutMS step:300];
230 return;
231 }
232#else
233 Q_UNUSED(powerOffTimeoutMS);
234#endif // Q_OS_MACOS
235 [elapsedTimer cancelTimer];
236 [self stopScanSafe];
237 [manager setDelegate:nil];
238 internalState = ErrorPoweredOff;
239 // On macOS we report PoweredOffError and our C++ owner will delete us
240 // (here we're kwnon as 'self'). Connection is Qt::QueuedConnection so we
241 // are apparently safe to call -stopNotifier after the signal.
242 emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
243 [self stopNotifier];
244 } else {
245 // The following two states we ignore (from Apple's docs):
246 //"
247 // -CBCentralManagerStateUnknown
248 // The current state of the central manager is unknown;
249 // an update is imminent.
250 //
251 // -CBCentralManagerStateResetting
252 // The connection with the system service was momentarily
253 // lost; an update is imminent. "
254 // Wait for this imminent update.
255 }
256
257#pragma clang diagnostic pop
258}
259
260- (void)stopScanSafe
261{
262 // CoreBluetooth warns about API misused if we call stopScan in a state
263 // other than powered on. Hence this 'Safe' ...
264 if (!manager)
265 return;
266
267#pragma clang diagnostic push
268#pragma clang diagnostic ignored "-Wunguarded-availability-new"
269
270 if (internalState == InquiryActive) {
271 const auto state = manager.get().state;
272 if (state == CBManagerStatePoweredOn)
273 [manager stopScan];
274 }
275
276#pragma clang diagnostic pop
277}
278
279- (void)stopNotifier
280{
281 if (notifier) {
282 notifier->disconnect();
283 notifier->deleteLater();
284 notifier = nullptr;
285 }
286}
287
288- (void)stop
289{
290 [self stopScanSafe];
291 [manager setDelegate:nil];
292 [elapsedTimer cancelTimer];
293 [self stopNotifier];
294 internalState = InquiryCancelled;
295}
296
297- (void)centralManager:(CBCentralManager *)central
298 didDiscoverPeripheral:(CBPeripheral *)peripheral
299 advertisementData:(NSDictionary *)advertisementData
300 RSSI:(NSNumber *)RSSI
301{
302 using namespace DarwinBluetooth;
303
304 if (central != manager)
305 return;
306
307 if (internalState != InquiryActive)
308 return;
309
310 if (!notifier)
311 return;
312
313 QBluetoothUuid deviceUuid;
314
315 if (!peripheral.identifier) {
316 qCWarning(QT_BT_DARWIN) << "peripheral without NSUUID";
317 return;
318 }
319
320 deviceUuid = DarwinBluetooth::qt_uuid(peripheral.identifier);
321
322 if (deviceUuid.isNull()) {
323 qCWarning(QT_BT_DARWIN) << "no way to address peripheral, QBluetoothUuid is null";
324 return;
325 }
326
327 const AdvertisementData qtAdvData(advertisementData);
328 QString name(qtAdvData.localName);
329 if (!name.size() && peripheral.name)
330 name = QString::fromNSString(peripheral.name);
331
332 // TODO: fix 'classOfDevice' (0 for now).
333 QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0);
334 if (RSSI)
335 newDeviceInfo.setRssi([RSSI shortValue]);
336
337 if (qtAdvData.serviceUuids.size())
338 newDeviceInfo.setServiceUuids(qtAdvData.serviceUuids);
339
340 const QList<quint16> keysManufacturer = qtAdvData.manufacturerData.keys();
341 for (quint16 key : keysManufacturer)
342 newDeviceInfo.setManufacturerData(key, qtAdvData.manufacturerData.value(key));
343
344 const QList<QBluetoothUuid> keysService = qtAdvData.serviceData.keys();
345 for (QBluetoothUuid key : keysService)
346 newDeviceInfo.setServiceData(key, qtAdvData.serviceData.value(key));
347
348 // CoreBluetooth scans only for LE devices.
349 newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
350 emit notifier->deviceDiscovered(newDeviceInfo);
351}
352
353@end
const int powerOffTimeoutMS
QBluetoothUuid qt_uuid(IOBluetoothSDPUUID *uuid)
Definition btutility.mm:91
AdvertisementData(NSDictionary *AdvertisementData)
QHash< QBluetoothUuid, QByteArray > serviceData
QHash< quint16, QByteArray > manufacturerData