【问题标题】:How could I achieve maximum thread safety with a read/write BLE Gatt Characteristic?如何通过读/写 BLE Gatt 特性实现最大的线程安全?
【发布时间】:2016-08-12 16:27:53
【问题描述】:

我正在与通过一个特性向我发送大量数据的 BLE 设备通信。相同的 Characteristic 用于向设备发送数据。

在Androids BluetoothGattCharacteristic里面有方法

public byte[] getValue() {
    return mValue;
}

public boolean setValue(byte[] value) {
    mValue = value;
    return true;
}

但是,执行发生在不同的线程中。 Android 运行大约 5 个不同的活页夹线程,它们调用

onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)

我现在尝试获取数组作为回调中的第一个操作,但不能保证另一个线程(不受我控制)同时设置数组。

虽然上述方法似乎可以解决问题,但更复杂的问题是“针对传入的数据流”发送数据。

我必须使用相同的特性将数据向下发送到设备,所以我先setValue(),然后BluetoothGatt.writeCharacteristic

public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic) {
// some null checks etc

//now it locks the device
synchronized(mDeviceBusy) {
    if (mDeviceBusy) return false;
    mDeviceBusy = true;
}

//the actual execution

return true;
}

然后我会在某个时候收到来自某个线程的回调

onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) 

但是,当我获取该值并尝试检查它是否是我想要发送的内容时,有时它已经是一个刚刚从其他线程更新的接收包。

如何在不访问 Android BLE API 或堆栈等的情况下使其更加线程安全?

【问题讨论】:

  • 非常好的问题。我只能说,当我几年前测试这个时(但只在一个方向:通知)我测试了在 onCharacteristicChanged 处理程序中做一个线程睡眠,发现它实际上一次只做一个回调,即它等待为了完成直到下一个被调用。尽管我注意到回调总是在不同的线程上。
  • 据我在 BluetoothGatt 源代码中看到的,导致 onCharacteristicChanged 的​​ onNotify 不查看 mDeviceBusy 锁,只是覆盖删除任何全双工功能的特征值
  • 我发现“Binder”机制确保每个 BluetoothGatt 对象一次只执行一个回调。这意味着不存在获取通知流的问题。但是,如果从另一个线程完成写入并且在同一特性上同时收到通知,则仍然存在竞争条件。
  • 而正是这种竞争条件才是危险的。我只是无法确定我发送/接收的内容是否真实,或者它是否是我在收到内容时刚刚写入缓冲区的内容。
  • 有人解决了这个问题吗?

标签: android multithreading bluetooth-lowenergy


【解决方案1】:

在 SDK27 中,BluetoothGatt.java 中的 onNotify() 回调函数已更新为调用 Runnable 中的 BluetoothGattCharacteristic.setValue()BluetoothGattCallback.onCharacteristicChanged() em> run().

此更改允许我们将所有对 BluetoothGattCharacteristic.setValue() 的调用(包括对特征的出站写入和入站通知)强制到同一线程上,从而消除了破坏 BluetoothGattCharacteristic.mValue 的竞争条件;

  1. 创建HandlerThread
  2. 创建一个Handler 附加到您的HandlerThread
  3. 将您的Handler 传递给BluetoothDevice.connectGatt() - 恭喜,当收到通知时,您的HandlerThread 将调用setValue()onCharacteristicChanged()
  4. 当您想写入特征时,通过您的 Handler 将您的 setValue()writeCharacteristic() 发布到您的 HandlerThread

现在所有参与竞态条件的函数调用都在同一个线程上执行,消除了竞态条件。

【讨论】:

  • 处理程序Call requires API level 26的方法
【解决方案2】:

在 SDK27 之前,似乎不可能通过单一特性进行可靠的全双工通信 - 使用公共 API。

很烦。

不过,如果你愿意作弊的话,似乎有办法。 (可能有不止一种作弊方法;下面描述的一种影响相当小。)

问题在于将 BluetoothGattCharacteristic 中的mValue 字段用于两个相互冲突的目的 - 发送和接收。这是相当糟糕的 API 设计;一个 API 级别的修复是引入另一个字段,以便每个方向都有一个。另一种方法是在发送数据时不使用 setValue() 方法,而是将有问题的数据作为参数提供给 writeCharacteristic() (尽管由于值字段可能在获取/设置期间进行编码,这很复杂,支持多种数据类型)。但是,API 维护人员似乎选择了不同的路径。

无论如何 - 要解决此问题,请确保接收的值和要发送的值存储在不同的位置。更具体地说,在 BluetoothGattCharacteristic 的两个不同实例的值字段中。

现在,无法使用官方 API 克隆 BGC。 使用公共构造函数,您可以获得一个实例,该实例设置了除serviceinstanceId 之外的所有字段。这些有公共吸气剂 - 设置它们,使用反射访问 setService()setInstanceId() - 这是欺骗的部分。

我已经测试了这种方法[1],它似乎可以按预期工作。 :-)

[1] 实际上是这种方法的一种变体;在较新的 SDK 版本中,有问题的类是 Parcelable,因此我尽可能通过 Parcel-serialization 克隆对象,以防将来的实现添加更多字段。这会处理除 service 字段之外的所有内容,您仍需要使用反射进行设置。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多