概述

本系列文章目的是描述aosp如何处理蓝牙,把蓝牙代码分为三个部分,从上到下依次是bt-frameworks、bt-JNI、bt协议栈。

  • bt-framewarks。<aosp>/frameworks目录中和蓝牙相关代码。都是Java代码。
  • bt-JNI。<aosp>/packages目录中和蓝牙相关代码。差不多都在<aosp>/packages/apps/Bluetooth。靠近bt-framewarks的是Java代码,靠近bt协议栈的是C代码。
  • bt协议栈。<aosp>/system/bt目录中源码。都是C代码。它们会编译成一个so,像libbluetooth.so。

依据的是Android 12源代码,下载地址:Android12.0 SDK 。运行的Android主板是Firefly ROC-RK3588S-PC。

  • BTIF: Bluetooth Interface 
  • BTU : Bluetooth Upper Layer 
  • BTM: Bluetooth Manager 
  • BTE: Bluetooth embedded system 
  • BTA :Blueetooth application layer 
  • CO: call out\CI: call in 
  • HF : Handsfree Profile 
  • HH: HID Host Profile 
  • HL: Health Device Profile
  • AV:audio\vidio 
  • AG: audio gateway
  • r: audio/video registration 
  • gattc: GATT client
  • BLE: Bluetooth Low Energy

 

一、tBTA_GATTC_CB

图1 tBTA_GATTC_CB

整个bt协议栈只有一个tBTA_GATTC_CB:全局变量bta_gattc_cb。

p_rcb经常表示一个指向tBTA_GATTC_RCB的指针。p_clcb经常表示一个指向tBTA_GATTC_CLCB的指针。p_srcb经常表示一个指向tBTA_GATTC_SERV的指针。

tBTA_GATTC_RCB的关键字段是client_if。tBTA_GATTC_CLCB关键字段有两种,第一种bta_conn_id,第二种p_rcb->client_if+bda+transport。

对一次连接,一般先生成一个p_rcb,然后是p_clcb,在获取到peripheral的gatt数据库后,生成p_clcb->p_srcb。

p_clcb->p_srcb指向的内存块怎么来自哪?——调用bta_gattc_clcb_alloc生成一个tBTA_GATTC_CLCB时,用bta_gattc_srcb_alloc分配的,内存来自known_server中第一个in_use=false的tBTA_GATTC_SERV。

clcb的单元数小于cl_rcb,那是不是会有多个cl_rcb指向同一个clcb?——不清楚。但是,出现的情况看去和它相反,多个clcb使用同一个rcb,rcb中的num_clcb,指示了使用它的clcb个数。这些clcb有什么特点:拥有一样的client_if,但不一样的remote_bda或transport。我不清楚,当出现多个clcb使用一个rcb时,这是什么场景。

 

二、ble center app编程

阅读bt协议栈代码,有时只是希望能让自已的center app填对参数、更稳定,这里列出几个和center app编程较相关结论。

final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
BluetoothGatt bluetoothGatt;

2.1 BluetoothDevice.connectGatt的transport参数

{
    int type = device.getType();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && device.getType() == BluetoothDevice.DEVICE_TYPE_DUAL) {
        bluetoothGatt = device.connectGatt(mActivity, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
    } else {
        bluetoothGatt = device.connectGatt(mActivity, false, gattCallback);
    }
}

是DEVICE_TYPE_DUAL类型的peripheral时,他的空中传输既可用传统的BREDR,也可用更新LE,那bt协议栈会选哪种?——它会依赖transport参数,如果不是AUTO,就用transport值,否则取传统的BREDR。基于理论上LE要好于BREDR, 这里强制DEVICE_TYPE_DUAL类型使用LE方式。

2.2 {onConnectionStateChange(status == STATE_CONNECTED)}内是否可调用mBluetoothGatt.discoverServices()

可以,而且建议这么做,这样能最快让app读取到peripheral的gatt数据库。

