前言:公司属于北斗通信行业,项目大多都需要和各式各样的硬件设备相结合来满足项目需求,因此所涉及到的各种技术也相对比较冷门。前段时间有个项目用到了一款定制 Android 设备,其中有多个接口,包括两个 USB 接口和一个 RS232 串口,需要用到其中一个 USB 连接北斗设备实现指令互通,经过摸索现在也大致了解了 Android USB(host)通信流程后续还有另一个项目则用到了 USB(accessory)有空再写一篇。在这里记录和分享一下,有错漏或可优化之处欢迎大家留言。
一、导入模块
工具基于 github 上的串口工具库:usb-serial-for-android
1. 在项目级 build.gradle 文件添加 jitpack.io 库
maven { url 'https://jitpack.io' }
2. 添加依赖
implementation 'com.github.mik3y:usb-serial-for-android:3.4.6' 可能有更新的版本自行修改
3. 本地
2023年11月30日更新:最近把代码拉到家里的电脑准备在家办公时恰好停网了导致第三方库导入不了,于是就到工具库源码上把代码都拉到了本地,如果你也恰好遇到了这种情况也可以这样处理。这里顺手打包了一份,自己重新导入一下即可:https://download.csdn.net/download/lxt1292352578/88565529
二、上代码
DEMO:https://github.com/LXTTTTTT/USBtoSerialPortDemo
源码资源:https://download.csdn.net/download/lxt1292352578/88717549
复制再把报红的部分直接去掉或者换成自己的就能直接使用
package com.example.SecondProject.Utils.Transfer.USB; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbManager; import android.os.Build; import android.util.Log; import android.widget.Toast; import com.example.SecondProject.Base.MainApplication; import com.example.SecondProject.BuildConfig; import com.example.SecondProject.Global.Constant; import com.example.SecondProject.Global.Variable; import com.example.SecondProject.Utils.DataUtil; import com.example.SecondProject.Utils.NotificationCenter; import com.example.SecondProject.Utils.ProtocolUtil; import com.hoho.android.usbserial.driver.UsbSerialDriver; import com.hoho.android.usbserial.driver.UsbSerialPort; import com.hoho.android.usbserial.driver.UsbSerialProber; import com.hoho.android.usbserial.util.SerialInputOutputManager; import java.io.IOException; import java.util.ArrayList; import java.util.List; // usb 数据传输工具 public class USBTransferUtil { private String TAG = "USBTransferUtil"; private MainApplication APP = MainApplication.getInstance(); // 主程序,替换为你自己的 private UsbManager manager = (UsbManager) APP.getSystemService(Context.USB_SERVICE); // usb管理器 private BroadcastReceiver usbReceiver; // 广播监听:判断usb设备授权操作 private static final String INTENT_ACTION_GRANT_USB = BuildConfig.APPLICATION_ID + ".INTENT_ACTION_GRANT_USB"; // usb权限请求标识 private final String IDENTIFICATION = " USB-Serial Controller D"; // 目标设备标识 private List<UsbSerialDriver> availableDrivers = new ArrayList<>(); // 所有可用设备 private UsbSerialDriver usbSerialDriver; // 当前连接的设备 private UsbDeviceConnection usbDeviceConnection; // 连接对象 private UsbSerialPort usbSerialPort; // 设备端口对象,通过这个读写数据 private SerialInputOutputManager inputOutputManager; // 数据输入输出流管理器 // 连接参数,按需求自行修改 --------- private int baudRate = 115200; // 波特率 private int dataBits = 8; // 数据位 private int stopBits = UsbSerialPort.STOPBITS_1; // 停止位 private int parity = UsbSerialPort.PARITY_NONE;// 奇偶校验 // 单例 ------------------------- private static USBTransferUtil usbTransferUtil; public static synchronized USBTransferUtil getInstance() { if(usbTransferUtil == null){ usbTransferUtil = new USBTransferUtil(); } return usbTransferUtil; } public void connect(){ // “Variable.isConnectUSB” 我的变量标识,自行删除或修改 if(!Variable.isConnectUSB){ registerReceiver(); // 注册广播监听 refreshDevice(); // 拿到已连接的usb设备列表 connectDevice(); // 建立连接 } } // 注册usb授权监听广播 public void registerReceiver(){ usbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if(INTENT_ACTION_GRANT_USB.equals(intent.getAction())) { // 授权操作完成,连接 // boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); // 不知为何获取到的永远都是 false 因此无法判断授权还是拒绝 connectDevice(); } } }; APP.registerReceiver(usbReceiver,new IntentFilter(INTENT_ACTION_GRANT_USB)); } // 刷新当前可用 usb设备 public void refreshDevice(){ availableDrivers.clear(); availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager); Log.e(TAG, "当前可用 usb 设备数量: " + availableDrivers.size() ); // 有设备可以连接 if(availableDrivers.size() != 0){ // 当时开发用的是定制平板电脑有 2 个usb口,所以搜索到两个 if(availableDrivers.size()>1){ for (int i = 0; i < availableDrivers.size(); i++) { UsbSerialDriver availableDriver = availableDrivers.get(i); // 我是通过 ProductName 这个参数来识别我要连接的设备 if(availableDriver.getDevice().getProductName().equals(IDENTIFICATION)){ usbSerialDriver = availableDriver; } } } // 通常手机只有充电口 1 个 else { usbSerialDriver = availableDrivers.get(0); } usbSerialPort = usbSerialDriver.getPorts().get(0); // 一般设备的端口都只有一个,具体要参考设备的说明文档 // 同时申请设备权限 if(!manager.hasPermission(usbSerialDriver.getDevice())){ int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0; PendingIntent usbPermissionIntent = PendingIntent.getBroadcast(APP, 0, new Intent(INTENT_ACTION_GRANT_USB), flags); manager.requestPermission(usbSerialDriver.getDevice(), usbPermissionIntent); } } // 没有设备 else { APP.showToast("请先接入设备",0); } } // 连接设备 public void connectDevice(){ if(usbSerialDriver == null || inputOutputManager != null){return;} // 判断是否拥有权限 boolean hasPermission = manager.hasPermission(usbSerialDriver.getDevice()); if(hasPermission){ usbDeviceConnection = manager.openDevice(usbSerialDriver.getDevice()); // 拿到连接对象 if(usbSerialPort == null){return;} try { usbSerialPort.open(usbDeviceConnection); // 打开串口 usbSerialPort.setParameters(baudRate, dataBits, stopBits, parity); // 设置串口参数:波特率 - 115200 , 数据位 - 8 , 停止位 - 1 , 奇偶校验 - 无 startReceiveData(); // 开启读数据线程 } catch (IOException e) { e.printStackTrace(); } }else { APP.showToast("请先授予权限再连接",0); } } // 开启数据接收监听 public void startReceiveData(){ if(usbSerialPort == null || !usbSerialPort.isOpen()){return;} inputOutputManager = new SerialInputOutputManager(usbSerialPort, new SerialInputOutputManager.Listener() { @Override public void onNewData(byte[] data) { // 在这里处理接收到的 usb 数据 String data_str = DataUtil.bytes2string(data).toUpperCase(); Log.e(TAG, "收到 usb 数据: " + data_str); } @Override public void onRunError(Exception e) { Log.e(TAG, "usb 断开了" ); disconnect(); e.printStackTrace(); } }); inputOutputManager.start(); Variable.isConnectUSB = true; // 修改连接标识 NotificationCenter.standard().postNotification(Constant.CONNECT_USB); // 发送全局广播 APP.showToast("连接成功" ,Toast.LENGTH_SHORT); } // 下发数据 public void write(String data_hex){ if(usbSerialPort != null){ Log.e(TAG, "当前usb状态: isOpen-" + usbSerialPort.isOpen() ); // 当串口打开时再下发 if(usbSerialPort.isOpen()){ byte[] data_bytes = DataUtil.hex2bytes(data_hex); // 将字符数据转化为 byte[] if (data_bytes == null || data_bytes.length == 0) return; try { usbSerialPort.write(data_bytes,100); // 写入数据 } catch (IOException e) { e.printStackTrace(); } }else { APP.showToast(" usb 未连接" ,Toast.LENGTH_SHORT); } } } // 断开连接 public void disconnect(){ try{ // 停止数据接收监听 if(inputOutputManager != null){ inputOutputManager.stop(); inputOutputManager = null; } // 关闭端口 if(usbSerialPort != null){ usbSerialPort.close(); usbSerialPort = null; } // 关闭连接 if(usbDeviceConnection != null){ usbDeviceConnection.close(); usbDeviceConnection = null; } // 清除设备 if(usbSerialDriver != null){ usbSerialDriver = null; } // 清空设备列表 availableDrivers.clear(); // 注销广播监听 APP.unregisterReceiver(usbReceiver); Variable.isConnectUSB = false; // 修改标识 NotificationCenter.standard().postNotification(Constant.DISCONNECT_USB); // 发送广播 APP.showToast("断开连接",0); }catch (Exception e){ e.printStackTrace(); } } // 下发初始化指令 public void init_device(){ try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } write(ProtocolUtil.CCICR(0,"00")); // 查询 IC 信息 } }
三、使用例子
1. 使用场景
原本项目设备使用的是左边这种标准的 USB 转串口线连接北斗设备,但是现在手头没有当时的设备,恰好有一条右图这种 type-c 转串口线,那就用手机和电脑连接模拟使用场景吧
连接之后是这个样子的
手机接口被占用了,这时可以使用无线调试,附个无线调试教程:https://blog.csdn.net/lxt1292352578/article/details/131954052
2. 使用案例
电脑需要准备一个串口调试工具,注意需要把串口参数设置为相同
附个串口调试工具:https://download.csdn.net/download/lxt1292352578/88595528
直接调用连接设备方法:USBTransferUtil.getInstance().connect();
注意首次连接需要手动授权
连接成功后在电脑上使用串口调试助手发送数据模拟互通过程
下发数据方法:USBTransferUtil.getInstance().write("363636");
由于我下发北斗指令时使用的是16进制字符,有其他需求自行在方法里面修改
接收数据的处理:直接在工具类内部处理,不知道是因为设备的不同还是转接线的不同,这里接收到的数据是碎片化的而不像我在开发时收到的是完整的数据,恰好之前开发串口连接北斗设备时也是这种碎片化数据,之后有空的时候顺便做一个北斗协议拼接/解析教程吧
互通成功
3. 设备接入监听
Android系统在每次拔插 USB 设备时都会广播一个意图,这样如果我们需要在 USB 设备连接时进行某种操作只需要在 manifest 文件里面给对应的 activty 添加一个声明并指定过滤规则即可
<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" />
在xml资源文件夹中添加 device_filter 文件
附上过滤规则文件代码:这里包含了大部分USB设备的厂商的厂商ID和产品ID,如有别的需求自行增加
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingDefaultResource"> <!-- 0x0403 / 0x60??: FTDI --> <usb-device vendor-id="1027" product-id="24577" /> <!-- 0x6001: FT232R --> <usb-device vendor-id="1027" product-id="24592" /> <!-- 0x6010: FT2232H --> <usb-device vendor-id="1027" product-id="24593" /> <!-- 0x6011: FT4232H --> <usb-device vendor-id="1027" product-id="24596" /> <!-- 0x6014: FT232H --> <usb-device vendor-id="1027" product-id="24597" /> <!-- 0x6015: FT230X, FT231X, FT234XD --> <!-- 0x10C4 / 0xEA??: Silabs CP210x --> <usb-device vendor-id="4292" product-id="60000" /> <!-- 0xea60: CP2102 and other CP210x single port devices --> <usb-device vendor-id="4292" product-id="60016" /> <!-- 0xea70: CP2105 --> <usb-device vendor-id="4292" product-id="60017" /> <!-- 0xea71: CP2108 --> <!-- 0x067B / 0x23?3: Prolific PL2303x --> <usb-device vendor-id="1659" product-id="8963" /> <!-- 0x2303: PL2303HX, HXD, TA, ... --> <usb-device vendor-id="1659" product-id="9123" /> <!-- 0x23a3: PL2303GC --> <usb-device vendor-id="1659" product-id="9139" /> <!-- 0x23b3: PL2303GB --> <usb-device vendor-id="1659" product-id="9155" /> <!-- 0x23c3: PL2303GT --> <usb-device vendor-id="1659" product-id="9171" /> <!-- 0x23d3: PL2303GL --> <usb-device vendor-id="1659" product-id="9187" /> <!-- 0x23e3: PL2303GE --> <usb-device vendor-id="1659" product-id="9203" /> <!-- 0x23f3: PL2303GS --> <!-- 0x1a86 / 0x?523: Qinheng CH34x --> <usb-device vendor-id="6790" product-id="21795" /> <!-- 0x5523: CH341A --> <usb-device vendor-id="6790" product-id="29987" /> <!-- 0x7523: CH340 --> <!-- CDC driver --> <usb-device vendor-id="9025" /> <!-- 0x2341 / ......: Arduino --> <usb-device vendor-id="5824" product-id="1155" /> <!-- 0x16C0 / 0x0483: Teensyduino --> <usb-device vendor-id="1003" product-id="8260" /> <!-- 0x03EB / 0x2044: Atmel Lufa --> <usb-device vendor-id="7855" product-id="4" /> <!-- 0x1eaf / 0x0004: Leaflabs Maple --> <usb-device vendor-id="3368" product-id="516" /> <!-- 0x0d28 / 0x0204: ARM mbed --> <usb-device vendor-id="1155" product-id="22336" /><!-- 0x0483 / 0x5740: ST CDC --> <usb-device vendor-id="11914" product-id="5" /> <!-- 0x2E8A / 0x0005: Raspberry Pi Pico Micropython --> <usb-device vendor-id="11914" product-id="10" /> <!-- 0x2E8A / 0x000A: Raspberry Pi Pico SDK --> <usb-device vendor-id="6790" product-id="21972" /><!-- 0x1A86 / 0x55D4: Qinheng CH9102F --> </resources>
通过声明以上的intent-filter和meta-data,表明它是一个能够处理USB设备连接事件的Activity,并且根据res/xml/device_filter.xml中的规则对连接的USB设备进行过滤和处理。这样,在Android设备连接USB设备时,系统就会将相应的事件发送给Activity,从而实现对USB设备的交互和数据处理。比如我在这个 activity 的 onResume 生命周期添加连接USB设备设备操作,这样当系统发送了USB设备接入通知并授权成功后就能直接跳转到这个 activity 并进行后续操作,简单粗暴。
系统监测到 USB 设备接入并发送通知,授权并跳转至对应 activity :
四、小结
整个连接流程大致是这样的:
获取当前系统可用的 USB 设备列表 → 选中对应的USB设备并申请权限(首次)→ 获取设备端口(通常只有一个)→ 按照特定参数打开端口
具体操作参考代码,参考的项目是上文提到的工具库官方demo:
https://github.com/mik3y/usb-serial-for-android/tree/master
我的demo:https://github.com/LXTTTTTT/USBtoSerialPortDemo
源码资源:https://download.csdn.net/download/lxt1292352578/88717549
原工具库官方demo代码比较复杂,我只是按照自己的需求把对应的部分整理出来所以比较简单。由于这个功能用的不多所以也没有做过多的封装几乎所有的连接/通信操作都直接写在工具类里面,如果有需求自行改造即可。