本系列文章目的是描述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

整个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。
- registerApp。registerApp会在图1中的cl_rcb[BTA_GATTC_CL_MAX]找一个in_use==false的单元/插槽,然后把这插槽cl_rcb[BTA_GATTC_CL_MAX]内从1开始的索引记为client_if。
- 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会这么个流程。
- 想断开连接了,app调用disconnect()。
- 某个时刻,{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日志,须要做两步。
- “设置”中开启“开发者模式”。
- 在“开发者选项”,打开“蓝牙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; }
判断文件路径用的是这么个规则。
- getprop persist.bluetooth.btsnooppath。读取属性“persist.bluetooth.btsnooppath”,如果非空,它的值就是日志文件路径。
- 如果“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”。

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数据。