可以这么认为,device.connectGatt内部依次执行两个操作:registerApp、connect。

  1. registerApp。registerApp会在图1中的cl_rcb[BTA_GATTC_CL_MAX]找一个in_use==false的单元/插槽,然后把这插槽cl_rcb[BTA_GATTC_CL_MAX]内从1开始的索引记为client_if。
  2. connect。建立和peripheral之间的蓝牙空中通信,成功后,几乎是在同时会执行两个操作,当然,这是在两个线程并发执行。一是开始向peripheral读gatt数据库,二是回调{onConnectionStateChange}。

在{onConnectionStateChange},app立即里调用discoverServices()。由于bt协议栈从peripheral读取gatt数据库需要点时间,它收到要处理discoverServices时,极可能还没读到整个gatt数据库。出现这种情况时,bt协议栈会记住这个请求,待读出过整gatt数据库时,发现有discoverServices请求,那它会回调app的{onConnectionStateChange}。

如果bt协议栈处理discoverServices时,已读出整个gatt数据库,那自然更是没问题了,立即回调{onConnectionStateChange}。

2.3 disconnect和close

上面有说到device.connectGatt内部依次执行两个操作:registerApp、connect。

disconnect(),是connectGatt二操作中connect的逆操作。拆掉和peripheral的蓝牙空中连接,并回调{onConnectionStateChange(status == STATE_DISCONNECTED)}。由于没回收client_if,之后可以调用mBluetoothGatt.connect()进行重连。

close()。是connectGatt二操作中registerApp的逆操作。它会向bt协议栈注销此次申请到的client_if,释放所在的cl_rcb。因已将clint_if还给bt协议栈,想再次连接必须调用BluetoothDevice.connectGatt()。

类似BluetoothDevice.connectGatt一个函数就包括mService.registerClient()、mService.clientConnect(),建议app“一个函数”凋用disconnect、close。“一个函数”加引号,是因为中间必须处理参数newState是BluetoothProfile.STATE_DISCONNECTED的{onConnectionStateChange}。必须在onConnectionStateChange处理完私有任务后,再调用close。于是disconnect、close会这么个流程。

  1. 想断开连接了,app调用disconnect()。
  2. 某个时刻,{onConnectionStateChange}被调用。函数首先处理私有操作,然后调用close(),释放client_if。

2.4 bt协议栈如何存储读到的gatt数据库

读到gatt数据库后,除把这数据库上传给app,bt协议栈还会在内部存储下来。存储可分为两处:存储在内存,存储在文件。

  • 存储在内存。一个gatt数据库会占用一个tBTA_GATTC_SERV内存块,见图1中的known_server[BTA_GATTC_KNOWN_SR_MAX],一个gatt数据库会占用当中一个单元。在tBTA_GATTC_SERV内,数据库具体存储在gatt_database字段。
  • 存储在文件。当这个peripheral是配对过时,还会存储到文件。cache文件名示例:/data/misc/bluetooth/gatt_cache_cc4b731d7f1d,文件名中的cc4b731d7f1d是mac地址。对通常的ios、android连接,是没有配对过的。

2.5 断开“孤悬”连接

用的是跨平台蓝牙编程时,会出现“孤悬”连接。所谓“孤悬”连接,是app认为它不存在,可实际存在的连接。

BluetoothDevice.connectGatt(...)后,会得到一个BluetoothGatt,记为mBluetoothGatt。虽然这个mBluetoothGatt != null,那它是不是真存在一个有效连接?——不一定。是不是存在一个有效连接,要看bt-frameworks是否回调了{onConnectionStateChange(status == STATE_CONNECTED)}。

作为跨平台app,因为没用java编程,没法直接控制mBluetoothGatt,而是用{onConnectionStateChange(status == STATE_CONNECTED)}回调,来知道这个mBluetoothGatt存在了一个有效连接。

这里就出现个问题,从app调用BluetoothDevice.connectGatt(...),到app收到onConnectionStateChange,这间隔是不确定的。而app不可能无限制等下去。假设主观设置了10秒,如果10秒没收到onConnectionStateChange,那认为连接失败。而一旦10秒后成功连接了,那就产生了一条“孤悬”连接。

