Content
Part # 2 (connecting / disconnecting)
Part # 3 (read / write), you are here
In the previous article, we talked in detail about connecting / disconnecting BLE devices. This article is about reading and writing characteristics, and turning on / off notifications .
Reading and writing characteristics
Many developers who start working with BLE on Android are facing problems with reading / writing BLE characteristics. On Stackoverflow full of people offering just use delay ... Most of these tips incorrect.
There are two main reasons for the problems:
Read / write operations are asynchronous . This means that the method call will return immediately, but you will receive the result of the call a little later - in the corresponding callbacks. For example
onCharacteristicRead()
oronCharacteristicWrite()
.
. , , .
BluetoothGatt
, . Google … (. :mDeviceBusy
mDeviceBusyLock
).
, , , BLE. , , , . .
BluetoothGatt.java
mDeviceBusy
, :
public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
if ((characteristic.getProperties()
& BluetoothGattCharacteristic.PROPERTY_READ) == 0) {
return false;
}
if (VDBG) Log.d(TAG, "readCharacteristic() - uuid: " + characteristic.getUuid());
if (mService == null || mClientIf == 0) return false;
BluetoothGattService service = characteristic.getService();
if (service == null) return false;
BluetoothDevice device = service.getDevice();
if (device == null) return false;
synchronized (mDeviceBusy) {
if (mDeviceBusy) return false;
mDeviceBusy = true;
}
try {
mService.readCharacteristic(mClientIf, device.getAddress(),
characteristic.getInstanceId(), AUTHENTICATION_NONE);
} catch (RemoteException e) {
Log.e(TAG, "", e);
mDeviceBusy = false;
return false;
}
return true;
}
/, mDeviceBusy
false :
public void onCharacteristicRead(String address,
int status,
int handle,
byte[] value) {
if (VDBG) {
Log.d(TAG, "onCharacteristicRead() - Device=" + address
+ " handle=" + handle + " Status=" + status);
}
if (!address.equals(mDevice.getAddress())) {
return;
}
synchronized (mDeviceBusy) {
mDeviceBusy = false;
}
....
/ , . - . BLE , , . ! – . , , «» , . , , . BLE. iOS CoreBluetooth
(. : , Bluetooth Android).
BluetoothGatt
. , Android BluetoothGatt
, (. : , ). , Queue
Runnable
commandQueueBusy
:
private Queue<Runnable> commandQueue;
private boolean commandQueueBusy;
Runnable
. (readCharacteristic):
public boolean readCharacteristic(final BluetoothGattCharacteristic characteristic) {
if(bluetoothGatt == null) {
Log.e(TAG, "ERROR: Gatt is 'null', ignoring read request");
return false;
}
// Check if characteristic is valid
if(characteristic == null) {
Log.e(TAG, "ERROR: Characteristic is 'null', ignoring read request");
return false;
}
// Check if this characteristic actually has READ property
if((characteristic.getProperties() & PROPERTY_READ) == 0 ) {
Log.e(TAG, "ERROR: Characteristic cannot be read");
return false;
}
// Enqueue the read command now that all checks have been passed
boolean result = commandQueue.add(new Runnable() {
@Override
public void run() {
if(!bluetoothGatt.readCharacteristic(characteristic)) {
Log.e(TAG, String.format("ERROR: readCharacteristic failed for characteristic: %s", characteristic.getUuid()));
completedCommand();
} else {
Log.d(TAG, String.format("reading characteristic <%s>", characteristic.getUuid()));
nrTries++;
}
}
});
if(result) {
nextCommand();
} else {
Log.e(TAG, "ERROR: Could not enqueue read characteristic command");
}
return result;
}
, ( ) , . Runnable
, readCharacteristic()
, . , (. : , ). false
, , «» , . nextCommand()
, :
private void nextCommand() {
// If there is still a command being executed then bail out
if(commandQueueBusy) {
return;
}
// Check if we still have a valid gatt object
if (bluetoothGatt == null) {
Log.e(TAG, String.format("ERROR: GATT is 'null' for peripheral '%s', clearing command queue", getAddress()));
commandQueue.clear();
commandQueueBusy = false;
return;
}
// Execute the next command in the queue
if (commandQueue.size() > 0) {
final Runnable bluetoothCommand = commandQueue.peek();
commandQueueBusy = true;
nrTries = 0;
bleHandler.post(new Runnable() {
@Override
public void run() {
try {
bluetoothCommand.run();
} catch (Exception ex) {
Log.e(TAG, String.format("ERROR: Command exception for device '%s'", getName()), ex);
}
}
});
}
}
, peek()
Runnable
, . .
:
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic,
int status) {
// Perform some checks on the status field
if (status != GATT_SUCCESS) {
Log.e(TAG, String.format(Locale.ENGLISH,"ERROR: Read failed for characteristic: %s, status %d", characteristic.getUuid(), status));
completedCommand();
return;
}
// Characteristic has been read so processes it
...
// We done, complete the command
completedCommand();
}
completedCommand()
. .
, Runnable
poll()
:
private void completedCommand() {
commandQueueBusy = false;
isRetrying = false;
commandQueue.poll();
nextCommand();
}
(, ), . , Runnable
completedCommand()
. – :
private void retryCommand() {
commandQueueBusy = false;
Runnable currentCommand = commandQueue.peek();
if(currentCommand != null) {
if (nrTries >= MAX_TRIES) {
// Max retries reached, give up on this one and proceed
Log.v(TAG, "Max number of tries reached");
commandQueue.poll();
} else {
isRetrying = true;
}
}
nextCommand();
}
, . , . , :
WRITE_TYPE_DEFAULT
( , , );
WRITE_TYPE_NO_RESPONSE
( ).
( , ).
Android , . Android, :
...
if ((mProperties & PROPERTY_WRITE_NO_RESPONSE) != 0) {
mWriteType = WRITE_TYPE_NO_RESPONSE;
} else {
mWriteType = WRITE_TYPE_DEFAULT;
}
...
, , . , WRITE_TYPE_NO_RESPONSE
. !
, :
// Check if this characteristic actually supports this writeType
int writeProperty;
switch (writeType) {
case WRITE_TYPE_DEFAULT: writeProperty = PROPERTY_WRITE; break;
case WRITE_TYPE_NO_RESPONSE : writeProperty = PROPERTY_WRITE_NO_RESPONSE; break;
case WRITE_TYPE_SIGNED : writeProperty = PROPERTY_SIGNED_WRITE; break;
default: writeProperty = 0; break;
}
if((characteristic.getProperties() & writeProperty) == 0 ) {
Log.e(TAG, String.format(Locale.ENGLISH,"ERROR: Characteristic <%s> does not support writeType '%s'", characteristic.getUuid(), writeTypeToString(writeType)));
return false;
}
Android!
, bytesToWrite
:
characteristic.setValue(bytesToWrite);
characteristic.setWriteType(writeType);
if (!bluetoothGatt.writeCharacteristic(characteristic)) {
Log.e(TAG, String.format("ERROR: writeCharacteristic failed for characteristic: %s", characteristic.getUuid()));
completedCommand();
} else {
Log.d(TAG, String.format("writing <%s> to characteristic <%s>", bytes2String(bytesToWrite), characteristic.getUuid()));
nrTries++;
}
/
, . , .
Android:
setCharacteristicNotification
. Bluetooth .
1 2
unsigned int16
(Client Characteristic Configuration, - ). CCC UUID 2902.
1 2? « » Bluetooth . Bluetooth, – . , , , , . Android : Bluetooth , . , 1 , 2 – . , 0. , CCC.
iOS setNotify()
. , Android, , , :
private final String CCC_DESCRIPTOR_UUID = "00002902-0000-1000-8000-00805f9b34fb";
public boolean setNotify(BluetoothGattCharacteristic characteristic,
final boolean enable) {
// Check if characteristic is valid
if(characteristic == null) {
Log.e(TAG, "ERROR: Characteristic is 'null', ignoring setNotify request");
return false;
}
// Get the CCC Descriptor for the characteristic
final BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(CCC_DESCRIPTOR_UUID));
if(descriptor == null) {
Log.e(TAG, String.format("ERROR: Could not get CCC descriptor for characteristic %s", characteristic.getUuid()));
return false;
}
// Check if characteristic has NOTIFY or INDICATE properties and set the correct byte value to be written
byte[] value;
int properties = characteristic.getProperties();
if ((properties & PROPERTY_NOTIFY) > 0) {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
} else if ((properties & PROPERTY_INDICATE) > 0) {
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
} else {
Log.e(TAG, String.format("ERROR: Characteristic %s does not have notify or indicate property", characteristic.getUuid()));
return false;
}
final byte[] finalValue = enable ? value : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
// Queue Runnable to turn on/off the notification now that all checks have been passed
boolean result = commandQueue.add(new Runnable() {
@Override
public void run() {
// First set notification for Gatt object if(!bluetoothGatt.setCharacteristicNotification(descriptor.getCharacteristic(), enable)) {
Log.e(TAG, String.format("ERROR: setCharacteristicNotification failed for descriptor: %s", descriptor.getUuid()));
}
// Then write to descriptor
descriptor.setValue(finalValue);
boolean result;
result = bluetoothGatt.writeDescriptor(descriptor);
if(!result) {
Log.e(TAG, String.format("ERROR: writeDescriptor failed for descriptor: %s", descriptor.getUuid()));
completedCommand();
} else {
nrTries++;
}
}
});
if(result) {
nextCommand();
} else {
Log.e(TAG, "ERROR: Could not enqueue write command");
}
return result;
}
CCC onDescriptorWrite
. CCC . , , .
@Override
public void onDescriptorWrite(BluetoothGatt gatt,
final BluetoothGattDescriptor descriptor,
final int status) {
// Do some checks first
final BluetoothGattCharacteristic parentCharacteristic = descriptor.getCharacteristic();
if(status!= GATT_SUCCESS) {
Log.e(TAG, String.format("ERROR: Write descriptor failed value <%s>, device: %s, characteristic: %s", bytes2String(currentWriteBytes), getAddress(), parentCharacteristic.getUuid()));
}
// Check if this was the Client Configuration Descriptor if(descriptor.getUuid().equals(UUID.fromString(CCC_DESCRIPTOR_UUID))) {
if(status==GATT_SUCCESS) {
// Check if we were turning notify on or off
byte[] value = descriptor.getValue();
if (value != null) {
if (value[0] != 0) {
// Notify set to on, add it to the set of notifying characteristics notifyingCharacteristics.add(parentCharacteristic.getUuid());
}
} else {
// Notify was turned off, so remove it from the set of notifying characteristics notifyingCharacteristics.remove(parentCharacteristic.getUuid());
}
}
}
// This was a setNotify operation
....
} else {
// This was a normal descriptor write....
...
});
}
completedCommand();
}
– isNotifying()
:
public boolean isNotifying(BluetoothGattCharacteristic characteristic) {
return notifyingCharacteristics.contains(characteristic.getUuid());
}
, , . Android-5 15. 7 4. 15 . , , .
, / , / , . , BLE :
. , , Bluetooth Health Thermometer . , . , ;
. , . BLE, BLE, , . , : , , , ..
, – . , , (30Hz ).
:
BluetoothGattCharacteristic
, .
:
, Android ( ). , Android ;
Android BluetoothGattCharacteristic . (services discovering) . , Android
BluetoothGattCharacteristic
. (race condition) .
, . , .
, :
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic) {
// Copy the byte array so we have a threadsafe copy
final byte[] value = new byte[characteristic.getValue().length];
System.arraycopy(
characteristic.getValue(),
0, value, 0,
characteristic.getValue().length);
// Characteristic has new value so pass it on for processing
bleHandler.post(new Runnable() {
@Override
public void run() {
myProcessor.onCharacteristicUpdate(BluetoothPeripheral.this, value, characteristic);
}
});
}
BLE Android. BLE , - .
Android :
(
main
);
BluetoothGattCallback
(Binder
);
main . Binder . Binder, Android , Binder . , sleep()
- . , BluetoothGatt
, Binder, .
:
BluetoothGattCallback
, (. : main - , , UI, );
Binder
;
– Handler
. , Handler
onCharacteristicUpdate
.
:
Handler bleHandler = new Handler();
Handler
main
:
Handler bleHandler = new Handler(Looper.getMainLooper());
Scroll back and take a look at our method nextCommand()
, each Runnable
is executed in our own Handler
, hence we ensure that all commands are executed outside of the thread Binder
.
Next: Bonding
In this article, we figured out how to read and write characteristics, turn on and off notifications / notifications. In the next article, we will explore in detail the process of conjugation with a device (bonding).
Can't wait to work with BLE? Try my Blessed for Android library . It takes all the approaches in this article series and makes it easy to work with BLE in your application.