- 要求peripheral的广播包必须出现一个uuid。
- 不要让一个特征同时具有notify、indicate属性。原因:iOS设置notify、indicate用的是一个函数,当属性只有这们中一个时,它会正确设置,如果同时存在,它只会设置indication。
- read、notify区别。1)read时,何时读到数据的发起权在center。notify时,何时读到数据的发起权在peripheral。2)read是一次性猛读,notify是间隔性地小数据实时读。
一、tble架构

<librose>/ble.cpp实现tble的源码,除tble外,那里还有个tpble。tble针对的是center,tpble针对的peripheral。
在windows,不知道怎么控制蓝牙,在SDL层的部分是个伪实现。按着自个设备的service数据库修改SDL相关源码,这对调试阶段非常有用。
二、反馈机制

图2显示了Android上的读特征操作。center app调用bluetoothGatt.readCharacteristic向本地ble模块发起read,本地ble模块于是通过蓝牙总线向peripheral发送read请求。同时,readCharacteristic函数也就结束了。在将来等个时刻,center的ble模块从蓝牙总线收到peripheral发来数据,判断出是针对那次read的反馈,调用app提供的回调函数onCharacteristicRead。
读特征要使用反馈机制的根本原因是必须借助蓝牙总线。一旦数据要经过总线收发,就没法确定对端啥时能处理出结果,而你也不可能无限期等下去。因为要依赖蓝牙总线,其它操作也须要反馈机制,像连接、断开、写特征、notify特征。
在图2,为进一步把这反馈告知app,java代码写的onCharacteristic会调用C写的nativeCharacteristicRead,后者则调用SDL_BleCallbacks::read_characteristic。这是一个函数指针,rose在初始化tble时,会让它指向tble::did_read_characteristic。did_read_characteristic则进一步调用app重载的app_did_read_characteristic。
android反馈函数 | SDL_BleCallbacks/tble::did_ | 说明 |
onScanResult | discover_peripheral | 扫描到一个peripheral |
onConnectionStateChange | connect_peripheral | 连接了peripheral |
onConnectionStateChange | disconnect_peripheral | 断开了连接 |
onServicesDiscovered | discover_services | 获得peripheral所有服务 |
android不须要 | discover_characteristics | 获得一服务中的所有特征 |
onCharacteristicRead | read_characteristic | (特征)读到了数据,notify=false |
onCharacteristicWrite | write_characteristic | (特征)数据到了peripheral |
onDescriptorWrite | notify_characteristic | (特征)bool类型notify设置到了peripheral |
onCharacteristicChanged | read_characteristic | (特征)读到了数据,notify=true |
这回调和蓝牙无关 | release_peripheral | 释放了一个SDL_peripheral |
2.1、序列化到主线程
蓝牙总线的传输不确定性导致出现了反馈机制,这里有个问题:反馈发生时,回调的函数是在哪个线程执行?——根据不同操作系统有不同实现,iOS是在主线程,android则不是。

