Android BLE 从不接收来自特性的通知

Android BLE never receiving notifications from characteristics

我已经苦苦挣扎了一段时间,试图让我的 BLE 设备与我的 android 应用程序通信。

首先,这是我的 BLE 处理的完整代码:

BleCentral.java

import java.util.HashMap;
import java.util.function.Supplier;

import android.util.Log;
import android.content.Context;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothManager;

public class BleCentral
{
    private HashMap<String, BluetoothGatt> m_connectedDevices;

    public BleCentral()
    {
        m_connectedDevices = new HashMap<>();
    }

    private BluetoothAdapter m_GetAdapter(Context ctx)
    {
        final BluetoothManager bleMgr = (BluetoothManager)(ctx.getSystemService(Context.BLUETOOTH_SERVICE));
        BluetoothAdapter adapter = bleMgr.getAdapter();

        if (adapter == null || !adapter.isEnabled())
        {
            Log.e("BLE Central", "BLE either not available or not enabled. Please do something about it.");
            return null;
        }

        return adapter;
    }

    public BluetoothDevice GetDevice(Context ctx, String address)
    {
        return m_GetAdapter(ctx).getRemoteDevice(address);
    }

    public <T extends BlePeripheral>
    T Connect(Context ctx, String address, Supplier<T> supplier)
    {
        BluetoothDevice device = GetDevice(ctx, address);

        T result = supplier.get();
        m_connectedDevices.put(address, device.connectGatt(ctx, false, result, BluetoothDevice.TRANSPORT_LE));

        return result;
    }
}

BlePeripheral.java

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.UUID;

import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.os.Build;
import android.os.Handler;
import android.util.Log;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattService;


