返回

蓝牙hci_inquiry指定LAP无效?原因分析与Raw Socket解决方法

Linux

蓝牙开发踩坑:为何 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

Wireshark capture showing HCI_Inquiry command with LAP 0x9e8b33

这就怪了。明明传了参数,怎么好像没起作用? 有些开发者甚至尝试过用 libusb 直接控制 USB 蓝牙适配器发送 Inquiry 命令,发现是可以用自定义 LAP 的,这说明硬件本身没问题。那问题到底出在哪?怎样才能让程序按照我们指定的 LAP 去查询呢?

问题根源分析:hci_inquiry 的“自作主张”

这事儿吧,原因可能不在于你的代码写错了,也不是 hci_lib 里有个明显的 bug。更可能的情况是,hci_inquiry 这个函数作为 BlueZ 提供的一个相对高层的封装,它在设计上可能就简化了 Inquiry 的流程,或者说,它主要面向的是最常见的“通用查询”场景。

  1. 高层封装的默认行为: hci_inquiry 位于 bluez-libs 中,它封装了底层的 HCI 命令细节。为了简化通用设备发现流程,它很可能在内部实现中,对于标准的 Inquiry 请求,默认或强制使用了 GIAC (0x9e8b33)。蓝牙协议里,GIAC 是最常用、最通用的 LAP,用于发现所有处于“可发现”模式的设备。
  2. LIAC 的特殊性: 使用特定的 LAP (构成 LIAC - Limited Inquiry Access Code) 的查询,通常不是为了发现所有设备,而是用在特定的、预定义的场景下,比如快速连接已知类型的设备或在特定设置流程中。hci_lib 的设计者可能认为这种需求不那么普遍,或者处理起来更复杂,因此没有在 hci_inquiry 这个便捷函数里完全、直接地支持自定义 LAP 的所有情况,即使参数列表里包含了 lap
  3. 内核交互的可能影响: 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_STATUSEVT_CMD_COMPLETE 事件,指示命令是否成功接收和开始执行。检查这些事件的状态码很重要。

方案二:深入源码探究竟(硬核玩家专属)

如果你对 BlueZ 和 Linux 内核蓝牙协议栈的工作方式有浓厚兴趣,并且不畏惧阅读 C 源码,可以尝试:

  1. 研究 bluez-libs 源码: 找到 hci_inquiry 函数(位于 lib/hci.c 或类似文件中)的实现,看它内部是如何构建和发送 HCI 命令的。是否有可能通过设置某些不常用的 flags 组合,或者在调用 hci_inquiry 前后执行特定操作,来影响 LAP 的使用?
  2. 探索内核蓝牙子系统: 如果 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 是最可靠的技术路径。