【低功耗蓝牙】⑤ 蓝牙HID协议实战:从键盘到游戏手柄的ESP32实现

张开发
2026/5/11 6:56:41 15 分钟阅读
【低功耗蓝牙】⑤ 蓝牙HID协议实战:从键盘到游戏手柄的ESP32实现
1. 蓝牙HID协议入门指南第一次接触蓝牙HID协议时我完全被那些专业术语搞晕了。HID全称Human Interface Device翻译过来就是人机交互设备。简单来说就是键盘、鼠标、游戏手柄这些我们天天用的输入设备。以前这些设备都是通过USB线连接现在用蓝牙无线连接方便多了。蓝牙HID设备有个专业术语叫HOGPHID over GATT Profile。这个协议规定了蓝牙设备如何模拟传统USB HID设备的功能。我在开发过程中发现ESP32特别适合用来做这类项目因为它内置蓝牙功能价格便宜而且社区支持很好。2. 蓝牙HID设备开发基础2.1 必备条件要让ESP32变成一个蓝牙HID设备需要满足两个基本条件首先广播数据里要包含HID的UUID和设备外观信息。HID服务的UUID固定是0x1812不同设备的外观代码不一样键盘是0x03C1鼠标是0x03C2游戏手柄是0x03C3。这个信息很重要电脑或手机就是靠这个识别你是什么设备的。其次要在GATT服务中实现HID规范要求的服务和特性。这里涉及到几个关键特性0x2A4AHID信息包含版本号、国家代码等0x2A4BReport Map定义数据格式0x2A4C控制点用于主机和设备通信0x2A4D数据交互特性0x2A4E协议模式2.2 开发环境准备我用的是MicroPython开发环境版本要在1.16以上。安装好固件后记得导入必要的模块from machine import Pin import ubluetooth from bluetooth import UUID3. 蓝牙键盘实战3.1 键盘实现原理键盘的实现核心在于Report Map的设计。这个数据结构定义了按键信息的编码方式。我刚开始做的时候经常搞混各个字节的含义后来画了个示意图才明白第一个字节是修饰键Ctrl、Shift等第二位保留后面6个字节可以表示6个普通按键。比如发送0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00就表示按下A键。3.2 完整代码解析下面是我调试通过的键盘实现代码ble ubluetooth.BLE() ble.active(True) ble.config(gap_nameESP Keyboard) ble.config(mtu23) HIDS ( UUID(0x1812), ( (UUID(0x2A4A), ubluetooth.FLAG_READ), (UUID(0x2A4B), ubluetooth.FLAG_READ), (UUID(0x2A4C), ubluetooth.FLAG_WRITE), (UUID(0x2A4D), ubluetooth.FLAG_READ | ubluetooth.FLAG_NOTIFY, ((UUID(0x2908), 1),)), (UUID(0x2A4D), ubluetooth.FLAG_READ | ubluetooth.FLAG_WRITE, ((UUID(0x2908), 1),)), (UUID(0x2A4E), ubluetooth.FLAG_READ | ubluetooth.FLAG_WRITE), ), )这里有个小技巧键盘需要两个0x2A4D特性一个用于发送按键信息一个用于接收指示灯状态。而鼠标和游戏手柄通常只需要一个。4. 蓝牙自拍杆的特殊实现4.1 自拍杆的本质你可能想不到蓝牙自拍杆其实是个简化版的键盘它利用了手机的一个特性在相机界面音量键可以当快门用。所以自拍杆本质上就是模拟按下音量键的动作。4.2 媒体按键的实现实现媒体控制功能需要特殊的Report MapMEDIA_REPORT bytes([ 0x05, 0x0C, # Usage Page (Consumer) 0x09, 0x01, # Usage (Consumer Control) 0xA1, 0x01, # Collection (Application) 0x85, 0x01, # Report Id (1) # 省略部分字节... 0x09, 0xEA, # Usage (Volume Down) 0x81, 0x06, # Input (Data,Value,Relative,Bit Field) 0xC0 # End Collection ])这个设计很巧妙只用一个字节就能表示各种媒体控制功能。比如0x10表示音量减0x20表示音量加。我在项目中测试时发现不同手机对这个协议的支持程度可能不一样需要多测试几款设备。5. 蓝牙鼠标开发详解5.1 鼠标数据格式鼠标的数据格式和键盘完全不同。通常用4个字节表示第一个字节按键状态左键、右键、中键第二个字节X轴移动量-127到127第三个字节Y轴移动量第四个字节滚轮状态5.2 实战代码这是我调试通过的鼠标实现MOUSE_REPORT bytes([ 0x05, 0x01, # USAGE_PAGE (Generic Desktop) 0x09, 0x02, # USAGE (Mouse) 0xa1, 0x01, # COLLECTION (Application) 0x85, 0x01, # REPORT_ID (1) # 省略部分字节... 0x81, 0x06, # Input(Data, Variable, Relative); 3 position bytes 0xc0, # END_COLLECTION 0xc0 # END_COLLECTION ])实际使用时要注意移动量是相对值不是绝对坐标。我刚开始犯了个错误以为是要传绝对位置结果鼠标指针乱飞。6. 蓝牙游戏手柄开发6.1 手柄数据结构游戏手柄的数据结构最复杂通常包含摇杆的X/Y轴坐标各1字节多个按钮状态通常用1个字节的各个位表示6.2 完整实现方案这是我做的一个简易手柄实现JOY_REPORT bytes([ 0x05, 0x01, # USAGE_PAGE (Generic Desktop) 0x09, 0x04, # USAGE (Joystick) 0xa1, 0x01, # COLLECTION (Application) 0x85, 0x01, # REPORT_ID (1) # 省略部分字节... 0x81, 0x02, # Input (Data, Variable, Absolute) 0xc0, # END_COLLECTION 0xc0 # END_COLLECTION ])摇杆的坐标范围是-127到127中间位置是0。按钮状态用位表示比如0x01表示第一个按钮按下0x03表示前两个按钮都按下。7. 开发中的常见问题7.1 连接稳定性问题我遇到过设备配对后无法自动连接的问题。后来发现需要在代码中添加重连逻辑或者每次使用前手动删除配对信息。这不是ESP32的问题而是蓝牙HID协议的特性决定的。7.2 数据格式一致性Report Map和实际发送的数据必须严格匹配。我有次修改了Report Map但忘了更新发送代码结果设备完全没反应。调试这类问题时建议先用现成的例子测试确保基础功能正常后再修改。7.3 添加额外服务实际产品中通常还需要添加设备信息服务(DIS)和电池服务(BAS)。这两个服务不是必须的但有了它们用户体验会好很多。比如电池服务可以让用户知道手柄还剩多少电量。

更多文章