Skip to content

Instantly share code, notes, and snippets.

@AndreKR
Created July 12, 2020 18:55
Show Gist options
  • Save AndreKR/c42466b7379f5da742d03b32880a0421 to your computer and use it in GitHub Desktop.
Save AndreKR/c42466b7379f5da742d03b32880a0421 to your computer and use it in GitHub Desktop.
Read a BLE characteristic on Android
package com.example.android.app;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.util.Log;
import java.util.UUID;
/**
* Manages the state of the bluetooth connection
*
* The Android BLE API is completely asynchronous and with different state changes
* reported by different callbacks. In order to do the necessary calls in the correct order you have
* to manage the current connection state yourself.
*/
public class BleStateMachine extends BluetoothGattCallback {
public enum ErrorType {
GATT_FAILURE,
ONCONNECTIONSTATECHANGE_WITHOUT_GATT_SUCCESS,
UNEXPECTED_CONNECTION_EVENT,
UNEXPECTED_DISCOVERY_EVENT
}
public static abstract class ReadCallback {
void onReadCompleted(ErrorType error, int data) {};
void onReadCompleted(ErrorType error, String data) {};
}
public static class InvalidStateException extends Exception {}
private enum State {
CONNECTING,
DISCOVERY,
READING,
IDLE,
FAILURE
}
private static class PendingRead {
private UUID characeristicUuid;
private ReadCallback callback;
}
// From the constructor
private String remoteMac;
private UUID serviceUuid;
private BluetoothAdapter bluetoothAdapter;
private Activity activity;
// State
private State currentState;
private PendingRead readInProgress = null;
// Internal onbjects
private BluetoothGatt gatt;
/**
* @param activity The result callback will run on this activity's UI thread. Also needed for
* connectGatt, the reason is undocumented.
* @param bluetoothAdapter The BluetoothAdapter instance to use
* @param remoteMac The MAC address of the device to connect to
*/
public BleStateMachine(Activity activity, BluetoothAdapter bluetoothAdapter, String remoteMac, UUID serviceUuid) {
this.activity = activity;
this.bluetoothAdapter = bluetoothAdapter;
this.remoteMac = remoteMac;
this.serviceUuid = serviceUuid;
currentState = State.CONNECTING;
openConnection();
}
public void tryRead(UUID characteristicUuid, ReadCallback cb) {
try {
read(characteristicUuid, cb);
} catch (InvalidStateException ignored) {}
}
/**
* Read a characteristic
*
* @param cb A callback that is called when the reading is done, regardless of whether it was
* successful. (The Android BLE API is asynchronous, so this is as well.)
*/
public void read(UUID characteristicUuid, ReadCallback cb) throws InvalidStateException {
if (readInProgress != null) {
throw new InvalidStateException();
}
PendingRead pr = new PendingRead();
pr.characeristicUuid = characteristicUuid;
pr.callback = cb;
this.readInProgress = pr;
if (currentState == State.FAILURE) {
// Current strategy after a failure: Try to start over with a fresh connection. (We
// currently don't differentiate between different points of failure nor do we pick up
// at the step where it failed.)
currentState = State.CONNECTING;
reviveConnection();
} else if (currentState == State.IDLE) {
currentState = State.READING;
readCharacteristic();
} else {
// From all other states we should eventually get to the point where the read is started
nop();
}
}
public void close() {
if (readInProgress != null)
readInProgress = null;
if (gatt != null) {
gatt.disconnect();
gatt.close();
}
}
private void nop() {}
private void reviveConnection() {
gatt.disconnect();
gatt.close();
openConnection();
}
private void openConnection() {
BluetoothDevice dev = bluetoothAdapter.getRemoteDevice(remoteMac);
// Here is the missing documentation for the autoConnect flag:
// https://stackoverflow.com/questions/40156699/which-correct-flag-of-autoconnect-in-connectgatt-of-ble/40187086#40187086
gatt = dev.connectGatt(activity, false, this);
}
private void readCharacteristic() {
BluetoothGattService service = gatt.getService(serviceUuid);
BluetoothGattCharacteristic charac = service.getCharacteristic(readInProgress.characeristicUuid);
gatt.readCharacteristic(charac);
}
private void report(final ErrorType error, final int intData, final String stringData) {
final ReadCallback cb = readInProgress.callback; // Not sure if we have to save this before the Runnable runs on the UI thread?
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
cb.onReadCompleted(error, intData);
cb.onReadCompleted(error, stringData);
}
});
readInProgress = null;
}
private void reportSuccess(int intData, String stringData) {
report(null, intData, stringData);
}
private void reportFailure(ErrorType error)
{
if (readInProgress != null) {
report(error, 0, null);
}
}
//region BluetoothGattCallback implementation
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
Log.d("GATT", "onConnectionStateChange " + status + " " + newState);
if (status != BluetoothGatt.GATT_SUCCESS) {
currentState = State.FAILURE;
reportFailure(ErrorType.ONCONNECTIONSTATECHANGE_WITHOUT_GATT_SUCCESS);
return;
}
if (newState == BluetoothProfile.STATE_CONNECTED) {
if (currentState == State.CONNECTING) {
currentState = State.DISCOVERY;
gatt.discoverServices();
} else {
currentState = State.FAILURE;
reportFailure(ErrorType.UNEXPECTED_CONNECTION_EVENT);
}
} else {
// We were disconnected, reconnect
currentState = State.CONNECTING;
gatt.connect();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt ignored, int status) {
Log.d("GATT", "onServicesDiscovered " + status);
if (currentState == State.DISCOVERY) {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (readInProgress != null) {
currentState = State.READING;
readCharacteristic();
} else {
currentState = State.IDLE;
}
} else {
currentState = State.FAILURE;
reportFailure(ErrorType.GATT_FAILURE);
}
} else {
currentState = State.FAILURE;
reportFailure(ErrorType.UNEXPECTED_DISCOVERY_EVENT);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt ignored, BluetoothGattCharacteristic characteristic, int status) {
Log.d("GATT", "onCharacteristicRead " + status);
if (status == BluetoothGatt.GATT_SUCCESS) {
currentState = State.IDLE;
reportSuccess(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0), characteristic.getStringValue(0));
} else {
// Only this read failed, otherwise this connection should still be ready to handle
// transactions, so we set it to IDLE instead of FAILURE.
currentState = State.IDLE;
reportFailure(ErrorType.GATT_FAILURE);
}
}
//endregion
}
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
ble = new BleStateMachine(this, bluetoothAdapter, "xx:xx:xx:xx:xx:xx", UUID.fromString("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"));
ble.read(UUID.fromString("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"), new BleStateMachine.ReadCallback() {
@Override
void onReadCompleted(BleStateMachine.ErrorType error, int data) {
if (error != null) {
if (error == BleStateMachine.ErrorType.GATT_FAILURE) {
new AlertDialog.Builder(MainActivity.this)
.setMessage("Unable to fetch data")
.show();
} else {
new AlertDialog.Builder(MainActivity.this)
.setMessage("Unexpected event: " + error.name())
.show();
}
return;
}
new AlertDialog.Builder(MainActivity.this)
.setMessage(String.valueOf(data))
.show();
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment