trouble-host Rust BLE 键盘如何发送并释放按键
结论
custom_task 中按下和释放按键的报告格式是正确的:
0x04是 HID Usage ID,对应键盘上的a。- 全零的 8 字节报告表示所有按键均已释放。
- 按下后等待 100 ms,再发送释放报告也没有问题。
问题在 REPORT_MAP。末尾三项都写成了 Usage Page,实际应依次为 Usage Minimum、Usage Maximum 和 Input。由于当前描述符没有正确声明 6 个普通按键槽,Linux 可能会错误解析报告,导致按键未释放或 HID 设备识别异常。
notify 需要主机通过 CCCD 订阅,外设不能替主机开启通知。使用 bluetoothctl 测试通用 GATT 时,手动启用 notify 很正常。Linux 将设备识别为标准 BLE HID 键盘后,HID 驱动通常会自动订阅。
修正 Report Map
原代码末尾是:
0x05, 0x07, //Usage Page (Key Codes)
0x05, 0x01, // Usage Minimum (0)
0x05, 0x01, // Usage Maximum (101)
0x05, 0x01, // Input (Data, Array) - 6 key slots
这几项都使用了 0x05,而 0x05 表示 Usage Page。应改为:
const REPORT_MAP: &[u8] = &[
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0xE0, // Usage Minimum (Keyboard LeftControl)
0x29, 0xE7, // Usage Maximum (Keyboard Right GUI)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute): modifiers
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Constant): reserved byte
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101)
0x81, 0x00, // Input (Data, Array, Absolute): 6 key slots
0xC0, // End Collection
];
需要改动的是这三项:
0x19, 0x00, // Usage Minimum
0x29, 0x65, // Usage Maximum
0x81, 0x00, // Input
修正后,8 字节输入报告的布局才会与发送的数据一致:
byte 0: modifier bits
byte 1: reserved
byte 2..7: 最多 6 个按键的 Usage ID
按下 a 时发送:
[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]
释放所有按键时发送:
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
检查 notify 的返回值
当前代码使用 let _ = ... 丢弃了通知错误。主机尚未订阅、连接已经断开或发送失败时,按下和释放报告都可能没有送达。
至少要把错误记录下来:
async fn custom_task<C: Controller, P: PacketPool>(
server: &Server<'_>,
conn: &GattConnection<'_, '_, P>,
_stack: &Stack<'_, C, P>,
) {
let input_report = server.hid_service.input_report;
let press_report: [u8; 8] =
[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00];
let release_report: [u8; 8] =
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
loop {
if let Err(error) = input_report.notify(conn, &press_report, false).await {
println!("failed to send key press: {:?}", error);
Timer::after_millis(100).await;
continue;
}
Timer::after_millis(100).await;
if let Err(error) = input_report.notify(conn, &release_report, false).await {
println!("failed to send key release: {:?}", error);
}
Timer::after_secs(2).await;
}
}
具体错误类型和判断方式可能随 trouble-host 版本变化,不要依赖未经确认的错误枚举名称。调试时可以先打印实际错误。
更完整的固件应等主机订阅输入报告后再发送键盘事件。连接建立后立即发送通知,第一批通知很可能失败。
notify 为什么需要主机开启
带有 notify 属性的特征值通常有一个 Client Characteristic Configuration Descriptor,即 CCCD,其 UUID 为 0x2902。主机需要向 CCCD 写入:
0x0001
写入后才能订阅 Notification。
代码中的描述符:
#[descriptor(uuid = "2908", read, value = [0x00, 0x01])]
是 Report Reference Descriptor,不是 CCCD,含义如下:
Report ID = 0
Report Type = 1,Input Report
input_report 已声明 notify:
#[characteristic(uuid = "2A4D", read, notify)]
trouble-host 应根据对应版本的宏实现,创建通知所需的 CCCD。可以通过 bluetoothctl、btgatt-client 或抓包检查 GATT 属性表,确认它是否已经生成。
订阅由客户端控制。外设不能直接将 CCCD 设置为已启用,也不应在固件中模拟主机写入。标准 HID 主机完成服务发现并接管设备后,一般会自动订阅 Input Report。bluetoothctl 的交互式 GATT 操作只是通用调试接口,测试时手动执行 notify on 很正常。
让 Linux 自动按 HID 键盘处理
如果每次都要手动进入 Report Characteristic 执行 notify on,需要检查 Linux 是否已将设备绑定到 HID over GATT 驱动。只建立普通 BLE 连接还不够。
可以在 bluetoothctl 中完成配对、信任和连接:
power on
agent on
default-agent
scan on
pair <device-address>
trust <device-address>
connect <device-address>
还要检查这些配置:
- 广播中包含 HID Service UUID
0x1812。 - GAP Appearance 为 Keyboard。
- 主机能够读取 HID Service、Report Map、Input Report 和 Report Reference Descriptor。
- 主机已经完成配对,而非仅通过 GATT 调试菜单临时连接。
- 固件保存 bond 信息,并在重连时恢复。
- 设备身份地址保持稳定,避免 Linux 在每次启动时都将其识别为新设备。
代码中的:
AdStructure::CompleteServiceUuids16(&[[0x12, 0x18]])
按照该 API 使用的字节顺序表示 0x1812。如果当前 trouble-host 版本对 UUID 类型或字节序采用了其他封装,应以该版本的接口定义为准。
修改 GATT 属性或 Report Map 后,Linux 可能仍在使用以前缓存的服务信息。调试时可以先删除旧设备,再重新扫描和配对:
remove <device-address>
scan on
pair <device-address>
trust <device-address>
connect <device-address>
注意循环发送造成的错觉
当前任务大约每隔 2 秒发送一次 a:
press
等待 100 ms
release
等待 2 s
重复
即使释放成功,终端中仍会不断出现新的 a。要判断是否真的发生了“卡键”,可以查看输入事件或抓取通知数据,确认每个:
00 00 04 00 00 00 00 00
后面都紧跟:
00 00 00 00 00 00 00 00
如果抓包中能看到这两个通知,而 Linux 仍认为按键没有释放,应继续检查 Report Map、旧的 GATT 缓存和 HID 驱动绑定情况。根据给出的代码,应先修正 Report Map,同时保留并检查 notify() 的返回值。
备注:内容仅供参考。
