diff options
Diffstat (limited to 'src/bluetooth/osx')
-rw-r--r-- | src/bluetooth/osx/osxbtconnectionmonitor.mm | 9 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtdeviceinquiry.mm | 19 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtsdpinquiry.mm | 160 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtservicerecord.mm | 50 |
4 files changed, 208 insertions, 30 deletions
diff --git a/src/bluetooth/osx/osxbtconnectionmonitor.mm b/src/bluetooth/osx/osxbtconnectionmonitor.mm index f41bbed5..159bceb5 100644 --- a/src/bluetooth/osx/osxbtconnectionmonitor.mm +++ b/src/bluetooth/osx/osxbtconnectionmonitor.mm @@ -96,6 +96,15 @@ using namespace QT_NAMESPACE; if (!device) return; + if (!monitor) { + // Rather surprising: monitor == nullptr means we stopped monitoring. + // So apparently this thingie is still alive and keeps receiving + // notifications. + qCWarning(QT_BT_OSX, + "Connection notification received in a monitor that was cancelled"); + return; + } + QT_BT_MAC_AUTORELEASEPOOL; // All Obj-C objects are autoreleased. diff --git a/src/bluetooth/osx/osxbtdeviceinquiry.mm b/src/bluetooth/osx/osxbtdeviceinquiry.mm index 2fd0d2db..4757331c 100644 --- a/src/bluetooth/osx/osxbtdeviceinquiry.mm +++ b/src/bluetooth/osx/osxbtdeviceinquiry.mm @@ -112,11 +112,14 @@ const uint8_t IOBlueoothInquiryLengthS = 15; m_active = true; [m_inquiry clearFoundDevices]; + + qCDebug(QT_BT_OSX) << "Starting device inquiry with" + << IOBlueoothInquiryLengthS << "second timeout limit."; const IOReturn result = [m_inquiry start]; if (result != kIOReturnSuccess) { // QtBluetooth will probably convert an error into UnknownError, // losing the actual information. - qCWarning(QT_BT_OSX) << "failed with IOKit error code:" << result; + qCWarning(QT_BT_OSX) << "device inquiry start failed with IOKit error code:" << result; m_active = false; } else { // Docs say it's 10 s. by default, we set it to 15 s. (see -initWithDelegate:), @@ -124,11 +127,14 @@ const uint8_t IOBlueoothInquiryLengthS = 15; watchDog.reset(new QTimer); watchDog->connect(watchDog.get(), &QTimer::timeout, watchDog.get(), [self]{ qCWarning(QT_BT_OSX, "Manually interrupting IOBluetoothDeviceInquiry"); + qCDebug(QT_BT_OSX) << "Found devices:" << [m_inquiry foundDevices]; [self stop]; }); watchDog->setSingleShot(true); - watchDog->setInterval(IOBlueoothInquiryLengthS * 2 * 1000); // Let's make it twice as long. + // Let's use 17 s. so that IOBluetooth, if it respects 'inquiryLength' + // has a chance to stop before we do: + watchDog->setInterval((IOBlueoothInquiryLengthS + 2) * 1000); watchDog->start(); } @@ -148,6 +154,8 @@ const uint8_t IOBlueoothInquiryLengthS = 15; - (void)deviceInquiryComplete:(IOBluetoothDeviceInquiry *)sender error:(IOReturn)error aborted:(BOOL)aborted { + qCDebug(QT_BT_OSX) << "deviceInquiryComplete, error:" << error + << "user-stopped:" << aborted; if (!m_active) return; @@ -158,11 +166,11 @@ const uint8_t IOBlueoothInquiryLengthS = 15; if (error != kIOReturnSuccess && !aborted) { // QtBluetooth has not too many error codes, 'UnknownError' is not really - // useful, report the actual error code here: + // useful, log the actual error code here: qCWarning(QT_BT_OSX) << "IOKit error code: " << error; - m_delegate->error(error); // Let watchDog to stop it, calling -stop at timeout, otherwise, - // it looks like inquiry continues and keeps reporting new devices found. + // it looks like inquiry continues even after this error and + // keeps reporting new devices found. } else { // Either a normal completion or from a timer slot. watchDog.reset(); @@ -174,6 +182,7 @@ const uint8_t IOBlueoothInquiryLengthS = 15; - (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry *)sender device:(IOBluetoothDevice *)device { + qCDebug(QT_BT_OSX) << "deviceInquiryDeviceFound:" << [device nameOrAddress]; if (sender != m_inquiry) // Can never happen in the current version. return; diff --git a/src/bluetooth/osx/osxbtsdpinquiry.mm b/src/bluetooth/osx/osxbtsdpinquiry.mm index a2b02b1a..e60b9787 100644 --- a/src/bluetooth/osx/osxbtsdpinquiry.mm +++ b/src/bluetooth/osx/osxbtsdpinquiry.mm @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2022 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtBluetooth module of the Qt Toolkit. @@ -43,8 +43,12 @@ #include "osxbtutility_p.h" #include "btdelegates_p.h" +#include <QtCore/qoperatingsystemversion.h> #include <QtCore/qvariant.h> #include <QtCore/qstring.h> +#include <QtCore/qtimer.h> + +#include <memory> QT_BEGIN_NAMESPACE @@ -52,6 +56,8 @@ namespace OSXBluetooth { namespace { +const int basebandConnectTimeoutMS = 20000; + QBluetoothUuid sdp_element_to_uuid(IOBluetoothSDPDataElement *element) { QT_BT_MAC_AUTORELEASEPOOL; @@ -78,11 +84,20 @@ QVector<QBluetoothUuid> extract_service_class_ID_list(IOBluetoothSDPServiceRecor QT_BT_MAC_AUTORELEASEPOOL; IOBluetoothSDPDataElement *const idList = [record getAttributeDataElement:kBluetoothSDPAttributeIdentifierServiceClassIDList]; - if (!idList || [idList getTypeDescriptor] != kBluetoothSDPDataElementTypeDataElementSequence) - return {}; QVector<QBluetoothUuid> uuids; - NSArray *const arr = [idList getArrayValue]; + if (!idList) + return uuids; + + NSArray *arr = nil; + if ([idList getTypeDescriptor] == kBluetoothSDPDataElementTypeDataElementSequence) + arr = [idList getArrayValue]; + else if ([idList getTypeDescriptor] == kBluetoothSDPDataElementTypeUUID) + arr = @[idList]; + + if (!arr) + return uuids; + for (IOBluetoothSDPDataElement *dataElement in arr) { const auto qtUuid = sdp_element_to_uuid(dataElement); if (!qtUuid.isNull()) @@ -200,7 +215,7 @@ QVector<QBluetoothUuid> extract_services_uuids(IOBluetoothDevice *device) return uuids; } -} +} // namespace OSXBluetooth QT_END_NAMESPACE @@ -213,6 +228,9 @@ using namespace OSXBluetooth; QT_PREPEND_NAMESPACE(DarwinBluetooth::SDPInquiryDelegate) *delegate; IOBluetoothDevice *device; bool isActive; + + // Needed to workaround a broken SDP on Monterey: + std::unique_ptr<QTimer> connectionWatchdog; } - (id)initWithDelegate:(DarwinBluetooth::SDPInquiryDelegate *)aDelegate @@ -242,17 +260,41 @@ using namespace OSXBluetooth; return [self performSDPQueryWithDevice:address filters:emptyFilter]; } +- (void)interruptSDPQuery +{ + // To be only executed on timer. + Q_ASSERT(connectionWatchdog.get()); + // If device was reset, so the timer should be, we can never be here then. + Q_ASSERT(device); + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + qCDebug(QT_BT_OSX) << "couldn't connect to device" << [device nameOrAddress] + << ", ending SDP inquiry."; + + // Stop the watchdog and close the connection as otherwise there could be + // later "connectionComplete" callbacks + connectionWatchdog->stop(); + [device closeConnection]; + + delegate->SDPInquiryError(device, kIOReturnTimeout); + [device release]; + device = nil; + isActive = false; +} + - (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address filters:(const QList<QBluetoothUuid> &)qtFilters { Q_ASSERT_X(!isActive, Q_FUNC_INFO, "SDP query in progress"); Q_ASSERT_X(!address.isNull(), Q_FUNC_INFO, "invalid target device address"); + qCDebug(QT_BT_OSX) << "Starting and SDP inquiry for address:" << address; QT_BT_MAC_AUTORELEASEPOOL; // We first try to allocate "filters": ObjCScopedPointer<NSMutableArray> array; - if (qtFilters.size()) { + if (QOperatingSystemVersion::current() <= QOperatingSystemVersion::MacOSBigSur + && qtFilters.size()) { // See the comment about filters on Monterey below. array.reset([[NSMutableArray alloc] init]); if (!array) { qCCritical(QT_BT_OSX) << "failed to allocate an uuid filter"; @@ -281,7 +323,65 @@ using namespace OSXBluetooth; ObjCScopedPointer<IOBluetoothDevice> oldDevice(device); device = newDevice.data(); + qCDebug(QT_BT_OSX) << "Device" << [device nameOrAddress] << "connected:" + << bool([device isConnected]) << "paired:" << bool([device isPaired]); IOReturn result = kIOReturnSuccess; + + if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur) { + // SDP query on Monterey does not follow its own documented/expected behavior: + // - a simple performSDPQuery was previously ensuring baseband connection + // to be opened, now it does not do so, instead logs a warning and returns + // immediately. + // - a version with UUID filters simply does nothing except it immediately + // returns kIOReturnSuccess. + + // If the device was not yet connected, connect it first + if (![device isConnected]) { + qCDebug(QT_BT_OSX) << "Device" << [device nameOrAddress] + << "is not connected, connecting it first"; + result = [device openConnection:self]; + // The connection may succeed immediately. But if it didn't, start a connection timer + // which has two guardian roles: + // 1. Guard against connect attempt taking too long time + // 2. Sometimes on Monterey the callback indicating "connection completion" is + // not received even though the connection has in fact succeeded + if (![device isConnected]) { + qCDebug(QT_BT_OSX) << "Starting connection monitor for device" + << [device nameOrAddress] << "with timeout limit of" + << basebandConnectTimeoutMS/1000 << "seconds."; + connectionWatchdog.reset(new QTimer); + connectionWatchdog->setSingleShot(false); + QObject::connect(connectionWatchdog.get(), &QTimer::timeout, + connectionWatchdog.get(), + [self] () { + qCDebug(QT_BT_OSX) << "Connection monitor timeout for device:" + << [device nameOrAddress] + << ", connected:" << bool([device isConnected]); + // Device can sometimes get properly connected without IOBluetooth + // calling the connectionComplete callback, so we check the status here + if ([device isConnected]) + [self connectionComplete:device status:kIOReturnSuccess]; + else + [self interruptSDPQuery]; + }); + connectionWatchdog->start(basebandConnectTimeoutMS); + } + } + + if ([device isConnected]) + result = [device performSDPQuery:self]; + + if (result != kIOReturnSuccess) { + qCCritical(QT_BT_OSX, "failed to start an SDP query"); + device = oldDevice.take(); + } else { + newDevice.take(); + isActive = true; + } + + return result; + } // Monterey's code path. + if (qtFilters.size()) result = [device performSDPQuery:self uuids:array]; else @@ -291,25 +391,53 @@ using namespace OSXBluetooth; qCCritical(QT_BT_OSX) << "failed to start an SDP query"; device = oldDevice.take(); } else { - isActive = true; newDevice.take(); + isActive = true; } return result; } -- (void)stopSDPQuery +- (void)connectionComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status { - // There is no API to stop it, - // but there is a 'stop' member-function in Qt and - // after it's called sdpQueryComplete must be somehow ignored. + qCDebug(QT_BT_OSX) << "connectionComplete for device" << [aDevice nameOrAddress] + << "with status:" << status; + if (aDevice != device) { + // Connection was previously cancelled, probably, due to the timeout. + return; + } + + // The connectionComplete may be invoked by either the IOBluetooth callback or our + // connection watchdog. In either case stop the watchdog if it exists + if (connectionWatchdog) + connectionWatchdog->stop(); + if (status == kIOReturnSuccess) + status = [aDevice performSDPQuery:self]; + + if (status != kIOReturnSuccess) { + isActive = false; + qCWarning(QT_BT_OSX, "failed to open connection or start an SDP query"); + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + delegate->SDPInquiryError(aDevice, status); + } +} + +- (void)stopSDPQuery +{ + // There is no API to stop it SDP on device, but there is a 'stop' + // member-function in Qt and after it's called sdpQueryComplete + // must be somehow ignored (device != aDevice in a callback). [device release]; device = nil; + isActive = false; + connectionWatchdog.reset(); } - (void)sdpQueryComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status { + qCDebug(QT_BT_OSX) << "sdpQueryComplete for device:" << [aDevice nameOrAddress] + << "with status:" << status; // Can happen - there is no legal way to cancel an SDP query, // after the 'reset' device can never be // the same as the cancelled one. @@ -320,6 +448,15 @@ using namespace OSXBluetooth; isActive = false; + // If we used the manual connection establishment, close the + // connection here. Otherwise the IOBluetooth may call stray + // connectionComplete or sdpQueryCompletes + if (connectionWatchdog) { + qCDebug(QT_BT_OSX) << "Closing the connection established for SDP inquiry."; + connectionWatchdog.reset(); + [device closeConnection]; + } + if (status != kIOReturnSuccess) delegate->SDPInquiryError(aDevice, status); else @@ -327,3 +464,4 @@ using namespace OSXBluetooth; } @end + diff --git a/src/bluetooth/osx/osxbtservicerecord.mm b/src/bluetooth/osx/osxbtservicerecord.mm index 23d55d13..2d868f63 100644 --- a/src/bluetooth/osx/osxbtservicerecord.mm +++ b/src/bluetooth/osx/osxbtservicerecord.mm @@ -146,7 +146,7 @@ void add_attribute(const QVariant &var, AttributeId key, Dictionary dict) return; const Number num(variant_to_nsnumber<ValueType>(var)); - [dict setObject:num forKey:[NSString stringWithFormat:@"%d", int(key)]]; + [dict setObject:num forKey:[NSString stringWithFormat:@"%x", int(key)]]; } template<> @@ -160,7 +160,7 @@ void add_attribute<QString>(const QVariant &var, AttributeId key, Dictionary dic const QString string(var.value<QString>()); if (string.length()) { if (NSString *const nsString = string.toNSString()) - [dict setObject:nsString forKey:[NSString stringWithFormat:@"%d", int(key)]]; + [dict setObject:nsString forKey:[NSString stringWithFormat:@"%x", int(key)]]; } } @@ -173,7 +173,7 @@ void add_attribute<QBluetoothUuid>(const QVariant &var, AttributeId key, Diction return; SDPUUid ioUUID(iobluetooth_uuid(var.value<QBluetoothUuid>())); - [dict setObject:ioUUID forKey:[NSString stringWithFormat:@"%d", int(key)]]; + [dict setObject:ioUUID forKey:[NSString stringWithFormat:@"%x", int(key)]]; } template<> @@ -207,6 +207,25 @@ void add_attribute(const QVariant &var, NSMutableArray *list) } template<> +void add_attribute<unsigned short>(const QVariant &var, NSMutableArray *list) +{ + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (!var.canConvert<unsigned short>()) + return; + + const Number num(variant_to_nsnumber<unsigned short>(var)); + + NSDictionary* dict = @{ + @"DataElementType" : [NSNumber numberWithInt:1], + @"DataElementSize" : [NSNumber numberWithInt:2], + @"DataElementValue" : num + }; + + [list addObject: dict]; +} + +template<> void add_attribute<QString>(const QVariant &var, NSMutableArray *list) { Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); @@ -273,7 +292,7 @@ void add_rfcomm_protocol_descriptor_list(uint16 channelID, Dictionary dict) [rfcommList addObject:rfcommDict]; [descriptorList addObject:rfcommList]; - [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%d", + [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%x", kBluetoothSDPAttributeIdentifierProtocolDescriptorList]]; } @@ -299,7 +318,7 @@ void add_l2cap_protocol_descriptor_list(uint16 psm, Dictionary dict) [l2capList addObject:l2capDict]; [descriptorList addObject:l2capList]; - [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%d", + [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%x", kBluetoothSDPAttributeIdentifierProtocolDescriptorList]]; } @@ -310,10 +329,10 @@ bool add_attribute(const QVariant &var, AttributeId key, NSMutableArray *list) if (var.canConvert<Sequence>()) return false; - if (var.canConvert<QString>()) { + if (var.type() == QVariant::String) { //ServiceName, ServiceDescription, ServiceProvider. add_attribute<QString>(var, list); - } else if (var.canConvert<QBluetoothUuid>()) { + } else if (var.userType() == qMetaTypeId<QBluetoothUuid>()) { add_attribute<QBluetoothUuid>(var, list); } else { // Here we need 'key' to understand the type. @@ -325,6 +344,9 @@ bool add_attribute(const QVariant &var, AttributeId key, NSMutableArray *list) case QSInfo::ServiceInfoTimeToLive: add_attribute<unsigned>(var, list); break; + case QSInfo::BluetoothProfileDescriptorList: + add_attribute<unsigned short>(var, list); + break; case QSInfo::ServiceAvailability: add_attribute<unsigned char>(var, list); break; @@ -348,10 +370,10 @@ bool add_attribute(const QBluetoothServiceInfo &serviceInfo, AttributeId key, Di if (var.canConvert<Sequence>()) return false; - if (var.canConvert<QString>()) { + if (var.type() == QVariant::String) { //ServiceName, ServiceDescription, ServiceProvider. add_attribute<QString>(var, key, dict); - } else if (var.canConvert<QBluetoothUuid>()) { + } else if (var.userType() == qMetaTypeId<QBluetoothUuid>()) { add_attribute<QBluetoothUuid>(serviceInfo.attribute(key), key, dict); } else { // We can have different integer types actually, so I have to check @@ -385,14 +407,15 @@ bool add_sequence_attribute(const QVariant &var, AttributeId key, NSMutableArray if (var.isNull() || !var.canConvert<Sequence>()) return false; + NSMutableArray *const nested = [NSMutableArray array]; + [list addObject:nested]; + const Sequence sequence(var.value<Sequence>()); for (const QVariant &var : sequence) { if (var.canConvert<Sequence>()) { - NSMutableArray *const nested = [NSMutableArray array]; add_sequence_attribute(var, key, nested); - [list addObject:nested]; } else { - add_attribute(var, key, list); + add_attribute(var, key, nested); } } @@ -415,8 +438,7 @@ bool add_sequence_attribute(const QBluetoothServiceInfo &serviceInfo, AttributeI if (!add_sequence_attribute(element, key, list)) add_attribute(element, key, list); } - [dict setObject:list forKey:[NSString stringWithFormat:@"%d", int(key)]]; - + [dict setObject:list forKey:[NSString stringWithFormat:@"%x", int(key)]]; return true; } |