图3是发生notify读反馈实例,反馈函数onCharacteristicChanged是在一个叫“Binder:4471_1”线程内调用,很显然,它不是主线程。
为让代码安全、简洁,要求SDL层调用SDL_BleCallbacks内函数时,都必须运行在主线程。即这个序列化是在SDL实现的,对上层的Rose、app来说,它们看到的是反馈都在主线程执行。
为全面,简单说下Android怎么实现序列化,核心是使用一个存储蓝牙操作的数组:BleDids。
<SDL>/src/peripheral/android/SDL_sysble.c ------ typedef union { uint32_t type; DidDiscoverPeripheral DiscoverPeripheral; DidConnectionStateChange ConnectionStateChange; DidServicesDiscovered ServicesDiscovered; DidCharacteristicRead CharacteristicRead; DidCharacteristicWrite CharacteristicWrite; DidDescriptorWrite DescriptorWrite; DidPConnectionStateChange PConnectionStateChange; DidPCharacteristicRead PCharacteristicRead; DidPNotificationSent PNotificationSent; } BleDid; #define BLEDID_COUNT 24 static BleDid BleDids[BLEDID_COUNT];
BleDids用于在两线程间共享数据。数组的一个单元对应一个蓝牙操作。让再以读特征为例。
线程:Binder:4471_1
- 通过JNI规范,java层的onCharacteristicRead调用C函数Java_org_librose_SDLBle_nativeCharacteristicRead。
- (Java_org_librose_SDLBle_nativeCharacteristicRead)BleDid* did = &BleDids[bledids_wt],从BleDids取出下一个空闲单元。
- (Java_org_librose_SDLBle_nativeCharacteristicRead)以当前参数填充did指向的BleDid单元。
线程:主线程
- app时间片函数event::pump会调用Android_PumpEvents。
- (Android_PumpEvents)从BleDids取出did,解析出这是nativeCharacteristicRead操作,做适当处理后,调用tble注册的回调read_characteristic。
Binder:4471_1把蓝牙操作写入BleDids,主线程则从BleDids读取操作,从而实现序列化到主线程。
三、操作characteristic(特征)
操作包括write、read、notify、indicate。命名是站在center角度。举个例子,read是center向peripheral读数据。
indicate和notify的区别在于,indicate时,center发数据给peripheral,peripheral收到数据后,需再发应答给center。notify时,center发数据给peripheral,peripheral不必给center任何应答。
这个应答不是上面反馈机制中的反馈。虽然notify没给center应答,但notify依旧有反馈。
write也分为有应答和没有应答。如果有应答,write成功会收到peripheral的确认消息,但是会降低写入速率。
不要让一个特征同时具有notify、indicate属性。iOS设置notify、indicate用的是一个函数,当属性只有这们中一个时,它会正确设置,如果同时存在,它只会设置indicate。
3.1 流程
图2描述了read流程,这里补全其实它操作。知道它们,有助于理解read、notify区别。为直观,用三个特征的uuid作为示例。
- fd01:可写characteristic。
- fd02:可读characteristic。
- fd03:可notify。可notify的特征须要有Descriptor。没有Descriptor,则不能回调app注册的onDescriptorWrite,没了它,很难实现反馈。
notify流程
设置notify阶段
- (center)app调用gatt.setCharacteristicNotification(chara true),参数chara是fd03对应的BluetoothGattCharacteristic。
- (peripheral)ble模块收到修改notification请求,进行设置。
- (center)ble模块收到notification设置成功,触发反馈,调用app注册的onDescriptorWrite。
收发数据阶段
- (peripheral)向fd03写入数据。
- (center)ble模块收到fd03有变动,触发反馈,调用app注册的onCharacteristicChanged。
- (center)在onCharacteristicRead,app调用characteristic.getValue()得到peripheral写入到fd03的数据。这个数据已经存放了center。
read流程
- (center)app调用bluetoothGatt.readCharacteristic(chara),参数chara是fd02对应的BluetoothGattCharacteristic。
- (center)ble模块把这个读请求发向peripheral。
- (peripheral)调用app的onCharacteristicReadRequest。
- (peripheral)在onCharacteristicReadRequest,调用bluetoothGattServer.sendResponse(..., characteristic.getValue()),把fd02当前数据发向center。
- (center)ble模块收到fd02有可读数据,触发反馈,调用app注册的onCharacteristicRead。
- (center)在characteristic.getValue(),app调用characteristic.getValue()得到peripheral写入到fd02的数据,这个数据已经存放在center了吗?
小结下notify、read区别。
- read时,何时读到数据的发起权在center。notify时,何时读到数据的发起权在peripheral。
- read是一次性猛读,notify是间隔性地小数据实时读。设想有一个体温计,每隔5秒测一次温度,测出的数据会放到本地缓存。同时,如果有center连接,可以每隔5秒发新测的温度到center。对center来说,如果想知道历史温度,那就用read,一次性猛读。如果想实时监控温度,就用notify,
write流程
- (center)app调用bluetoothGatt.writeCharacteristic(chara),参数chara是fd01对应的BluetoothGattCharacteristic,并且已调用characteristic.setValue(value)把要写到periphral的数据放进该chara。
- (center)ble模块把这个写请求发向peripheral。
- (peripheral)调用app的onCharacteristicWriteRequest,参数“byte[] requestBytes”表示写入的数据。
- (center)ble模块认为已把数据写向fd01了,触发反馈,调用app注册的onCharacteristicWrite。
四、控制协议公开的蓝牙设备
如果是用容器加蓝牙小程序这种模式,而且容器代码开源,那么不管编写小程序用什么语言,该蓝牙设备的控制协议必然会是透明的。举个例子,launcher这个app是容器,蓝牙灯以小程序方式融入launcher。由于launcher开源,而小程序是以着调用launcher提供的蓝牙api实现自已功能,那么即使小程序编写语言是反汇编都有难度的C,破解者只要监控launcher那些被调的api时序、以及传入什么参数,就能推出控制蓝牙逻辑,进而小结出它的控制协议。
一旦公开蓝牙设备的控制协议,这就遇到个安全性问题:陌生人扫到这个peripheral,如何防止他按公开的协议控制这peripheral?——想到方法是加密。
一种方法,发送发将数据加密,将收方解密。这有个疑问,假设加密算法用的是aes,一些性能不高的单片机是否留有性能冗余下处理aes加密、解密。二是真有必要每次都做这么严格的数据加密吗。举个例子,要控制灯的开和关,数据可能只是一字节,不让别人知道1表示开、0表示关,真有那么必要。
个人想到另一种方法,这正是launcher中rdpd这peripheral在用的,rdpd协议参考“Leagor IP Discovery Protocol(LIPDP)”。把协议中命令分为两类:非敏感命令和敏感命令。对非敏感命令,peripheral对它们不做执行条件检查,像rdpd中的查询IP,即使陌生人知道这机器人IP,反正也进不了这机器人所在的局域网。对敏感命令,像rdpd的获取wifi列表,连接wifi,删除某个wifi,则要求center须要提供一个访问密码(rdpd把这密码称为蓝牙密码),只有密码核验通过了,才能执行这些命令。另外,所有命令的请求、应答用的是明文,不做任何加密。
这种方法对peripheral多了几个要求。
- peripheral须要有地方能修改蓝牙密码。长使时间使用一个密码是不安全的,只要center认为须要,随时要能修改这密码。修改就是存储一个新密码到peripheral,让peripheral认为它才是接下的正确密码。rdpd是靠luancher在界面提供个按钮进行修改。对蓝牙灯,没有操作界面,可能得通过蓝牙命令进行修改。
- center连上peripheral后,协议须要有命令让center传下蓝牙密码,以便让peripheral进行校验,成功后进入可执行敏感操作状态。在rdpd,这是通过updateip_req实现的。
- peripheral须要有地方能复位蓝牙密码。center可能忘了当前peripheral存储的是什么密码,须要有方法让复位到一个已知密码。rdpd是靠luancher在界面提供个按钮进行修改。对蓝牙灯,没有操作界面,又不知道蓝牙密码,没法通过命令复位,可能得要靠硬件,像长按复位按钮5秒,恢复密码到“123456”。
使用tble的launcher、kdestkop都是开源的,意味着用它写的蓝牙小程序,它们对应设备都是公开的控制协议。