Android Bluetooth Low Energy (BLE) - Cooking Right, Part # 3 (read / write)

Content

Part # 1 (scanning)





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()



     or  onCharacteristicWrite()



    .





  • . , , .  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:





  1.  setCharacteristicNotification



    . Bluetooth .





  2.  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.












All Articles