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