summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Blasche <alexander.blasche@qt.io>2017-01-19 16:27:24 +0100
committerAlex Blasche <alexander.blasche@qt.io>2017-01-27 09:19:26 +0000
commit3cf6938938c7d56f04b43cfd951a2d6b31db7ee1 (patch)
treee2fe58c5a41852710bddc4a8de08b671c5d028d9
parent3a978da2e9b1614a6f3abc1af55ae42ac86cb4c0 (diff)
Implement QLEService::writeCharacteristic() for Android peripheral
This includes the correct handling of client characteristic notifications (CCNs). As per Bluetooth spec this descriptor is unique for each device. If a characteristic was changed all chars with enabled CCNs are notified. CCN settings are even retained while the remote device is not connected. Change-Id: Iec612e6a5e70206a6be0263e10e615c26def7559 Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io> Reviewed-by: Alex Blasche <alexander.blasche@qt.io>
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java199
-rw-r--r--src/bluetooth/qlowenergycontroller_android.cpp56
2 files changed, 230 insertions, 25 deletions
diff --git a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java
index 76b738bd..7930d0fd 100644
--- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java
+++ b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java
@@ -58,7 +58,13 @@ import android.bluetooth.le.BluetoothLeAdvertiser;
import android.os.ParcelUuid;
import android.util.Log;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.HashMap;
import java.util.UUID;
public class QtBluetoothLEServer {
@@ -76,6 +82,115 @@ public class QtBluetoothLEServer {
private BluetoothGattServer mGattServer = null;
private BluetoothLeAdvertiser mLeAdvertiser = null;
+ /*
+ As per Bluetooth specification each connected device can have individual and persistent
+ Client characteristic configurations (see Bluetooth Spec 5.0 Vol 3 Part G 3.3.3.3)
+ This class manages the existing configurrations.
+ */
+ private class ClientCharacteristicManager {
+ private final HashMap<BluetoothGattCharacteristic, List<Entry>> notificationStore = new HashMap<BluetoothGattCharacteristic, List<Entry>>();
+
+ private class Entry {
+ BluetoothDevice device = null;
+ byte[] value = null;
+ boolean isConnected = false;
+ }
+
+ public void insertOrUpdate(BluetoothGattCharacteristic characteristic,
+ BluetoothDevice device, byte[] newValue)
+ {
+ if (notificationStore.containsKey(characteristic)) {
+
+ List<Entry> entries = notificationStore.get(characteristic);
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).device.equals(device)) {
+ Entry e = entries.get(i);
+ e.value = newValue;
+ entries.set(i, e);
+ return;
+ }
+ }
+
+ // not match so far -> add device to list
+ Entry e = new Entry();
+ e.device = device;
+ e.value = newValue;
+ e.isConnected = true;
+ entries.add(e);
+ return;
+ }
+
+ // new characteristic
+ Entry e = new Entry();
+ e.device = device;
+ e.value = newValue;
+ e.isConnected = true;
+ List<Entry> list = new LinkedList<Entry>();
+ list.add(e);
+ notificationStore.put(characteristic, list);
+ }
+
+ /*
+ Marks client characteristic configuration entries as (in)active based the associated
+ devices general connectivity state.
+ This function avoids that existing configurations are not acted
+ upon when the associated device is not connected.
+ */
+ public void markDeviceConnectivity(BluetoothDevice device, boolean isConnected)
+ {
+ final Iterator<BluetoothGattCharacteristic> keys = notificationStore.keySet().iterator();
+ while (keys.hasNext()) {
+ final BluetoothGattCharacteristic characteristic = keys.next();
+ final List<Entry> entries = notificationStore.get(characteristic);
+ if (entries == null)
+ continue;
+
+ ListIterator<Entry> charConfig = entries.listIterator();
+ while (charConfig.hasNext()) {
+ Entry e = charConfig.next();
+ if (e.device.equals(device))
+ e.isConnected = isConnected;
+ }
+ }
+ }
+
+ // Returns list of all BluetoothDevices which require notification or indication.
+ // No match returns an empty list.
+ List<BluetoothDevice> getToBeUpdatedDevices(BluetoothGattCharacteristic characteristic)
+ {
+ ArrayList<BluetoothDevice> result = new ArrayList<BluetoothDevice>();
+ if (!notificationStore.containsKey(characteristic))
+ return result;
+
+ final ListIterator<Entry> iter = notificationStore.get(characteristic).listIterator();
+ while (iter.hasNext())
+ result.add(iter.next().device);
+
+ return result;
+ }
+
+ // Returns null if no match; otherwise the configured actual client characteristic
+ // configuration value
+ byte[] valueFor(BluetoothGattCharacteristic characteristic, BluetoothDevice device)
+ {
+ if (!notificationStore.containsKey(characteristic))
+ return null;
+
+ List<Entry> entries = notificationStore.get(characteristic);
+ for (int i = 0; i < entries.size(); i++) {
+ final Entry entry = entries.get(i);
+ if (entry.device.equals(device) && entry.isConnected == true)
+ return entries.get(i).value;
+ }
+
+ return null;
+ }
+ }
+
+ private static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = UUID
+ .fromString("00002902-0000-1000-8000-00805f9b34fb");
+ ClientCharacteristicManager clientCharacteristicManager = new ClientCharacteristicManager();
+
public QtBluetoothLEServer(Context context)
{
qtContext = context;
@@ -115,8 +230,10 @@ public class QtBluetoothLEServer {
switch (newState) {
case BluetoothProfile.STATE_DISCONNECTED:
qtControllerState = 0; // QLowEnergyController::UnconnectedState
+ clientCharacteristicManager.markDeviceConnectivity(device, false);
break;
case BluetoothProfile.STATE_CONNECTED:
+ clientCharacteristicManager.markDeviceConnectivity(device, true);
qtControllerState = 2; // QLowEnergyController::ConnectedState
break;
}
@@ -161,11 +278,12 @@ public class QtBluetoothLEServer {
{
Log.w(TAG, "onCharacteristicWriteRequest");
int resultStatus = BluetoothGatt.GATT_SUCCESS;
-
+ boolean sendNotificationOrIndication = false;
if (!preparedWrite) { // regular write
if (offset == 0) {
characteristic.setValue(value);
leServerCharacteristicChanged(qtObject, characteristic, value);
+ sendNotificationOrIndication = true;
} else {
// This should not really happen as per Bluetooth spec
Log.w(TAG, "onCharacteristicWriteRequest: !preparedWrite, offset " + offset + ", Not supported");
@@ -185,6 +303,8 @@ public class QtBluetoothLEServer {
if (responseNeeded)
mGattServer.sendResponse(device, requestId, resultStatus, offset, value);
+ if (sendNotificationOrIndication)
+ sendNotificationsOrIndications(characteristic);
super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
}
@@ -192,12 +312,18 @@ public class QtBluetoothLEServer {
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor)
{
- byte[] dataArray;
+ byte[] dataArray = descriptor.getValue();
try {
- dataArray = Arrays.copyOfRange(descriptor.getValue(), offset, descriptor.getValue().length);
+ if (descriptor.getUuid().equals(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)) {
+ dataArray = clientCharacteristicManager.valueFor(descriptor.getCharacteristic(), device);
+ if (dataArray == null)
+ dataArray = descriptor.getValue();
+ }
+
+ dataArray = Arrays.copyOfRange(dataArray, offset, dataArray.length);
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, dataArray);
} catch (Exception ex) {
- Log.w(TAG, "onDescriptorReadRequest: " + requestId + " " + offset + " " + descriptor.getValue().length);
+ Log.w(TAG, "onDescriptorReadRequest: " + requestId + " " + offset + " " + dataArray.length);
ex.printStackTrace();
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
}
@@ -213,6 +339,12 @@ public class QtBluetoothLEServer {
if (!preparedWrite) { // regular write
if (offset == 0) {
descriptor.setValue(value);
+
+ if (descriptor.getUuid().equals(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)) {
+ clientCharacteristicManager.insertOrUpdate(descriptor.getCharacteristic(),
+ device, value);
+ }
+
leServerDescriptorWritten(qtObject, descriptor, value);
} else {
// This should not really happen as per Bluetooth spec
@@ -248,6 +380,7 @@ public class QtBluetoothLEServer {
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
super.onNotificationSent(device, status);
+ Log.w(TAG, "onNotificationSent" + device + " " + status);
}
// MTU change disabled since it requires API level 22. Right now we only enforce lvl 21
@@ -306,6 +439,64 @@ public class QtBluetoothLEServer {
}
/*
+ Check the client characteristics configuration for the given characteristic
+ and sends notifications or indications as per required.
+ */
+ private void sendNotificationsOrIndications(BluetoothGattCharacteristic characteristic)
+ {
+ final ListIterator<BluetoothDevice> iter =
+ clientCharacteristicManager.getToBeUpdatedDevices(characteristic).listIterator();
+
+ // TODO This quick loop over multiple devices should be synced with onNotificationSent().
+ // The next notifyCharacteristicChanged() call must wait until onNotificationSent()
+ // was received. At this becomes an issue when the server accepts multiple remote
+ // devices at the same time.
+ while (iter.hasNext()) {
+ final BluetoothDevice device = iter.next();
+ final byte[] clientCharacteristicConfig = clientCharacteristicManager.valueFor(characteristic, device);
+ if (clientCharacteristicConfig != null) {
+ if (Arrays.equals(clientCharacteristicConfig, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
+ mGattServer.notifyCharacteristicChanged(device, characteristic, false);
+ } else if (Arrays.equals(clientCharacteristicConfig, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)) {
+ mGattServer.notifyCharacteristicChanged(device, characteristic, true);
+ }
+ }
+ }
+ }
+
+ /*
+ Updates the local database value for the given characteristic with \a charUuid and
+ \a newValue. If notifications for this task are enabled an approproiate notification will
+ be send to the remote client.
+
+ This function is called from the Qt thread.
+ */
+ public boolean writeCharacteristic(BluetoothGattService service, UUID charUuid, byte[] newValue)
+ {
+ BluetoothGattCharacteristic foundChar = null;
+ List<BluetoothGattCharacteristic> charList = service.getCharacteristics();
+ for (BluetoothGattCharacteristic iter: charList) {
+ if (iter.getUuid().equals(charUuid) && foundChar == null) {
+ foundChar = iter;
+ // don't break here since we want to check next condition below on next iteration
+ } else if (iter.getUuid().equals(charUuid)) {
+ Log.w(TAG, "Found second char with same UUID. Wrong char may have been selected.");
+ break;
+ }
+ }
+
+ if (foundChar == null) {
+ Log.w(TAG, "writeCharacteristic: update for unknown characteristic failed");
+ return false;
+ }
+
+ foundChar.setValue(newValue);
+ sendNotificationsOrIndications(foundChar);
+
+ return true;
+ }
+
+ /*
* Call back handler for Advertisement requests.
*/
private AdvertiseCallback mAdvertiseListener = new AdvertiseCallback()
diff --git a/src/bluetooth/qlowenergycontroller_android.cpp b/src/bluetooth/qlowenergycontroller_android.cpp
index c21705cf..3934bd99 100644
--- a/src/bluetooth/qlowenergycontroller_android.cpp
+++ b/src/bluetooth/qlowenergycontroller_android.cpp
@@ -54,6 +54,21 @@ Q_DECLARE_LOGGING_CATEGORY(QT_BT_ANDROID)
Q_DECLARE_METATYPE(QAndroidJniObject)
+// Conversion: QBluetoothUuid -> java.util.UUID
+static QAndroidJniObject javaUuidfromQtUuid(const QBluetoothUuid& uuid)
+{
+ QString output = uuid.toString();
+ // cut off leading and trailing brackets
+ output = output.mid(1, output.size()-2);
+
+ QAndroidJniObject javaString = QAndroidJniObject::fromString(output);
+ QAndroidJniObject javaUuid = QAndroidJniObject::callStaticObjectMethod(
+ "java/util/UUID", "fromString", "(Ljava/lang/String;)Ljava/util/UUID;",
+ javaString.object());
+
+ return javaUuid;
+}
+
QLowEnergyControllerPrivate::QLowEnergyControllerPrivate()
: QObject(),
state(QLowEnergyController::UnconnectedState),
@@ -239,12 +254,26 @@ void QLowEnergyControllerPrivate::writeCharacteristic(
bool result = false;
if (hub) {
- qCDebug(QT_BT_ANDROID) << "Write characteristic with handle " << charHandle
- << newValue.toHex() << "(service:" << service->uuid
- << ", writeWithResponse:" << (mode == QLowEnergyService::WriteWithResponse)
- << ", signed:" << (mode == QLowEnergyService::WriteSigned) << ")";
- result = hub->javaObject().callMethod<jboolean>("writeCharacteristic", "(I[BI)Z",
- charHandle, payload, mode);
+ if (role == QLowEnergyController::CentralRole) {
+ qCDebug(QT_BT_ANDROID) << "Write characteristic with handle " << charHandle
+ << newValue.toHex() << "(service:" << service->uuid
+ << ", writeWithResponse:" << (mode == QLowEnergyService::WriteWithResponse)
+ << ", signed:" << (mode == QLowEnergyService::WriteSigned) << ")";
+ result = hub->javaObject().callMethod<jboolean>("writeCharacteristic", "(I[BI)Z",
+ charHandle, payload, mode);
+ } else { // peripheral mode
+ qCDebug(QT_BT_ANDROID) << "Write server characteristic with handle " << charHandle
+ << newValue.toHex() << "(service:" << service->uuid;
+
+ const auto &characteristic = characteristicForHandle(charHandle);
+ if (characteristic.isValid()) {
+ const QAndroidJniObject charUuid = javaUuidfromQtUuid(characteristic.uuid());
+ result = hub->javaObject().callMethod<jboolean>(
+ "writeCharacteristic",
+ "(Landroid/bluetooth/BluetoothGattService;Ljava/util/UUID;[B)Z",
+ service->androidService.object(), charUuid.object(), payload);
+ }
+ }
}
if (env->ExceptionOccurred()) {
@@ -1007,21 +1036,6 @@ void QLowEnergyControllerPrivate::requestConnectionUpdate(const QLowEnergyConnec
qCWarning(QT_BT_ANDROID) << "Cannot set connection update priority";
}
-// Conversion: QBluetoothUuid -> java.util.UUID
-static QAndroidJniObject javaUuidfromQtUuid(const QBluetoothUuid& uuid)
-{
- QString output = uuid.toString();
- // cut off leading and trailing brackets
- output = output.mid(1, output.size()-2);
-
- QAndroidJniObject javaString = QAndroidJniObject::fromString(output);
- QAndroidJniObject javaUuid = QAndroidJniObject::callStaticObjectMethod(
- "java/util/UUID", "fromString", "(Ljava/lang/String;)Ljava/util/UUID;",
- javaString.object());
-
- return javaUuid;
-}
-
/*
* Returns the Java char permissions based on the given characteristic data.
*/