蓝牙hci_inquiry指定LAP无效?原因分析与Raw Socket解决方法
2025-05-03 08:22:37
蓝牙开发踩坑:为何 hci_inquiry 函数无视你指定的 LAP?
搞蓝牙开发的时候,你可能会用到 BlueZ 提供的 hci_inquiry
函数来扫描周围的设备。一般情况下挺好使,但当你尝试指定一个特定的 LAP (Limited Access Procedure) 地址时,比如想用 0x00, 0x8b, 0x9e
来进行限定查询,可能会遇到个怪事:抓包一看,发出去的 Inquiry 请求用的 LAP 根本不是你指定的那个,反而是固定的 0x9e8b33
,也就是 GIAC (General/Unlimited Inquiry Access Code)。
就像下面这段代码遇到的情况:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/l2cap.h>
int main(int argc, char** argv) {
inquiry_info *ii = NULL;
int max_rsp, num_rsp;
int dev_id, sock, len, flags;
int i;
// char addr[19] = { 0 }; // Unused in this minimal example
// char name[248] = { 0 }; // Unused in this minimal example
// 获取默认蓝牙适配器的 ID
dev_id = hci_get_route(NULL);
// 打开 HCI 设备
sock = hci_open_dev( dev_id );
if (dev_id < 0 || sock < 0) {
perror("打开 HCI 设备失败");
exit(1);
}
len = 3; // 查询持续时间 (大约 len * 1.28 秒)
max_rsp = 255; // 最大响应设备数
flags = IREQ_CACHE_FLUSH; // 清除缓存的旧结果
ii = (inquiry_info*)malloc(max_rsp * sizeof(inquiry_info));
if (!ii) {
perror("内存分配失败");
close(sock);
exit(1);
}
// 期望使用的 LAP (Limited Access Procedure)
uint8_t lap[3] = { 0x00, 0x8b, 0x9e }; // 对应 0x9e8b00
printf("尝试使用 LAP: 0x%02x%02x%02x 进行查询...\n", lap[2], lap[1], lap[0]);
// 发起蓝牙设备查询
// 注意:这里的 lap 参数似乎会被忽略
num_rsp = hci_inquiry(dev_id, len, max_rsp, lap, &ii, flags);
if (num_rsp < 0) {
perror("hci_inquiry 执行失败");
} else {
printf("查询完成,找到 %d 个设备。\n", num_rsp);
// 实际应用中,这里会处理 ii 数组中的结果
}
free( ii );
close( sock );
return 0;
}
运行这段代码,同时用 Wireshark 抓取 bluetooth
接口的通信,会发现发出的 HCI_Inquiry
命令里的 LAP 始终是 0x9e8b33
(GIAC),而不是代码里指定的 0x9e8b00
。
这就怪了。明明传了参数,怎么好像没起作用? 有些开发者甚至尝试过用 libusb
直接控制 USB 蓝牙适配器发送 Inquiry 命令,发现是可以用自定义 LAP 的,这说明硬件本身没问题。那问题到底出在哪?怎样才能让程序按照我们指定的 LAP 去查询呢?
问题根源分析:hci_inquiry
的“自作主张”
这事儿吧,原因可能不在于你的代码写错了,也不是 hci_lib
里有个明显的 bug。更可能的情况是,hci_inquiry
这个函数作为 BlueZ 提供的一个相对高层的封装,它在设计上可能就简化了 Inquiry 的流程,或者说,它主要面向的是最常见的“通用查询”场景。
- 高层封装的默认行为:
hci_inquiry
位于bluez-libs
中,它封装了底层的 HCI 命令细节。为了简化通用设备发现流程,它很可能在内部实现中,对于标准的 Inquiry 请求,默认或强制使用了 GIAC (0x9e8b33
)。蓝牙协议里,GIAC 是最常用、最通用的 LAP,用于发现所有处于“可发现”模式的设备。 - LIAC 的特殊性: 使用特定的 LAP (构成 LIAC - Limited Inquiry Access Code) 的查询,通常不是为了发现所有设备,而是用在特定的、预定义的场景下,比如快速连接已知类型的设备或在特定设置流程中。
hci_lib
的设计者可能认为这种需求不那么普遍,或者处理起来更复杂,因此没有在hci_inquiry
这个便捷函数里完全、直接地支持自定义 LAP 的所有情况,即使参数列表里包含了lap
。 - 内核交互的可能影响:
hci_lib
最终还是需要通过系统调用与内核的蓝牙协议栈 (net/bluetooth
) 交互,才能让蓝牙控制器执行操作。内核在处理来自用户态通过hci_lib
发起的查询请求时,也可能存在一些策略或限制,倾向于执行通用查询。
简单来说,虽然 hci_inquiry
函数签名里有 lap
这个参数,但在实际执行路径上,它(或其调用的更底层函数,乃至内核)可能并没有严格使用你传入的值,而是选择了“更安全”或“更通用”的 GIAC。你的硬件和驱动程序确实能发送带有任意 LAP 的 Inquiry 命令(libusb
实验证明了这点),但通过 hci_inquiry
这个“官方推荐”路径时,自由度受到了限制。
解决方案:绕过限制,掌控 LAP
既然标准的 hci_inquiry
函数不给力,咱们就得想办法绕开它的限制。
方案一:硬核出击 —— 使用 Raw HCI Socket
这是最直接也最可靠的方法。既然 hci_inquiry
这个高级接口不按我们说的做,那咱们就直接跟内核的蓝牙 HCI 层对话,手动发送原始的 HCI 命令。这样就能完全控制命令的每一个字节,包括 LAP。
原理与作用
通过创建一个 PF_BLUETOOTH
协议族、SOCK_RAW
类型的套接字,并绑定到 BTPROTO_HCI
,你的程序就能直接读写进出蓝牙控制器(通过内核)的 HCI 事件和命令。你可以自己构建一个符合蓝牙核心规范的 HCI_Inquiry
命令包,把想要的 LAP 填进去,然后通过这个 Raw Socket 发送出去。同样,也需要通过这个 Socket 来接收和解析查询结果等 HCI 事件。
操作步骤与代码示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
// HCI Command Packet structure (simplified preamble)
typedef struct {
uint16_t opcode; // OCF & OGF combined
uint8_t plen; // Parameter Total Length
// Parameters follow here...
} __attribute__((packed)) hci_command_hdr;
// HCI_Inquiry command parameters
typedef struct {
uint8_t lap[3]; // LAP to use for inquiry
uint8_t length; // Inquiry duration (N * 1.28 sec)
uint8_t num_rsp; // Max number of responses (0 = unlimited within duration)
} __attribute__((packed)) hci_inquiry_cp;
int main(int argc, char **argv) {
int dev_id, dd; // dd is the raw socket descriptor
struct sockaddr_hci addr;
struct hci_filter flt;
hci_command_hdr *cmd_hdr;
hci_inquiry_cp *cp;
unsigned char buf[HCI_MAX_FRAME_SIZE];
ssize_t len;
// 1. 获取设备 ID
dev_id = hci_get_route(NULL);
if (dev_id < 0) {
perror("无法获取默认蓝牙设备");
exit(1);
}
printf("使用设备 hci%d\n", dev_id);
// 2. 创建 Raw HCI Socket
dd = socket(PF_BLUETOOTH, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, BTPROTO_HCI);
if (dd < 0) {
perror("无法创建 Raw HCI Socket");
exit(1);
}
// 3. 绑定 Socket 到指定 HCI 设备
memset(&addr, 0, sizeof(addr));
addr.hci_family = AF_BLUETOOTH;
addr.hci_dev = dev_id;
addr.hci_channel = HCI_CHANNEL_RAW; // Use the RAW channel
if (bind(dd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
perror("无法绑定 Raw HCI Socket 到设备");
close(dd);
exit(1);
}
// 4. 设置 HCI 事件过滤器 (可选但推荐)
// 我们关心 Command Complete 和 Inquiry Result 事件
hci_filter_clear(&flt);
hci_filter_set_ptype(HCI_EVENT_PKT, &flt); // We want event packets
hci_filter_set_event(EVT_CMD_COMPLETE, &flt);
hci_filter_set_event(EVT_INQUIRY_RESULT, &flt);
hci_filter_set_event(EVT_INQUIRY_RESULT_WITH_RSSI, &flt);
hci_filter_set_event(EVT_EXTENDED_INQUIRY_RESULT, &flt);
hci_filter_set_event(EVT_INQUIRY_COMPLETE, &flt);
if (setsockopt(dd, SOL_HCI, HCI_FILTER, &flt, sizeof(flt)) < 0) {
perror("无法设置 HCI 过滤器");
close(dd);
exit(1);
}
// 5. 构建 HCI_Inquiry 命令
memset(buf, 0, sizeof(buf));
cmd_hdr = (hci_command_hdr *)buf;
cp = (hci_inquiry_cp *)(buf + HCI_COMMAND_HDR_SIZE);
uint16_t opcode = cmd_opcode_pack(OGF_LINK_CTL, OCF_INQUIRY); // OGF=0x01, OCF=0x0001 => opcode=0x0401
cmd_hdr->opcode = htobs(opcode); // Host To Bluetooth Short
cmd_hdr->plen = sizeof(hci_inquiry_cp); // Parameter length
// --- 这里设置你想要的 LAP ---
cp->lap[0] = 0x00; // 注意字节序,对应蓝牙地址的最低位字节
cp->lap[1] = 0x8b;
cp->lap[2] = 0x9e; // 最高位字节,即 Wireshark 里看到的第一个字节
// ---------------------------
cp->length = 3; // 查询持续时间 N * 1.28s
cp->num_rsp = 0; // 0 表示不限制响应数量,直到超时
printf("通过 Raw Socket 发送 HCI_Inquiry 命令,使用 LAP: 0x%02x%02x%02x\n", cp->lap[2], cp->lap[1], cp->lap[0]);
// 6. 发送命令
if (write(dd, buf, HCI_COMMAND_HDR_SIZE + sizeof(hci_inquiry_cp)) < 0) {
perror("写入 HCI_Inquiry 命令失败");
// 可以尝试恢复设备状态,比如发送 HCI_Reset
hci_send_cmd(dd, OGF_HOST_CTL, OCF_RESET, 0, NULL);
close(dd);
exit(1);
}
printf("HCI_Inquiry 命令已发送。现在需要循环读取并处理 HCI 事件...\n");
// 7. 接收和处理 HCI 事件 (简化示例,实际需要循环和完整解析)
// 实际应用中,你需要一个循环来读取 socket (read(dd, ...))
// 并解析返回的 HCI Event Packets,特别是:
// - EVT_CMD_STATUS or EVT_CMD_COMPLETE for HCI_Inquiry to see if it was accepted.
// - EVT_INQUIRY_RESULT / EVT_INQUIRY_RESULT_WITH_RSSI / EVT_EXTENDED_INQUIRY_RESULT for found devices.
// - EVT_INQUIRY_COMPLETE when the inquiry process finishes.
// 这里仅作演示,不包含完整的事件处理逻辑。
sleep(cp->length * 2 + 1); // 等待查询大致完成 (非常粗略的等待)
printf("Raw socket 示例结束。\n");
// 8. 清理
// 在实际应用结束前,可能需要发送 HCI_Inquiry_Cancel 命令 (如果查询仍在进行)
// hci_send_cmd(dd, OGF_LINK_CTL, OCF_INQUIRY_CANCEL, 0, NULL);
close(dd);
return 0;
}
安全建议
- 权限: 使用 Raw HCI Socket 通常需要特权。你的程序要么以 root 用户运行,要么需要被授予
CAP_NET_ADMIN
能力 (capability)。推荐后者:
这样普通用户也能运行它,但仅限于执行需要# 编译你的程序 gcc your_program.c -o your_program -lbluetooth # 设置 capability sudo setcap cap_net_admin+eip ./your_program
CAP_NET_ADMIN
的网络相关操作。 - 资源管理: 确保在程序退出前正确关闭 socket (
close(dd)
)。如果你的查询被中断,可能需要显式发送HCI_Inquiry_Cancel
命令来停止蓝牙控制器的查询动作,避免其一直处于查询状态。
进阶使用技巧
- 非阻塞 I/O 与事件循环: 上面的例子是阻塞发送且等待时间固定。实际应用中,Raw Socket 应该设为非阻塞模式(已在示例中通过
SOCK_NONBLOCK
设置),并使用select()
,poll()
, 或epoll()
来监听 socket 上的可读事件。这样可以异步处理 HCI 事件,程序不会被read()
调用卡住,能同时处理其他任务或用户交互。 - 完整的事件解析: HCI 事件种类繁多,你需要仔细解析每个收到的事件包,判断其类型(如
EVT_INQUIRY_RESULT
)并提取所需信息(如设备地址 BD_ADDR, 时钟偏移, RSSI 等)。bluez-libs
提供的头文件 (bluetooth/hci.h
) 定义了各种事件结构体,可以帮助解析。 - 错误处理: 发送 HCI 命令后,控制器会返回
EVT_CMD_STATUS
或EVT_CMD_COMPLETE
事件,指示命令是否成功接收和开始执行。检查这些事件的状态码很重要。
方案二:深入源码探究竟(硬核玩家专属)
如果你对 BlueZ 和 Linux 内核蓝牙协议栈的工作方式有浓厚兴趣,并且不畏惧阅读 C 源码,可以尝试:
- 研究
bluez-libs
源码: 找到hci_inquiry
函数(位于lib/hci.c
或类似文件中)的实现,看它内部是如何构建和发送 HCI 命令的。是否有可能通过设置某些不常用的flags
组合,或者在调用hci_inquiry
前后执行特定操作,来影响 LAP 的使用? - 探索内核蓝牙子系统: 如果
hci_lib
确实是忠实传递了 LAP 参数,那么问题可能出在内核层(net/bluetooth/hci_sock.c
,net/bluetooth/hci_request.c
等)。分析内核处理来自 HCI Socket 的 Inquiry 请求时的逻辑。
这条路非常耗时,并且需要对蓝牙协议和内核编程有相当的理解。但如果成功,你可能会发现未文档化的特性或确认这是一个确实的设计限制。
方案三:换个思路或工具?
- 检查
bluetoothctl
或 Management API: BlueZ 还提供了命令行工具bluetoothctl
和一个更现代的管理接口 Management API (Mgmt API)。可以研究一下这些工具或接口是否提供了更细粒度的设备发现控制,包括指定 LAP。不过,它们底层可能最终还是依赖类似的 HCI 命令路径,不一定能绕开这个问题。 - 接受现状,调整策略: 如果你的应用场景并非绝对、严格地需要使用特定的 LIAC 进行发现,那么接受
hci_inquiry
使用 GIAC 的行为可能是最简单的做法。如果必须使用特定 LAP,那么 Raw HCI Socket 就是目前看来最可行的“官方框架内”解决方案。
总而言之,hci_inquiry
函数在处理 lap
参数时表现出的“不听话”,更像是一个高层 API 为简化通用场景而做出的设计选择(或限制),而非简单的 bug。想要完全掌控 Inquiry 命令的细节,尤其是使用非标准的 LAP 时,直接操作 Raw HCI Socket 是最可靠的技术路径。