1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-19 06:51:57 +02:00

Merge BLE rework/fixes done in #335

This commit is contained in:
Erik Johansson
2018-10-29 21:36:09 +01:00
4 changed files with 222 additions and 202 deletions

View File

@@ -196,6 +196,8 @@ public class OpenScale {
selectScaleUser(-1); selectScaleUser(-1);
throw new Exception("could not find the selected user"); throw new Exception("could not find the selected user");
} }
Timber.d("Selected user is now %s (%d)",
selectedScaleUser.getUserName(), selectedScaleUser.getId());
return selectedScaleUser; return selectedScaleUser;
} }
} catch (Exception e) { } catch (Exception e) {

View File

@@ -78,10 +78,8 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
private static final UUID SERVICE_CHANGED = private static final UUID SERVICE_CHANGED =
UUID.fromString("00002A05-0000-1000-8000-00805F9B34FB"); UUID.fromString("00002A05-0000-1000-8000-00805F9B34FB");
private static final UUID CLIENT_CHARACTERISTICS_CONFIGURATION_BEURER = private static final UUID CLIENT_CHARACTERISTICS_CONFIGURATION =
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"); UUID.fromString("00002902-0000-1000-8000-00805F9B34FB");
private static final UUID CLIENT_CHARACTERISTICS_CONFIGURATION_SANITAS =
UUID.fromString("00002901-0000-1000-8000-00805F9B34FB");
private static final UUID CUSTOM_SERVICE_1 = private static final UUID CUSTOM_SERVICE_1 =
UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB"); UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB");
@@ -156,10 +154,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
seenUsers = new TreeSet<>(); seenUsers = new TreeSet<>();
// Setup notification // Setup notification
UUID clientCharacteristicsConfiguration = deviceType == DeviceType.SANITAS_SBF70_70 setNotificationOn(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT, CLIENT_CHARACTERISTICS_CONFIGURATION);
? CLIENT_CHARACTERISTICS_CONFIGURATION_SANITAS
: CLIENT_CHARACTERISTICS_CONFIGURATION_BEURER;
setNotificationOn(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT, clientCharacteristicsConfiguration);
break; break;
case 1: case 1:
// Say "Hello" to the scale // Say "Hello" to the scale
@@ -486,6 +481,12 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
(byte) (data[2] & 0xFF), (byte) (data[3] & 0xFF), (byte) (data[2] & 0xFF), (byte) (data[3] & 0xFF),
}); });
if (currentScaleUserId == 0) {
Timber.i("Initial weight set; disconnecting...");
setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE);
return;
}
return; return;
} }

View File

