0

I am trying to read a characteristic from a BLE device, and just to do that I ended up with this huge state machine. And this is not even handling timeouts or multiple queued read requests yet, let alone notifications. Should I be able to do away with some of that and let Android handle it?

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
}
AndreKR
  • 32,613
  • 18
  • 106
  • 168

1 Answers1

0

Well, in order to read a BLE characteristic on a device, you need to:

  1. Establish a connexion to the BluetoothGatt instance of the device

  2. Complete service discovery using discoverServices() on this instance

  3. Call readCharacteristic() on this instance.

This is what your sample is doing.

If you already know the UUID of the service and characteristic you want to read, you can read the charac as follows:

BluetoothGattService service = gatt.getService(serviceUUID);
BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUUID);

Here are more code samples on using readCharacteristic()

https://www.programcreek.com/java-api-examples/?class=android.bluetooth.BluetoothGatt&method=readCharacteristic

matdev
  • 4,115
  • 6
  • 35
  • 56
  • Are you sure? The docblock of `getService()` says "This function requires that service discovery has been completed for the given device." – AndreKR Jul 15 '20 at 11:23
  • And in fact I think I remember it returning `null` until I added service discovery. – AndreKR Jul 15 '20 at 11:24