/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtBluetooth module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qlowenergycharacteristicdata.h" #include "qlowenergydescriptordata.h" #include "osxbtperipheralmanager_p.h" #include "qlowenergyservicedata.h" #include "osxbtnotifier_p.h" #include "qbluetooth.h" #include #include #include #include namespace { CBCharacteristicProperties cb_properties(const QLowEnergyCharacteristicData &data) { // Direct 'mapping' is ok. return CBCharacteristicProperties(int(data.properties())); } CBAttributePermissions cb_permissions(const QLowEnergyCharacteristicData &data) { using QLEC = QLowEnergyCharacteristic; const auto props = data.properties(); CBAttributePermissions cbFlags = {}; if ((props & QLEC::Write) || (props & QLEC::WriteNoResponse) || (props & QLEC::WriteSigned)) { cbFlags = CBAttributePermissions(cbFlags | CBAttributePermissionsWriteable); } if (props & QLEC::Read) cbFlags = CBAttributePermissions(cbFlags | CBAttributePermissionsReadable); if (data.writeConstraints() & QBluetooth::AttEncryptionRequired) cbFlags = CBAttributePermissions(cbFlags | CBAttributePermissionsWriteEncryptionRequired); if (data.readConstraints() & QBluetooth::AttEncryptionRequired) cbFlags = CBAttributePermissions(cbFlags | CBAttributePermissionsReadEncryptionRequired); return cbFlags; } ObjCStrongReference create_characteristic(const QLowEnergyCharacteristicData &data) { const ObjCStrongReference ch([[CBMutableCharacteristic alloc] initWithType:cb_uuid(data.uuid()) properties:cb_properties(data) value:nil permissions:cb_permissions(data)], false /*do not retain*/); return ch; } ObjCStrongReference create_descriptor(const QLowEnergyDescriptorData &data) { // CoreBluetooth supports only: /* "That said, only two of these are currently supported when creating local, mutable descriptors: the characteristic user description descriptor and the characteristic format descriptor, represented by the CBUUID constants CBUUIDCharacteristicUserDescriptionString and CBUUIDCharacteristicFormatString" */ if (data.uuid() != QBluetoothUuid::CharacteristicUserDescription && data.uuid() != QBluetoothUuid::CharacteristicPresentationFormat) { qCWarning(QT_BT_OSX) << "unsupported descriptor" << data.uuid(); return {}; } QT_BT_MAC_AUTORELEASEPOOL // Descriptors are immutable with CoreBluetooth, that's why we // have to provide a value here and not able to change it later. ObjCStrongReference value; if (data.uuid() == QBluetoothUuid::CharacteristicUserDescription) { const QString asQString(QString::fromUtf8(data.value())); value.reset(asQString.toNSString()); } else { const auto nsData = data_from_bytearray(data.value()); value.reset(nsData.data()); } const ObjCStrongReference d([[CBMutableDescriptor alloc] initWithType:cb_uuid(data.uuid()) value:value], false /*do not retain*/); return d; } quint32 qt_countGATTEntries(const QLowEnergyServiceData &data) { const auto maxu32 = std::numeric_limits::max(); // + 1 for a service itself. quint32 nEntries = 1 + quint32(data.includedServices().count()); for (const auto &ch : data.characteristics()) { if (maxu32 - 2 < nEntries) return {}; nEntries += 2; if (maxu32 - ch.descriptors().count() < nEntries) return {}; nEntries += ch.descriptors().count(); } return nEntries; } bool qt_validate_value_range(const QLowEnergyCharacteristicData &data) { if (data.minimumValueLength() > data.maximumValueLength() || data.minimumValueLength() < 0) { return false; } return data.value().size() <= data.maximumValueLength(); } } @interface QT_MANGLE_NAMESPACE(OSXBTPeripheralManager) (PrivateAPI) - (void)addConnectedCentral:(CBCentral *)central; - (void)removeConnectedCentral:(CBCentral *)central; - (CBService *)findIncludedService:(const QBluetoothUuid &)qtUUID; - (void)addIncludedServices:(const QLowEnergyServiceData &)data to:(CBMutableService *)cbService qtService:(QLowEnergyServicePrivate *)qtService; - (void)addCharacteristicsAndDescriptors:(const QLowEnergyServiceData &)data to:(CBMutableService *)cbService qtService:(QLowEnergyServicePrivate *)qtService; - (CBATTError)validateWriteRequest:(CBATTRequest *)request; @end @implementation QT_MANGLE_NAMESPACE(OSXBTPeripheralManager) - (id)initWith:(LECBManagerNotifier *)aNotifier { if (self = [super init]) { Q_ASSERT(aNotifier); notifier = aNotifier; state = PeripheralState::idle; nextServiceToAdd = {}; connectedCentrals.reset([[NSMutableSet alloc] init]); maxNotificationValueLength = std::numeric_limits::max(); } return self; } - (void)dealloc { [self detach]; [super dealloc]; } - (QSharedPointer)addService:(const QLowEnergyServiceData &)data { using QLES = QLowEnergyService; const auto nEntries = qt_countGATTEntries(data); if (!nEntries || nEntries > std::numeric_limits::max() - lastHandle) { qCCritical(QT_BT_OSX) << "addService: not enough handles"; return {}; } QT_BT_MAC_AUTORELEASEPOOL const BOOL primary = data.type() == QLowEnergyServiceData::ServiceTypePrimary; const auto cbUUID = cb_uuid(data.uuid()); const ObjCStrongReference newCBService([[CBMutableService alloc] initWithType:cbUUID primary:primary], false /*do not retain*/); if (!newCBService) { qCCritical(QT_BT_OSX) << "addService: failed to create CBMutableService"; return {}; } auto newQtService = QSharedPointer::create(); newQtService->state = QLowEnergyService::LocalService; newQtService->uuid = data.uuid(); newQtService->type = primary ? QLES::PrimaryService : QLES::IncludedService; newQtService->startHandle = ++lastHandle; // Controller will be set by ... controller :) [self addIncludedServices:data to:newCBService qtService:newQtService.data()]; [self addCharacteristicsAndDescriptors:data to:newCBService qtService:newQtService.data()]; services.push_back(newCBService); serviceIndex[data.uuid()] = newCBService; newQtService->endHandle = lastHandle; return newQtService; } - (void) setParameters:(const QLowEnergyAdvertisingParameters &)parameters data:(const QLowEnergyAdvertisingData &)data scanResponse:(const QLowEnergyAdvertisingData &)scanResponse { Q_UNUSED(parameters) // This is the last method we call on the controller's thread // before starting advertising on the Qt's LE queue. // From Apple's docs: /* - (void)startAdvertising:(NSDictionary *)advertisementData Advertises peripheral manager data. * advertisementData - An optional dictionary containing the data you want to advertise. The possible keys of an advertisementData dictionary are detailed in CBCentralManagerDelegate Protocol Reference. That said, only two of the keys are supported for peripheral manager objects: CBAdvertisementDataLocalNameKey and CBAdvertisementDataServiceUUIDsKey. */ QT_BT_MAC_AUTORELEASEPOOL advertisementData.reset([[NSMutableDictionary alloc] init]); if (!advertisementData) { qCWarning(QT_BT_OSX) << "setParameters: failed to allocate " "NSMutableDictonary (advertisementData)"; return; } auto localName = scanResponse.localName(); if (!localName.size()) localName = data.localName(); if (localName.size()) { [advertisementData setObject:localName.toNSString() forKey:CBAdvertisementDataLocalNameKey]; } if (!data.services().count() && !scanResponse.services().count()) return; const ObjCScopedPointer uuids([[NSMutableArray alloc] init]); if (!uuids) { qCWarning(QT_BT_OSX) << "setParameters: failed to allocate " "NSMutableArray (services uuids)"; return; } for (const auto &qtUUID : data.services()) { const auto cbUUID = cb_uuid(qtUUID); if (cbUUID) [uuids addObject:cbUUID]; } for (const auto &qtUUID : scanResponse.services()) { const auto cbUUID = cb_uuid(qtUUID); if (cbUUID) [uuids addObject:cbUUID]; } if ([uuids count]) { [advertisementData setObject:uuids forKey:CBAdvertisementDataServiceUUIDsKey]; } } - (void)startAdvertising { state = PeripheralState::waitingForPowerOn; if (manager) [manager setDelegate:nil]; manager.reset([[CBPeripheralManager alloc] initWithDelegate:self queue:OSXBluetooth::qt_LE_queue()]); } - (void)stopAdvertising { [manager stopAdvertising]; state = PeripheralState::idle; } - (void)detach { if (notifier) { notifier->disconnect(); notifier->deleteLater(); notifier = nullptr; } if (state == PeripheralState::advertising) { [manager stopAdvertising]; [manager setDelegate:nil]; state = PeripheralState::idle; } } - (void)write:(const QByteArray &)value charHandle:(QLowEnergyHandle)charHandle { if (!notifier) return; QT_BT_MAC_AUTORELEASEPOOL if (!charMap.contains(charHandle) || !valueRanges.contains(charHandle)) { emit notifier->CBManagerError(QLowEnergyController::UnknownError); return; } const auto & range = valueRanges[charHandle]; if (value.size() < int(range.first) || value.size() > int(range.second) #ifdef Q_OS_IOS || value.size() > OSXBluetooth::maxValueLength) { #else ) { #endif qCWarning(QT_BT_OSX) << "ignoring value of invalid length" << value.size(); return; } emit notifier->characteristicWritten(charHandle, value); const auto nsData = mutable_data_from_bytearray(value); charValues[charHandle] = nsData; // We copy data here: sending update requests is async (see sendUpdateRequests), // by the time we're allowed to actually send them, the data can change again // and we'll send an 'out of order' value. const ObjCStrongReference copy([NSData dataWithData:nsData], true); updateQueue.push_back(UpdateRequest{charHandle, copy}); [self sendUpdateRequests]; } - (void) addServicesToPeripheral { Q_ASSERT(manager); if (nextServiceToAdd < services.size()) [manager addService:services[nextServiceToAdd++]]; } // CBPeripheralManagerDelegate: - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { if (peripheral != manager || !notifier) return; #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) if (peripheral.state == CBManagerStatePoweredOn) { #else if (peripheral.state == CBPeripheralManagerStatePoweredOn) { #endif // "Bluetooth is currently powered on and is available to use." if (state == PeripheralState::waitingForPowerOn) { [manager removeAllServices]; nextServiceToAdd = {}; state = PeripheralState::advertising; [self addServicesToPeripheral]; } return; } /* "A state with a value lower than CBPeripheralManagerStatePoweredOn implies that advertising has stopped and that any connected centrals have been disconnected." */ [connectedCentrals removeAllObjects]; if (state == PeripheralState::advertising) { state = PeripheralState::waitingForPowerOn; } else if (state == PeripheralState::connected) { state = PeripheralState::idle; emit notifier->disconnected(); } // The next four states are _below_ "powered off"; according to the docs: /* "In addition, the local database is cleared and all services must be explicitly added again." */ #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) if (peripheral.state == CBManagerStateUnauthorized || peripheral.state == CBManagerStateUnsupported) { #else if (peripheral.state == CBPeripheralManagerStateUnauthorized || peripheral.state == CBPeripheralManagerStateUnsupported) { #endif emit notifier->LEnotSupported(); state = PeripheralState::idle; } } - (void)peripheralManager:(CBPeripheralManager *)peripheral willRestoreState:(NSDictionary *)dict { Q_UNUSED(peripheral) Q_UNUSED(dict) // NOOP atm. } - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error { if (peripheral != manager || !notifier) return; if (error) { NSLog(@"failed to start advertising, error: %@", error); state = PeripheralState::idle; emit notifier->CBManagerError(QLowEnergyController::AdvertisingError); } } - (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error { Q_UNUSED(service) if (peripheral != manager || !notifier) return; if (error) { NSLog(@"failed to add a service, error: %@", error); emit notifier->CBManagerError(QLowEnergyController::AdvertisingError); state = PeripheralState::idle; return; } if (nextServiceToAdd == services.size()) [manager startAdvertising:[advertisementData count] ? advertisementData.data() : nil]; else [self addServicesToPeripheral]; } - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic { Q_UNUSED(characteristic) if (peripheral != manager || !notifier) return; [self addConnectedCentral:central]; if (const auto handle = charMap.key(characteristic)) emit notifier->notificationEnabled(handle, true); } - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic { Q_UNUSED(characteristic) if (peripheral != manager || !notifier) return; [self removeConnectedCentral:central]; if (![connectedCentrals count]) { if (const auto handle = charMap.key(characteristic)) emit notifier->notificationEnabled(handle, false); } } - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request { if (peripheral != manager || !notifier) return; QT_BT_MAC_AUTORELEASEPOOL const auto handle = charMap.key(request.characteristic); if (!handle || !charValues.contains(handle)) { qCWarning(QT_BT_OSX) << "invalid read request, unknown characteristic"; [manager respondToRequest:request withResult:CBATTErrorInvalidHandle]; return; } const auto &value = charValues[handle]; if (request.offset > [value length]) { qCWarning(QT_BT_OSX) << "invalid offset in a read request"; [manager respondToRequest:request withResult:CBATTErrorInvalidOffset]; return; } [self addConnectedCentral:request.central]; NSData *dataToSend = nil; if (!request.offset) { dataToSend = value; } else { dataToSend = [value subdataWithRange: NSMakeRange(request.offset, [value length] - request.offset)]; } request.value = dataToSend; [manager respondToRequest:request withResult:CBATTErrorSuccess]; } - (void)writeValueForCharacteristic:(QLowEnergyHandle) charHandle withWriteRequest:(CBATTRequest *)request { Q_ASSERT(charHandle); Q_ASSERT(request); Q_ASSERT(valueRanges.contains(charHandle)); const auto &range = valueRanges[charHandle]; Q_ASSERT(request.offset <= range.second && request.value.length <= range.second - request.offset); Q_ASSERT(charValues.contains(charHandle)); NSMutableData *const value = charValues[charHandle]; if (request.offset + request.value.length > value.length) [value increaseLengthBy:request.offset + request.value.length - value.length]; [value replaceBytesInRange:NSMakeRange(request.offset, request.value.length) withBytes:request.value.bytes]; } - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests { QT_BT_MAC_AUTORELEASEPOOL if (peripheral != manager || !notifier) { // Detached already. return; } // We first test if all requests are valid // since CoreBluetooth requires "all or none" // and respond only _once_ to the first one. for (CBATTRequest *request in requests) { const auto status = [self validateWriteRequest:request]; if (status != CBATTErrorSuccess) { [manager respondToRequest:[requests objectAtIndex:0] withResult:status]; return; } } std::map updated; for (CBATTRequest *request in requests) { // Transition to 'connected' if needed. [self addConnectedCentral:request.central]; const auto charHandle = charMap.key(request.characteristic); const auto prevLen = updated[charHandle]; updated[charHandle] = std::max(request.offset + request.value.length, prevLen); [self writeValueForCharacteristic:charHandle withWriteRequest:request]; } for (const auto pair : updated) { const auto handle = pair.first; NSMutableData *value = charValues[handle]; value.length = pair.second; emit notifier->characteristicUpdated(handle, qt_bytearray(value)); const ObjCStrongReference copy([NSData dataWithData:value], true); updateQueue.push_back(UpdateRequest{handle, copy}); } if (requests.count) { [manager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess]; } [self sendUpdateRequests]; } - (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral { if (peripheral != manager || !notifier) { // Detached. return; } [self sendUpdateRequests]; } - (void)sendUpdateRequests { QT_BT_MAC_AUTORELEASEPOOL while (updateQueue.size()) { const auto &request = updateQueue.front(); if (charMap.contains(request.charHandle)) { if ([connectedCentrals count] && maxNotificationValueLength < [request.value length]) { qCWarning(QT_BT_OSX) << "value of length" << [request.value length] << "will possibly be truncated to" << maxNotificationValueLength; } const BOOL res = [manager updateValue:request.value forCharacteristic:static_cast(charMap[request.charHandle]) onSubscribedCentrals:nil]; if (!res) { // Have to wait for the 'ManagerIsReadyToUpdate'. break; } } updateQueue.pop_front(); } } // Private API: - (void)addConnectedCentral:(CBCentral *)central { if (!central) return; if (!notifier) { // We were detached. return; } maxNotificationValueLength = std::min(maxNotificationValueLength, central.maximumUpdateValueLength); QT_BT_MAC_AUTORELEASEPOOL if (state == PeripheralState::advertising) { state = PeripheralState::connected; [manager stopAdvertising]; emit notifier->connected(); } if (![connectedCentrals containsObject:central.identifier]) [connectedCentrals addObject:central.identifier]; } - (void)removeConnectedCentral:(CBCentral *)central { if (!notifier) { // Detached. return; } QT_BT_MAC_AUTORELEASEPOOL if ([connectedCentrals containsObject:central.identifier]) [connectedCentrals removeObject:central.identifier]; if (state == PeripheralState::connected && ![connectedCentrals count]) { state = PeripheralState::idle; emit notifier->disconnected(); } if (![connectedCentrals count]) maxNotificationValueLength = std::numeric_limits::max(); } - (CBService *)findIncludedService:(const QBluetoothUuid &)qtUUID { const auto it = serviceIndex.find(qtUUID); if (it == serviceIndex.end()) return nil; return it->second; } - (void)addIncludedServices:(const QLowEnergyServiceData &)data to:(CBMutableService *)cbService qtService:(QLowEnergyServicePrivate *)qtService { Q_ASSERT(cbService); Q_ASSERT(qtService); QT_BT_MAC_AUTORELEASEPOOL ObjCScopedPointer included([[NSMutableArray alloc] init]); if (!included) { qCWarning(QT_BT_OSX) << "addIncludedSerivces: failed " "to allocate NSMutableArray"; return; } for (auto includedService : data.includedServices()) { if (CBService *cbs = [self findIncludedService:includedService->serviceUuid()]) { [included addObject:cbs]; qtService->includedServices << includedService->serviceUuid(); ++lastHandle; } else { qCWarning(QT_BT_OSX) << "can not use" << includedService->serviceUuid() << "as included, it has to be added first"; } } if ([included count]) cbService.includedServices = included; } - (void)addCharacteristicsAndDescriptors:(const QLowEnergyServiceData &)data to:(CBMutableService *)cbService qtService:(QLowEnergyServicePrivate *)qtService { Q_ASSERT(cbService); Q_ASSERT(qtService); QT_BT_MAC_AUTORELEASEPOOL ObjCScopedPointer newCBChars([[NSMutableArray alloc] init]); if (!newCBChars) { qCWarning(QT_BT_OSX) << "addCharacteristicsAndDescritptors: " "failed to allocate NSMutableArray " "(characteristics)"; return; } for (const auto &ch : data.characteristics()) { if (!qt_validate_value_range(ch)) { qCWarning(QT_BT_OSX) << "addCharacteristicsAndDescritptors: " "invalid value size/min-max length"; continue; } #ifdef Q_OS_IOS if (ch.value().length() > OSXBluetooth::maxValueLength) { qCWarning(QT_BT_OSX) << "addCharacteristicsAndDescritptors: " "value exceeds the maximal permitted " "value length (" << OSXBluetooth::maxValueLength << "octets) on the platform"; continue; } #endif const auto cbChar(create_characteristic(ch)); if (!cbChar) { qCWarning(QT_BT_OSX) << "addCharacteristicsAndDescritptors: " "failed to allocate a characteristic"; continue; } const auto nsData(mutable_data_from_bytearray(ch.value())); if (!nsData) { qCWarning(QT_BT_OSX) << "addCharacteristicsAndDescritptors: " "addService: failed to allocate NSData (char value)"; continue; } [newCBChars addObject:cbChar]; const auto declHandle = ++lastHandle; // CB part: charMap[declHandle] = cbChar; charValues[declHandle] = nsData; valueRanges[declHandle] = ValueRange(ch.minimumValueLength(), ch.maximumValueLength()); // QT part: QLowEnergyServicePrivate::CharData charData; charData.valueHandle = declHandle; charData.uuid = ch.uuid(); charData.properties = ch.properties(); charData.value = ch.value(); const ObjCScopedPointer newCBDescs([[NSMutableArray alloc] init]); if (!newCBDescs) { qCWarning(QT_BT_OSX) << "addCharacteristicsAndDescritptors: " "failed to allocate NSMutableArray " "(descriptors)"; continue; } for (const auto &desc : ch.descriptors()) { // CB part: const auto cbDesc(create_descriptor(desc)); const auto descHandle = ++lastHandle; if (cbDesc) { // See comments in create_descriptor on // why cbDesc can be nil. [newCBDescs addObject:cbDesc]; } // QT part: QLowEnergyServicePrivate::DescData descData; descData.uuid = desc.uuid(); descData.value = desc.value(); charData.descriptorList.insert(descHandle, descData); } if ([newCBDescs count]) cbChar.data().descriptors = newCBDescs.data(); // retains qtService->characteristicList.insert(declHandle, charData); } if ([newCBChars count]) cbService.characteristics = newCBChars.data(); } - (CBATTError)validateWriteRequest:(CBATTRequest *)request { Q_ASSERT(request); QT_BT_MAC_AUTORELEASEPOOL const auto handle = charMap.key(request.characteristic); if (!handle || !charValues.contains(handle)) return CBATTErrorInvalidHandle; Q_ASSERT(valueRanges.contains(handle)); const auto &range = valueRanges[handle]; if (request.offset > range.second) return CBATTErrorInvalidOffset; if (request.value.length > range.second - request.offset) return CBATTErrorInvalidAttributeValueLength; return CBATTErrorSuccess; } @end