手把手教你用Android实现OTG外设通信:从U盘读写到扫码枪接入
你有没有想过,你的安卓手机不仅能充电、上网,还能像电脑一样插U盘、接键盘、连扫码枪?这并不是什么黑科技,而是早已内置于Android系统中的USB On-The-Go(OTG)功能。在仓储盘点、医疗设备、工业PDA甚至智能收银台中,这项技术正悄悄改变着移动终端的能力边界。
今天,我们就来彻底拆解Android下的OTG主机模式开发——不讲空话,不堆概念,只给你能跑起来的代码和踩过坑后的实战经验。
为什么是OTG?移动设备的“类PC化”之路
过去,我们用手机拍照片、刷视频;现在,越来越多场景要求它承担起“现场工作站”的角色:巡检人员要导出传感器数据,药店需要扫描药品条码,工厂工人得把检测结果存进U盘带回办公室……这些任务如果每次都靠Wi-Fi上传或蓝牙传输,不仅慢,还容易受环境干扰。
而OTG的出现,让这一切变得简单直接:一根小小的转接线,就能让你的平板变身迷你主机,即插即用U盘、键盘、鼠标、串口设备等标准USB外设。
✅关键优势一句话总结:
高速 + 低延迟 + 兼容性强 + 无需配对,特别适合对稳定性要求高的专业场景。
从Android 3.1(API Level 12)开始,系统原生支持OTG Host Mode,通过UsbManager暴露接口,开发者可以完全掌控外设连接与数据交互全过程。
核心机制揭秘:当一个U盘插入时,系统到底做了什么?
当你把U盘插入带OTG功能的安卓设备时,背后其实发生了一连串精密协作:
- 硬件检测:Type-C或Micro USB的ID引脚被拉低,触发内核驱动(如dwc2)识别为“有设备接入”;
- 设备枚举:系统向U盘发送请求,获取它的厂商ID(VID)、产品ID(PID)、设备类(Class)、端点信息等;
- 广播通知:Android发出
ACTION_USB_DEVICE_ATTACHED广播; - 权限协商:App收到广播后,调用
requestPermission()弹出授权对话框; - 建立连接:用户点击“允许”,App获得
UsbDeviceConnection,可进行读写操作。
整个流程由Linux USB子系统支撑,上层应用只需关注业务逻辑即可。
UsbManager:一切的起点
所有OTG开发的第一步,都是获取这个系统服务:
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);有了它,你可以做三件最重要的事:
- 查看当前已连接的设备列表
- 监听插拔事件
- 请求访问权限
如何监听设备插入?
必须注册一个广播接收器,并动态注册Intent Filter(静态注册在部分机型上无效):
private static final String ACTION_USB_PERMISSION = "com.example.otgdemo.USB_PERMISSION"; BroadcastReceiver usbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device != null && intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { // 用户点了“允许” connectToDevice(device); } } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); handleDeviceAttached(device); // 检查是否为目标设备 } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); cleanupDeviceResources(device); // 释放资源 } } }; // 注册广播 IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); registerReceiver(usbReceiver, filter);⚠️注意Android 12+兼容性问题:
如果你的目标SDK >= 31,记得给PendingIntent加上标志位:
PendingIntent.FLAG_MUTABLE否则权限请求不会弹窗!
实战一:读取U盘文件内容(无需root)
很多人以为Android不能读U盘是因为权限限制,其实更大的问题是——系统并不自动挂载FAT/exFAT格式的U盘。即使你能看到设备,也无法像PC那样直接访问/mnt/media_rw/...路径。
那怎么办?答案是:自己解析文件系统。
这里推荐使用开源库libaums,它封装了完整的USB Mass Storage协议栈,支持SCSI命令、CBW/CSW帧处理、分区表解析和FAT32/exFAT读写。
添加依赖
implementation 'com.github.mjdev:libaums:0.9.0'连接并读取U盘根目录
public void openUsbDrive(UsbDevice device) { UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); try { // 创建通信通道 UsbCommunication communication = new UsbCommunicationFactory(manager, device).create(); // 包装成块设备 BlockDevice blockDevice = new UsbBlockDevice(communication); blockDevice.init(); // 解析主引导记录(MBR),取第一个有效分区 Partition partition = new MBRPartition(blockDevice).getPartitions().get(0); // 初始化文件系统 FileSystem fs = FAT32FileSystem.read(partition); // 列出根目录文件 List<FileSystemObject> files = fs.getRootDirectory().getList(); for (FileSystemObject f : files) { Log.d("OTG", "文件名: " + f.getName() + ", 大小: " + f.getLength()); } // 读取某个文本文件 FsFile targetFile = fs.getRootDirectory().search("log.txt"); if (targetFile != null) { InputStream is = targetFile.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String line; while ((line = reader.readLine()) != null) { Log.d("FILE_CONTENT", line); } reader.close(); } } catch (IOException e) { e.printStackTrace(); } }💡提示:libaums目前只支持读写,不支持创建新文件系统或格式化。但对于绝大多数数据导入/导出需求已经足够。
实战二:接入HID设备(键盘、扫码枪)
HID(Human Interface Device)是最常见的USB设备类型之一。有趣的是,很多条码扫描枪本质上就是一台“会打字的键盘”,它们以HID身份接入后,直接模拟按键输入。
这意味着:只要你禁用软键盘,就可以让扫码内容自动填入输入框,体验丝滑流畅。
但如果你想自定义行为呢?比如区分普通键盘和专用扫码枪?或者提取原始数据包做二次处理?
那就需要手动打开设备,监听中断端点。
手动读取HID设备数据流
public void readHidDevice(UsbDevice device, UsbDeviceConnection connection) { UsbInterface hidInterface = device.getInterface(0); if (!connection.claimInterface(hidInterface, true)) { Log.e("OTG", "无法声明接口"); return; } UsbEndpoint interruptIn = null; for (int i = 0; i < hidInterface.getEndpointCount(); i++) { UsbEndpoint ep = hidInterface.getEndpoint(i); if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_INT && ep.getDirection() == UsbConstants.USB_DIR_IN) { interruptIn = ep; break; } } if (interruptIn == null) { Log.e("OTG", "未找到IN方向的中断端点"); return; } new Thread(() -> { byte[] buffer = new byte[interruptIn.getMaxPacketSize()]; while (true) { int ret = connection.bulkTransfer(interruptIn, buffer, buffer.length, 1000); if (ret > 0) { parseHidReport(Arrays.copyOf(buffer, ret)); } else if (ret == -1) { break; // 错误或断开 } } }).start(); }解析键盘报告(简化版)
private void parseHidReport(byte[] report) { byte modifier = report[0]; // Shift/Ctrl等修饰键 byte[] keyCodes = Arrays.copyOfRange(report, 2, 8); // 健盘码数组 StringBuilder sb = new StringBuilder(); for (byte code : keyCodes) { if (code != 0) { String key = UsbHidKeycodeMapper.keycodeToString(code); sb.append(key).append(" "); } } if (sb.length() > 0) { Log.d("HID_INPUT", "输入: " + sb.toString().trim()); } }📌应用场景举例:
- 条码扫描枪连续扫多个码时,每个码末尾通常带回车符,可用于触发提交动作。
- 游戏手柄可通过HID上报摇杆坐标和按钮状态,实现本地控制。
常见问题与避坑指南
别急着上线,先看看这些你一定会遇到的问题:
| 问题 | 原因分析 | 解决方案 |
|---|---|---|
| 插上没反应 | OTG线质量问题 | 使用带芯片的主动式OTG线,避免“只能充电”的劣质线材 |
| 权限请求不弹窗 | PendingIntent未设置FLAG_MUTABLE | Android 12+必须添加.setFlag(PendingIntent.FLAG_MUTABLE) |
| U盘识别但读不出来 | 分区表异常或exFAT不支持 | 使用libaums手动解析;确保U盘格式为FAT32 |
| 扫码枪输入重复 | 系统默认注入 + 自己又读了一遍 | 若使用手动读取,关闭系统HID映射(需root或定制ROM) |
| 设备供电不足 | OTG输出电流有限(一般≤500mA) | 高功耗设备外接电源,或使用带供电的集线器 |
最佳实践建议
- 设备过滤更精准
在res/xml/device_filter.xml中指定只监听特定VID/PID:
```xml
```
并在AndroidManifest.xml中关联:
xml <intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" />
避免内存泄漏
每次断开都要及时调用connection.close()和unregisterReceiver()。提升用户体验
可预先缓存已授权设备,在重启后尝试自动重连(仍需用户确认)。多品牌适配测试不可少
华为、小米、三星等厂商对OTG支持程度不同,务必实机测试。
写在最后:OTG不只是“插U盘”那么简单
掌握OTG开发,意味着你能让Android设备真正成为一个通用数据枢纽。无论是:
- 工业现场快速导出日志文件,
- 零售门店构建低成本POS系统,
- 教育场景实现作业批量提交,
- 辅助设备接入物理键盘帮助视障人士,
……都有它的用武之地。
随着USB Type-C成为主流,PD快充协议普及,未来的OTG甚至可能支持反向供电、多设备级联、音视频同步传输等功能。今天的底层能力积累,正是为了迎接那一天的到来。
如果你在项目中实现了有趣的OTG功能,欢迎留言交流!我们一起推动移动嵌入式开发的边界。