A Sample Android App to change Characteristic Value

In this tutorial we will create a small app(which is very similar to nRF Connect app) that will change a characteristic value of a beacon. We will need this app if we need to make the beacon buzz or stop it from buzzing.

So let's get started. In our scenario, we will need to create a client which can connect to the advertising device, and read / write on characteristics.

First, we declare the BLUETOOTH and BLUETOOTH_ADMIN permissions. BLUETOOTH_ADMIN is required to initiate discovery, or automatically enable Bluetooth on the device.

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-feature android:name="android.hardware.bluetooth_le" />

We also specify that our app requires the bluetooth_le feature to work.

Creation of the client:
Scanning BLE devices can be quite complex so we will use a unified third party compat library: Android BLE Scanner Compat library
BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();

// We want to receive a list of found devices every 0.1 second
ScanSettings settings = new ScanSettings.Builder()
  .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
  .setReportDelay(100)
  .build();

// We only want to scan for devices advertising beacon service  
ScanFilter scanFilter = new ScanFilter.Builder()
  .setServiceUuid(new ParcelUuid(SERVICE_UUID)).build();
scanner.startScan(Arrays.asList(scanFilter), settings, mScanCallback);
SERVICE_UUID is the uuid of our beacon for the particular service where the characteric we are going to read and change belongs.

We should create a handler that stops the scan after a few seconds (e.g. 10 seconds), and stop scanning as soon as we find the desired device.

The startScan method takes a ScanCallback implemented below:

private final ScanCallback mScanCallback = new ScanCallback() {
  @Override
  public void onScanResult(int callbackType, ScanResult result) {
    // We scan with report delay > 0. This will never be called.
  }

  @Override
  public void onBatchScanResults(List<ScanResult> results) {
    if (!results.isEmpty()) {
      ScanResult result = results.get(0);
      BluetoothDevice device = result.getDevice();
      String deviceAddress = device.getAddress();
      // Device detected, we can automatically connect to it and stop the scan
    }
  }

  @Override
  public void onScanFailed(int errorCode) {
    // Scan error
  }
};
onBatchScanResults will periodically return a list of detected devices, depending on the setReportDelay value you specified earlier.

You can add these devices to a list so that users can select which device they want to connect to.
In our case, we know there’s only one device advertising our custom service, so we will automatically connect to it once detected.

To connect to a GATT server, use the device address to get an instance of a BluetoothDevice, and then call the connectGatt method:

BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceAddress);
mGatt = device.connectGatt(mContext, false, mGattCallback);
This method takes a BluetoothGattCallback, very similar to the BluetoothGattServerCallback we saw earlier, containing callback methods when a characteristic / descriptor has been read / write.

Be prepared! You are going to see a succession of callbacks. Each operation has an associated callback. We can’t perform two Bluetooth operations, e.g. two write operations, at the same time. We will have to wait for one to finish before we can start the next one.

When the GATT connection succeeds, the onConnectionStateChange will be called.
You can start here discovering services when the device is connected successfully:

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
  if (newState == BluetoothProfile.STATE_CONNECTED) {
    Log.i(TAG, "Connected to GATT client. Attempting to start service discovery");
    gatt.discoverServices();
  } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
    Log.i(TAG, "Disconnected from GATT client");
  }
}
The discoverServices method will then call an onServicesDiscovered method you’ll have to override.
Our client wants to be notified of each CHARACTERISTIC_COUNTER_UUID change, so it’s the right place to start writing to the descriptor:

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
  if (status != BluetoothGatt.GATT_SUCCESS) {
    // Handle the error
    return;
  }

  // Get the counter characteristic
  BluetoothGattCharacteristic characteristic = gatt
    .getService(SERVICE_UUID)
    .getCharacteristic(CHARACTERISTIC_COUNTER_UUID);

  // Enable notifications for this characteristic locally
  gatt.setCharacteristicNotification(characteristic, true);

  // Write on the config descriptor to be notified when the value changes
  BluetoothGattDescriptor descriptor =
    characteristic.getDescriptor(DESCRIPTOR_CONFIG_UUID);
  descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
  gatt.writeDescriptor(descriptor);
}
Now, we want to read the counter value. We can’t do it in the onServicesDiscovered method, as there is already a pending write operation on a descriptor.
So, we will override onDescriptorWrite and start reading our characteristic here, after the descriptor has been written:

