diff options
72 files changed, 5297 insertions, 1383 deletions
diff --git a/.qmake.conf b/.qmake.conf index a2a0d418..fec66b73 100644 --- a/.qmake.conf +++ b/.qmake.conf @@ -1,3 +1,3 @@ load(qt_build_config) -MODULE_VERSION = 5.7.1 +MODULE_VERSION = 5.8.1 diff --git a/config.tests/bluez/bluez.pro b/config.tests/bluez/bluez.pro index bb5fa2dd..28dcadcb 100644 --- a/config.tests/bluez/bluez.pro +++ b/config.tests/bluez/bluez.pro @@ -1,8 +1 @@ -TEMPLATE = app - -CONFIG += link_pkgconfig -PKGCONFIG += bluez - -TARGET = bluez - SOURCES += main.cpp diff --git a/config.tests/bluez_le/bluez_le.pro b/config.tests/bluez_le/bluez_le.pro index 27b19a45..28dcadcb 100644 --- a/config.tests/bluez_le/bluez_le.pro +++ b/config.tests/bluez_le/bluez_le.pro @@ -1,8 +1 @@ -TEMPLATE = app - -CONFIG += link_pkgconfig -PKGCONFIG += bluez - -TARGET = bluez_le - SOURCES += main.cpp diff --git a/configure.json b/configure.json new file mode 100644 index 00000000..e63e6f2a --- /dev/null +++ b/configure.json @@ -0,0 +1,5 @@ +{ + "subconfigs": [ + "src/bluetooth" + ] +} diff --git a/dist/changes-5.8.0 b/dist/changes-5.8.0 new file mode 100644 index 00000000..4e013c13 --- /dev/null +++ b/dist/changes-5.8.0 @@ -0,0 +1,76 @@ +Qt 5.8 introduces many new features and improvements as well as bugfixes +over the 5.7.x series. For more details, refer to the online documentation +included in this distribution. The documentation is also available online: + + http://doc.qt.io/qt-5/index.html + +The Qt version 5.8 series is binary compatible with the 5.7.x series. +Applications compiled for 5.7 will continue to run with 5.8. + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + + https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* Library * +**************************************************************************** + +QtBluetooth +----------- + + - Added Bluetooth Low Energy peripheral role support for iOS/macOS. + - Added WinRT support for QBluetoothDeviceDiscoveryAgent and Bluetooth Low + Energy central role. + - [QTBUG-53012] Added API to set the timeout of Bluetooth Low Energy device + discoveries. + - Fixed minor documentation issues. + - [QTBUG-46253] Added API to select the device discovery mode utilized by + QBluetoothDeviceDiscoveryAgent (btle vs classic vs combined). + - [QTBUG-46377] Added preferredSecurityGlags() and setPreferredSecurityFlags() + methods to QBluetoothSocket. + - Adjusted QtBluetooth to the changes required by the Qt Lite build system. + +QtNfc +----- + + - [QTBUG-55297] Fixed ODR violation in QLlcpServer on Android. + +**************************************************************************** +* Platform Specific Changes * +**************************************************************************** + +Android +------- + + - [QTBUG-56625] Parsed list of advertised services found in LE scan + record and pass them to API client via QBluetoothDeviceInfo::serviceUuids() + - [QTBUG-55035] Added support for new runtime permission check when running Bluetooth + applications on Android 7.x. The Bluetooth stack requires Location permissions + when running a device discovery. + - [QTBUG-52692] Redesigned/Simplified Android central role implementation and prevented + blocking of service discovery in case the peripheral does not behave as per standard. + - [QTBUG-56625] Improved Low Energy device discovery by evaluating scan records and + feeding their content into QBluetoothDeviceInfo instances. + - [QTBUG-45066] Fixed crash in QBluetoothDeviceDiscoveryAgent ctor due to Java exception. + +iOS/macOS +-------- + + - Removed iOS v 6.x support from the code base + - [QTBUG-56898] Fixed a crash when writing GATT attributes. + - Adjusted code base to latest macOS and iOS releases. + - [QTBUG-53331] Forced error signal when calling + QBluetoothLocalDevice::requestPairing(). + - [QTBUG-52690] Added QLowEnergyController::remoteDeviceUuid() to expose the iOS/macOS + specific device UUID. Those two platforms do not expose Bluetooth addresses to + application developers. + +Linux/Bluez +----------- + + - Fixed build of sdpscanner due to incorrect build order + diff --git a/examples/bluetooth/heartlistener/heartrate.cpp b/examples/bluetooth/heartlistener/heartrate.cpp index 7077bf1c..c206b69e 100644 --- a/examples/bluetooth/heartlistener/heartrate.cpp +++ b/examples/bluetooth/heartlistener/heartrate.cpp @@ -50,6 +50,7 @@ HeartRate::HeartRate(): { //! [devicediscovery-1] m_deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); + m_deviceDiscoveryAgent->setLowEnergyDiscoveryTimeout(5000); connect(m_deviceDiscoveryAgent, SIGNAL(deviceDiscovered(const QBluetoothDeviceInfo&)), this, SLOT(addDevice(const QBluetoothDeviceInfo&))); @@ -73,7 +74,7 @@ void HeartRate::deviceSearch() qDeleteAll(m_devices); m_devices.clear(); //! [devicediscovery-2] - m_deviceDiscoveryAgent->start(); + m_deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); //! [devicediscovery-2] setMessage("Scanning for devices..."); } diff --git a/examples/bluetooth/lowenergyscanner/device.cpp b/examples/bluetooth/lowenergyscanner/device.cpp index 5ef2cf76..28a051f5 100644 --- a/examples/bluetooth/lowenergyscanner/device.cpp +++ b/examples/bluetooth/lowenergyscanner/device.cpp @@ -54,6 +54,7 @@ Device::Device(): { //! [les-devicediscovery-1] discoveryAgent = new QBluetoothDeviceDiscoveryAgent(); + discoveryAgent->setLowEnergyDiscoveryTimeout(5000); connect(discoveryAgent, SIGNAL(deviceDiscovered(const QBluetoothDeviceInfo&)), this, SLOT(addDevice(const QBluetoothDeviceInfo&))); connect(discoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), @@ -84,7 +85,7 @@ void Device::startDeviceDiscovery() setUpdate("Scanning for devices ..."); //! [les-devicediscovery-2] - discoveryAgent->start(); + discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); //! [les-devicediscovery-2] if (discoveryAgent->isActive()) { diff --git a/qtconnectivity.pro b/qtconnectivity.pro index b74830b8..dc0f77e3 100644 --- a/qtconnectivity.pro +++ b/qtconnectivity.pro @@ -1,7 +1,3 @@ requires(!android|qtHaveModule(androidextras)) -load(configure) -qtCompileTest(bluez) -qtCompileTest(bluez_le) -qtCompileTest(linux_crypto_api) load(qt_parts) diff --git a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java index 9fe88e9c..5c336dc0 100644 --- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java +++ b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java @@ -48,12 +48,17 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; import android.content.Context; +import android.os.Handler; +import android.os.Looper; import android.util.Log; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; import java.util.ArrayList; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; +import java.util.NoSuchElementException; import java.util.UUID; public class QtBluetoothLE { @@ -65,6 +70,34 @@ public class QtBluetoothLE { private String mRemoteGattAddress; private final UUID clientCharacteristicUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + /* + * The atomic synchronizes the timeoutRunnable thread and the response thread for the pending + * I/O job. Whichever thread comes first will pass the atomic gate. The other thread is + * cut short. + */ + private AtomicInteger handleForTimeout = new AtomicInteger(-1); // -1 implies not running + private final int RUNNABLE_TIMEOUT = 3000; // 3 seconds + private final Handler timeoutHandler = new Handler(Looper.getMainLooper()); + + private class TimeoutRunnable implements Runnable { + public TimeoutRunnable(int handle) { pendingJobHandle = handle; } + @Override + public void run() { + boolean timeoutStillValid = handleForTimeout.compareAndSet(pendingJobHandle, -1); + if (timeoutStillValid) { + Log.w(TAG, "****** Timeout for request on handle " + (pendingJobHandle & 0xffff)); + Log.w(TAG, "****** Looks like the characteristic or descriptor does NOT act in " + + "accordance to Bluetooth 4.x spec."); + Log.w(TAG, "****** Please check server implementation. Continuing under " + + "reservation."); + interruptCurrentIO(pendingJobHandle & 0xffff); + } + } + + // contains handle (0xffff) and top 2 byte contain the job type (0xffff0000) + private int pendingJobHandle = -1; + }; + /* Pointer to the Qt object that "owns" the Java object */ @SuppressWarnings({"CanBeFinal", "WeakerAccess"}) @@ -152,8 +185,12 @@ public class QtBluetoothLE { switch (status) { case BluetoothGatt.GATT_SUCCESS: errorCode = 0; break; //QLowEnergyController::NoError + case 8: // link loss + case 22: // BTA_GATT_CONN_LMP_TIMEOUT + Log.w(TAG, "Connection Error: Try to delay connect() call after previous activity"); + errorCode = 5; break; //QLowEnergyController::ConnectionError default: - Log.w(TAG, "Unhandled error code on connectionStateChanged: " + status); + Log.w(TAG, "Unhandled error code on connectionStateChanged: " + status + " " + newState); errorCode = status; break; //TODO deal with all errors } leConnectionStateChange(qtObject, errorCode, qLowEnergyController_State); @@ -182,64 +219,72 @@ public class QtBluetoothLE { android.bluetooth.BluetoothGattCharacteristic characteristic, int status) { - //runningHandle is only used during serviceDetailsDiscovery - //If it is -1 we got an update outside of the details discovery process - final boolean isServiceDiscoveryRun = (runningHandle != -1); - - if (status != BluetoothGatt.GATT_SUCCESS) { - Log.w(TAG, "onCharacteristicRead error: " + status); + int foundHandle = -1; + synchronized (this) { + foundHandle = handleForCharacteristic(characteristic); + if (foundHandle == -1 || foundHandle >= entries.size() ) { + Log.w(TAG, "Cannot find characteristic read request for read notification - handle: " + + foundHandle + " size: " + entries.size()); + + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; + } - // read errors during serviceDiscovery are ignored - if (isServiceDiscoveryRun) + performNextIO(); return; + } } - synchronized (this) { - if (uuidToEntry.isEmpty()) // ignore data if internal setup is not ready; - return; + boolean requestTimedOut = !handleForTimeout.compareAndSet( + modifiedReadWriteHandle(foundHandle, IoJobType.Read), -1); + if (requestTimedOut) { + Log.w(TAG, "Late char read reply after timeout was hit for handle " + foundHandle); + // Timeout has hit before this response -> ignore the response + // no need to unlock ioJobPending -> the timeout has done that already + return; } - // once we have a service discovery run we report regular changes - if (!isServiceDiscoveryRun) { + GattEntry entry = entries.get(foundHandle); + final boolean isServiceDiscoveryRun = !entry.valueKnown; + entry.valueKnown = true; - int foundHandle = -1; - synchronized (this) { - foundHandle = handleForCharacteristic(characteristic); + if (status == BluetoothGatt.GATT_SUCCESS) { + // Qt manages handles starting at 1, in Java we use a system starting with 0 + //TODO avoid sending service uuid -> service handle should be sufficient + leCharacteristicRead(qtObject, characteristic.getService().getUuid().toString(), + foundHandle + 1, characteristic.getUuid().toString(), + characteristic.getProperties(), characteristic.getValue()); + } else { + if (isServiceDiscoveryRun) { + Log.w(TAG, "onCharacteristicRead during discovery error: " + status); + + Log.d(TAG, "Non-readable characteristic " + characteristic.getUuid() + + " for service " + characteristic.getService().getUuid()); + leCharacteristicRead(qtObject, characteristic.getService().getUuid().toString(), + foundHandle + 1, characteristic.getUuid().toString(), + characteristic.getProperties(), characteristic.getValue()); + } else { + // This must be in sync with QLowEnergyService::CharacteristicReadError + final int characteristicReadError = 5; + leServiceError(qtObject, foundHandle + 1, characteristicReadError); } + } - synchronized (readWriteQueue) { - ioJobPending = false; - } + if (isServiceDiscoveryRun) { - if (foundHandle == -1) { - Log.w(TAG, "Out-of-detail-discovery: char update failed. " + - "Cannot find handle for characteristic"); - } else { - if (status == BluetoothGatt.GATT_SUCCESS) { - leCharacteristicRead(qtObject, characteristic.getService().getUuid().toString(), - foundHandle + 1, characteristic.getUuid().toString(), - characteristic.getProperties(), characteristic.getValue()); - } else { - // This must be in sync with QLowEnergyService::CharacteristicReadError - final int characteristicReadError = 5; - leServiceError(qtObject, foundHandle + 1, characteristicReadError); - } - } + // last entry of pending service discovery run -> send discovery finished state update + GattEntry serviceEntry = entries.get(entry.associatedServiceHandle); + if (serviceEntry.endHandle == foundHandle) + finishCurrentServiceDiscovery(entry.associatedServiceHandle); + } - performNextIO(); - return; + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; } - GattEntry entry = entries.get(runningHandle); - entry.valueKnown = true; - entries.set(runningHandle, entry); - - // Qt manages handles starting at 1, in Java we use a system starting with 0 - //TODO avoid sending service uuid -> service handle should be sufficient - leCharacteristicRead(qtObject, characteristic.getService().getUuid().toString(), - runningHandle + 1, characteristic.getUuid().toString(), - characteristic.getProperties(), characteristic.getValue()); - performServiceDetailDiscoveryForHandle(runningHandle + 1, false); + performNextIO(); } public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt, @@ -255,6 +300,15 @@ public class QtBluetoothLE { return; } + boolean requestTimedOut = !handleForTimeout.compareAndSet( + modifiedReadWriteHandle(handle, IoJobType.Write), -1); + if (requestTimedOut) { + Log.w(TAG, "Late char write reply after timeout was hit for handle " + handle); + // Timeout has hit before this response -> ignore the response + // no need to unlock ioJobPending -> the timeout has done that already + return; + } + int errorCode; //This must be in sync with QLowEnergyService::ServiceError switch (status) { @@ -287,81 +341,84 @@ public class QtBluetoothLE { android.bluetooth.BluetoothGattDescriptor descriptor, int status) { - //runningHandle is only used during serviceDetailsDiscovery - //If it is -1 we got an update outside of the details discovery process - final boolean isServiceDiscoveryRun = (runningHandle != -1); - - if (status != BluetoothGatt.GATT_SUCCESS) { - Log.w(TAG, "onDescriptorRead error: " + status); - - // read errors during serviceDiscovery are ignored - if (isServiceDiscoveryRun) - return; - } - + int foundHandle = -1; synchronized (this) { - if (uuidToEntry.isEmpty()) // ignore data if internal setup is not ready; + foundHandle = handleForDescriptor(descriptor); + if (foundHandle == -1 || foundHandle >= entries.size() ) { + Log.w(TAG, "Cannot find descriptor read request for read notification - handle: " + + foundHandle + " size: " + entries.size()); + + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; + } + performNextIO(); return; + } } + boolean requestTimedOut = !handleForTimeout.compareAndSet( + modifiedReadWriteHandle(foundHandle, IoJobType.Read), -1); + if (requestTimedOut) { + Log.w(TAG, "Late descriptor read reply after timeout was hit for handle " + + foundHandle); + // Timeout has hit before this response -> ignore the response + // no need to unlock ioJobPending -> the timeout has done that already + return; + } - if (!isServiceDiscoveryRun) { + GattEntry entry = entries.get(foundHandle); + final boolean isServiceDiscoveryRun = !entry.valueKnown; + entry.valueKnown = true; - int foundHandle = -1; - synchronized (this) { - foundHandle = handleForDescriptor(descriptor); + if (status == BluetoothGatt.GATT_SUCCESS) { + //TODO avoid sending service and characteristic uuid -> handles should be sufficient + leDescriptorRead(qtObject, descriptor.getCharacteristic().getService().getUuid().toString(), + descriptor.getCharacteristic().getUuid().toString(), foundHandle + 1, + descriptor.getUuid().toString(), descriptor.getValue()); + } else { + if (isServiceDiscoveryRun) { + //ignore + Log.w(TAG, "onDescriptorcRead during discovery error: " + status); + } else { + // This must be in sync with QLowEnergyService::DescriptorReadError + final int descriptorReadError = 6; + leServiceError(qtObject, foundHandle + 1, descriptorReadError); } - synchronized (readWriteQueue) { - ioJobPending = false; + } + + if (isServiceDiscoveryRun) { + // last entry of pending service discovery run? ->send discovery finished state update + GattEntry serviceEntry = entries.get(entry.associatedServiceHandle); + if (serviceEntry.endHandle == foundHandle) { + finishCurrentServiceDiscovery(entry.associatedServiceHandle); } - if (foundHandle == -1) { - Log.w(TAG, "Out-of-detail-discovery: char update failed. " + - "Cannot find handle for descriptor."); - } else { - if (status == BluetoothGatt.GATT_SUCCESS) { - leDescriptorRead(qtObject, descriptor.getCharacteristic().getService().getUuid().toString(), - descriptor.getCharacteristic().getUuid().toString(), foundHandle + 1, - descriptor.getUuid().toString(), descriptor.getValue()); - } else { - // This must be in sync with QLowEnergyService::DescriptorReadError - final int descriptorReadError = 6; - leServiceError(qtObject, foundHandle + 1, descriptorReadError); + /* Some devices preset ClientCharacteristicConfiguration descriptors + * to enable notifications out of the box. However the additional + * BluetoothGatt.setCharacteristicNotification call prevents + * automatic notifications from coming through. Hence we manually set them + * up here. + */ + if (descriptor.getUuid().compareTo(clientCharacteristicUuid) == 0) { + byte[] bytearray = descriptor.getValue(); + final int value = (bytearray != null && bytearray.length > 0) ? bytearray[0] : 0; + // notification or indication bit set? + if ((value & 0x03) > 0) { + Log.d(TAG, "Found descriptor with automatic notifications."); + mBluetoothGatt.setCharacteristicNotification( + descriptor.getCharacteristic(), true); } } - - performNextIO(); - return; } - - GattEntry entry = entries.get(runningHandle); - entry.valueKnown = true; - entries.set(runningHandle, entry); - //TODO avoid sending service and characteristic uuid -> handles should be sufficient - leDescriptorRead(qtObject, descriptor.getCharacteristic().getService().getUuid().toString(), - descriptor.getCharacteristic().getUuid().toString(), runningHandle+1, - descriptor.getUuid().toString(), descriptor.getValue()); - - /* Some devices preset ClientCharacteristicConfiguration descriptors - * to enable notifications out of the box. However the additional - * BluetoothGatt.setCharacteristicNotification call prevents - * automatic notifications from coming through. Hence we manually set them - * up here. - */ - - if (descriptor.getUuid().compareTo(clientCharacteristicUuid) == 0) { - final int value = descriptor.getValue()[0]; - // notification or indication bit set? - if ((value & 0x03) > 0) { - Log.d(TAG, "Found descriptor with automatic notifications."); - mBluetoothGatt.setCharacteristicNotification( - descriptor.getCharacteristic(), true); - } + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; } - performServiceDetailDiscoveryForHandle(runningHandle + 1, false); + performNextIO(); } public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt, @@ -373,6 +430,16 @@ public class QtBluetoothLE { int handle = handleForDescriptor(descriptor); + boolean requestTimedOut = !handleForTimeout.compareAndSet( + modifiedReadWriteHandle(handle, IoJobType.Write), -1); + if (requestTimedOut) { + Log.w(TAG, "Late descriptor write reply after timeout was hit for handle " + + handle); + // Timeout has hit before this response -> ignore the response + // no need to unlock ioJobPending -> the timeout has done that already + return; + } + int errorCode; //This must be in sync with QLowEnergyService::ServiceError switch (status) { @@ -413,7 +480,31 @@ public class QtBluetoothLE { return false; } - mBluetoothGatt = mRemoteGattDevice.connectGatt(qtContext, false, gattCallback); + try { + // BluetoothDevice.,connectGatt(Context, boolean, BluetoothGattCallback, int) was + // officially introduced by Android API v23. Earlier Android versions have a private + // implementation already though. Let's check at runtime and use it if possible. + // + // In general the new connectGatt() seems to be much more reliable than the function + // that doesn't specify the transport layer. + + Class[] args = new Class[4]; + args[0] = android.content.Context.class; + args[1] = boolean.class; + args[2] = android.bluetooth.BluetoothGattCallback.class; + args[3] = int.class; + Method connectMethod = mRemoteGattDevice.getClass().getDeclaredMethod("connectGatt", args); + if (connectMethod != null) { + mBluetoothGatt = (BluetoothGatt) connectMethod.invoke(mRemoteGattDevice, qtContext, + false, gattCallback, + 2 /*TRANSPORT_LE*/); + Log.w(TAG, "Using Android v23 BluetoothDevice.connectGatt()"); + } + } catch (Exception ex) { + // fallback to less reliable API 18 version + mBluetoothGatt = mRemoteGattDevice.connectGatt(qtContext, false, gattCallback); + } + return mBluetoothGatt != null; } @@ -440,7 +531,15 @@ public class QtBluetoothLE { public BluetoothGattService service = null; public BluetoothGattCharacteristic characteristic = null; public BluetoothGattDescriptor descriptor = null; - public int endHandle; + /* + * endHandle defined for GattEntryType.Service and GattEntryType.CharacteristicValue + * If the type is service this is the value of the last Gatt entry belonging to the very + * same service. If the type is a char value it is the entries index inside + * the "entries" list. + */ + public int endHandle = -1; + // pointer back to the handle that describes the service that this GATT entry belongs to + public int associatedServiceHandle; } private enum IoJobType @@ -456,8 +555,12 @@ public class QtBluetoothLE { public IoJobType jobType; } + // service uuid -> service handle mapping (there can be more than one service with same uuid) private final Hashtable<UUID, List<Integer>> uuidToEntry = new Hashtable<UUID, List<Integer>>(100); + // index into array is equivalent to handle id private final ArrayList<GattEntry> entries = new ArrayList<GattEntry>(100); + //backlog of to be discovered services + // TODO remove private final LinkedList<Integer> servicesToBeDiscovered = new LinkedList<Integer>(); @@ -557,10 +660,12 @@ public class QtBluetoothLE { GattEntry serviceEntry = new GattEntry(); serviceEntry.type = GattEntryType.Service; serviceEntry.service = service; - entries.add(entry); + entries.add(serviceEntry); // remember handle for the service for later update int serviceHandle = entries.size() - 1; + //point to itself -> mostly done for consistence reasons with other entries + serviceEntry.associatedServiceHandle = serviceHandle; //some devices may have more than one service with the same uuid List<Integer> old = uuidToEntry.get(service.getUuid()); @@ -575,11 +680,15 @@ public class QtBluetoothLE { entry = new GattEntry(); entry.type = GattEntryType.Characteristic; entry.characteristic = characteristic; + entry.associatedServiceHandle = serviceHandle; + //entry.endHandle = .. undefined entries.add(entry); // this emulates GATT value attributes entry = new GattEntry(); entry.type = GattEntryType.CharacteristicValue; + entry.associatedServiceHandle = serviceHandle; + entry.endHandle = entries.size(); // special case -> current index in entries list entries.add(entry); // add all descriptors @@ -588,30 +697,31 @@ public class QtBluetoothLE { entry = new GattEntry(); entry.type = GattEntryType.Descriptor; entry.descriptor = desc; + entry.associatedServiceHandle = serviceHandle; + //entry.endHandle = .. undefined entries.add(entry); } } // update endHandle of current service serviceEntry.endHandle = entries.size() - 1; - entries.set(serviceHandle, serviceEntry); } entries.trimToSize(); } - private int currentServiceInDiscovery = -1; - private int runningHandle = -1; - private void resetData() { synchronized (this) { - runningHandle = -1; - currentServiceInDiscovery = -1; uuidToEntry.clear(); entries.clear(); servicesToBeDiscovered.clear(); } + + // kill all timeout handlers + timeoutHandler.removeCallbacksAndMessages(null); + handleForTimeout.set(-1); + synchronized (readWriteQueue) { readWriteQueue.clear(); } @@ -654,28 +764,15 @@ public class QtBluetoothLE { return false; } - // current service already under investigation - if (currentServiceInDiscovery == serviceHandle) - return true; - - if (currentServiceInDiscovery != -1) { - // we are currently discovering another service - // we queue the new one up until we finish the previous one - if (!entry.valueKnown) { - servicesToBeDiscovered.add(serviceHandle); - Log.w(TAG, "Service discovery already running on another service, " + - "queueing request for " + serviceUuid); - } else { - Log.w(TAG, "Service already known"); - } + // current service already discovered or under investigation + if (entry.valueKnown || servicesToBeDiscovered.contains(serviceHandle)) { + Log.w(TAG, "Service already known or to be discovered"); return true; } - if (!entry.valueKnown) { - performServiceDetailDiscoveryForHandle(serviceHandle, true); - } else { - Log.w(TAG, "Service already discovered"); - } + servicesToBeDiscovered.add(serviceHandle); + scheduleServiceDetailDiscovery(serviceHandle); + performNextIO(); } catch (Exception ex) { ex.printStackTrace(); @@ -716,98 +813,76 @@ public class QtBluetoothLE { return builder.toString(); } - private void finishCurrentServiceDiscovery() + //TODO function not yet used + private void finishCurrentServiceDiscovery(int handleDiscoveredService) { - int currentEntry = currentServiceInDiscovery; - GattEntry discoveredService = entries.get(currentServiceInDiscovery); + Log.w(TAG, "Finished current discovery for service handle " + handleDiscoveredService); + GattEntry discoveredService = entries.get(handleDiscoveredService); discoveredService.valueKnown = true; - entries.set(currentServiceInDiscovery, discoveredService); - - runningHandle = -1; - currentServiceInDiscovery = -1; - - leServiceDetailDiscoveryFinished(qtObject, discoveredService.service.getUuid().toString(), - currentEntry + 1, discoveredService.endHandle + 1); - - if (!servicesToBeDiscovered.isEmpty()) { + synchronized (this) { try { - int nextService = servicesToBeDiscovered.remove(); - performServiceDetailDiscoveryForHandle(nextService, true); - } catch (IndexOutOfBoundsException ex) { + servicesToBeDiscovered.removeFirst(); + } catch (NoSuchElementException ex) { Log.w(TAG, "Expected queued service but didn't find any"); } } + + leServiceDetailDiscoveryFinished(qtObject, discoveredService.service.getUuid().toString(), + handleDiscoveredService + 1, discoveredService.endHandle + 1); } - private synchronized void performServiceDetailDiscoveryForHandle(int nextHandle, boolean searchStarted) + /* + Internal Helper function for discoverServiceDetails() + + Adds all Gatt entries for the given service to the readWriteQueue to be discovered. + This function only ever adds read requests to the queue. + + //TODO function not yet used + */ + private void scheduleServiceDetailDiscovery(int serviceHandle) { - try { - if (searchStarted) { - currentServiceInDiscovery = nextHandle; - runningHandle = ++nextHandle; - } else { - runningHandle = nextHandle; - } + GattEntry serviceEntry = entries.get(serviceHandle); + final int endHandle = serviceEntry.endHandle; - GattEntry entry; - try { - entry = entries.get(nextHandle); - } catch (IndexOutOfBoundsException ex) { - //ex.printStackTrace(); - Log.w(TAG, "Last entry of last service read"); - finishCurrentServiceDiscovery(); - return; - } + if (serviceHandle == endHandle) { + Log.w(TAG, "scheduleServiceDetailDiscovery: service is empty; nothing to discover"); + finishCurrentServiceDiscovery(serviceHandle); + return; + } - boolean result; - switch (entry.type) { - case Characteristic: - result = mBluetoothGatt.readCharacteristic(entry.characteristic); - try { - if (!result) { - // add characteristic now since we won't get a read update later one - // this is possible when the characteristic is not readable - Log.d(TAG, "Non-readable characteristic " + entry.characteristic.getUuid() + - " for service " + entry.characteristic.getService().getUuid()); - leCharacteristicRead(qtObject, entry.characteristic.getService().getUuid().toString(), - nextHandle + 1, entry.characteristic.getUuid().toString(), - entry.characteristic.getProperties(), entry.characteristic.getValue()); - performServiceDetailDiscoveryForHandle(runningHandle + 1, false); - } - } catch (Exception ex) - { - ex.printStackTrace(); - } - break; - case CharacteristicValue: - // ignore -> nothing to do for this artificial type - performServiceDetailDiscoveryForHandle(runningHandle + 1, false); - break; - case Descriptor: - result = mBluetoothGatt.readDescriptor(entry.descriptor); - if (!result) { - // atm all descriptor types are readable - Log.d(TAG, "Non-readable descriptor " + entry.descriptor.getUuid() + - " for service/char" + entry.descriptor.getCharacteristic().getService().getUuid() + - "/" + entry.descriptor.getCharacteristic().getUuid()); - leDescriptorRead(qtObject, - entry.descriptor.getCharacteristic().getService().getUuid().toString(), - entry.descriptor.getCharacteristic().getUuid().toString(), - nextHandle+1, entry.descriptor.getUuid().toString(), - entry.descriptor.getValue()); - performServiceDetailDiscoveryForHandle(runningHandle + 1, false); - } - break; - case Service: - finishCurrentServiceDiscovery(); - break; - default: - Log.w(TAG, "Invalid GATT attribute type"); - break; - } + synchronized (readWriteQueue) { + // entire block inside mutex to ensure all service discovery jobs go in one after the other + // ensures that serviceDiscovered() signal is sent when required - } catch(Exception ex) { - ex.printStackTrace(); + + // serviceHandle + 1 -> ignore service handle itself + for (int i = serviceHandle + 1; i <= endHandle; i++) { + GattEntry entry = entries.get(i); + + switch (entry.type) { + case Characteristic: + case Descriptor: + // we schedule CharacteristicValue for initial discovery to simplify + // detection of the end of service discovery process + // performNextIO() ignores CharacteristicValue GATT entries + case CharacteristicValue: + break; + case Service: + // should not really happen unless endHandle is wrong + Log.w(TAG, "scheduleServiceDetailDiscovery: wrong endHandle"); + return; + } + + // only descriptor and characteristic fall through to this point + ReadWriteJob newJob = new ReadWriteJob(); + newJob.entry = entry; + newJob.jobType = IoJobType.Read; + + final boolean result = readWriteQueue.add(newJob); + if (!result) + Log.w(TAG, "Cannot add service discovery job for " + serviceEntry.service.getUuid() + + " on item " + entry.type); + } } } @@ -964,6 +1039,30 @@ public class QtBluetoothLE { return true; } + // Called by TimeoutRunnable if the current I/O job timeoutd out. + // By the time we reach this point the handleForTimeout counter has already been reset + // and the regular responses will be blocked off. + private void interruptCurrentIO(int handle) + { + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; + } + + performNextIO(); + + GattEntry entry = entries.get(handle); + if (entry == null) + return; + if (entry.valueKnown) + return; + entry.valueKnown = true; + + GattEntry serviceEntry = entries.get(entry.associatedServiceHandle); + if (serviceEntry != null && serviceEntry.endHandle == handle) + finishCurrentServiceDiscovery(entry.associatedServiceHandle); + } + /* The queuing is required because two writeCharacteristic/writeDescriptor calls cannot execute at the same time. The second write must happen after the @@ -976,50 +1075,111 @@ public class QtBluetoothLE { boolean skip = false; final ReadWriteJob nextJob; + int handle = -1; + synchronized (readWriteQueue) { if (readWriteQueue.isEmpty() || ioJobPending) return; nextJob = readWriteQueue.remove(); + switch (nextJob.entry.type) { + case Characteristic: + handle = handleForCharacteristic(nextJob.entry.characteristic); + break; + case Descriptor: + handle = handleForDescriptor(nextJob.entry.descriptor); + break; + case CharacteristicValue: + handle = nextJob.entry.endHandle; + default: + break; + } + + // timeout handler and handleForTimeout atomic must be setup before + // executing the request. Sometimes the callback is quicker than executing the + // remainder of this function. Therefore enable the atomic early such that + // callback handlers start hanging in the readWriteQueue sync block which + // we are still occupying here. + timeoutHandler.removeCallbacksAndMessages(null); // remove any timeout handlers + handleForTimeout.set(modifiedReadWriteHandle(handle, nextJob.jobType)); - Log.w(TAG, "Performing queued job " + nextJob.jobType); if (nextJob.jobType == IoJobType.Read) skip = executeReadJob(nextJob); else skip = executeWriteJob(nextJob); - if (!skip) + if (skip) { + handleForTimeout.set(-1); // not a pending call -> release atomic + } else { ioJobPending = true; + timeoutHandler.postDelayed(new TimeoutRunnable( + modifiedReadWriteHandle(handle, nextJob.jobType)), RUNNABLE_TIMEOUT); + } + + Log.w(TAG, "Performing queued job, handle: " + handle + " " + nextJob.jobType + " (" + + (nextJob.requestedWriteType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) + + ") ValueKnown: " + nextJob.entry.valueKnown + " Skipping: " + skip + + " " + nextJob.entry.type); } - if (skip) { - Log.w(TAG, "Skipping: " + nextJob.entry.type); + GattEntry entry = nextJob.entry; + if (skip) { /* BluetoothGatt.[read|write][Characteristic|Descriptor]() immediately return in cases where meta data doesn't match the intended action (e.g. trying to write to read-only char). When this happens - we have to report an error back to Qt. This is not required during + we have to report an error back to Qt. The error report is not required during the initial service discovery though. */ - final boolean isServiceDiscoveryRun = (runningHandle != -1); - if (!isServiceDiscoveryRun) { - int handle = -1; - if (nextJob.entry.type == GattEntryType.Characteristic) - handle = handleForCharacteristic(nextJob.entry.characteristic); - else - handle = handleForDescriptor(nextJob.entry.descriptor); + if (handle != -1) { + // during service discovery we do not report error but emit characteristicRead() + // any other time a failure emits serviceError() signal + + final boolean isServiceDiscovery = !entry.valueKnown; - if (handle != -1) { + if (isServiceDiscovery) { + entry.valueKnown = true; + switch (entry.type) { + case Characteristic: + Log.d(TAG, "Non-readable characteristic " + entry.characteristic.getUuid() + + " for service " + entry.characteristic.getService().getUuid()); + leCharacteristicRead(qtObject, entry.characteristic.getService().getUuid().toString(), + handle + 1, entry.characteristic.getUuid().toString(), + entry.characteristic.getProperties(), entry.characteristic.getValue()); + break; + case Descriptor: + // atm all descriptor types are readable + Log.d(TAG, "Non-readable descriptor " + entry.descriptor.getUuid() + + " for service/char" + entry.descriptor.getCharacteristic().getService().getUuid() + + "/" + entry.descriptor.getCharacteristic().getUuid()); + leDescriptorRead(qtObject, + entry.descriptor.getCharacteristic().getService().getUuid().toString(), + entry.descriptor.getCharacteristic().getUuid().toString(), + handle + 1, entry.descriptor.getUuid().toString(), + entry.descriptor.getValue()); + break; + case CharacteristicValue: + // for more details see scheduleServiceDetailDiscovery(int) + // ignore and continue unless last entry + GattEntry serviceEntry = entries.get(entry.associatedServiceHandle); + if (serviceEntry.endHandle == handle) + finishCurrentServiceDiscovery(entry.associatedServiceHandle); + break; + case Service: + Log.w(TAG, "Scheduling of Service Gatt entry for service discovery should never happen."); + break; + } + } else { int errorCode = 0; // The error codes below must be in sync with QLowEnergyService::ServiceError if (nextJob.jobType == IoJobType.Read) { - errorCode = (nextJob.entry.type == GattEntryType.Characteristic) ? - 5 : 6; // CharacteristicReadError : DescriptorReadError + errorCode = (entry.type == GattEntryType.Characteristic) ? + 5 : 6; // CharacteristicReadError : DescriptorReadError } else { - errorCode = (nextJob.entry.type == GattEntryType.Characteristic) ? - 2 : 3; // CharacteristicWriteError : DescriptorWriteError + errorCode = (entry.type == GattEntryType.Characteristic) ? + 2 : 3; // CharacteristicWriteError : DescriptorWriteError } leServiceError(qtObject, handle + 1, errorCode); @@ -1109,12 +1269,52 @@ public class QtBluetoothLE { return true; // skip break; case Service: - case CharacteristicValue: return true; + case CharacteristicValue: + return true; //skip } return false; } + /* + * Modifies and returns the given \a handle such that the job + * \a type is encoded into the returned handle. Hereby we take advantage of the fact that + * a Bluetooth Low Energy handle is only 16 bit. The handle will be the bottom two bytes + * and the job type will be in the top 2 bytes. + * + * top 2 bytes + * - 0x01 -> Read Job + * - 0x02 -> Write Job + * + * This is done in connection with handleForTimeout and assists in the process of + * detecting accidental interruption by the timeout handler. + * If two requests for the same handle are scheduled behind each other there is the + * theoretical chance that the first request comes back normally while the second request + * is interrupted by the timeout handler. This risk still exists but this function ensures that + * at least back to back requests of differing types cannot affect each other via the timeout + * handler. + */ + private int modifiedReadWriteHandle(int handle, IoJobType type) + { + int modifiedHandle = handle; + // ensure we have 16bit handle only + if (handle > 0xFFFF) + Log.w(TAG, "Invalid handle"); + + modifiedHandle = (modifiedHandle & 0xFFFF); + + switch (type) { + case Write: + modifiedHandle = (modifiedHandle | 0x00010000); + break; + case Read: + modifiedHandle = (modifiedHandle | 0x00020000); + break; + } + + return modifiedHandle; + } + public native void leConnectionStateChange(long qtObject, int wasErrorTransition, int newState); public native void leServicesDiscovered(long qtObject, int errorCode, String uuidList); public native void leServiceDetailDiscoveryFinished(long qtObject, final String serviceUuid, diff --git a/src/bluetooth/android/devicediscoverybroadcastreceiver.cpp b/src/bluetooth/android/devicediscoverybroadcastreceiver.cpp index c807df7f..9c9c0409 100644 --- a/src/bluetooth/android/devicediscoverybroadcastreceiver.cpp +++ b/src/bluetooth/android/devicediscoverybroadcastreceiver.cpp @@ -511,7 +511,8 @@ QBluetoothDeviceInfo DeviceDiscoveryBroadcastReceiver::retrieveDeviceInfo(JNIEnv if (scanRecord != nullptr) { // Parse scan record jboolean isCopy; - const char *scanRecordBuffer = reinterpret_cast<const char *>(env->GetByteArrayElements(scanRecord, &isCopy)); + jbyte *elems = env->GetByteArrayElements(scanRecord, &isCopy); + const char *scanRecordBuffer = reinterpret_cast<const char *>(elems); const int scanRecordLength = env->GetArrayLength(scanRecord); QList<QBluetoothUuid> serviceUuids; @@ -560,6 +561,8 @@ QBluetoothDeviceInfo DeviceDiscoveryBroadcastReceiver::retrieveDeviceInfo(JNIEnv } info.setServiceUuids(serviceUuids, QBluetoothDeviceInfo::DataIncomplete); + + env->ReleaseByteArrayElements(scanRecord, elems, JNI_ABORT); } if (QtAndroidPrivate::androidSdkVersion() >= 18) { diff --git a/src/bluetooth/bluetooth.pro b/src/bluetooth/bluetooth.pro index 1857e983..bf2dce74 100644 --- a/src/bluetooth/bluetooth.pro +++ b/src/bluetooth/bluetooth.pro @@ -78,7 +78,7 @@ SOURCES += \ qlowenergycontroller.cpp \ qlowenergyserviceprivate.cpp -config_bluez:qtHaveModule(dbus) { +qtConfig(bluez):qtHaveModule(dbus) { QT_FOR_PRIVATE += dbus DEFINES += QT_BLUEZ_BLUETOOTH @@ -98,16 +98,13 @@ config_bluez:qtHaveModule(dbus) { # old versions of Bluez do not have the required BTLE symbols - config_bluez_le { + qtConfig(bluez_le) { SOURCES += \ qleadvertiser_bluez.cpp \ qlowenergycontroller_bluez.cpp \ lecmaccalculator.cpp - config_linux_crypto_api:DEFINES += CONFIG_LINUX_CRYPTO_API - else:message("Linux crypto API not present, signed writes will not work.") + qtConfig(linux_crypto_api): DEFINES += CONFIG_LINUX_CRYPTO_API } else { - message("Bluez version is too old to support Bluetooth Low Energy.") - message("Only classic Bluetooth will be available.") DEFINES += QT_BLUEZ_NO_BTLE include(dummy/dummy.pri) SOURCES += \ @@ -153,14 +150,10 @@ config_bluez:qtHaveModule(dbus) { qlowenergycontroller_osx.mm \ qlowenergyservice_osx.mm - SOURCES += \ - qlowenergycontroller_p.cpp - PRIVATE_HEADERS += qbluetoothsocket_osx_p.h \ qbluetoothserver_osx_p.h \ qbluetoothtransferreply_osx_p.h \ qbluetoothtransferreply_osx_p.h \ - qbluetoothdevicediscoverytimer_osx_p.h \ qlowenergycontroller_osx_p.h SOURCES -= qbluetoothdevicediscoveryagent.cpp @@ -182,8 +175,7 @@ config_bluez:qtHaveModule(dbus) { qlowenergyservice_osx.mm PRIVATE_HEADERS += \ - qlowenergycontroller_osx_p.h \ - qbluetoothdevicediscoverytimer_osx_p.h + qlowenergycontroller_osx_p.h include(osx/osxbt.pri) SOURCES += \ @@ -198,6 +190,21 @@ config_bluez:qtHaveModule(dbus) { SOURCES -= qlowenergycontroller_p.cpp SOURCES -= qlowenergyservice.cpp SOURCES -= qlowenergycontroller.cpp +} else:if(winphone|winrt-*-msvc2015) { + DEFINES += QT_WINRT_BLUETOOTH + QT += core-private + + # remove dummy warning once platform port is complete + include(dummy/dummy.pri) + + SOURCES += \ + qbluetoothdevicediscoveryagent_winrt.cpp \ + qbluetoothlocaldevice_p.cpp \ + qbluetoothserver_p.cpp \ + qbluetoothservicediscoveryagent_p.cpp \ + qbluetoothserviceinfo_p.cpp \ + qbluetoothsocket_p.cpp \ + qlowenergycontroller_winrt.cpp } else { message("Unsupported Bluetooth platform, will not build a working QtBluetooth library.") message("Either no Qt D-Bus found or no BlueZ headers available.") @@ -212,6 +219,12 @@ config_bluez:qtHaveModule(dbus) { qlowenergycontroller_p.cpp } +winrt-*-msvc2015 { + MODULE_WINRT_CAPABILITIES_DEVICE += \ + bluetooth.genericAttributeProfile \ + bluetooth.rfcomm +} + OTHER_FILES += HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS diff --git a/src/bluetooth/configure.json b/src/bluetooth/configure.json new file mode 100644 index 00000000..1cf43f36 --- /dev/null +++ b/src/bluetooth/configure.json @@ -0,0 +1,71 @@ +{ + "module": "bluetooth", + "testDir": "../../config.tests", + + "libraries": { + "bluez": { + "label": "BlueZ", + "test": "bluez", + "sources": [ + { "type": "pkgConfig", "args": "bluez" }, + "-lbluetooth" + ] + } + }, + + "tests": { + "bluez_le": { + "label": "BlueZ Low Energy", + "type": "compile", + "test": "bluez_le" + }, + "linux_crypto_api": { + "label": "Linux Crypto API", + "type": "compile", + "test": "linux_crypto_api" + } + }, + + "features": { + "bluez": { + "label": "BlueZ", + "condition": "libs.bluez", + "output": [ "privateFeature" ] + }, + "bluez_le": { + "label": "BlueZ Low Energy", + "condition": "features.bluez && tests.bluez_le", + "output": [ "privateFeature" ] + }, + "linux_crypto_api": { + "label": "Linux Crypto API", + "condition": "features.bluez_le && tests.linux_crypto_api", + "output": [ "privateFeature" ] + } + }, + + "report": [ + { + "type": "note", + "condition": "features.bluez_le && !features.linux_crypto_api", + "message": "Linux crypto API not present. BTLE signed writes will not work." + }, + { + "type": "note", + "condition": "features.bluez && !features.bluez_le", + "message": "Bluez version is too old to support Bluetooth Low Energy. +Only classic Bluetooth will be available." + } + ], + + "summary": [ + { + "section": "Qt Bluetooth", + "entries": [ + "bluez", + "bluez_le", + "linux_crypto_api" + ] + } + ] +} diff --git a/src/bluetooth/doc/src/bluetooth-index.qdoc b/src/bluetooth/doc/src/bluetooth-index.qdoc index b168cd2d..92b846b4 100644 --- a/src/bluetooth/doc/src/bluetooth-index.qdoc +++ b/src/bluetooth/doc/src/bluetooth-index.qdoc @@ -33,9 +33,50 @@ The Bluetooth API provides connectivity between Bluetooth enabled devices. -Currently, the API is supported on the following platforms: \l{Qt for Android}{Android}, -\l{Qt for iOS}{iOS}, \l{Qt for Linux/X11}{Linux} -(\l{http://www.bluez.org}{BlueZ 4.x/5.x}) and \l{Qt for macOS}{\macos}. +Currently, the API is supported on the following platforms: + +\table +\header + \li API Feature + \li \l {Qt for Android}{Android} + \li \l {Qt for iOS}{iOS} + \li \l {Qt for Linux/X11}{Linux (BlueZ 4.x/5.x)} + \li \l {Qt for OS X}{macOS} + \li \l {Qt for WinRT}{WinRT} + \li \l {Qt for Windows}{Windows} +\row + \li Classic Bluetooth + \li x + \li x + \li x + \li x + \li + \li +\row + \li Bluetooth LE Central + \li x + \li x + \li x + \li x + \li x + \li +\row + \li Bluetooth LE Peripheral + \li + \li x + \li x + \li x + \li + \li +\row + \li Bluetooth LE Advertisement & Scanning + \li + \li + \li + \li + \li + \li +\endtable \section1 Overview @@ -49,8 +90,8 @@ Qt Bluetooth supports Bluetooth Low Energy development for client/central role u Further details can be found in the \l {Bluetooth Low Energy Overview}{Bluetooth Low Energy Overview} section. -A new addition in this Qt Bluetooth 5.7 release covers support for Bluetooth Low Energy -applications performing the peripheral/server role. This new API is a Technology Preview. +A new addition since the Qt Bluetooth 5.7 release covers support for Bluetooth Low Energy +applications performing the peripheral/server role. This new API remains in Technology Preview. \section1 Getting Started @@ -136,4 +177,19 @@ The \l QtBluetooth module exports the following \li \l {btfiletransfer}{Bluetooth File Transfer} \endlist \endlist + +\section1 Licenses and Attributions + +Qt Bluetooth is available under commercial licenses from \l{The Qt Company}. +In addition, it is available under the +\l{GNU Lesser General Public License, version 3}, or +the \l{GNU General Public License, version 2}. +See \l{Qt Licensing} for further details. + +On Linux, Qt Bluetooth uses a separate executable, \c sdpscanner, +to integrate with the official Linux bluetooth protocol stack +BlueZ. BlueZ is available under the \l{GNU General Public License, +version 2}. + +\generatelist{groupsbymodule attributions-qtbluetooth} */ diff --git a/src/bluetooth/doc/src/bluetooth-le-overview.qdoc b/src/bluetooth/doc/src/bluetooth-le-overview.qdoc index 8e78c487..a2fb10d9 100644 --- a/src/bluetooth/doc/src/bluetooth-le-overview.qdoc +++ b/src/bluetooth/doc/src/bluetooth-le-overview.qdoc @@ -37,8 +37,8 @@ Low Energy devices. The Qt Bluetooth Low Energy API for the central role was introduced by Qt 5.4. Since Qt 5.5 that part of the API is final and a compatibility guarantee is given for future releases. - In Qt 5.7, additional API supporting the peripheral role was added as a Technology Preview, - with the backend only implemented for Linux/BlueZ. + Since Qt 5.7, additional API supporting the peripheral role was added as a Technology Preview, + with the backend implemented for Linux/BlueZ, iOS and macOS. \section1 What Is Bluetooth Low Energy diff --git a/src/bluetooth/osx/osxbt.pri b/src/bluetooth/osx/osxbt.pri index 5ca833cc..13187e4f 100644 --- a/src/bluetooth/osx/osxbt.pri +++ b/src/bluetooth/osx/osxbt.pri @@ -1,5 +1,6 @@ SOURCES += osx/uistrings.cpp osx/osxbtnotifier.cpp PRIVATE_HEADERS += osx/uistrings_p.h +//QMAKE_CXXFLAGS_WARN_ON += -Wno-nullability-completeness CONFIG(osx) { PRIVATE_HEADERS += osx/osxbtutility_p.h \ @@ -16,7 +17,8 @@ CONFIG(osx) { osx/osxbtledeviceinquiry_p.h \ osx/osxbluetooth_p.h \ osx/osxbtcentralmanager_p.h \ - osx/osxbtnotifier_p.h + osx/osxbtnotifier_p.h \ + osx/osxbtperipheralmanager_p.h OBJECTIVE_SOURCES += osx/osxbtutility.mm \ osx/osxbtdevicepair.mm \ @@ -30,15 +32,22 @@ CONFIG(osx) { osx/osxbtsocketlistener.mm \ osx/osxbtobexsession.mm \ osx/osxbtledeviceinquiry.mm \ - osx/osxbtcentralmanager.mm + osx/osxbtcentralmanager.mm \ + osx/osxbtperipheralmanager.mm } else { PRIVATE_HEADERS += osx/osxbtutility_p.h \ osx/osxbtledeviceinquiry_p.h \ osx/osxbluetooth_p.h \ osx/osxbtcentralmanager_p.h \ osx/osxbtnotifier_p.h + ios { + PRIVATE_HEADERS += osx/osxbtperipheralmanager_p.h + } OBJECTIVE_SOURCES += osx/osxbtutility.mm \ osx/osxbtledeviceinquiry.mm \ osx/osxbtcentralmanager.mm + ios { + OBJECTIVE_SOURCES += osx/osxbtperipheralmanager.mm + } } diff --git a/src/bluetooth/osx/osxbtcentralmanager.mm b/src/bluetooth/osx/osxbtcentralmanager.mm index 06425753..f7218ca6 100644 --- a/src/bluetooth/osx/osxbtcentralmanager.mm +++ b/src/bluetooth/osx/osxbtcentralmanager.mm @@ -43,7 +43,6 @@ #include "osxbtnotifier_p.h" #include <QtCore/qloggingcategory.h> -#include <QtCore/qsysinfo.h> #include <QtCore/qdebug.h> #include <algorithm> @@ -116,7 +115,7 @@ QT_END_NAMESPACE @implementation QT_MANGLE_NAMESPACE(OSXBTCentralManager) -- (id)initWith:(OSXBluetooth::LECentralNotifier *)aNotifier +- (id)initWith:(OSXBluetooth::LECBManagerNotifier *)aNotifier { if (self = [super init]) { manager = nil; @@ -171,9 +170,9 @@ QT_END_NAMESPACE if (!manager) { managerState = OSXBluetooth::CentralManagerIdle; - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to allocate a central manager"; + qCWarning(QT_BT_OSX) << "failed to allocate a central manager"; if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::ConnectionError); + emit notifier->CBManagerError(QLowEnergyController::ConnectionError); } } else if (managerState != OSXBluetooth::CentralManagerUpdating) { [self retrievePeripheralAndConnect]; @@ -187,7 +186,7 @@ QT_END_NAMESPACE Q_FUNC_INFO, "invalid state"); if ([self isConnected]) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "already connected"; + qCDebug(QT_BT_OSX) << "already connected"; if (notifier) emit notifier->connected(); return; @@ -203,62 +202,37 @@ QT_END_NAMESPACE // Retrieve a peripheral first ... ObjCScopedPointer<NSMutableArray> uuids([[NSMutableArray alloc] init]); if (!uuids) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to allocate identifiers"; + qCWarning(QT_BT_OSX) << "failed to allocate identifiers"; if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::ConnectionError); + emit notifier->CBManagerError(QLowEnergyController::ConnectionError); return; } -#if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_7_0) - if (QSysInfo::MacintoshVersion >= qt_OS_limit(QSysInfo::MV_10_9, QSysInfo::MV_IOS_7_0)) { - const quint128 qtUuidData(deviceUuid.toUInt128()); - // STATIC_ASSERT on sizes would be handy! - uuid_t uuidData = {}; - std::copy(qtUuidData.data, qtUuidData.data + 16, uuidData); - const ObjCScopedPointer<NSUUID> nsUuid([[NSUUID alloc] initWithUUIDBytes:uuidData]); - if (!nsUuid) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to allocate NSUUID identifier"; - if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::ConnectionError); - return; - } - - [uuids addObject:nsUuid]; - // With the latest CoreBluetooth, we can synchronously retrive peripherals: - QT_BT_MAC_AUTORELEASEPOOL; - NSArray *const peripherals = [manager retrievePeripheralsWithIdentifiers:uuids]; - if (!peripherals || peripherals.count != 1) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to retrive a peripheral"; - if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::UnknownRemoteDeviceError); - return; - } - peripheral = [static_cast<CBPeripheral *>([peripherals objectAtIndex:0]) retain]; - [self connectToPeripheral]; - return; - } -#endif - // Either SDK or the target is below 10.9/7.0 - if (![manager respondsToSelector:@selector(retrievePeripherals:)]) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to retrive a peripheral"; + const quint128 qtUuidData(deviceUuid.toUInt128()); + uuid_t uuidData = {}; + std::copy(qtUuidData.data, qtUuidData.data + 16, uuidData); + const ObjCScopedPointer<NSUUID> nsUuid([[NSUUID alloc] initWithUUIDBytes:uuidData]); + if (!nsUuid) { + qCWarning(QT_BT_OSX) << "failed to allocate NSUUID identifier"; if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::UnknownRemoteDeviceError); + emit notifier->CBManagerError(QLowEnergyController::ConnectionError); return; } - OSXBluetooth::CFStrongReference<CFUUIDRef> cfUuid(OSXBluetooth::cf_uuid(deviceUuid)); - if (!cfUuid) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to create CFUUID object"; + [uuids addObject:nsUuid]; + // With the latest CoreBluetooth, we can synchronously retrive peripherals: + QT_BT_MAC_AUTORELEASEPOOL; + NSArray *const peripherals = [manager retrievePeripheralsWithIdentifiers:uuids]; + if (!peripherals || peripherals.count != 1) { + qCWarning(QT_BT_OSX) << "failed to retrive a peripheral"; if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::ConnectionError); + emit notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError); return; } - // With ARC this cast will be illegal: - [uuids addObject:(id)cfUuid.data()]; - // Unfortunately, with old Core Bluetooth this call is asynchronous ... - managerState = OSXBluetooth::CentralManagerConnecting; - [manager performSelector:@selector(retrievePeripherals:) withObject:uuids.data()]; + + peripheral = [static_cast<CBPeripheral *>([peripherals objectAtIndex:0]) retain]; + [self connectToPeripheral]; } - (void)connectToPeripheral @@ -270,11 +244,11 @@ QT_END_NAMESPACE // The state is still the same - connecting. if ([self isConnected]) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "already connected"; + qCDebug(QT_BT_OSX) << "already connected"; if (notifier) emit notifier->connected(); } else { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "trying to connect"; + qCDebug(QT_BT_OSX) << "trying to connect"; managerState = OSXBluetooth::CentralManagerConnecting; [manager connectPeripheral:peripheral options:nil]; } @@ -285,18 +259,7 @@ QT_END_NAMESPACE if (!peripheral) return false; -#if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_7_0) - using OSXBluetooth::qt_OS_limit; - - if (QSysInfo::MacintoshVersion >= qt_OS_limit(QSysInfo::MV_10_9, QSysInfo::MV_IOS_7_0)) - return peripheral.state == CBPeripheralStateConnected; -#endif - // Either SDK or the target is below 10.9/7.0 ... - if (![peripheral respondsToSelector:@selector(isConnected)]) - return false; - - // Ugly cast to deal with id being a pointer ... - return reinterpret_cast<quintptr>([peripheral performSelector:@selector(isConnected)]); + return peripheral.state == CBPeripheralStateConnected; } - (void)disconnectFromDevice @@ -389,7 +352,7 @@ QT_END_NAMESPACE Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); if (servicesToDiscoverDetails.contains(serviceUuid)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO <<"already discovering for " + qCWarning(QT_BT_OSX) << "already discovering for" << serviceUuid; return; } @@ -402,11 +365,11 @@ QT_END_NAMESPACE return; } - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unknown service uuid " + qCWarning(QT_BT_OSX) << "unknown service uuid" << serviceUuid; if (notifier) { - emit notifier->CBCentralManagerError(serviceUuid, + emit notifier->CBManagerError(serviceUuid, QLowEnergyService::UnknownError); } } @@ -499,9 +462,9 @@ QT_END_NAMESPACE const QLowEnergyHandle maxHandle = std::numeric_limits<QLowEnergyHandle>::max(); if (nHandles >= maxHandle || lastValidHandle > maxHandle - nHandles) { // Well, that's unlikely :) But we must be sure. - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "can not allocate more handles"; + qCWarning(QT_BT_OSX) << "can not allocate more handles"; if (notifier) - notifier->CBCentralManagerError(serviceUuid, QLowEnergyService::OperationError); + notifier->CBManagerError(serviceUuid, QLowEnergyService::OperationError); return; } @@ -603,7 +566,7 @@ QT_END_NAMESPACE const LERequest request(requests.dequeue()); if (request.type == LERequest::CharRead) { if (!charMap.contains(request.handle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "characteristic with handle" + qCWarning(QT_BT_OSX) << "characteristic with handle" << request.handle << "not found"; return [self performNextRequest]; } @@ -613,7 +576,7 @@ QT_END_NAMESPACE [peripheral readValueForCharacteristic:charMap[request.handle]]; } else { if (!descMap.contains(request.handle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "descriptor with handle" + qCWarning(QT_BT_OSX) << "descriptor with handle" << request.handle << "not found"; return [self performNextRequest]; } @@ -640,8 +603,8 @@ QT_END_NAMESPACE if (request.type == LERequest::DescWrite) { if (!descMap.contains(request.handle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "handle: " - << request.handle << " not found"; + qCWarning(QT_BT_OSX) << "handle:" << request.handle + << "not found"; return [self performNextRequest]; } @@ -649,8 +612,7 @@ QT_END_NAMESPACE ObjCStrongReference<NSData> data(data_from_bytearray(request.value)); if (!data) { // Even if qtData.size() == 0, we still need NSData object. - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed " - "to allocate an NSData object"; + qCWarning(QT_BT_OSX) << "failed to allocate an NSData object"; return [self performNextRequest]; } @@ -661,8 +623,8 @@ QT_END_NAMESPACE return [peripheral writeValue:data.data() forDescriptor:descriptor]; } else { if (!charMap.contains(request.handle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "characteristic with " - "handle: " << request.handle << " not found"; + qCWarning(QT_BT_OSX) << "characteristic with handle:" + << request.handle << "not found"; return [self performNextRequest]; } @@ -687,7 +649,7 @@ QT_END_NAMESPACE ObjCStrongReference<NSData> data(data_from_bytearray(request.value)); if (!data) { // Even if qtData.size() == 0, we still need NSData object. - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to allocate NSData object"; + qCWarning(QT_BT_OSX) << "failed to allocate NSData object"; return [self performNextRequest]; } @@ -717,11 +679,10 @@ QT_END_NAMESPACE Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); if (!charMap.contains(charHandle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO - << "unknown characteristic handle " + qCWarning(QT_BT_OSX) << "unknown characteristic handle" << charHandle; if (notifier) { - emit notifier->CBCentralManagerError(serviceUuid, + emit notifier->CBManagerError(serviceUuid, QLowEnergyService::DescriptorWriteError); } return; @@ -732,10 +693,9 @@ QT_END_NAMESPACE // it back, so check _now_ that we really have this descriptor. const QBluetoothUuid qtUuid(QBluetoothUuid::ClientCharacteristicConfiguration); if (![self descriptor:qtUuid forCharacteristic:charMap[charHandle]]) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO - << "no client characteristic configuration found"; + qCWarning(QT_BT_OSX) << "no client characteristic configuration found"; if (notifier) { - emit notifier->CBCentralManagerError(serviceUuid, + emit notifier->CBManagerError(serviceUuid, QLowEnergyService::DescriptorWriteError); } return; @@ -760,9 +720,9 @@ QT_END_NAMESPACE QT_BT_MAC_AUTORELEASEPOOL; if (!charMap.contains(charHandle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "characteristic: " << charHandle << " not found"; + qCWarning(QT_BT_OSX) << "characteristic:" << charHandle << "not found"; if (notifier) { - emit notifier->CBCentralManagerError(serviceUuid, + emit notifier->CBManagerError(serviceUuid, QLowEnergyService::CharacteristicReadError); } @@ -789,10 +749,9 @@ QT_END_NAMESPACE QT_BT_MAC_AUTORELEASEPOOL; if (!charMap.contains(charHandle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "characteristic: " - << charHandle << " not found"; + qCWarning(QT_BT_OSX) << "characteristic:" << charHandle << "not found"; if (notifier) { - emit notifier->CBCentralManagerError(serviceUuid, + emit notifier->CBManagerError(serviceUuid, QLowEnergyService::CharacteristicWriteError); } return; @@ -816,10 +775,9 @@ QT_END_NAMESPACE Q_ASSERT_X(descHandle, Q_FUNC_INFO, "invalid descriptor handle (0)"); if (!descMap.contains(descHandle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "handle:" - << descHandle << "not found"; + qCWarning(QT_BT_OSX) << "handle:" << descHandle << "not found"; if (notifier) { - emit notifier->CBCentralManagerError(serviceUuid, + emit notifier->CBManagerError(serviceUuid, QLowEnergyService::DescriptorReadError); } return; @@ -842,10 +800,9 @@ QT_END_NAMESPACE Q_ASSERT_X(descHandle, Q_FUNC_INFO, "invalid descriptor handle (0)"); if (!descMap.contains(descHandle)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "handle: " - << descHandle << " not found"; + qCWarning(QT_BT_OSX) << "handle:" << descHandle << "not found"; if (notifier) { - emit notifier->CBCentralManagerError(serviceUuid, + emit notifier->CBManagerError(serviceUuid, QLowEnergyService::DescriptorWriteError); } return; @@ -1029,27 +986,25 @@ QT_END_NAMESPACE if ([obj isKindOfClass:[CBCharacteristic class]]) { CBCharacteristic *const ch = static_cast<CBCharacteristic *>(obj); if (!charMap.key(ch)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected " - "characteristic, no handle found"; + qCWarning(QT_BT_OSX) << "unexpected characteristic, no handle found"; return false; } } else if ([obj isKindOfClass:[CBDescriptor class]]) { CBDescriptor *const d = static_cast<CBDescriptor *>(obj); if (!descMap.key(d)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected " - "descriptor, no handle found"; + qCWarning(QT_BT_OSX) << "unexpected descriptor, no handle found"; return false; } } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid object, " - "characteristic or descriptor required"; + qCWarning(QT_BT_OSX) << "invalid object, characteristic " + "or descriptor required"; return false; } if (valuesToWrite.contains(obj)) { // It can be a result of some previous errors - for example, // we never got a callback from a previous write. - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "already has a cached value for this " + qCWarning(QT_BT_OSX) << "already has a cached value for this " "object, the value will be replaced"; } @@ -1114,7 +1069,7 @@ QT_END_NAMESPACE // and reset managerState from CentralManagerUpdating. managerState = CentralManagerIdle; if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::InvalidBluetoothAdapterError); + emit notifier->CBManagerError(QLowEnergyController::InvalidBluetoothAdapterError); } return; } @@ -1133,7 +1088,7 @@ QT_END_NAMESPACE // TODO: we need a better error + // what will happen if later the state changes to PoweredOn??? if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::InvalidBluetoothAdapterError); + emit notifier->CBManagerError(QLowEnergyController::InvalidBluetoothAdapterError); } return; } @@ -1153,30 +1108,6 @@ QT_END_NAMESPACE } } -- (void)centralManager:(CBCentralManager *)central didRetrievePeripherals:(NSArray *)peripherals -{ - Q_UNUSED(central) - - // This method is required for iOS before 7.0 and OS X below 10.9. - Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central manager (nil)"); - - if (managerState != OSXBluetooth::CentralManagerConnecting) { - // Canceled by calling -disconnectFromDevice method. - return; - } - - managerState = OSXBluetooth::CentralManagerIdle; - - if (!peripherals || peripherals.count != 1) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO <<"unexpected number of peripherals (!= 1)"; - if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::UnknownRemoteDeviceError); - } else { - peripheral = [static_cast<CBPeripheral *>([peripherals objectAtIndex:0]) retain]; - [self connectToPeripheral]; - } -} - - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral { Q_UNUSED(central) @@ -1207,7 +1138,7 @@ QT_END_NAMESPACE managerState = OSXBluetooth::CentralManagerIdle; // TODO: better error mapping is required. if (notifier) - notifier->CBCentralManagerError(QLowEnergyController::UnknownRemoteDeviceError); + notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError); } - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)aPeripheral @@ -1221,9 +1152,9 @@ QT_END_NAMESPACE if (error && managerState == OSXBluetooth::CentralManagerDisconnecting) { managerState = OSXBluetooth::CentralManagerIdle; - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to disconnect"; + qCWarning(QT_BT_OSX) << "failed to disconnect"; if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::UnknownRemoteDeviceError); + emit notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError); } else { managerState = OSXBluetooth::CentralManagerIdle; if (notifier) @@ -1248,7 +1179,7 @@ QT_END_NAMESPACE NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); // TODO: better error mapping required. if (notifier) - emit notifier->CBCentralManagerError(QLowEnergyController::UnknownError); + emit notifier->CBManagerError(QLowEnergyController::UnknownError); } else { [self discoverIncludedServices]; } @@ -1344,7 +1275,7 @@ QT_END_NAMESPACE NSLog(@"%s failed with error: %@", Q_FUNC_INFO, error); // We did not discover any characteristics and can not discover descriptors, // inform our delegate (it will set a service state also). - emit notifier->CBCentralManagerError(qt_uuid(service.UUID), QLowEnergyController::UnknownError); + emit notifier->CBManagerError(qt_uuid(service.UUID), QLowEnergyController::UnknownError); } else { [self readCharacteristics:service]; } @@ -1379,7 +1310,7 @@ QT_END_NAMESPACE if (chHandle && chHandle == currentReadHandle) { currentReadHandle = 0; requestPending = false; - emit notifier->CBCentralManagerError(qtUuid, QLowEnergyService::CharacteristicReadError); + emit notifier->CBManagerError(qtUuid, QLowEnergyService::CharacteristicReadError); [self performNextRequest]; } return; @@ -1402,7 +1333,7 @@ QT_END_NAMESPACE // updated values ... // TODO: this must be properly tested. if (!chHandle) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "unexpected update notification, " + qCCritical(QT_BT_OSX) << "unexpected update notification, " "no characteristic handle found"; return; } @@ -1481,7 +1412,7 @@ QT_END_NAMESPACE if (dHandle && dHandle == currentReadHandle) { currentReadHandle = 0; requestPending = false; - emit notifier->CBCentralManagerError(qtUuid, QLowEnergyService::DescriptorReadError); + emit notifier->CBManagerError(qtUuid, QLowEnergyService::DescriptorReadError); [self performNextRequest]; } return; @@ -1513,7 +1444,7 @@ QT_END_NAMESPACE } } else { if (!dHandle) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "unexpected value update notification, " + qCCritical(QT_BT_OSX) << "unexpected value update notification, " "no descriptor handle found"; return; } @@ -1556,13 +1487,13 @@ QT_END_NAMESPACE // Error or not, but the cached value has to be deleted ... const QByteArray valueToReport(valuesToWrite.value(characteristic, QByteArray())); if (!valuesToWrite.remove(characteristic)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no updated value found" - " for characteristic"; + qCWarning(QT_BT_OSX) << "no updated value found " + "for characteristic"; } if (error) { NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); - emit notifier->CBCentralManagerError(qt_uuid(characteristic.service.UUID), + emit notifier->CBManagerError(qt_uuid(characteristic.service.UUID), QLowEnergyService::CharacteristicWriteError); } else { const QLowEnergyHandle cHandle = charMap.key(characteristic); @@ -1592,16 +1523,15 @@ QT_END_NAMESPACE // Error or not, a value (if any) must be removed. const QByteArray valueToReport(valuesToWrite.value(descriptor, QByteArray())); if (!valuesToWrite.remove(descriptor)) - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no updated value found"; + qCWarning(QT_BT_OSX) << "no updated value found"; if (error) { NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); - emit notifier->CBCentralManagerError(qt_uuid(descriptor.characteristic.service.UUID), + emit notifier->CBManagerError(qt_uuid(descriptor.characteristic.service.UUID), QLowEnergyService::DescriptorWriteError); } else { const QLowEnergyHandle dHandle = descMap.key(descriptor); - Q_ASSERT_X(dHandle, Q_FUNC_INFO, - "descriptor not found in the descriptors map"); + Q_ASSERT_X(dHandle, Q_FUNC_INFO, "descriptor not found in the descriptors map"); emit notifier->descriptorWritten(dHandle, valueToReport); } @@ -1631,7 +1561,7 @@ QT_END_NAMESPACE if (error) { NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); // In Qt's API it's a descriptor write actually. - emit notifier->CBCentralManagerError(qt_uuid(characteristic.service.UUID), + emit notifier->CBManagerError(qt_uuid(characteristic.service.UUID), QLowEnergyService::DescriptorWriteError); } else if (nRemoved) { const QLowEnergyHandle dHandle = descMap.key(descriptor); diff --git a/src/bluetooth/osx/osxbtcentralmanager_p.h b/src/bluetooth/osx/osxbtcentralmanager_p.h index 3eee2190..697d922c 100644 --- a/src/bluetooth/osx/osxbtcentralmanager_p.h +++ b/src/bluetooth/osx/osxbtcentralmanager_p.h @@ -72,7 +72,7 @@ class QLowEnergyServicePrivate; namespace OSXBluetooth { -class LECentralNotifier; +class LECBManagerNotifier; enum CentralManagerState { @@ -141,7 +141,7 @@ QT_END_NAMESPACE QT_PREPEND_NAMESPACE(QBluetoothUuid) deviceUuid; - QT_PREPEND_NAMESPACE(OSXBluetooth)::LECentralNotifier *notifier; + QT_PREPEND_NAMESPACE(OSXBluetooth)::LECBManagerNotifier *notifier; // Quite a verbose service discovery machinery // (a "graph traversal"). @@ -170,7 +170,7 @@ QT_END_NAMESPACE CBPeripheral *peripheral; } -- (id)initWith:(QT_PREPEND_NAMESPACE(OSXBluetooth)::LECentralNotifier *)notifier; +- (id)initWith:(QT_PREPEND_NAMESPACE(OSXBluetooth)::LECBManagerNotifier *)notifier; - (void)dealloc; // IMPORTANT: _all_ these methods are to be executed on qt_LE_queue, diff --git a/src/bluetooth/osx/osxbtdeviceinquiry.mm b/src/bluetooth/osx/osxbtdeviceinquiry.mm index 0420a67e..79a8a92c 100644 --- a/src/bluetooth/osx/osxbtdeviceinquiry.mm +++ b/src/bluetooth/osx/osxbtdeviceinquiry.mm @@ -73,8 +73,7 @@ QT_USE_NAMESPACE [m_inquiry setUpdateNewDeviceNames:NO];//Useless, disable! m_delegate = delegate; } else { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create " - "a device inquiry"; + qCCritical(QT_BT_OSX) << "failed to create a device inquiry"; } m_active = false; @@ -113,8 +112,7 @@ QT_USE_NAMESPACE if (result != kIOReturnSuccess) { // QtBluetooth will probably convert an error into UnknownError, // loosing the actual information. - qCWarning(QT_BT_OSX) << Q_FUNC_INFO <<"failed with " - "IOKit error code: " << result; + qCWarning(QT_BT_OSX) << "failed with IOKit error code:" << result; m_active = false; } @@ -130,6 +128,8 @@ QT_USE_NAMESPACE const IOReturn res = [m_inquiry stop]; if (res != kIOReturnSuccess) m_active = true; + else + qCDebug(QT_BT_OSX) << "-stop, success (waiting for 'inquiryComplete')"; return res; } @@ -152,7 +152,7 @@ QT_USE_NAMESPACE if (error != kIOReturnSuccess) { // QtBluetooth has not too many error codes, 'UnknownError' is not really // useful, report the actual error code here: - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "IOKit error code: " << error; + qCWarning(QT_BT_OSX) << "IOKit error code: " << error; m_delegate->error(sender, error); } else { m_delegate->inquiryFinished(sender); diff --git a/src/bluetooth/osx/osxbtdevicepair.mm b/src/bluetooth/osx/osxbtdevicepair.mm index dbb2fa4b..dcaa3536 100644 --- a/src/bluetooth/osx/osxbtdevicepair.mm +++ b/src/bluetooth/osx/osxbtdevicepair.mm @@ -41,7 +41,6 @@ #include "osxbtutility_p.h" #include <QtCore/qloggingcategory.h> -#include <QtCore/qsysinfo.h> #include <QtCore/qdebug.h> QT_BEGIN_NAMESPACE @@ -88,18 +87,8 @@ QT_USE_NAMESPACE - (void)dealloc { -#if QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9) - // Stop also sets a delegate to nil (Apple's docs). - // 10.9 only. - if (QSysInfo::MacintoshVersion >= QSysInfo::MV_10_9) - [m_pairing stop]; - else - [m_pairing setDelegate:nil]; -#else - [m_pairing setDelegate:nil]; -#endif + [m_pairing stop]; [m_pairing release]; - [super dealloc]; } @@ -116,14 +105,13 @@ QT_USE_NAMESPACE // Device is autoreleased. IOBluetoothDevice *const device = [IOBluetoothDevice deviceWithAddress:&iobtAddress]; if (!device) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create a device " - "to pair with"; + qCCritical(QT_BT_OSX) << "failed to create a device to pair with"; return kIOReturnError; } m_pairing = [[IOBluetoothDevicePair pairWithDevice:device] retain]; if (!m_pairing) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create pair"; + qCCritical(QT_BT_OSX) << "failed to create pair"; return kIOReturnError; } diff --git a/src/bluetooth/osx/osxbtl2capchannel.mm b/src/bluetooth/osx/osxbtl2capchannel.mm index e18e9e25..02ec4f90 100644 --- a/src/bluetooth/osx/osxbtl2capchannel.mm +++ b/src/bluetooth/osx/osxbtl2capchannel.mm @@ -104,13 +104,13 @@ QT_USE_NAMESPACE withPSM:(BluetoothL2CAPChannelID)psm { if (address.isNull()) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "invalid peer address"; + qCCritical(QT_BT_OSX) << "invalid peer address"; return kIOReturnNoDevice; } // Can never be called twice. if (connected || device || channel) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "connection is already active"; + qCCritical(QT_BT_OSX) << "connection is already active"; return kIOReturnStillOpen; } @@ -119,13 +119,13 @@ QT_USE_NAMESPACE const BluetoothDeviceAddress iobtAddress = OSXBluetooth::iobluetooth_address(address); device = [IOBluetoothDevice deviceWithAddress:&iobtAddress]; if (!device) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create a device"; + qCCritical(QT_BT_OSX) << "failed to create a device"; return kIOReturnNoDevice; } const IOReturn status = [device openL2CAPChannelAsync:&channel withPSM:psm delegate:self]; if (status != kIOReturnSuccess) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to open L2CAP channel"; + qCCritical(QT_BT_OSX) << "failed to open L2CAP channel"; // device is still autoreleased. device = nil; return status; diff --git a/src/bluetooth/osx/osxbtledeviceinquiry.mm b/src/bluetooth/osx/osxbtledeviceinquiry.mm index 55ffc36f..2dabd0c1 100644 --- a/src/bluetooth/osx/osxbtledeviceinquiry.mm +++ b/src/bluetooth/osx/osxbtledeviceinquiry.mm @@ -39,19 +39,19 @@ #include "osxbtledeviceinquiry_p.h" #include "qbluetoothdeviceinfo.h" +#include "osxbtnotifier_p.h" #include "qbluetoothuuid.h" #include "osxbtutility_p.h" #include <QtCore/qloggingcategory.h> -#include <QtCore/qsysinfo.h> #include <QtCore/qdebug.h> +#include <algorithm> + QT_BEGIN_NAMESPACE namespace OSXBluetooth { -#if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_6_0) - QBluetoothUuid qt_uuid(NSUUID *nsUuid) { if (!nsUuid) @@ -64,58 +64,74 @@ QBluetoothUuid qt_uuid(NSUUID *nsUuid) return QBluetoothUuid(qtUuidData); } -#endif - -QBluetoothUuid qt_uuid(CFUUIDRef uuid) +const int timeStepMS = 100; + +const int powerOffTimeoutMS = 30000; +const qreal powerOffTimeStepS = 30. / 100.; + +struct AdvertisementData { + // That's what CoreBluetooth has: + // CBAdvertisementDataLocalNameKey + // CBAdvertisementDataTxPowerLevelKey + // CBAdvertisementDataServiceUUIDsKey + // CBAdvertisementDataServiceDataKey + // CBAdvertisementDataManufacturerDataKey + // CBAdvertisementDataOverflowServiceUUIDsKey + // CBAdvertisementDataIsConnectable + // CBAdvertisementDataSolicitedServiceUUIDsKey + + // For now, we "parse": + QString localName; + QList<QBluetoothUuid> serviceUuids; + // TODO: other keys probably? + AdvertisementData(NSDictionary *AdvertisementData); +}; + +AdvertisementData::AdvertisementData(NSDictionary *advertisementData) { - if (!uuid) - return QBluetoothUuid(); - - const CFUUIDBytes data = CFUUIDGetUUIDBytes(uuid); - quint128 qtUuidData = {{data.byte0, data.byte1, data.byte2, data.byte3, - data.byte4, data.byte5, data.byte6, data.byte7, - data.byte8, data.byte9, data.byte10, data.byte11, - data.byte12, data.byte13, data.byte14, data.byte15}}; - - return QBluetoothUuid(qtUuidData); -} - -typedef ObjCStrongReference<NSString> StringStrongReference; + if (!advertisementData) + return; -StringStrongReference uuid_as_nsstring(CFUUIDRef uuid) -{ - // We use the UUDI's string representation as a key in a dictionary. - if (!uuid) - return StringStrongReference(); + // ... constant CBAdvertisementDataLocalNameKey ... + // NSString containing the local name of a peripheral. + NSObject *value = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey]; + if (value && [value isKindOfClass:[NSString class]]) + localName = QString::fromNSString(static_cast<NSString *>(value)); - CFStringRef cfStr = CFUUIDCreateString(kCFAllocatorDefault, uuid); - if (!cfStr) - return StringStrongReference(); + // ... constant CBAdvertisementDataServiceUUIDsKey ... + // A list of one or more CBUUID objects, representing CBService UUIDs. - // Imporant: with ARC this will require a different cast/ownership! - return StringStrongReference((NSString *)cfStr, false); + value = [advertisementData objectForKey:CBAdvertisementDataServiceUUIDsKey]; + if (value && [value isKindOfClass:[NSArray class]]) { + NSArray *uuids = static_cast<NSArray *>(value); + for (CBUUID *cbUuid in uuids) + serviceUuids << qt_uuid(cbUuid); + } } } - QT_END_NAMESPACE QT_USE_NAMESPACE @interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) (PrivateAPI) <CBCentralManagerDelegate> +// These two methods are scheduled with a small time step +// within a given timeout, they either re-schedule +// themselves or emit a signal/stop some operation. - (void)stopScan; - (void)handlePoweredOff; @end @implementation QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) -- (id)init +-(id)initWithNotifier:(LECBManagerNotifier *)aNotifier { if (self = [super init]) { - uuids.reset([[NSMutableSet alloc] init]); + Q_ASSERT(aNotifier); + notifier = aNotifier; internalState = InquiryStarting; - state.store(int(internalState)); + inquiryTimeoutMS = OSXBluetooth::defaultLEScanTimeoutMS; } return self; @@ -129,27 +145,36 @@ QT_USE_NAMESPACE [manager stopScan]; } + if (notifier) { + notifier->disconnect(); + notifier->deleteLater(); + } + [super dealloc]; } - (void)stopScan { - // Scan's "timeout" - we consider LE device - // discovery finished. using namespace OSXBluetooth; + // We never schedule stopScan if there is no timeout: + Q_ASSERT(inquiryTimeoutMS > 0); + if (internalState == InquiryActive) { - if (scanTimer.elapsed() >= qt_LE_deviceInquiryLength() * 1000) { - // We indeed stop now: + const int elapsed = scanTimer.elapsed(); + if (elapsed >= inquiryTimeoutMS) { [manager stopScan]; [manager setDelegate:nil]; internalState = InquiryFinished; - state.store(int(internalState)); + Q_ASSERT(notifier); + emit notifier->discoveryFinished(); } else { + // Re-schedule 'stopScan': dispatch_queue_t leQueue(qt_LE_queue()); Q_ASSERT(leQueue); + const int timeChunkMS = std::min(inquiryTimeoutMS - elapsed, timeStepMS); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, - int64_t(qt_LE_deviceInquiryLength() / 100. * NSEC_PER_SEC)), + int64_t(timeChunkMS / 1000. * NSEC_PER_SEC)), leQueue, ^{ [self stopScan]; @@ -164,30 +189,32 @@ QT_USE_NAMESPACE // the system shows an alert asking to enable // Bluetooth in the 'Settings' app. If not done yet (after 30 // seconds) - we consider it an error. + using namespace OSXBluetooth; + if (internalState == InquiryStarting) { - if (errorTimer.elapsed() >= 30000) { + if (errorTimer.elapsed() >= powerOffTimeoutMS) { [manager setDelegate:nil]; internalState = ErrorPoweredOff; - state.store(int(internalState)); + Q_ASSERT(notifier); + emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); } else { - dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); + dispatch_queue_t leQueue(qt_LE_queue()); Q_ASSERT(leQueue); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(30 / 100. * NSEC_PER_SEC)), + (int64_t)(powerOffTimeStepS * NSEC_PER_SEC)), leQueue, ^{ [self handlePoweredOff]; }); - } } } -- (void)start +- (void)startWithTimeout:(int)timeout { dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); - Q_ASSERT(leQueue); + inquiryTimeoutMS = timeout; manager.reset([[CBCentralManager alloc] initWithDelegate:self queue:leQueue]); } @@ -199,61 +226,73 @@ QT_USE_NAMESPACE if (internalState != InquiryActive && internalState != InquiryStarting) return; + Q_ASSERT(notifier); + using namespace OSXBluetooth; dispatch_queue_t leQueue(qt_LE_queue()); Q_ASSERT(leQueue); #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) - const CBManagerState cbState(central.state); - if (cbState == CBManagerStatePoweredOn) { + const CBManagerState state(central.state); + if (state == CBManagerStatePoweredOn) { #else - const CBCentralManagerState cbState(central.state); - if (cbState == CBCentralManagerStatePoweredOn) { + const CBCentralManagerState state(central.state); + if (state == CBCentralManagerStatePoweredOn) { #endif if (internalState == InquiryStarting) { internalState = InquiryActive; - // Scan time is actually 10 seconds. Having a block with such delay can prevent - // 'self' from being deleted in time, which is not good. So we split this - // 10 s. timeout into smaller 'chunks'. - scanTimer.start(); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, - int64_t(qt_LE_deviceInquiryLength() / 100. * NSEC_PER_SEC)), - leQueue, - ^{ - [self stopScan]; - }); + + if (inquiryTimeoutMS > 0) { + // We have a finite-length discovery, schedule stopScan, + // with a smaller time step, otherwise it can prevent + // 'self' from being deleted in time, which is not good + // (the block will retain 'self', waiting for timeout). + scanTimer.start(); + const int timeChunkMS = std::min(timeStepMS, inquiryTimeoutMS); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + int64_t(timeChunkMS / 1000. * NSEC_PER_SEC)), + leQueue, + ^{ + [self stopScan]; + }); + } + [manager scanForPeripheralsWithServices:nil options:nil]; } // Else we ignore. #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) - } else if (cbState == CBManagerStateUnsupported || cbState == CBManagerStateUnauthorized) { + } else if (state == CBManagerStateUnsupported || state == CBManagerStateUnauthorized) { #else - } else if (cbState == CBCentralManagerStateUnsupported || cbState == CBCentralManagerStateUnauthorized) { + } else if (state == CBCentralManagerStateUnsupported || state == CBCentralManagerStateUnauthorized) { #endif if (internalState == InquiryActive) { [manager stopScan]; - // Not sure how this is possible at all, probably, can never happen. + // Not sure how this is possible at all, + // probably, can never happen. internalState = ErrorPoweredOff; + emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); } else { internalState = ErrorLENotSupported; + emit notifier->LEnotSupported(); } [manager setDelegate:nil]; #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) - } else if (cbState == CBManagerStatePoweredOff) { + } else if (state == CBManagerStatePoweredOff) { #else - } else if (cbState == CBCentralManagerStatePoweredOff) { + } else if (state == CBCentralManagerStatePoweredOff) { #endif if (internalState == InquiryStarting) { #ifndef Q_OS_OSX - // On iOS a user can see at this point an alert asking to enable - // Bluetooth in the "Settings" app. If a user does, + // On iOS a user can see at this point an alert asking to + // enable Bluetooth in the "Settings" app. If a user does, // we'll receive 'PoweredOn' state update later. - // No change in state. Wait for 30 seconds (we split it into 'chunks' not - // to retain 'self' for too long ) ... + // No change in internalState. Wait for 30 seconds + // (we split it into smaller steps not to retain 'self' for + // too long ) ... errorTimer.start(); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(30 / 100. * NSEC_PER_SEC)), + (int64_t)(powerOffTimeStepS * NSEC_PER_SEC)), leQueue, ^{ [self handlePoweredOff]; @@ -261,9 +300,10 @@ QT_USE_NAMESPACE return; #endif internalState = ErrorPoweredOff; + emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); } else { - internalState = ErrorPoweredOff; [manager stopScan]; + emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); } [manager setDelegate:nil]; @@ -279,8 +319,6 @@ QT_USE_NAMESPACE // lost; an update is imminent. " // Wait for this imminent update. } - - state.store(int(internalState)); } - (void)stop @@ -290,14 +328,17 @@ QT_USE_NAMESPACE [manager setDelegate:nil]; internalState = InquiryCancelled; - state.store(int(internalState)); + + notifier->disconnect(); + notifier->deleteLater(); + notifier = nullptr; } -- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral - advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI +- (void)centralManager:(CBCentralManager *)central + didDiscoverPeripheral:(CBPeripheral *)peripheral + advertisementData:(NSDictionary *)advertisementData + RSSI:(NSNumber *)RSSI { - Q_UNUSED(advertisementData); - using namespace OSXBluetooth; if (central != manager) @@ -306,49 +347,20 @@ QT_USE_NAMESPACE if (internalState != InquiryActive) return; - QBluetoothUuid deviceUuid; - -#if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_7_0) - if (QSysInfo::MacintoshVersion >= qt_OS_limit(QSysInfo::MV_10_9, QSysInfo::MV_IOS_7_0)) { - if (!peripheral.identifier) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "peripheral without NSUUID"; - return; - } + if (!notifier) + return; - if ([uuids containsObject:peripheral.identifier]) { - // We already know this peripheral ... - return; - } + QBluetoothUuid deviceUuid; - [uuids addObject:peripheral.identifier]; - deviceUuid = OSXBluetooth::qt_uuid(peripheral.identifier); + if (!peripheral.identifier) { + qCWarning(QT_BT_OSX) << "peripheral without NSUUID"; + return; } -#endif - // Either SDK or the target is below 10.9/7.0: - // The property UUID was finally removed in iOS 9, we have - // to avoid compilation errors ... - if (deviceUuid.isNull()) { - CFUUIDRef cfUUID = Q_NULLPTR; - - if ([peripheral respondsToSelector:@selector(UUID)]) { - // This will require a bridged cast if we switch to ARC ... - cfUUID = reinterpret_cast<CFUUIDRef>([peripheral performSelector:@selector(UUID)]); - } - - if (!cfUUID) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "peripheral without CFUUID"; - return; - } - StringStrongReference key(uuid_as_nsstring(cfUUID)); - if ([uuids containsObject:key.data()]) - return; // We've seen this peripheral before ... - [uuids addObject:key.data()]; - deviceUuid = OSXBluetooth::qt_uuid(cfUUID); - } + deviceUuid = OSXBluetooth::qt_uuid(peripheral.identifier); if (deviceUuid.isNull()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no way to address peripheral, QBluetoothUuid is null"; + qCWarning(QT_BT_OSX) << "no way to address peripheral, QBluetoothUuid is null"; return; } @@ -356,23 +368,23 @@ QT_USE_NAMESPACE if (peripheral.name) name = QString::fromNSString(peripheral.name); + const AdvertisementData qtAdvData(advertisementData); + if (!name.size()) // Probably, it's not possible to have one and not the other. + name = qtAdvData.localName; + // TODO: fix 'classOfDevice' (0 for now). QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0); if (RSSI) newDeviceInfo.setRssi([RSSI shortValue]); - // CoreBluetooth scans only for LE devices. - newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); - devices.append(newDeviceInfo); -} -- (LEInquiryState) inquiryState -{ - return LEInquiryState(state.load()); -} + if (qtAdvData.serviceUuids.size()) { + newDeviceInfo.setServiceUuids(qtAdvData.serviceUuids, + QBluetoothDeviceInfo::DataIncomplete); + } -- (const QList<QBluetoothDeviceInfo> &)discoveredDevices -{ - return devices; + // CoreBluetooth scans only for LE devices. + newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); + emit notifier->deviceDiscovered(newDeviceInfo); } @end diff --git a/src/bluetooth/osx/osxbtledeviceinquiry_p.h b/src/bluetooth/osx/osxbtledeviceinquiry_p.h index 71e8ef53..fc787a6d 100644 --- a/src/bluetooth/osx/osxbtledeviceinquiry_p.h +++ b/src/bluetooth/osx/osxbtledeviceinquiry_p.h @@ -58,7 +58,6 @@ #include <QtCore/qelapsedtimer.h> #include <QtCore/qglobal.h> -#include <QtCore/qatomic.h> #include <QtCore/qlist.h> #include <Foundation/Foundation.h> @@ -67,8 +66,20 @@ QT_BEGIN_NAMESPACE class QBluetoothUuid; +namespace OSXBluetooth +{ + +class LECBManagerNotifier; + +} + QT_END_NAMESPACE +// Ugly but all these QT_PREPEND_NAMESPACE etc. are even worse ... +using OSXBluetooth::LECBManagerNotifier; +using OSXBluetooth::ObjCScopedPointer; +using QT_PREPEND_NAMESPACE(QElapsedTimer); + enum LEInquiryState { InquiryStarting, @@ -81,29 +92,26 @@ enum LEInquiryState @interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) : NSObject { - QT_PREPEND_NAMESPACE(OSXBluetooth)::ObjCScopedPointer<NSMutableSet> uuids; - QT_PREPEND_NAMESPACE(OSXBluetooth)::ObjCScopedPointer<CBCentralManager> manager; + LECBManagerNotifier *notifier; + ObjCScopedPointer<CBCentralManager> manager; QList<QBluetoothDeviceInfo> devices; - LEInquiryState internalState; - QT_PREPEND_NAMESPACE(QAtomicInt) state; + int inquiryTimeoutMS; // Timers to check if we can execute delayed callbacks: QT_PREPEND_NAMESPACE(QElapsedTimer) errorTimer; QT_PREPEND_NAMESPACE(QElapsedTimer) scanTimer; } -- (id)init; +- (id)initWithNotifier:(LECBManagerNotifier *)aNotifier; - (void)dealloc; -// IMPORTANT: both 'start' and 'stop' are to be executed on the "Qt's LE queue". -- (void)start; +// IMPORTANT: both 'startWithTimeout' and 'stop' +// can be executed only on the "Qt's LE queue". +- (void)startWithTimeout:(int)timeout; - (void)stop; -- (LEInquiryState)inquiryState; -- (const QList<QBluetoothDeviceInfo> &)discoveredDevices; - @end #endif diff --git a/src/bluetooth/osx/osxbtnotifier_p.h b/src/bluetooth/osx/osxbtnotifier_p.h index 6cb2b019..47ee6ba1 100644 --- a/src/bluetooth/osx/osxbtnotifier_p.h +++ b/src/bluetooth/osx/osxbtnotifier_p.h @@ -52,7 +52,9 @@ // +#include "qbluetoothdevicediscoveryagent.h" #include "qlowenergycontroller.h" +#include "qbluetoothdeviceinfo.h" #include "qbluetoothuuid.h" #include "qbluetooth.h" @@ -68,11 +70,14 @@ class QLowEnergyServicePrivate; namespace OSXBluetooth { -class LECentralNotifier : public QObject +class LECBManagerNotifier : public QObject { Q_OBJECT Q_SIGNALS: + void deviceDiscovered(QBluetoothDeviceInfo deviceInfo); + void discoveryFinished(); + void connected(); void disconnected(); @@ -83,11 +88,13 @@ Q_SIGNALS: void characteristicUpdated(QLowEnergyHandle charHandle, const QByteArray &value); void descriptorRead(QLowEnergyHandle descHandle, const QByteArray &value); void descriptorWritten(QLowEnergyHandle descHandle, const QByteArray &value); + void notificationEnabled(QLowEnergyHandle charHandle, bool enabled); void LEnotSupported(); - void CBCentralManagerError(QLowEnergyController::Error error); - void CBCentralManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyController::Error error); - void CBCentralManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyService::ServiceError error); + void CBManagerError(QBluetoothDeviceDiscoveryAgent::Error error); + void CBManagerError(QLowEnergyController::Error error); + void CBManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyController::Error error); + void CBManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyService::ServiceError error); }; diff --git a/src/bluetooth/osx/osxbtobexsession.mm b/src/bluetooth/osx/osxbtobexsession.mm index 9e324405..b8e604c8 100644 --- a/src/bluetooth/osx/osxbtobexsession.mm +++ b/src/bluetooth/osx/osxbtobexsession.mm @@ -203,7 +203,7 @@ QList<OBEXHeader> qt_bluetooth_headers(const uint8_t *data, std::size_t length) break; } default: - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid header format"; + qCWarning(QT_BT_OSX) << "invalid header format"; return empty; } @@ -358,7 +358,7 @@ bool check_connect_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode response = e->u.connectCommandResponseData.serverResponseOpCode; return response == kOBEXResponseCodeSuccessWithFinalBit; } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected event type"; + qCWarning(QT_BT_OSX) << "unexpected event type"; error = kOBEXGeneralError; return false; } @@ -378,7 +378,7 @@ bool check_put_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode &re return response == kOBEXResponseCodeContinueWithFinalBit || response == kOBEXResponseCodeSuccessWithFinalBit; } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected event type"; + qCWarning(QT_BT_OSX) << "unexpected event type"; error = kOBEXGeneralError; return false; } @@ -395,7 +395,7 @@ bool check_abort_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode & response = e->u.abortCommandResponseData.serverResponseOpCode; return response == kOBEXResponseCodeSuccessWithFinalBit; } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected event type"; + qCWarning(QT_BT_OSX) << "unexpected event type"; return false; } } @@ -443,13 +443,13 @@ QT_USE_NAMESPACE const BluetoothDeviceAddress addr(OSXBluetooth::iobluetooth_address(deviceAddress)); device = [[IOBluetoothDevice deviceWithAddress:&addr] retain]; if (!device) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to create an IOBluetoothDevice"; + qCWarning(QT_BT_OSX) << "failed to create an IOBluetoothDevice"; return self; } session = [[IOBluetoothOBEXSession alloc] initWithDevice:device channelID:port]; if (!session) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to create an OBEX session"; + qCWarning(QT_BT_OSX) << "failed to create an OBEX session"; return self; } @@ -474,7 +474,7 @@ QT_USE_NAMESPACE - (OBEXError)OBEXConnect { if (!session) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid session (nil)"; + qCWarning(QT_BT_OSX) << "invalid session (nil)"; return kOBEXGeneralError; } @@ -517,7 +517,7 @@ QT_USE_NAMESPACE } if (currentRequest != OBEXConnect) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "called while there is no " + qCWarning(QT_BT_OSX) << "called while there is no " "active connect request"; return; } @@ -596,7 +596,7 @@ QT_USE_NAMESPACE Q_ASSERT_X(session, Q_FUNC_INFO, "invalid OBEX session (nil)"); if (currentRequest != OBEXAbort) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "called while there " + qCWarning(QT_BT_OSX) << "called while there " "is no ABORT request"; return; } @@ -628,13 +628,13 @@ QT_USE_NAMESPACE // a payload. const qint64 fileSize = input->size(); if (fileSize <= 0 || fileSize >= std::numeric_limits<uint32_t>::max()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid input file size"; + qCWarning(QT_BT_OSX) << "invalid input file size"; return kOBEXBadArgumentError; } ObjCStrongReference<NSMutableData> headers([[NSMutableData alloc] init], false); if (!headers) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to allocate headers"; + qCWarning(QT_BT_OSX) << "failed to allocate headers"; return kOBEXNoResourcesError; } @@ -644,16 +644,14 @@ QT_USE_NAMESPACE if (connectionIDFound) { if (!append_four_byte_header(headers, kOBEXHeaderIDConnectionID, connectionID)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "append connection ID header"; + qCWarning(QT_BT_OSX) << "failed to append connection ID header"; return kOBEXNoResourcesError; } } if (name.length()) { if (!append_unicode_header(headers, kOBEXHeaderIDName, name)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to append " - "a unicode string"; + qCWarning(QT_BT_OSX) << "failed to append a unicode string"; return kOBEXNoResourcesError; } } @@ -666,7 +664,7 @@ QT_USE_NAMESPACE if (!chunk || ![chunk length]) { // We do not support PUT-DELETE (?) // At least the first chunk is expected to be non-empty. - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid input stream"; + qCWarning(QT_BT_OSX) << "invalid input stream"; return kOBEXBadArgumentError; } @@ -713,7 +711,7 @@ QT_USE_NAMESPACE } if (currentRequest != OBEXPut) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "called while the current " + qCWarning(QT_BT_OSX) << "called while the current " "request is not a put request"; return; } @@ -735,8 +733,7 @@ QT_USE_NAMESPACE // 0 for the headers length, no more headers. ObjCStrongReference<NSMutableData> chunk(next_data_chunk(*inputStream, session, 0, lastChunk)); if (!chunk && !lastChunk) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "allocate the next memory chunk"; + qCWarning(QT_BT_OSX) << "failed to allocate the next memory chunk"; return; } @@ -752,8 +749,7 @@ QT_USE_NAMESPACE refCon:Q_NULLPTR]; if (status != kOBEXSuccess) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "send the next memory chunk"; + qCWarning(QT_BT_OSX) << "failed to send the next memory chunk"; currentRequest = OBEXNoop; if (delegate) // Response code is not important here. delegate->OBEXPutError(kOBEXNoResourcesError, 0); diff --git a/src/bluetooth/osx/osxbtperipheralmanager.mm b/src/bluetooth/osx/osxbtperipheralmanager.mm new file mode 100644 index 00000000..9c443cf6 --- /dev/null +++ b/src/bluetooth/osx/osxbtperipheralmanager.mm @@ -0,0 +1,864 @@ +/**************************************************************************** +** +** 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 <QtCore/qdebug.h> +#include <QtCore/qlist.h> + +#include <algorithm> +#include <limits> + +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<CBMutableCharacteristic> create_characteristic(const QLowEnergyCharacteristicData &data) +{ + const ObjCStrongReference<CBMutableCharacteristic> 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<CBMutableDescriptor> 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<NSObject> 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<CBMutableDescriptor> 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<quint32>::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<NSUInteger>::max(); + } + + return self; +} + +- (void)dealloc +{ + [self detach]; + [super dealloc]; +} + +- (QSharedPointer<QLowEnergyServicePrivate>)addService:(const QLowEnergyServiceData &)data +{ + using QLES = QLowEnergyService; + + const auto nEntries = qt_countGATTEntries(data); + if (!nEntries || nEntries > std::numeric_limits<QLowEnergyHandle>::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<CBMutableService> + 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<QLowEnergyServicePrivate>::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<NSMutableArray> 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<NSData> 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) + 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) + 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<QLowEnergyHandle, NSUInteger> 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<NSData> 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<CBMutableCharacteristic *>(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<NSUInteger>::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<NSMutableArray> 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<NSMutableArray> 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<NSMutableArray> 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 diff --git a/src/bluetooth/osx/osxbtperipheralmanager_p.h b/src/bluetooth/osx/osxbtperipheralmanager_p.h new file mode 100644 index 00000000..19a831d8 --- /dev/null +++ b/src/bluetooth/osx/osxbtperipheralmanager_p.h @@ -0,0 +1,190 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef OSXBTPERIPHERALMANAGER_P_H +#define OSXBTPERIPHERALMANAGER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of internal files. This header file may change from version to version +// without notice, or even be removed. +// +// We mean it. +// + +#include "osxbtutility_p.h" + +#include "qlowenergyadvertisingparameters.h" +#include "qlowenergyserviceprivate_p.h" +#include "qbluetoothuuid.h" +#include "qbluetooth.h" + +#include <QtCore/qsharedpointer.h> +#include <QtCore/qbytearray.h> +#include <QtCore/qsysinfo.h> +#include <QtCore/qglobal.h> +#include <QtCore/qpair.h> +#include <QtCore/qmap.h> + +#include <vector> +#include <deque> +#include <map> + +#include <Foundation/Foundation.h> + +#include "osxbluetooth_p.h" + +QT_BEGIN_NAMESPACE + +class QLowEnergyServiceData; + +namespace OSXBluetooth +{ + +class LECBManagerNotifier; + +} + +QT_END_NAMESPACE + + +// Exposing names in a header is ugly, but constant QT_PREPEND_NAMESPACE is even worse ... +// After all, this header is to be included only in its own and controller's *.mm files. + +QT_USE_NAMESPACE + +using namespace OSXBluetooth; + + +template<class Type> +using GenericLEMap = QMap<QLowEnergyHandle, Type>; + +enum class PeripheralState +{ + idle, + waitingForPowerOn, + advertising, + connected +}; + +struct UpdateRequest +{ + UpdateRequest() = default; + UpdateRequest(QLowEnergyHandle handle, const ObjCStrongReference<NSData> &val) + : charHandle(handle), + value(val) + { + } + + QLowEnergyHandle charHandle = {}; + ObjCStrongReference<NSData> value; +}; + +using ValueRange = QPair<NSUInteger, NSUInteger>; + +@interface QT_MANGLE_NAMESPACE(OSXBTPeripheralManager) : NSObject<CBPeripheralManagerDelegate> +{ + ObjCScopedPointer<CBPeripheralManager> manager; + LECBManagerNotifier *notifier; + + QLowEnergyHandle lastHandle; + // Services in this vector are placed in such order: + // the one that has included services, must + // follow its included services to avoid exceptions from CBPeripheralManager. + std::vector<ObjCStrongReference<CBMutableService>> services; + decltype(services.size()) nextServiceToAdd; + + // Lookup map for included services: + std::map<QBluetoothUuid, CBService *> serviceIndex; + ObjCScopedPointer<NSMutableDictionary> advertisementData; + + GenericLEMap<CBCharacteristic *> charMap; + GenericLEMap<ObjCStrongReference<NSMutableData>> charValues; + + QMap<QLowEnergyHandle, ValueRange> valueRanges; + + std::deque<UpdateRequest> updateQueue; + + ObjCScopedPointer<NSMutableSet> connectedCentrals; + + PeripheralState state; + NSUInteger maxNotificationValueLength; +} + +- (id)initWith:(LECBManagerNotifier *)notifier; +- (void)dealloc; + +- (QSharedPointer<QLowEnergyServicePrivate>)addService:(const QLowEnergyServiceData &)data; +- (void) setParameters:(const QLowEnergyAdvertisingParameters &)parameters + data:(const QLowEnergyAdvertisingData &)data + scanResponse:(const QLowEnergyAdvertisingData &)scanResponse; + +// To be executed on the Qt's special BTLE dispatch queue. +- (void)startAdvertising; +- (void)stopAdvertising; +- (void)detach; + +- (void)write:(const QByteArray &)value + charHandle:(QLowEnergyHandle)charHandle; + + +// CBPeripheralManagerDelegate's callbacks (BTLE queue). +- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral; +- (void)peripheralManager:(CBPeripheralManager *)peripheral + willRestoreState:(NSDictionary *)dict; +- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral + error:(NSError *)error; +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didAddService:(CBService *)service error:(NSError *)error; +- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central + didSubscribeToCharacteristic:(CBCharacteristic *)characteristic; +- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central + didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic; +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveReadRequest:(CBATTRequest *)request; +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveWriteRequests:(NSArray *)requests; +- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral; + +@end + +#endif diff --git a/src/bluetooth/osx/osxbtrfcommchannel.mm b/src/bluetooth/osx/osxbtrfcommchannel.mm index e929f335..ea679ec8 100644 --- a/src/bluetooth/osx/osxbtrfcommchannel.mm +++ b/src/bluetooth/osx/osxbtrfcommchannel.mm @@ -97,13 +97,13 @@ QT_USE_NAMESPACE withChannelID:(BluetoothRFCOMMChannelID)channelID { if (address.isNull()) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "invalid peer address"; + qCCritical(QT_BT_OSX) << "invalid peer address"; return kIOReturnNoDevice; } // Can never be called twice. if (connected || device || channel) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "connection is already active"; + qCCritical(QT_BT_OSX) << "connection is already active"; return kIOReturnStillOpen; } @@ -112,14 +112,14 @@ QT_USE_NAMESPACE const BluetoothDeviceAddress iobtAddress = OSXBluetooth::iobluetooth_address(address); device = [IOBluetoothDevice deviceWithAddress:&iobtAddress]; if (!device) { // TODO: do I always check this BTW??? Apple's docs say nothing about nil. - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create a device"; + qCCritical(QT_BT_OSX) << "failed to create a device"; return kIOReturnNoDevice; } const IOReturn status = [device openRFCOMMChannelAsync:&channel withChannelID:channelID delegate:self]; if (status != kIOReturnSuccess) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to open L2CAP channel"; + qCCritical(QT_BT_OSX) << "failed to open L2CAP channel"; // device is still autoreleased. device = nil; return status; diff --git a/src/bluetooth/osx/osxbtsdpinquiry.mm b/src/bluetooth/osx/osxbtsdpinquiry.mm index a1d6af1c..a0bfdeef 100644 --- a/src/bluetooth/osx/osxbtsdpinquiry.mm +++ b/src/bluetooth/osx/osxbtsdpinquiry.mm @@ -184,7 +184,7 @@ using namespace OSXBluetooth; if (qtFilters.size()) { array.reset([[NSMutableArray alloc] init]); if (!array) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to allocate an uuid filter"; + qCCritical(QT_BT_OSX) << "failed to allocate an uuid filter"; return kIOReturnError; } @@ -195,7 +195,7 @@ using namespace OSXBluetooth; } if (int([array count]) != qtFilters.size()) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create an uuid filter"; + qCCritical(QT_BT_OSX) << "failed to create an uuid filter"; return kIOReturnError; } } @@ -203,7 +203,7 @@ using namespace OSXBluetooth; const BluetoothDeviceAddress iobtAddress(iobluetooth_address(address)); ObjCScopedPointer<IOBluetoothDevice> newDevice([[IOBluetoothDevice deviceWithAddress:&iobtAddress] retain]); if (!newDevice) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create an IOBluetoothDevice object"; + qCCritical(QT_BT_OSX) << "failed to create an IOBluetoothDevice object"; return kIOReturnError; } @@ -217,7 +217,7 @@ using namespace OSXBluetooth; result = [device performSDPQuery:self]; if (result != kIOReturnSuccess) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to start an SDP query"; + qCCritical(QT_BT_OSX) << "failed to start an SDP query"; device = oldDevice.take(); } else { isActive = true; diff --git a/src/bluetooth/osx/osxbtutility.mm b/src/bluetooth/osx/osxbtutility.mm index 942fed1c..1508c89f 100644 --- a/src/bluetooth/osx/osxbtutility.mm +++ b/src/bluetooth/osx/osxbtutility.mm @@ -37,6 +37,7 @@ ** ****************************************************************************/ +#include "qlowenergycharacteristicdata.h" #include "qbluetoothaddress.h" #include "osxbtutility_p.h" #include "qbluetoothuuid.h" @@ -70,6 +71,10 @@ Q_LOGGING_CATEGORY(QT_BT_OSX, "qt.bluetooth.ios") namespace OSXBluetooth { +const int defaultLEScanTimeoutMS = 25000; +// We use it only on iOS for now: +const int maxValueLength = 512; + QString qt_address(NSString *address) { if (address && address.length) { @@ -314,6 +319,19 @@ ObjCStrongReference<NSData> data_from_bytearray(const QByteArray & qtData) return result; } +ObjCStrongReference<NSMutableData> mutable_data_from_bytearray(const QByteArray &qtData) +{ + using MutableData = ObjCStrongReference<NSMutableData>; + + if (!qtData.size()) + return MutableData([[NSMutableData alloc] init], false); + + MutableData result([[NSMutableData alloc] initWithLength:qtData.size()], false); + [result replaceBytesInRange:NSMakeRange(0, qtData.size()) + withBytes:qtData.constData()]; + return result; +} + // A small RAII class for a dispatch queue. class SerialDispatchQueue { @@ -350,11 +368,6 @@ dispatch_queue_t qt_LE_queue() return leQueue.data(); } -unsigned qt_LE_deviceInquiryLength() -{ - return 10; -} - } QT_END_NAMESPACE diff --git a/src/bluetooth/osx/osxbtutility_p.h b/src/bluetooth/osx/osxbtutility_p.h index de3d3ea2..148ebc0b 100644 --- a/src/bluetooth/osx/osxbtutility_p.h +++ b/src/bluetooth/osx/osxbtutility_p.h @@ -55,13 +55,14 @@ #include <QtCore/qloggingcategory.h> #include <QtCore/qscopedpointer.h> -#include <QtCore/qsysinfo.h> #include <QtCore/qglobal.h> #include <Foundation/Foundation.h> QT_BEGIN_NAMESPACE +class QLowEnergyCharacteristicData; +class QBluetoothAddress; class QBluetoothUuid; namespace OSXBluetooth { @@ -280,7 +281,7 @@ QString qt_address(NSString *address); #ifndef QT_IOS_BLUETOOTH -class QBluetoothAddress qt_address(const BluetoothDeviceAddress *address); +QBluetoothAddress qt_address(const BluetoothDeviceAddress *address); BluetoothDeviceAddress iobluetooth_address(const QBluetoothAddress &address); ObjCStrongReference<IOBluetoothSDPUUID> iobluetooth_uuid(const QBluetoothUuid &uuid); @@ -296,22 +297,14 @@ bool equal_uuids(const QBluetoothUuid &qtUuid, CBUUID *cbUuid); bool equal_uuids(CBUUID *cbUuid, const QBluetoothUuid &qtUuid); QByteArray qt_bytearray(NSData *data); QByteArray qt_bytearray(NSObject *data); -ObjCStrongReference<NSData> data_from_bytearray(const QByteArray & qtData); -inline QSysInfo::MacVersion qt_OS_limit(QSysInfo::MacVersion osxVersion, QSysInfo::MacVersion iosVersion) -{ -#ifdef Q_OS_OSX - Q_UNUSED(iosVersion) - return osxVersion; -#else - Q_UNUSED(osxVersion) - return iosVersion; -#endif -} +ObjCStrongReference<NSData> data_from_bytearray(const QByteArray &qtData); +ObjCStrongReference<NSMutableData> mutable_data_from_bytearray(const QByteArray &qtData); dispatch_queue_t qt_LE_queue(); -// LE scan, in seconds. -unsigned qt_LE_deviceInquiryLength(); + +extern const int defaultLEScanTimeoutMS; +extern const int maxValueLength; } // namespace OSXBluetooth diff --git a/src/bluetooth/qbluetooth.cpp b/src/bluetooth/qbluetooth.cpp index 483fe65a..37a4774d 100644 --- a/src/bluetooth/qbluetooth.cpp +++ b/src/bluetooth/qbluetooth.cpp @@ -101,5 +101,6 @@ namespace QBluetooth { Q_LOGGING_CATEGORY(QT_BT, "qt.bluetooth") Q_LOGGING_CATEGORY(QT_BT_ANDROID, "qt.bluetooth.android") Q_LOGGING_CATEGORY(QT_BT_BLUEZ, "qt.bluetooth.bluez") +Q_LOGGING_CATEGORY(QT_BT_WINRT, "qt.bluetooth.winphone") QT_END_NAMESPACE diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent.cpp b/src/bluetooth/qbluetoothdevicediscoveryagent.cpp index b6faee75..b033ae3c 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent.cpp +++ b/src/bluetooth/qbluetoothdevicediscoveryagent.cpp @@ -40,9 +40,12 @@ #include "qbluetoothdevicediscoveryagent.h" #include "qbluetoothdevicediscoveryagent_p.h" +#include <QtCore/qloggingcategory.h> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(QT_BT) + /*! \class QBluetoothDeviceDiscoveryAgent \inmodule QtBluetooth @@ -88,6 +91,8 @@ QT_BEGIN_NAMESPACE platform. The error is set in response to a call to \l start(). An example for such cases are iOS versions below 5.0 which do not support Bluetooth device search at all. This value was introduced by Qt 5.5. + \value UnsupportedDiscoveryMethod One of the requested discovery methods is not supported by + the current platform. This value was introduced by Qt 5.8. \value UnknownError An unknown error has occurred. */ @@ -110,6 +115,22 @@ QT_BEGIN_NAMESPACE */ /*! + \enum QBluetoothDeviceDiscoveryAgent::DiscoveryMethod + + This enum descibes the type of discovery method employed by the QBluetoothDeviceDiscoveryAgent. + + \value NoMethod The discovery is not possible. None of the available + methods are supported. + \value ClassicMethod The discovery process searches for Bluetooth Classic + (BaseRate) devices. + \value LowEnergyMethod The discovery process searches for Bluetooth Low Energy + devices. + + \sa supportedDiscoveryMethods() + \since 5.8 +*/ + +/*! \fn void QBluetoothDeviceDiscoveryAgent::deviceDiscovered(const QBluetoothDeviceInfo &info) This signal is emitted when the Bluetooth device described by \a info is discovered. @@ -232,16 +253,106 @@ QList<QBluetoothDeviceInfo> QBluetoothDeviceDiscoveryAgent::discoveredDevices() } /*! + Sets the maximum search time for Bluetooth Low Energy device search to + \a timeout in milliseconds. If \a timeout is \c 0 the discovery runs + until \l stop() is called. + + This reflects the fact that the discovery process for Bluetooth Low Energy devices + is mostly open ended. The platform continues to look for more devices until the search is + manually stopped. The timeout ensures that the search is aborted after \a timeout milliseconds. + Of course, it is still possible to manually abort the discovery by calling \l stop(). + + The new timeout value does not take effect until the device search is restarted. + In addition the timeout does not affect the classic Bluetooth device search. Depending on + the platform the classic search may add more time to the total discovery process + beyond \a timeout. + + \sa lowEnergyDiscoveryTimeout() + \since 5.8 + */ +void QBluetoothDeviceDiscoveryAgent::setLowEnergyDiscoveryTimeout(int timeout) +{ + Q_D(QBluetoothDeviceDiscoveryAgent); + + // cannot deliberately turn it off + if (d->lowEnergySearchTimeout < 0 || timeout < 0) { + qCDebug(QT_BT) << "The Bluetooth Low Energy device discovery timeout cannot be negative " + "or set on a backend which does not support this feature."; + return; + } + + d->lowEnergySearchTimeout = timeout; +} + +/*! + Returns a timeout in milliseconds that is applied to the Bluetooth Low Energy device search. + A value of \c -1 implies that the platform does not support this property and the timeout for + the device search cannot be adjusted. A return value of \c 0 + implies a never-ending search which must be manually stopped via \l stop(). + + \sa setLowEnergyDiscoveryTimeout() + \since 5.8 + */ +int QBluetoothDeviceDiscoveryAgent::lowEnergyDiscoveryTimeout() const +{ + Q_D(const QBluetoothDeviceDiscoveryAgent); + return d->lowEnergySearchTimeout; +} + +/*! + \fn QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() + + This function returns the discovery methods supported by the current platform. + It can be used to limit the scope of the device discovery. + + \since 5.8 +*/ + +/*! Starts Bluetooth device discovery, if it is not already started. The deviceDiscovered() signal is emitted as each device is discovered. The finished() signal - is emitted once device discovery is complete. + is emitted once device discovery is complete. The discovery utilizes the maximum set of + supported discovery methods on the platform. + + \sa supportedDiscoveryMethods() */ void QBluetoothDeviceDiscoveryAgent::start() { Q_D(QBluetoothDeviceDiscoveryAgent); if (!isActive() && d->lastError != InvalidBluetoothAdapterError) - d->start(); + d->start(supportedDiscoveryMethods()); +} + +/*! + Start Bluetooth device discovery, if it is not already started and the provided + \a methods are supported. + The discovery \a methods limit the scope of the device search. + For example, if the target service or device is a Bluetooth Low Energy device, + this function could be used to limit the search to Bluetooth Low Energy devices and + thereby reduces the discovery time significantly. + + \since 5.8 +*/ +void QBluetoothDeviceDiscoveryAgent::start(DiscoveryMethods methods) +{ + if (methods == NoMethod) + return; + + DiscoveryMethods supported = + QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods(); + + Q_D(QBluetoothDeviceDiscoveryAgent); + if (!((supported & methods) == methods)) { + d->lastError = UnsupportedDiscoveryMethod; + d->errorString = QBluetoothDeviceDiscoveryAgent::tr("One or more device discovery methods " + "are not supported on this platform"); + emit error(d->lastError); + return; + } + + if (!isActive() && d->lastError != InvalidBluetoothAdapterError) + d->start(methods); } /*! diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent.h b/src/bluetooth/qbluetoothdevicediscoveryagent.h index 954ae704..84087605 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent.h +++ b/src/bluetooth/qbluetoothdevicediscoveryagent.h @@ -65,6 +65,7 @@ public: PoweredOffError, InvalidBluetoothAdapterError, UnsupportedPlatformError, + UnsupportedDiscoveryMethod, UnknownError = 100 // New errors must be added before Unknown error }; Q_ENUM(Error) @@ -75,6 +76,15 @@ public: }; Q_ENUM(InquiryType) + enum DiscoveryMethod + { + NoMethod = 0x0, + ClassicMethod = 0x01, + LowEnergyMethod = 0x02, + }; + Q_DECLARE_FLAGS(DiscoveryMethods, DiscoveryMethod) + Q_FLAG(DiscoveryMethods) + explicit QBluetoothDeviceDiscoveryAgent(QObject *parent = Q_NULLPTR); explicit QBluetoothDeviceDiscoveryAgent(const QBluetoothAddress &deviceAdapter, QObject *parent = Q_NULLPTR); @@ -91,8 +101,13 @@ public: QList<QBluetoothDeviceInfo> discoveredDevices() const; + void setLowEnergyDiscoveryTimeout(int msTimeout); + int lowEnergyDiscoveryTimeout() const; + + static DiscoveryMethods supportedDiscoveryMethods(); public Q_SLOTS: void start(); + void start(DiscoveryMethods method); void stop(); Q_SIGNALS: @@ -116,6 +131,8 @@ private: #endif }; +Q_DECLARE_OPERATORS_FOR_FLAGS(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods) + QT_END_NAMESPACE #endif diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_android.cpp b/src/bluetooth/qbluetoothdevicediscoveryagent_android.cpp index 1a5df9e7..e76ddff7 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_android.cpp +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_android.cpp @@ -45,6 +45,7 @@ #include <QtCore/private/qjnihelpers_p.h> #include "android/devicediscoverybroadcastreceiver_p.h" #include <QtAndroidExtras/QAndroidJniEnvironment> +#include <QtAndroid> QT_BEGIN_NAMESPACE @@ -66,6 +67,7 @@ QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate( leScanTimeout(0), pendingCancel(false), pendingStart(false), + lowEnergySearchTimeout(25000), q_ptr(parent) { QAndroidJniEnvironment env; @@ -101,8 +103,16 @@ bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const return m_active != NoScanActive; } -void QBluetoothDeviceDiscoveryAgentPrivate::start() +QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() { + return (LowEnergyMethod | ClassicMethod); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods) +{ + //TODO Implement discovery method handling (see input parameter) + requestedMethods = methods; + if (pendingCancel) { pendingStart = true; return; @@ -136,6 +146,34 @@ void QBluetoothDeviceDiscoveryAgentPrivate::start() return; } + // check Android v23+ permissions + // -> BTLE search requires android.permission.ACCESS_COARSE_LOCATION + if (requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod + && QtAndroid::androidSdkVersion() >= 23) + { + QString permission(QLatin1String("android.permission.ACCESS_COARSE_LOCATION")); + + // do we have required permission already, if so nothing to do + if (QtAndroidPrivate::checkPermission(permission) == QtAndroidPrivate::PermissionsResult::Denied) { + qCWarning(QT_BT_ANDROID) << "Requesting ACCESS_COARSE_LOCATION permission"; + + QAndroidJniEnvironment env; + const QHash<QString, QtAndroidPrivate::PermissionsResult> results = + QtAndroidPrivate::requestPermissionsSync(env, QStringList() << permission); + if (!results.contains(permission) + || results[permission] == QtAndroidPrivate::PermissionsResult::Denied) + { + qCWarning(QT_BT_ANDROID) << "Search not possible due to missing permission (ACCESS_COARSE_LOCATION)"; + lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; + errorString = QBluetoothDeviceDiscoveryAgent::tr("Missing Location permission. Search is not possible"); + emit q->error(lastError); + return; + } + } + + qCWarning(QT_BT_ANDROID) << "ACCESS_COARSE_LOCATION permission available"; + } + // install Java BroadcastReceiver if (!receiver) { // SDP based device discovery @@ -148,18 +186,35 @@ void QBluetoothDeviceDiscoveryAgentPrivate::start() discoveredDevices.clear(); - const bool success = adapter.callMethod<jboolean>("startDiscovery"); - if (!success) { - lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError; - errorString = QBluetoothDeviceDiscoveryAgent::tr("Discovery cannot be started"); - emit q->error(lastError); - return; - } + // by arbitrary definition we run classic search first + if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) { + const bool success = adapter.callMethod<jboolean>("startDiscovery"); + if (!success) { + lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError; + errorString = QBluetoothDeviceDiscoveryAgent::tr("Classic Discovery cannot be started"); + emit q->error(lastError); + return; + } - m_active = SDPScanActive; + m_active = SDPScanActive; + qCDebug(QT_BT_ANDROID) + << "QBluetoothDeviceDiscoveryAgentPrivate::start() - Classic search successfully started."; + } else { + // LE search only requested + Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); - qCDebug(QT_BT_ANDROID) - << "QBluetoothDeviceDiscoveryAgentPrivate::start() - successfully executed."; + if (QtAndroidPrivate::androidSdkVersion() < 18) { + qCDebug(QT_BT_ANDROID) << "Skipping Bluetooth Low Energy device scan due to" + "insufficient Android version."; + m_active = NoScanActive; + lastError = QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod; + errorString = QBluetoothDeviceDiscoveryAgent::tr("Low Energy Discovery not supported"); + emit q->error(lastError); + return; + } + + startLowEnergyScan(); + } } void QBluetoothDeviceDiscoveryAgentPrivate::stop() @@ -200,7 +255,7 @@ void QBluetoothDeviceDiscoveryAgentPrivate::processSdpDiscoveryFinished() emit q->canceled(); } else if (pendingStart) { pendingStart = pendingCancel = false; - start(); + start(requestedMethods); } else { // check that it didn't finish due to turned off Bluetooth Device const int state = adapter.callMethod<jint>("getState"); @@ -212,6 +267,13 @@ void QBluetoothDeviceDiscoveryAgentPrivate::processSdpDiscoveryFinished() return; } + // no BTLE scan requested + if (!(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)) { + m_active = NoScanActive; + emit q->finished(); + return; + } + // start LE scan if supported if (QtAndroidPrivate::androidSdkVersion() < 18) { qCDebug(QT_BT_ANDROID) << "Skipping Bluetooth Low Energy device scan"; @@ -293,15 +355,21 @@ void QBluetoothDeviceDiscoveryAgentPrivate::startLowEnergyScan() return; } + // wait interval and sum up what was found if (!leScanTimeout) { leScanTimeout = new QTimer(this); leScanTimeout->setSingleShot(true); - leScanTimeout->setInterval(25000); connect(leScanTimeout, &QTimer::timeout, this, &QBluetoothDeviceDiscoveryAgentPrivate::stopLowEnergyScan); } - leScanTimeout->start(); + if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required + leScanTimeout->setInterval(lowEnergySearchTimeout); + leScanTimeout->start(); + } + + qCDebug(QT_BT_ANDROID) + << "QBluetoothDeviceDiscoveryAgentPrivate::start() - Low Energy search successfully started."; } void QBluetoothDeviceDiscoveryAgentPrivate::stopLowEnergyScan() diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_bluez.cpp b/src/bluetooth/qbluetoothdevicediscoveryagent_bluez.cpp index 0243d31f..5288eaf8 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_bluez.cpp +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_bluez.cpp @@ -68,10 +68,12 @@ QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate( adapterBluez5(0), discoveryTimer(0), useExtendedDiscovery(false), + lowEnergySearchTimeout(-1), // remains -1 on BlueZ 4 -> timeout not supported q_ptr(parent) { Q_Q(QBluetoothDeviceDiscoveryAgent); if (isBluez5()) { + lowEnergySearchTimeout = 20000; managerBluez5 = new OrgFreedesktopDBusObjectManagerInterface( QStringLiteral("org.bluez"), QStringLiteral("/"), @@ -115,8 +117,17 @@ bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const return (adapter || adapterBluez5); } -void QBluetoothDeviceDiscoveryAgentPrivate::start() +QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() { + return (ClassicMethod | LowEnergyMethod); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods /*methods*/) +{ + // Currently both BlueZ backends do not distinguish discovery methods. + // The DBus API's always return both device types. Therefore we ignore + // the passed in methods. + if (pendingCancel == true) { pendingStart = true; return; @@ -273,16 +284,18 @@ void QBluetoothDeviceDiscoveryAgentPrivate::startBluez5() } } - // wait 20s and sum up what was found + // wait interval and sum up what was found if (!discoveryTimer) { discoveryTimer = new QTimer(q); discoveryTimer->setSingleShot(true); - discoveryTimer->setInterval(20000); // 20s QObject::connect(discoveryTimer, SIGNAL(timeout()), q, SLOT(_q_discoveryFinished())); } - discoveryTimer->start(); + if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required + discoveryTimer->setInterval(lowEnergySearchTimeout); + discoveryTimer->start(); + } } void QBluetoothDeviceDiscoveryAgentPrivate::stop() @@ -436,7 +449,9 @@ void QBluetoothDeviceDiscoveryAgentPrivate::_q_propertyChanged(const QString &na pendingStart = false; pendingCancel = false; - start(); + // start parameter ignored since Bluez 4 doesn't distinguish them + start(QBluetoothDeviceDiscoveryAgent::ClassicMethod + | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); } else { // happens when agent is created while other agent called StopDiscovery() if (!adapter) @@ -514,7 +529,8 @@ void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryFinished() } else if (pendingStart) { pendingStart = false; pendingCancel = false; - start(); + start(QBluetoothDeviceDiscoveryAgent::ClassicMethod + | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); } else { emit q->finished(); } diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm b/src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm index 0e4b460f..3f4c6755 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm @@ -37,16 +37,17 @@ ** ****************************************************************************/ -#include "qbluetoothdevicediscoverytimer_osx_p.h" #include "qbluetoothdevicediscoveryagent.h" #include "osx/osxbtledeviceinquiry_p.h" #include "qbluetoothlocaldevice.h" #include "qbluetoothdeviceinfo.h" +#include "osx/osxbtnotifier_p.h" #include "osx/osxbtutility_p.h" #include "osx/uistrings_p.h" #include "qbluetoothuuid.h" #include <QtCore/qloggingcategory.h> +#include <QtCore/qobject.h> #include <QtCore/qglobal.h> #include <QtCore/qstring.h> #include <QtCore/qdebug.h> @@ -56,12 +57,24 @@ QT_BEGIN_NAMESPACE -using OSXBluetooth::ObjCScopedPointer; +namespace +{ + +void registerQDeviceDiscoveryMetaType() +{ + static bool initDone = false; + if (!initDone) { + qRegisterMetaType<QBluetoothDeviceInfo>(); + qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::Error>(); + initDone = true; + } +} + +}//namespace -class QBluetoothDeviceDiscoveryAgentPrivate +class QBluetoothDeviceDiscoveryAgentPrivate : public QObject { friend class QBluetoothDeviceDiscoveryAgent; - friend class OSXBluetooth::DDATimerHandler; public: QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &address, @@ -70,17 +83,16 @@ public: bool isActive() const; - void start(); + void start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods m); void stop(); private: - typedef QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) LEDeviceInquiryObjC; + using LEDeviceInquiryObjC = QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry); void LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error); void LEnotSupported(); void LEdeviceFound(const QBluetoothDeviceInfo &info); void LEinquiryFinished(); - void checkLETimeout(); void setError(QBluetoothDeviceDiscoveryAgent::Error, const QString &text = QString()); @@ -91,63 +103,30 @@ private: QBluetoothDeviceDiscoveryAgent::InquiryType inquiryType; - typedef ObjCScopedPointer<LEDeviceInquiryObjC> LEDeviceInquiry; + using LEDeviceInquiry = OSXBluetooth::ObjCScopedPointer<LEDeviceInquiryObjC>; LEDeviceInquiry inquiryLE; - typedef QList<QBluetoothDeviceInfo> DevicesList; + using DevicesList = QList<QBluetoothDeviceInfo>; DevicesList discoveredDevices; bool startPending; bool stopPending; - QScopedPointer<OSXBluetooth::DDATimerHandler> timer; + int lowEnergySearchTimeout; }; -namespace OSXBluetooth { - -DDATimerHandler::DDATimerHandler(QBluetoothDeviceDiscoveryAgentPrivate *d) - : owner(d) -{ - Q_ASSERT_X(owner, Q_FUNC_INFO, "invalid pointer"); - - timer.setSingleShot(false); - connect(&timer, &QTimer::timeout, this, &DDATimerHandler::onTimer); -} - -void DDATimerHandler::start(int msec) -{ - Q_ASSERT_X(msec > 0, Q_FUNC_INFO, "invalid time interval"); - if (timer.isActive()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "timer is active"; - return; - } - - timer.start(msec); -} - -void DDATimerHandler::stop() -{ - timer.stop(); -} - -void DDATimerHandler::onTimer() -{ - Q_ASSERT(owner); - owner->checkLETimeout(); -} - -} - QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &adapter, QBluetoothDeviceDiscoveryAgent *q) : q_ptr(q), lastError(QBluetoothDeviceDiscoveryAgent::NoError), inquiryType(QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry), startPending(false), - stopPending(false) + stopPending(false), + lowEnergySearchTimeout(OSXBluetooth::defaultLEScanTimeoutMS) { Q_UNUSED(adapter); + registerQDeviceDiscoveryMetaType(); Q_ASSERT_X(q != Q_NULLPTR, Q_FUNC_INFO, "invalid q_ptr (null)"); } @@ -158,7 +137,7 @@ QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() if (dispatch_queue_t leQueue = OSXBluetooth::qt_LE_queue()) { // Local variable to be retained ... LEDeviceInquiryObjC *inq = inquiryLE.data(); - dispatch_async(leQueue, ^{ + dispatch_sync(leQueue, ^{ [inq stop]; }); } @@ -175,7 +154,7 @@ bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const return inquiryLE; } -void QBluetoothDeviceDiscoveryAgentPrivate::start() +void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods /*methods*/) { Q_ASSERT_X(!isActive(), Q_FUNC_INFO, "called on active device discovery agent"); @@ -186,26 +165,37 @@ void QBluetoothDeviceDiscoveryAgentPrivate::start() using namespace OSXBluetooth; - inquiryLE.reset([[LEDeviceInquiryObjC alloc] init]); + QScopedPointer<LECBManagerNotifier> notifier(new LECBManagerNotifier); + // Connections: + using ErrMemFunPtr = void (LECBManagerNotifier::*)(QBluetoothDeviceDiscoveryAgent::Error); + notifier->connect(notifier.data(), ErrMemFunPtr(&LECBManagerNotifier::CBManagerError), + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError); + notifier->connect(notifier.data(), &LECBManagerNotifier::LEnotSupported, + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported); + notifier->connect(notifier.data(), &LECBManagerNotifier::discoveryFinished, + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished); + notifier->connect(notifier.data(), &LECBManagerNotifier::deviceDiscovered, + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceFound); + + inquiryLE.reset([[LEDeviceInquiryObjC alloc] initWithNotifier:notifier.data()]); + if (inquiryLE) + notifier.take(); // Whatever happens next, inquiryLE is already the owner ... + dispatch_queue_t leQueue(qt_LE_queue()); if (!leQueue || !inquiryLE) { setError(QBluetoothDeviceDiscoveryAgent::UnknownError, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED)); emit q_ptr->error(lastError); + return; } discoveredDevices.clear(); setError(QBluetoothDeviceDiscoveryAgent::NoError); - // CoreBluetooth does not have a timeout. We start a timer here - // and check if scan really started and if yes if we have a timeout. - timer.reset(new OSXBluetooth::DDATimerHandler(this)); - timer->start(2000); - // Create a local variable - to have a strong referece in a block. LEDeviceInquiryObjC *inq = inquiryLE.data(); dispatch_async(leQueue, ^{ - [inq start]; + [inq startWithTimeout:lowEnergySearchTimeout]; }); } @@ -223,7 +213,7 @@ void QBluetoothDeviceDiscoveryAgentPrivate::stop() // Create a local variable - to have a strong referece in a block. LEDeviceInquiryObjC *inq = inquiryLE.data(); - dispatch_async(leQueue, ^{ + dispatch_sync(leQueue, ^{ [inq stop]; }); // We consider LE scan to be stopped immediately and @@ -241,7 +231,6 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDisco Q_FUNC_INFO, "unexpected error"); inquiryLE.reset(); - timer->stop(); startPending = false; stopPending = false; @@ -252,7 +241,6 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDisco void QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported() { inquiryLE.reset(); - timer->stop(); startPending = false; stopPending = false; @@ -281,7 +269,6 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceFound(const QBluetoothDevice void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() { inquiryLE.reset(); - timer->stop(); if (stopPending && !startPending) { stopPending = false; @@ -289,47 +276,14 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() } else if (startPending) { startPending = false; stopPending = false; - start(); + // always the same method for start() on iOS + // classic search not supported + start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); } else { emit q_ptr->finished(); } } -void QBluetoothDeviceDiscoveryAgentPrivate::checkLETimeout() -{ - Q_ASSERT_X(inquiryLE, Q_FUNC_INFO, "LE device inquiry is nil"); - - using namespace OSXBluetooth; - - const LEInquiryState state([inquiryLE inquiryState]); - if (state == InquiryStarting || state == InquiryActive) - return; // Wait ... - - if (state == ErrorPoweredOff) - return LEinquiryError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); - - if (state == ErrorLENotSupported) - return LEnotSupported(); - - if (state == InquiryFinished) { - // Process found devices if any ... - const QList<QBluetoothDeviceInfo> leDevices([inquiryLE discoveredDevices]); - foreach (const QBluetoothDeviceInfo &info, leDevices) { - // We were cancelled on a previous device discovered signal ... - if (!inquiryLE) - break; - LEdeviceFound(info); - } - - if (inquiryLE) - LEinquiryFinished(); - return; - } - - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected inquiry state in LE timeout"; - // Actually, this deserves an assert :) -} - void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAgent::Error error, const QString &text) { @@ -373,7 +327,7 @@ QBluetoothDeviceDiscoveryAgent::QBluetoothDeviceDiscoveryAgent( d_ptr(new QBluetoothDeviceDiscoveryAgentPrivate(deviceAdapter, this)) { if (!deviceAdapter.isNull()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "local device address is " + qCWarning(QT_BT_OSX) << "local device address is " "not available, provided address is ignored"; d_ptr->setError(InvalidBluetoothAdapterError); } @@ -399,14 +353,39 @@ QList<QBluetoothDeviceInfo> QBluetoothDeviceDiscoveryAgent::discoveredDevices() return d_ptr->discoveredDevices; } +QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() +{ + return LowEnergyMethod; +} + void QBluetoothDeviceDiscoveryAgent::start() { if (d_ptr->lastError != InvalidBluetoothAdapterError) { if (!isActive()) - d_ptr->start(); + d_ptr->start(supportedDiscoveryMethods()); else - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "already started"; + qCDebug(QT_BT_OSX) << "already started"; + } +} + +void QBluetoothDeviceDiscoveryAgent::start(DiscoveryMethods methods) +{ + if (methods == NoMethod) + return; + + DiscoveryMethods supported = + QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods(); + + if (!((supported & methods) == methods)) { + d_ptr->lastError = UnsupportedDiscoveryMethod; + d_ptr->errorString = QBluetoothDeviceDiscoveryAgent::tr("One or more device discovery methods " + "are not supported on this platform"); + emit error(d_ptr->lastError); + return; } + + if (!isActive() && d_ptr->lastError != InvalidBluetoothAdapterError) + d_ptr->start(methods); } void QBluetoothDeviceDiscoveryAgent::stop() @@ -430,4 +409,21 @@ QString QBluetoothDeviceDiscoveryAgent::errorString() const return d_ptr->errorString; } +int QBluetoothDeviceDiscoveryAgent::lowEnergyDiscoveryTimeout() const +{ + return d_ptr->lowEnergySearchTimeout; +} + +void QBluetoothDeviceDiscoveryAgent::setLowEnergyDiscoveryTimeout(int timeout) +{ + // cannot deliberately turn it off + if (timeout < 0) { + qCDebug(QT_BT_OSX) << "The Bluetooth Low Energy device discovery timeout cannot be negative."; + return; + } + + d_ptr->lowEnergySearchTimeout = timeout; + return; +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm b/src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm index 63eab2b9..b308f7cc 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm @@ -37,22 +37,23 @@ ** ****************************************************************************/ -#include "qbluetoothdevicediscoverytimer_osx_p.h" #include "qbluetoothdevicediscoveryagent.h" #include "osx/osxbtledeviceinquiry_p.h" #include "osx/osxbtdeviceinquiry_p.h" #include "qbluetoothlocaldevice.h" #include "osx/osxbtsdpinquiry_p.h" #include "qbluetoothdeviceinfo.h" +#include "osx/osxbtnotifier_p.h" #include "osx/osxbtutility_p.h" #include "osx/osxbluetooth_p.h" #include "osx/uistrings_p.h" #include "qbluetoothhostinfo.h" +#include "qbluetoothaddress.h" +#include "osx/uistrings_p.h" #include "qbluetoothuuid.h" #include <QtCore/qloggingcategory.h> #include <QtCore/qscopedpointer.h> -#include <QtCore/qdatetime.h> #include <QtCore/qglobal.h> #include <QtCore/qstring.h> #include <QtCore/qdebug.h> @@ -62,23 +63,40 @@ QT_BEGIN_NAMESPACE -using OSXBluetooth::ObjCScopedPointer; +namespace +{ + +void registerQDeviceDiscoveryMetaType() +{ + static bool initDone = false; + if (!initDone) { + qRegisterMetaType<QBluetoothDeviceInfo>(); + qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::Error>(); + initDone = true; + } +} + +}//namespace -class QBluetoothDeviceDiscoveryAgentPrivate : public OSXBluetooth::DeviceInquiryDelegate +class QBluetoothDeviceDiscoveryAgentPrivate : public QObject, + public OSXBluetooth::DeviceInquiryDelegate { friend class QBluetoothDeviceDiscoveryAgent; - friend class OSXBluetooth::DDATimerHandler; public: - typedef QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) LEDeviceInquiryObjC; + template<class T> + using ObjCScopedPointer = OSXBluetooth::ObjCScopedPointer<T>; + using LEDeviceInquiryObjC = QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry); QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress & address, QBluetoothDeviceDiscoveryAgent *q); - virtual ~QBluetoothDeviceDiscoveryAgentPrivate(); + + ~QBluetoothDeviceDiscoveryAgentPrivate() Q_DECL_OVERRIDE; bool isValid() const; bool isActive() const; - void start(); + void start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods); + void startClassic(); void startLE(); void stop(); @@ -94,7 +112,6 @@ private: void error(IOBluetoothDeviceInquiry *inq, IOReturn error) Q_DECL_OVERRIDE; void deviceFound(IOBluetoothDeviceInquiry *inq, IOBluetoothDevice *device) Q_DECL_OVERRIDE; - // void LEinquiryFinished(); void LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error); void LEnotSupported(); @@ -104,9 +121,8 @@ private: void deviceFound(const QBluetoothDeviceInfo &newDeviceInfo); void setError(IOReturn error, const QString &text = QString()); - void setError(QBluetoothDeviceDiscoveryAgent::Error, const QString &text = QString()); - - void checkLETimeout(); + void setError(QBluetoothDeviceDiscoveryAgent::Error, + const QString &text = QString()); QBluetoothDeviceDiscoveryAgent *q_ptr; AgentState agentState; @@ -121,56 +137,22 @@ private: QBluetoothDeviceDiscoveryAgent::InquiryType inquiryType; - typedef ObjCScopedPointer<DeviceInquiryObjC> DeviceInquiry; + using DeviceInquiry = ObjCScopedPointer<DeviceInquiryObjC>; DeviceInquiry inquiry; - typedef ObjCScopedPointer<LEDeviceInquiryObjC> LEDeviceInquiry; + using LEDeviceInquiry = ObjCScopedPointer<LEDeviceInquiryObjC>; LEDeviceInquiry inquiryLE; - typedef ObjCScopedPointer<IOBluetoothHostController> HostController; + using HostController = ObjCScopedPointer<IOBluetoothHostController>; HostController hostController; - typedef QList<QBluetoothDeviceInfo> DevicesList; + using DevicesList = QList<QBluetoothDeviceInfo>; DevicesList discoveredDevices; - QScopedPointer<OSXBluetooth::DDATimerHandler> timer; + int lowEnergySearchTimeout; + QBluetoothDeviceDiscoveryAgent::DiscoveryMethods requestedMethods; }; -namespace OSXBluetooth { - -DDATimerHandler::DDATimerHandler(QBluetoothDeviceDiscoveryAgentPrivate *d) - : owner(d) -{ - Q_ASSERT_X(owner, Q_FUNC_INFO, "invalid pointer"); - - timer.setSingleShot(false); - connect(&timer, &QTimer::timeout, this, &DDATimerHandler::onTimer); -} - -void DDATimerHandler::start(int msec) -{ - Q_ASSERT_X(msec > 0, Q_FUNC_INFO, "invalid time interval"); - if (timer.isActive()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "timer is active"; - return; - } - - timer.start(msec); -} - -void DDATimerHandler::stop() -{ - timer.stop(); -} - -void DDATimerHandler::onTimer() -{ - Q_ASSERT(owner); - owner->checkLETimeout(); -} - -} - QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &adapter, QBluetoothDeviceDiscoveryAgent *q) : q_ptr(q), @@ -179,26 +161,22 @@ QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(con startPending(false), stopPending(false), lastError(QBluetoothDeviceDiscoveryAgent::NoError), - inquiryType(QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry) + inquiryType(QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry), + lowEnergySearchTimeout(OSXBluetooth::defaultLEScanTimeoutMS), + requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod + | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) { + registerQDeviceDiscoveryMetaType(); + Q_ASSERT_X(q != Q_NULLPTR, Q_FUNC_INFO, "invalid q_ptr (null)"); HostController controller([[IOBluetoothHostController defaultController] retain]); if (!controller || [controller powerState] != kBluetoothHCIPowerStateON) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "no default host " - "controller or adapter is off"; - return; - } - - DeviceInquiry newInquiry([[DeviceInquiryObjC alloc]initWithDelegate:this]); - if (!newInquiry) { // Obj-C's way of "reporting errors": - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "initialize an inquiry"; + qCCritical(QT_BT_OSX) << "no default host controller or adapter is off"; return; } hostController.reset(controller.take()); - inquiry.reset(newInquiry.take()); } QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() @@ -208,7 +186,7 @@ QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() if (dispatch_queue_t leQueue = OSXBluetooth::qt_LE_queue()) { // Local variable to be retained ... LEDeviceInquiryObjC *inq = inquiryLE.data(); - dispatch_async(leQueue, ^{ + dispatch_sync(leQueue, ^{ [inq stop]; }); } @@ -217,12 +195,7 @@ QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() bool QBluetoothDeviceDiscoveryAgentPrivate::isValid() const { - // isValid() - Qt does not use exceptions, but the ctor - // can fail to initialize some important data-members - // (and the error is probably not even related to Bluetooth at all) - // - say, allocation error - this is what meant here by valid/invalid. - return hostController && [hostController powerState] == kBluetoothHCIPowerStateON - && inquiry; + return hostController && [hostController powerState] == kBluetoothHCIPowerStateON; } bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const @@ -236,23 +209,56 @@ bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const return agentState != NonActive; } -void QBluetoothDeviceDiscoveryAgentPrivate::start() +void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods) { - Q_ASSERT_X(isValid(), Q_FUNC_INFO, "called on invalid device discovery agent"); - Q_ASSERT_X(!isActive(), Q_FUNC_INFO, "called on active device discovery agent"); - Q_ASSERT_X(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError, - Q_FUNC_INFO, "called with an invalid Bluetooth adapter"); + Q_ASSERT(isValid()); + Q_ASSERT(!isActive()); + Q_ASSERT(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError); + Q_ASSERT(methods & (QBluetoothDeviceDiscoveryAgent::ClassicMethod + | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)); + + requestedMethods = methods; if (stopPending) { startPending = true; return; } - agentState = ClassicScan; + // This function (re)starts the scan(s) from the scratch; + // starting from Classic if it's in 'methods' (or LE scan if not). + agentState = NonActive; discoveredDevices.clear(); setError(QBluetoothDeviceDiscoveryAgent::NoError); + if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) + return startClassic(); + + startLE(); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::startClassic() +{ + Q_ASSERT(isValid()); + Q_ASSERT(!isActive()); + Q_ASSERT(lastError == QBluetoothDeviceDiscoveryAgent::NoError); + Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod); + Q_ASSERT(agentState == NonActive); + + if (!inquiry) { + // The first Classic scan for this DDA. + inquiry.reset([[DeviceInquiryObjC alloc]initWithDelegate:this]); + if (!inquiry) { + qCCritical(QT_BT_OSX) << "failed to initialize an Classic device inquiry"; + setError(QBluetoothDeviceDiscoveryAgent::UnknownError, + QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED)); + emit q_ptr->error(lastError); + return; + } + } + + agentState = ClassicScan; + const IOReturn res = [inquiry start]; if (res != kIOReturnSuccess) { setError(res, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED)); @@ -263,13 +269,29 @@ void QBluetoothDeviceDiscoveryAgentPrivate::start() void QBluetoothDeviceDiscoveryAgentPrivate::startLE() { - Q_ASSERT_X(isValid(), Q_FUNC_INFO, "called on invalid device discovery agent"); - Q_ASSERT_X(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError, - Q_FUNC_INFO, "called with an invalid Bluetooth adapter"); + Q_ASSERT(isValid()); + Q_ASSERT(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError); + Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); using namespace OSXBluetooth; - inquiryLE.reset([[LEDeviceInquiryObjC alloc] init]); + QScopedPointer<LECBManagerNotifier> notifier(new LECBManagerNotifier); + // Connections: + using ErrMemFunPtr = void (LECBManagerNotifier::*)(QBluetoothDeviceDiscoveryAgent::Error); + notifier->connect(notifier.data(), ErrMemFunPtr(&LECBManagerNotifier::CBManagerError), + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError); + notifier->connect(notifier.data(), &LECBManagerNotifier::LEnotSupported, + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported); + notifier->connect(notifier.data(), &LECBManagerNotifier::discoveryFinished, + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished); + using DeviceMemFunPtr = void (QBluetoothDeviceDiscoveryAgentPrivate::*)(const QBluetoothDeviceInfo &); + notifier->connect(notifier.data(), &LECBManagerNotifier::deviceDiscovered, + this, DeviceMemFunPtr(&QBluetoothDeviceDiscoveryAgentPrivate::deviceFound)); + + // Check queue and create scanner: + inquiryLE.reset([[LEDeviceInquiryObjC alloc] initWithNotifier:notifier.data()]); + if (inquiryLE) + notifier.take(); // Whatever happens next, inquiryLE is already the owner ... dispatch_queue_t leQueue(qt_LE_queue()); if (!leQueue || !inquiryLE) { @@ -277,18 +299,15 @@ void QBluetoothDeviceDiscoveryAgentPrivate::startLE() QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED_LE)); agentState = NonActive; emit q_ptr->error(lastError); + return; } + // Now start in on LE queue: agentState = LEScan; - // CoreBluetooth does not have a timeout. We start a timer here - // and check if scan is active/finished/finished with error(s). - timer.reset(new OSXBluetooth::DDATimerHandler(this)); - timer->start(2000); - // We need the local variable so that it's retained ... LEDeviceInquiryObjC *inq = inquiryLE.data(); dispatch_async(leQueue, ^{ - [inq start]; + [inq startWithTimeout:lowEnergySearchTimeout]; }); } @@ -310,7 +329,7 @@ void QBluetoothDeviceDiscoveryAgentPrivate::stop() if (agentState == ClassicScan) { const IOReturn res = [inquiry stop]; if (res != kIOReturnSuccess) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to stop"; + qCWarning(QT_BT_OSX) << "failed to stop"; startPending = prevStart; stopPending = false; setError(res, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STOPPED)); @@ -321,7 +340,7 @@ void QBluetoothDeviceDiscoveryAgentPrivate::stop() Q_ASSERT(leQueue); // We need the local variable so that it's retained ... LEDeviceInquiryObjC *inq = inquiryLE.data(); - dispatch_async(leQueue, ^{ + dispatch_sync(leQueue, ^{ [inq stop]; }); // We consider LE scan to be stopped immediately and @@ -346,13 +365,17 @@ void QBluetoothDeviceDiscoveryAgentPrivate::inquiryFinished(IOBluetoothDeviceInq } else if (startPending) { startPending = false; stopPending = false; - start(); + start(requestedMethods); } else { // We can be here _only_ if a classic scan - // finished in a normal way (not cancelled). + // finished in a normal way (not cancelled) + // and requestedMethods includes LowEnergyMethod. // startLE() will take care of old devices // not supporting Bluetooth 4.0. - startLE(); + if (requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) + startLE(); + else + emit q_ptr->finished(); } } @@ -384,7 +407,7 @@ void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(IOBluetoothDeviceInquiry // Let's collect some info about this device: const QBluetoothAddress deviceAddress(OSXBluetooth::qt_address([device getAddress])); if (deviceAddress.isNull()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid Bluetooth address"; + qCWarning(QT_BT_OSX) << "invalid Bluetooth address"; return; } @@ -414,8 +437,7 @@ void QBluetoothDeviceDiscoveryAgentPrivate::setError(IOReturn error, const QStri setError(QBluetoothDeviceDiscoveryAgent::UnknownError, text); } -void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAgent::Error error, - const QString &text) +void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAgent::Error error, const QString &text) { lastError = error; @@ -442,53 +464,18 @@ void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAg } if (lastError != QBluetoothDeviceDiscoveryAgent::NoError) - qCDebug(QT_BT_OSX) << "error set: "<<errorString; -} - -void QBluetoothDeviceDiscoveryAgentPrivate::checkLETimeout() -{ - Q_ASSERT_X(agentState == LEScan, Q_FUNC_INFO, "invalid agent state"); - Q_ASSERT_X(inquiryLE, Q_FUNC_INFO, "LE device inquiry is nil"); - - using namespace OSXBluetooth; - - const LEInquiryState state([inquiryLE inquiryState]); - if (state == InquiryStarting || state == InquiryActive) - return; // Wait ... - - if (state == ErrorPoweredOff) - return LEinquiryError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); - - if (state == ErrorLENotSupported) - return LEnotSupported(); - - if (state == InquiryFinished) { - // Process found devices if any ... - const QList<QBluetoothDeviceInfo> leDevices([inquiryLE discoveredDevices]); - foreach (const QBluetoothDeviceInfo &info, leDevices) { - // We were cancelled on a previous device discovered signal ... - if (agentState != LEScan) - break; - deviceFound(info); - } - - if (agentState == LEScan) - LEinquiryFinished(); - return; - } - - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected inquiry state in LE timeout"; - // Actually, this deserves an assert :) + qCDebug(QT_BT_OSX) << "error set:"<<errorString; } void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error) { - // At the moment the only error reported can be 'powered off' error, it happens - // after the LE scan started (so we have LE support and this is a real PoweredOffError). - Q_ASSERT(error == QBluetoothDeviceDiscoveryAgent::PoweredOffError); + Q_ASSERT(error == QBluetoothDeviceDiscoveryAgent::PoweredOffError + || error == QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); - timer->stop(); inquiryLE.reset(); + + startPending = false; + stopPending = false; agentState = NonActive; setError(error); emit q_ptr->error(lastError); @@ -496,9 +483,18 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDisco void QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported() { - // Not supported is not an error (we still have 'Classic'). qCDebug(QT_BT_OSX) << "no Bluetooth LE support"; - LEinquiryFinished(); + + if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) { + // Having both Classic | LE means this is not an error. + LEinquiryFinished(); + } else { + // In the past this was never an error, that's why we have + // LEnotSupported as a special method. But now, since + // we can have separate Classic/LE scans, we have to report it + // as UnsupportedDiscoveryMethod. + LEinquiryError(QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); + } } void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() @@ -506,7 +502,6 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() // The same logic as in inquiryFinished, but does not start LE scan. agentState = NonActive; inquiryLE.reset(); - timer->stop(); if (stopPending && !startPending) { stopPending = false; @@ -514,7 +509,7 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() } else if (startPending) { startPending = false; stopPending = false; - start(); //Start from a classic scan again. + start(requestedMethods); //Start again. } else { emit q_ptr->finished(); } @@ -574,7 +569,7 @@ QBluetoothDeviceDiscoveryAgent::InquiryType QBluetoothDeviceDiscoveryAgent::inqu return d_ptr->inquiryType; } -void QBluetoothDeviceDiscoveryAgent::setInquiryType(QBluetoothDeviceDiscoveryAgent::InquiryType type) +void QBluetoothDeviceDiscoveryAgent::setInquiryType(InquiryType type) { d_ptr->inquiryType = type; } @@ -584,12 +579,33 @@ QList<QBluetoothDeviceInfo> QBluetoothDeviceDiscoveryAgent::discoveredDevices() return d_ptr->discoveredDevices; } +QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() +{ + return ClassicMethod | LowEnergyMethod; +} + void QBluetoothDeviceDiscoveryAgent::start() { + start(supportedDiscoveryMethods()); +} + +void QBluetoothDeviceDiscoveryAgent::start(DiscoveryMethods methods) +{ + if (methods == NoMethod) + return; + + if ((supportedDiscoveryMethods() & methods) != methods) { + d_ptr->lastError = UnsupportedDiscoveryMethod; + d_ptr->errorString = tr("One or more device discovery methods " + "are not supported on this platform"); + emit error(d_ptr->lastError); + return; + } + if (d_ptr->lastError != InvalidBluetoothAdapterError) { if (d_ptr->isValid()) { if (!isActive()) - d_ptr->start(); + d_ptr->start(methods); } else { // We previously failed to initialize d_ptr correctly: // either some memory allocation problem or @@ -626,4 +642,21 @@ QString QBluetoothDeviceDiscoveryAgent::errorString() const return d_ptr->errorString; } +void QBluetoothDeviceDiscoveryAgent::setLowEnergyDiscoveryTimeout(int timeout) +{ + // cannot deliberately turn it off + if (timeout < 0) { + qCDebug(QT_BT_OSX) << "The Bluetooth Low Energy device discovery timeout cannot be negative."; + return; + } + + d_ptr->lowEnergySearchTimeout = timeout; + return; +} + +int QBluetoothDeviceDiscoveryAgent::lowEnergyDiscoveryTimeout() const +{ + return d_ptr->lowEnergySearchTimeout; +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_p.cpp b/src/bluetooth/qbluetoothdevicediscoveryagent_p.cpp index 09076d6d..e3646db9 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_p.cpp +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_p.cpp @@ -53,6 +53,7 @@ QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate( QBluetoothDeviceDiscoveryAgent *parent) : inquiryType(QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry), lastError(QBluetoothDeviceDiscoveryAgent::NoError), + lowEnergySearchTimeout(-1), q_ptr(parent) { Q_UNUSED(deviceAdapter); @@ -68,7 +69,12 @@ bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const return false; } -void QBluetoothDeviceDiscoveryAgentPrivate::start() +QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() +{ + return QBluetoothDeviceDiscoveryAgent::NoMethod; +} + +void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods) { Q_Q(QBluetoothDeviceDiscoveryAgent); lastError = QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError; diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_p.h b/src/bluetooth/qbluetoothdevicediscoveryagent_p.h index f49ff8b7..de8006be 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_p.h +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_p.h @@ -78,10 +78,14 @@ class QDBusVariant; QT_END_NAMESPACE #endif +#ifdef QT_WINRT_BLUETOOTH +class QWinRTBluetoothDeviceDiscoveryWorker; +#endif + QT_BEGIN_NAMESPACE class QBluetoothDeviceDiscoveryAgentPrivate -#if defined(QT_ANDROID_BLUETOOTH) +#if defined(QT_ANDROID_BLUETOOTH) || defined(QT_WINRT_BLUETOOTH) : public QObject { Q_OBJECT @@ -95,7 +99,7 @@ public: QBluetoothDeviceDiscoveryAgent *parent); ~QBluetoothDeviceDiscoveryAgentPrivate(); - void start(); + void start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods); void stop(); bool isActive() const; @@ -155,6 +159,20 @@ private: QTimer extendedDiscoveryTimer; #endif +#ifdef QT_WINRT_BLUETOOTH +private slots: + void registerDevice(const QBluetoothDeviceInfo &info); + void onScanFinished(); + void onScanCanceled(); + +private: + void disconnectAndClearWorker(); + QPointer<QWinRTBluetoothDeviceDiscoveryWorker> worker; + QTimer *leScanTimer; +#endif + + int lowEnergySearchTimeout; + QBluetoothDeviceDiscoveryAgent::DiscoveryMethods requestedMethods; QBluetoothDeviceDiscoveryAgent *q_ptr; }; diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp b/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp new file mode 100644 index 00000000..7813bfdc --- /dev/null +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp @@ -0,0 +1,647 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://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 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qbluetoothdevicediscoveryagent.h" +#include "qbluetoothdevicediscoveryagent_p.h" +#include "qbluetoothaddress.h" +#include "qbluetoothuuid.h" +#include "qfunctions_winrt.h" + +#include <QtCore/QLoggingCategory> +#include <QtCore/private/qeventdispatcher_winrt_p.h> + +#include <wrl.h> +#include <windows.devices.enumeration.h> +#include <windows.devices.bluetooth.h> +#include <windows.foundation.collections.h> +#include <windows.storage.streams.h> + +#ifndef Q_OS_WINPHONE +#include <windows.devices.bluetooth.advertisement.h> + +using namespace ABI::Windows::Devices::Bluetooth::Advertisement; +#endif // !Q_OS_WINPHONE + +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Foundation::Collections; +using namespace ABI::Windows::Devices; +using namespace ABI::Windows::Devices::Bluetooth; +using namespace ABI::Windows::Devices::Enumeration; + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINRT) + +#define WARN_AND_RETURN_IF_FAILED(msg, ret) \ + if (FAILED(hr)) { \ + qCWarning(QT_BT_WINRT) << msg; \ + ret; \ + } + +class QWinRTBluetoothDeviceDiscoveryWorker : public QObject +{ + Q_OBJECT +public: + explicit QWinRTBluetoothDeviceDiscoveryWorker(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods); + ~QWinRTBluetoothDeviceDiscoveryWorker(); + void start(); + void stop(); + +private: + void startDeviceDiscovery(QBluetoothDeviceDiscoveryAgent::DiscoveryMethod mode); + void onDeviceDiscoveryFinished(IAsyncOperation<DeviceInformationCollection *> *op, + QBluetoothDeviceDiscoveryAgent::DiscoveryMethod mode); + void gatherDeviceInformation(IDeviceInformation *deviceInfo, + QBluetoothDeviceDiscoveryAgent::DiscoveryMethod mode); + void gatherMultipleDeviceInformation(IVectorView<DeviceInformation *> *devices, + QBluetoothDeviceDiscoveryAgent::DiscoveryMethod mode); + void setupLEDeviceWatcher(); + void classicBluetoothInfoFromDeviceIdAsync(HSTRING deviceId); + void leBluetoothInfoFromDeviceIdAsync(HSTRING deviceId); + void leBluetoothInfoFromAddressAsync(quint64 address); + HRESULT onPairedClassicBluetoothDeviceFoundAsync(IAsyncOperation<BluetoothDevice *> *op, AsyncStatus status ); + HRESULT onPairedBluetoothLEDeviceFoundAsync(IAsyncOperation<BluetoothLEDevice *> *op, AsyncStatus status); + HRESULT onBluetoothLEDeviceFoundAsync(IAsyncOperation<BluetoothLEDevice *> *op, AsyncStatus status); + enum PairingCheck { + CheckForPairing, + OmitPairingCheck + }; + HRESULT onBluetoothLEDeviceFound(ComPtr<IBluetoothLEDevice> device, PairingCheck pairingCheck = CheckForPairing); + +public slots: + void handleLeTimeout(); + +Q_SIGNALS: + void deviceFound(const QBluetoothDeviceInfo &info); + void scanFinished(); + void scanCanceled(); + +public: + quint8 requestedModes; + +private: +#ifndef Q_OS_WINPHONE + ComPtr<IBluetoothLEAdvertisementWatcher> m_leWatcher; + EventRegistrationToken m_leDeviceAddedToken; + QVector<quint64> m_foundLEDevices; +#endif // !Q_OS_WINPHONE + int m_pendingPairedDevices; + + ComPtr<IBluetoothDeviceStatics> m_deviceStatics; + ComPtr<IBluetoothLEDeviceStatics> m_leDeviceStatics; +}; + +QWinRTBluetoothDeviceDiscoveryWorker::QWinRTBluetoothDeviceDiscoveryWorker(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods) + : requestedModes(methods) + , m_pendingPairedDevices(0) +{ + qRegisterMetaType<QBluetoothDeviceInfo>(); + + HRESULT hr = GetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Devices_Bluetooth_BluetoothDevice).Get(), &m_deviceStatics); + Q_ASSERT_SUCCEEDED(hr); + hr = GetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Devices_Bluetooth_BluetoothLEDevice).Get(), &m_leDeviceStatics); + Q_ASSERT_SUCCEEDED(hr); +} + +QWinRTBluetoothDeviceDiscoveryWorker::~QWinRTBluetoothDeviceDiscoveryWorker() +{ + stop(); +} + +void QWinRTBluetoothDeviceDiscoveryWorker::start() +{ + QEventDispatcherWinRT::runOnXamlThread([this]() { + if (requestedModes & QBluetoothDeviceDiscoveryAgent::ClassicMethod) + startDeviceDiscovery(QBluetoothDeviceDiscoveryAgent::ClassicMethod); + + if (requestedModes & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) { + startDeviceDiscovery(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + setupLEDeviceWatcher(); + } + return S_OK; + }); + + qCDebug(QT_BT_WINRT) << "Worker started"; +} + +void QWinRTBluetoothDeviceDiscoveryWorker::stop() +{ +#ifndef Q_OS_WINPHONE + if (m_leWatcher) { + HRESULT hr = m_leWatcher->Stop(); + Q_ASSERT_SUCCEEDED(hr); + if (m_leDeviceAddedToken.value) { + hr = m_leWatcher->remove_Received(m_leDeviceAddedToken); + Q_ASSERT_SUCCEEDED(hr); + } + } +#endif // !Q_OS_WINPHONE +} + +void QWinRTBluetoothDeviceDiscoveryWorker::startDeviceDiscovery(QBluetoothDeviceDiscoveryAgent::DiscoveryMethod mode) +{ + HString deviceSelector; + ComPtr<IDeviceInformationStatics> deviceInformationStatics; + HRESULT hr = GetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Devices_Enumeration_DeviceInformation).Get(), &deviceInformationStatics); + WARN_AND_RETURN_IF_FAILED("Could not obtain device information statics", return); + if (mode == QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) + m_leDeviceStatics->GetDeviceSelector(deviceSelector.GetAddressOf()); + else + m_deviceStatics->GetDeviceSelector(deviceSelector.GetAddressOf()); + ComPtr<IAsyncOperation<DeviceInformationCollection *>> op; + hr = deviceInformationStatics->FindAllAsyncAqsFilter(deviceSelector.Get(), &op); + WARN_AND_RETURN_IF_FAILED("Could not start bluetooth device discovery operation", return); + hr = op->put_Completed( + Callback<IAsyncOperationCompletedHandler<DeviceInformationCollection *>>([this, mode](IAsyncOperation<DeviceInformationCollection *> *op, AsyncStatus) { + onDeviceDiscoveryFinished(op, mode); + return S_OK; + }).Get()); + WARN_AND_RETURN_IF_FAILED("Could not add callback to bluetooth device discovery operation", return); +} + +void QWinRTBluetoothDeviceDiscoveryWorker::onDeviceDiscoveryFinished(IAsyncOperation<DeviceInformationCollection *> *op, QBluetoothDeviceDiscoveryAgent::DiscoveryMethod mode) +{ + qCDebug(QT_BT_WINRT) << (mode == QBluetoothDeviceDiscoveryAgent::ClassicMethod ? "BT" : "BTLE") + << " scan completed"; + ComPtr<IVectorView<DeviceInformation *>> devices; + HRESULT hr; + hr = op->GetResults(&devices); + Q_ASSERT_SUCCEEDED(hr); + gatherMultipleDeviceInformation(devices.Get(), mode); +} + +void QWinRTBluetoothDeviceDiscoveryWorker::gatherDeviceInformation(IDeviceInformation *deviceInfo, QBluetoothDeviceDiscoveryAgent::DiscoveryMethod mode) +{ + HString deviceId; + HRESULT hr; + hr = deviceInfo->get_Id(deviceId.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + if (mode == QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) + leBluetoothInfoFromDeviceIdAsync(deviceId.Get()); + else + classicBluetoothInfoFromDeviceIdAsync(deviceId.Get()); +} + +void QWinRTBluetoothDeviceDiscoveryWorker::gatherMultipleDeviceInformation(IVectorView<DeviceInformation *> *devices, QBluetoothDeviceDiscoveryAgent::DiscoveryMethod mode) +{ + quint32 deviceCount; + HRESULT hr = devices->get_Size(&deviceCount); + Q_ASSERT_SUCCEEDED(hr); + m_pendingPairedDevices += deviceCount; + for (quint32 i = 0; i < deviceCount; ++i) { + ComPtr<IDeviceInformation> device; + hr = devices->GetAt(i, &device); + Q_ASSERT_SUCCEEDED(hr); + gatherDeviceInformation(device.Get(), mode); + } +} + +void QWinRTBluetoothDeviceDiscoveryWorker::setupLEDeviceWatcher() +{ +#ifndef Q_OS_WINPHONE + HRESULT hr = RoActivateInstance(HString::MakeReference(RuntimeClass_Windows_Devices_Bluetooth_Advertisement_BluetoothLEAdvertisementWatcher).Get(), &m_leWatcher); + Q_ASSERT_SUCCEEDED(hr); + hr = m_leWatcher->add_Received(Callback<ITypedEventHandler<BluetoothLEAdvertisementWatcher *, BluetoothLEAdvertisementReceivedEventArgs *>>([this](IBluetoothLEAdvertisementWatcher *, IBluetoothLEAdvertisementReceivedEventArgs *args) { + quint64 address; + HRESULT hr; + hr = args->get_BluetoothAddress(&address); + Q_ASSERT_SUCCEEDED(hr); + if (m_foundLEDevices.contains(address)) + return S_OK; + + m_foundLEDevices.append(address); + leBluetoothInfoFromAddressAsync(address); + return S_OK; + }).Get(), &m_leDeviceAddedToken); + Q_ASSERT_SUCCEEDED(hr); + hr = m_leWatcher->Start(); + Q_ASSERT_SUCCEEDED(hr); +#endif // !Q_OS_WINPHONE +} + +void QWinRTBluetoothDeviceDiscoveryWorker::handleLeTimeout() +{ + if (m_pendingPairedDevices == 0) + emit scanFinished(); + else + emit scanCanceled(); + deleteLater(); +} + +// "deviceFound" will be emitted at the end of the deviceFromIdOperation callback +void QWinRTBluetoothDeviceDiscoveryWorker::classicBluetoothInfoFromDeviceIdAsync(HSTRING deviceId) +{ + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([deviceId, this]() { + ComPtr<IAsyncOperation<BluetoothDevice *>> deviceFromIdOperation; + // on Windows 10 FromIdAsync might ask for device permission. We cannot wait here but have to handle that asynchronously + HRESULT hr = m_deviceStatics->FromIdAsync(deviceId, &deviceFromIdOperation); + if (FAILED(hr)) { + --m_pendingPairedDevices; + qCWarning(QT_BT_WINRT) << "Could not obtain bluetooth device from id"; + return S_OK; + } + + hr = deviceFromIdOperation->put_Completed(Callback<IAsyncOperationCompletedHandler<BluetoothDevice *>> + (this, &QWinRTBluetoothDeviceDiscoveryWorker::onPairedClassicBluetoothDeviceFoundAsync).Get()); + if (FAILED(hr)) { + --m_pendingPairedDevices; + qCWarning(QT_BT_WINRT) << "Could not register device found callback"; + return S_OK; + } + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + +// "deviceFound" will be emitted at the end of the deviceFromIdOperation callback +void QWinRTBluetoothDeviceDiscoveryWorker::leBluetoothInfoFromDeviceIdAsync(HSTRING deviceId) +{ + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([deviceId, this]() { + ComPtr<IAsyncOperation<BluetoothLEDevice *>> deviceFromIdOperation; + // on Windows 10 FromIdAsync might ask for device permission. We cannot wait here but have to handle that asynchronously + HRESULT hr = m_leDeviceStatics->FromIdAsync(deviceId, &deviceFromIdOperation); + if (FAILED(hr)) { + --m_pendingPairedDevices; + qCWarning(QT_BT_WINRT) << "Could not obtain bluetooth device from id"; + return S_OK; + } + + hr = deviceFromIdOperation->put_Completed(Callback<IAsyncOperationCompletedHandler<BluetoothLEDevice *>> + (this, &QWinRTBluetoothDeviceDiscoveryWorker::onPairedBluetoothLEDeviceFoundAsync).Get()); + if (FAILED(hr)) { + --m_pendingPairedDevices; + qCWarning(QT_BT_WINRT) << "Could not register device found callback"; + return S_OK; + } + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + +// "deviceFound" will be emitted at the end of the deviceFromAdressOperation callback +void QWinRTBluetoothDeviceDiscoveryWorker::leBluetoothInfoFromAddressAsync(quint64 address) +{ + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([address, this]() { + ComPtr<IAsyncOperation<BluetoothLEDevice *>> deviceFromAddressOperation; + // on Windows 10 FromBluetoothAddressAsync might ask for device permission. We cannot wait + // here but have to handle that asynchronously + HRESULT hr = m_leDeviceStatics->FromBluetoothAddressAsync(address, &deviceFromAddressOperation); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain bluetooth device from address"; + return S_OK; + } + + hr = deviceFromAddressOperation->put_Completed(Callback<IAsyncOperationCompletedHandler<BluetoothLEDevice *>> + (this, &QWinRTBluetoothDeviceDiscoveryWorker::onBluetoothLEDeviceFoundAsync).Get()); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not register device found callback"; + return S_OK; + } + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + +HRESULT QWinRTBluetoothDeviceDiscoveryWorker::onPairedClassicBluetoothDeviceFoundAsync(IAsyncOperation<BluetoothDevice *> *op, AsyncStatus status) +{ + --m_pendingPairedDevices; + if (status != AsyncStatus::Completed) + return S_OK; + + ComPtr<IBluetoothDevice> device; + HRESULT hr = op->GetResults(&device); + Q_ASSERT_SUCCEEDED(hr); + + if (!device) + return S_OK; + + UINT64 address; + HString name; + ComPtr<IBluetoothClassOfDevice> classOfDevice; + UINT32 classOfDeviceInt; + hr = device->get_BluetoothAddress(&address); + Q_ASSERT_SUCCEEDED(hr); + hr = device->get_Name(name.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + const QString btName = QString::fromWCharArray(WindowsGetStringRawBuffer(name.Get(), nullptr)); + hr = device->get_ClassOfDevice(&classOfDevice); + Q_ASSERT_SUCCEEDED(hr); + hr = classOfDevice->get_RawValue(&classOfDeviceInt); + Q_ASSERT_SUCCEEDED(hr); + IVectorView <Rfcomm::RfcommDeviceService *> *deviceServices; + hr = device->get_RfcommServices(&deviceServices); + Q_ASSERT_SUCCEEDED(hr); + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + Q_ASSERT_SUCCEEDED(hr); + QList<QBluetoothUuid> uuids; + for (uint i = 0; i < serviceCount; ++i) { + ComPtr<Rfcomm::IRfcommDeviceService> service; + hr = deviceServices->GetAt(i, &service); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<Rfcomm::IRfcommServiceId> id; + hr = service->get_ServiceId(&id); + Q_ASSERT_SUCCEEDED(hr); + GUID uuid; + hr = id->get_Uuid(&uuid); + Q_ASSERT_SUCCEEDED(hr); + uuids.append(QBluetoothUuid(uuid)); + } + + qCDebug(QT_BT_WINRT) << "Discovered BT device: " << QString::number(address) << btName + << "Num UUIDs" << uuids.count(); + + QBluetoothDeviceInfo info(QBluetoothAddress(address), btName, classOfDeviceInt); + info.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration); + info.setServiceUuids(uuids, QBluetoothDeviceInfo::DataIncomplete); + info.setCached(true); + + QMetaObject::invokeMethod(this, "deviceFound", Qt::AutoConnection, + Q_ARG(QBluetoothDeviceInfo, info)); + return S_OK; +} + +HRESULT QWinRTBluetoothDeviceDiscoveryWorker::onPairedBluetoothLEDeviceFoundAsync(IAsyncOperation<BluetoothLEDevice *> *op, AsyncStatus status) +{ + --m_pendingPairedDevices; + if (status != AsyncStatus::Completed) + return S_OK; + + ComPtr<IBluetoothLEDevice> device; + HRESULT hr; + hr = op->GetResults(&device); + Q_ASSERT_SUCCEEDED(hr); + return onBluetoothLEDeviceFound(device, OmitPairingCheck); +} + +HRESULT QWinRTBluetoothDeviceDiscoveryWorker::onBluetoothLEDeviceFoundAsync(IAsyncOperation<BluetoothLEDevice *> *op, AsyncStatus status) +{ + if (status != AsyncStatus::Completed) + return S_OK; + + ComPtr<IBluetoothLEDevice> device; + HRESULT hr; + hr = op->GetResults(&device); + Q_ASSERT_SUCCEEDED(hr); + return onBluetoothLEDeviceFound(device, PairingCheck::CheckForPairing); +} + +HRESULT QWinRTBluetoothDeviceDiscoveryWorker::onBluetoothLEDeviceFound(ComPtr<IBluetoothLEDevice> device, PairingCheck pairingCheck) +{ + if (!device) { + qCDebug(QT_BT_WINRT) << "onBluetoothLEDeviceFound: No device given"; + return S_OK; + } + + if (pairingCheck == CheckForPairing) { +#ifndef Q_OS_WINPHONE + ComPtr<IBluetoothLEDevice2> device2; + HRESULT hr = device.As(&device2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IDeviceInformation> deviceInfo; + hr = device2->get_DeviceInformation(&deviceInfo); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IDeviceInformation2> deviceInfo2; + hr = deviceInfo.As(&deviceInfo2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IDeviceInformationPairing> pairing; + hr = deviceInfo2->get_Pairing(&pairing); + Q_ASSERT_SUCCEEDED(hr); + boolean isPaired; + hr = pairing->get_IsPaired(&isPaired); + Q_ASSERT_SUCCEEDED(hr); + // We need a paired device in order to be able to obtain its information + if (!isPaired) { + ComPtr<IAsyncOperation<DevicePairingResult *>> pairingOp; + hr = pairing.Get()->PairAsync(&pairingOp); + Q_ASSERT_SUCCEEDED(hr); + pairingOp.Get()->put_Completed( + Callback<IAsyncOperationCompletedHandler<DevicePairingResult *>>([device, this](IAsyncOperation<DevicePairingResult *> *op, AsyncStatus status) { + if (status != AsyncStatus::Completed) { + qCDebug(QT_BT_WINRT) << "Could not pair device"; + return S_OK; + } + + ComPtr<IDevicePairingResult> result; + op->GetResults(&result); + + DevicePairingResultStatus pairingStatus; + result.Get()->get_Status(&pairingStatus); + + if (pairingStatus != DevicePairingResultStatus_Paired) { + qCDebug(QT_BT_WINRT) << "Could not pair device"; + return S_OK; + } + + onBluetoothLEDeviceFound(device, OmitPairingCheck); + return S_OK; + }).Get()); + return S_OK; + } +#else // !Q_OS_WINPHONE + Q_ASSERT(false); +#endif // Q_OS_WINPHONE + } + + UINT64 address; + HString name; + HRESULT hr = device->get_BluetoothAddress(&address); + Q_ASSERT_SUCCEEDED(hr); + hr = device->get_Name(name.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + const QString btName = QString::fromWCharArray(WindowsGetStringRawBuffer(name.Get(), nullptr)); + IVectorView <GenericAttributeProfile::GattDeviceService *> *deviceServices; + hr = device->get_GattServices(&deviceServices); + Q_ASSERT_SUCCEEDED(hr); + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + Q_ASSERT_SUCCEEDED(hr); + QList<QBluetoothUuid> uuids; + for (uint i = 0; i < serviceCount; ++i) { + ComPtr<GenericAttributeProfile::IGattDeviceService> service; + hr = deviceServices->GetAt(i, &service); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<Rfcomm::IRfcommServiceId> id; + GUID uuid; + hr = service->get_Uuid(&uuid); + Q_ASSERT_SUCCEEDED(hr); + uuids.append(QBluetoothUuid(uuid)); + } + + qCDebug(QT_BT_WINRT) << "Discovered BTLE device: " << QString::number(address) << btName + << "Num UUIDs" << uuids.count(); + + QBluetoothDeviceInfo info(QBluetoothAddress(address), btName, 0); + info.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); + info.setServiceUuids(uuids, QBluetoothDeviceInfo::DataIncomplete); + info.setCached(true); + + QMetaObject::invokeMethod(this, "deviceFound", Qt::AutoConnection, + Q_ARG(QBluetoothDeviceInfo, info)); + return S_OK; +} + +QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate( + const QBluetoothAddress &deviceAdapter, + QBluetoothDeviceDiscoveryAgent *parent) + + : inquiryType(QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry), + lastError(QBluetoothDeviceDiscoveryAgent::NoError), + lowEnergySearchTimeout(25000), + q_ptr(parent), + leScanTimer(0) +{ + Q_UNUSED(deviceAdapter); +} + +QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() +{ + disconnectAndClearWorker(); +} + +bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const +{ + return worker; +} + +QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() +{ + return (ClassicMethod | LowEnergyMethod); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods) +{ + if (worker) + return; + + worker = new QWinRTBluetoothDeviceDiscoveryWorker(methods); + discoveredDevices.clear(); + connect(worker, &QWinRTBluetoothDeviceDiscoveryWorker::deviceFound, + this, &QBluetoothDeviceDiscoveryAgentPrivate::registerDevice); + connect(worker, &QWinRTBluetoothDeviceDiscoveryWorker::scanFinished, + this, &QBluetoothDeviceDiscoveryAgentPrivate::onScanFinished); + connect(worker, &QWinRTBluetoothDeviceDiscoveryWorker::scanCanceled, + this, &QBluetoothDeviceDiscoveryAgentPrivate::onScanCanceled); + worker->start(); + + if (lowEnergySearchTimeout > 0 && methods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) { // otherwise no timeout and stop() required + if (!leScanTimer) { + leScanTimer = new QTimer(this); + leScanTimer->setSingleShot(true); + } + connect(leScanTimer, &QTimer::timeout, + worker, &QWinRTBluetoothDeviceDiscoveryWorker::handleLeTimeout); + leScanTimer->setInterval(lowEnergySearchTimeout); + leScanTimer->start(); + } +} + +void QBluetoothDeviceDiscoveryAgentPrivate::stop() +{ + Q_Q(QBluetoothDeviceDiscoveryAgent); + if (worker) { + worker->stop(); + disconnectAndClearWorker(); + emit q->canceled(); + } + if (leScanTimer) { + leScanTimer->stop(); + worker->deleteLater(); + } +} + +void QBluetoothDeviceDiscoveryAgentPrivate::registerDevice(const QBluetoothDeviceInfo &info) +{ + Q_Q(QBluetoothDeviceDiscoveryAgent); + + for (QList<QBluetoothDeviceInfo>::iterator iter = discoveredDevices.begin(); + iter != discoveredDevices.end(); ++iter) { + if (iter->address() == info.address()) { + qCDebug(QT_BT_WINRT) << "Updating device" << iter->name() << iter->address(); + // merge service uuids + QList<QBluetoothUuid> uuids = iter->serviceUuids(); + uuids.append(info.serviceUuids()); + const QSet<QBluetoothUuid> uuidSet = uuids.toSet(); + if (iter->serviceUuids().count() != uuidSet.count()) + iter->setServiceUuids(uuidSet.toList(), QBluetoothDeviceInfo::DataIncomplete); + if (iter->coreConfigurations() != info.coreConfigurations()) + iter->setCoreConfigurations(QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration); + return; + } + } + + discoveredDevices << info; + emit q->deviceDiscovered(info); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::onScanFinished() +{ + Q_Q(QBluetoothDeviceDiscoveryAgent); + disconnectAndClearWorker(); + emit q->finished(); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::onScanCanceled() +{ + Q_Q(QBluetoothDeviceDiscoveryAgent); + disconnectAndClearWorker(); + emit q->canceled(); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::disconnectAndClearWorker() +{ + Q_Q(QBluetoothDeviceDiscoveryAgent); + if (!worker) + return; + + disconnect(worker, &QWinRTBluetoothDeviceDiscoveryWorker::scanCanceled, + this, &QBluetoothDeviceDiscoveryAgentPrivate::onScanCanceled); + disconnect(worker, &QWinRTBluetoothDeviceDiscoveryWorker::scanFinished, + this, &QBluetoothDeviceDiscoveryAgentPrivate::onScanFinished); + disconnect(worker, &QWinRTBluetoothDeviceDiscoveryWorker::deviceFound, + q, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered); + if (leScanTimer) { + disconnect(leScanTimer, &QTimer::timeout, + worker, &QWinRTBluetoothDeviceDiscoveryWorker::handleLeTimeout); + } + worker.clear(); +} + +QT_END_NAMESPACE + +#include <qbluetoothdevicediscoveryagent_winrt.moc> diff --git a/src/bluetooth/qbluetoothdevicediscoverytimer_osx_p.h b/src/bluetooth/qbluetoothdevicediscoverytimer_osx_p.h deleted file mode 100644 index 88906ffd..00000000 --- a/src/bluetooth/qbluetoothdevicediscoverytimer_osx_p.h +++ /dev/null @@ -1,85 +0,0 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ - -#ifndef QBLUETOOTHDEVICEDISCOVERYTIMER_OSX_P_H -#define QBLUETOOTHDEVICEDISCOVERYTIMER_OSX_P_H - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -#include <QtCore/qglobal.h> -#include <QtCore/qtimer.h> - -QT_BEGIN_NAMESPACE - -class QBluetoothDeviceDiscoveryAgentPrivate; - -namespace OSXBluetooth { - -class DDATimerHandler : public QObject -{ - Q_OBJECT - -public: - DDATimerHandler(QBluetoothDeviceDiscoveryAgentPrivate *d); - - void start(int msec); - void stop(); - -private slots: - void onTimer(); - -private: - QTimer timer; - QBluetoothDeviceDiscoveryAgentPrivate *owner; -}; - -} - -QT_END_NAMESPACE - -#endif // QBLUETOOTHDEVICEDISCOVERYTIMER_OSX_P_H diff --git a/src/bluetooth/qbluetoothlocaldevice_android.cpp b/src/bluetooth/qbluetoothlocaldevice_android.cpp index 72162fd4..f36e184c 100644 --- a/src/bluetooth/qbluetoothlocaldevice_android.cpp +++ b/src/bluetooth/qbluetoothlocaldevice_android.cpp @@ -87,18 +87,36 @@ QAndroidJniObject *QBluetoothLocalDevicePrivate::adapter() return obj; } -void QBluetoothLocalDevicePrivate::initialize(const QBluetoothAddress &address) +static QAndroidJniObject getDefaultAdapter() { - QAndroidJniEnvironment env; - QAndroidJniObject adapter = QAndroidJniObject::callStaticObjectMethod( "android/bluetooth/BluetoothAdapter", "getDefaultAdapter", "()Landroid/bluetooth/BluetoothAdapter;"); if (!adapter.isValid()) { + QAndroidJniEnvironment env; if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); } + + // workaround stupid bt implementations where first call of BluetoothAdapter.getDefaultAdapter() always fails + adapter = QAndroidJniObject::callStaticObjectMethod( + "android/bluetooth/BluetoothAdapter", "getDefaultAdapter", + "()Landroid/bluetooth/BluetoothAdapter;"); + if (!adapter.isValid()) { + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + } + } + return adapter; +} + +void QBluetoothLocalDevicePrivate::initialize(const QBluetoothAddress &address) +{ + QAndroidJniObject adapter = getDefaultAdapter(); + if (!adapter.isValid()) { qCWarning(QT_BT_ANDROID) << "Device does not support Bluetooth"; return; } @@ -304,33 +322,7 @@ QList<QBluetoothHostInfo> QBluetoothLocalDevice::allDevices() // Android only supports max of one device (so far) QList<QBluetoothHostInfo> localDevices; - QAndroidJniEnvironment env; - jclass btAdapterClass = env->FindClass("android/bluetooth/BluetoothAdapter"); - if (btAdapterClass == NULL) { - qCWarning(QT_BT_ANDROID) - << "Native registration unable to find class android/bluetooth/BluetoothAdapter"; - return localDevices; - } - - jmethodID getDefaultAdapterID - = env->GetStaticMethodID(btAdapterClass, "getDefaultAdapter", - "()Landroid/bluetooth/BluetoothAdapter;"); - if (getDefaultAdapterID == NULL) { - qCWarning(QT_BT_ANDROID) - << "Native registration unable to get method ID: " \ - "getDefaultAdapter of android/bluetooth/BluetoothAdapter"; - env->DeleteLocalRef(btAdapterClass); - return localDevices; - } - - jobject btAdapterObject = env->CallStaticObjectMethod(btAdapterClass, getDefaultAdapterID); - if (btAdapterObject == NULL) { - qCWarning(QT_BT_ANDROID) << "Device does not support Bluetooth"; - env->DeleteLocalRef(btAdapterClass); - return localDevices; - } - - QAndroidJniObject o(btAdapterObject); + QAndroidJniObject o = getDefaultAdapter(); if (o.isValid()) { QBluetoothHostInfo info; info.setName(o.callObjectMethod("getName", "()Ljava/lang/String;").toString()); @@ -338,10 +330,6 @@ QList<QBluetoothHostInfo> QBluetoothLocalDevice::allDevices() "()Ljava/lang/String;").toString())); localDevices.append(info); } - - env->DeleteLocalRef(btAdapterObject); - env->DeleteLocalRef(btAdapterClass); - return localDevices; } diff --git a/src/bluetooth/qbluetoothlocaldevice_osx.mm b/src/bluetooth/qbluetoothlocaldevice_osx.mm index c005e2ea..45fa310a 100644 --- a/src/bluetooth/qbluetoothlocaldevice_osx.mm +++ b/src/bluetooth/qbluetoothlocaldevice_osx.mm @@ -118,29 +118,25 @@ QBluetoothLocalDevicePrivate::QBluetoothLocalDevicePrivate(QBluetoothLocalDevice HostController defaultController([[IOBluetoothHostController defaultController] retain]); if (!defaultController) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "init a host controller object"; + qCCritical(QT_BT_OSX) << "failed to init a host controller object"; return; } if (!address.isNull()) { NSString *const hciAddress = [defaultController addressAsString]; if (!hciAddress) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "obtain an address"; + qCCritical(QT_BT_OSX) << "failed to obtain an address"; return; } BluetoothDeviceAddress iobtAddress = {}; if (IOBluetoothNSStringToDeviceAddress(hciAddress, &iobtAddress) != kIOReturnSuccess) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "invalid " - "local device's address"; + qCCritical(QT_BT_OSX) << "invalid local device's address"; return; } if (address != OSXBluetooth::qt_address(&iobtAddress)) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "invalid " - "local device's address"; + qCCritical(QT_BT_OSX) << "invalid local device's address"; return; } } @@ -183,8 +179,7 @@ void QBluetoothLocalDevicePrivate::requestPairing(const QBluetoothAddress &addre if ([device isPaired]) { emitPairingFinished(address, pairing, true); } else if ([pos.value() start] != kIOReturnSuccess) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "start a new pairing request"; + qCCritical(QT_BT_OSX) << "failed to start a new pairing request"; emitError(QBluetoothLocalDevice::PairingError, true); } return; @@ -195,8 +190,7 @@ void QBluetoothLocalDevicePrivate::requestPairing(const QBluetoothAddress &addre // it'll just finish with success (skipping any intermediate steps). PairingRequest newRequest([[ObjCPairingRequest alloc] initWithTarget:address delegate:this], false); if (!newRequest) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "allocate a new pairing request"; + qCCritical(QT_BT_OSX) << "failed to allocate a new pairing request"; emitError(QBluetoothLocalDevice::PairingError, true); return; } @@ -205,8 +199,7 @@ void QBluetoothLocalDevicePrivate::requestPairing(const QBluetoothAddress &addre const IOReturn result = [newRequest start]; if (result != kIOReturnSuccess) { pairingRequests.erase(pos); - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "start a new pairing request"; + qCCritical(QT_BT_OSX) << "failed to start a new pairing request"; emitError(QBluetoothLocalDevice::PairingError, true); } } diff --git a/src/bluetooth/qbluetoothlocaldevice_p.cpp b/src/bluetooth/qbluetoothlocaldevice_p.cpp index f266d64c..e9177f39 100644 --- a/src/bluetooth/qbluetoothlocaldevice_p.cpp +++ b/src/bluetooth/qbluetoothlocaldevice_p.cpp @@ -103,6 +103,9 @@ void QBluetoothLocalDevice::requestPairing(const QBluetoothAddress &address, Pai { Q_UNUSED(address); Q_UNUSED(pairing); + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(QBluetoothLocalDevice::Error, + QBluetoothLocalDevice::PairingError)); } QBluetoothLocalDevice::Pairing QBluetoothLocalDevice::pairingStatus( diff --git a/src/bluetooth/qbluetoothserver_osx.mm b/src/bluetooth/qbluetoothserver_osx.mm index 549a07cc..8896651d 100644 --- a/src/bluetooth/qbluetoothserver_osx.mm +++ b/src/bluetooth/qbluetoothserver_osx.mm @@ -95,7 +95,7 @@ QBluetoothServerPrivate::QBluetoothServerPrivate(QSInfo::Protocol type, QBluetoo { Q_ASSERT_X(q_ptr, Q_FUNC_INFO, "invalid q_ptr (null)"); if (serverType == QSInfo::UnknownProtocol) - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unknown protocol"; + qCWarning(QT_BT_OSX) << "unknown protocol"; } QBluetoothServerPrivate::~QBluetoothServerPrivate() @@ -116,7 +116,7 @@ bool QBluetoothServerPrivate::startListener(quint16 realPort) Q_ASSERT_X(realPort, Q_FUNC_INFO, "invalid port"); if (serverType == QSInfo::UnknownProtocol) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid protocol"; + qCWarning(QT_BT_OSX) << "invalid protocol"; return false; } @@ -217,7 +217,7 @@ void QBluetoothServerPrivate::registerServer(QBluetoothServerPrivate *server, qu Q_ASSERT_X(!psmIsBusy(port), Q_FUNC_INFO, "port is busy"); busyPSMs()[port] = server; } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "can not register a server " + qCWarning(QT_BT_OSX) << "can not register a server " "with unknown protocol type"; } } @@ -234,7 +234,7 @@ QBluetoothServerPrivate *QBluetoothServerPrivate::registeredServer(quint16 port, if (it != busyPSMs().end()) return it.value(); } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid protocol"; + qCWarning(QT_BT_OSX) << "invalid protocol"; } return Q_NULLPTR; @@ -251,17 +251,17 @@ void QBluetoothServerPrivate::unregisterServer(QBluetoothServerPrivate *server) if (it != busyChannels().end()) { busyChannels().erase(it); } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "server is not registered"; + qCWarning(QT_BT_OSX) << "server is not registered"; } } else if (type == QSInfo::L2capProtocol) { ServerMapIterator it = busyPSMs().find(port); if (it != busyPSMs().end()) { busyPSMs().erase(it); } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "server is not registered"; + qCWarning(QT_BT_OSX) << "server is not registered"; } } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid protocol"; + qCWarning(QT_BT_OSX) << "invalid protocol"; } } @@ -292,16 +292,15 @@ bool QBluetoothServer::listen(const QBluetoothAddress &address, quint16 port) typedef QBluetoothServerPrivate::ObjCListener ObjCListener; if (d_ptr->listener) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "already in listen mode, " - "close server first"; + qCWarning(QT_BT_OSX) << "already in listen mode, close server first"; return false; } const QBluetoothLocalDevice device(address); if (!device.isValid()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "device does not support Bluetooth or " + qCWarning(QT_BT_OSX) << "device does not support Bluetooth or" << address.toString() - << " is not a valid local adapter"; + << "is not a valid local adapter"; d_ptr->lastError = UnknownError; emit error(UnknownError); return false; @@ -309,7 +308,7 @@ bool QBluetoothServer::listen(const QBluetoothAddress &address, quint16 port) const QBluetoothLocalDevice::HostMode hostMode = device.hostMode(); if (hostMode == QBluetoothLocalDevice::HostPoweredOff) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "Bluetooth device is powered off"; + qCWarning(QT_BT_OSX) << "Bluetooth device is powered off"; d_ptr->lastError = PoweredOffError; emit error(PoweredOffError); return false; @@ -318,7 +317,7 @@ bool QBluetoothServer::listen(const QBluetoothAddress &address, quint16 port) const QSInfo::Protocol type = d_ptr->serverType; if (type == QSInfo::UnknownProtocol) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid protocol"; + qCWarning(QT_BT_OSX) << "invalid protocol"; d_ptr->lastError = UnsupportedProtocolError; emit error(d_ptr->lastError); return false; @@ -332,14 +331,14 @@ bool QBluetoothServer::listen(const QBluetoothAddress &address, quint16 port) if (port) { if (type == QSInfo::RfcommProtocol) { if (d_ptr->channelIsBusy(port)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO <<"server port: " - << port << "already registered"; + qCWarning(QT_BT_OSX) << "server port:" << port + << "already registered"; d_ptr->lastError = ServiceAlreadyRegisteredError; } } else { if (d_ptr->psmIsBusy(port)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "server port: " - << port << "already registered"; + qCWarning(QT_BT_OSX) << "server port:" << port + << "already registered"; d_ptr->lastError = ServiceAlreadyRegisteredError; } } @@ -354,7 +353,7 @@ bool QBluetoothServer::listen(const QBluetoothAddress &address, quint16 port) } if (!port) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "all ports are busy"; + qCWarning(QT_BT_OSX) << "all ports are busy"; d_ptr->lastError = ServiceAlreadyRegisteredError; emit error(d_ptr->lastError); return false; diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm index 5c4efca2..4a52b379 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm +++ b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm @@ -335,7 +335,7 @@ void QBluetoothServiceDiscoveryAgentPrivate::SDPInquiryError(IOBluetoothDevice * { Q_UNUSED(device) - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "inquiry failed with IOKit code: " << int(errorCode); + qCWarning(QT_BT_OSX) << "inquiry failed with IOKit code:" << int(errorCode); discoveredDevices.clear(); // TODO: find a better mapping from IOReturn to QBluetoothServiceDiscoveryAgent::Error. diff --git a/src/bluetooth/qbluetoothserviceinfo_osx.mm b/src/bluetooth/qbluetoothserviceinfo_osx.mm index d6aa7592..a25653ab 100644 --- a/src/bluetooth/qbluetoothserviceinfo_osx.mm +++ b/src/bluetooth/qbluetoothserviceinfo_osx.mm @@ -46,7 +46,6 @@ #include <QtCore/qloggingcategory.h> #include <QtCore/qvariant.h> -#include <QtCore/qsysinfo.h> #include <QtCore/qglobal.h> #include <QtCore/qmutex.h> #include <QtCore/qmap.h> @@ -56,44 +55,6 @@ QT_BEGIN_NAMESPACE -namespace { - -// This is not in osxbtutility_p, since it's not required -// in general and just fixes the problem with SDK < 10.9, -// where we have to care about about IOBluetoothSDPServiceRecordRef. -class ServiceRecordRefGuard -{ -public: - ServiceRecordRefGuard() - : recordRef(Q_NULLPTR) - { - } - explicit ServiceRecordRefGuard(IOBluetoothSDPServiceRecordRef r) - : recordRef(r) - { - } - ~ServiceRecordRefGuard() - { - if (recordRef) // Requires non-NULL pointers. - CFRelease(recordRef); - } - - void reset(IOBluetoothSDPServiceRecordRef r) - { - if (recordRef) - CFRelease(recordRef); - // Take the ownership: - recordRef = r; - } - -private: - IOBluetoothSDPServiceRecordRef recordRef; - - Q_DISABLE_COPY(ServiceRecordRefGuard) -}; - -} - class QBluetoothServiceInfoPrivate { public: @@ -144,48 +105,23 @@ bool QBluetoothServiceInfoPrivate::registerService(const QBluetoothAddress &loca serviceDict(iobluetooth_service_dictionary(*q_ptr)); if (!serviceDict) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to create a service dictionary"; + qCWarning(QT_BT_OSX) << "failed to create a service dictionary"; return false; } - ServiceRecordRefGuard refGuard; SDPRecord newRecord; -#if QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9) - if (QSysInfo::MacintoshVersion >= QSysInfo::MV_10_9) { - newRecord.reset([[IOBluetoothSDPServiceRecord - publishedServiceRecordWithDictionary:serviceDict] retain]); - } else { -#else - { -#endif - IOBluetoothSDPServiceRecordRef recordRef = Q_NULLPTR; - // With ARC this will require a different cast? - const IOReturn status = IOBluetoothAddServiceDict((CFDictionaryRef)serviceDict.data(), &recordRef); - if (status != kIOReturnSuccess) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to register a service record"; - return false; - } - - refGuard.reset(recordRef); - newRecord.reset([[IOBluetoothSDPServiceRecord withSDPServiceRecordRef:recordRef] retain]); - // It's weird, but ... it's not possible to release a record ref yet. - } + newRecord.reset([[IOBluetoothSDPServiceRecord + publishedServiceRecordWithDictionary:serviceDict] retain]); if (!newRecord) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to register a service record"; - // In case of SDK < 10.9 it's not possible to remove a service record ... - // no way to obtain record handle yet. + qCWarning(QT_BT_OSX) << "failed to register a service record"; return false; } BluetoothSDPServiceRecordHandle newRecordHandle = 0; if ([newRecord getServiceRecordHandle:&newRecordHandle] != kIOReturnSuccess) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to register a service record"; -#if QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9) - if (QSysInfo::MacintoshVersion >= QSysInfo::MV_10_9) - [newRecord removeServiceRecord]; -#endif - // With SDK < 10.9 there is no way to unregister at this point ... + qCWarning(QT_BT_OSX) << "failed to register a service record"; + [newRecord removeServiceRecord]; return false; } @@ -211,17 +147,8 @@ bool QBluetoothServiceInfoPrivate::registerService(const QBluetoothAddress &loca } if (!configured) { -#if QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9) - if (QSysInfo::MacintoshVersion >= QSysInfo::MV_10_9) { - [newRecord removeServiceRecord]; - } else { -#else - {// Just to balance braces ... -#endif - IOBluetoothRemoveServiceWithRecordHandle(newRecordHandle); - } - - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to register a service record"; + [newRecord removeServiceRecord]; + qCWarning(QT_BT_OSX) << "failed to register a service record"; return false; } @@ -247,16 +174,7 @@ bool QBluetoothServiceInfoPrivate::unregisterService() Q_ASSERT_X(serviceRecord, Q_FUNC_INFO, "service registered, but serviceRecord is nil"); -#if QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9) - if (QSysInfo::MacintoshVersion >= QSysInfo::MV_10_9) { - [serviceRecord removeServiceRecord]; - } else { -#else - { -#endif - // Assert on newRecordHandle? Is 0 a valid/invalid handle? - IOBluetoothRemoveServiceWithRecordHandle(serviceRecordHandle); - } + [serviceRecord removeServiceRecord]; serviceRecord.reset(nil); diff --git a/src/bluetooth/qbluetoothsocket_android.cpp b/src/bluetooth/qbluetoothsocket_android.cpp index ab8cd876..56d4f77b 100644 --- a/src/bluetooth/qbluetoothsocket_android.cpp +++ b/src/bluetooth/qbluetoothsocket_android.cpp @@ -252,15 +252,15 @@ bool QBluetoothSocketPrivate::fallBackConnect(QAndroidJniObject uuid, int channe if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); - } - - if (socketChannel - == remoteDevice.getStaticField<jint>("android/bluetooth/BluetoothDevice", "ERROR") - || socketChannel == -1) { - qCWarning(QT_BT_ANDROID) << "Cannot determine RFCOMM service channel."; } else { - qCWarning(QT_BT_ANDROID) << "Using found rfcomm channel" << socketChannel; - channel = socketChannel; + if (socketChannel + == remoteDevice.getStaticField<jint>("android/bluetooth/BluetoothDevice", "ERROR") + || socketChannel == -1) { + qCWarning(QT_BT_ANDROID) << "Cannot determine RFCOMM service channel."; + } else { + qCWarning(QT_BT_ANDROID) << "Using found rfcomm channel" << socketChannel; + channel = socketChannel; + } } } diff --git a/src/bluetooth/qbluetoothsocket_osx.mm b/src/bluetooth/qbluetoothsocket_osx.mm index 69663e58..75712868 100644 --- a/src/bluetooth/qbluetoothsocket_osx.mm +++ b/src/bluetooth/qbluetoothsocket_osx.mm @@ -452,7 +452,7 @@ void QBluetoothSocket::connectToService(const QBluetoothServiceInfo &service, Op } if (state() != UnconnectedState && state() != ServiceLookupState) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "called on a busy socket"; + qCWarning(QT_BT_OSX) << "called on a busy socket"; d_ptr->errorString = QCoreApplication::translate(SOCKET, SOC_CONNECT_IN_PROGRESS); setSocketError(OperationError); return; @@ -468,8 +468,8 @@ void QBluetoothSocket::connectToService(const QBluetoothServiceInfo &service, Op } else { // Try service discovery. if (service.serviceUuid().isNull()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "No port, " - "no PSM, and no UUID provided, unable to connect"; + qCWarning(QT_BT_OSX) << "No port, no PSM, and no " + "UUID provided, unable to connect"; return; } @@ -489,7 +489,7 @@ void QBluetoothSocket::connectToService(const QBluetoothAddress &address, const } if (state() != QBluetoothSocket::UnconnectedState) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "called on a busy socket"; + qCWarning(QT_BT_OSX) << "called on a busy socket"; d_ptr->errorString = QCoreApplication::translate(SOCKET, SOC_CONNECT_IN_PROGRESS); setSocketError(QBluetoothSocket::OperationError); return; @@ -513,7 +513,7 @@ void QBluetoothSocket::connectToService(const QBluetoothAddress &address, quint1 } if (state() != QBluetoothSocket::UnconnectedState) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "called on a busy socket"; + qCWarning(QT_BT_OSX) << "called on a busy socket"; d_ptr->errorString = QCoreApplication::translate(SOCKET, SOC_CONNECT_IN_PROGRESS); setSocketError(OperationError); return; @@ -554,7 +554,7 @@ void QBluetoothSocket::setSocketState(QBluetoothSocket::SocketState state) // We can register for L2CAP/RFCOMM open notifications, // that's different from 'listen' and is implemented // in QBluetoothServer. - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "listening sockets are not supported"; + qCWarning(QT_BT_OSX) << "listening sockets are not supported"; } } diff --git a/src/bluetooth/qbluetoothtransferreply_osx.mm b/src/bluetooth/qbluetoothtransferreply_osx.mm index 99c6cab1..02133860 100644 --- a/src/bluetooth/qbluetoothtransferreply_osx.mm +++ b/src/bluetooth/qbluetoothtransferreply_osx.mm @@ -177,7 +177,7 @@ void QBluetoothTransferReplyOSXPrivate::sendConnect(const QBluetoothAddress &dev errorString.clear(); if (device.isNull() || !channelID) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid device address or port"; + qCWarning(QT_BT_OSX) << "invalid device address or port"; setReplyError(QBluetoothTransferReply::HostNotFoundError, QCoreApplication::translate(TRANSFER_REPLY, TR_INVAL_TARGET)); return; @@ -186,7 +186,7 @@ void QBluetoothTransferReplyOSXPrivate::sendConnect(const QBluetoothAddress &dev OBEXSession newSession([[ObjCOBEXSession alloc] initWithDelegate:this remoteDevice:device channelID:channelID]); if (!newSession) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to allocate OSXBTOBEXSession object"; + qCWarning(QT_BT_OSX) << "failed to allocate OSXBTOBEXSession object"; setReplyError(QBluetoothTransferReply::UnknownError, QCoreApplication::translate(TRANSFER_REPLY, TR_SESSION_NO_START)); @@ -201,7 +201,7 @@ void QBluetoothTransferReplyOSXPrivate::sendConnect(const QBluetoothAddress &dev if ([session isConnected]) sendPut();// Connected, send a PUT request. } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "OBEXConnect failed"; + qCWarning(QT_BT_OSX) << "OBEXConnect failed"; if (error == QBluetoothTransferReply::NoError) { // The error is not set yet. @@ -355,7 +355,7 @@ QBluetoothTransferReplyOSX::QBluetoothTransferReplyOSX(QIODevice *input, if (input) { QMetaObject::invokeMethod(this, "start", Qt::QueuedConnection); } else { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid input stream (null)"; + qCWarning(QT_BT_OSX) << "invalid input stream (null)"; osx_d_ptr->requestComplete = true; osx_d_ptr->errorString = QCoreApplication::translate(TRANSFER_REPLY, TR_INVALID_DEVICE); osx_d_ptr->error = FileNotFoundError; @@ -416,7 +416,7 @@ bool QBluetoothTransferReplyOSX::start() if (!osx_d_ptr->isActive()) { // Step 0: find a channelID. if (request().address().isNull()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "invalid device address"; + qCWarning(QT_BT_OSX) << "invalid device address"; osx_d_ptr->setReplyError(HostNotFoundError, QCoreApplication::translate(TRANSFER_REPLY, TR_INVAL_TARGET)); return false; @@ -455,7 +455,8 @@ void QBluetoothTransferReplyOSX::serviceDiscoveryFinished() void QBluetoothTransferReplyOSX::serviceDiscoveryError(QBluetoothServiceDiscoveryAgent::Error errorCode) { - Q_ASSERT_X(osx_d_ptr->agent.data(), Q_FUNC_INFO, "invalid service discovery agent (null)"); + Q_ASSERT_X(osx_d_ptr->agent.data(), Q_FUNC_INFO, + "invalid service discovery agent (null)"); if (errorCode == QBluetoothServiceDiscoveryAgent::PoweredOffError) { // There's nothing else we can do. diff --git a/src/bluetooth/qlowenergycontroller.cpp b/src/bluetooth/qlowenergycontroller.cpp index 6581c4ad..03278276 100644 --- a/src/bluetooth/qlowenergycontroller.cpp +++ b/src/bluetooth/qlowenergycontroller.cpp @@ -198,13 +198,17 @@ Q_DECLARE_LOGGING_CATEGORY(QT_BT) This signal is emitted when the controller successfully connects to the remote Low Energy device (if the controller is in the \l CentralRole) or if a remote Low Energy device connected to the controller (if the controller is in the \l PeripheralRole). + On iOS and OS X this signal is not reliable if the controller is in the \l PeripheralRole + - the controller only guesses that some central connected to our peripheral as + soon as this central tries to write/read a characteristic/descriptor. */ /*! \fn void QLowEnergyController::disconnected() This signal is emitted when the controller disconnects from the remote - Low Energy device or vice versa. + Low Energy device or vice versa. On iOS and OS X this signal is unreliable + if the controller is in the \l PeripheralRole. */ /*! @@ -308,6 +312,9 @@ void QLowEnergyControllerPrivate::setError( bool QLowEnergyControllerPrivate::isValidLocalAdapter() { +#ifdef QT_WINRT_BLUETOOTH + return true; +#endif if (localAdapter.isNull()) return false; @@ -628,6 +635,23 @@ QBluetoothAddress QLowEnergyController::remoteAddress() const } /*! + Returns the unique identifier of the remote Bluetooth Low Energy device. + + On macOS/iOS/tvOS CoreBluetooth does not expose/accept hardware addresses for + LE devices; instead developers are supposed to use unique 128-bit UUIDs, generated + by CoreBluetooth. These UUIDS will stay constant for the same central <-> peripheral + pair and we use them when connecting to a remote device. For a controller in the + \l CentralRole, this value will always be the one passed in when the controller + object was created. For a controller in the \l PeripheralRole, this value is invalid. + + \since 5.8 + */ +QBluetoothUuid QLowEnergyController::remoteDeviceUuid() const +{ + return QBluetoothUuid(); +} + +/*! Returns the name of the remote Bluetooth Low Energy device, if the controller is in the \l CentralRole. Otherwise the result is unspecified. @@ -687,6 +711,11 @@ void QLowEnergyController::connectToDevice() { Q_D(QLowEnergyController); + if (role() != CentralRole) { + qCWarning(QT_BT) << "Connection can only be established while in central role"; + return; + } + if (!d->isValidLocalAdapter()) { d->setError(QLowEnergyController::InvalidBluetoothAdapterError); return; @@ -708,6 +737,10 @@ void QLowEnergyController::connectToDevice() This function does nothing if the controller is in the \l UnconnectedState. + If the controller is in the peripheral role, it stops advertising too. + The application must restart the advertising mode by calling + \l startAdvertising(). + \sa connectToDevice() */ void QLowEnergyController::disconnectFromDevice() diff --git a/src/bluetooth/qlowenergycontroller.h b/src/bluetooth/qlowenergycontroller.h index 4ee07531..1c4fa83f 100644 --- a/src/bluetooth/qlowenergycontroller.h +++ b/src/bluetooth/qlowenergycontroller.h @@ -107,6 +107,7 @@ public: QBluetoothAddress localAddress() const; QBluetoothAddress remoteAddress() const; + QBluetoothUuid remoteDeviceUuid() const; QString remoteName() const; diff --git a/src/bluetooth/qlowenergycontroller_bluez.cpp b/src/bluetooth/qlowenergycontroller_bluez.cpp index e85fcb34..f8d2ba2a 100644 --- a/src/bluetooth/qlowenergycontroller_bluez.cpp +++ b/src/bluetooth/qlowenergycontroller_bluez.cpp @@ -48,6 +48,7 @@ #include <QtCore/QFileInfo> #include <QtCore/QLoggingCategory> #include <QtCore/QSettings> +#include <QtCore/QTimer> #include <QtBluetooth/QBluetoothLocalDevice> #include <QtBluetooth/QBluetoothSocket> #include <QtBluetooth/QLowEnergyCharacteristicData> @@ -132,6 +133,13 @@ #define ATT_ERROR_UNSUPPRTED_GROUP_TYPE 0x10 #define ATT_ERROR_INSUF_RESOURCES 0x11 #define ATT_ERROR_APPLICATION_START 0x80 +//------------------------------------------ +// The error codes in this block are +// implementation specific errors + +#define ATT_ERROR_REQUEST_STALLED 0x81 + +//------------------------------------------ #define ATT_ERROR_APPLICATION_END 0x9f #define APPEND_VALUE true @@ -211,7 +219,7 @@ static void dumpErrorInformation(const QByteArray &response) errorString = QStringLiteral("insufficient resources to complete request"); break; default: if (errorCode >= ATT_ERROR_APPLICATION_START && errorCode <= ATT_ERROR_APPLICATION_END) - errorString = QStringLiteral("application error"); + errorString = QStringLiteral("application error: %1").arg(errorCode); else errorString = QStringLiteral("unknown error code"); break; @@ -304,6 +312,100 @@ void QLowEnergyControllerPrivate::init() signingData.insert(remoteDevice.toUInt64(), SigningData(csrk)); } ); + + if (role == QLowEnergyController::CentralRole) { + if (Q_UNLIKELY(!qEnvironmentVariableIsEmpty("BLUETOOTH_GATT_TIMEOUT"))) { + bool ok = false; + int value = qEnvironmentVariableIntValue("BLUETOOTH_GATT_TIMEOUT", &ok); + if (ok) + gattRequestTimeout = value; + } + + // permit disabling of timeout behavior via environment variable + if (gattRequestTimeout > 0) { + qCWarning(QT_BT_BLUEZ) << "Enabling GATT request timeout behavior" << gattRequestTimeout; + requestTimer = new QTimer(this); + requestTimer->setSingleShot(true); + requestTimer->setInterval(gattRequestTimeout); + connect(requestTimer, &QTimer::timeout, + this, &QLowEnergyControllerPrivate::handleGattRequestTimeout); + } + } +} + +void QLowEnergyControllerPrivate::handleGattRequestTimeout() +{ + // antyhing open that might require cancellation or a warning? + if (encryptionChangePending) { + // We cannot really recover for now but the warning is essential for debugging + qCWarning(QT_BT_BLUEZ) << "****** Encryption change event blocking further GATT requests"; + return; + } + + if (!openRequests.isEmpty() && requestPending) { + requestPending = false; // reset pending flag + const Request currentRequest = openRequests.dequeue(); + + qCWarning(QT_BT_BLUEZ).nospace() << "****** Request type 0x" << hex << currentRequest.command + << " to server/peripheral timed out"; + qCWarning(QT_BT_BLUEZ) << "****** Looks like the characteristic or descriptor does NOT act in" + << "accordance to Bluetooth 4.x spec."; + qCWarning(QT_BT_BLUEZ) << "****** Please check server implementation." + << "Continuing under reservation."; + + quint8 command = currentRequest.command; + const auto createRequestErrorMessage = [](quint8 opcodeWithError, + QLowEnergyHandle handle) { + QByteArray errorPackage(ERROR_RESPONSE_HEADER_SIZE, Qt::Uninitialized); + errorPackage[0] = ATT_OP_ERROR_RESPONSE; + errorPackage[1] = opcodeWithError; // e.g. ATT_OP_READ_REQUEST + putBtData(handle, errorPackage.data() + 2); // + errorPackage[4] = ATT_ERROR_REQUEST_STALLED; + + return errorPackage; + }; + + switch (command) { + case ATT_OP_EXCHANGE_MTU_REQUEST: // MTU change request + // never received reply to MTU request + // it is safe to skip and go to next request + sendNextPendingRequest(); + break; + case ATT_OP_READ_BY_GROUP_REQUEST: // primary or secondary service discovery + case ATT_OP_READ_BY_TYPE_REQUEST: // characteristic or included service discovery + // jump back into usual response handling with custom error code + // 2nd param "0" as required by spec + processReply(currentRequest, createRequestErrorMessage(command, 0)); + break; + case ATT_OP_READ_REQUEST: // read descriptor or characteristic value + case ATT_OP_READ_BLOB_REQUEST: // read long descriptor or characteristic + case ATT_OP_WRITE_REQUEST: // write descriptor or characteristic + { + uint handleData = currentRequest.reference.toUInt(); + const QLowEnergyHandle charHandle = (handleData & 0xffff); + const QLowEnergyHandle descriptorHandle = ((handleData >> 16) & 0xffff); + processReply(currentRequest, createRequestErrorMessage(command, + descriptorHandle ? descriptorHandle : charHandle)); + } + break; + case ATT_OP_FIND_INFORMATION_REQUEST: // get descriptor information + processReply(currentRequest, createRequestErrorMessage( + command, currentRequest.reference2.toUInt())); + break; + case ATT_OP_PREPARE_WRITE_REQUEST: // prepare to write long desc or char + case ATT_OP_EXECUTE_WRITE_REQUEST: // execute long write of desc or char + { + uint handleData = currentRequest.reference.toUInt(); + const QLowEnergyHandle attrHandle = (handleData & 0xffff); + processReply(currentRequest, + createRequestErrorMessage(command, attrHandle)); + } + break; + default: + // not a command used by central role implementation + return; + } + } } QLowEnergyControllerPrivate::~QLowEnergyControllerPrivate() @@ -537,6 +639,19 @@ void QLowEnergyControllerPrivate::resetController() receivedMtuExchangeRequest = false; securityLevelValue = -1; connectionHandle = 0; + + // public API behavior requires stop of advertisement + if (role == QLowEnergyController::PeripheralRole && advertiser) + advertiser->stopAdvertising(); +} + +void QLowEnergyControllerPrivate::restartRequestTimer() +{ + if (!requestTimer) + return; + + if (gattRequestTimeout > 0) + requestTimer->start(gattRequestTimeout); } void QLowEnergyControllerPrivate::l2cpReadyRead() @@ -564,7 +679,8 @@ void QLowEnergyControllerPrivate::l2cpReadyRead() processUnsolicitedReply(incomingPacket); return; } - + //-------------------------------------------------- + // Peripheral side packet handling case ATT_OP_EXCHANGE_MTU_REQUEST: handleExchangeMtuRequest(incomingPacket); return; @@ -608,6 +724,7 @@ void QLowEnergyControllerPrivate::l2cpReadyRead() qCWarning(QT_BT_BLUEZ) << "received unexpected handle value confirmation"; } return; + //-------------------------------------------------- default: //only solicited replies finish pending requests requestPending = false; @@ -713,6 +830,7 @@ void QLowEnergyControllerPrivate::sendNextPendingRequest() // << request.payload.toHex(); requestPending = true; + restartRequestTimer(); sendPacket(request.payload); } @@ -1920,8 +2038,10 @@ bool QLowEnergyControllerPrivate::increaseEncryptLevelfRequired(quint8 errorCode return false; if (securityLevelValue != BT_SECURITY_HIGH) { qCDebug(QT_BT_BLUEZ) << "Requesting encrypted link"; - if (setSecurityLevel(BT_SECURITY_HIGH)) + if (setSecurityLevel(BT_SECURITY_HIGH)) { + restartRequestTimer(); return true; + } } break; default: diff --git a/src/bluetooth/qlowenergycontroller_osx.mm b/src/bluetooth/qlowenergycontroller_osx.mm index 47f65965..a2923d81 100644 --- a/src/bluetooth/qlowenergycontroller_osx.mm +++ b/src/bluetooth/qlowenergycontroller_osx.mm @@ -38,11 +38,14 @@ ** ****************************************************************************/ +#include "osx/osxbtnotifier_p.h" #include "osx/osxbtutility_p.h" #include "osx/uistrings_p.h" + #include "qlowenergyserviceprivate_p.h" #include "qlowenergycontroller_osx_p.h" +#include "qlowenergyservicedata.h" #include "qbluetoothlocaldevice.h" #include "qbluetoothdeviceinfo.h" #include "qlowenergycontroller.h" @@ -51,7 +54,6 @@ #include <QtCore/qloggingcategory.h> #include <QtCore/qsharedpointer.h> #include <QtCore/qbytearray.h> -#include <QtCore/qsysinfo.h> #include <QtCore/qglobal.h> #include <QtCore/qstring.h> #include <QtCore/qlist.h> @@ -84,8 +86,7 @@ ServicePrivate qt_createLEService(QLowEnergyControllerPrivateOSX *controller, CB CBUUID *const cbUuid = cbService.UUID; if (!cbUuid) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "invalid service, " - "UUID is nil"; + qCDebug(QT_BT_OSX) << "invalid service, UUID is nil"; return ServicePrivate(); } @@ -102,18 +103,12 @@ ServicePrivate qt_createLEService(QLowEnergyControllerPrivateOSX *controller, CB // TODO: isPrimary is ... always 'NO' - to be investigated. /* - #if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_6_0) - using OSXBluetooth::qt_OS_limit; - if (QSysInfo::MacintoshVersion >= qt_OS_limit(QSysInfo::MV_10_9, QSysInfo::MV_IOS_6_0)) { - if (!cbService.isPrimary) { - // Our guess included/not was probably wrong. - newService->type &= ~QLowEnergyService::PrimaryService; - newService->type |= QLowEnergyService::IncludedService; - } + if (!cbService.isPrimary) { + // Our guess included/not was probably wrong. + newService->type &= ~QLowEnergyService::PrimaryService; + newService->type |= QLowEnergyService::IncludedService; } - #endif */ - // No such property before 10_9/6_0. return newService; } @@ -136,36 +131,8 @@ UUIDList qt_servicesUuids(NSArray *services) } -QLowEnergyControllerPrivateOSX::QLowEnergyControllerPrivateOSX(QLowEnergyController *q) - : q_ptr(q), - lastError(QLowEnergyController::NoError), - controllerState(QLowEnergyController::UnconnectedState), - addressType(QLowEnergyController::PublicAddress) -{ - registerQLowEnergyControllerMetaType(); - - // This is the "wrong" constructor - no valid device UUID to connect later. - Q_ASSERT_X(q, Q_FUNC_INFO, "invalid q_ptr (null)"); - - using OSXBluetooth::LECentralNotifier; - - // We still create a manager, to simplify error handling later. - QScopedPointer<LECentralNotifier> notifier(new LECentralNotifier); - centralManager.reset([[ObjCCentralManager alloc] initWith:notifier.data()]); - if (!centralManager) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO - << "failed to initialize central manager"; - return; - } else if (!connectSlots(notifier.data())) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO - << "failed to connect to notifier's signals"; - } - - // Ownership was taken by central manager. - notifier.take(); -} - -QLowEnergyControllerPrivateOSX::QLowEnergyControllerPrivateOSX(QLowEnergyController *q, +QLowEnergyControllerPrivateOSX::QLowEnergyControllerPrivateOSX(QLowEnergyController::Role r, + QLowEnergyController *q, const QBluetoothDeviceInfo &deviceInfo) : q_ptr(q), deviceUuid(deviceInfo.deviceUuid()), @@ -178,44 +145,67 @@ QLowEnergyControllerPrivateOSX::QLowEnergyControllerPrivateOSX(QLowEnergyControl Q_ASSERT_X(q, Q_FUNC_INFO, "invalid q_ptr (null)"); - using OSXBluetooth::LECentralNotifier; + using OSXBluetooth::LECBManagerNotifier; + + role = r; - QScopedPointer<LECentralNotifier> notifier(new LECentralNotifier); - centralManager.reset([[ObjCCentralManager alloc] initWith:notifier.data()]); - if (!centralManager) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO - << "failed to initialize central manager"; + QScopedPointer<LECBManagerNotifier> notifier(new LECBManagerNotifier); + if (role == QLowEnergyController::PeripheralRole) { +#ifndef Q_OS_TVOS + peripheralManager.reset([[ObjCPeripheralManager alloc] initWith:notifier.data()]); + if (!peripheralManager) { + qCWarning(QT_BT_OSX) << "failed to initialize peripheral manager"; + return; + } +#else + qCWarning(QT_BT_OSX) << "peripheral role is not supported on your platform"; return; - } else if (!connectSlots(notifier.data())) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO - << "failed to connect to notifier's signals"; +#endif + } else { + centralManager.reset([[ObjCCentralManager alloc] initWith:notifier.data()]); + if (!centralManager) { + qCWarning(QT_BT_OSX) << "failed to initialize central manager"; + return; + } } + if (!connectSlots(notifier.data())) { + qCWarning(QT_BT_OSX) << "failed to connect to notifier's signal(s)"; + } // Ownership was taken by central manager. notifier.take(); } QLowEnergyControllerPrivateOSX::~QLowEnergyControllerPrivateOSX() { - // TODO: dispatch_sync 'setDelegate:Q_NULLPRT' to our CBCentralManager's delegate. - if (dispatch_queue_t leQueue = OSXBluetooth::qt_LE_queue()) { - ObjCCentralManager *manager = centralManager.data(); - dispatch_sync(leQueue, ^{ - [manager detach]; - }); + if (const auto leQueue = OSXBluetooth::qt_LE_queue()) { + if (role == QLowEnergyController::CentralRole) { + const auto manager = centralManager.data(); + dispatch_sync(leQueue, ^{ + [manager detach]; + }); + } else { +#ifndef Q_OS_TVOS + const auto manager = peripheralManager.data(); + dispatch_sync(leQueue, ^{ + [manager detach]; + }); +#endif + } } } bool QLowEnergyControllerPrivateOSX::isValid() const { +#ifdef Q_OS_TVOS return centralManager; +#else + return centralManager || peripheralManager; +#endif } void QLowEnergyControllerPrivateOSX::_q_connected() { - Q_ASSERT_X(controllerState == QLowEnergyController::ConnectingState, - Q_FUNC_INFO, "invalid state"); - controllerState = QLowEnergyController::ConnectedState; emit q_ptr->stateChanged(QLowEnergyController::ConnectedState); @@ -226,10 +216,11 @@ void QLowEnergyControllerPrivateOSX::_q_disconnected() { controllerState = QLowEnergyController::UnconnectedState; - invalidateServices(); + if (role == QLowEnergyController::CentralRole) + invalidateServices(); + emit q_ptr->stateChanged(QLowEnergyController::UnconnectedState); emit q_ptr->disconnected(); - } void QLowEnergyControllerPrivateOSX::_q_serviceDiscoveryFinished() @@ -258,7 +249,7 @@ void QLowEnergyControllerPrivateOSX::_q_serviceDiscoveryFinished() continue; if (discoveredServices.contains(newService->uuid)) { // It's a bit stupid we first created it ... - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "discovered service with a duplicated UUID " + qCDebug(QT_BT_OSX) << "discovered service with a duplicated UUID" << newService->uuid; continue; } @@ -310,7 +301,7 @@ void QLowEnergyControllerPrivateOSX::_q_serviceDiscoveryFinished() toVisitNext.resetWithoutRetain([[NSMutableArray alloc] init]); } } else { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "no services found"; + qCDebug(QT_BT_OSX) << "no services found"; } for (ServiceMap::const_iterator it = discoveredServices.constBegin(); it != discoveredServices.constEnd(); ++it) { @@ -332,7 +323,7 @@ void QLowEnergyControllerPrivateOSX::_q_serviceDetailsDiscoveryFinished(QSharedP Q_ASSERT(service); if (!discoveredServices.contains(service->uuid)) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "unknown service uuid: " + qCDebug(QT_BT_OSX) << "unknown service uuid:" << service->uuid; return; } @@ -357,7 +348,7 @@ void QLowEnergyControllerPrivateOSX::_q_characteristicRead(QLowEnergyHandle char QLowEnergyCharacteristic characteristic(characteristicForHandle(charHandle)); if (!characteristic.isValid()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unknown characteristic"; + qCWarning(QT_BT_OSX) << "unknown characteristic"; return; } @@ -374,14 +365,14 @@ void QLowEnergyControllerPrivateOSX::_q_characteristicWritten(QLowEnergyHandle c ServicePrivate service(serviceForHandle(charHandle)); if (service.isNull()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "can not find service for characteristic handle " + qCWarning(QT_BT_OSX) << "can not find service for characteristic handle" << charHandle; return; } QLowEnergyCharacteristic characteristic(characteristicForHandle(charHandle)); if (!characteristic.isValid()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unknown characteristic"; + qCWarning(QT_BT_OSX) << "unknown characteristic"; return; } @@ -410,7 +401,7 @@ void QLowEnergyControllerPrivateOSX::_q_characteristicUpdated(QLowEnergyHandle c QLowEnergyCharacteristic characteristic(characteristicForHandle(charHandle)); if (!characteristic.isValid()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unknown characteristic"; + qCWarning(QT_BT_OSX) << "unknown characteristic"; return; } @@ -427,7 +418,7 @@ void QLowEnergyControllerPrivateOSX::_q_descriptorRead(QLowEnergyHandle dHandle, const QLowEnergyDescriptor qtDescriptor(descriptorForHandle(dHandle)); if (!qtDescriptor.isValid()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unknown descriptor " << dHandle; + qCWarning(QT_BT_OSX) << "unknown descriptor" << dHandle; return; } @@ -443,7 +434,7 @@ void QLowEnergyControllerPrivateOSX::_q_descriptorWritten(QLowEnergyHandle dHand const QLowEnergyDescriptor qtDescriptor(descriptorForHandle(dHandle)); if (!qtDescriptor.isValid()) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unknown descriptor " << dHandle; + qCWarning(QT_BT_OSX) << "unknown descriptor" << dHandle; return; } @@ -453,6 +444,49 @@ void QLowEnergyControllerPrivateOSX::_q_descriptorWritten(QLowEnergyHandle dHand emit service->descriptorWritten(qtDescriptor, value); } +void QLowEnergyControllerPrivateOSX::_q_notificationEnabled(QLowEnergyHandle charHandle, + bool enabled) +{ + // CoreBluetooth in peripheral role does not allow mutable descriptors, + // in central we can only call setNotification:enabled/disabled. + // But from Qt API's point of view, a central has to write into + // client characteristic configuration descriptor. So here we emulate + // such a write (we cannot say if it's a notification or indication and + // report as both). + + Q_ASSERT_X(role == QLowEnergyController::PeripheralRole, Q_FUNC_INFO, + "controller has an invalid role, 'peripheral' expected"); + Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); + + const QLowEnergyCharacteristic qtChar(characteristicForHandle(charHandle)); + if (!qtChar.isValid()) { + qCWarning(QT_BT_OSX) << "unknown characteristic" << charHandle; + return; + } + + const QLowEnergyDescriptor qtDescriptor = + qtChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); + if (!qtDescriptor.isValid()) { + qCWarning(QT_BT_OSX) << "characteristic" << charHandle + << "does not have a client characteristic " + "descriptor"; + return; + } + + ServicePrivate service(serviceForHandle(charHandle)); + if (service.data()) { + // It's a 16-bit value, the least significant bit is for notifications, + // the next one - for indications (thus 1 means notifications enabled, + // 2 - indications enabled). + // 3 is the maximum value and it means both enabled. + QByteArray value(2, 0); + if (enabled) + value[0] = 3; + updateValueOfDescriptor(charHandle, qtDescriptor.handle(), value, false); + emit service->descriptorWritten(qtDescriptor, value); + } +} + void QLowEnergyControllerPrivateOSX::_q_LEnotSupported() { // Report as an error. But this should not be possible @@ -461,7 +495,7 @@ void QLowEnergyControllerPrivateOSX::_q_LEnotSupported() // be supported. } -void QLowEnergyControllerPrivateOSX::_q_CBCentralManagerError(QLowEnergyController::Error errorCode) +void QLowEnergyControllerPrivateOSX::_q_CBManagerError(QLowEnergyController::Error errorCode) { // Errors reported during connect and general errors. @@ -478,8 +512,8 @@ void QLowEnergyControllerPrivateOSX::_q_CBCentralManagerError(QLowEnergyControll // a service/characteristic - related error. } -void QLowEnergyControllerPrivateOSX::_q_CBCentralManagerError(const QBluetoothUuid &serviceUuid, - QLowEnergyController::Error errorCode) +void QLowEnergyControllerPrivateOSX::_q_CBManagerError(const QBluetoothUuid &serviceUuid, + QLowEnergyController::Error errorCode) { // Errors reported while discovering service details etc. Q_UNUSED(errorCode) // TODO: setError? @@ -489,16 +523,16 @@ void QLowEnergyControllerPrivateOSX::_q_CBCentralManagerError(const QBluetoothUu ServicePrivate qtService(discoveredServices.value(serviceUuid)); qtService->setState(QLowEnergyService::InvalidService); } else { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "error reported for unknown service " + qCDebug(QT_BT_OSX) << "error reported for unknown service" << serviceUuid; } } -void QLowEnergyControllerPrivateOSX::_q_CBCentralManagerError(const QBluetoothUuid &serviceUuid, - QLowEnergyService::ServiceError errorCode) +void QLowEnergyControllerPrivateOSX::_q_CBManagerError(const QBluetoothUuid &serviceUuid, + QLowEnergyService::ServiceError errorCode) { if (!discoveredServices.contains(serviceUuid)) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "unknown service uuid: " + qCDebug(QT_BT_OSX) << "unknown service uuid:" << serviceUuid; return; } @@ -514,10 +548,12 @@ void QLowEnergyControllerPrivateOSX::connectToDevice() Q_FUNC_INFO, "invalid state"); Q_ASSERT_X(!deviceUuid.isNull(), Q_FUNC_INFO, "invalid private controller (no device uuid)"); + Q_ASSERT_X(role != QLowEnergyController::PeripheralRole, + Q_FUNC_INFO, "invalid role (peripheral)"); dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); if (!leQueue) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no LE queue found"; + qCWarning(QT_BT_OSX) << "no LE queue found"; setErrorDescription(QLowEnergyController::UnknownError); return; } @@ -537,10 +573,12 @@ void QLowEnergyControllerPrivateOSX::discoverServices() Q_ASSERT_X(isValid(), Q_FUNC_INFO, "invalid private controller"); Q_ASSERT_X(controllerState != QLowEnergyController::UnconnectedState, Q_FUNC_INFO, "not connected to peripheral"); + Q_ASSERT_X(role != QLowEnergyController::PeripheralRole, + Q_FUNC_INFO, "invalid role (peripheral)"); dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); if (!leQueue) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no LE queue found"; + qCWarning(QT_BT_OSX) << "no LE queue found"; setErrorDescription(QLowEnergyController::UnknownError); return; } @@ -559,20 +597,21 @@ void QLowEnergyControllerPrivateOSX::discoverServiceDetails(const QBluetoothUuid Q_ASSERT_X(isValid(), Q_FUNC_INFO, "invalid private controller"); if (controllerState != QLowEnergyController::DiscoveredState) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO - << "can not discover service details in the current state, " - << "QLowEnergyController::DiscoveredState is expected"; + // This will also exclude peripheral role, since controller + // can never be in discovered state ... + qCWarning(QT_BT_OSX) << "can not discover service details in the current state, " + "QLowEnergyController::DiscoveredState is expected"; return; } if (!discoveredServices.contains(serviceUuid)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unknown service: " << serviceUuid; + qCWarning(QT_BT_OSX) << "unknown service: " << serviceUuid; return; } dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); if (!leQueue) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no LE queue found"; + qCWarning(QT_BT_OSX) << "no LE queue found"; return; } @@ -593,32 +632,37 @@ void QLowEnergyControllerPrivateOSX::setNotifyValue(QSharedPointer<QLowEnergySer Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); Q_ASSERT_X(isValid(), Q_FUNC_INFO, "invalid controller"); + if (role == QLowEnergyController::PeripheralRole) { + qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; + service->setError(QLowEnergyService::DescriptorWriteError); + return; + } + if (newValue.size() > 2) { // Qt's API requires an error on such write. // With Core Bluetooth we do not write any descriptor, // but instead call a special method. So it's better to // intercept wrong data size here: - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "client characteristic configuration descriptor " + qCWarning(QT_BT_OSX) << "client characteristic configuration descriptor" "is 2 bytes, but value size is: " << newValue.size(); service->setError(QLowEnergyService::DescriptorWriteError); return; } if (!discoveredServices.contains(service->uuid)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no service with uuid: " - << service->uuid << " found"; + qCWarning(QT_BT_OSX) << "no service with uuid:" << service->uuid << "found"; return; } if (!service->characteristicList.contains(charHandle)) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "no characteristic with handle: " - << charHandle << " found"; + qCDebug(QT_BT_OSX) << "no characteristic with handle:" + << charHandle << "found"; return; } dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); if (!leQueue) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no LE queue found"; + qCWarning(QT_BT_OSX) << "no LE queue found"; return; } ObjCCentralManager *manager = centralManager.data(); @@ -637,21 +681,26 @@ void QLowEnergyControllerPrivateOSX::readCharacteristic(QSharedPointer<QLowEnerg Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); Q_ASSERT_X(isValid(), Q_FUNC_INFO, "invalid controller"); + if (role == QLowEnergyController::PeripheralRole) { + qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; + return; + } + if (!discoveredServices.contains(service->uuid)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no service with uuid:" + qCWarning(QT_BT_OSX) << "no service with uuid:" << service->uuid << "found"; return; } if (!service->characteristicList.contains(charHandle)) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "no characteristic with handle:" + qCDebug(QT_BT_OSX) << "no characteristic with handle:" << charHandle << "found"; return; } dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); if (!leQueue) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no LE queue found"; + qCWarning(QT_BT_OSX) << "no LE queue found"; return; } // Attention! We have to copy UUID. @@ -669,36 +718,47 @@ void QLowEnergyControllerPrivateOSX::writeCharacteristic(QSharedPointer<QLowEner Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); Q_ASSERT_X(isValid(), Q_FUNC_INFO, "invalid controller"); - // We can work only with services, found on a given peripheral - // (== created by the given LE controller), - // otherwise we can not write anything at all. + // We can work only with services found on a given peripheral + // (== created by the given LE controller). + if (!discoveredServices.contains(service->uuid)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no service with uuid: " + qCWarning(QT_BT_OSX) << "no service with uuid:" << service->uuid << " found"; return; } if (!service->characteristicList.contains(charHandle)) { - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "no characteristic with handle: " + qCDebug(QT_BT_OSX) << "no characteristic with handle:" << charHandle << " found"; return; } dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); if (!leQueue) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no LE queue found"; + qCWarning(QT_BT_OSX) << "no LE queue found"; return; } - // Attention! Copy objects! - const QBluetoothUuid serviceUuid(service->uuid); + // Attention! We have to copy objects! const QByteArray newValueCopy(newValue); - ObjCCentralManager *const manager = centralManager.data(); - dispatch_async(leQueue, ^{ - [manager write:newValueCopy - charHandle:charHandle + if (role == QLowEnergyController::CentralRole) { + const QBluetoothUuid serviceUuid(service->uuid); + const auto manager = centralManager.data(); + dispatch_async(leQueue, ^{ + [manager write:newValueCopy + charHandle:charHandle onService:serviceUuid withResponse:mode == QLowEnergyService::WriteWithResponse]; - }); + }); + } else { +#ifndef Q_OS_TVOS + const auto manager = peripheralManager.data(); + dispatch_async(leQueue, ^{ + [manager write:newValueCopy charHandle:charHandle]; + }); +#else + qCWarning(QT_BT_OSX) << "peripheral role is not supported on your platform"; +#endif + } } quint16 QLowEnergyControllerPrivateOSX::updateValueOfCharacteristic(QLowEnergyHandle charHandle, @@ -728,15 +788,20 @@ void QLowEnergyControllerPrivateOSX::readDescriptor(QSharedPointer<QLowEnergySer Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); Q_ASSERT_X(isValid(), Q_FUNC_INFO, "invalid controller"); + if (role == QLowEnergyController::PeripheralRole) { + qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; + return; + } + if (!discoveredServices.contains(service->uuid)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no service with uuid:" + qCWarning(QT_BT_OSX) << "no service with uuid:" << service->uuid << "found"; return; } dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); if (!leQueue) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no LE queue found"; + qCWarning(QT_BT_OSX) << "no LE queue found"; return; } // Attention! Copy objects! @@ -755,18 +820,23 @@ void QLowEnergyControllerPrivateOSX::writeDescriptor(QSharedPointer<QLowEnergySe Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); Q_ASSERT_X(isValid(), Q_FUNC_INFO, "invalid controller"); + if (role == QLowEnergyController::PeripheralRole) { + qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; + return; + } + // We can work only with services found on a given peripheral // (== created by the given LE controller), // otherwise we can not write anything at all. if (!discoveredServices.contains(service->uuid)) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no service with uuid: " + qCWarning(QT_BT_OSX) << "no service with uuid:" << service->uuid << " found"; return; } dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); if (!leQueue) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no LE queue found"; + qCWarning(QT_BT_OSX) << "no LE queue found"; return; } // Attention! Copy objects! @@ -868,17 +938,23 @@ void QLowEnergyControllerPrivateOSX::setErrorDescription(QLowEnergyController::E errorString.clear(); break; case QLowEnergyController::UnknownRemoteDeviceError: - errorString = QCoreApplication::translate(LE_CONTROLLER, LEC_RDEV_NO_FOUND); + errorString = QLowEnergyController::tr("Remote device cannot be found"); break; case QLowEnergyController::InvalidBluetoothAdapterError: - errorString = QCoreApplication::translate(LE_CONTROLLER, LEC_NO_LOCAL_DEV); + errorString = QLowEnergyController::tr("Cannot find local adapter"); break; case QLowEnergyController::NetworkError: - errorString = QCoreApplication::translate(LE_CONTROLLER, LEC_IO_ERROR); + errorString = QLowEnergyController::tr("Error occurred during connection I/O"); + break; + case QLowEnergyController::ConnectionError: + errorString = QLowEnergyController::tr("Error occurred trying to connect to remote device."); + break; + case QLowEnergyController::AdvertisingError: + errorString = QLowEnergyController::tr("Error occurred trying to start advertising"); break; case QLowEnergyController::UnknownError: default: - errorString = QCoreApplication::translate(LE_CONTROLLER, LEC_UNKNOWN_ERROR); + errorString = QLowEnergyController::tr("Unknown Error"); break; } } @@ -893,38 +969,40 @@ void QLowEnergyControllerPrivateOSX::invalidateServices() discoveredServices.clear(); } -bool QLowEnergyControllerPrivateOSX::connectSlots(OSXBluetooth::LECentralNotifier *notifier) +bool QLowEnergyControllerPrivateOSX::connectSlots(OSXBluetooth::LECBManagerNotifier *notifier) { - using OSXBluetooth::LECentralNotifier; + using OSXBluetooth::LECBManagerNotifier; Q_ASSERT_X(notifier, Q_FUNC_INFO, "invalid notifier object (null)"); - bool ok = connect(notifier, &LECentralNotifier::connected, + bool ok = connect(notifier, &LECBManagerNotifier::connected, this, &QLowEnergyControllerPrivateOSX::_q_connected); - ok = ok && connect(notifier, &LECentralNotifier::disconnected, + ok = ok && connect(notifier, &LECBManagerNotifier::disconnected, this, &QLowEnergyControllerPrivateOSX::_q_disconnected); - ok = ok && connect(notifier, &LECentralNotifier::serviceDiscoveryFinished, + ok = ok && connect(notifier, &LECBManagerNotifier::serviceDiscoveryFinished, this, &QLowEnergyControllerPrivateOSX::_q_serviceDiscoveryFinished); - ok = ok && connect(notifier, &LECentralNotifier::serviceDetailsDiscoveryFinished, + ok = ok && connect(notifier, &LECBManagerNotifier::serviceDetailsDiscoveryFinished, this, &QLowEnergyControllerPrivateOSX::_q_serviceDetailsDiscoveryFinished); - ok = ok && connect(notifier, &LECentralNotifier::characteristicRead, + ok = ok && connect(notifier, &LECBManagerNotifier::characteristicRead, this, &QLowEnergyControllerPrivateOSX::_q_characteristicRead); - ok = ok && connect(notifier, &LECentralNotifier::characteristicWritten, + ok = ok && connect(notifier, &LECBManagerNotifier::characteristicWritten, this, &QLowEnergyControllerPrivateOSX::_q_characteristicWritten); - ok = ok && connect(notifier, &LECentralNotifier::characteristicUpdated, + ok = ok && connect(notifier, &LECBManagerNotifier::characteristicUpdated, this, &QLowEnergyControllerPrivateOSX::_q_characteristicUpdated); - ok = ok && connect(notifier, &LECentralNotifier::descriptorRead, + ok = ok && connect(notifier, &LECBManagerNotifier::descriptorRead, this, &QLowEnergyControllerPrivateOSX::_q_descriptorRead); - ok = ok && connect(notifier, &LECentralNotifier::descriptorWritten, + ok = ok && connect(notifier, &LECBManagerNotifier::descriptorWritten, this, &QLowEnergyControllerPrivateOSX::_q_descriptorWritten); - ok = ok && connect(notifier, &LECentralNotifier::LEnotSupported, + ok = ok && connect(notifier, &LECBManagerNotifier::notificationEnabled, + this, &QLowEnergyControllerPrivateOSX::_q_notificationEnabled); + ok = ok && connect(notifier, &LECBManagerNotifier::LEnotSupported, this, &QLowEnergyControllerPrivateOSX::_q_LEnotSupported); - ok = ok && connect(notifier, SIGNAL(CBCentralManagerError(QLowEnergyController::Error)), - this, SLOT(_q_CBCentralManagerError(QLowEnergyController::Error))); - ok = ok && connect(notifier, SIGNAL(CBCentralManagerError(const QBluetoothUuid &, QLowEnergyController::Error)), - this, SLOT(_q_CBCentralManagerError(const QBluetoothUuid &, QLowEnergyController::Error))); - ok = ok && connect(notifier, SIGNAL(CBCentralManagerError(const QBluetoothUuid &, QLowEnergyService::ServiceError)), - this, SLOT(_q_CBCentralManagerError(const QBluetoothUuid &, QLowEnergyService::ServiceError))); + ok = ok && connect(notifier, SIGNAL(CBManagerError(QLowEnergyController::Error)), + this, SLOT(_q_CBManagerError(QLowEnergyController::Error))); + ok = ok && connect(notifier, SIGNAL(CBManagerError(const QBluetoothUuid &, QLowEnergyController::Error)), + this, SLOT(_q_CBManagerError(const QBluetoothUuid &, QLowEnergyController::Error))); + ok = ok && connect(notifier, SIGNAL(CBManagerError(const QBluetoothUuid &, QLowEnergyService::ServiceError)), + this, SLOT(_q_CBManagerError(const QBluetoothUuid &, QLowEnergyService::ServiceError))); if (!ok) notifier->disconnect(); @@ -935,26 +1013,24 @@ bool QLowEnergyControllerPrivateOSX::connectSlots(OSXBluetooth::LECentralNotifie QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddress, QObject *parent) : QObject(parent), - d_ptr(new QLowEnergyControllerPrivateOSX(this)) + d_ptr(new QLowEnergyControllerPrivateOSX(CentralRole, this)) { OSX_D_PTR; - osx_d_ptr->role = CentralRole; osx_d_ptr->remoteAddress = remoteAddress; osx_d_ptr->localAddress = QBluetoothLocalDevice().address(); - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "construction with remote address " + qCWarning(QT_BT_OSX) << "construction with remote address " "is not supported!"; } QLowEnergyController::QLowEnergyController(const QBluetoothDeviceInfo &remoteDevice, QObject *parent) : QObject(parent), - d_ptr(new QLowEnergyControllerPrivateOSX(this, remoteDevice)) + d_ptr(new QLowEnergyControllerPrivateOSX(CentralRole, this, remoteDevice)) { OSX_D_PTR; - osx_d_ptr->role = CentralRole; osx_d_ptr->localAddress = QBluetoothLocalDevice().address(); // That's the only "real" ctor - with Core Bluetooth we need a _valid_ deviceUuid // from 'remoteDevice'. @@ -964,24 +1040,23 @@ QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddres const QBluetoothAddress &localAddress, QObject *parent) : QObject(parent), - d_ptr(new QLowEnergyControllerPrivateOSX(this)) + d_ptr(new QLowEnergyControllerPrivateOSX(CentralRole, this)) { OSX_D_PTR; - osx_d_ptr->role = CentralRole; osx_d_ptr->remoteAddress = remoteAddress; osx_d_ptr->localAddress = localAddress; - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "construction with remote/local " + qCWarning(QT_BT_OSX) << "construction with remote/local " "addresses is not supported!"; } QLowEnergyController::QLowEnergyController(QObject *parent) - : QObject(parent), d_ptr(new QLowEnergyControllerPrivateOSX(this)) + : QObject(parent), + d_ptr(new QLowEnergyControllerPrivateOSX(PeripheralRole, this)) { OSX_D_PTR; - osx_d_ptr->role = PeripheralRole; osx_d_ptr->localAddress = QBluetoothLocalDevice().address(); } @@ -1023,6 +1098,13 @@ QBluetoothAddress QLowEnergyController::remoteAddress() const return osx_d_ptr->remoteAddress; } +QBluetoothUuid QLowEnergyController::remoteDeviceUuid() const +{ + OSX_D_PTR; + + return osx_d_ptr->deviceUuid; +} + QString QLowEnergyController::remoteName() const { OSX_D_PTR; @@ -1059,11 +1141,16 @@ void QLowEnergyController::connectToDevice() // A memory allocation problem. if (!osx_d_ptr->isValid()) - return osx_d_ptr->_q_CBCentralManagerError(UnknownError); + return osx_d_ptr->_q_CBManagerError(UnknownError); + + if (role() == PeripheralRole) { + qCWarning(QT_BT_OSX) << "can not connect in peripheral role"; + return osx_d_ptr->_q_CBManagerError(ConnectionError); + } // No QBluetoothDeviceInfo provided during construction. if (osx_d_ptr->deviceUuid.isNull()) - return osx_d_ptr->_q_CBCentralManagerError(UnknownRemoteDeviceError); + return osx_d_ptr->_q_CBManagerError(UnknownRemoteDeviceError); if (osx_d_ptr->controllerState != UnconnectedState) return; @@ -1078,6 +1165,13 @@ void QLowEnergyController::disconnectFromDevice() OSX_D_PTR; + if (role() == PeripheralRole) { + // CoreBluetooth API intentionally does not provide any way of closing + // a connection. All we can do here is to stop the advertisement. + stopAdvertising(); + return; + } + if (osx_d_ptr->isValid()) { const ControllerState oldState = osx_d_ptr->controllerState; @@ -1101,14 +1195,19 @@ void QLowEnergyController::disconnectFromDevice() emit stateChanged(UnconnectedState); } } else { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "qt LE queue is nil," - << "can not dispatch 'disconnect'"; + qCCritical(QT_BT_OSX) << "qt LE queue is nil, " + "can not dispatch 'disconnect'"; } } } void QLowEnergyController::discoverServices() { + if (role() == PeripheralRole) { + qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; + return; + } + if (state() != ConnectedState) return; @@ -1159,30 +1258,126 @@ void QLowEnergyController::startAdvertising(const QLowEnergyAdvertisingParameter const QLowEnergyAdvertisingData &advertisingData, const QLowEnergyAdvertisingData &scanResponseData) { - Q_UNUSED(params); - Q_UNUSED(advertisingData); - Q_UNUSED(scanResponseData); - qCWarning(QT_BT_OSX) << "LE advertising not implemented for OS X"; +#ifdef Q_OS_TVOS + Q_UNUSED(params) + Q_UNUSED(advertisingData) + Q_UNUSED(scanResponseData) + qCWarning(QT_BT_OSX) << "advertising is not supported on your platform"; +#else + OSX_D_PTR; + + if (!osx_d_ptr->isValid()) + return osx_d_ptr->_q_CBManagerError(UnknownError); + + if (role() != PeripheralRole) { + qCWarning(QT_BT_OSX) << "invalid role"; + return; + } + + if (state() != UnconnectedState) { + qCWarning(QT_BT_OSX) << "invalid state" << state(); + return; + } + + auto leQueue(OSXBluetooth::qt_LE_queue()); + if (!leQueue) { + qCWarning(QT_BT_OSX) << "no LE queue found"; + osx_d_ptr->setErrorDescription(QLowEnergyController::UnknownError); + return; + } + + [osx_d_ptr->peripheralManager setParameters:params + data:advertisingData + scanResponse:scanResponseData]; + + osx_d_ptr->controllerState = AdvertisingState; + emit stateChanged(AdvertisingState); + + const auto manager = osx_d_ptr->peripheralManager.data(); + dispatch_async(leQueue, ^{ + [manager startAdvertising]; + }); +#endif } void QLowEnergyController::stopAdvertising() { - qCWarning(QT_BT_OSX) << "LE advertising not implemented for OS X"; +#ifdef Q_OS_TVOS + qCWarning(QT_BT_OSX) << "advertising is not supported on your platform"; +#else + OSX_D_PTR; + + if (!osx_d_ptr->isValid()) + return osx_d_ptr->_q_CBManagerError(UnknownError); + + if (state() != AdvertisingState) { + qCDebug(QT_BT_OSX) << "cannot stop advertising, called in state" << state(); + return; + } + + if (const auto leQueue = OSXBluetooth::qt_LE_queue()) { + const auto manager = osx_d_ptr->peripheralManager.data(); + dispatch_sync(leQueue, ^{ + [manager stopAdvertising]; + }); + + osx_d_ptr->controllerState = UnconnectedState; + emit stateChanged(UnconnectedState); + } else { + qCWarning(QT_BT_OSX) << "no LE queue found"; + osx_d_ptr->setErrorDescription(QLowEnergyController::UnknownError); + return; + } +#endif } -QLowEnergyService *QLowEnergyController::addService(const QLowEnergyServiceData &service, +QLowEnergyService *QLowEnergyController::addService(const QLowEnergyServiceData &data, QObject *parent) { - Q_UNUSED(service); - Q_UNUSED(parent); - qCWarning(QT_BT_OSX) << "GATT server functionality not implemented for OS X"; +#ifdef Q_OS_TVOS + Q_UNUSED(data) + Q_UNUSED(parent) + qCWarning(QT_BT_OSX) << "peripheral role is not supported on your platform"; +#else + OSX_D_PTR; + + if (!osx_d_ptr->isValid()) { + osx_d_ptr->_q_CBManagerError(UnknownError); + return nullptr; + } + + if (role() != PeripheralRole) { + qCWarning(QT_BT_OSX) << "not in peripheral role"; + return nullptr; + } + + if (state() != UnconnectedState) { + qCWarning(QT_BT_OSX) << "invalid state"; + return nullptr; + } + + if (!data.isValid()) { + qCWarning(QT_BT_OSX) << "invalid service"; + return nullptr; + } + + for (auto includedService : data.includedServices()) + includedService->d_ptr->type |= QLowEnergyService::IncludedService; + + if (const auto servicePrivate = [osx_d_ptr->peripheralManager addService:data]) { + servicePrivate->setController(osx_d_ptr); + osx_d_ptr->discoveredServices.insert(servicePrivate->uuid, servicePrivate); + return new QLowEnergyService(servicePrivate, parent); + } +#endif + return nullptr; } void QLowEnergyController::requestConnectionUpdate(const QLowEnergyConnectionParameters ¶ms) { Q_UNUSED(params); - qCWarning(QT_BT_OSX) << "Connection update not implemented for OS X"; + qCWarning(QT_BT_OSX) << "Connection update not implemented on your platform"; } QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycontroller_osx_p.h b/src/bluetooth/qlowenergycontroller_osx_p.h index 853e1f9b..5cceb9e6 100644 --- a/src/bluetooth/qlowenergycontroller_osx_p.h +++ b/src/bluetooth/qlowenergycontroller_osx_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include "osx/osxbtperipheralmanager_p.h" #include "qlowenergyserviceprivate_p.h" #include "osx/osxbtcentralmanager_p.h" #include "qlowenergycontroller_p.h" @@ -61,6 +62,7 @@ #include "qbluetoothuuid.h" #include <QtCore/qsharedpointer.h> +#include <QtCore/qsysinfo.h> #include <QtCore/qglobal.h> #include <QtCore/qstring.h> #include <QtCore/qmap.h> @@ -70,13 +72,13 @@ QT_BEGIN_NAMESPACE namespace OSXBluetooth { -class LECentralNotifier; +class LECBManagerNotifier; } class QByteArray; -// While suffix 'OSX', it's also for iOS. +// Suffix 'OSX' is a legacy, it's also iOS. class QLowEnergyControllerPrivateOSX : public QLowEnergyControllerPrivate { friend class QLowEnergyController; @@ -84,9 +86,8 @@ class QLowEnergyControllerPrivateOSX : public QLowEnergyControllerPrivate Q_OBJECT public: - QLowEnergyControllerPrivateOSX(QLowEnergyController *q); - QLowEnergyControllerPrivateOSX(QLowEnergyController *q, - const QBluetoothDeviceInfo &uuid); + QLowEnergyControllerPrivateOSX(QLowEnergyController::Role role, QLowEnergyController *q, + const QBluetoothDeviceInfo &info = QBluetoothDeviceInfo()); ~QLowEnergyControllerPrivateOSX(); bool isValid() const; @@ -103,20 +104,18 @@ private Q_SLOTS: void _q_characteristicUpdated(QLowEnergyHandle charHandle, const QByteArray &value); void _q_descriptorRead(QLowEnergyHandle descHandle, const QByteArray &value); void _q_descriptorWritten(QLowEnergyHandle charHandle, const QByteArray &value); + void _q_notificationEnabled(QLowEnergyHandle charHandle, bool enabled); void _q_LEnotSupported(); - void _q_CBCentralManagerError(QLowEnergyController::Error error); - void _q_CBCentralManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyController::Error error); - void _q_CBCentralManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyService::ServiceError error); + void _q_CBManagerError(QLowEnergyController::Error error); + void _q_CBManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyController::Error error); + void _q_CBManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyService::ServiceError error); private: void connectToDevice(); void discoverServices(); void discoverServiceDetails(const QBluetoothUuid &serviceUuid); - // TODO: all these read/write /setNotify can be simplified - - // by just passing either characteristic or descriptor (that - // has all needed information - service, handle, etc.). void setNotifyValue(QSharedPointer<QLowEnergyServicePrivate> service, QLowEnergyHandle charHandle, const QByteArray &newValue); @@ -149,7 +148,7 @@ private: void setErrorDescription(QLowEnergyController::Error errorCode); void invalidateServices(); - bool connectSlots(OSXBluetooth::LECentralNotifier *notifier); + bool connectSlots(OSXBluetooth::LECBManagerNotifier *notifier); QLowEnergyController *q_ptr; QBluetoothUuid deviceUuid; @@ -170,6 +169,12 @@ private: typedef OSXBluetooth::ObjCScopedPointer<ObjCCentralManager> CentralManager; CentralManager centralManager; +#ifndef Q_OS_TVOS + typedef QT_MANGLE_NAMESPACE(OSXBTPeripheralManager) ObjCPeripheralManager; + typedef OSXBluetooth::ObjCScopedPointer<ObjCPeripheralManager> PeripheralManager; + PeripheralManager peripheralManager; +#endif + typedef QMap<QBluetoothUuid, QSharedPointer<QLowEnergyServicePrivate> > ServiceMap; typedef ServiceMap::const_iterator ConstServiceIterator; typedef ServiceMap::iterator ServiceIterator; diff --git a/src/bluetooth/qlowenergycontroller_p.h b/src/bluetooth/qlowenergycontroller_p.h index 2256025c..19d10567 100644 --- a/src/bluetooth/qlowenergycontroller_p.h +++ b/src/bluetooth/qlowenergycontroller_p.h @@ -82,6 +82,11 @@ QT_END_NAMESPACE #elif defined(QT_ANDROID_BLUETOOTH) #include <QtAndroidExtras/QAndroidJniObject> #include "android/lowenergynotificationhub_p.h" +#elif defined(QT_WINRT_BLUETOOTH) +#include <wrl.h> +#include <windows.devices.bluetooth.h> + +class QWinRTLowEnergyServiceHandler; #endif #include <functional> @@ -89,6 +94,7 @@ QT_END_NAMESPACE QT_BEGIN_NAMESPACE class QLowEnergyServiceData; +class QTimer; #if defined(QT_BLUEZ_BLUETOOTH) && !defined(QT_BLUEZ_NO_BTLE) class HciManager; @@ -271,6 +277,24 @@ private: HciManager *hciManager; QLeAdvertiser *advertiser; QSocketNotifier *serverSocketNotifier; + QTimer *requestTimer = nullptr; + + /* + Defines the maximum number of milliseconds the implementation will + wait for requests that require a response. + + This addresses the problem that some non-conformant BTLE devices + do not implement the request/response system properly. In such cases + the queue system would hang forever. + + Once timeout has been triggered we gracefully continue with the next request. + Depending on the type of the timed out ATT command we either ignore it + or artifically trigger an error response to ensure the API gives the + appropriate response. Potentially this can cause problems when the + response for the dropped requests arrives very late. That's why a big warning + is printed about the compromised state when a timeout is triggered. + */ + int gattRequestTimeout = 20000; void handleConnectionRequest(); void closeServerSocket(); @@ -387,12 +411,15 @@ private: const QLowEnergyHandle descriptorHandle, const QByteArray &newValue); + void restartRequestTimer(); + private slots: void l2cpConnected(); void l2cpDisconnected(); void l2cpErrorChanged(QBluetoothSocket::SocketError); void l2cpReadyRead(); void encryptionChangedEvent(const QBluetoothAddress&, bool); + void handleGattRequestTimeout(); #elif defined(QT_ANDROID_BLUETOOTH) LowEnergyNotificationHub *hub; @@ -414,6 +441,34 @@ private slots: QLowEnergyService::ServiceError errorCode); void characteristicChanged(int charHandle, const QByteArray &data); void serviceError(int attributeHandle, QLowEnergyService::ServiceError errorCode); +#elif defined(QT_WINRT_BLUETOOTH) +private slots: + void characteristicChanged(int charHandle, const QByteArray &data); + +private: + Microsoft::WRL::ComPtr<ABI::Windows::Devices::Bluetooth::IBluetoothLEDevice> mDevice; + EventRegistrationToken mStatusChangedToken; + struct ValueChangedEntry { + ValueChangedEntry() {} + ValueChangedEntry(Microsoft::WRL::ComPtr<ABI::Windows::Devices::Bluetooth::GenericAttributeProfile::IGattCharacteristic> c, + EventRegistrationToken t) + : characteristic(c) + , token(t) + { + } + + Microsoft::WRL::ComPtr<ABI::Windows::Devices::Bluetooth::GenericAttributeProfile::IGattCharacteristic> characteristic; + EventRegistrationToken token; + }; + QVector<ValueChangedEntry> mValueChangedTokens; + + Microsoft::WRL::ComPtr<ABI::Windows::Devices::Bluetooth::GenericAttributeProfile::IGattDeviceService> getNativeService(const QBluetoothUuid &serviceUuid); + Microsoft::WRL::ComPtr<ABI::Windows::Devices::Bluetooth::GenericAttributeProfile::IGattCharacteristic> getNativeCharacteristic(const QBluetoothUuid &serviceUuid, const QBluetoothUuid &charUuid); + + void registerForValueChanges(const QBluetoothUuid &serviceUuid, const QBluetoothUuid &charUuid); + + void obtainIncludedServices(QSharedPointer<QLowEnergyServicePrivate> servicePointer, + Microsoft::WRL::ComPtr<ABI::Windows::Devices::Bluetooth::GenericAttributeProfile::IGattDeviceService> nativeService); #endif private: QLowEnergyController *q_ptr; @@ -424,6 +479,11 @@ Q_DECLARE_TYPEINFO(QLowEnergyControllerPrivate::Attribute, Q_MOVABLE_TYPE); QT_END_NAMESPACE +#ifdef QT_WINRT_BLUETOOTH +Q_DECLARE_METATYPE(QLowEnergyCharacteristic) +Q_DECLARE_METATYPE(QLowEnergyDescriptor) +#endif // QT_WINRT_BLUETOOTH + #endif // QT_OSX_BLUETOOTH || QT_IOS_BLUETOOTH #endif // QLOWENERGYCONTROLLERPRIVATE_P_H diff --git a/src/bluetooth/qlowenergycontroller_winrt.cpp b/src/bluetooth/qlowenergycontroller_winrt.cpp new file mode 100644 index 00000000..d6fc4952 --- /dev/null +++ b/src/bluetooth/qlowenergycontroller_winrt.cpp @@ -0,0 +1,1103 @@ +/**************************************************************************** +** +** 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 "qlowenergycontroller_p.h" + +#include <QtCore/qfunctions_winrt.h> +#include <QtCore/QLoggingCategory> +#include <private/qeventdispatcher_winrt_p.h> + +#include <functional> +#include <robuffer.h> +#include <windows.devices.enumeration.h> +#include <windows.devices.bluetooth.h> +#include <windows.foundation.collections.h> +#include <windows.storage.streams.h> + +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Foundation::Collections; +using namespace ABI::Windows::Devices; +using namespace ABI::Windows::Devices::Bluetooth; +using namespace ABI::Windows::Devices::Bluetooth::GenericAttributeProfile; +using namespace ABI::Windows::Devices::Enumeration; +using namespace ABI::Windows::Storage::Streams; + +QT_BEGIN_NAMESPACE + +typedef ITypedEventHandler<BluetoothLEDevice *, IInspectable *> StatusHandler; +typedef ITypedEventHandler<GattCharacteristic *, GattValueChangedEventArgs *> ValueChangedHandler; +typedef GattReadClientCharacteristicConfigurationDescriptorResult ClientCharConfigDescriptorResult; +typedef IGattReadClientCharacteristicConfigurationDescriptorResult IClientCharConfigDescriptorResult; + +Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINRT) + +static QVector<QBluetoothUuid> getIncludedServiceIds(const ComPtr<IGattDeviceService> &service) +{ + QVector<QBluetoothUuid> result; + ComPtr<IGattDeviceService2> service2; + HRESULT hr = service.As(&service2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IVectorView<GattDeviceService *>> includedServices; + hr = service2->GetAllIncludedServices(&includedServices); + Q_ASSERT_SUCCEEDED(hr); + + uint count; + hr = includedServices->get_Size(&count); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < count; ++i) { + ComPtr<IGattDeviceService> includedService; + hr = includedServices->GetAt(i, &includedService); + Q_ASSERT_SUCCEEDED(hr); + GUID guuid; + hr = includedService->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + const QBluetoothUuid service(guuid); + result << service; + + result << getIncludedServiceIds(includedService); + } + return result; +} + +static QByteArray byteArrayFromBuffer(const ComPtr<IBuffer> &buffer, bool isWCharString = false) +{ + ComPtr<Windows::Storage::Streams::IBufferByteAccess> byteAccess; + HRESULT hr = buffer.As(&byteAccess); + Q_ASSERT_SUCCEEDED(hr); + char *data; + hr = byteAccess->Buffer(reinterpret_cast<byte **>(&data)); + Q_ASSERT_SUCCEEDED(hr); + UINT32 size; + hr = buffer->get_Length(&size); + Q_ASSERT_SUCCEEDED(hr); + if (isWCharString) { + QString valueString = QString::fromUtf16(reinterpret_cast<ushort *>(data)).left(size / 2); + return valueString.toUtf8(); + } + return QByteArray(data, size); +} + +static QByteArray byteArrayFromGattResult(const ComPtr<IGattReadResult> &gattResult, bool isWCharString = false) +{ + ComPtr<ABI::Windows::Storage::Streams::IBuffer> buffer; + HRESULT hr; + hr = gattResult->get_Value(&buffer); + Q_ASSERT_SUCCEEDED(hr); + return byteArrayFromBuffer(buffer, isWCharString); +} + +class QWinRTLowEnergyServiceHandler : public QObject +{ + Q_OBJECT +public: + QWinRTLowEnergyServiceHandler(const QBluetoothUuid &service, const ComPtr<IGattDeviceService2> &deviceService) + : mService(service) + , mDeviceService(deviceService) + { + qCDebug(QT_BT_WINRT) << __FUNCTION__; + } + + ~QWinRTLowEnergyServiceHandler() + { + } + +public slots: + void obtainCharList() + { + QVector<QBluetoothUuid> indicateChars; + quint16 startHandle = 0; + quint16 endHandle = 0; + qCDebug(QT_BT_WINRT) << __FUNCTION__; + ComPtr<IVectorView<GattCharacteristic *>> characteristics; + HRESULT hr = mDeviceService->GetAllCharacteristics(&characteristics); + Q_ASSERT_SUCCEEDED(hr); + if (!characteristics) { + emit charListObtained(mService, mCharacteristicList, indicateChars, startHandle, endHandle); + QThread::currentThread()->quit(); + return; + } + + uint characteristicsCount; + hr = characteristics->get_Size(&characteristicsCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < characteristicsCount; ++i) { + ComPtr<IGattCharacteristic> characteristic; + hr = characteristics->GetAt(i, &characteristic); + Q_ASSERT_SUCCEEDED(hr); + quint16 handle; + hr = characteristic->get_AttributeHandle(&handle); + Q_ASSERT_SUCCEEDED(hr); + QLowEnergyServicePrivate::CharData charData; + charData.valueHandle = handle + 1; + if (startHandle == 0 || startHandle > handle) + startHandle = handle; + if (endHandle == 0 || endHandle < handle) + endHandle = handle; + GUID guuid; + hr = characteristic->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + charData.uuid = QBluetoothUuid(guuid); + GattCharacteristicProperties properties; + hr = characteristic->get_CharacteristicProperties(&properties); + Q_ASSERT_SUCCEEDED(hr); + charData.properties = QLowEnergyCharacteristic::PropertyTypes(properties); + if (charData.properties & QLowEnergyCharacteristic::Read) { + ComPtr<IAsyncOperation<GattReadResult *>> readOp; + hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IGattReadResult> readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + if (readResult) + charData.value = byteArrayFromGattResult(readResult); + } + ComPtr<IGattCharacteristic2> characteristic2; + hr = characteristic.As(&characteristic2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IVectorView<GattDescriptor *>> descriptors; + hr = characteristic2->GetAllDescriptors(&descriptors); + Q_ASSERT_SUCCEEDED(hr); + uint descriptorCount; + hr = descriptors->get_Size(&descriptorCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint j = 0; j < descriptorCount; ++j) { + QLowEnergyServicePrivate::DescData descData; + ComPtr<IGattDescriptor> descriptor; + hr = descriptors->GetAt(j, &descriptor); + Q_ASSERT_SUCCEEDED(hr); + quint16 descHandle; + hr = descriptor->get_AttributeHandle(&descHandle); + Q_ASSERT_SUCCEEDED(hr); + GUID descriptorUuid; + hr = descriptor->get_Uuid(&descriptorUuid); + Q_ASSERT_SUCCEEDED(hr); + descData.uuid = QBluetoothUuid(descriptorUuid); + if (descData.uuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + ComPtr<IAsyncOperation<ClientCharConfigDescriptorResult *>> readOp; + hr = characteristic->ReadClientCharacteristicConfigurationDescriptorAsync(&readOp); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IClientCharConfigDescriptorResult> readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + GattClientCharacteristicConfigurationDescriptorValue value; + hr = readResult->get_ClientCharacteristicConfigurationDescriptor(&value); + Q_ASSERT_SUCCEEDED(hr); + quint16 result = 0; + bool correct = false; + if (value & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + result |= GattClientCharacteristicConfigurationDescriptorValue_Indicate; + correct = true; + } + if (value & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + result |= GattClientCharacteristicConfigurationDescriptorValue_Notify; + correct = true; + } + if (value == GattClientCharacteristicConfigurationDescriptorValue_None) { + correct = true; + } + if (!correct) + continue; + + descData.value = QByteArray(2, Qt::Uninitialized); + qToLittleEndian(result, descData.value.data()); + indicateChars << charData.uuid; + } else { + ComPtr<IAsyncOperation<GattReadResult *>> readOp; + hr = descriptor->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IGattReadResult> readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + if (descData.uuid == QBluetoothUuid::CharacteristicUserDescription) + descData.value = byteArrayFromGattResult(readResult, true); + else + descData.value = byteArrayFromGattResult(readResult); + } + charData.descriptorList.insert(descHandle, descData); + } + mCharacteristicList.insert(handle, charData); + } + emit charListObtained(mService, mCharacteristicList, indicateChars, startHandle, endHandle); + QThread::currentThread()->quit(); + } + +public: + QBluetoothUuid mService; + ComPtr<IGattDeviceService2> mDeviceService; + QHash<QLowEnergyHandle, QLowEnergyServicePrivate::CharData> mCharacteristicList; + +signals: + void charListObtained(const QBluetoothUuid &service, QHash<QLowEnergyHandle, + QLowEnergyServicePrivate::CharData> charList, + QVector<QBluetoothUuid> indicateChars, + QLowEnergyHandle startHandle, QLowEnergyHandle endHandle); +}; + +QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() + : QObject(), + state(QLowEnergyController::UnconnectedState), + error(QLowEnergyController::NoError) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + + qRegisterMetaType<QLowEnergyCharacteristic>(); + qRegisterMetaType<QLowEnergyDescriptor>(); +} + +QLowEnergyControllerPrivate::~QLowEnergyControllerPrivate() +{ + if (mDevice && mStatusChangedToken.value) + mDevice->remove_ConnectionStatusChanged(mStatusChangedToken); + + qCDebug(QT_BT_WINRT) << "Unregistering " << mValueChangedTokens.count() << " value change tokens"; + for (const ValueChangedEntry &entry : mValueChangedTokens) + entry.characteristic->remove_ValueChanged(entry.token); +} + +void QLowEnergyControllerPrivate::init() +{ +} + +void QLowEnergyControllerPrivate::connectToDevice() +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + Q_Q(QLowEnergyController); + if (remoteDevice.isNull()) { + qWarning() << "Invalid/null remote device address"; + setError(QLowEnergyController::UnknownRemoteDeviceError); + return; + } + + setState(QLowEnergyController::ConnectingState); + + ComPtr<IBluetoothLEDeviceStatics> deviceStatics; + HRESULT hr = GetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Devices_Bluetooth_BluetoothLEDevice).Get(), &deviceStatics); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IAsyncOperation<BluetoothLEDevice *>> deviceFromIdOperation; + hr = deviceStatics->FromBluetoothAddressAsync(remoteDevice.toUInt64(), &deviceFromIdOperation); + Q_ASSERT_SUCCEEDED(hr); + hr = QWinRTFunctions::await(deviceFromIdOperation, mDevice.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + + if (!mDevice) { + qCDebug(QT_BT_WINRT) << "Could not find LE device"; + setError(QLowEnergyController::InvalidBluetoothAdapterError); + setState(QLowEnergyController::UnconnectedState); + } + BluetoothConnectionStatus status; + hr = mDevice->get_ConnectionStatus(&status); + Q_ASSERT_SUCCEEDED(hr); + hr = QEventDispatcherWinRT::runOnXamlThread([this, q]() { + HRESULT hr; + hr = mDevice->add_ConnectionStatusChanged(Callback<StatusHandler>([this, q](IBluetoothLEDevice *dev, IInspectable *) { + BluetoothConnectionStatus status; + HRESULT hr; + hr = dev->get_ConnectionStatus(&status); + Q_ASSERT_SUCCEEDED(hr); + if (state == QLowEnergyController::ConnectingState + && status == BluetoothConnectionStatus::BluetoothConnectionStatus_Connected) { + setState(QLowEnergyController::ConnectedState); + emit q->connected(); + } else if (state == QLowEnergyController::ConnectedState + && status == BluetoothConnectionStatus::BluetoothConnectionStatus_Disconnected) { + setState(QLowEnergyController::UnconnectedState); + emit q->disconnected(); + } + return S_OK; + }).Get(), &mStatusChangedToken); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); + + if (status == BluetoothConnectionStatus::BluetoothConnectionStatus_Connected) { + setState(QLowEnergyController::ConnectedState); + emit q->connected(); + return; + } + + ComPtr<IVectorView <GattDeviceService *>> deviceServices; + hr = mDevice->get_GattServices(&deviceServices); + Q_ASSERT_SUCCEEDED(hr); + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + Q_ASSERT_SUCCEEDED(hr); + // Windows Phone automatically connects to the device as soon as a service value is read/written. + // Thus we read one value in order to establish the connection. + for (uint i = 0; i < serviceCount; ++i) { + ComPtr<IGattDeviceService> service; + hr = deviceServices->GetAt(i, &service); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IGattDeviceService2> service2; + hr = service.As(&service2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IVectorView<GattCharacteristic *>> characteristics; + hr = service2->GetAllCharacteristics(&characteristics); + if (hr == E_ACCESSDENIED) { + // Everything will work as expected up until this point if the manifest capabilties + // for bluetooth LE are not set. + qCWarning(QT_BT_WINRT) << "Could not obtain characteristic list. Please check your " + "manifest capabilities"; + setState(QLowEnergyController::UnconnectedState); + setError(QLowEnergyController::ConnectionError); + return; + } else { + Q_ASSERT_SUCCEEDED(hr); + } + uint characteristicsCount; + hr = characteristics->get_Size(&characteristicsCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint j = 0; j < characteristicsCount; ++j) { + ComPtr<IGattCharacteristic> characteristic; + hr = characteristics->GetAt(j, &characteristic); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IAsyncOperation<GattReadResult *>> op; + GattCharacteristicProperties props; + hr = characteristic->get_CharacteristicProperties(&props); + Q_ASSERT_SUCCEEDED(hr); + if (!(props & GattCharacteristicProperties_Read)) + continue; + hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode::BluetoothCacheMode_Uncached, &op); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IGattReadResult> result; + hr = QWinRTFunctions::await(op, result.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<ABI::Windows::Storage::Streams::IBuffer> buffer; + hr = result->get_Value(&buffer); + Q_ASSERT_SUCCEEDED(hr); + if (!buffer) { + qCDebug(QT_BT_WINRT) << "Problem reading value"; + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + } + return; + } + } +} + +void QLowEnergyControllerPrivate::disconnectFromDevice() +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + Q_Q(QLowEnergyController); + setState(QLowEnergyController::UnconnectedState); + emit q->disconnected(); +} + +ComPtr<IGattDeviceService> QLowEnergyControllerPrivate::getNativeService(const QBluetoothUuid &serviceUuid) +{ + ComPtr<IGattDeviceService> deviceService; + HRESULT hr; + hr = mDevice->GetGattService(serviceUuid, &deviceService); + if (FAILED(hr)) + qCDebug(QT_BT_WINRT) << "Could not obtain native service for Uuid" << serviceUuid; + return deviceService; +} + +ComPtr<IGattCharacteristic> QLowEnergyControllerPrivate::getNativeCharacteristic(const QBluetoothUuid &serviceUuid, const QBluetoothUuid &charUuid) +{ + ComPtr<IGattDeviceService> service = getNativeService(serviceUuid); + if (!service) + return nullptr; + + ComPtr<IVectorView<GattCharacteristic *>> characteristics; + HRESULT hr = service->GetCharacteristics(charUuid, &characteristics); + RETURN_IF_FAILED("Could not obtain native characteristics for service", return nullptr); + ComPtr<IGattCharacteristic> characteristic; + hr = characteristics->GetAt(0, &characteristic); + RETURN_IF_FAILED("Could not obtain first characteristic for service", return nullptr); + return characteristic; +} + +void QLowEnergyControllerPrivate::registerForValueChanges(const QBluetoothUuid &serviceUuid, const QBluetoothUuid &charUuid) +{ + qCDebug(QT_BT_WINRT) << "Registering characteristic" << charUuid << "in service" + << serviceUuid << "for value changes"; + for (const ValueChangedEntry &entry : mValueChangedTokens) { + GUID guuid; + HRESULT hr; + hr = entry.characteristic->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + if (QBluetoothUuid(guuid) == charUuid) + return; + } + ComPtr<IGattCharacteristic> characteristic = getNativeCharacteristic(serviceUuid, charUuid); + + EventRegistrationToken token; + HRESULT hr; + hr = characteristic->add_ValueChanged(Callback<ValueChangedHandler>([this](IGattCharacteristic *characteristic, IGattValueChangedEventArgs *args) { + HRESULT hr; + quint16 handle; + hr = characteristic->get_AttributeHandle(&handle); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IBuffer> buffer; + hr = args->get_CharacteristicValue(&buffer); + Q_ASSERT_SUCCEEDED(hr); + characteristicChanged(handle, byteArrayFromBuffer(buffer)); + return S_OK; + }).Get(), &token); + Q_ASSERT_SUCCEEDED(hr); + mValueChangedTokens.append(ValueChangedEntry(characteristic, token)); + qCDebug(QT_BT_WINRT) << "Characteristic" << charUuid << "in service" + << serviceUuid << "registered for value changes"; +} + +void QLowEnergyControllerPrivate::obtainIncludedServices(QSharedPointer<QLowEnergyServicePrivate> servicePointer, + ComPtr<IGattDeviceService> service) +{ + Q_Q(QLowEnergyController); + ComPtr<IGattDeviceService2> service2; + HRESULT hr = service.As(&service2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IVectorView<GattDeviceService *>> includedServices; + hr = service2->GetAllIncludedServices(&includedServices); + Q_ASSERT_SUCCEEDED(hr); + + uint count; + hr = includedServices->get_Size(&count); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < count; ++i) { + ComPtr<IGattDeviceService> includedService; + hr = includedServices->GetAt(i, &includedService); + Q_ASSERT_SUCCEEDED(hr); + GUID guuid; + hr = includedService->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + const QBluetoothUuid includedUuid(guuid); + QSharedPointer<QLowEnergyServicePrivate> includedPointer; + if (serviceList.contains(includedUuid)) { + includedPointer = serviceList.value(includedUuid); + } else { + QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate(); + priv->uuid = includedUuid; + priv->setController(this); + + includedPointer = QSharedPointer<QLowEnergyServicePrivate>(priv); + serviceList.insert(includedUuid, includedPointer); + } + includedPointer->type |= QLowEnergyService::IncludedService; + servicePointer->includedServices.append(includedUuid); + + obtainIncludedServices(includedPointer, includedService); + + emit q->serviceDiscovered(includedUuid); + } +} + +void QLowEnergyControllerPrivate::discoverServices() +{ + Q_Q(QLowEnergyController); + + qCDebug(QT_BT_WINRT) << "Service discovery initiated"; + ComPtr<IVectorView<GattDeviceService *>> deviceServices; + HRESULT hr = mDevice->get_GattServices(&deviceServices); + Q_ASSERT_SUCCEEDED(hr); + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < serviceCount; ++i) { + ComPtr<IGattDeviceService> deviceService; + hr = deviceServices->GetAt(i, &deviceService); + Q_ASSERT_SUCCEEDED(hr); + GUID guuid; + hr = deviceService->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + const QBluetoothUuid service(guuid); + + QSharedPointer<QLowEnergyServicePrivate> pointer; + if (serviceList.contains(service)) { + pointer = serviceList.value(service); + } else { + QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate(); + priv->uuid = service; + priv->setController(this); + + pointer = QSharedPointer<QLowEnergyServicePrivate>(priv); + serviceList.insert(service, pointer); + } + pointer->type |= QLowEnergyService::PrimaryService; + + obtainIncludedServices(pointer, deviceService); + + emit q->serviceDiscovered(service); + } + + setState(QLowEnergyController::DiscoveredState); + emit q->discoveryFinished(); +} + +void QLowEnergyControllerPrivate::discoverServiceDetails(const QBluetoothUuid &service) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service; + if (!serviceList.contains(service)) { + qCWarning(QT_BT_WINRT) << "Discovery done of unknown service:" + << service.toString(); + return; + } + + ComPtr<IGattDeviceService> deviceService = getNativeService(service); + if (!deviceService) { + qCDebug(QT_BT_WINRT) << "Could not obtain native service for uuid " << service; + return; + } + + //update service data + QSharedPointer<QLowEnergyServicePrivate> pointer = serviceList.value(service); + + pointer->setState(QLowEnergyService::DiscoveringServices); + ComPtr<IGattDeviceService2> deviceService2; + HRESULT hr = deviceService.As(&deviceService2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IVectorView<GattDeviceService *>> deviceServices; + hr = deviceService2->GetAllIncludedServices(&deviceServices); + Q_ASSERT_SUCCEEDED(hr); + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < serviceCount; ++i) { + ComPtr<IGattDeviceService> includedService; + hr = deviceServices->GetAt(i, &includedService); + Q_ASSERT_SUCCEEDED(hr); + GUID guuid; + hr = includedService->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + + const QBluetoothUuid service(guuid); + if (service.isNull()) { + qCDebug(QT_BT_WINRT) << "Could not find service"; + return; + } + + pointer->includedServices.append(service); + + // update the type of the included service + QSharedPointer<QLowEnergyServicePrivate> otherService = serviceList.value(service); + if (!otherService.isNull()) + otherService->type |= QLowEnergyService::IncludedService; + } + + QWinRTLowEnergyServiceHandler *worker = new QWinRTLowEnergyServiceHandler(service, deviceService2); + QThread *thread = new QThread; + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &QWinRTLowEnergyServiceHandler::obtainCharList); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, &QThread::finished, worker, &QObject::deleteLater); + connect(worker, &QWinRTLowEnergyServiceHandler::charListObtained, + [this, thread](const QBluetoothUuid &service, QHash<QLowEnergyHandle, QLowEnergyServicePrivate::CharData> charList + , QVector<QBluetoothUuid> indicateChars + , QLowEnergyHandle startHandle, QLowEnergyHandle endHandle) { + if (!serviceList.contains(service)) { + qCWarning(QT_BT_WINRT) << "Discovery done of unknown service:" + << service.toString(); + return; + } + + QSharedPointer<QLowEnergyServicePrivate> pointer = serviceList.value(service); + pointer->startHandle = startHandle; + pointer->endHandle = endHandle; + pointer->characteristicList = charList; + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([indicateChars, service, this]() { + for (const QBluetoothUuid &indicateChar : indicateChars) + registerForValueChanges(service, indicateChar); + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); + + pointer->setState(QLowEnergyService::ServiceDiscovered); + thread->exit(0); + }); + thread->start(); +} + +void QLowEnergyControllerPrivate::startAdvertising(const QLowEnergyAdvertisingParameters &, const QLowEnergyAdvertisingData &, const QLowEnergyAdvertisingData &) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivate::stopAdvertising() +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivate::requestConnectionUpdate(const QLowEnergyConnectionParameters &) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivate::readCharacteristic(const QSharedPointer<QLowEnergyServicePrivate> service, + const QLowEnergyHandle charHandle) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle; + Q_ASSERT(!service.isNull()); + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << charHandle << "could not be found in service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicReadError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, service, this]() { + const QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + if (!(charData.properties & QLowEnergyCharacteristic::Read)) + qCDebug(QT_BT_WINRT) << "Read flag is not set for characteristic" << charData.uuid; + + ComPtr<IGattCharacteristic> characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicReadError); + return S_OK; + } + ComPtr<IAsyncOperation<GattReadResult*>> readOp; + HRESULT hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + Q_ASSERT_SUCCEEDED(hr); + auto readCompletedLambda = [charData, charHandle, service] + (IAsyncOperation<GattReadResult*> *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "read operation failed."; + service->setError(QLowEnergyService::CharacteristicReadError); + return S_OK; + } + ComPtr<IGattReadResult> characteristicValue; + HRESULT hr; + hr = op->GetResults(&characteristicValue); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for characteristic" << charHandle; + service->setError(QLowEnergyService::CharacteristicReadError); + return S_OK; + } + + const QByteArray value = byteArrayFromGattResult(characteristicValue); + QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + charData.value = value; + service->characteristicList.insert(charHandle, charData); + emit service->characteristicRead(QLowEnergyCharacteristic(service, charHandle), value); + return S_OK; + }; + hr = readOp->put_Completed(Callback<IAsyncOperationCompletedHandler<GattReadResult *>>(readCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + +void QLowEnergyControllerPrivate::readDescriptor(const QSharedPointer<QLowEnergyServicePrivate> service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descHandle) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << descHandle; + Q_ASSERT(!service.isNull()); + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "in characteristic" << charHandle + << "cannot be found in service" << service->uuid; + service->setError(QLowEnergyService::DescriptorReadError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, descHandle, service, this]() { + QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + ComPtr<IGattCharacteristic> characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + + // Get native descriptor + if (!charData.descriptorList.contains(descHandle)) + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "cannot be found in characteristic" << charHandle; + QLowEnergyServicePrivate::DescData descData = charData.descriptorList.value(descHandle); + if (descData.uuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + ComPtr<IAsyncOperation<ClientCharConfigDescriptorResult *>> readOp; + HRESULT hr = characteristic->ReadClientCharacteristicConfigurationDescriptorAsync(&readOp); + Q_ASSERT_SUCCEEDED(hr); + auto readCompletedLambda = [&charData, charHandle, &descData, descHandle, service] + (IAsyncOperation<ClientCharConfigDescriptorResult *> *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "read operation failed"; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + ComPtr<IClientCharConfigDescriptorResult> iValue; + HRESULT hr; + hr = op->GetResults(&iValue); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + GattClientCharacteristicConfigurationDescriptorValue value; + hr = iValue->get_ClientCharacteristicConfigurationDescriptor(&value); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain value for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + quint16 result = 0; + bool correct = false; + if (value & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + result |= QLowEnergyCharacteristic::Indicate; + correct = true; + } + if (value & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + result |= QLowEnergyCharacteristic::Notify; + correct = true; + } + if (value == GattClientCharacteristicConfigurationDescriptorValue_None) + correct = true; + if (!correct) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle + << "read operation failed. Obtained unexpected value."; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + descData.value = QByteArray(2, Qt::Uninitialized); + qToLittleEndian(result, descData.value.data()); + charData.descriptorList.insert(descHandle, descData); + emit service->descriptorRead(QLowEnergyDescriptor(service, charHandle, descHandle), + descData.value); + return S_OK; + }; + hr = readOp->put_Completed(Callback<IAsyncOperationCompletedHandler<ClientCharConfigDescriptorResult *>>(readCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + } else { + ComPtr<IVectorView<GattDescriptor *>> descriptors; + HRESULT hr = characteristic->GetDescriptors(descData.uuid, &descriptors); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IGattDescriptor> descriptor; + hr = descriptors->GetAt(0, &descriptor); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IAsyncOperation<GattReadResult*>> readOp; + hr = descriptor->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + Q_ASSERT_SUCCEEDED(hr); + auto readCompletedLambda = [&charData, charHandle, &descData, descHandle, service] + (IAsyncOperation<GattReadResult*> *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "read operation failed"; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + ComPtr<IGattReadResult> descriptorValue; + HRESULT hr; + hr = op->GetResults(&descriptorValue); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + if (descData.uuid == QBluetoothUuid::CharacteristicUserDescription) + descData.value = byteArrayFromGattResult(descriptorValue, true); + else + descData.value = byteArrayFromGattResult(descriptorValue); + charData.descriptorList.insert(descHandle, descData); + emit service->descriptorRead(QLowEnergyDescriptor(service, charHandle, descHandle), + descData.value); + return S_OK; + }; + hr = readOp->put_Completed(Callback<IAsyncOperationCompletedHandler<GattReadResult *>>(readCompletedLambda).Get()); + return S_OK; + } + }); + Q_ASSERT_SUCCEEDED(hr); +} + +void QLowEnergyControllerPrivate::writeCharacteristic(const QSharedPointer<QLowEnergyServicePrivate> service, + const QLowEnergyHandle charHandle, + const QByteArray &newValue, + QLowEnergyService::WriteMode mode) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << newValue << mode; + Q_ASSERT(!service.isNull()); + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "cannot be found in service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicWriteError); + return; + } + + QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + const bool writeWithResponse = mode == QLowEnergyService::WriteWithResponse; + if (!(charData.properties & (writeWithResponse ? QLowEnergyCharacteristic::Write : QLowEnergyCharacteristic::WriteNoResponse))) + qCDebug(QT_BT_WINRT) << "Write flag is not set for characteristic" << charHandle; + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charData, charHandle, this, service, newValue, writeWithResponse]() { + ComPtr<IGattCharacteristic> characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + ComPtr<ABI::Windows::Storage::Streams::IBufferFactory> bufferFactory; + HRESULT hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_Storage_Streams_Buffer).Get(), &bufferFactory); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<ABI::Windows::Storage::Streams::IBuffer> buffer; + const int length = newValue.length(); + hr = bufferFactory->Create(length, &buffer); + Q_ASSERT_SUCCEEDED(hr); + hr = buffer->put_Length(length); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<Windows::Storage::Streams::IBufferByteAccess> byteAccess; + hr = buffer.As(&byteAccess); + Q_ASSERT_SUCCEEDED(hr); + byte *bytes; + hr = byteAccess->Buffer(&bytes); + Q_ASSERT_SUCCEEDED(hr); + memcpy(bytes, newValue, length); + ComPtr<IAsyncOperation<GattCommunicationStatus>> writeOp; + GattWriteOption option = writeWithResponse ? GattWriteOption_WriteWithResponse : GattWriteOption_WriteWithoutResponse; + hr = characteristic->WriteValueWithOptionAsync(buffer.Get(), option, &writeOp); + Q_ASSERT_SUCCEEDED(hr); + auto writeCompletedLambda =[charData, charHandle, newValue, service, this] + (IAsyncOperation<GattCommunicationStatus> *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "write operation failed"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + if (hr == E_BLUETOOTH_ATT_INVALID_ATTRIBUTE_VALUE_LENGTH) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "write operation was tried with invalid value length"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + Q_ASSERT_SUCCEEDED(hr); + if (result != GattCommunicationStatus_Success) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "write operation failed"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + // only update cache when property is readable. Otherwise it remains + // empty. + if (charData.properties & QLowEnergyCharacteristic::Read) + updateValueOfCharacteristic(charHandle, newValue, false); + emit service->characteristicWritten(QLowEnergyCharacteristic(service, charHandle), newValue); + return S_OK; + }; + hr = writeOp->put_Completed(Callback<IAsyncOperationCompletedHandler<GattCommunicationStatus>>(writeCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + +void QLowEnergyControllerPrivate::writeDescriptor( + const QSharedPointer<QLowEnergyServicePrivate> service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descHandle, + const QByteArray &newValue) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << descHandle << newValue; + Q_ASSERT(!service.isNull()); + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "in characteristic" << charHandle + << "could not be found in service" << service->uuid; + service->setError(QLowEnergyService::DescriptorWriteError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, descHandle, this, service, newValue]() { + const QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + ComPtr<IGattCharacteristic> characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + + // Get native descriptor + if (!charData.descriptorList.contains(descHandle)) + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "could not be found in Characteristic" << charHandle; + + QLowEnergyServicePrivate::DescData descData = charData.descriptorList.value(descHandle); + if (descData.uuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + GattClientCharacteristicConfigurationDescriptorValue value; + quint16 intValue = qFromLittleEndian<quint16>(newValue); + if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Indicate && intValue & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + qCWarning(QT_BT_WINRT) << "Setting both Indicate and Notify is not supported on WinRT"; + value = (GattClientCharacteristicConfigurationDescriptorValue)(GattClientCharacteristicConfigurationDescriptorValue_Indicate | GattClientCharacteristicConfigurationDescriptorValue_Notify); + } else if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + value = GattClientCharacteristicConfigurationDescriptorValue_Indicate; + } else if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + value = GattClientCharacteristicConfigurationDescriptorValue_Notify; + } else if (intValue == 0) { + value = GattClientCharacteristicConfigurationDescriptorValue_None; + } else { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed: Invalid value"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + ComPtr<IAsyncOperation<enum GattCommunicationStatus>> writeOp; + HRESULT hr = characteristic->WriteClientCharacteristicConfigurationDescriptorAsync(value, &writeOp); + Q_ASSERT_SUCCEEDED(hr); + auto writeCompletedLambda = [charHandle, descHandle, newValue, service, this] + (IAsyncOperation<GattCommunicationStatus> *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + if (result != GattCommunicationStatus_Success) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + updateValueOfDescriptor(charHandle, descHandle, newValue, false); + emit service->descriptorWritten(QLowEnergyDescriptor(service, charHandle, descHandle), newValue); + return S_OK; + }; + hr = writeOp->put_Completed(Callback<IAsyncOperationCompletedHandler<GattCommunicationStatus >>(writeCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + } else { + ComPtr<IVectorView<GattDescriptor *>> descriptors; + HRESULT hr = characteristic->GetDescriptors(descData.uuid, &descriptors); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<IGattDescriptor> descriptor; + hr = descriptors->GetAt(0, &descriptor); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<ABI::Windows::Storage::Streams::IBufferFactory> bufferFactory; + hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_Storage_Streams_Buffer).Get(), &bufferFactory); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<ABI::Windows::Storage::Streams::IBuffer> buffer; + const int length = newValue.length(); + hr = bufferFactory->Create(length, &buffer); + Q_ASSERT_SUCCEEDED(hr); + hr = buffer->put_Length(length); + Q_ASSERT_SUCCEEDED(hr); + ComPtr<Windows::Storage::Streams::IBufferByteAccess> byteAccess; + hr = buffer.As(&byteAccess); + Q_ASSERT_SUCCEEDED(hr); + byte *bytes; + hr = byteAccess->Buffer(&bytes); + Q_ASSERT_SUCCEEDED(hr); + memcpy(bytes, newValue, length); + ComPtr<IAsyncOperation<GattCommunicationStatus>> writeOp; + hr = descriptor->WriteValueAsync(buffer.Get(), &writeOp); + Q_ASSERT_SUCCEEDED(hr); + auto writeCompletedLambda = [charHandle, descHandle, newValue, service, this] + (IAsyncOperation<GattCommunicationStatus> *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + if (result != GattCommunicationStatus_Success) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + updateValueOfDescriptor(charHandle, descHandle, newValue, false); + emit service->descriptorWritten(QLowEnergyDescriptor(service, charHandle, descHandle), newValue); + return S_OK; + }; + hr = writeOp->put_Completed(Callback<IAsyncOperationCompletedHandler<GattCommunicationStatus>>(writeCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + } + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + + +void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServiceData &, QLowEnergyHandle) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivate::characteristicChanged( + int charHandle, const QByteArray &data) +{ + QSharedPointer<QLowEnergyServicePrivate> service = + serviceForHandle(charHandle); + if (service.isNull()) + return; + + qCDebug(QT_BT_WINRT) << "Characteristic change notification" << service->uuid + << charHandle << data.toHex(); + + QLowEnergyCharacteristic characteristic = characteristicForHandle(charHandle); + if (!characteristic.isValid()) { + qCWarning(QT_BT_WINRT) << "characteristicChanged: Cannot find characteristic"; + return; + } + + // only update cache when property is readable. Otherwise it remains + // empty. + if (characteristic.properties() & QLowEnergyCharacteristic::Read) + updateValueOfCharacteristic(characteristic.attributeHandle(), + data, false); + emit service->characteristicChanged(characteristic, data); +} + +QT_END_NAMESPACE + +#include "qlowenergycontroller_winrt.moc" diff --git a/src/bluetooth/qlowenergyservice.cpp b/src/bluetooth/qlowenergyservice.cpp index e9722f81..6e33c565 100644 --- a/src/bluetooth/qlowenergyservice.cpp +++ b/src/bluetooth/qlowenergyservice.cpp @@ -294,6 +294,8 @@ QT_BEGIN_NAMESPACE the read operation is not successful, the \l error() signal is emitted using the \l CharacteristicReadError flag. + \note This signal is only emitted for Central Role related use cases. + \sa readCharacteristic() \since 5.5 */ @@ -306,18 +308,14 @@ QT_BEGIN_NAMESPACE by calling \l writeCharacteristic(). If the write operation is not successful, the \l error() signal is emitted using the \l CharacteristicWriteError flag. - Since this signal is an indication of a successful write operation \a newValue - generally matches the value that was passed to the associated - \l writeCharacteristic() call. However, it may happen that the two values differ - from each other. This can occur in cases when the written value is - used by the remote device to trigger an operation and it returns some other value via - the written and/or change notification. Such cases are very specific to the - target device. In any case, the reception of the written signal can still be considered - as a sign that the target device received the to-be-written value. + The reception of the written signal can be considered as a sign that the target device + received the to-be-written value and reports back the status of write request. \note If \l writeCharacteristic() is called using the \l WriteWithoutResponse mode, this signal and the \l error() are never emitted. + \note This signal is only emitted for Central Role related use cases. + \sa writeCharacteristic() */ @@ -326,7 +324,7 @@ QT_BEGIN_NAMESPACE If the associated controller object is in the \l {QLowEnergyController::CentralRole}{central} role, this signal is emitted when the value of \a characteristic is changed by an event on the - peripheral. In that case, the signal emission implies that change notifications must + peripheral/device side. In that case, the signal emission implies that change notifications must have been activated via the characteristic's \l {QBluetoothUuid::ClientCharacteristicConfiguration}{ClientCharacteristicConfiguration} descriptor prior to the change event on the peripheral. More details on how this might be @@ -348,6 +346,8 @@ QT_BEGIN_NAMESPACE the read operation is not successful, the \l error() signal is emitted using the \l DescriptorReadError flag. + \note This signal is only emitted for Central Role related use cases. + \sa readDescriptor() \since 5.5 */ @@ -648,7 +648,8 @@ void QLowEnergyService::readCharacteristic( The call results in a write request or command to a remote peripheral. If the operation is successful, the \l characteristicWritten() signal is emitted; otherwise the \l CharacteristicWriteError - is set. + is set. Calling this function does not trigger the a \l characteristicChanged() + signal unless the peripheral itself changes the value again after the current write request. The \a mode parameter determines whether the remote device should send a write confirmation. The to-be-written \a characteristic must support the relevant diff --git a/src/bluetooth/qlowenergyservice_osx.mm b/src/bluetooth/qlowenergyservice_osx.mm index 97d64040..52c2ac87 100644 --- a/src/bluetooth/qlowenergyservice_osx.mm +++ b/src/bluetooth/qlowenergyservice_osx.mm @@ -208,7 +208,9 @@ void QLowEnergyService::writeCharacteristic(const QLowEnergyCharacteristic &ch, WriteMode mode) { QLowEnergyControllerPrivateOSX *const controller = qt_mac_le_controller(d_ptr); - if (controller == Q_NULLPTR || state() != ServiceDiscovered || !contains(ch)) { + if (controller == Q_NULLPTR || + (controller->role == QLowEnergyController::CentralRole && state() != ServiceDiscovered) || + !contains(ch)) { d_ptr->setError(QLowEnergyService::OperationError); return; } @@ -250,6 +252,8 @@ void QLowEnergyService::writeDescriptor(const QLowEnergyDescriptor &descriptor, { QLowEnergyControllerPrivateOSX *const controller = qt_mac_le_controller(d_ptr); if (controller == Q_NULLPTR || state() != ServiceDiscovered || !contains(descriptor)) { + // This operation error also includes LE controller in the peripheral role: + // on iOS/OS X - descriptors are immutable. d_ptr->setError(OperationError); return; } diff --git a/src/imports/bluetooth/plugin.cpp b/src/imports/bluetooth/plugin.cpp index eca5736e..f77a4d95 100644 --- a/src/imports/bluetooth/plugin.cpp +++ b/src/imports/bluetooth/plugin.cpp @@ -57,7 +57,7 @@ QT_USE_NAMESPACE class QBluetoothQmlPlugin : public QQmlExtensionPlugin { Q_OBJECT - Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) public: QBluetoothQmlPlugin(QObject *parent = 0) : QQmlExtensionPlugin(parent) { initResources(); } void registerTypes(const char *uri) @@ -81,9 +81,9 @@ public: qmlRegisterType<QDeclarativeBluetoothService >(uri, major, minor, "BluetoothService"); qmlRegisterType<QDeclarativeBluetoothSocket >(uri, major, minor, "BluetoothSocket"); - // Register the 5.7 types - // introduces 5.7 version, other existing 5.2 exports become automatically available under 5.2-5.6 - minor = 7; + // Register the 5.8 types + // introduces 5.8 version, other existing 5.2 exports become automatically available under 5.2-5.7 + minor = 8; qmlRegisterType<QDeclarativeBluetoothDiscoveryModel >(uri, major, minor, "BluetoothDiscoveryModel"); } }; diff --git a/src/imports/bluetooth/plugins.qmltypes b/src/imports/bluetooth/plugins.qmltypes index 9c47126d..0ffe3223 100644 --- a/src/imports/bluetooth/plugins.qmltypes +++ b/src/imports/bluetooth/plugins.qmltypes @@ -4,17 +4,17 @@ import QtQuick.tooling 1.2 // It is used for QML tooling purposes only. // // This file was auto-generated by: -// 'qmlplugindump -nonrelocatable QtBluetooth 5.7' +// 'qmlplugindump -nonrelocatable QtBluetooth 5.8' Module { - dependencies: ["QtQuick 2.0"] + dependencies: ["QtQuick 2.8"] Component { name: "QDeclarativeBluetoothDiscoveryModel" prototype: "QAbstractListModel" exports: [ "QtBluetooth/BluetoothDiscoveryModel 5.0", "QtBluetooth/BluetoothDiscoveryModel 5.2", - "QtBluetooth/BluetoothDiscoveryModel 5.7" + "QtBluetooth/BluetoothDiscoveryModel 5.8" ] exportMetaObjectRevisions: [0, 0, 0] Enum { diff --git a/src/imports/nfc/plugin.cpp b/src/imports/nfc/plugin.cpp index 5f2ccd26..d003f531 100644 --- a/src/imports/nfc/plugin.cpp +++ b/src/imports/nfc/plugin.cpp @@ -59,7 +59,7 @@ QT_USE_NAMESPACE class QNfcQmlPlugin : public QQmlExtensionPlugin { Q_OBJECT - Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) public: QNfcQmlPlugin(QObject *parent = 0) : QQmlExtensionPlugin(parent) { initResources(); } @@ -98,8 +98,8 @@ public: minor = 5; qmlRegisterType<QDeclarativeNearField, 1>(uri, major, minor, "NearField"); - // Register the 5.6 - 5.7 types - minor = 7; + // Register the 5.6 - 5.8 types + minor = 8; qmlRegisterType<QDeclarativeNearField, 1>(uri, major, minor, "NearField"); } diff --git a/src/imports/nfc/plugins.qmltypes b/src/imports/nfc/plugins.qmltypes index 7bba6d95..c9fae196 100644 --- a/src/imports/nfc/plugins.qmltypes +++ b/src/imports/nfc/plugins.qmltypes @@ -4,10 +4,10 @@ import QtQuick.tooling 1.2 // It is used for QML tooling purposes only. // // This file was auto-generated by: -// 'qmlplugindump -nonrelocatable QtNfc 5.7' +// 'qmlplugindump -nonrelocatable QtNfc 5.8' Module { - dependencies: ["QtQuick 2.0"] + dependencies: ["QtQuick 2.8"] Component { name: "QDeclarativeNdefFilter" prototype: "QObject" @@ -58,7 +58,7 @@ Module { "QtNfc/NearField 5.2", "QtNfc/NearField 5.4", "QtNfc/NearField 5.5", - "QtNfc/NearField 5.7" + "QtNfc/NearField 5.8" ] exportMetaObjectRevisions: [0, 0, 0, 1, 1] Property { name: "messageRecords"; type: "QQmlNdefRecord"; isList: true; isReadonly: true } diff --git a/src/src.pro b/src/src.pro index 97b32522..38be9a4f 100644 --- a/src/src.pro +++ b/src/src.pro @@ -21,6 +21,10 @@ qtHaveModule(quick) { SUBDIRS += imports } -config_bluez:qtHaveModule(dbus) { - SUBDIRS += tools/sdpscanner +include($$OUT_PWD/bluetooth/qtbluetooth-config.pri) +QT_FOR_CONFIG += bluetooth-private +qtConfig(bluez):qtHaveModule(dbus) { + sdpscanner.subdir = tools/sdpscanner + sdpscanner.depends = bluetooth + SUBDIRS += sdpscanner } diff --git a/src/tools/sdpscanner/qt_attribution.json b/src/tools/sdpscanner/qt_attribution.json new file mode 100644 index 00000000..9f2ada92 --- /dev/null +++ b/src/tools/sdpscanner/qt_attribution.json @@ -0,0 +1,12 @@ +{ + "Id": "bluez", + "Name": "BlueZ", + "QDocModule": "qtbluetooth", + "QtUsage": "On Linux, Qt Bluetooth uses a separate executable, sdpscanner, to integrate with the official Linux bluetooth protocol stack BlueZ. The Qt Bluetooth library does not link against BlueZ directly.", + + "Description": "BlueZ", + "Homepage": "http://www.bluez.org/", + "LicenseId": "GPL-2.0", + "License": "GNU General Public License v2.0 only", + "Copyright": "Copyright (C) 2000-2016 BlueZ Project." +} diff --git a/src/tools/sdpscanner/sdpscanner.pro b/src/tools/sdpscanner/sdpscanner.pro index e4773cae..78610ebd 100644 --- a/src/tools/sdpscanner/sdpscanner.pro +++ b/src/tools/sdpscanner/sdpscanner.pro @@ -5,8 +5,8 @@ QT = core SOURCES = main.cpp -CONFIG += link_pkgconfig -PKGCONFIG_PRIVATE += bluez +QT_FOR_CONFIG += bluetooth-private +QMAKE_USE += bluez load(qt_tool) diff --git a/sync.profile b/sync.profile index d193d5ac..64dac6e3 100644 --- a/sync.profile +++ b/sync.profile @@ -4,3 +4,15 @@ ); %moduleheaders = ( # restrict the module headers to those found in relative path ); + +@ignore_for_include_check = ( + + # OBEX auto-generated headers + "adapter1_bluez5_p.h", "adapter_p.h", "agent_p.h", "device1_bluez5_p.h", + "device_p.h", "manager_p.h", "obex_agent_p.h", "obex_client1_bluez5_p.h", + "obex_client_p.h", "obex_manager_p.h", "obex_objectpush1_bluez5_p.h", + "obex_transfer1_bluez5_p.h", "obex_transfer_p.h", "objectmanager_p.h", + "profile1_p.h", "properties_p.h", "service_p.h", + # NFC auto-generated headers + # Note: "adapter_p.h", "agent_p.h" and "manager_p.h" are duplicated here + "dbusobjectmanager_p.h", "dbusproperties_p.h", "tag_p.h"); diff --git a/tests/auto/qbluetoothdevicediscoveryagent/qbluetoothdevicediscoveryagent.pro b/tests/auto/qbluetoothdevicediscoveryagent/qbluetoothdevicediscoveryagent.pro index 36c88cdc..96880930 100644 --- a/tests/auto/qbluetoothdevicediscoveryagent/qbluetoothdevicediscoveryagent.pro +++ b/tests/auto/qbluetoothdevicediscoveryagent/qbluetoothdevicediscoveryagent.pro @@ -2,5 +2,9 @@ SOURCES += tst_qbluetoothdevicediscoveryagent.cpp TARGET=tst_qbluetoothdevicediscoveryagent CONFIG += testcase -QT = core concurrent bluetooth testlib +QT = core concurrent bluetooth-private testlib osx:QT += widgets + +qtConfig(bluez):qtHaveModule(dbus) { + DEFINES += QT_BLUEZ_BLUETOOTH +} diff --git a/tests/auto/qbluetoothdevicediscoveryagent/tst_qbluetoothdevicediscoveryagent.cpp b/tests/auto/qbluetoothdevicediscoveryagent/tst_qbluetoothdevicediscoveryagent.cpp index 192173c8..efc4d8a6 100644 --- a/tests/auto/qbluetoothdevicediscoveryagent/tst_qbluetoothdevicediscoveryagent.cpp +++ b/tests/auto/qbluetoothdevicediscoveryagent/tst_qbluetoothdevicediscoveryagent.cpp @@ -77,8 +77,13 @@ private slots: void tst_deviceDiscovery_data(); void tst_deviceDiscovery(); + + void tst_discoveryTimeout(); + + void tst_discoveryMethods(); private: int noOfLocalDevices; + bool isBluez5Runtime = false; }; tst_QBluetoothDeviceDiscoveryAgent::tst_QBluetoothDeviceDiscoveryAgent() @@ -91,12 +96,59 @@ tst_QBluetoothDeviceDiscoveryAgent::~tst_QBluetoothDeviceDiscoveryAgent() { } +#ifdef QT_BLUEZ_BLUETOOTH +// This section was adopted from tst_qloggingcategory.cpp +QString logMessage; + +QByteArray qMyMessageFormatString(QtMsgType type, const QMessageLogContext &context, + const QString &str) +{ + QByteArray message; + message.append(context.category); + switch (type) { + case QtDebugMsg: message.append(".debug"); break; + case QtInfoMsg: message.append(".info"); break; + case QtWarningMsg: message.append(".warning"); break; + case QtCriticalMsg:message.append(".critical"); break; + case QtFatalMsg: message.append(".fatal"); break; + } + message.append(": "); + message.append(qPrintable(str)); + + return message.simplified(); +} + +static void myCustomMessageHandler(QtMsgType type, + const QMessageLogContext &context, + const QString &msg) +{ + logMessage = qMyMessageFormatString(type, context, msg); +} +#endif + + + void tst_QBluetoothDeviceDiscoveryAgent::initTestCase() { qRegisterMetaType<QBluetoothDeviceInfo>(); qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::InquiryType>(); +#ifdef QT_BLUEZ_BLUETOOTH + // To distinguish Bluez 4 and 5 we peek into the debug output + // of first Bluetooth ctor. It executes a runtime test and prints the result + // as logging output. This avoids more complex runtime detection logic within this unit test. + QtMessageHandler oldMessageHandler; + oldMessageHandler = qInstallMessageHandler(myCustomMessageHandler); + + noOfLocalDevices = QBluetoothLocalDevice::allDevices().count(); + qInstallMessageHandler(oldMessageHandler); + isBluez5Runtime = logMessage.contains(QStringLiteral("Bluez 5")); + if (isBluez5Runtime) + qDebug() << "BlueZ 5 runtime detected."; +#else noOfLocalDevices = QBluetoothLocalDevice::allDevices().count(); +#endif + if (!noOfLocalDevices) return; @@ -419,10 +471,10 @@ void tst_QBluetoothDeviceDiscoveryAgent::tst_deviceDiscovery() } } } -#ifdef Q_OS_IOS - //On iOS, we do not have access to the local device/adapter, numberOfAdapters is 0, +#if defined(Q_OS_IOS) || defined(Q_OS_WINRT) + //On iOS/WinRT, we do not have access to the local device/adapter, numberOfAdapters is 0, //so we skip this test at all. - QSKIP("iOS: no local Bluetooth device available. Skipping remaining part of test."); + QSKIP("iOS/WinRT: no local Bluetooth device available. Skipping remaining part of test."); #endif //For multiple Bluetooth adapter do the check only for GeneralUnlimitedInquiry. @@ -431,6 +483,165 @@ void tst_QBluetoothDeviceDiscoveryAgent::tst_deviceDiscovery() } } + +void tst_QBluetoothDeviceDiscoveryAgent::tst_discoveryTimeout() +{ + QBluetoothDeviceDiscoveryAgent agent; + + // check default values +#if defined(Q_OS_OSX) || defined(Q_OS_IOS) || defined(Q_OS_ANDROID) || defined(Q_OS_WINRT) + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 25000); + agent.setLowEnergyDiscoveryTimeout(-1); // negative ignored + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 25000); + agent.setLowEnergyDiscoveryTimeout(20000); + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 20000); +#elif defined(QT_BLUEZ_BLUETOOTH) + if (isBluez5Runtime) { + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 20000); + agent.setLowEnergyDiscoveryTimeout(-1); // negative ignored + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 20000); + agent.setLowEnergyDiscoveryTimeout(25000); + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 25000); + } else { + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), -1); + agent.setLowEnergyDiscoveryTimeout(20000); // feature not supported -> ignored + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), -1); + } +#else + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), -1); + agent.setLowEnergyDiscoveryTimeout(20000); // feature not supported -> ignored + QCOMPARE(agent.lowEnergyDiscoveryTimeout(), -1); +#endif +} + +void tst_QBluetoothDeviceDiscoveryAgent::tst_discoveryMethods() +{ + const QBluetoothLocalDevice localDevice; + if (localDevice.allDevices().size() != 1) { + // On iOS it returns 0 but we still have working BT. +#ifndef Q_OS_IOS + QSKIP("This test expects exactly one local device working"); +#endif + } + + const QBluetoothDeviceDiscoveryAgent::DiscoveryMethods + supportedMethods = QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods(); + + QVERIFY(supportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod); + + QBluetoothDeviceDiscoveryAgent::DiscoveryMethod + unsupportedMethods = QBluetoothDeviceDiscoveryAgent::NoMethod; + QBluetoothDeviceInfo::CoreConfiguration + expectedConfiguration = QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration; + + if (supportedMethods == QBluetoothDeviceDiscoveryAgent::ClassicMethod) { + unsupportedMethods = QBluetoothDeviceDiscoveryAgent::LowEnergyMethod; + expectedConfiguration = QBluetoothDeviceInfo::BaseRateCoreConfiguration; + } else if (supportedMethods == QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) { + unsupportedMethods = QBluetoothDeviceDiscoveryAgent::ClassicMethod; + expectedConfiguration = QBluetoothDeviceInfo::LowEnergyCoreConfiguration; + } + + QBluetoothDeviceDiscoveryAgent agent; + QSignalSpy finishedSpy(&agent, SIGNAL(finished())); + QSignalSpy errorSpy(&agent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error))); + QSignalSpy discoveredSpy(&agent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo))); + + // NoMethod - should just immediately return: + agent.start(QBluetoothDeviceDiscoveryAgent::NoMethod); + QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError); + QVERIFY(!agent.isActive()); + QCOMPARE(finishedSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); + QCOMPARE(discoveredSpy.size(), 0); + + if (unsupportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod) { + agent.start(unsupportedMethods); + QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); + QVERIFY(!agent.isActive()); + QVERIFY(finishedSpy.isEmpty()); + QCOMPARE(errorSpy.size(), 1); + errorSpy.clear(); + QVERIFY(discoveredSpy.isEmpty()); + } + + // Start discovery, probably both Classic and LE methods: + agent.start(supportedMethods); + QVERIFY(agent.isActive()); + QVERIFY(errorSpy.isEmpty()); + + +#define RUN_DISCOVERY(maxTimeout, step, condition) \ + for (int scanTime = maxTimeout; (condition) && scanTime > 0; scanTime -= step) \ + QTest::qWait(step); + + // Wait for up to MaxScanTime for the scan to finish + const int timeStep = 15000; + RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty()) + + QVERIFY(!agent.isActive()); + QVERIFY(errorSpy.size() <= 1); + + if (errorSpy.size()) { + // For example, old iOS device could report it supports LE method, + // but it actually does not. + QVERIFY(supportedMethods == QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); + } else { + QVERIFY(finishedSpy.count() == 1); + QVERIFY(agent.error() == QBluetoothDeviceDiscoveryAgent::NoError); + QVERIFY(agent.errorString().isEmpty()); + + while (!discoveredSpy.isEmpty()) { + const QBluetoothDeviceInfo info = + qvariant_cast<QBluetoothDeviceInfo>(discoveredSpy.takeFirst().at(0)); + QVERIFY(info.isValid()); + QVERIFY(info.coreConfigurations() & expectedConfiguration); + } + } + + if (unsupportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod) + return; + + // Both methods were reported as supported. We already tested them + // above, now let's test first Classic then LE. + finishedSpy.clear(); + errorSpy.clear(); + discoveredSpy.clear(); + + agent.start(QBluetoothDeviceDiscoveryAgent::ClassicMethod); + QVERIFY(agent.isActive()); + QVERIFY(errorSpy.isEmpty()); + QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError); + + RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty()) + + QVERIFY(!agent.isActive()); + QVERIFY(errorSpy.isEmpty()); + QCOMPARE(finishedSpy.size(), 1); + + finishedSpy.clear(); + discoveredSpy.clear(); + + agent.start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + QVERIFY(agent.isActive()); + QVERIFY(errorSpy.isEmpty()); + + RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty() && errorSpy.isEmpty()) + + QVERIFY(!agent.isActive()); + QVERIFY(errorSpy.size() <= 1); + + if (errorSpy.size()) { + QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); + qDebug() << "QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods is inaccurate" + " on your platform/with your device, LowEnergyMethod is not supported"; + } else { + QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError); + QCOMPARE(finishedSpy.size(), 1); + } +} + QTEST_MAIN(tst_QBluetoothDeviceDiscoveryAgent) #include "tst_qbluetoothdevicediscoveryagent.moc" diff --git a/tests/auto/qbluetoothsocket/qbluetoothsocket.pro b/tests/auto/qbluetoothsocket/qbluetoothsocket.pro index 641ff6c7..8c9b2acb 100644 --- a/tests/auto/qbluetoothsocket/qbluetoothsocket.pro +++ b/tests/auto/qbluetoothsocket/qbluetoothsocket.pro @@ -3,7 +3,7 @@ TARGET = tst_qbluetoothsocket CONFIG += testcase testcase.timeout = 250 # this test is slow -QT = core concurrent network bluetooth testlib +QT = core concurrent network bluetooth-private testlib osx:QT += widgets OTHER_FILES += \ @@ -11,8 +11,8 @@ OTHER_FILES += \ osx { DEFINES += QT_OSX_BLUETOOTH -} else:android { +} else: android { DEFINES += QT_ANDROID_BLUETOOTH -} config_bluez:qtHaveModule(dbus) { +} else: qtConfig(bluez):qtHaveModule(dbus) { DEFINES += QT_BLUEZ_BLUETOOTH } diff --git a/tests/auto/qlowenergycontroller-gattserver/test/test.pro b/tests/auto/qlowenergycontroller-gattserver/test/test.pro index 45cff660..fc9c7a18 100644 --- a/tests/auto/qlowenergycontroller-gattserver/test/test.pro +++ b/tests/auto/qlowenergycontroller-gattserver/test/test.pro @@ -3,7 +3,7 @@ QT = core bluetooth bluetooth-private testlib TARGET = tst_qlowenergycontroller-gattserver CONFIG += testcase c++11 -config_linux_crypto_api:DEFINES += CONFIG_LINUX_CRYPTO_API -config_bluez_le:DEFINES += CONFIG_BLUEZ_LE +qtConfig(linux_crypto_api): DEFINES += CONFIG_LINUX_CRYPTO_API +qtConfig(bluez_le): DEFINES += CONFIG_BLUEZ_LE SOURCES += tst_qlowenergycontroller-gattserver.cpp diff --git a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp index 773c673b..6475b8c4 100644 --- a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp +++ b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp @@ -120,7 +120,7 @@ tst_QLowEnergyController::~tst_QLowEnergyController() void tst_QLowEnergyController::initTestCase() { -#ifndef Q_OS_MAC +#if !defined(Q_OS_MAC) && !defined(Q_OS_WINRT) if (remoteDevice.isNull() || QBluetoothLocalDevice::allDevices().isEmpty()) { qWarning("No remote device or local adapter found."); @@ -142,7 +142,7 @@ void tst_QLowEnergyController::initTestCase() QVERIFY(finishedSpy.isEmpty()); bool deviceFound = false; - devAgent->start(); + devAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); QTRY_VERIFY_WITH_TIMEOUT(finishedSpy.count() > 0, 30000); foreach (const QBluetoothDeviceInfo &info, devAgent->discoveredDevices()) { #ifndef Q_OS_MAC @@ -244,7 +244,7 @@ void tst_QLowEnergyController::tst_connect() { QList<QBluetoothHostInfo> localAdapters = QBluetoothLocalDevice::allDevices(); -#if defined(Q_OS_IOS) || defined(Q_OS_TVOS) +#if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(Q_OS_WINRT) if (!remoteDeviceInfo.isValid()) #else if (localAdapters.isEmpty() || !remoteDeviceInfo.isValid()) @@ -252,6 +252,7 @@ void tst_QLowEnergyController::tst_connect() QSKIP("No local Bluetooth or remote BTLE device found. Skipping test."); QLowEnergyController control(remoteDeviceInfo); + QCOMPARE(remoteDeviceInfo.deviceUuid(), control.remoteDeviceUuid()); QCOMPARE(control.role(), QLowEnergyController::CentralRole); QSignalSpy connectedSpy(&control, SIGNAL(connected())); QSignalSpy disconnectedSpy(&control, SIGNAL(disconnected())); @@ -260,7 +261,7 @@ void tst_QLowEnergyController::tst_connect() else QCOMPARE(control.remoteName(), remoteDeviceInfo.name()); -#if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) +#if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(Q_OS_WINRT) const QBluetoothAddress localAdapter = localAdapters.at(0).address(); QCOMPARE(control.localAddress(), localAdapter); QVERIFY(!control.localAddress().isNull()); @@ -403,7 +404,7 @@ void tst_QLowEnergyController::tst_connect() void tst_QLowEnergyController::tst_concurrentDiscovery() { -#ifndef Q_OS_MAC +#if !defined(Q_OS_MAC) && !defined(Q_OS_WINRT) QList<QBluetoothHostInfo> localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); @@ -440,7 +441,7 @@ void tst_QLowEnergyController::tst_concurrentDiscovery() 30000); } -#ifdef Q_OS_ANDROID +#if defined(Q_OS_ANDROID) || defined(Q_OS_WINRT) QCOMPARE(control.state(), QLowEnergyController::ConnectedState); QCOMPARE(control2.state(), QLowEnergyController::ConnectedState); control2.disconnectFromDevice(); @@ -1642,7 +1643,7 @@ void tst_QLowEnergyController::tst_defaultBehavior() void tst_QLowEnergyController::tst_writeCharacteristic() { -#ifndef Q_OS_MAC +#if !defined(Q_OS_MAC) && !defined(Q_OS_WINRT) QList<QBluetoothHostInfo> localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); @@ -1816,7 +1817,7 @@ void tst_QLowEnergyController::tst_writeCharacteristic() void tst_QLowEnergyController::tst_readWriteDescriptor() { -#ifndef Q_OS_MAC +#if !defined(Q_OS_MAC) && !defined(Q_OS_WINRT) QList<QBluetoothHostInfo> localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); @@ -2239,7 +2240,7 @@ void tst_QLowEnergyController::tst_customProgrammableDevice() */ void tst_QLowEnergyController::tst_errorCases() { -#ifndef Q_OS_MAC +#if !defined(Q_OS_MAC) && !defined(Q_OS_WINRT) QList<QBluetoothHostInfo> localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); @@ -2461,7 +2462,7 @@ void tst_QLowEnergyController::tst_errorCases() */ void tst_QLowEnergyController::tst_writeCharacteristicNoResponse() { -#ifndef Q_OS_MAC +#if !defined(Q_OS_MAC) && !defined(Q_OS_WINRT) QList<QBluetoothHostInfo> localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); |