@@ -56,6 +56,7 @@ public abstract class BluetoothCommunication {
private Handler callbackBtHandler; private Handler callbackBtHandler;
private Handler handler; private Handler handler;
private static boolean autoConnect = false;
private BluetoothGatt bluetoothGatt; private BluetoothGatt bluetoothGatt;
private boolean connectionEstablished; private boolean connectionEstablished;
private BluetoothGattCallback gattCallback; private BluetoothGattCallback gattCallback;
@@ -80,7 +81,6 @@ public abstract class BluetoothCommunication {
private Queue<GattObjectValue<BluetoothGattDescriptor>> descriptorRequestQueue; private Queue<GattObjectValue<BluetoothGattDescriptor>> descriptorRequestQueue;
private Queue<GattObjectValue<BluetoothGattCharacteristic>> characteristicRequestQueue; private Queue<GattObjectValue<BluetoothGattCharacteristic>> characteristicRequestQueue;
private boolean openRequest; private boolean openRequest;
private final Object lock = new Object();
public BluetoothCommunication(Context context) public BluetoothCommunication(Context context)
{ {
@@ -240,10 +240,8 @@ public abstract class BluetoothCommunication {
* @param btMachineState the machine state that should be set. * @param btMachineState the machine state that should be set.
*/ */
protected void setBtMachineState(BT_MACHINE_STATE btMachineState) { protected void setBtMachineState(BT_MACHINE_STATE btMachineState) {
synchronized (lock) { this.btMachineState = btMachineState;
this.btMachineState = btMachineState; handleRequests();
handleRequests();
}
} }
/** /**
@@ -254,13 +252,11 @@ public abstract class BluetoothCommunication {
* @param bytes the bytes that should be write * @param bytes the bytes that should be write
*/ */
protected void writeBytes(UUID service, UUID characteristic, byte[] bytes) { protected void writeBytes(UUID service, UUID characteristic, byte[] bytes) {
synchronized (lock) { characteristicRequestQueue.add(
characteristicRequestQueue.add( new GattObjectValue<>(
new GattObjectValue<>( bluetoothGatt.getService(service).getCharacteristic(characteristic),
bluetoothGatt.getService(service).getCharacteristic(characteristic), bytes));
bytes)); handleRequests();
handleRequests();
}
} }
/** /**
@@ -301,13 +297,11 @@ public abstract class BluetoothCommunication {
bluetoothGatt.getService(service).getCharacteristic(characteristic); bluetoothGatt.getService(service).getCharacteristic(characteristic);
bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true);
synchronized (lock) { descriptorRequestQueue.add(
descriptorRequestQueue.add( new GattObjectValue<>(
new GattObjectValue<>( gattCharacteristic.getDescriptor(descriptor),
gattCharacteristic.getDescriptor(descriptor), BluetoothGattDescriptor.ENABLE_INDICATION_VALUE));
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)); handleRequests();
handleRequests();
}
} }
catch (Exception e) { catch (Exception e) {
Timber.e(e); Timber.e(e);
@@ -328,13 +322,11 @@ public abstract class BluetoothCommunication {
bluetoothGatt.getService(service).getCharacteristic(characteristic); bluetoothGatt.getService(service).getCharacteristic(characteristic);
bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true);
synchronized (lock) { descriptorRequestQueue.add(
descriptorRequestQueue.add( new GattObjectValue<>(
new GattObjectValue<>( gattCharacteristic.getDescriptor(descriptor),
gattCharacteristic.getDescriptor(descriptor), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE));
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)); handleRequests();
handleRequests();
}
} }
catch (Exception e) { catch (Exception e) {
Timber.e(e); Timber.e(e);
@@ -354,13 +346,11 @@ public abstract class BluetoothCommunication {
bluetoothGatt.getService(service).getCharacteristic(characteristic); bluetoothGatt.getService(service).getCharacteristic(characteristic);
bluetoothGatt.setCharacteristicNotification(gattCharacteristic, false); bluetoothGatt.setCharacteristicNotification(gattCharacteristic, false);
synchronized (lock) { descriptorRequestQueue.add(
descriptorRequestQueue.add( new GattObjectValue<>(
new GattObjectValue<>( gattCharacteristic.getDescriptor(descriptor),
gattCharacteristic.getDescriptor(descriptor), BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)); handleRequests();
handleRequests();
}
} }
/** /**
@@ -417,12 +407,12 @@ public abstract class BluetoothCommunication {
public void connect(String hwAddress) { public void connect(String hwAddress) {
logBluetoothStatus(); logBluetoothStatus();
disconnect(false);
btAdapter.cancelDiscovery();
// Some good tips to improve BLE connections: // Some good tips to improve BLE connections:
// https://android.jlelse.eu/lessons-for-first-time-android-bluetooth-le-developers-i-learned-the-hard-way-fee07646624 // https://android.jlelse.eu/lessons-for-first-time-android-bluetooth-le-developers-i-learned-the-hard-way-fee07646624
btAdapter.cancelDiscovery();
stopLeScan();
// Don't do any cleanup if disconnected before fully connected // Don't do any cleanup if disconnected before fully connected
btMachineState = BT_MACHINE_STATE.BT_CLEANUP_STATE; btMachineState = BT_MACHINE_STATE.BT_CLEANUP_STATE;
@@ -436,7 +426,7 @@ public abstract class BluetoothCommunication {
} }
else { else {
Timber.d("No coarse location permission, connecting without LE scan"); Timber.d("No coarse location permission, connecting without LE scan");
connectGatt(hwAddress); connectGatt(btAdapter.getRemoteDevice(hwAddress));
} }
} }
@@ -458,49 +448,53 @@ public abstract class BluetoothCommunication {
} }
private void connectGatt(BluetoothDevice device) { private void connectGatt(BluetoothDevice device) {
Timber.i("Connecting to [%s] (driver: %s)", device.getAddress(), driverName()); Timber.i("Connecting to [%s] (%sdriver: %s)",
device.getAddress(), autoConnect ? "auto connect, " : "", driverName());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
bluetoothGatt = device.connectGatt( bluetoothGatt = device.connectGatt(
context, false, gattCallback, BluetoothDevice.TRANSPORT_LE); context, autoConnect, gattCallback, BluetoothDevice.TRANSPORT_LE);
} }
else { else {
bluetoothGatt = device.connectGatt(context, false, gattCallback); bluetoothGatt = device.connectGatt(context, autoConnect, gattCallback);
} }
} }
private void connectGatt(String hwAddress) {
connectGatt(btAdapter.getRemoteDevice(hwAddress));
}
private void startLeScanForDevice(final String hwAddress) { private void startLeScanForDevice(final String hwAddress) {
leScanCallback = new BluetoothAdapter.LeScanCallback() { leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override @Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
Timber.d("Found LE device %s [%s]", device.getName(), device.getAddress()); Timber.d("Found LE device %s [%s]", device.getName(), device.getAddress());
if (!device.getAddress().equals(hwAddress)) { if (!device.getAddress().equals(hwAddress)) {
return; return;
} }
synchronized (lock) { // Stop timeout and connect to the device on the main thread
stopLeScan(); handler.removeCallbacksAndMessages(leScanCallback);
connectGatt(device); handler.post(new Runnable() {
} @Override
public void run() {
// Check that bluetoothGatt == null in case the same device is found multiple times
// and thus multiple calls to connect to it are queued. Only the first will
// trigger the connect.
if (bluetoothGatt == null) {
connectGatt(device);
}
}
});
} }
}; };
Timber.d("Starting LE scan for device [%s]", hwAddress); // Try to connect to the device directly if the device isn't found in time
btAdapter.startLeScan(leScanCallback);
handler.postAtTime(new Runnable() { handler.postAtTime(new Runnable() {
@Override @Override
public void run() { public void run() {
Timber.d("Device not found in LE scan, connecting directly"); Timber.d("Device not found in LE scan, connecting directly");
synchronized (lock) { connectGatt(btAdapter.getRemoteDevice(hwAddress));
stopLeScan();
connectGatt(hwAddress);
}
} }
}, leScanCallback, SystemClock.uptimeMillis() + LE_SCAN_TIMEOUT_MS); }, leScanCallback, SystemClock.uptimeMillis() + LE_SCAN_TIMEOUT_MS);
Timber.d("Starting LE scan for device [%s]", hwAddress);
btAdapter.startLeScan(leScanCallback);
} }
private void stopLeScan() { private void stopLeScan() {
@@ -516,41 +510,38 @@ public abstract class BluetoothCommunication {
* Disconnect from a Bluetooth device * Disconnect from a Bluetooth device
*/ */
public void disconnect(boolean doCleanup) { public void disconnect(boolean doCleanup) {
synchronized (lock) { stopLeScan();
stopLeScan();
if (bluetoothGatt == null) {
return;
}
Timber.i("Disconnecting%s", doCleanup ? " (with cleanup)" : "");
if (bluetoothGatt == null) {
// Could be a pending connectGatt waiting
handler.removeCallbacksAndMessages(null); handler.removeCallbacksAndMessages(null);
callbackBtHandler = null; return;
}
if (doCleanup) { Timber.i("Disconnecting%s", doCleanup ? " (with cleanup)" : "");
if (btMachineState != BT_MACHINE_STATE.BT_CLEANUP_STATE) {
setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE); handler.removeCallbacksAndMessages(null);
nextMachineStateStep(); callbackBtHandler = null;
}
handler.post(new Runnable() { if (doCleanup) {
@Override if (btMachineState != BT_MACHINE_STATE.BT_CLEANUP_STATE) {
public void run() { setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE);
synchronized (lock) { nextMachineStateStep();
if (openRequest) { }
handler.postDelayed(this, 10); handler.post(new Runnable() {
} else { @Override
bluetoothGatt.close(); public void run() {
bluetoothGatt = null; if (openRequest) {
} handler.postDelayed(this, 10);
} } else {
disconnect(false);
} }
}); }
} });
else { }
bluetoothGatt.close(); else {
bluetoothGatt = null; bluetoothGatt.close();
} bluetoothGatt = null;
} }
} }
@@ -581,49 +572,48 @@ public abstract class BluetoothCommunication {
} }
private void handleRequests() { private void handleRequests() {
synchronized (lock) { // check for pending request
// check for pending request if (openRequest) {
if (openRequest) { Timber.d("Request pending (queue %d %d)",
Timber.d("Request pending (queue %d %d)", descriptorRequestQueue.size(), characteristicRequestQueue.size());
descriptorRequestQueue.size(), characteristicRequestQueue.size()); return; // yes, do nothing
return; // yes, do nothing
}
// handle descriptor requests first
GattObjectValue<BluetoothGattDescriptor> descriptor = descriptorRequestQueue.poll();
if (descriptor != null) {
descriptor.gattObject.setValue(descriptor.value);
Timber.d("Write descriptor %s: %s (queue: %d %d)",
descriptor.gattObject.getUuid(), byteInHex(descriptor.gattObject.getValue()),
descriptorRequestQueue.size(), characteristicRequestQueue.size());
if (!bluetoothGatt.writeDescriptor(descriptor.gattObject)) {
Timber.e("Failed to initiate write of descriptor %s",
descriptor.gattObject.getUuid());
}
openRequest = true;
return;
}
// handle characteristics requests second
GattObjectValue<BluetoothGattCharacteristic> characteristic = characteristicRequestQueue.poll();
if (characteristic != null) {
characteristic.gattObject.setValue(characteristic.value);
Timber.d("Write characteristic %s: %s (queue: %d %d)",
characteristic.gattObject.getUuid(), byteInHex(characteristic.gattObject.getValue()),
descriptorRequestQueue.size(), characteristicRequestQueue.size());
if (!bluetoothGatt.writeCharacteristic(characteristic.gattObject)) {
Timber.e("Failed to initiate write of characteristic %s",
characteristic.gattObject.getUuid());
}
openRequest = true;
return;
}
// After every command was executed, continue with the next step
nextMachineStateStep();
} }
// handle descriptor requests first
GattObjectValue<BluetoothGattDescriptor> descriptor = descriptorRequestQueue.poll();
if (descriptor != null) {
descriptor.gattObject.setValue(descriptor.value);
Timber.d("Write descriptor %s: %s (queue: %d %d)",
descriptor.gattObject.getUuid(), byteInHex(descriptor.gattObject.getValue()),
descriptorRequestQueue.size(), characteristicRequestQueue.size());
if (!bluetoothGatt.writeDescriptor(descriptor.gattObject)) {
Timber.e("Failed to initiate write of descriptor %s",
descriptor.gattObject.getUuid());
}
openRequest = true;
return;
}
// handle characteristics requests second
GattObjectValue<BluetoothGattCharacteristic> characteristic = characteristicRequestQueue.poll();
if (characteristic != null) {
characteristic.gattObject.setValue(characteristic.value);
Timber.d("Write characteristic %s: %s (type: %d; queue: %d %d)",
characteristic.gattObject.getUuid(), byteInHex(characteristic.gattObject.getValue()),
characteristic.gattObject.getWriteType(),
descriptorRequestQueue.size(), characteristicRequestQueue.size());
if (!bluetoothGatt.writeCharacteristic(characteristic.gattObject)) {
Timber.e("Failed to initiate write of characteristic %s",
characteristic.gattObject.getUuid());
}
openRequest = true;
return;
}
// After every command was executed, continue with the next step
nextMachineStateStep();
} }
/** /**
@@ -631,38 +621,45 @@ public abstract class BluetoothCommunication {
*/ */
protected class GattCallback extends BluetoothGattCallback { protected class GattCallback extends BluetoothGattCallback {
@Override @Override
public void onConnectionStateChange(final BluetoothGatt gatt, int status, int newState) { public void onConnectionStateChange(final BluetoothGatt gatt, final int status, int newState) {
Timber.d("onConnectionStateChange: status=%d, newState=%d", status, newState); Timber.d("onConnectionStateChange: status=%d, newState=%d", status, newState);
if (newState == BluetoothProfile.STATE_CONNECTED) { if (newState == BluetoothProfile.STATE_CONNECTED) {
synchronized (lock) { handler.post(new Runnable() {
stopLeScan(); @Override
} public void run() {
stopLeScan();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { connectionEstablished = true;
gatt.readPhy(); setBtStatus(BT_STATUS_CODE.BT_CONNECTION_ESTABLISHED);
} }
});
connectionEstablished = true; // Wait a short while after connecting before scanning for services
setBtStatus(BT_STATUS_CODE.BT_CONNECTION_ESTABLISHED); handler.postDelayed(new Runnable() {
@Override
try { public void run() {
Thread.sleep(1000); if (!gatt.discoverServices()) {
} Timber.e("Could not start service discovery");
catch (Exception e) { setBtStatus(BT_STATUS_CODE.BT_CONNECTION_LOST);
// Empty disconnect(false);
} }
if (!gatt.discoverServices()) { }
Timber.e("Could not start service discovery"); }, 1000);
setBtStatus(BT_STATUS_CODE.BT_CONNECTION_LOST);
disconnect(false);
}
} }
else if (newState == BluetoothProfile.STATE_DISCONNECTED) { else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
setBtStatus(connectionEstablished handler.post(new Runnable() {
? BT_STATUS_CODE.BT_CONNECTION_LOST @Override
: BT_STATUS_CODE.BT_NO_DEVICE_FOUND); public void run() {
disconnect(false); if (connectionEstablished && status != 0) {
autoConnect = !autoConnect;
}
setBtStatus(connectionEstablished
? BT_STATUS_CODE.BT_CONNECTION_LOST
: BT_STATUS_CODE.BT_NO_DEVICE_FOUND);
disconnect(false);
}
});
} }
} }
@@ -672,34 +669,35 @@ public abstract class BluetoothCommunication {
status, gatt.getServices().size()); status, gatt.getServices().size());
if (gatt.getServices().isEmpty()) { if (gatt.getServices().isEmpty()) {
setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "No services found"); handler.post(new Runnable() {
disconnect(false); @Override
public void run() {
setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "No services found");
disconnect(false);
}
});
return; return;
} }
synchronized (lock) { // Sleeping a while after discovering services fixes connection problems.
cmdStepNr = 0; // See https://github.com/NordicSemiconductor/Android-DFU-Library/issues/10
initStepNr = 0; // for some technical background.
cleanupStepNr = 0; handler.postDelayed(new Runnable() {
@Override
public void run() {
cmdStepNr = 0;
initStepNr = 0;
cleanupStepNr = 0;
// Clear from possible previous setups // Clear from possible previous setups
characteristicRequestQueue = new LinkedList<>(); characteristicRequestQueue = new LinkedList<>();
descriptorRequestQueue = new LinkedList<>(); descriptorRequestQueue = new LinkedList<>();
openRequest = false; openRequest = false;
}
try { // Start the state machine
// Sleeping a while after discovering services fixes connection problems. setBtMachineState(BT_MACHINE_STATE.BT_INIT_STATE);
// See https://github.com/NordicSemiconductor/Android-DFU-Library/issues/10 }
// for some technical background. }, 1000);
Thread.sleep(1000);
}
catch (Exception e) {
// Empty
}
// Start the state machine
setBtMachineState(BT_MACHINE_STATE.BT_INIT_STATE);
} }
@Override @Override
@@ -718,10 +716,8 @@ public abstract class BluetoothCommunication {
handler.postDelayed(new Runnable() { handler.postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
synchronized (lock) { openRequest = false;
openRequest = false; handleRequests();
handleRequests();
}
} }
}, 60); }, 60);
} }
@@ -741,27 +737,33 @@ public abstract class BluetoothCommunication {
} }
@Override @Override
public void onCharacteristicRead(BluetoothGatt gatt, public void onCharacteristicRead(final BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, final BluetoothGattCharacteristic characteristic,
int status) { final int status) {
Timber.d("onCharacteristicRead %s (status=%d): %s", Timber.d("onCharacteristicRead %s (status=%d): %s",
characteristic.getUuid(), status, byteInHex(characteristic.getValue())); characteristic.getUuid(), status, byteInHex(characteristic.getValue()));
synchronized (lock) { handler.post(new Runnable() {
onBluetoothDataRead(gatt, characteristic, status); @Override
postDelayedHandleRequests(); public void run() {
} onBluetoothDataRead(gatt, characteristic, status);
}
});
postDelayedHandleRequests();
} }
@Override @Override
public void onCharacteristicChanged(BluetoothGatt gatt, public void onCharacteristicChanged(final BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) { final BluetoothGattCharacteristic characteristic) {
Timber.d("onCharacteristicChanged %s: %s", Timber.d("onCharacteristicChanged %s: %s",
characteristic.getUuid(), byteInHex(characteristic.getValue())); characteristic.getUuid(), byteInHex(characteristic.getValue()));
synchronized (lock) { handler.post(new Runnable() {
onBluetoothDataChange(gatt, characteristic); @Override
} public void run() {
onBluetoothDataChange(gatt, characteristic);
}
});
} }
@Override @Override