@Override
public void onDescriptorWrite(BluetoothGatt gatt,
    BluetoothGattDescriptor descriptor, int status) {
  if (DESCRIPTOR_CONFIG_UUID.equals(descriptor.getUuid())) {
    BluetoothGattCharacteristic characteristic = gatt
      .getService(SERVICE_UUID)
      .getCharacteristic(CHARACTERISTIC_COUNTER_UUID);
    gatt.readCharacteristic(characteristic);
  }
}
When the characteristic is read, the onCharacteristicRead method will be called.
We can receive here the characteristic value, and update the UI:

@Override
public void onCharacteristicRead(BluetoothGatt gatt,
    BluetoothGattCharacteristic characteristic, int status) {
  readCounterCharacteristic(characteristic);
}

private void readCounterCharacteristic(BluetoothGattCharacteristic
    characteristic) {
  if (CHARACTERISTIC_COUNTER_UUID.equals(characteristic.getUuid())) {
    byte[] data = characteristic.getValue();
    int value = Ints.fromByteArray(data);
    // Update UI
  }
}
Finally, since we are now notified when the characteristic value automatically changes, we can override onCharacteristicChanged to once again read the counter value and update the UI
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
    BluetoothGattCharacteristic characteristic) {
  readCounterCharacteristic(characteristic);
}
Back in our BluetoothGattCallback, once the Client connects and we receive a GATT_SUCCESS and STATE_CONNECTED, we now must discover the services of the GATT Server.


public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    ...
    if (newState == BluetoothProfile.STATE_CONNECTED) {
        mConnected = true;
        gatt.discoverServices();
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
        ...
This will bring us to the next callback we must implement, onServicesDiscovered.

public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    super.onServicesDiscovered(gatt, status);
    if (status != BluetoothGatt.GATT_SUCCESS) {
        return;
    }
}
Just as you did before, check the status and return if it is not successful. If the discovery services was successful, we can now look for our Characteristic. Since we know the full UUID of both the Service and the Characteristic, we can access them directly.

public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        ...
        return;
    }
    BluetoothGattService service = gatt.getService(SERVICE_UUID);
    BluetoothGattCharacteristic characteristic = service.getCharacteristic(CHARACTERISTIC_UUID);
}
Now that we have found our Characteristic, we need to set the write type and enable notifications.

public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    ...
    BluetoothGattCharacteristic characteristic = service.getCharacteristic(CHARACTERISTIC_UUID);
    characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
    mInitialized = gatt.setCharacteristicNotification(characteristic, true);
}
mInitialized is used to signify that our Characteristic is fully ready for use. Without reaching this point, the Characteristic would not have the correct write type or notify us when there is a change. Make sure to set this to false when disconnecting from the GATT server. If you have added logs into each step, you should now be able to connect to the Server and see that the Client has initialized our Characteristic.

First wire up an EditText for user input and a Button to send the data. Before doing anything, make sure we are connected and our Characteristic is initialized. Then find our Characteristic and send the message.

private void sendMessage() {
    if (!mConnected || !mEchoInitialized) {
        return;
    }
    BluetoothGattService service = gatt.getService(SERVICE_UUID);
    BluetoothGattCharacteristic characteristic = service.getCharacteristic(CHARACTERISTIC_UUID);
    String message = mBinding.messageEditText.getText().toString();
}
In order to send the data we must first convert our String to byte[].
private void sendMessage() {
    ...
    String message = mBinding.messageEditText.getText().toString();
    byte[] messageBytes = new byte[0];
    try {
        messageBytes = message.getBytes("UTF-8");
    } catch (UnsupportedEncodingException e) {
        Log.e(TAG, "Failed to convert message string to byte array");
    }
}
Now set the value on the Characteristic and our message will be sent!

private void sendMessage() {
        ...
        Log.e(TAG, "Failed to convert message string to byte array");
    }
    characteristic.setValue(messageBytes);
    boolean success = mGatt.writeCharacteristic(characteristic);
}
Optionally, we could implement BluetoothGattCallback.onCharacteristicWrite and add a log to see if the write was successful.




No comments:

Post a Comment