本文介绍Bluedroid HFP的相关流程和知识点。
设备和手机连接之后,设备主动调用协议栈的接口去连接手机HFP。
1 | btif_hf_client_get_interface()->connect(); |
协议栈上下文切换
1 | bt_status_t connect(RawAddress* bd_addr) |
bt_jni_msg_ready()
这个作业会在btif上下文执行。
1 | static void bt_jni_msg_ready(void* context) { |
因此,BTIF_QUEUE_CONNECT_EVT这个事件,会在btif线程里,由queue_int_handle_evt()
这个方法来处理。
1 | static void queue_int_handle_evt(uint16_t event, char* p_param) { |
小结:
- 给应用层的方法,基本都是会转发到btif上下文来执行。这样有一个好处,就是由多线程调用变成单线程处理,在btif层就可以减少很多并发保护的代码,让系统更加简单可靠。
- 发送方申请xiaoxi内存,接收方释放消息内存。
- 所有的connect也是排队执行的,简化了应用场景。
发起HF Client连接
前面我们看到,传进来的连接方法是connect_int()
,而UUID是UUID_SERVCLASS_HF_HANDSFREE。
现在,在btif_task调用connect_int()
。
1 | static bt_status_t connect_int(RawAddress* bd_addr, uint16_t uuid) { |
在btif上下文调用bta的api是正确的。
1 | void BTA_HfClientOpen(const RawAddress& bd_addr, tBTA_SEC sec_mask, |
BTA_HfClientOpen()
仅仅是调用bta_sys_sendmsg()
把事件传到了btu_task。可见 btif 真的只是interface而已 。
bta_sys_sendmsg()
则是post消息到btu的loop,到btu_task执行,处理函数是bta_sys_event()
,它定义在bta_sys_main.cc中。
1 | void bta_sys_event(BT_HDR* p_msg) { |
我们看消息的定义:
1 | enum { |
而 HF Client 注册的函数是bta_hf_client_reg()
:
1 | tBTA_STATUS BTA_HfClientEnable(tBTA_HF_CLIENT_CBACK* p_cback, tBTA_SEC sec_mask, |
因此,BTA_HF_CLIENT_API_OPEN_EVT 这个event,会在btu_task中由bta_hf_client_hdl_event()
处理。
除了 enable 和 disable 之外的事件,都会被传给 bta hf client 状态机处理。
Bluedroid的状态机是整个蓝牙协议栈的核心驱动力。它的实现比较简单,但却非常有效!
状态变化:
current state: INIT
event: BTA_HF_CLIENT_API_OPEN_EVT
action: bta_hf_client_start_open
next state: OPENING
1 | void bta_hf_client_start_open(tBTA_HF_CLIENT_DATA* p_data) { |
要连接对端手机的handsfree服务,首先需要查找对端是否有ag的服务。
从设备的hci来看,也可以看到,设备先发起sdp,查找hf_ag的服务。
开始查找之前,我们看下如果连接冲突,是怎么处理的。
1 | void bta_hf_client_collision_cback(UNUSED_ATTR tBTA_SYS_CONN_STATUS status, |
可以看到,如果冲突了,则停止之前的连接,延时一会儿之后,我们重新发起连接。(为何是这个机制?不懂)
现在回到sdp查找服务。
1 | void bta_hf_client_do_disc(tBTA_HF_CLIENT_CB* client_cb) { |
- 准备db
- 发起服务发现请求
sdp是基于l2cap标准通道做的,因此它需要先建立l2cap连接。
1 | bool SDP_ServiceSearchAttributeRequest2(const RawAddress& p_bd_addr, |
1 | tCONN_CB* sdp_conn_originate(const RawAddress& p_bd_addr) { |
小结:
- hf client是基于状态机运行的,几乎所有profile都是
- hf client连接前,需要先sdp查找对端ag服务
- sdp是基于l2cap的,因此要先建立l2cap连接
L2CAP连接
两个设备想要发起SDP交互,首先需要建立L2CAP连接“
1 | uint16_t L2CA_ConnectReq(uint16_t psm, const RawAddress& p_bd_addr) { |
这个函数里面的第一个参数是 psm(Protocol/ServiceMultiplexer),这里简单介绍一下psm的概念:
- psm是通道服用,它用来指定再L2CAP上通信的更高层面的协议类型
- 相同的高层级的多个实例,可能使用不同的L2CAP通道,但它们可能使用相同的PSM的值
比如,rfcomm的多个通道可能使用相同的psm,但每个通道的cid可能是不同的。
这些是系统注册的psm,也叫做内置psm。
1 |
|
1 | uint16_t L2CA_ErtmConnectReq(uint16_t psm, const RawAddress& p_bd_addr, |
- 检查蓝牙是否打开
- 检查是否有link
- 检查psm是否是提前注册好的
- 申请ccb
- 连接channel
最终走到l2c_csm_execute
,交给l2cap的状态机处理。
接着,发起方和响应方会检查security的要求,如果不满足则可能还需要发起配对。
security满足要求之后,正式进行l2cap的通道连接,并对通道进行配置,最后才能开启数据传输。
L2CAP的状态机这里暂不分析,最后会在CST_CONFIG状态下收到L2CEVT_L2CAP_CONFIG_RSP事件,从而状态机转到CST_OPEN状态。
1 | static void l2c_csm_config(tL2C_CCB* p_ccb, uint16_t event, void* p_data) { |
这里的pL2CA_ConfigCfm_Cb
是sdp_init
时注册的reg_info,回调函数是sdp_config_cfm()
。
1 | void sdp_init(void) { |
1 | static void sdp_config_cfm(uint16_t l2cap_cid, tL2CAP_CFG_INFO* p_cfg) { |
SDP服务发现
1 | void sdp_disc_connected(tCONN_CB* p_ccb) { |
1 | static void process_service_search_attr_rsp(tCONN_CB* p_ccb, uint8_t* p_reply, |
这里构造了真正的搜索服务请求包,然后把请求包通过L2CAP发送了出去。
而接着手机回复了我们搜索服务的应答包。
我们接收对端发过来的sdp的应答,l2cap是通过sdp_data_ind()
回调传过来的。
1 | static void sdp_data_ind(uint16_t l2cap_cid, BT_HDR* p_msg) { |
1 | void sdp_disc_server_rsp(tCONN_CB* p_ccb, BT_HDR* p_msg) { |
process_service_search_attr_rsp
这个函数我们在前面看到一次了,但前面是作为发起的时候,现在是收到应答:
1 | static void process_service_search_attr_rsp(tCONN_CB* p_ccb, uint8_t* p_reply, |
- 提取attr信息
- 判断是否需要连续请求,如果需要则继续请求
- 连续保存attr属性
- 如果全部信息都拿到并保存好,则断开sdp连接
RFCOMM连接
HFP是基于RFCOMM的,找到对端的AG服务后,就要开始进行RFCOMM的连接了。
1 | void sdp_disconnect(tCONN_CB* p_ccb, uint16_t reason) { |
在sdp_disconnect
的时候,会回调告诉用户(这里的sdp的user是指的hfp)。这个cb是在bta_hf_client_do_disc()
的时候传入的bta_hf_client_sdp_cback()
:
1 | void bta_hf_client_do_disc(tBTA_HF_CLIENT_CB* client_cb) { |
在回调中,转发消息到btu_task进行处理,处理函数为bta_hf_client_hdl_event
。而这个事件,会传到hf_client的状态机。
1 | static void bta_hf_client_sdp_cback(uint16_t status, void* data) { |
状态机驱动:
current state:OPENING
event: BTA_HF_CLIENT_DISC_INT_RES_EVT
action: bta_hf_client_disc_int_res
next state: OPENING
1 | void bta_hf_client_disc_int_res(tBTA_HF_CLIENT_DATA* p_data) { |
bta_hf_client_sdp_find_attr
这个函数里面是从之前sdp保存在bta_hf_client_cb.scb.p_disc_db
里面的结果,提取出来,保存在bta_hf_client_cb
这个全局结构体中。
状态机驱动:
current state:OPENING
event: BTA_HF_CLIENT_DISC_OK_EVT
action: bta_hf_client_rfc_do_open
next state: OPENING
1 | void bta_hf_client_rfc_do_open(tBTA_HF_CLIENT_DATA* p_data) { |
到这里,正式开启RFCOMM的连接,然后把这个port绑定给hf_client。
从HCI可以看到,发现服务之后,就开始对通道进行认证加密,然后进行RFCOMM(channel3)的连接。
RFCOMM_CreateConnection
这个函数最后的一个参数是bta_hf_client_mgmt_cback
,RFCOMM连接成功后,会回调该函数。
1 | static void bta_hf_client_mgmt_cback(uint32_t code, uint16_t port_handle) { |
状态机驱动:
- current state:OPENING
- event: BTA_HF_CLIENT_RFC_OPEN_EVT
- action: bta_hf_client_rfc_open
- next state: OPEN
1 | void bta_hf_client_rfc_open(tBTA_HF_CLIENT_DATA* p_data) { |
RFCOMM打开成功之后,把该事件回调给bta,然后开启HFP SCL的建立流程。
1 | void bta_sys_conn_open(uint8_t id, uint8_t app_id, |
另外值得指出的是,这一次,状态机从OPENING状态转变成了OPEN状态,而在hf client的状态机中,状态进入OPEN,会回调给app:
1 | /* If the state has changed then notify the app of the corresponding change */ |
而BTA_HF_CLIENT_OPEN_EVT这个事件,会被抛到btif层,然后再回调给应用:
1 | static void btif_hf_client_upstreams_evt(uint16_t event, char* p_param) { |
可以看到,再RFCOMM建立连接后,第一次通过 connection_state_cb
回调给应用,接着开启SLC流程。
AT命令
接下来就是韩兆Hands-Free Profile进行AT命令的交互了。
1 | void bta_hf_client_slc_seq(tBTA_HF_CLIENT_CB* client_cb, bool error) { |
前面提到,bta_hf_client_setup_port
把bta_hf_client_port_cback
回调注册给rfcomm,rfcomm在该端口收到数据时,会回调该函数。
而bta_hf_client_port_cback
有直接交给了状态机:
1 | static void bta_hf_client_port_cback(UNUSED_ATTR uint32_t code, |
状态机驱动:
- current state:OPEN
- event: BTA_HF_CLIENT_RFC_DATA_EVT
- action: bta_hf_client_rfc_data
- next state: OPEN
1 | void bta_hf_client_rfc_data(tBTA_HF_CLIENT_DATA* p_data) { |
更多AT交互的细节,参考profile协议文档。
总结:
- 协议栈的实现中,基本没有看到锁的存在,这得益于它良好的分层设计,不同层级之间通过发消息和回调的方式进行交互,把跨层的交互集中到一起单线程处理
- 协议栈的实现依赖于它清晰可靠的状态机,状态机简化了我们的正常流程和异常处理
- 需要一个可靠的alarm超时机制,处理无线传输中的各种超时/失效异常
附录 - 常用AT命令
连接管理
连接建立
SLC,即sevice level connection,建立HFP SLC之前必须先建立RFCOMM通道。什么是SLC,SLC就是HFP HF和AG角色的一些AT command的交互,这些AT command交互完毕才叫HFP SLC建立。
图中虚线的部分是可选步骤,在SLC建立的时候可以不做。
比如AT+BAC=<HF Available Codecs>
这个命令是做codec的协商,但如果supported feature中不支持codec协商,那么就没有这一步,从而使用默认的CVSD。
Supported feature exchange
HF角色首先要发起AT+BRSF=<HF supported features>
,AG会回复自己支持的特性。
Codec Negotiation
如果HF和AG都支持BRSF中的Codec协商,则HF需发送AT+BAC=<HF Available Codecs>
告知AG自己所支持的codec类型。
在HFP规范中,CVSD(窄带)是强制支持的,mSBC(宽带)是可选的。
AG Indicators
AG Indicators简单理解是希望AG把哪些信息告知HF。它包括几个流程:
(1)HF发送AT+CIND=?
询问AG支持的indicators(包括service/call/callsetup/callheld/signal/roam/battchg)
AG回复+CIND:...
(2)HF发送at+cind?
询问各个indicators的状态
AG回复+CIND:...
(3)HF发送AT+CMER
使能/禁止AG的indicators状态更新
(4)AG后续indicators有变化,可以通过+CIEV
命令告知HF
AT+CHLD=?
如果HF&AG都支持三方通话,那么发送AT+CHLD=?
询问手机三方通话支持的特性有哪些:
0 = 释放所有的hell calls 或者为一个waiting call 设置 User Determined User Busy (UDUB)
1 = 释放所有的active calls,接受另外的(held or waiting) call
1
= 挂断指定索引的active call 2 = 把所有的active calls 变成hold,然后接受另外的(held or waiting)call
2
= 除了idx的call外,其他全部都hold 3 = 增加一路通话
4 = Connects the two calls and disconnects the subscriber from both calls (连接两个电话并且断开两个电话的订阅)
HF Indicators
① 如果HF & AG都支持HF Indicators的feature,那么HF发送AT+BIND=<HF supported HF indicators>
来告知AG支持哪些indicator
② 发送AT+BIND=?
问询AG支持哪些indicator
③ 发送AT+BIND?
问询AG哪些indicator是enable的
④ 发送AT+BIEV
来使能某一个indicator
HFP的indicator一共有两个:
- Enhanced Safety
- Battery Level
连接释放
SLC的断开直接断开RFCOMM的链路就行了。
传输手机状态
AT+CMER
,全局使能
AT+BIA
,某一个indicator的active/deactive
使能indicator后,AG状态更新会发送 +CIEV
消息告知HF
- 信号强度:AG报告信号的强度
- 漫游状态:手机有没有入网,是否有插卡
- 电池电量:0-5
- 查询手机网络:查询手机的网络,比如“中国移动”
- Report Extended Audio Gateway Error Results Code
如果有错误出现,AG会通过+CME ERROR:<err>
发送给HF
Call 状态更新
incoming call, outgoing call,接听,挂断,没信号等场景都会触发
Call setup状态更新
来电、去电时会报告状态更新
Call held状态更新
0 = No calls held
1 = Call is placed on hold or active/held calls swapped
2 = Call on old, no active call
三方通话的时候经常会出现这种状态。
接听/挂断
- 接听来电
使用ATA
命令来接通电话。
另外注意一下CIEV命令上报call和callsetup的时机。
拒接来电
AT+CHUP
拨号
拨打电话
ATDdd...dd
语音信箱
ATD>nnn...
尾号重播
AT+BLDN
获取本地号码/获取通话号码
获取本机号码
HF:
AT+CNUM
AG:
+CNUM, <number>, <type>[,,<service>]
获取当前通话状态
AT+CLCC