public abstract class BlePeripheral
    extends BluetoothGattCallback
{
    private final String kCCC_DESCRIPTOR_UUID = "00002902-0000-1000-8000-00805f9b34fb";

    private Handler       m_handler;
    private boolean       m_deviceReady;
    private BluetoothGatt m_bluetoothGatt;

    private Queue<CmdQueueItem> m_cmdQueue;
    private boolean             m_cmdQueueProcessing;

    // ------------------------------------------------------------------------
    // -- Own methods

    protected BlePeripheral()
    {
        m_handler       = new Handler();
        m_deviceReady   = false;
        m_bluetoothGatt = null;

        m_cmdQueue = new LinkedList<>();
        m_cmdQueueProcessing = false;
    }

    public boolean IsDeviceReady()
    { return m_deviceReady; }

    public void EnableNotifications(UUID service, UUID characteristic)
    {  EnqueueSetNotificationForCharacteristic(m_bluetoothGatt.getService(service).getCharacteristic(characteristic), true); }

    public void DisableNotifications(UUID service, UUID characteristic)
    {  EnqueueSetNotificationForCharacteristic(m_bluetoothGatt.getService(service).getCharacteristic(characteristic), false); }

    protected void WriteCharacteristic(UUID service, UUID characteristic, byte[] value, boolean requestResponse)
    { EnqueueWriteCharacteristic(m_bluetoothGatt.getService(service).getCharacteristic(characteristic), value, requestResponse); }

    protected void ReadCharacteristic(UUID service, UUID characteristic)
    { EnqueueReadCharacteristic(m_bluetoothGatt.getService(service).getCharacteristic(characteristic)); }

    // ------------------------------------------------------------------------
    // -- BluetoothGattCallback overrides

    @Override
    public void onConnectionStateChange(final BluetoothGatt gatt, int status, int newState)
    {
        super.onConnectionStateChange(gatt, status, newState);

        final BluetoothDevice device = gatt.getDevice();

        switch (status)
        {
        case 133: /* GATT_ERROR */
            Log.e("BLE", "GATT_ERROR");
            gatt.close();

            try   { Thread.sleep(150); }
            catch (InterruptedException e) { e.printStackTrace(); }
            break;

        case 0: /* GATT_SUCCESS */
            switch (newState)
            {
            case BluetoothGatt.STATE_CONNECTED:
                Log.i("BLE", "Connected to " + device.getAddress() + " (" + device.getName() + ")");
                m_bluetoothGatt     = gatt;

                int delayWhenBonded = (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) ? 2000 : 0;

                switch (device.getBondState())
                {
                case BluetoothDevice.BOND_NONE:
                    delayWhenBonded = 0;

                case BluetoothDevice.BOND_BONDED:
                    m_handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            boolean result = gatt.discoverServices();
                            if (!result)
                                Log.e("BLE", "discoverServices() failed to start");
                        }
                    }, delayWhenBonded);
                    break;

                case BluetoothDevice.BOND_BONDING:
                    Log.i("BLE", "Waiting for bonding to complete");
                    break;
                }
                break;

            case BluetoothGatt.STATE_DISCONNECTED:
                gatt.close();
                break;
            }

            break;
        }
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status)
    {
        super.onServicesDiscovered(gatt, status);

        if (status == 129 /* GATT_INTERNAL_ERROR */)
        {
            Log.e("BLE", "Service discovery failed");
            gatt.disconnect();
            return;
        }

        final List<BluetoothGattService> services = gatt.getServices();
        Log.i("BLE", "Discovered " + services.size() + " services for " + gatt.getDevice().getAddress());

        m_deviceReady   = SetupDevice(gatt);

        if (!m_deviceReady)
            Log.e("BLE", "Peripheral does not comply to this device's requirements");
    }

    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic)
    {
        super.onCharacteristicChanged(gatt, characteristic);

        Log.i("BLE", "onCharacteristicChanged: " + characteristic.getUuid());
        final byte[] value = new byte[characteristic.getValue().length];
        System.arraycopy(characteristic.getValue(), 0, value, 0, characteristic.getValue().length);

        OnUpdate(characteristic.getService().getUuid(), characteristic.getUuid(), value);
    }

    @Override
    public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, int status)
    {
        super.onCharacteristicRead(gatt, characteristic, status);

        ProcessCmdQueue();

        Log.i("BLE", "onCharacteristicRead: " + characteristic.getUuid());
        final byte[] value = new byte[characteristic.getValue().length];
        System.arraycopy(characteristic.getValue(), 0, value, 0, characteristic.getValue().length);

        OnUpdate(characteristic.getService().getUuid(), characteristic.getUuid(), value);
    }

    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, int status)
    {
        super.onCharacteristicWrite(gatt, characteristic, status);

        ProcessCmdQueue();

        Log.i("BLE", "onCharacteristicWrite: " + characteristic.getUuid());
    }

    @Override
    public void onDescriptorWrite(BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status)
    {
        super.onDescriptorWrite(gatt, descriptor, status);

        ProcessCmdQueue();

        final BluetoothGattCharacteristic parentCharacteristic = descriptor.getCharacteristic();

        if(status != 0 /* GATT_SUCCESS */)
        {
            Log.e("BLE", "WriteDescriptor failed for characteristic " + parentCharacteristic.getUuid());
            return;
        }

        if(descriptor.getUuid().equals(UUID.fromString(kCCC_DESCRIPTOR_UUID)))
        {
            if(status == 0 /* GATT_SUCCESS */)
            {
                byte[] value = descriptor.getValue();
                if (value != null)
                {
                    if (value[0] != 0)
                        Log.i("BLE", "Characteristic " + parentCharacteristic.getUuid() + " is now notifying");
                    else
                        Log.i("BLE", "Characteristic " + parentCharacteristic.getUuid() + " is now NOT notifying");
                }
            }
        }
    }

    // ------------------------------------------------------------------------
    // -- Command Queue implementation

    /* An enqueueable write operation - notification subscription or characteristic write */
    private class CmdQueueItem
    {
        BluetoothGattCharacteristic characteristic;
        byte[] dataToWrite; // Only used for characteristic write
        boolean writeWoRsp; // Only used for characteristic write
        boolean enabled;    // Only used for characteristic notification subscription
        public m_queueItemType type;
    }

    private enum m_queueItemType
    {
        SubscribeCharacteristic,
        ReadCharacteristic,
        WriteCharacteristic
    }

    /* queues enables/disables notification for characteristic */
    public void EnqueueSetNotificationForCharacteristic(BluetoothGattCharacteristic ch, boolean enabled)
    {
        // Add to queue because shitty Android GATT stuff is only synchronous
        CmdQueueItem m_queueItem    = new CmdQueueItem();
        m_queueItem.characteristic = ch;
        m_queueItem.enabled        = enabled;
        m_queueItem.type           = m_queueItemType.SubscribeCharacteristic;
        EnqueueBleCommand(m_queueItem);
    }

    /* queues enables/disables notification for characteristic */
    public void EnqueueWriteCharacteristic(final BluetoothGattCharacteristic ch, final byte[] dataToWrite, boolean requestResponse)
    {
        // Add to queue because shitty Android GATT stuff is only synchronous
        CmdQueueItem m_queueItem    = new CmdQueueItem();
        m_queueItem.characteristic = ch;
        m_queueItem.dataToWrite    = dataToWrite;
        m_queueItem.writeWoRsp     = !requestResponse;
        m_queueItem.type           = m_queueItemType.WriteCharacteristic;
        EnqueueBleCommand(m_queueItem);
    }

    /* request to fetch newest value stored on the remote device for particular characteristic */
    public void EnqueueReadCharacteristic(BluetoothGattCharacteristic ch)
    {
        // Add to queue because shitty Android GATT stuff is only synchronous
        CmdQueueItem m_queueItem = new CmdQueueItem();
        m_queueItem.characteristic = ch;
        m_queueItem.type = m_queueItemType.ReadCharacteristic;
        EnqueueBleCommand(m_queueItem);
    }

    /**
     * Add a transaction item to transaction queue
     * @param m_queueItem
     */
    private void EnqueueBleCommand(CmdQueueItem m_queueItem)
    {
        m_cmdQueue.add(m_queueItem);

        // If there is no other transmission processing, go do this one!
        if (!m_cmdQueueProcessing)
            ProcessCmdQueue();
    }

    /**
     * Call when a transaction has been completed.
     * Will process next transaction if queued
     */
    private void ProcessCmdQueue()
    {
        if (m_cmdQueue.size() <= 0)
        {
            m_cmdQueueProcessing = false;
            return;
        }

        m_cmdQueueProcessing = true;
        CmdQueueItem m_queueItem = m_cmdQueue.remove();

        switch (m_queueItem.type)
        {
            case WriteCharacteristic:
                writeDataToCharacteristic(m_queueItem.characteristic, m_queueItem.dataToWrite, m_queueItem.writeWoRsp);
                break;

            case SubscribeCharacteristic:
                setNotificationForCharacteristic(m_queueItem.characteristic, m_queueItem.enabled);
                break;

            case ReadCharacteristic:
                requestCharacteristicValue(m_queueItem.characteristic);
                break;
        }
    }

    public void requestCharacteristicValue(BluetoothGattCharacteristic ch)
    {
        if (m_bluetoothGatt == null)
            return;

        m_bluetoothGatt.readCharacteristic(ch);
    }

    private void writeDataToCharacteristic(final BluetoothGattCharacteristic ch, final byte[] dataToWrite, boolean writeWoRsp)
    {
        if (m_bluetoothGatt == null || ch == null)
            return;

        ch.setValue(dataToWrite);
        ch.setWriteType(writeWoRsp ? BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE : BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
        m_bluetoothGatt.writeCharacteristic(ch);
    }

    /* enables/disables notification for characteristic */
    private void setNotificationForCharacteristic(BluetoothGattCharacteristic ch, boolean enabled)
    {
        if (m_bluetoothGatt == null || ch == null)
            return;

        ch.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
        boolean success = m_bluetoothGatt.setCharacteristicNotification(ch, enabled);
        if(success)
        {
            // This is also sometimes required (e.g. for heart rate monitors) to enable notifications/indications
            // see: https://developer.bluetooth.org/gatt/descriptors/Pages/DescriptorViewer.aspx?u=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
            BluetoothGattDescriptor descriptor = ch.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
            if(descriptor != null)
            {
                if (enabled)
                    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                else
                    descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);

                if (m_bluetoothGatt.writeDescriptor(descriptor))
                {
                    Log.i("BLE Peripheral", "SetNotification (Set + CCC) succeeded!");
                }
            }
            else
                Log.i("BLE Peripheral", "SetNotification (Set only) succeeded!");
        }
        else
            Log.e("BLE Peripheral", "SetNotification failed!");
    }

    // ------------------------------------------------------------------------
    // -- Abstract methods

    protected abstract boolean SetupDevice(BluetoothGatt gatt);
    protected abstract void    OnUpdate(UUID service, UUID characteristic, final byte[] value);
}

