3、QNX蓝牙GAP层开发:设备发现、配对与绑定、连接建立与断开、安全设置
好,咱们进入蓝牙开发最核心的一层——GAP层。GAP,全称Generic Access Profile,说白了就是蓝牙设备的“社交礼仪”。它管的是设备怎么被发现、怎么互相认识、怎么建立连接、怎么安全地聊天。我在QNX车机上做蓝牙开发时,GAP层是打交道最多的部分,坑也最多。
3.1 设备发现:让车机“看见”周围设备
设备发现,就是让车机去扫描周围的蓝牙设备。QNX的蓝牙协议栈基于BlueZ,但车机环境做了不少定制。我个人习惯用ioctl操作蓝牙HCI层,而不是走D-Bus那套——车机上D-Bus太重了。
先看一个典型的扫描启动流程:
// 打开HCI套接字
int hci_sock = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
if (hci_sock < 0) {
perror("socket failed");
return -1;
}
// 设置扫描参数
struct hci_request req;
memset(&req, 0, sizeof(req));
// 启动扫描(允许发现)
inquiry_info *info = NULL;
int num_rsp = hci_inquiry(dev_id, 8, 255, NULL, &info,
IREQ_CACHE_FLUSH);
if (num_rsp < 0) {
perror("hci_inquiry failed");
close(hci_sock);
return -1;
}
// 遍历发现的设备
for (int i = 0; i < num_rsp; i++) {
char addr[18];
ba2str(&info[i].bdaddr, addr);
printf("发现设备: %s, RSSI: %d\n", addr, info[i].rssi);
}
嗯,这里要注意。车机环境下,扫描不能太频繁。我曾经在一个项目里,每2秒扫一次,结果车机蓝牙模块过热重启了。为什么?因为扫描时射频一直处于接收状态,功耗大、发热高。我建议扫描间隔至少5秒以上,每次扫描时长控制在10秒内。
3.2 配对与绑定:建立信任关系
配对,就是两个蓝牙设备交换密钥的过程。绑定,是把密钥存下来,下次不用再配。车机上,配对流程通常由手机发起,但车机需要做好响应准备。
QNX下配对的核心是处理HCI_EVENT_LINK_KEY_REQUEST和HCI_EVENT_PIN_CODE_REQUEST事件。我习惯用事件驱动的方式:
// 注册配对事件回调
void pairing_event_handler(uint8_t event, void *data) {
switch(event) {
case HCI_EVENT_PIN_CODE_REQUEST: {
// 车机通常使用固定PIN码(如"0000")
pin_code_reply *req = (pin_code_reply *)data;
char pin[] = "0000";
hci_send_cmd(hci_sock, OGF_LINK_CTL,
OCF_PIN_CODE_REPLY, 6,
&req->bdaddr, 4, pin);
break;
}
case HCI_EVENT_LINK_KEY_REQUEST: {
// 检查本地是否已存储该设备的密钥
link_key_reply *req = (link_key_reply *)data;
if (has_stored_key(&req->bdaddr)) {
// 使用存储的密钥回复
hci_send_cmd(hci_sock, OGF_LINK_CTL,
OCF_LINK_KEY_REPLY, ...);
} else {
// 拒绝,触发重新配对
hci_send_cmd(hci_sock, OGF_LINK_CTL,
OCF_LINK_KEY_NEG_REPLY, ...);
}
break;
}
}
}
绑定数据的存储,我建议用QNX的persistent_storage API,存到/data/bluetooth/目录下。每个设备一个文件,文件名用蓝牙地址的哈希值,内容包含Link Key、随机数、设备名称等。这样即使车机断电重启,绑定关系也不会丢。
3.3 连接建立与断开:管好通信链路
连接建立,就是两个设备从“认识”到“能聊天”的过程。GAP层负责建立ACL(Asynchronous Connection-Less)链路,这是所有上层协议(如RFCOMM、AVDTP)的基础。
车机上,连接通常由车机主动发起——比如用户点击“连接手机”按钮。代码逻辑如下:
// 主动连接远程设备
int connect_to_device(bdaddr_t *remote) {
int sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
if (sock < 0) return -1;
struct sockaddr_rc addr;
memset(&addr, 0, sizeof(addr));
addr.rc_family = AF_BLUETOOTH;
addr.rc_channel = 0; // 使用默认通道
bacpy(&addr.rc_bdaddr, remote);
// 设置连接超时(车机环境建议10秒)
struct timeval timeout = {10, 0};
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(sock);
return -1;
}
return sock;
}
断开连接时,我习惯先发一个HCI_DISCONNECT命令,而不是直接关socket。为什么?因为直接关socket会导致对端收到“意外断开”的错误码,用户体验不好。正确的做法是:
// 优雅断开连接
void disconnect_device(int handle) {
// 先发HCI断开命令
hci_send_cmd(hci_sock, OGF_LINK_CTL, OCF_DISCONNECT,
3, &handle, 0x13); // 0x13 = Remote User Terminated
// 等待断开完成事件
wait_for_event(hci_sock, HCI_EVENT_DISCONN_COMPLETE, 2000);
// 再关闭socket
close(bt_sock);
}
3.4 安全设置:别让车机变“肉鸡”
蓝牙安全,说白了就是防偷听、防篡改、防冒充。GAP层的安全设置主要涉及三个方面:认证、加密、授权。
QNX下,安全设置通过hci_write_simple_pairing_mode和hci_write_authentication_enable等命令控制。我建议车机强制开启以下安全策略:
| 安全项 | 推荐设置 | 说明 |
|---|---|---|
| Simple Pairing | 开启 | 使用Secure Simple Pairing(SSP),防止PIN码暴力破解 |
| MITM保护 | 开启 | 防止中间人攻击,配对时要求用户确认 |
| 加密强制 | 开启 | 所有音频和数据连接必须加密,拒绝未加密的连接 |
| IO能力 | DisplayYesNo | 车机有屏幕,可以显示数字并让用户确认 |
代码实现上,我通常在蓝牙初始化阶段就设置好安全参数:
// 初始化安全设置
void init_bt_security(int hci_sock) {
// 开启Simple Pairing
hci_send_cmd(hci_sock, OGF_HOST_CTL,
OCF_WRITE_SIMPLE_PAIRING_MODE, 1, &enable);
// 设置IO能力为DisplayYesNo
uint8_t io_cap = 0x01; // DisplayYesNo
hci_send_cmd(hci_sock, OGF_HOST_CTL,
OCF_WRITE_IO_CAPABILITY, 1, &io_cap);
// 强制加密
uint8_t encrypt_mode = 0x02; // 强制加密
hci_send_cmd(hci_sock, OGF_HOST_CTL,
OCF_WRITE_ENCRYPTION_MODE, 1, &encrypt_mode);
printf("蓝牙安全初始化完成\n");
}
还有一个容易被忽略的点:绑定设备的密钥管理。车机可能存储几十个手机的绑定信息,每个密钥长度16字节。如果密钥文件被读取,攻击者就能冒充已绑定的手机。我建议对存储的密钥文件进行加密,使用车机唯一的硬件ID(如TEE中的密钥)作为加密密钥。这样即使文件被拷贝,在其他设备上也无法解密。
嗯,GAP层开发就这些核心内容。说白了,设备发现要节制、配对要安全、连接要优雅、安全要强制。下一章我们讲GATT层,那是蓝牙低功耗(BLE)的主战场,车机上的蓝牙钥匙、胎压监测都用它。到时候再聊。