⌨️ 蓝牙键盘
基于 HID over GATT Profile 的无线键盘,支持多设备切换和按键无冲。
📐 键盘系统架构
flowchart TB
subgraph Keyboard["蓝牙键盘"]
MATRIX[按键矩阵] --> SCAN[扫描电路]
SCAN --> MCU[BLE MCU]
MCU --> ANT[天线]
MCU --> LED[状态 LED]
end
subgraph Host["主机设备"]
ANT2[天线] --> HOST[Host OS]
HOST --> HID[HID 驱动]
HID --> OS[操作系统]
end
ANT -.->|BLE 连接 | ANT2
style Keyboard fill:#e3f2fd,stroke:#007bff
style Host fill:#d4edda,stroke:#28a745
📋 HID 键盘报告格式
// HID 键盘输入报告 (标准 8 键无冲)
// Report ID: 0x01
// 总长度:8 bytes
typedef struct {
uint8_t report_id; // Report ID (0x01)
uint8_t modifier; // 修饰键 (Ctrl/Shift/Alt 等)
uint8_t reserved; // 保留
uint8_t keycode[6]; // 按键码 (最多 6 键同时按下)
} keyboard_input_report_t;
// 修饰键位定义
#define KB_MOD_LCTRL 0x01
#define KB_MOD_LSHIFT 0x02
#define KB_MOD_LALT 0x04
#define KB_MOD_LGUI 0x08 // Windows/Command
#define KB_MOD_RCTRL 0x10
#define KB_MOD_RSHIFT 0x20
#define KB_MOD_RALT 0x40
#define KB_MOD_RGUI 0x80
// 常用按键码 (Usage ID)
#define KB_KEY_A 0x04
#define KB_KEY_B 0x05
#define KB_KEY_C 0x06
#define KB_KEY_ENTER 0x28
#define KB_KEY_ESCAPE 0x29
#define KB_KEY_BACKSPACE 0x2A
#define KB_KEY_TAB 0x2B
#define KB_KEY_SPACE 0x2C
#define KB_KEY_F1 0x3A
#define KB_KEY_F12 0x45
// 示例:按下 Ctrl+A (全选)
uint8_t keyboard_report[] = {
0x01, // Report ID
KB_MOD_LCTRL, // Modifier: Left Ctrl
0x00, // Reserved
KB_KEY_A, 0x00, 0x00, // Keycode: A
0x00, 0x00
};
// 示例:按下 Shift+1 (感叹号)
uint8_t keyboard_report_shift1[] = {
0x01,
KB_MOD_LSHIFT, // Modifier: Left Shift
0x00,
0x1E, 0x00, 0x00, // Keycode: 1
0x00, 0x00
};
📊 HID 报告描述符
// 键盘 HID 报告描述符
const uint8_t keyboard_report_descriptor[] = {
0x05, 0x01, // Usage Page (Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
// 修饰键 (8 个)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0xE0, // Usage Minimum (224) - Left Ctrl
0x29, 0xE7, // Usage Maximum (231) - 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, Var, Abs)
// 保留字节
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Cnst)
// LED 输出 (5 个:NumLock/CapsLock/ScrollLock/Compose/Kana)
0x05, 0x08, // Usage Page (LED)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x91, 0x02, // Output (Data, Var, Abs)
// LED 填充
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x91, 0x01, // Output (Cnst)
// 按键 (6 键无冲)
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, Arr, Abs)
0xC0 // End Collection
};
// LED 定义
#define LED_NUM_LOCK 0x01
#define LED_CAPS_LOCK 0x02
#define LED_SCROLL_LOCK 0x04
#define LED_COMPOSE 0x08
#define LED_KANA 0x10
📈 完整工作流程
sequenceDiagram
participant KB as 蓝牙键盘
participant Host as 主机
Note over KB,Host: 1. 配对模式
KB->>KB: 长按配对键
KB->>Host: ADV_IND (键盘)
Note over KB,Host: 2. 连接配对
Host->>KB: CONNECT_IND
Host->>KB: 配对请求
KB-->>Host: 确认配对
Note over KB,Host: 3. 服务发现
Host->>KB: 发现 HID 服务
Host->>KB: 读取报告描述符
Note over KB,Host: 4. 启用通知
Host->>KB: 写入 CCCD
Note over KB,Host: 5. 按键输入
loop 按键事件
KB->>Host: 按下通知
KB->>Host: 释放通知 (全 0)
end
Note over KB,Host: 6. LED 控制
Host->>KB: 输出报告 (CapsLock)
KB->>KB: 点亮 LED
Note over KB,Host: 7. 低功耗
KB->>KB: 无操作 10s
KB->>Host: 进入休眠
💻 固件代码示例
#include "ble_hids.h"
// HID 服务句柄
BLE_HIDS_DEF(m_hids, 1);
// 键盘报告缓冲区
static uint8_t m_kb_report[8];
static uint8_t m_last_keys[6] = {0};
// 初始化键盘 HID 服务
void keyboard_hids_init(void) {
ble_hids_init_t hids_init = {0};
// 配置报告映射
hids_init.rep_map.p_rep_map = (uint8_t *)keyboard_report_descriptor;
hids_init.rep_map.rep_map_len = sizeof(keyboard_report_descriptor);
// 配置输入报告
ble_hids_inp_rep_t *p_inp_rep = &hids_init.inp_rep[0];
p_inp_rep->max_len = 8;
p_inp_rep->rep_ref.report_id = 1;
p_inp_rep->rep_ref.report_type = BLE_HIDS_INPUT_REP;
p_inp_rep->evt_handler = kb_input_evt_handler;
// 配置输出报告 (LED 控制)
ble_hids_outp_rep_t *p_outp_rep = &hids_init.outp_rep[0];
p_outp_rep->max_len = 1;
p_outp_rep->rep_ref.report_id = 1;
p_outp_rep->rep_ref.report_type = BLE_HIDS_OUTPUT_REP;
p_outp_rep->evt_handler = kb_output_evt_handler;
p_outp_rep->security_mode.ccm.cps = 1;
hids_init.inp_rep_count = 1;
hids_init.outp_rep_count = 1;
// HID 信息
hids_init.hid_information.bcd_hid = 0x0111;
hids_init.hid_information.b_country_code = 0;
hids_init.hid_information.flags = 0x01; // 远程唤醒
ble_hids_init(&m_hids, &hids_init);
}
// 发送按键报告
void keyboard_send_keys(uint8_t modifier, uint8_t *keys, uint8_t count) {
m_kb_report[0] = 0x01; // Report ID
m_kb_report[1] = modifier; // Modifier
m_kb_report[2] = 0x00; // Reserved
// 填充按键 (最多 6 个)
for (int i = 0; i < 6; i++) {
m_kb_report[3 + i] = (i < count) ? keys[i] : 0x00;
}
// 发送输入报告
ble_hids_inp_rep_send(&m_hids, 0, 8, m_kb_report, m_conn_handle);
}
// 按键按下
void key_press(uint8_t keycode) {
uint8_t keys[6] = {0};
uint8_t count = 0;
// 查找空位
for (int i = 0; i < 6; i++) {
if (m_last_keys[i] == keycode) {
return; // 已按下,忽略
}
if (m_last_keys[i] == 0x00) {
m_last_keys[i] = keycode;
count = i + 1;
break;
}
}
// 构建报告
memcpy(keys, m_last_keys, 6);
keyboard_send_keys(0, keys, count);
}
// 按键释放
void key_release(uint8_t keycode) {
// 从数组中移除
for (int i = 0; i < 6; i++) {
if (m_last_keys[i] == keycode) {
m_last_keys[i] = 0x00;
}
}
// 发送空报告或剩余按键
keyboard_send_keys(0, m_last_keys, 6);
}
// LED 输出报告处理
void kb_output_evt_handler(ble_hids_t * p_hids,
ble_hids_evt_t * p_evt) {
if (p_evt->evt_type == BLE_HIDS_EVT_OUTP_REP_WRITE) {
uint8_t led_state = p_evt->params.outp_rep.data[0];
// 控制 LED
if (led_state & LED_CAPS_LOCK) {
led_on(CAPS_LOCK_LED);
} else {
led_off(CAPS_LOCK_LED);
}
if (led_state & LED_NUM_LOCK) {
led_on(NUM_LOCK_LED);
} else {
led_off(NUM_LOCK_LED);
}
}
}
⌨️ 多设备切换
flowchart TB
subgraph Profiles["配对配置文件"]
P1[设备 1
PC] P2[设备 2
手机] P3[设备 3
平板] end SWITCH[切换按键] --> SELECT{选择设备} SELECT -->|Fn+1| P1 SELECT -->|Fn+2| P2 SELECT -->|Fn+3| P3 P1 --> CONNECT1[连接设备 1] P2 --> CONNECT2[连接设备 2] P3 --> CONNECT3[连接设备 3] style Profiles fill:#e3f2fd,stroke:#007bff style SWITCH fill:#ffc107,color:#000
PC] P2[设备 2
手机] P3[设备 3
平板] end SWITCH[切换按键] --> SELECT{选择设备} SELECT -->|Fn+1| P1 SELECT -->|Fn+2| P2 SELECT -->|Fn+3| P3 P1 --> CONNECT1[连接设备 1] P2 --> CONNECT2[连接设备 2] P3 --> CONNECT3[连接设备 3] style Profiles fill:#e3f2fd,stroke:#007bff style SWITCH fill:#ffc107,color:#000
// 多设备切换实现
typedef struct {
ble_gap_addr_t addr; // 设备地址
ble_gap_irk_t irk; // 身份解析密钥
bool bonded; // 已配对
} device_profile_t;
// 存储 3 个设备配置
static device_profile_t m_profiles[3];
static uint8_t m_current_device = 0;
// 切换设备
void switch_device(uint8_t index) {
if (index >= 3) return;
// 断开当前连接
sd_ble_gap_disconnect(m_conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
// 更新当前设备
m_current_device = index;
// 使用对应设备的密钥广播
if (m_profiles[index].bonded) {
// 已配对,直接连接
sd_ble_gap_connect(&m_profiles[index].addr,
&m_scan_params,
&m_conn_params,
NULL);
} else {
// 未配对,进入广播模式
start_advertising();
}
// 更新 LED 指示
update_led_indicator(index);
}
⚡ 功耗优化
| 参数 | 配置值 | 说明 |
|---|---|---|
| 连接间隔 | 20ms - 50ms | 键盘不需要超低延迟 |
| 从机延迟 | 4-9 | 允许跳过事件 |
| 监督超时 | 2s | 平衡功耗与响应 |
| 广播间隔 | 30-60ms | 快速重连 |
| 休眠电流 | <5μA | 深度睡眠 |
🔍 调试要点
常见问题
- 按键重复:去抖时间不足
- 多键无冲:检查报告格式
- LED 不亮:输出报告未启用
- 切换失败:清除配对信息
推荐工具
- Device Manager: Windows 查看
- Bluetooth Explorer: macOS 调试
- nRF Sniffer: 空口抓包
- Keyboard Tester: 在线测试
🔧 键盘数据包构建详解
从按键按下到蓝牙空中传输,每一层如何添加头部,组合成完整的数据包。
数据包组合流程
flowchart TB
subgraph Step1["步骤 1: 按键输入"]
A1["按键码 + 修饰键
8 bytes"] end subgraph Step2["步骤 2: HID 报告"] A2["Report ID + 数据
8 bytes"] end subgraph Step3["步骤 3: GATT 层"] A3["特征值句柄 + 报告"] end subgraph Step4["步骤 4: ATT 层"] A4["Opcode + Handle + Data"] end subgraph Step5["步骤 5: L2CAP 层"] A5["L2CAP Header
Length + CID"] end subgraph Step6["步骤 6: HCI 层"] A6["HCI ACL Header
Handle + Length"] end subgraph Step7["步骤 7: LL+PHY"] A7["LL Header + MIC + CRC
前导码 + 访问地址"] end A1 --> A2 A2 --> A3 A3 --> A4 A4 --> A5 A5 --> A6 A6 --> A7 style Step1 fill:#d4edda,stroke:#28a745 style Step2 fill:#e3f2fd,stroke:#007bff style Step3 fill:#e3f2fd,stroke:#007bff style Step4 fill:#e3f2fd,stroke:#007bff style Step5 fill:#fff3cd,stroke:#ffc107 style Step6 fill:#d4edda,stroke:#28a745 style Step7 fill:#d4edda,stroke:#28a745
8 bytes"] end subgraph Step2["步骤 2: HID 报告"] A2["Report ID + 数据
8 bytes"] end subgraph Step3["步骤 3: GATT 层"] A3["特征值句柄 + 报告"] end subgraph Step4["步骤 4: ATT 层"] A4["Opcode + Handle + Data"] end subgraph Step5["步骤 5: L2CAP 层"] A5["L2CAP Header
Length + CID"] end subgraph Step6["步骤 6: HCI 层"] A6["HCI ACL Header
Handle + Length"] end subgraph Step7["步骤 7: LL+PHY"] A7["LL Header + MIC + CRC
前导码 + 访问地址"] end A1 --> A2 A2 --> A3 A3 --> A4 A4 --> A5 A5 --> A6 A6 --> A7 style Step1 fill:#d4edda,stroke:#28a745 style Step2 fill:#e3f2fd,stroke:#007bff style Step3 fill:#e3f2fd,stroke:#007bff style Step4 fill:#e3f2fd,stroke:#007bff style Step5 fill:#fff3cd,stroke:#ffc107 style Step6 fill:#d4edda,stroke:#28a745 style Step7 fill:#d4edda,stroke:#28a745
逐步构建过程
// ========== 步骤 1: 按键输入 ==========
// 键盘矩阵扫描检测按键
uint8_t modifier = 0x02; // Left Shift
uint8_t keycode = 0x1E; // 数字键 1
// 组合后为 Shift+1 = !
// ========== 步骤 2: HID 报告封装 ==========
// 构建键盘输入报告 (8 bytes)
uint8_t keyboard_report[8] = {
0x01, // [0] Report ID: 1
0x02, // [1] Modifier: Left Shift
0x00, // [2] Reserved: 0
0x1E, // [3] Keycode 1: 1
0x00, // [4] Keycode 2: 0 (无)
0x00, // [5] Keycode 3: 0 (无)
0x00, // [6] Keycode 4: 0 (无)
0x00 // [7] Keycode 5: 0 (无)
};
// HID 报告总长度:8 bytes
// 特征值句柄 (假设)
uint16_t report_handle = 0x002B; // 输入报告特征值句柄
// ========== 步骤 3: GATT 层封装 ==========
// GATT 不添加额外头部,直接使用 ATT
// 但需要知道特征值句柄和 Report ID
// ========== 步骤 4: ATT 层封装 ==========
// 添加 ATT Header (Opcode + Handle)
uint8_t att_packet[] = {
0x1B, // [0] Opcode: Handle Value Notification
0x2B, 0x00, // [1-2] Handle: 0x002B (小端序)
// GATT 数据 (HID 报告)
0x01, 0x02, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00
};
// ATT 层总长度:11 bytes
// ========== 步骤 5: L2CAP 层封装 ==========
// 添加 L2CAP Header (Length + CID)
uint8_t l2cap_packet[] = {
0x0B, 0x00, // [0-1] Length: 11 (ATT 包长度)
0x04, 0x00, // [2-3] CID: 0x0004 (ATT 信道)
// ATT Packet (从步骤 4)
0x1B, 0x2B, 0x00, 0x01, 0x02, 0x00, 0x1E, 0x00,
0x00, 0x00, 0x00
};
// L2CAP 层总长度:15 bytes
// ========== 步骤 6: HCI ACL 层封装 ==========
// 添加 HCI ACL Header (Handle + Data Length)
uint8_t hci_packet[] = {
0x01, 0x00, // [0-1] Handle: 0x0001 (连接句柄)
0x0F, 0x00, // [2-3] Data Length: 15 (L2CAP 包长度)
// L2CAP Packet (从步骤 5)
0x0B, 0x00, 0x04, 0x00, 0x1B, 0x2B, 0x00, 0x01,
0x02, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00
};
// HCI 层总长度:19 bytes
// ========== 步骤 7: Link Layer 封装 ==========
// LL 层添加:LL Header + MIC(加密) + CRC
// LL Data PDU 结构:
// +----------+------------+---------+-----+
// | LL Header| LL Payload | MIC(4B) | CRC |
// | (2B) | (0-27B) | |(3B) |
// +----------+------------+---------+-----+
// LL Header (2 bytes)
uint8_t ll_header[] = {
0x05, // [0] LLID=01 (数据), NESN=0, SN=0, MD=0
0x0F // [1] Length: 15 bytes (HCI 数据长度)
};
// LL Payload = HCI Packet (19 bytes)
// 19 < 27-2-4-3 = 18, 需要分段发送!
// 分段 1 (LL PDU #1) - 前 23 bytes
uint8_t ll_pdu1[] = {
0x05, 0x17, // LL Header (Length=23)
// HCI 数据前 19 bytes
0x01, 0x00, 0x0F, 0x00, 0x0B, 0x00, 0x04, 0x00,
0x1B, 0x2B, 0x00, 0x01, 0x02, 0x00, 0x1E, 0x00,
0x00, 0x00, 0x00,
// MIC (4 bytes, 如果启用加密)
0xA1, 0xB2, 0xC3, 0xD4,
// CRC (3 bytes)
0x12, 0x34, 0x56
};
// ========== 步骤 8: PHY 层封装 ==========
// PHY 添加:前导码 + 访问地址
// PHY Packet 结构:
// +--------+----------+------------+-----+
// | Preamble| Access | Payload | CRC |
// | (1B) | Address | (LL PDU) | |
// | | (4B) | | |
// +--------+----------+------------+-----+
uint8_t preamble = 0x55; // 前导码 (01010101)
uint32_t access_address = 0x8E89BED6; // 访问地址 (BLE 固定)
// Payload = LL PDU (从步骤 7)
// CRC 已在 LL 层计算
完整数据包结构图
flowchart LR
subgraph PHY_Packet["PHY Packet (空中传输)"]
P1["Preamble
1B"] P2["Access Addr
4B"] P3["LL PDU
可变"] P4["CRC
3B"] end subgraph LL_PDU["LL PDU"] L1["LL Header
2B"] L2["LL Payload
0-27B"] L3["MIC
4B"] end subgraph LL_Payload["LL Payload = HCI Packet"] H1["HCI ACL Header
4B"] H2["L2CAP Packet
可变"] end subgraph L2CAP["L2CAP Packet"] C1["L2CAP Header
4B"] C2["ATT PDU
可变"] end subgraph ATT["ATT PDU"] A1["ATT Header
3B"] A2["GATT Data
可变"] end subgraph GATT["GATT Data = HID Report"] G1["Report ID
1B"] G2["Modifier
1B"] G3["Reserved
1B"] G4["Keycode[0]
1B"] G5["Keycode[1-5]
5B"] end P3 --> LL_PDU L2 --> LL_Payload H2 --> L2CAP C2 --> ATT A2 --> GATT style PHY_Packet fill:#d4edda,stroke:#28a745 style LL_PDU fill:#e3f2fd,stroke:#007bff style LL_Payload fill:#fff3cd,stroke:#ffc107 style L2CAP fill:#e3f2fd,stroke:#007bff style ATT fill:#e3f2fd,stroke:#007bff style GATT fill:#d4edda,stroke:#28a745
1B"] P2["Access Addr
4B"] P3["LL PDU
可变"] P4["CRC
3B"] end subgraph LL_PDU["LL PDU"] L1["LL Header
2B"] L2["LL Payload
0-27B"] L3["MIC
4B"] end subgraph LL_Payload["LL Payload = HCI Packet"] H1["HCI ACL Header
4B"] H2["L2CAP Packet
可变"] end subgraph L2CAP["L2CAP Packet"] C1["L2CAP Header
4B"] C2["ATT PDU
可变"] end subgraph ATT["ATT PDU"] A1["ATT Header
3B"] A2["GATT Data
可变"] end subgraph GATT["GATT Data = HID Report"] G1["Report ID
1B"] G2["Modifier
1B"] G3["Reserved
1B"] G4["Keycode[0]
1B"] G5["Keycode[1-5]
5B"] end P3 --> LL_PDU L2 --> LL_Payload H2 --> L2CAP C2 --> ATT A2 --> GATT style PHY_Packet fill:#d4edda,stroke:#28a745 style LL_PDU fill:#e3f2fd,stroke:#007bff style LL_Payload fill:#fff3cd,stroke:#ffc107 style L2CAP fill:#e3f2fd,stroke:#007bff style ATT fill:#e3f2fd,stroke:#007bff style GATT fill:#d4edda,stroke:#28a745
数据包大小计算
| 层 | 头部大小 | 数据大小 | 本层总大小 | 累计大小 |
|---|---|---|---|---|
| HID Report | 0 B | 8 B (键盘报告) | 8 B | 8 B |
| ATT | 3 B (Opcode+Handle) | 8 B (HID) | 11 B | 11 B |
| L2CAP | 4 B (Length+CID) | 11 B (ATT) | 15 B | 15 B |
| HCI ACL | 4 B (Handle+Length) | 15 B (L2CAP) | 19 B | 19 B |
| LL | 2 B (Header) | 19 B (HCI) | 25 B (+MIC 4B +CRC 3B) | 32 B |
| PHY | 5 B (Preamble+Access) | 32 B (LL) | 37 B (+CRC 3B) | 40 B |
注意: 键盘报告 8 bytes,加上协议头部后约 40 bytes,通常可以一次 LL PDU 发送。
实际字节流 (十六进制)
# 最终在空中传输的完整字节流 (从内到外):
# 应用数据 (键盘报告 8 bytes):
01 02 00 1E 00 00 00 00
# + ATT Header (3 bytes):
1B 2B 00 01 02 00 1E 00 00 00 00
# + L2CAP Header (4 bytes):
0B 00 04 00 1B 2B 00 01 02 00 1E 00 00 00 00
# + HCI ACL Header (4 bytes):
0F 00 0B 00 04 00 1B 2B 00 01 02 00 1E 00 00 00 00
# + LL Header + MIC + CRC:
05 13 0F 00 0B 00 04 00 1B 2B 00 01 02 00 1E 00 00 00 00 A1 B2 C3 D4 12 34 56
# + PHY Preamble + Access Address:
55 8E 89 BE D6 05 13 0F 00 0B 00 04 00 1B 2B 00 01 02 00 1E 00 00 00 00 A1 B2 C3 D4 12 34 56
# 最终空中传输:约 40 bytes (单个包)
常见按键数据包
| 按键 | Modifier | Keycode[0] | 完整报告 |
|---|---|---|---|
| A | 0x00 | 0x04 | 01 00 00 04 00 00 00 00 |
| Ctrl+A | 0x01 | 0x04 | 01 01 00 04 00 00 00 00 |
| Enter | 0x00 | 0x28 | 01 00 00 28 00 00 00 00 |
| Escape | 0x00 | 0x29 | 01 00 00 29 00 00 00 00 |
| Space | 0x00 | 0x2C | 01 00 00 2C 00 00 00 00 |
| F1 | 0x00 | 0x3A | 01 00 00 3A 00 00 00 00 |
| CapsLock | 0x00 | 0x39 | 01 00 00 39 00 00 00 00 |
| Shift+1 (!) | 0x02 | 0x1E | 01 02 00 1E 00 00 00 00 |
| Ctrl+Alt+Del | 0x05 | 0x4C | 01 05 00 4C 00 00 00 00 |
| 多键 (ABC) | 0x00 | 0x04,0x05,0x06 | 01 00 00 04 05 06 00 00 |
| 按键释放 | 0x00 | 0x00 | 01 00 00 00 00 00 00 00 |
按键按下与释放序列
# 按下并释放按键 "A" 的完整过程:
# 1. 初始状态 (无按键)
01 00 00 00 00 00 00 00
# 2. 按下 A (发送按键码)
01 00 00 04 00 00 00 00
# 3. 释放 A (发送空报告)
01 00 00 00 00 00 00 00
# 每个报告都会通过 BLE Notify 发送到主机
# 典型延迟:7.5ms - 20ms (取决于连接间隔)