三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于蓝牙HID协议实现无网络自动化设备控制:ESP32实战指南

基于蓝牙HID协议实现无网络自动化设备控制:ESP32实战指南

你是否遇到过这样的场景:想用电脑远程控制手机,却发现需要安装复杂的软件、配置网络、甚至还要在同一Wi-Fi下?或者,当你需要将手机屏幕投屏到电脑进行演示时,却因为兼容性问题、网络延迟或软件权限而卡住?

传统的投屏方案,无论是基于Wi-Fi的DLNA/AirPlay,还是基于USB的ADB调试,都或多或少存在一些“硬伤”:依赖网络环境、需要配对授权、延迟不稳定,或者干脆在某些设备上无法使用。有没有一种更底层、更通用、甚至能在无网络环境下工作的控制方式?

答案是:有。这就是蓝牙HID(Human Interface Device)。你可能对这个名词感到陌生,但它驱动的设备你每天都在用——无线键盘、鼠标、游戏手柄,都是通过蓝牙HID协议与电脑或手机通信的。本文将深入探讨一个极具潜力的技术方向:利用蓝牙HID协议,实现无需复杂软件的自动化投屏与设备控制

这篇文章要解决的核心问题是:如何绕过传统投屏方案的高层依赖,通过模拟蓝牙键盘/鼠标的底层输入,实现对目标设备(如手机、平板、甚至智能电视)的“无感”自动化控制。这不仅仅是另一个投屏工具,而是一种思维转换——将控制指令“伪装”成最通用的外设输入信号。

对于开发者、测试工程师、自动化脚本编写者以及极客玩家而言,掌握这项技术意味着:

  1. 环境无关性:不依赖特定APP、不要求Wi-Fi网络,只要设备支持蓝牙,就有可能建立连接。
  2. 高权限模拟:可以模拟物理按键(如Home、Back、音量键),这是很多软件方案无法做到的。
  3. 低延迟与高可靠性:HID是操作系统最底层的输入接口之一,响应直接且稳定。
  4. 自动化潜力:可以编写脚本,通过蓝牙HID连续发送指令,实现复杂的自动化操作流程。

接下来,我们将从原理到实践,完整拆解“蓝牙HID操作”的技术栈,并提供一个从概念验证到代码实现的完整路径。

1. 蓝牙HID:重新理解“控制”的底层逻辑

在深入技术细节之前,我们必须先扭转一个观念:我们不是在“投屏”,而是在“模拟输入”。

传统投屏(如Scrcpy、ApowerMirror)的核心逻辑是“图像传输+指令回传”

  1. 在受控端(手机)启动一个服务,捕获屏幕帧。
  2. 通过网络(USB网络共享或Wi-Fi)将压缩后的视频流发送到控制端(电脑)。
  3. 控制端显示图像,并将用户的鼠标点击、键盘输入等事件,通过网络传回受控端执行。 这个过程对两端软件有强依赖,且严重受网络质量影响。

蓝牙HID控制的核心逻辑是“角色扮演+协议欺骗”

  1. 我们的控制设备(如一块ESP32开发板或一台装有特定蓝牙栈的电脑)扮演成一个蓝牙键盘或鼠标
  2. 目标设备(手机/电脑/平板)将其识别为一个普通的外接输入设备
  3. 控制设备通过标准的蓝牙HID协议,向目标设备发送键盘按键码鼠标移动/点击数据包
  4. 目标设备操作系统接收到这些数据包,会将其与物理键盘/鼠标产生的输入一视同仁地处理。

这意味着,从目标设备的视角看,它只是连接了一个新的无线键盘,而这个“键盘”恰好是由我们的程序控制的。我们由此获得了在操作系统输入层级进行控制的能力。

关键优势对比