View File

@@ -74,13 +74,28 @@ public class BluetoothDebug extends BluetoothCommunication {
return names.substring(0, names.length() - 2); return names.substring(0, names.length() - 2);
} }
private String writeTypeToString(int type) {
if (type == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
return "no response";
}
if (type == BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) {
return "default";
}
if (type == BluetoothGattCharacteristic.WRITE_TYPE_SIGNED) {
return "signed";
}
return String.format("unknown type %d", type);
}
private void logService(BluetoothGattService service, boolean included) { private void logService(BluetoothGattService service, boolean included) {
Timber.d("Service %s%s", service.getUuid(), included ? " (included)" : ""); Timber.d("Service %s%s", service.getUuid(), included ? " (included)" : "");
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
Timber.d("|- characteristic %s (instance %d): %s (permissions=0x%x)", Timber.d("|- characteristic %s (instance %d): %s, write type: %s (permissions=0x%x)",
characteristic.getUuid(), characteristic.getInstanceId(), characteristic.getUuid(), characteristic.getInstanceId(),
propertiesToString(characteristic.getProperties()), characteristic.getPermissions()); propertiesToString(characteristic.getProperties()),
writeTypeToString(characteristic.getWriteType()),
characteristic.getPermissions());
byte[] value = characteristic.getValue(); byte[] value = characteristic.getValue();
if (value != null && value.length > 0) { if (value != null && value.length > 0) {
Timber.d("|--> value: %s (%s)", byteInHex(value), Timber.d("|--> value: %s (%s)", byteInHex(value),