安卓USB HID通信实战:从协议到代码与抓包调试
简介这份安卓 USB HID 通信 Demo 面向需要让手机通过 OTG 直接连接单片机的移动开发者和嵌入式工程师完整演示了 UsbManager 设备发现、权限申请、端点读写、HID 报告解析及断开释放等关键流程。包内共 1055 个文件压缩包约 17.44MB以 Java 源码、XML 布局与配置、Gradle 工程文件为主同时包含 PNG 界面与文档图片、JSON/bin 构建缓存等可直接导入 Android Studio 运行调试。已有 3752 人学习下载。借助 Ground_Station 项目场景读者可对照 USB HID 协议理解和实际代码快速搭建安卓作为主机的单片机通信通道资源还配有工程目录与构建脚本适合作为学习 USB Host 模式、OTG 调试和 HID 数据收发的起步模板。 先说个我自己的经历。去年接了一个需求要做一款安卓App去控制公司定制的USB HID键盘实现背光模式切换、按键宏录制这些功能。需求方觉得“安卓通过USB控制键盘”应该不难我一开始也是这么想的——安卓不是自带UsbManager吗枚举设备、打开连接、收发数据看起来就是几行代码的事。真正动手后发现网上一搜“安卓USB通信”搜出来的几乎全是CH340转串口的教程HID相关的完整Demo少得可怜官方文档只讲API不讲流程。我折腾了整整一个周末才把最小可通信的Demo跑通中间踩的坑包括权限回调丢失、拔线崩溃、数据格式对不上。这篇文章就是把这些经历重新梳理一遍带你从HID协议基础、UsbManager的枚举与权限走到中断收发代码、热插拔处理最后用wireshark抓包定位问题。如果你也是要在安卓侧和HID外设做通信这篇文章可以直接当参考手册用。1. 为什么安卓做USB HID通信的Demo这么少1.1 大多数安卓USB教程都在“转串口”思路完全不同很多开发者第一反应是去搜“安卓 UsbManager 通信”结果发现教程里全是CH340、CP2102这类USB转串口芯片的玩法UsbManager拿到设备后找到CDC接口然后往批量端点写数据。整个流程跟操作一个虚拟串口差不多底层数据流是流式的发什么对方收什么。一旦换成HID这套思路就失灵了。HID设备不管收还是发走的是“报告”模型数据被封装成固定格式的Input/Output Report有Report ID、有长度限制不是简单的“把字节流丢过去”。这也是为什么HID方向的完整Demo少。串口方案的范式太成熟抄作业容易而HID需要理解协议层概念写出来还要结合具体硬件很少有人愿意花时间整理成通用教程。我翻遍了GitHub和各大社区能找到的项目大部分是串口通信库的封装真的针对HID中断端点收发、而且直接能跑的安卓项目一只手数得过来。1.2 先确认你的设备是不是HID别被接口名骗了动手之前第一件事不是写代码是确认你的设备到底枚举成什么。拿Windows举例子插上设备后设备管理器里显示“HID-compliant device”或者“USB输入设备”那基本可以判断是HID协议如果显示“COM端口”那是串口方案。在Linux上可以用lsusb -v看接口描述符重点关注bInterfaceClass字段——HID设备的接口类通常是03串口类CDC ACM是0A。这里有个容易踩的坑有些设备是复合设备一个USB口同时带HID接口和串口接口比如“HIDCDC二合一”的定制外设。这种情况下你用串口教程里的方式去枚举可能把HID接口当成串口接口打开后续数据全乱。所以写代码前先用工具看清楚设备上有几个接口、每个接口的类、端点和方向再决定走哪条路。另外热搜里总有人问“hid keyboard device是什么键盘”其实就是标准的HID键盘安卓系统会直接把它的按键事件当成系统输入普通App根本收不到要做自定义通信设备端最好用厂商自定义的Usage Page别用标准键盘用途。2. USB HID协议的核心报告、中断端点与控制传输2.1 HID设备靠“报告”说话与串口流式传输是两套逻辑HID协议里最核心的词是“报告”。设备通过HID Report Descriptor报告描述符告诉主机我支持哪些输入项、输出项、每个项多长、值域范围是多少。真正通信时双方交换的是一份份结构化的报告不是连续的字节流。比如一个标准键盘每次会发一个8字节的Input Report里面包含修饰键、保留位、按键码按键状态变了就重新发一份。对安卓侧来说UsbManager没有替你把HID这层抽象掉它只给你原始端点。所以你在代码里看到的东西就是“往端点写8个字节”或者“从端点读8个字节”但这些字节的每个位置代表什么意思需要你和设备端固件先把报告格式对齐。这一条特别重要很多Demo跑不起来不是因为代码不对而是收发双方对“第0字节是什么、长度是几”根本没谈拢。2.2 中断端点和最大包长数据是怎么从A点到B点的HID设备的数据传输通常走中断端点。这里说的“中断”不是硬件上拉一根中断线而是主机按固定间隔去轮询设备有没有新报告有就取走。HID标准里键盘、鼠标这类设备默认就是这个模式。安卓的UsbEndpoint里中断端点的类型是UsbConstants.USB_ENDPOINT_XFER_INT方向有USB_DIR_IN设备到主机和USB_DIR_OUT主机到设备两种。关键问题是端点最大包长。全速设备的中断端点常见最大包长是64字节低速设备可能只有8字节。如果一次要发的报告超过最大包长就需要自己拆包。这个数字不是拍脑袋定的可以读endpoint.getMaxPacketSize()拿到。我遇到过设备端固件把输入报告设定成128字节但中断端点最大包长只有64安卓这边一次bulkTransfer只发64字节设备端收一半就开始按完整报告解析结果自然全错。2.3 什么时候该用控制传输SET_REPORT与GET_REPORT除了中断端点HID还有一类通道叫控制传输用来交换Feature Report典型场景是配置设备参数。比如设备上有个可调的灵敏度、呼吸灯模式很多时候不走中断端点而是通过控制传输发SET_REPORT指令去设置用GET_REPORT去读取当前配置。安卓侧对应的方法是UsbDeviceConnection.controlTransfer。用起来要小心setup包里的参数。以SET_REPORT为例请求类型是USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE请求号是0x09wValue的高字节是报告类型0x02代表Feature Report低字节是Report IDwIndex填接口编号后面跟着报告数据。如果设备没用Report IDwValue高字节依旧要填低字节填0。还有一个容易被忽略的点控制传输虽然也可以读写但对时间敏感的数据不推荐走这里实时性不如中断端点频繁用还容易把USB总线堵住。3. 从零跑通通信Demo枚举、权限、中断收发的完整代码链路3.1 准备清单OTG设备、Manifest声明和权限先确认你的安卓设备支持USB Host模式现在绝大多数手机和平板都支持但个别低端机器系统裁剪过可能没开放。最简单的方法是在Manifest里加一条声明如果设备不支持应用根本装不上uses-feature android:nameandroid.hardware.usb.host android:requiredtrue /注意这里不需要额外写uses-permissionUSB Host的访问权限是系统默认授予的网上有些老教程让你加android.permission.USB_PERMISSION那是错的加了反而可能让部分系统出现兼容问题。真正需要你处理的是运行时弹窗授权这个后面章节详细说。硬件上用一个OTG转接头把外设接到安卓设备先确认系统有没有识别出来。可以在开发者选项里打开“USB检查”或者直接写个简单的Activity打印UsbManager.getDeviceList()。如果列表是空的先检查线材和转接头别急着调代码我吃过这个亏换了一根杂牌OTG线之后设备列表突然就出来了。3.2 找到HID接口和中断端点拿到UsbDevice之后第一步是从设备对象里找到我们需要的接口和端点。代码逻辑不复杂但很容易写漏private UsbInterface findHidInterface(UsbDevice device) { for (int i 0; i device.getInterfaceCount(); i) { UsbInterface intf device.getInterface(i); if (intf.getInterfaceClass() UsbConstants.USB_CLASS_HID) { for (int j 0; j intf.getEndpointCount(); j) { UsbEndpoint ep intf.getEndpoint(j); if (ep.getType() UsbConstants.USB_ENDPOINT_XFER_INT) { // 这里拿到中断端点再按方向存起来 if (ep.getDirection() UsbConstants.USB_DIR_IN) { inEndpoint ep; } else { outEndpoint ep; } } } return intf; } } return null; }判断HID接口时getInterfaceClass() 3是最直觉的条件但部分厂商设备接口类字段写得并不规范有的返回0有的返回255。这时候你可以退而求其次直接看这个接口下有没有中断端点有就按HID处理。当然最稳的还是先抓包确认设备描述符再写死VID/PID和接口索引。3.3 打开设备、申请权限、中断收发连接设备前必须完成权限弹窗的流程。系统会弹一个“是否允许访问USB设备”的对话框这个操作是异步的结果通过广播返回private static final String ACTION_USB_PERMISSION com.yourpackage.USB_PERMISSION; PendingIntent permissionIntent PendingIntent.getBroadcast( context, 0, new Intent(ACTION_USB_PERMISSION), Build.VERSION.SDK_INT 31 ? PendingIntent.FLAG_MUTABLE : 0); usbManager.requestPermission(device, permissionIntent);在广播接收器里判断EXTRA_PERMISSION_GRANTED结果为true才能继续。Android 12以后PendingIntent必须显式声明FLAG_MUTABLE或FLAG_IMMUTABLE这里要按系统版本区分否则低版本手机可能因为识别不了FLAG_MUTABLE出问题。拿到权限后就可以打开连接了UsbDeviceConnection connection usbManager.openDevice(device); if (connection ! null) { connection.claimInterface(hidInterface, true); }中断发送最简单的方式是bulkTransfer虽然名字带bulk但对中断端点同样适用int sent connection.bulkTransfer(outEndpoint, data, data.length, 2000);如果返回的sent是-1说明底层发送失败大概率是端点选择错误或设备已断开。中断读取推荐用UsbRequest配合requestWait它能走异步队列不阻塞主线程UsbRequest request new UsbRequest(); request.initialize(connection, inEndpoint); ByteBuffer buffer ByteBuffer.allocate(inEndpoint.getMaxPacketSize()); request.setClientData(buffer); request.queue(buffer, buffer.capacity()); // 在子线程里等待完成 UsbRequest completed connection.requestWait();这里有个细节requestWait会一直阻塞直到有报告进来或者设备断开所以必须在工作线程里跑并且要处理设备断开时的异常退出不然线程会一直挂死。4. 踩坑实录权限回调丢失、热插拔崩溃、设备无响应4.1 权限弹窗点了“确定”广播回调却没来这是我在网上被问得最多的问题。明明弹窗弹出来了用户也点了确定但广播接收器就是没收到。最常见的两个原因一是PendingIntent的Action和你接收器注册的Action不一致这种低级错误排查时要先盯紧常量字符串二是在Activity的onCreate里注册接收器但requestPermission调用太早广播发出来时接收器还没注册完成结果就丢了。我的做法是把广播接收器放在onResume里注册onPause里反注册并且requestPermission也放在onResume之后。这样既避免重复注册也确保接收器处于活跃状态。另一个稳妥的做法是直接用registerReceiver注册全局接收器不用LocalBroadcastManager因为USB权限广播是系统发出的不走应用内广播。4.2 设备拔掉后崩溃问题出在生命周期管理还有一个高频崩溃点是用户正在通信时把USB线拔了。这时候UsbDeviceConnection会立刻变失效再调用bulkTransfer或requestWait直接抛异常。建议在初始化时就注册ACTION_USB_DEVICE_DETACHED广播拔线时第一时间做三件事关闭connection、释放接口、退出读线程。我自己的写法是在接收器里维护一个volatile boolean usbConnected标志读线程每轮循环检查这个标志拔线后立刻退出循环并关闭连接。别指望系统帮你清理你有多个界面或服务引用同一个连接对象时不及时释放后续会引发一连串的“connection already closed”崩溃。另外重插设备后原来的UsbDevice对象可能已经失效必须重新调用getDeviceList()拉取新列表再走一遍权限流程。4.3 报告格式没对齐收发看着成功实则无效最头疼的一类问题代码全对连接也正常发送返回的长度正确但设备端就是没反应。这种问题九成以上是报告格式没对齐。比如有的固件要求每包数据第一个字节是Report ID通常从1开始有的要求第一字节固定填0作为占位还有的设备要求长度必须正好等于端点最大包长短了不处理。我们自己也踩过这个坑固件端配置了一个Report ID安卓这边一直没填设备端每次解析都把第一个数据字节当Report ID吃掉导致数据整体错位看起来“发送成功”但内容全错。后来我跟固件同事约定所有报告统一帧头第一字节固定Report ID第二字节是命令字后续才是业务数据。这个约定写进接口文档后通信问题直接少了一半。所以拿到外设后第一件事不是写代码是找设备端要报告格式定义哪怕对方说“就是简单的数据收发”也要把每一字节的含义问清楚。5. 验证与落地用wireshark抓USB通信并补上产品化要做的功课5.1 Linux下用usbmon和wireshark抓取USB通信先回答一个搜烂了的问题wireshark能抓USB通信吗答案是能但要在Linux环境下抓Windows下对USB协议层的支持始终比较麻烦。在Linux上先加载usbmon模块然后打开wireshark选择对应的usbmon接口。sudo modprobe usbmon sudo wiresharkwireshark的接口列表里会出现多个usbmonN分别对应不同的USB控制器和端口。如果不确定设备挂在哪个控制器下可以把设备拔插一次看哪个接口有新的URB记录产生基本就能锁定。抓包时建议先把设备拔掉开启抓包后再插上这样能把完整的枚举过程、配置过程、数据传输过程都录下来后续分析非常方便。5.2 抓包定位思路先从枚举过程确认端点和包长真正调试时我最常干的一件事是抓一遍枚举过程确认设备有几个配置、几个接口、每个端点是什么类型、最大包长是多少。在wireshark里过滤usb.device_address nn是设备地址重点看GET_DESCRIPTOR的响应里面包含了完整端点配置。这一步能帮你提前发现“我以为的端点”和“设备实际的端点”之间的差异。实际通信数据的分析过滤条件用usb.endpoint_address 0x81这种形式就能看到设备发给主机的所有中断输入报告。反过来看主机发给设备的数据过滤usb.endpoint_address 0x01输出端点地址。选一条URB记录展开Data字段能看到完整的载荷字节。这时候把安卓侧发送的字节和wireshark里抓到的字节放一起比对问题就能精确定位到应用层还是协议层。我通常用这个方法跟固件同事“对锚点”两边以同一包数据作为参照确认收发格式是否一致。5.3 从Demo到产品会被低估的几个细节如果只是验证可行性跑通上面的Demo就够了。但要真正放进产品里还有几个容易被低估的点。第一个是设备模式切换问题。有些HID设备有几种工作模式切换模式后设备会重新枚举VID/PID可能不变但接口配置变化之前保存的端点引用全部失效。应用侧要监听重新枚举事件并恢复状态。第二个是系统抢占问题。如果设备枚举成标准键盘系统输入管理器会直接消费中断报告你的App拿不到数据。解决办法还是回到协议本身设备端使用厂商自定义的Usage Page不要用标准键盘用途这样数据能绕开系统输入栈直接送到App。第三个是断线重连和多设备管理。做产品时通常要维护一个设备列表记录每个设备的序列号和对应的端点配置。拔插、休眠唤醒、系统广播优先级这些小问题都会在真机联调时一一暴露出来。建议在联调阶段就写一个简单的调试页面能实时显示当前设备状态、线程状态、最近收发字节的十六进制内容极大减少和硬件同事来回扯皮的时间。我自己到最后都是靠这个页面定位问题的比在wireshark里翻半天快得多。本文还有配套的精品资源点击获取