特性传统网络投屏方案蓝牙HID控制方案
依赖环境需要网络(USB/Wi-Fi),需要两端安装软件仅需蓝牙硬件支持,无需目标端安装特定软件
控制权限受限于投屏软件提供的API,通常无法模拟系统键可模拟几乎所有物理按键(包括Power, Home, Volume)
延迟受网络波动和编码/解码影响,通常较高极低,接近物理外设延迟
自动化需调用软件接口,可能不稳定直接发送HID报告,稳定可靠,易于脚本化
适用场景需要观看屏幕内容的远程协助、演示自动化测试、无障碍辅助、遥控器、后台自动化

2. 核心概念与技术栈解析

要实现蓝牙HID控制,你需要理解以下几个核心概念:

1. HID (Human Interface Device) 协议: 这是USB和蓝牙设备中用于定义人机交互设备(键盘、鼠标、游戏手柄、遥控器)如何与主机通信的标准。它规定了设备如何描述自身功能(通过HID描述符),以及如何以报告(Report)的形式发送数据(如按下了A键)。

2. GATT (Generic Attribute Profile): 这是低功耗蓝牙(BLE)中用于数据传输的核心协议。HID over GATT (HOGP) 是BLE设备实现HID功能的规范。我们的设备需要通过GATT服务来暴露HID功能。

3. HID报告描述符 (Report Descriptor): 这是一个二进制数据结构,用于告诉主机“我这个设备是什么、能发送哪些数据”。例如,一个键盘的报告描述符会定义它支持哪些按键、是否有LED指示灯(如Caps Lock)。这是欺骗主机的关键,我们需要精心构造一个描述符,让手机认为我们是一个标准的键盘或鼠标。

4. 配对与绑定 (Pairing & Bonding): 为了安全,蓝牙设备通常需要配对。对于HID设备,许多操作系统(如iOS、Android)支持“Just Works”配对模式,无需输入PIN码,这简化了自动连接流程。配对成功后,设备信息会被保存(绑定),下次可以快速重连。

技术栈选择: 要实现一个蓝牙HID控制器,通常有以下几种路径:

  • 微控制器方案:使用ESP32(最流行)、nRF52系列、RP2040等支持BLE的MCU。它们功耗低、成本低,可以做成独立的硬件“遥控器”。这是本文重点。
  • PC软件方案:在Linux/Windows/Mac上,利用系统蓝牙栈(如BlueZ on Linux)将自己模拟成一个蓝牙HID设备。这更适合在电脑上运行控制脚本。
  • 手机反控方案:理论上,一部手机也可以模拟成HID设备控制另一部手机,但这涉及更复杂的系统权限。

3. 环境准备:以ESP32为例的硬件与软件搭建

我们选择ESP32作为实践平台,因为它性价比高、社区资源丰富、且Arduino/ESP-IDF开发环境成熟。

所需硬件

  1. ESP32开发板(任何带有蓝牙功能的型号均可,如ESP32-DevKitC、NodeMCU-32S)。
  2. USB数据线(用于供电和烧录程序)。
  3. 目标设备(一部支持蓝牙的Android手机或iPhone,用于测试)。

软件环境准备

  1. 安装Arduino IDE:从Arduino官网下载并安装。
  2. 添加ESP32开发板支持
    • 打开Arduino IDE,进入文件 -> 首选项
    • 在“附加开发板管理器网址”中输入:
      https://espressif.github.io/arduino-esp32/package_esp32_index.json
    • 点击“确定”。
  3. 安装ESP32开发板
    • 进入工具 -> 开发板 -> 开发板管理器
    • 搜索“esp32”,找到由Espressif Systems提供的“esp32”平台,点击安装。
  4. 安装必要的库:对于BLE HID,我们将使用ESP32 Arduino核心自带的BLE库,通常无需额外安装。

开发板配置

  • 在Arduino IDE中,选择工具 -> 开发板 -> ESP32 Arduino,然后选择你具体的开发板型号(如“ESP32 Dev Module”)。
  • 选择正确的端口(工具 -> 端口)。
  • 其他设置可保持默认。

4. 核心流程拆解:从零构建一个蓝牙HID键盘

让我们把目标拆解为可执行的步骤:

步骤1:初始化BLE并设置设备信息首先,我们需要启动BLE,并给我们的设备起一个名字,让手机在蓝牙列表里能找到它,比如叫“My HID Keyboard”。

步骤2:创建HID设备并发布服务这是最关键的一步。我们需要创建一个符合HID over GATT规范的服务,并包含以下特征值(Characteristics):

  • HID信息:包含厂商ID、产品ID、版本等。
  • 报告映射:即HID报告描述符,告诉手机我们是键盘。
  • 协议模式:设置为“报告”模式。
  • 输入报告:用于向主机(手机)发送按键数据。
  • 输出报告:用于接收来自主机的数据(如键盘LED状态),非必需。
  • 控制和数据:用于管理连接。

步骤3:构造HID报告描述符我们需要定义自己的报告描述符。一个最简单的键盘描述符需要说明:我有一个8字节的输入报告,其中前2个字节是修饰键(Ctrl, Shift等),后面6个字节是普通按键码。

步骤4:广播并等待连接启动广播,让手机可以扫描并发现我们的“键盘”。

步骤5:连接后发送按键数据一旦手机连接成功,我们就可以通过向“输入报告”特征值写入数据来模拟按键。数据格式必须严格符合我们在报告描述符中定义的格式。

步骤6:处理连接与断开事件需要编写回调函数,妥善处理连接建立和断开的事件,以便在断开后重新开始广播。

5. 完整示例:ESP32蓝牙HID键盘实现代码

下面是一个完整的Arduino Sketch示例,它将ESP32模拟成一个蓝牙键盘,连接后每隔5秒自动发送一次“Volume Up”按键信号。

// 文件:esp32_hid_keyboard.ino #include <BLEDevice.h> #include <BLEUtils.h> #include <BLEServer.h> #include <BLE2902.h> // 定义HID服务的UUID(标准键盘鼠标HID服务) #define SERVICE_UUID "1812" // Human Interface Device // 定义报告映射特征的UUID #define REPORT_MAP_UUID "2A4B" // 定义HID信息特征的UUID #define HID_INFO_UUID "2A4A" // 定义报告特征的UUID(用于发送按键数据) #define REPORT_UUID "2A4D" // 定义协议模式特征的UUID #define PROTOCOL_MODE_UUID "2A4E" // 自定义的特征UUID,用于控制和数据通道(遵循HOGP规范) #define CONTROL_POINT_UUID "2A4C" #define DATA_IN_UUID "2A4D" // 注意:此UUID与REPORT_UUID相同,但在不同服务中含义不同,实践中需区分。此处为简化,我们只使用一个报告特征。 BLEServer *pServer; BLECharacteristic *pReportCharacteristic; bool deviceConnected = false; // 标准的键盘HID报告描述符 // 它描述了一个包含8字节输入报告的键盘 uint8_t hidReportDescriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x85, 0x01, // Report ID (1) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0xE0, // Usage Minimum (224) 0x29, 0xE7, // Usage Maximum (231) 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) ; Modifier byte 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x01, // Input (Cnst,Arr,Abs) ; Reserved byte 0x95, 0x05, // Report Count (5) 0x75, 0x01, // Report Size (1) 0x05, 0x08, // Usage Page (LEDs) 0x19, 0x01, // Usage Minimum (1) 0x29, 0x05, // Usage Maximum (5) 0x91, 0x02, // Output (Data,Var,Abs) ; LED report 0x95, 0x01, // Report Count (1) 0x75, 0x03, // Report Size (3) 0x91, 0x01, // Output (Cnst,Arr,Abs) ; LED report padding 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0x00, // Usage Minimum (0) 0x29, 0x65, // Usage Maximum (101) 0x81, 0x00, // Input (Data,Arr,Abs) ; Key arrays (6 bytes) 0xC0 // End Collection }; // 服务器回调类:处理连接事件 class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected = true; Serial.println("Device connected"); }; void onDisconnect(BLEServer* pServer) { deviceConnected = false; Serial.println("Device disconnected"); // 断开后重新开始广播,以便再次连接 pServer->getAdvertising()->start(); Serial.println("Advertising restarted"); } }; void setup() { Serial.begin(115200); Serial.println("Starting BLE HID Keyboard example..."); // 初始化BLE设备,名称将显示在手机的蓝牙列表中 BLEDevice::init("My HID Keyboard"); // 创建BLE服务器 pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); // 创建HID服务 BLEService *pService = pServer->createService(SERVICE_UUID); // 创建HID信息特征 (0x0111表示版本1.11,0x00, 0x01表示国家代码) BLECharacteristic *pHidInfo = pService->createCharacteristic( HID_INFO_UUID, BLECharacteristic::PROPERTY_READ ); uint8_t hidInfo[] = {0x01, 0x11, 0x00, 0x01}; pHidInfo->setValue(hidInfo, sizeof(hidInfo)); // 创建报告映射特征 BLECharacteristic *pReportMap = pService->createCharacteristic( REPORT_MAP_UUID, BLECharacteristic::PROPERTY_READ ); pReportMap->setValue(hidReportDescriptor, sizeof(hidReportDescriptor)); // 创建协议模式特征 (设置为报告模式,值=1) BLECharacteristic *pProtocolMode = pService->createCharacteristic( PROTOCOL_MODE_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE_NR ); uint8_t protocolMode = 0x01; // Report protocol mode pProtocolMode->setValue(&protocolMode, 1); // 创建报告特征(用于发送按键数据) pReportCharacteristic = pService->createCharacteristic( REPORT_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_WRITE_NR ); // 添加客户端配置描述符,允许通知 pReportCharacteristic->addDescriptor(new BLE2902()); // 创建控制点特征 BLECharacteristic *pControlPoint = pService->createCharacteristic( CONTROL_POINT_UUID, BLECharacteristic::PROPERTY_WRITE_NR ); // 启动服务 pService->start(); // 开始广播 BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(true); pAdvertising->setMinPreferred(0x06); // 有助于提高iOS连接稳定性 pAdvertising->setMinPreferred(0x12); BLEDevice::startAdvertising(); Serial.println("BLE HID Keyboard is now advertising. Connect to 'My HID Keyboard' from your phone."); } // 发送按键报告的辅助函数 // 参数:modifier (修饰键,如Ctrl, Shift), keyCode (普通按键码) void sendKey(uint8_t modifier, uint8_t keyCode) { if (!deviceConnected) { Serial.println("Device not connected, cannot send key."); return; } // HID键盘报告格式:8字节数组 // [0]: modifier keys // [1]: reserved // [2-7]: key codes (最多支持6个按键同时按下) uint8_t report[] = {modifier, 0, keyCode, 0, 0, 0, 0, 0}; pReportCharacteristic->setValue(report, sizeof(report)); pReportCharacteristic->notify(); Serial.printf("Key sent: Modifier=0x%02X, KeyCode=0x%02X\n", modifier, keyCode); // 发送一个“所有键释放”的报告,模拟按键松开 delay(100); // 短暂延迟,模拟按键按下时间 uint8_t releaseReport[] = {0, 0, 0, 0, 0, 0, 0, 0}; pReportCharacteristic->setValue(releaseReport, sizeof(releaseReport)); pReportCharacteristic->notify(); Serial.println("Key released."); } void loop() { if (deviceConnected) { // 示例:每隔5秒发送一次“音量增加”按键 // HID Usage ID for Volume Increment is 0xE9 sendKey(0, 0xE9); // 修饰键为0,按键码为0xE9 delay(5000); } // 在断开连接时,loop()中的广播重启已由回调函数处理 }

代码关键逻辑解释

  1. HID报告描述符hidReportDescriptor数组定义了设备类型。它告诉连接方:“我是一个键盘,我的数据报告是8个字节,第一个字节是修饰键,第三个字节开始是普通键...”。这是整个模拟的基石。
  2. 服务与特征:我们创建了一个标准的HID服务(UUID: 0x1812),并在其下创建了多个特征,其中REPORT_UUID特征是我们发送按键数据的通道。
  3. 连接管理:通过MyServerCallbacks类处理连接和断开事件,确保断开后能重新广播。
  4. 发送按键sendKey函数构造一个8字节的HID报告。第一个字节是修饰键(如Ctrl、Alt),第三个字节是普通按键码。发送后,紧接着发送一个全零的报告来模拟“释放按键”,这是必须的,否则手机会认为按键一直被按住。
  5. 示例按键:在loop中,我们发送按键码0xE9,这在HID规范中对应“Volume Increment”(音量增加)。你可以在手机的HID Usage Tables文档中找到所有按键码。

6. 运行、验证与效果测试

1. 编译与上传

  • 将上述代码复制到Arduino IDE中。
  • 确保开发板和端口选择正确。
  • 点击“上传”按钮。

2. 在手机上操作

  • 打开手机的蓝牙设置
  • 开始扫描新设备。你应该能看到一个名为“My HID Keyboard”的设备。
  • 点击进行配对。对于键盘设备,通常会自动完成“Just Works”配对,无需输入PIN码。
  • 配对成功后,手机可能会显示“已连接”或“已配对”。

3. 验证效果

  • 连接成功后,打开手机上的任何界面(如主屏幕或音乐播放器)。
  • 观察ESP32的串口监视器(工具 -> 串口监视器,波特率115200)。你会看到“Device connected”日志。
  • 等待5秒(或在代码中调整delay时间),你应该会听到手机的音量增加了一格。串口监视器会打印“Key sent”和“Key released”的信息。
  • 恭喜!你已经通过蓝牙HID协议,用ESP32无线控制了手机的系统功能。

4. 扩展测试

  • 修改sendKey函数的参数,发送其他按键码。例如,0xE2是静音,0xEA是音量减小,0x4F是键盘上的右箭头。
  • 尝试组合键:将modifier参数设置为0x02(代表Left Shift),同时发送一个字母键码(如0x04代表‘a’),手机应该会输入大写‘A’。

7. 常见问题与排查思路

在实际操作中,你可能会遇到以下问题:

问题现象可能原因排查方式解决方案
手机蓝牙列表里找不到设备1. ESP32未正确启动或广播。
2. 手机蓝牙未开启或未刷新。
3. 设备名称广播异常。
1. 检查串口日志,看是否有启动成功的提示。
2. 重启手机蓝牙,或尝试其他蓝牙扫描APP。
3. 检查代码中BLEDevice::init的名称。
1. 确认ESP32供电稳定,代码上传成功。
2. 确保手机蓝牙支持BLE(Android 4.3+/iOS 7+)。
3. 尝试将设备名称改短,避免特殊字符。
配对失败或连接立即断开1. HID服务或描述符不符合规范。
2. 缺少必要的特征或描述符。
3. 手机系统对HID设备有特殊限制(如iOS)。
1. 检查串口是否有错误输出。
2. 对比标准的HOGP服务结构。
3. 查看手机系统通知或蓝牙设置中的错误信息。
1. 仔细核对hidReportDescriptor,确保其正确。
2. 确保创建了BLE2902描述符用于通知。
3. 对于iOS,尝试在广播设置中setMinPreferred(0x06)setMinPreferred(0x12)
连接成功但按键无反应1. 未在连接状态下发送数据。
2. 报告数据格式错误。
3. 未发送“释放按键”报告。
4. 按键码不正确。
1. 确认deviceConnected变量为true
2. 检查sendKey函数构造的报告数组。
3. 确认发送了全零的报告来释放按键。
4. 查阅HID Usage Table,验证按键码。
1. 在sendKey函数开始处检查连接状态。
2. 确保报告数组长度为8,且格式与描述符匹配。
3. 在发送按键报告后,必须发送释放报告。
4. 先使用已知正确的系统键(如音量键)测试。
按键反应延迟或卡顿1. ESP32处理速度或蓝牙栈拥堵。
2.delay时间过长阻塞主循环。
3. 手机端处理延迟。
1. 减少串口打印输出以节省资源。
2. 使用非阻塞的定时方式(如millis())替代delay()
3. 检查手机后台应用是否过多。
1. 优化代码逻辑,避免在关键循环中做耗时操作。
2. 实现一个简单的状态机来管理按键发送时序。
3. 重启手机或关闭不必要的蓝牙设备。
无法模拟某些特定按键(如Home、Power)1. 操作系统出于安全考虑,限制了某些系统级按键的模拟。
2. 报告描述符未声明对这些按键的支持。
3. 使用的按键码不是HID规范中的标准Consumer Page代码。
1. 查阅Android/iOS开发文档,了解HID限制。
2. 检查报告描述符中Usage Page和Usage的范围是否包含了目标按键。
3. 确认发送的是Consumer Control Page的代码(如0x40对应Home)。
1. 对于Android,可能需要辅助功能权限或特殊API,纯HID可能无法实现。
2. 修改报告描述符,包含Consumer Control Page。
3. 尝试使用已知可用的媒体键进行测试。

8. 进阶应用与工程实践建议

掌握了基础键盘模拟后,你可以将这个技术应用到更复杂的场景中。

1. 组合键与复杂序列: 你可以编程实现一系列按键操作。例如,模拟“Ctrl+C”复制操作:

// 模拟 Ctrl+C sendKey(0x01, 0x06); // 0x01是Left Ctrl, 0x06是‘c’键 delay(100);

通过编排这样的序列,可以实现复制、粘贴、截屏、应用切换等复杂操作。

2. 模拟鼠标: 你需要创建一个新的HID报告描述符来定义鼠标设备。鼠标报告通常包含按键状态、X/Y轴位移和滚轮数据。然后创建对应的服务和特征来发送鼠标报告。

3. 与自动化脚本结合: ESP32可以接收来自串口、Wi-Fi或传感器的指令,然后触发相应的蓝牙HID操作。例如,你可以:

  • 写一个Python脚本在电脑上运行,通过串口向ESP32发送命令字符串(如“TYPE:Hello World”),ESP32解析后模拟键盘输入。
  • 让ESP32连接Wi-Fi,作为一个简单的HTTP服务器,接收网络请求来控制手机。
  • 连接一个红外接收头,让ESP32学习电视遥控器的信号,然后将其转化为蓝牙HID命令来控制手机或电脑的媒体播放。

4. 低功耗优化: 如果你的设备需要电池供电,需要进行低功耗优化:

  • setup()中调用BLEDevice::setPower(ESP_PWR_LVL_N12)降低蓝牙发射功率(会减少连接距离)。
  • 在断开连接后,除了重新广播,还可以让ESP32进入深度睡眠,定时唤醒检查是否需要重连。
  • 精简代码,移除不必要的串口打印。

5. 生产环境注意事项

  • 安全性:目前的示例没有加密或认证。在生产环境中,应考虑使用带认证的配对方式(如Passkey Entry),防止未经授权的设备连接并控制你的手机。
  • 稳定性:增加连接状态监控和断线重连机制,确保长时间运行稳定。
  • 用户交互:可以考虑增加一个物理按钮,只有在按下按钮时设备才进入广播模式,平时处于休眠状态,提高安全性和电池寿命。
  • 兼容性测试:在不同品牌和型号的Android手机、iPhone、iPad、Windows电脑、Mac上进行测试,确保HID描述符和协议模式被广泛接受。

从“模拟一个按键”到“构建一个自动化控制系统”,蓝牙HID技术为你打开了一扇新的大门。它剥离了网络和特定软件的依赖,回归到最本质的“输入”与“控制”。无论是用于硬件原型开发、自动化测试,还是创造独特的交互设备,理解并掌握这一底层协议,都将让你在物联网和自动化项目中拥有更强大的武器。建议你将本文的示例代码作为起点,深入阅读ESP32-BLE-Arduino库的文档和HID官方规范,探索更多可能性。

← 返回列表