若许吧,10秒算是比较大了,还算安全。如果app犯了编程错误,把这溢出时间缩短了,像4秒,这产生“孤悬”连接可能性大增。有人会说,这是编程错误,是不该出现的。但如果开发的是一个平台,上面运行蓝牙小程序,作为平台开发者,得容许小程序偶尔犯这个错时,平台能处理掉这错误。

按通常逻辑,对“孤悬”连接,app会按连接失败来处理,后面不发mBluetoothGatt.disconnect、mBluetoothGatt.close。而这连接一直不断开,后果就大了。对center这边,虽然还能扫到其它perihal,但都不会连接成功了;如果同时还作为peripherl,那其它center不再能扫描到它。对peripheral这边,也不再能再被扫描到了。

app要解决这问题,须要一个额外断开操作。在rose,是用了一个参数为null的SDL_BleDisconnectPeripheral(nullptr)。

tble::~tble()
{
  SDL_BleDisconnectPeripheral(nullptr);
  ...
}

相应地,java层的SDLBle.disonnect要作相应处理。

<launcher>/src/main/java/org/librose/SDLBle.java
------
    public void disconnect(String address) {
        if (mBluetoothGatt == null) {
            return;
        }
        if (address != null) {
            ...
        } else {

为什么要先把mBluetoothGatt置为null?——执行BluetoothGatt.disconnect后,bt-frameorks会调用mGattCallback.onConnectionStateChange(newState == BluetoothProfile.STATE_DISCONNECTED),在那里,要调用mSingleton.close(address),内中又会调用mBluetoothGatt.close(),那是执行在另一个线程,这两个mBluetoothGatt.close()会出现争抢。这里事先把mBluetoothGatt设为null,避免争抢。

为什么不让在onConnectionStateChange(newState == BluetoothProfile.STATE_DISCONNECTED)内调用mBluetoothGatt.close()?——害怕onConnectionStateChange(newState == BluetoothProfile.STATE_DISCONNECTED)不会被回调。也许在bt-frameworks层或以下,认为这个mBluetoothGatt已经clientDisconnect()了,只是没被unregisterApp()而已。这样做,确保会调用BluetoothGatt.close()

            BluetoothGatt bluetoothGatt = mBluetoothGatt;
            mBluetoothGatt = null;

            bluetoothGatt.disconnect();

为什么要延时?——有时间让bluetoothGatt.disconnect()执行更多操作。那里主要工作是在其它线程执行的。不过都是本地蓝牙操作,应该较快,60毫应该够了。它往往是关闭哪窗口要执行的,延时太长使得关闭窗口时间变成,影响用户体验。

            try {
                Thread.sleep(60);
            } catch (InterruptedException e) { }
            bluetoothGatt.close();
            bluetoothGatt = null;
        }
    }

2.6 同时作为peripheral,连接独占问题

我们知道:不能扫描到一个正连接着的peripherl。因为peripheral一旦连接了,就不会发广播包。

这有个疑问,如果一个android设备,既要作为center去连其它peripheral,又要作为peripheral让别的center来连自已。这就存在两条连接,两连接是否能共存?——应该不能,至少rk3588s-pc是这样。

既然不能共存,难免遇到连接争抢问题,就需要引入先优级。在launcher,是作为center的优先化高于peripheral。举个例子,rk3588s-pc作为peripheral正被一个center连接着,这时用户要用蓝牙控制空调,即rk3588s-pc要作为center去连空调。这时响应控制空调是高优先级,为正确执行,只能断开作为peripheral时的那个连接。

怎么断开?有两种方法。

第一种:BluetoothGattServer.cancelConnection

要注意,如果要使用这种方法,那得当“newState==BluetoothProfile.STATE_CONNECTED 时,必须调用 BluetoothGattServer.connect()”。参考:BluetoothGattServer cancelConnection不会取消连接

@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
    super.onConnectionStateChange(device, status, newState);
    if (newState == BluetoothProfile.STATE_CONNECTED){
        mDevice = device;
        mBluetoothGattServer.connect(device, false);
    }else {
        mDevice = null;
    }
}

private void cancelConnection(){
    if (mDevice != null) {
        mBluetoothGattServer.cancelConnection(mDevice);
    }
}

第二种:结束广播

