Recently, in the summary of BLE related operations in the project, a lot of problems were exposed when reviewing the code. Points of reference value are summarized here. Note that this is not a guide to getting started, so make sure you’ve actually worked on the BLE API.

BluetoothGattCallback thread problem

BluetoothGattCallback gattCallBack=new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if(newState== BluetoothProfile.STATE_CONNECTED){ gatt.discoverServices(); }}@Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            if (status == BluetoothGatt.GATT_SUCCESS) {
               // Block 1}}@Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {}@Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {}@Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {}Copy the code

BluetoothGattCallback is arguably the most basic API. Whether you want to read, write or subscribe, you can’t get around this callback. Most people would call discoverServices() directly from onConnectionStateChange, but it’s even more embarrassing: In the == code block 1== of the above example, there are also a large number of people who call readCharacteristic() or writeCharacteristic() directly/indirectly. So why do I find this embarrassing? For IPC reasons, the entire GattCallback callback actually runs in the binder thread. So if you perform time-consuming operations in onServicesDiscovered, yes, your UI thread does not block. However, the Binder does block. Leading to onCharacteristicRead onCharacteristicWrite, onCharacteristicChanged these three data callback waiting for your operation is completed, If you do not receive onCharacteristicRead feedback after calling readCharacteristic, check. In fact, for ordinary scenes, should not be so strict requirements, right? But the asynchrony in this case has led most people to blur the idea of threads.

Read /write Characteristic feedback

Let’s start with the official notes


/**
     * Reads the requested characteristic from the associated remote device.
     *
     * <p>This is an asynchronous operation. The result of the read operation
     * is reported by the {@link BluetoothGattCallback#onCharacteristicRead}
     * callback.
     *
     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
     *
     * @param characteristic Characteristic to read from the remote device
     * @return true, if the read operation was initiated successfully
     */
     
Copy the code

If you’ve handled the API, it’s clear that readCharacteristic results are the asynchronous callback described above in gattcallback#onCharacteristicRead. @return true, if the read operation was initiated successfully return false

 public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
        // A paragraph is omitted

        synchronized(mDeviceBusy) {
            if (mDeviceBusy) return false;
            mDeviceBusy = true;
        }
         try {
            mService.readCharacteristic(mClientIf, device.getAddress(),
                characteristic.getInstanceId(), AUTHENTICATION_NONE);
        } catch (RemoteException e) {
            mDeviceBusy = false;
            return false;
        }

        return true;
    }
Copy the code

MDeviceBusy the sign bit determines the interval between you two, speaking, reading and writing (usually) at about 25 ms, BLE, speaking, reading and writing, must wait for the last time to read and write, is waiting for onCharacteristicRead/onCharacteristicWrite callback. So in order to do short continuous reads and writes, the old code in the project looks like this:


 @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicRead(gatt, characteristic, status);
            /** * a series of flag bit judgment, progress identifier */
            while(running){ gatt.readCharacteristic(characteristic); }}Copy the code

The result is an unmanageable succession of callbacks, and, as mentioned above, this code can cause blocking. This may not seem ugly to read, but OTA Bluetooth update is required for most iot Bluetooth devices. The upgrade involves writing firmware files in blocks to the device, and you have to mark and control the progress in onCharacteristicWrite, making this asynchronous clutter your process.

I want the API

While refactoring the code and running through the old code, I couldn’t help but feel overwhelmed by the chaotic, ‘running around’ process. Part of the problem can be attributed to the fuzzy concept of threads, a chaotic callback hell. Admittedly, android’s official API seems fine because of IPC and non-blocking Ui threads, but for my project, I feel bad about it. So the priority is to sort out the logic and solve the callback hell.

I don’t need asynchrony, I just want spaghetti code

As long as you are clear about your worker threads, I think ditching the official asynchrony is the best solution. Take OTA update for example: writeCharacteristic -> wait ->onCharacteristicWrite-> Loop write to the next block of data we can use lock control to wrap asynchronous synchronization into blocking synchronization:

while(running && otaBin.nextBlock()){
    byte[] block = otaBin.get();
    xxx.write(block);
}

Copy the code

Code like this is easy to read (manual funny)

To achieve this flow, I give the following reference :(ps: Read and write share the same lock)

public void write(byte[] datas){
        if (mc==null) {return; } ioLock.lock(); mc.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); mc.setValue(datas); mGatt.writeCharacteristic(mc); }// Then in gattCallback

@Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            // Some parsing operations?
            ioLock.unlock();
        }


Copy the code

About the lock

From the above analysis, it is obvious that what I need is a non-reentrant lock.

Here ARE two ways:

//CAS spins
public class CASSpinLock {
    private AtomicInteger status = new AtomicInteger(0);

    public void lock(a) {
        while(! status.compareAndSet(0.1)) { Thread.yield(); }}public void unlock(a) {
        status.compareAndSet(1.0); }}// How threads wake up from sleep
public class SpinLock {
    private ArrayBlockingQueue queue = new ArrayBlockingQueue(1);
    private Object signObj=new Object();

    public boolean lock(a){
        boolean result=false;
        try {
            result = queue.offer(signObj,10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            returnresult; }}public void unlock(a){
        try {
            if (queue.size()==0) {return;
            }
            queue.poll(10, TimeUnit.SECONDS);
        } catch(Exception e) { e.printStackTrace(); }}Copy the code

In these two ways, CAS will lead to an increase in CPU usage, and the thread sleep mechanism needs to be frequently woken up, and the cost of hibernation is unknown. I would suggest not using CAS because bluetooth consumes a lot of power.

Finally give the example code point I go to Github