在工业PDA、零售收银等场景中,USB扫码枪是常见的数据采集设备。不同于蓝牙扫码枪需要配对连接,USB扫码枪即插即用,稳定性更高。本文将详细介绍在Android平台上通过USB转串口方案实现扫码枪数据接收的全过程。
核心实现原理:扫码枪通过USB接口模拟串口设备,Android应用通过USB Host模式与设备通信,监听串口数据并解析条码信息。相比HID模式(键盘输入方式),串口模式具有以下优势:
在app/build.gradle中添加USB串口通信库:
groovy复制implementation 'com.github.mik3y:usb-serial-for-android:3.7.0'
注意:该库支持市面上主流的USB转串口芯片,包括FTDI、CDC、CP210x等驱动。
xml复制<!-- 声明使用USB主机模式 -->
<uses-feature android:name="android.hardware.usb.host" android:required="true"/>
<!-- USB设备访问权限 -->
<uses-permission android:name="android.permission.USB_PERMISSION" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.MANAGE_USB"
tools:ignore="ProtectedPermissions" />
<!-- USB设备插入自动启动 -->
<activity android:name=".ui.MainActivity">
<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/usb_device_filter" />
</activity>
在res/xml/usb_device_filter.xml中定义支持的设备:
xml复制<resources>
<!-- 常见USB转串口芯片 -->
<usb-device vendor-id="6790" product-id="29987" /> <!-- CH340 -->
<usb-device vendor-id="1659" product-id="8963" /> <!-- PL2303 -->
<usb-device vendor-id="4292" product-id="60000" /> <!-- CP2102 -->
<usb-device vendor-id="1027" product-id="24577" /> <!-- FTDI -->
</resources>
技巧:通过ScannerManager.printAllUsbDevices()可打印已连接设备的VID/PID,方便添加到过滤列表。
kotlin复制class ScannerManager(context: Context) {
private val BAUD_RATE = 9600 // 常见扫码枪波特率
private val CONNECT_RETRY_DELAY = 1000L
private val MAX_RETRY_COUNT = 3
// USB管理相关
private val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
private var usbSerialPort: UsbSerialPort? = null
private var connection: UsbDeviceConnection? = null
// 数据回调
private var scanCallback: ((String) -> Unit)? = null
private val stringBuilder = StringBuilder()
private val serialListener = object : SerialInputOutputManager.Listener {
override fun onNewData(data: ByteArray) {
val receivedString = String(data, Charset.forName("GBK"))
processReceivedData(receivedString)
}
override fun onRunError(e: Exception) {
Log.e(TAG, "串口错误: ${e.message}")
reconnect()
}
}
private fun processReceivedData(data: String) {
for (char in data) {
when (char) {
'\r', '\n' -> { // 条码结束符
if (stringBuilder.isNotEmpty()) {
val barcode = stringBuilder.toString().trim()
scanCallback?.invoke(barcode)
stringBuilder.clear()
}
}
else -> stringBuilder.append(char)
}
}
}
}
kotlin复制fun connectToDevice(device: UsbDevice) {
try {
// 1. 获取驱动
val driver = UsbSerialProber.getDefaultProber().probeDevice(device)
?: throw Exception("驱动获取失败")
// 2. 建立连接
connection = usbManager.openDevice(device)
?: throw Exception("连接建立失败")
// 3. 打开端口
usbSerialPort = driver.ports[0].apply {
open(connection)
setParameters(BAUD_RATE, 8, 1, 0) // 波特率9600,8数据位,1停止位,无校验
}
// 4. 启动数据监听
SerialInputOutputManager(usbSerialPort, serialListener).apply {
Executors.newSingleThreadExecutor().submit(this)
}
} catch (e: Exception) {
Log.e(TAG, "连接失败: ${e.message}")
reconnect()
}
}
kotlin复制private var retryCount = 0
private fun reconnect() {
if (retryCount < MAX_RETRY_COUNT) {
retryCount++
Handler(Looper.getMainLooper()).postDelayed({
findAndConnectScanner()
}, CONNECT_RETRY_DELAY)
} else {
Log.e(TAG, "达到最大重试次数")
}
}
kotlin复制class MainActivity : AppCompatActivity() {
private lateinit var scannerManager: ScannerManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
scannerManager = ScannerManager(this).apply {
init()
setScanCallback { barcode ->
runOnUiThread {
findViewById<TextView>(R.id.tv_barcode).text = barcode
}
}
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent?.action == UsbManager.ACTION_USB_DEVICE_ATTACHED) {
Handler().postDelayed({
scannerManager.findAndConnectScanner()
}, 500)
}
}
}
kotlin复制override fun onResume() {
super.onResume()
if (!scannerManager.isConnected()) {
scannerManager.findAndConnectScanner()
}
}
override fun onDestroy() {
scannerManager.release()
super.onDestroy()
}
对于系统级应用,可通过Root权限绕过常规权限检查:
kotlin复制private fun grantUsbPermissionByRoot(device: UsbDevice): Boolean {
return try {
val devicePath = device.deviceName
Runtime.getRuntime().exec("su -c chmod 666 $devicePath").waitFor() == 0
} catch (e: Exception) {
false
}
}
通过记录设备VID/PID实现多设备管理:
kotlin复制private val connectedDevices = mutableMapOf<String, UsbSerialPort>()
fun addDevice(device: UsbDevice) {
val key = "${device.vendorId}:${device.productId}"
if (!connectedDevices.containsKey(key)) {
// 建立新连接
}
}
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 找不到设备 | 1. 未启用USB Host模式 2. 设备未在过滤列表中 |
1. 检查AndroidManifest配置 2. 打印设备VID/PID并更新usb_device_filter.xml |
| 权限被拒绝 | 1. 未请求权限 2. 用户拒绝授权 |
1. 调用requestPermission() 2. 引导用户授权或使用Root方案 |
| 数据接收不全 | 1. 波特率不匹配 2. 编码格式错误 |
1. 确认扫码枪波特率设置 2. 尝试GBK/UTF-8等编码 |
kotlin复制usbManager.deviceList.values.forEach { device ->
Log.d("USB", "设备: ${device.deviceName}, VID=${device.vendorId}, PID=${device.productId}")
}
kotlin复制serialListener = object : SerialInputOutputManager.Listener {
override fun onNewData(data: ByteArray) {
Log.d("RAW", HexDump.dumpHexString(data))
}
}
连接稳定性:
数据处理:
资源管理:
实际项目中,我们通过这套方案在工业PDA上实现了每秒30+次扫码的稳定采集。关键点在于波特率匹配、正确的编码处理以及稳健的错误恢复机制。对于特殊字符条码,建议使用Hex模式调试确认原始数据格式。