用于创建和连接到设备的代码

BleCentral central = new BleCentral();
m_customDevice     = central.Connect(this, deviceMacAddress, () -> new CustomDevice());

CustomDevice 只是继承了 BlePeripheral class,实现了 SetupDevice(检查所有服务和特性是否存在)和 OnUpdate(接收新数据并处理它)。

现在,有两件事困扰着我:

因为我是 BLE 外围设备上代码 运行 的幕后黑手,所以我可以保证我试图从中获取数据的特征是 NOTIFY 类型(而不是例如 INDICATE)。

如果它能以任何方式提供帮助,唯一的 NOTIFY 特性就是尽可能频繁地发送一个 56 字节的数组,其中填充了 14 个浮点数。使用 Web Bluetooth 或 NativeScript(使用 nativescript-bluetooth 插件)的早期原型向我展示了这确实有效,在这些情况下,我大约每 90 毫秒得到一次结果。

我想我已经将这段代码重写了大约 3 次,我有点绝望了,所以任何朝着正确方向前进的帮助都将不胜感激。 :D

非常感谢!

Edit:为了科学,我尝试在设备上将特性切换为 READ 特性,然后生成线程读取每秒发送一次,而不是等待通知。好吧,调用了 onCharacteristicRead,但是传递给它的字节数组的长度始终为零...

我发现了问题 - 它与 Java 代码根本无关,而是由于我的 BLE 设备中的硬件故障导致所有值更新被丢弃。