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
qbluetoothdevicediscoveryagent_darwin.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
6
8
9#ifdef Q_OS_MACOS
12
13#include <IOBluetooth/IOBluetooth.h>
14#endif // Q_OS_MACOS
15
17#include "darwin/btnotifier_p.h"
18#include "darwin/btutility_p.h"
19#include "darwin/uistrings_p.h"
20#include "qbluetoothhostinfo.h"
21#include "darwin/uistrings_p.h"
22#include "qbluetoothaddress.h"
23#include "darwin/btraii_p.h"
24#include "qbluetoothuuid.h"
25
26#include <QtCore/qloggingcategory.h>
27#include <QtCore/qcoreapplication.h>
28#include <QtCore/qpermissions.h>
29#include <QtCore/qvector.h>
30#include <QtCore/qglobal.h>
31#include <QtCore/qstring.h>
32#include <QtCore/qdebug.h>
33
34#include <Foundation/Foundation.h>
35
36#include <CoreBluetooth/CoreBluetooth.h>
37
39
40namespace
41{
42
44{
45 static bool initDone = false;
46 if (!initDone) {
47 qRegisterMetaType<QBluetoothDeviceInfo>();
48 qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::Error>();
49 initDone = true;
50 }
51}
52
53} //namespace
54
57 adapterAddress(adapter),
58 agentState(NonActive),
59 lowEnergySearchTimeout(DarwinBluetooth::defaultLEScanTimeoutMS),
60#ifdef Q_OS_MACOS
61 requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod),
62#else
63 requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod),
64#endif // Q_OS_MACOS
65 q_ptr(q)
66{
68
69 Q_ASSERT_X(q != nullptr, Q_FUNC_INFO, "invalid q_ptr (null)");
70}
71
73{
74 if (inquiryLE && agentState != NonActive) {
75 // We want the LE scan to stop as soon as possible.
76 if (dispatch_queue_t leQueue = DarwinBluetooth::qt_LE_queue()) {
77 // Local variable to be retained ...
78 DarwinBTLEDeviceInquiry *inq = inquiryLE.getAs<DarwinBTLEDeviceInquiry>();
79 dispatch_sync(leQueue, ^{
80 [inq stop];
81 });
82 }
83 }
84}
85
87{
88 if (startPending)
89 return true;
90
91 if (stopPending)
92 return false;
93
94 return agentState != NonActive;
95}
96
97void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
98{
99 using namespace DarwinBluetooth;
100
101 Q_ASSERT(!isActive());
104
105#ifdef Q_OS_MACOS
106 if (!controller) {
107 IOBluetoothHostController *hostController = [IOBluetoothHostController defaultController];
108 if (!hostController) {
109 qCWarning(QT_BT_DARWIN) << "No default Bluetooth controller found";
111 emit q_ptr->errorOccurred(lastError);
112 return;
113 } else if ([hostController powerState] != kBluetoothHCIPowerStateON) {
114 qCWarning(QT_BT_DARWIN) << "Default Bluetooth controller is OFF";
116 emit q_ptr->errorOccurred(lastError);
117 return;
118 } else if (!adapterAddress.isNull()) {
119 // If user has provided the local address, check that it matches with the actual
120 NSString *const hciAddress = [hostController addressAsString];
121 if (adapterAddress != QBluetoothAddress(QString::fromNSString(hciAddress))) {
122 qCWarning(QT_BT_DARWIN) << "Provided address" << adapterAddress
123 << "does not match with adapter:" << hciAddress;
125 emit q_ptr->errorOccurred(lastError);
126 return;
127 }
128 }
129 controller.reset(hostController, DarwinBluetooth::RetainPolicy::doInitialRetain);
130 }
131#endif // Q_OS_MACOS
132
133 // To be able to scan for devices, iOS requires Info.plist containing
134 // NSBluetoothAlwaysUsageDescription entry with a string, explaining
135 // the usage of Bluetooth interface. macOS also requires this description,
136 // starting from Monterey.
137
138 // No Classic on iOS, and Classic does not require a description on macOS:
140 const auto permissionStatus = qApp->checkPermission(QBluetoothPermission{});
141 if (permissionStatus != Qt::PermissionStatus::Granted) {
142 qCWarning(QT_BT_DARWIN,
143 "Use of Bluetooth LE requires explicitly requested permissions.");
145 emit q_ptr->errorOccurred(lastError);
146 // Arguably, Classic scan is still possible, but let's keep the logic
147 // simple.
148 return;
149 }
150 }
151
152 requestedMethods = methods;
153
154 if (stopPending) {
155 startPending = true;
156 return;
157 }
158
159 // This function (re)starts the scan(s) from the scratch;
160 // starting from Classic if it's in 'methods' (or LE scan if not).
161
162 agentState = NonActive;
163 discoveredDevices.clear();
165#ifdef Q_OS_MACOS
167 return startClassic();
168#endif // Q_OS_MACOS
169
170 startLE();
171}
172
173#ifdef Q_OS_MACOS
174
175void QBluetoothDeviceDiscoveryAgentPrivate::startClassic()
176{
177 Q_ASSERT(!isActive());
180 Q_ASSERT(agentState == NonActive);
181
183
184 if (!inquiry) {
185 // The first Classic scan for this DDA.
186 inquiry.reset([[DarwinBTClassicDeviceInquiry alloc] initWithDelegate:this],
187 DarwinBluetooth::RetainPolicy::noInitialRetain);
188
189 if (!inquiry) {
190 qCCritical(QT_BT_DARWIN) << "failed to initialize an Classic device inquiry";
193 emit q_ptr->errorOccurred(lastError);
194 return;
195 }
196 }
197
198 agentState = ClassicScan;
199
200 const IOReturn res = [inquiry.getAs<DarwinBTClassicDeviceInquiry>() start];
201 if (res != kIOReturnSuccess) {
203 agentState = NonActive;
204 emit q_ptr->errorOccurred(lastError);
205 }
206}
207
208#endif // Q_OS_MACOS
209
210void QBluetoothDeviceDiscoveryAgentPrivate::startLE()
211{
214
215 using namespace DarwinBluetooth;
216
217 std::unique_ptr<LECBManagerNotifier> notifier = std::make_unique<LECBManagerNotifier>();
218 // Connections:
219 using ErrMemFunPtr = void (LECBManagerNotifier::*)(QBluetoothDeviceDiscoveryAgent::Error);
220 notifier->connect(notifier.get(), ErrMemFunPtr(&LECBManagerNotifier::CBManagerError),
221 this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError);
222 notifier->connect(notifier.get(), &LECBManagerNotifier::LEnotSupported,
223 this, &QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported);
224 notifier->connect(notifier.get(), &LECBManagerNotifier::discoveryFinished,
225 this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished);
226 using DeviceMemFunPtr = void (QBluetoothDeviceDiscoveryAgentPrivate::*)(const QBluetoothDeviceInfo &);
227 notifier->connect(notifier.get(), &LECBManagerNotifier::deviceDiscovered,
228 this, DeviceMemFunPtr(&QBluetoothDeviceDiscoveryAgentPrivate::deviceFound));
229
230 // Check queue and create scanner:
231 inquiryLE.reset([[DarwinBTLEDeviceInquiry alloc] initWithNotifier:notifier.get()],
232 DarwinBluetooth::RetainPolicy::noInitialRetain);
233 if (inquiryLE)
234 notifier.release(); // Whatever happens next, inquiryLE is already the owner ...
235
236 dispatch_queue_t leQueue(qt_LE_queue());
237 if (!leQueue || !inquiryLE) {
240 agentState = NonActive;
241 emit q_ptr->errorOccurred(lastError);
242 return;
243 }
244
245 // Now start in on LE queue:
246 agentState = LEScan;
247 // We need the local variable so that it's retained ...
248 DarwinBTLEDeviceInquiry *inq = inquiryLE.getAs<DarwinBTLEDeviceInquiry>();
249 dispatch_async(leQueue, ^{
250 [inq startWithTimeout:lowEnergySearchTimeout];
251 });
252}
253
255{
256 Q_ASSERT_X(isActive(), Q_FUNC_INFO, "called whithout active inquiry");
258 Q_FUNC_INFO, "called with invalid bluetooth adapter");
259
260 using namespace DarwinBluetooth;
261
262 const bool prevStart = startPending;
263 startPending = false;
264 stopPending = true;
265
267
268#ifdef Q_OS_MACOS
269 if (agentState == ClassicScan) {
270 const IOReturn res = [inquiry.getAs<DarwinBTClassicDeviceInquiry>() stop];
271 if (res != kIOReturnSuccess) {
272 qCWarning(QT_BT_DARWIN) << "failed to stop";
273 startPending = prevStart;
274 stopPending = false;
276 emit q_ptr->errorOccurred(lastError);
277 }
278 } else {
279#else
280 {
281 Q_UNUSED(prevStart);
282#endif // Q_OS_MACOS
283 dispatch_queue_t leQueue(qt_LE_queue());
284 Q_ASSERT(leQueue);
285 // We need the local variable so that it's retained ...
286 DarwinBTLEDeviceInquiry *inq = inquiryLE.getAs<DarwinBTLEDeviceInquiry>();
287 dispatch_sync(leQueue, ^{
288 [inq stop];
289 });
290 // We consider LE scan to be stopped immediately and
291 // do not care about this LEDeviceInquiry object anymore.
292 LEinquiryFinished();
293 }
294}
295
296#ifdef Q_OS_MACOS
297
298void QBluetoothDeviceDiscoveryAgentPrivate::inquiryFinished()
299{
300 // The subsequent start(LE) function (if any)
301 // will (re)set the correct state.
302 agentState = NonActive;
303
304 if (stopPending && !startPending) {
305 stopPending = false;
306 emit q_ptr->canceled();
307 } else if (startPending) {
308 startPending = false;
309 stopPending = false;
310 start(requestedMethods);
311 } else {
312 // We can be here _only_ if a classic scan
313 // finished in a normal way (not cancelled)
314 // and requestedMethods includes LowEnergyMethod.
315 // startLE() will take care of old devices
316 // not supporting Bluetooth 4.0.
318 startLE();
319 else
320 emit q_ptr->finished();
321 }
322}
323
324void QBluetoothDeviceDiscoveryAgentPrivate::error(IOReturn error)
325{
326 startPending = false;
327 stopPending = false;
328
330
331 emit q_ptr->errorOccurred(lastError);
332}
333
334void QBluetoothDeviceDiscoveryAgentPrivate::classicDeviceFound(void *obj)
335{
336 auto device = static_cast<IOBluetoothDevice *>(obj);
337 Q_ASSERT_X(device, Q_FUNC_INFO, "invalid IOBluetoothDevice (nil)");
338
339 Q_ASSERT_X(agentState == ClassicScan, Q_FUNC_INFO,
340 "invalid agent state (expected classic scan)");
341
343
344 // Let's collect some info about this device:
345 const QBluetoothAddress deviceAddress(DarwinBluetooth::qt_address([device getAddress]));
346 if (deviceAddress.isNull()) {
347 qCWarning(QT_BT_DARWIN) << "invalid Bluetooth address";
348 return;
349 }
350
352 if (device.name)
353 deviceName = QString::fromNSString(device.name);
354
355 const auto classOfDevice = qint32(device.classOfDevice);
356
357 QBluetoothDeviceInfo deviceInfo(deviceAddress, deviceName, classOfDevice);
358 deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration);
359 deviceInfo.setRssi(device.RSSI);
360
361 const QList<QBluetoothUuid> uuids(DarwinBluetooth::extract_services_uuids(device));
362 deviceInfo.setServiceUuids(uuids);
363
364 deviceFound(deviceInfo);
365}
366
367void QBluetoothDeviceDiscoveryAgentPrivate::setError(IOReturn error, const QString &text)
368{
369 if (error == kIOReturnSuccess)
371 else if (error == kIOReturnNoPower)
373 else
375}
376
377#endif // Q_OS_MACOS
378
379void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAgent::Error error, const QString &text)
380{
381 lastError = error;
382
383 if (!text.isEmpty()) {
384 errorString = text;
385 } else {
386 switch (lastError) {
388 errorString = QString();
389 break;
392 break;
395 break;
398 break;
401 break;
404 break;
406 default:
408 }
409 }
410}
411
412void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error)
413{
417
418 inquiryLE.reset();
419
420 startPending = false;
421 stopPending = false;
422 agentState = NonActive;
424 emit q_ptr->errorOccurred(lastError);
425}
426
427void QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported()
428{
429 qCDebug(QT_BT_DARWIN) << "no Bluetooth LE support";
430
431#ifdef Q_OS_MACOS
432 if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) {
433 // Having both Classic | LE means this is not an error.
434 LEinquiryFinished();
435 } else {
436 // In the past this was never an error, that's why we have
437 // LEnotSupported as a special method. But now, since
438 // we can have separate Classic/LE scans, we have to report it
439 // as UnsupportedDiscoveryMethod.
441 }
442#else
443 inquiryLE.reset();
444 startPending = false;
445 stopPending = false;
447 emit q_ptr->errorOccurred(lastError);
448#endif
449}
450
451void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished()
452{
453 // The same logic as in inquiryFinished, but does not start LE scan.
454 agentState = NonActive;
455 inquiryLE.reset();
456
457 if (stopPending && !startPending) {
458 stopPending = false;
459 emit q_ptr->canceled();
460 } else if (startPending) {
461 startPending = false;
462 stopPending = false;
463 start(requestedMethods); //Start again.
464 } else {
465 emit q_ptr->finished();
466 }
467}
468
469void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(const QBluetoothDeviceInfo &newDeviceInfo)
470{
471 // Core Bluetooth does not allow us to access addresses, we have to use uuid instead.
472 // This uuid has nothing to do with uuids in Bluetooth in general (it's generated by
473 // Apple's framework using some algorithm), but it's a 128-bit uuid after all.
474 const bool isLE =
475#ifdef Q_OS_MACOS
476 newDeviceInfo.coreConfigurations() == QBluetoothDeviceInfo::LowEnergyCoreConfiguration;
477#else
478 true;
479#endif // Q_OS_MACOS
480 for (qsizetype i = 0, e = discoveredDevices.size(); i < e; ++i) {
481 if (isLE) {
482 if (discoveredDevices[i].deviceUuid() == newDeviceInfo.deviceUuid()) {
483 QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
484 if (discoveredDevices[i].rssi() != newDeviceInfo.rssi()) {
485 qCDebug(QT_BT_DARWIN) << "Updating RSSI for" << newDeviceInfo.address()
486 << newDeviceInfo.rssi();
487 discoveredDevices[i].setRssi(newDeviceInfo.rssi());
488 updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI);
489 }
490
491 if (discoveredDevices[i].manufacturerData() != newDeviceInfo.manufacturerData()) {
492 qCDebug(QT_BT_DARWIN) << "Updating manufacturer data for" << newDeviceInfo.address();
493 const QList<quint16> keys = newDeviceInfo.manufacturerIds();
494 for (auto key: keys)
495 discoveredDevices[i].setManufacturerData(key, newDeviceInfo.manufacturerData(key));
497 }
498
499 if (discoveredDevices[i].serviceData() != newDeviceInfo.serviceData()) {
500 qCDebug(QT_BT_DARWIN) << "Updating service data for" << newDeviceInfo.address();
501 const QList<QBluetoothUuid> keys = newDeviceInfo.serviceIds();
502 for (auto key : keys)
503 discoveredDevices[i].setServiceData(key, newDeviceInfo.serviceData(key));
504 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ServiceData);
505 }
506
507 if (lowEnergySearchTimeout > 0) {
508 if (discoveredDevices[i] != newDeviceInfo) {
509 discoveredDevices.replace(i, newDeviceInfo);
510 emit q_ptr->deviceDiscovered(newDeviceInfo);
511 } else {
512 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
513 emit q_ptr->deviceUpdated(discoveredDevices[i], updatedFields);
514 }
515
516 return;
517 }
518
519 discoveredDevices.replace(i, newDeviceInfo);
520 emit q_ptr->deviceDiscovered(newDeviceInfo);
521
522 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
523 emit q_ptr->deviceUpdated(discoveredDevices[i], updatedFields);
524
525 return;
526 }
527 } else {
528#ifdef Q_OS_MACOS
529 if (discoveredDevices[i].address() == newDeviceInfo.address()) {
530 if (discoveredDevices[i] == newDeviceInfo)
531 return;
532
533 discoveredDevices.replace(i, newDeviceInfo);
534 emit q_ptr->deviceDiscovered(newDeviceInfo);
535 return;
536 }
537#else
538 Q_UNREACHABLE();
539#endif // Q_OS_MACOS
540 }
541 }
542
543 discoveredDevices.append(newDeviceInfo);
544 emit q_ptr->deviceDiscovered(newDeviceInfo);
545}
546
547QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods()
548{
549#ifdef Q_OS_MACOS
550 return ClassicMethod | LowEnergyMethod;
551#else
552 return LowEnergyMethod;
553#endif // Q_OS_MACOS
554}
555
static JNINativeMethod methods[]
quint8 rssi
QBluetoothUuid deviceUuid
DarwinBluetooth::LECBManagerNotifier * notifier
IOBluetoothDevice * device
#define QT_BT_MAC_AUTORELEASEPOOL
Definition btutility_p.h:78
\inmodule QtBluetooth
void start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent)
static DiscoveryMethods supportedDiscoveryMethods()
This function returns the discovery methods supported by the current platform.
void errorOccurred(QBluetoothDeviceDiscoveryAgent::Error error)
This signal is emitted when an error occurs during Bluetooth device discovery.
void canceled()
This signal is emitted when device discovery is aborted by a call to stop().
void finished()
This signal is emitted when Bluetooth device discovery completes.
void deviceUpdated(const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields)
This signal is emitted when the agent receives additional information about the Bluetooth device desc...
void deviceDiscovered(const QBluetoothDeviceInfo &info)
This signal is emitted when the Bluetooth device described by info is discovered.
Error
Indicates all possible error conditions found during Bluetooth device discovery.
\inmodule QtBluetooth
Access Bluetooth peripherals.
static QString translate(const char *context, const char *key, const char *disambiguation=nullptr, int n=-1)
\threadsafe
qsizetype size() const noexcept
Definition qlist.h:397
void replace(qsizetype i, parameter_type t)
Definition qlist.h:543
void append(parameter_type t)
Definition qlist.h:458
void clear()
Definition qlist.h:434
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2960
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
bool isEmpty() const noexcept
Returns true if the string has no characters; otherwise returns false.
Definition qstring.h:192
#define this
Definition dialogs.cpp:9
QString text
QList< QBluetoothUuid > extract_services_uuids(IOBluetoothDevice *device)
const int defaultLEScanTimeoutMS
Definition btutility.mm:30
QString qt_address(NSString *address)
Definition btutility.mm:36
void qt_test_iobluetooth_runloop()
Definition btutility.mm:125
dispatch_queue_t qt_LE_queue()
Definition btutility.mm:324
Combined button and popup list for selecting options.
QString deviceName()
#define Q_FUNC_INFO
#define qApp
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
static QDBusError::ErrorType get(const char *name)
#define qCCritical(category,...)
#define qCWarning(category,...)
#define qCDebug(category,...)
GLuint64 key
GLuint start
GLhandleARB obj
[2]
GLuint res
GLuint GLuint64EXT address
GLdouble GLdouble GLdouble GLdouble q
Definition qopenglext.h:259
static void setError(QJsonObject *response, const QString &msg)
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define Q_ASSERT_X(cond, x, msg)
Definition qrandom.cpp:48
const char DD_NOT_STOPPED[]
Definition uistrings.cpp:22
const char DD_NOT_STARTED[]
Definition uistrings.cpp:20
const char DD_POWERED_OFF[]
Definition uistrings.cpp:14
const char DD_NOT_STARTED_LE[]
Definition uistrings.cpp:21
const char DD_NOTSUPPORTED[]
Definition uistrings.cpp:17
const char DD_UNKNOWN_ERROR[]
Definition uistrings.cpp:18
QT_BEGIN_NAMESPACE const char DEV_DISCOVERY[]
Definition uistrings.cpp:13
const char DD_INVALID_ADAPTER[]
Definition uistrings.cpp:15
const char DD_IO[]
Definition uistrings.cpp:16
const char DD_MISSING_PERMISSION[]
Definition uistrings.cpp:19
#define emit
#define Q_UNUSED(x)
int qint32
Definition qtypes.h:49
ptrdiff_t qsizetype
Definition qtypes.h:165
QStringList keys