都不广播了,作为peripherl的连接肯定要断了。

乍一看,这两种方法应该都行。但实际使用后,我是都有点问题,问题表现在那之后,即使作为center的连接断开了,其它center扫描我这个peripheral时,更多出现扫不到情况。当然,不排除是正操作的rk3588s-pc的蓝牙驱动可能有bug。在那板子,还出现个很怪现象。

作为peripheral正被连接着,此时如果不做额外处理,强行作为center去连其它peripheral,这连接自然是失败,奇怪的是即使断开所有连接后,后面作为center去连其它peripheral就都是失败。出这种情况时,它作为peripheral时的功能,好像是正常的。

 

三、btsnoop日志

什么是btsnoop日志,可参考“安卓蓝牙如何查看和分析btsnoop.log”。要让bt输出btsnoop日志,须要做两步。

  1. “设置”中开启“开发者模式”。
  2. 在“开发者选项”,打开“蓝牙HCI信息收集日志”。

做这些后,再有蓝牙空中传输时,这些传输日志就会存储在btsnoopt日志文件。

那日志文件在哪里?——android没有统一规定。以roc-rk3588s-pc为例,它是这么确定日志文件路径的。

<aosp>/system/bt/hci/src/btsnoop.cc
------
#define BTSNOOP_PATH_PROPERTY "persist.bluetooth.btsnooppath"
#define DEFAULT_BTSNOOP_PATH "/data/misc/bluetooth/logs/btsnoop_hci.log"

std::string get_btsnoop_log_path(bool filtered) {
  char btsnoop_path[PROPERTY_VALUE_MAX];
  osi_property_get(BTSNOOP_PATH_PROPERTY, btsnoop_path, DEFAULT_BTSNOOP_PATH);
  std::string result(btsnoop_path);
  if (filtered) result = result.append(".filtered");

  return result;
}

判断文件路径用的是这么个规则。

  1. getprop persist.bluetooth.btsnooppath。读取属性“persist.bluetooth.btsnooppath”,如果非空,它的值就是日志文件路径。
  2. 如果“persist.bluetooth.btsnooppath”为空,取文件路径:/data/misc/bluetooth/logs/btsnoop_hci.log。

在roc-rk3588s-pc,“persist.bluetooth.btsnooppath”值是/sdcard/btsnoop_hci.cfa。可由于权限原因,创建/sdcard中的这个文件失败,那只把这文件改到其它目录,像默认的“/data/misc/bluetooth/logs/btsnoop_hci.log”。

setprop persist.bluetooth.btsnooppath /data/misc/bluetooth/logs/btsnoop_hci.log

但是,上面的setprop还无法修改“persist.bluetooth.btsnooppath”,最后是通过改aosp源码,让get_btsnoop_log_path返回这路径。

日志文件名是btsnoop_hci.log,除了它,同目录下会有btsnoop_hci.log.last。这是上次启动时存储的btsnoop日志。换句话说,一旦设备重新启动,这次的btsnoop_hci.log就是那时的btsnoop_hci.log.last。

3.1 理解btsnoop日志

一个btsnoop日志文件:btsnoop_hci.log,包括扫描,连接,读取gatt数据库。22:22:13:ca:0d:00是center蓝牙地址,5c:39:53:5b:5f:94是peripheral蓝牙地址。先是扫描,12:20:38起开始连接。gatt数据库见“connectGatt(4/4):bta_gattc_start_discover”。

图2 wireshark查看btsnoop日志

HCI是Host Controller Interface缩写,当中host可认为是ble编程中的center,controller则是peripheral。

HCI Command Packet。单向,center发给peripheral,主要是HCI 命令, 注意是命令,不是数据。

HCI Event Packet。单向,peripheral发给center。对应于command packet。

HCI ACL Data Packet。双向, center发给peripheral或者相反。主要是L2CAP发送和接收的数据,我们上层的所有数据,注意是数据,而不是命令,都是通过这个type来传递的。

HCI Synchronous Data Packet:用来传输语音(SCO)数据的。注意一般都会通过PCM接口来传输SCO数据。

 

全部评论: 0

    写评论: