
一对几十块的ESP32-C3把Android自动化从ADB里解放出来做Android自动化测试和群控的朋友应该都体会过ADB方案的那些痛点手机必须开启USB调试插着线还经常被驱动和授权弹窗折腾遇到MTK或部分国产机型还会时不时掉线。更烦的是一旦自动化任务跑到一半设备被锁屏或者弹出授权框整个流程就卡死了。我一直在想能不能绕开USB和ADB用更接近真人操作的方式去控制手机。后来我换了个思路把ESP32-C3伪装成一个蓝牙HID触摸屏直接通过蓝牙向Android设备发送标准的触摸屏HID报文。手机端不需要ROOT、不需要USB调试、不装任何App只要配对蓝牙就能被“隔空触控”。这个方案我跑了大半年现在日常的自动化脚本、群控刷任务、甚至给客户做演示都靠它顶着。这篇文章我就把这个项目的完整思路、核心代码和踩坑记录全部整理出来希望对想做无线自动化控制的朋友有帮助。1. 项目概述与方案选型1.1 为什么偏偏是ESP32-C3市面上能做蓝牙HID的设备不少比如nRF52840、DA14695甚至普通的HC-05蓝牙串口模块也能改造成HID但我最后选了ESP32-C3核心原因有三个。第一是价格和产能。ESP32-C3模块在国产供应链里已经非常成熟几十块钱就能拿到带板载天线的模组比nRF52840便宜了一大截。而且乐鑫的芯片供货一直很稳不会像某些小众芯片那样动不动缺货涨价。第二是开发环境友好。ESP-IDF提供了非常完善的蓝牙HID示例工程底层协议栈封装得很好你不需要精通蓝牙GATT规范就能做出一个像模像样的HID设备。如果你习惯用Arduino IDE也有现成的BLE-HID库可以用对新手特别友好。第三是性能冗余。ESP32-C3是RISC-V架构单核160MHz虽然算力不算强但处理HID报文发送这种轻量级任务绰绰有余。更关键的是它支持WiFi和蓝牙共存这意味着我可以一边通过WiFi接收上位机下发的指令一边通过蓝牙HID执行触控操作实现真正的无线群控。1.2 四类模拟触控方案横向对比在确定蓝牙HID方案之前我把市面上主流的Android自动化交互方式都过了一遍这里给大家整理成一张对比表方案是否需要USB是否需要Root是否需要装App延迟表现稳定程度ADB input命令是否否低中易受授权弹窗影响自动化App如Auto.js否部分需要是中中依赖无障碍服务硬件键盘映射鼠标否否否低高但无法模拟触摸手势蓝牙HID触摸屏否否否中低高系统级识别从表格可以看出来蓝牙HID触摸屏方案最大的优势在于系统级识别。Android系统会把它当成一块真正的触摸屏所以触摸事件的分发路径和真机手指滑动完全一致不会被自动化检测框架识别为ADB注入或无障碍事件这对做数据采集和自动化脚本来说价值非常大。1.3 这个方案的应用场景除了自动化测试蓝牙HID触摸屏方案在几个场景里表现特别突出。比如旧手机变成智能相框。我家有一台闲置的旧平板插上ESP32-C3后我可以坐在沙发上用手机App控制它翻页、缩放图片比伸手去戳屏幕优雅多了。再比如无障碍辅助设备。我朋友的渐冻症康复中心就在用类似方案通过自定义手势板代替鼠标键盘帮助手部运动障碍的患者操作手机。因为蓝牙HID是系统级协议不需要在每台手机上装辅助软件部署成本低很多。当然用得最多的还是自动化群控。我现在的测试机架上挂了8台Android设备每台设备配一个ESP32-C3加一个几块钱的TP4056锂电池模块通过一个主控板统一调度跑App的日常巡检和稳定性测试一天跑下来极少出现断连。2. 蓝牙HID触控原理剖析2.1 HID协议和触摸屏的“翻译官”蓝牙HIDHuman Interface Device本质上是一套描述“人机交互设备”的协议。鼠标、键盘、游戏手柄、触摸屏都属于HID设备但它们上报数据的方式完全不同。关键就在于报告描述符Report Descriptor它相当于设备的“说明书”告诉手机这个设备有几个按键、几个坐标轴、数据长度是多少。我举个生活化的例子。你插上一个USB键盘系统怎么知道按下的是字母A而不是数字1就是因为键盘的HID报告描述符规定了第几个字节代表哪个按键。触摸屏也是一样报告描述符里声明了设备的用途是“数字化器Digitizer”然后定义了X轴、Y轴坐标以及触摸压力的数据格式。Android系统在蓝牙配对完成后会读取这份“说明书”然后决定把设备识别为鼠标、键盘还是触摸屏。我们只要把报告描述符写成触摸屏的格式Android就会乖乖地把收到的数据当成触摸事件处理。2.2 触摸屏报告描述符的设计这是我工程里精简后的核心报告描述符大家可以对着看。// 蓝牙HID触摸屏报告描述符单点触控 static const uint8_t hidReportDescriptor[] { 0x05, 0x0D, // Usage Page (Digitizer) 0x09, 0x04, // Usage (Touch Screen) 0xA1, 0x01, // Collection (Application) // 第1个触摸点 0x09, 0x20, // Usage (Finger) 0xA1, 0x00, // Collection (Physical) 0x09, 0x32, // Usage (In Range) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x95, 0x01, // Report Count (1) 0x75, 0x01, // Report Size (1) 0x81, 0x02, // Input (Data,Var,Abs) 0x09, 0x42, // Usage (Tip Switch) 0x81, 0x02, // Input (Data,Var,Abs) 0x09, 0x35, // Usage (Touch Contact) 0x81, 0x02, // Input (Data,Var,Abs) 0x95, 0x05, // Report Count (5) 0x81, 0x03, // Input (Const,Var,Abs) // X坐标16位0~4095 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x0F, // Logical Maximum (4095) 0x95, 0x01, // Report Count (1) 0x75, 0x10, // Report Size (16) 0x81, 0x02, // Input (Data,Var,Abs) // Y坐标16位0~4095 0x09, 0x31, // Usage (Y) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x0F, // Logical Maximum (4095) 0x95, 0x01, // Report Count (1) 0x75, 0x10, // Report Size (16) 0x81, 0x02, // Input (Data,Var,Abs) 0xC0, // End Collection 0xC0 // End Collection };这段描述符最关键的地方在于声明了两个16位坐标轴X和Y逻辑范围是0到4095。Android系统看到这个范围后会自动把坐标映射到屏幕的实际分辨率上。比如手机屏幕是1080x2400那么我发送X540Y1200就相当于触摸屏幕正中央。2.3 绝对坐标和相对坐标的模式选择HID触摸屏有两种坐标上报模式一种是绝对坐标一种是相对坐标。绝对坐标就类似于触摸屏直接上报物理位置相对坐标则像鼠标只上报位移量。这里要特别提醒一点如果你用的是传统鼠标的HID描述符报告的是相对位移那么实现“滑动屏幕”这类操作就会非常痛苦因为你需要自己去计算累计位移还要处理加速度问题。而触摸屏天然就是绝对坐标发送多少就是多少不需要做转换Android解析出来的触摸轨迹非常干净这也是推荐用触摸屏描述符而不是鼠标描述符的原因。2.4 多点触控和手势识别的底层逻辑Android的多点触控依赖的是HID描述符里声明多个Finger集合。上面只是单点的描述符如果要做双指缩放就要在Application集合里再塞入一个Finger集合同时增加Contact ID字段让Android能区分是第几根手指的数据包。手势识别可以放在ESP32端做也可以放在上位机做我的工程里选择的是后者。ESP32相当于一个执行器只管把坐标数据发出去手势的插值运算、轨迹规划全部由上位机Python脚本或手机App计算好然后通过串口或WiFi把每一帧的坐标发给ESP32。这种设计的好处是更换代码逻辑时不用反复烧写固件调参效率高了很多。3. 环境准备与工程搭建3.1 硬件清单与接线这个项目用到的硬件非常简洁核心物料如下ESP32-C3开发板我用的合宙ESP32-C3带Type-C串口几十块钱一块1000mAh锂电池可选做移动场景时用几个按键和LED灯用来做状态提示不是必须一个5V转3.3V的稳压模块如果用电池供电需要接线就更简单了ESP32-C3开发板的IO口直接接按键LED接IO8板载LED也可以电池正负极接开发板的5V和GND就行。如果你只是做无线控制连按键都可以省掉直接用串口控制。3.2 ESP-IDF开发环境搭建我写代码用的乐鑫官方的ESP-IDF开发框架推荐使用VSCode搭配Espressif IDF插件图形化界面安装省心不少。安装步骤很简单直接去乐鑫官网下载ESP-IDF离线安装包勾选ESP32-C3支持即可。创建工程时可以直接用官方示例ble_hid_device_demo作为基础然后在此基础上修改报告描述符和蓝牙配置。如果你更喜欢Arduino环境那可以用ESP32-BLE-HID这个库它封装了大部分细节十几行代码就能跑起来但是灵活性和调试能力会比IDF差一些我建议还是花点时间学IDF后面改起来爽得多。用IDF创建完工程后别忘了在menuconfig里配置一下蓝牙模式我用的是Bluetooth - Bluedroid Dual Mode因为后面可能还要兼容经典蓝牙设备所以直接配了双模。3.3 Android端的必要设置Android端准备工作比想象中少很多打开蓝牙并配对ESP32-C3配对码通常是123456或0000。关闭“触控声音和振动”反馈避免每次触摸都有声音干扰。如果Android版本是9以上需要在开发者选项里把“蓝牙HCI窥探”打开这样后期可以通过抓HCI包排查HID报文发送是否正常。不需要ROOT不需要打开“USB调试”也不需要安装任何App。这也是这个方案最香的地方系统层面就把ESP32-C3当成了一块屏幕。有一点需要注意Android对“触摸屏设备”的识别是全局的配对成功后ESP32-C3会接管整个系统的触摸输入所以如果你想同时保留物理触摸屏需要保证手机自身的触摸屏驱动一直在线。如果你用的是平板或开发板通常没有问题如果是某些对触摸屏管理比较特殊的机型可能需要在驱动层做一点适配这个我后面在常见问题里详细说。4. 核心代码实现与参数调优4.1 蓝牙HID初始化流程先看初始化这块。在app_main里我们要依次完成NVS非易失性存储、蓝牙协议栈、GATT服务和HID设备的初始化。void app_main(void) { // 初始化NVS用于保存蓝牙配对信息 esp_err_t ret nvs_flash_init(); if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret nvs_flash_init(); } ESP_ERROR_CHECK(ret); // 初始化蓝牙协议栈 ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ret esp_bt_controller_init(bt_cfg); ESP_ERROR_CHECK(ret); ret esp_bt_controller_enable(ESP_BT_MODE_BLE); ESP_ERROR_CHECK(ret); ret esp_bluedroid_init(); ESP_ERROR_CHECK(ret); ret esp_bluedroid_enable(); ESP_ERROR_CHECK(ret); // 注册HID回调并开始广播 esp_hid_device_config_t hid_config { .vendor_id 0x1234, .product_id 0x5678, .device_name ESP32-C3 Touch, .report_desc hidReportDescriptor, .report_desc_len sizeof(hidReportDescriptor), }; ret esp_hid_device_register(hid_config, g_hid_device, hid_event_handler); ESP_ERROR_CHECK(ret); }这里有个小坑esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)必须在esp_bt_controller_init()之前调用不然内存不够用。因为是纯BLE模式所以把经典蓝牙的内存释放掉给BLE协议栈和App让路。如果你希望同时支持经典蓝牙和BLE就不能这么做需要配置双模。hid_event_handler是核心的事件回调函数所有HID事件连接、断开、上报数据、上位机下发命令都会进入这个函数。在回调里我主要处理三种事件连接建立、连接断开、报告发送完成。连接建立后我会点亮LED表示就绪断开后自动重启广播保证手机重新配对时能快速找到设备。4.2 单点触控与滑动手势实现这是整个项目的灵魂。单点触控的报文格式在上面已经定义了8字节的结构第1个字节放状态标志位手指是否在范围内、是否按下第2和第3字节放X坐标低字节在前第4和第5字节放Y坐标。发送一个触摸点这样实现typedef struct { uint8_t header; uint8_t x_low; uint8_t x_high; uint8_t y_low; uint8_t y_high; uint8_t reserved[3]; } __attribute__((packed)) touch_report_t; void send_touch_event(uint16_t x, uint16_t y, bool touch, bool in_range) { touch_report_t report {0}; report.header 0x00; if (in_range) report.header | 0x01; if (touch) report.header | 0x02; if (touch) report.header | 0x04; report.x_low x 0xFF; report.x_high (x 8) 0xFF; report.y_low y 0xFF; report.y_high (y 8) 0xFF; esp_hid_device_send_report(g_hid_device, ESP_HID_REPORT_TYPE_INPUT, report_bytes, sizeof(touch_report_t)); }滑动手势的关键在于插值。如果直接把起点和终点的坐标发出去Android的触摸事件会被判定为“瞬间跳动”不产生滑动效果。我用的方法是在起点和终点之间生成一串连续的中间点每个点之间延时8到15毫秒形成一段平滑的轨迹。void send_swipe_gesture(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, int duration_ms) { int steps duration_ms / 10; // 每10ms一个点 if (steps 2) steps 2; send_touch_event(x0, y0, true, true); // 手指按下 for (int i 1; i steps; i) { uint16_t xi x0 (x1 - x0) * i / steps; uint16_t yi y0 (y1 - y0) * i / steps; send_touch_event(xi, yi, true, true); vTaskDelay(pdMS_TO_TICKS(10)); } send_touch_event(x1, y1, false, false); // 手指抬起 }实测下来这个10ms一个点的频率在绝大多数Android设备上都很稳滑动轨迹非常丝滑没有任何掉帧感觉。如果你要模拟飞快的滑动比如刷视频时的快速上滑可以把duration调短到30~50ms步数相应减少滑动效果依然很自然。4.3 多点触控与双指缩放多点触控的实现我也不卖关子还是基于HID报告描述符的扩展。我在单点报告描述符的基础上把Finger集合复制了一份并加上了Contact Identifier字段这样Android就能识别这是第二根手指。多点触摸的报告报文长度会随着最大触点数量增长。比如最大支持两个触点那么报告的总长度是单点的两倍多。每次更新时我需要把两个触点的数据都填进报文里缺一个都不能发送否则Android会解析错位导致触摸乱飘。发送双指缩放手势的核心思路是两指按下 - 分别向对方移动 - 保持一段时间 - 同时抬起。我在上位机里预计算了每帧两个触点的坐标然后按顺序发送。实测在相册应用里做双指缩放手势缩放跟手程度和真实手指几乎无差别。4.4 参数调优和延迟优化把参数调优的经验分享给大家这是最容易踩坑的地方参数项推荐值说明插值间隔8~15ms太短会导致报文堆积太长则滑动有卡顿报告发送次数每秒40~60次这是Android触控框架比较舒适的频率单次滑动总时长100~500ms根据模拟场景调整刷信息流用短的双击间隔150~250ms太短会被识别为单击太长又变两次单击坐标范围0~4095与Report Descriptor一致映射到屏幕分辨率延迟方面我自己实测在Android 13手机上从ESP32发送报文到屏幕响应的延迟大概在30~50ms左右比真机手指略高一点点但体感上几乎无差别。如果你对延迟要求极高还可以启用BLE的DATA_LENGTH_EXTENSION和2M PHY功能这两个参数可以在menuconfig里打开实测还能再降个10~15ms。4.5 Wi-Fi蓝牙协同的扩展设计前面提到过ESP32-C3支持WiFi和蓝牙共存。我在工程里把WiFi也跑起来了这样上位机可以通过UDP把要执行的手势指令发给ESP32ESP32再转发为蓝牙HID报文。这个设计对自动化集群特别有用。我当时的做法是ESP32上电后自动连接指定WiFi并开启一个UDP服务端口上位机PC或服务器只要往这个端口发一条JSON指令比如{cmd:swipe,x0:540,y0:1200,x1:540,y1:300,duration:200}ESP32就能解析并执行滑动。这样一来整个控制链路就从USB线变成了无线网络扩展性一下子就打开了。WiFi和蓝牙同时开启时要注意天线频分复用的配置。我用的是esp_coex默认配置默认情况下会出现蓝牙偶尔卡顿的情况后来在menuconfig里把CONFIG_ESP_COEX_SW_COEXIST_ENABLE打开并把蓝牙的功耗模式改为ESP_PM_NO_LIGHT_SLEEP问题就解决了。5. 实操过程与效果验证5.1 从编译烧录到配对成功的全流程下面是我验证过一遍的完整操作流程照着走基本不会出错。第一步准备代码。把官方ble_hid_device_demo工程复制一份替换掉里面的hid_device_demo.c文件换成我前面讲的核心代码。然后修改menuconfig里的蓝牙设备名称和厂商ID保持和你手机里的配对记录一致。第二步编译烧录。在VSCode的IDF终端里执行idf.py set-target esp32c3 idf.py menuconfig idf.py build idf.py -p /dev/ttyUSB0 flash monitor烧录成功后开发板的串口监视器会打印出蓝牙MAC地址和广播状态。此时如果用手机扫描就能看到一个名为ESP32-C3 Touch的蓝牙设备。第三步手机配对。打开手机蓝牙设置点击配对。配对码输入123456代码里默认的。配对成功后ESP32-C3的板载LED会常亮。第四步验证触控。我建议直接用电脑的串口工具发送一个触摸指令。如果ESP32和电脑通过USB串口相连那么直接在串口工具里手动输入目标坐标比如540,1200,1表示按下再输入540,1200,0表示抬起此时手机屏幕的对应位置理论上就会收到一次触摸事件。如果没反应大概率是报告描述符写错了或者Android版本对HID触摸屏支持不完整。5.2 手势稳定性的实测数据为了验证稳定性我在8台不同品牌和Android版本的设备上跑了同一套自动化脚本。结果如下Android 10滑动、点击、双击识别全部正常。Android 11正常但需要打开“触摸显示”才能在屏幕上看到触摸点轨迹部分国产ROM触摸反馈有轻微延迟。Android 12最流畅的一版系统级触摸响应非常跟手。Android 13及以上依然稳定但在某些OEM设备上如果系统开启了“防误触模式”可能会有概率把HID触摸误判为误触需要去设置里关掉防误触。跑压力测试那几天我让ESP32持续循环执行“上滑—点按—双击”这三个手势24小时断联次数大概是2~3次并且基本都是WiFi和蓝牙信号干扰导致的。在纯蓝牙模式下不开WiFi24小时断联0次。5.3 和ADB方案的数据对比我把这个HID方案和我之前常用的ADB input命令方案做了个简单对比。指标ADB方式蓝牙HID方式延迟20~30ms30~50ms触摸事件识别容易被检测为ADB注入系统级真实触摸是否依赖USB线是否配对时间即插即用首次配对约10秒脚本开发难度低中需要自己处理坐标映射结论很直观ADB胜在开发难度低但蓝牙HID胜在部署灵活和“真实”。如果你的自动化场景需要频繁切换设备、或者设备物理位置不方便插线那蓝牙HID方案绝对值得投资。6. 常见问题排查与避坑指南6.1 典型问题速查表问题现象可能原因解决方案Android不识别为触摸屏只识别为鼠标报告描述符里没有声明Digitizer页面检查HID描述符确认包含0x05, 0x0D等关键字段配对成功但没有触摸反馈报告长度不对或坐标范围超限确认X/Y值在0~4095范围且报文长度和描述符一致触摸时灵时不灵电压不稳定或者BLE信号被WiFi干扰换更稳的电源调整WiFi共存配置双指缩放手势乱跳第二根手指的Contact ID没填对两个触点必须使用不同的Contact ID连接后过一会自动断开蓝牙广播关闭、或电池电压低于3.3V检查esp_hid_device_register后的广播状态补充低压保护逻辑部分手机只能用一次无法重复触控Android端的BLE连接参数协商问题在menuconfig里把蓝牙连接间隔设置为30ms左右并启用DLE6.2 我踩过的三个印象最深的坑第一个坑是报告描述符里的“In Range”和“Tip Switch”位序搞反了。一开始我照着网上某篇博客抄结果Android永远只识别为鼠标模式点按不生效。后来我用Bluetooth HCI Logger抓包对比了真机触摸屏的描述符才发现是Usage Page写错了。所以强烈建议新手先用HCI抓包工具把真机触摸屏的HID描述符导出来再对照着改代码。第二个坑是BLE连接参数的协商问题。默认的BLE连接间隔可能在150ms左右导致滑动时明显掉帧。后来我把连接间隔调整到30ms触控跟手程度大幅提升。不过要注意连接间隔太短会增加功耗对电池供电的设备不友好。平衡下来我选的是40ms。第三个坑和WiFi蓝牙共存有关。刚开始跑自动化集群8台设备的ESP32全部同时开WiFi出现了一个很诡异的现象蓝牙触控偶尔会卡顿几秒然后恢复正常。我排查了半天才意识到是设备间WiFi信号互相干扰了蓝牙的2.4GHz频段。后来把WiFi切换到5GHz频段问题迎刃而解。如果你的路由器不支持5GHz那就只能错峰发送指令避免多台设备同时传输大数据。6.3 定制化的小技巧最后分享两个我做项目时觉得特别实用的定制技巧。如果你需要模拟“长按”手势可以用这个思路手指按下后不抬起持续发送同一坐标的触摸事件保持300到500ms以上。Android系统会根据持续时间和位置变化自动生成长按事件完全不需要额外逻辑。如果你想模拟“物理按键”操作比如音量键那就不能走触摸屏描述符了。需要再建一个独立的HID键盘或消费控制设备用另一个报告描述符。我的工程里实现方式是注册多个HID子设备在事件处理函数里根据上报的HID usage来分发。因为原理类似这里就不展开了。根据我个人经验蓝牙HID方案虽然在触控手势模拟上已经做得很接近真机但依然有一些Android应用会做触摸事件特征分析比如检测相邻触摸点的时间间隔是否异常规律或者坐标轨迹是否为直线。如果你的自动化场景对真实性要求极高建议在插值算法里加入随机扰动模拟手指抖动的轨迹而不是老老实实地走直线。这个细节实测能大幅降低被检测的概率算是自动化脚本里比较进阶的玩法了。