Created
July 12, 2020 18:55
-
-
Save AndreKR/c42466b7379f5da742d03b32880a0421 to your computer and use it in GitHub Desktop.
Read a BLE characteristic on Android
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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