当前位置: 首页 > news >正文

Android串口通信实战:从零构建高效SerialPort工具类

1. 为什么需要串口通信工具类

在Android开发中,串口通信是个既常见又特殊的场景。你可能在工业控制、物联网设备、POS机等硬件交互场景中见过它的身影。我刚开始接触串口开发时,每次新项目都要重新写一遍基础代码,不仅效率低下,还容易在数据收发和线程管理上踩坑。

串口通信的核心痛点在于:它不像网络请求那样有成熟的框架可用。每次开发都要处理设备权限、波特率设置、数据流读写、线程同步等底层细节。更麻烦的是,调试时经常遇到数据丢失、线程阻塞的问题,这些问题在真机测试时才会暴露,调试成本很高。

举个例子,去年我在开发一个智能柜锁项目时,就因为没处理好数据接收线程的释放,导致APP在后台运行几小时后内存泄漏。后来通过封装工具类,统一管理线程生命周期,才彻底解决了这个问题。这也是为什么我们需要一个封装完善的SerialPort工具类——它能帮我们避开这些"坑",让开发效率提升至少50%。

2. 搭建开发环境

2.1 引入必备依赖

现在最流行的Android串口库是licheedev维护的android-serialport,它源自Google官方demo但做了大量优化。在app模块的build.gradle中添加:

dependencies { implementation 'com.licheedev:android-serialport:2.1.3' }

这个库已经帮我们处理了JNI层的串口操作,包括:

  • 设备文件打开/关闭
  • 波特率设置
  • 数据校验位配置
  • 流控制设置

注意:如果你的设备系统路径特殊,需要提前设置su路径。比如某些定制ROM会把su放在/system/xbin/下:

SerialPort.setSuPath("/system/xbin/su")

2.2 配置设备权限

Android串口通信需要root权限,通常有两种方案:

  1. 使用已root的设备
  2. 在系统镜像中预置su二进制文件

实测中发现,很多工业设备(如扫码枪、工控平板)出厂时已经配置好su权限。可以通过adb shell执行ls /dev/tty*查看可用串口设备,常见的有:

  • /dev/ttyS0 (通常对应RS232)
  • /dev/ttyUSB0 (USB转串口)
  • /dev/ttyAMA0 (蓝牙串口)

3. 核心工具类设计

3.1 线程模型设计

串口通信必须使用多线程——主线程不能阻塞在IO操作上。我们的工具类采用"双线程+Handler"架构:

class SerialPortUtil { // 发送线程:HandlerThread + Handler private lateinit var mSendingHandlerThread: HandlerThread private lateinit var mSendingHandler: Handler // 接收线程:自定义Thread private lateinit var mSerialPortReceivedThread: SerialPortReceivedThread // 数据流 private lateinit var mFileInputStream: FileInputStream private lateinit var mFileOutputStream: FileOutputStream }

这种设计有三大优势:

  1. 发送消息通过Handler排队,避免并发写入冲突
  2. 接收线程独立运行,实时性有保障
  3. 生命周期可控,避免内存泄漏

3.2 数据收发实现

发送数据时,我们采用异步消息机制:

fun sendBytes(bytes: ByteArray?): Boolean { try { Runnable { val message = Message.obtain() message.obj = bytes mSendingHandler.sendMessage(message) Thread.sleep(100) // 防止发送过快 }.run() } catch (e: Exception) { Log.e(TAG, "发送失败: ${e.message}") return false } return true }

接收数据则需要自定义线程持续监听:

private inner class SerialPortReceivedThread( private val inputStream: FileInputStream ) : Thread() { override fun run() { while (!isInterrupted) { try { val available = inputStream.available() if (available > 0) { val buffer = ByteArray(available) inputStream.read(buffer) mISerialPortDataListener.onDataReceived(buffer) } } catch (e: IOException) { Log.e(TAG, "接收异常: ${e.message}") break } } } }

4. 实战应用示例

4.1 基础使用流程

在Activity中集成串口功能只需四步:

class MainActivity : AppCompatActivity(), IOpenSerialPortListener, ISerialPortDataListener { override fun onCreate(savedInstanceState: Bundle?) { // 1. 设置su路径(可选) SerialPort.setSuPath("/system/bin/su") // 2. 配置监听器 SerialPortUtil.instance.apply { setIOpenSerialPortListener(this@MainActivity) setISerialPortDataListener(this@MainActivity) } // 3. 打开串口 SerialPortUtil.instance.open(File("/dev/ttyS3")) // 4. 发送指令 val command = byteArrayOf(0x01, 0x02, 0x03) SerialPortUtil.instance.sendBytes(command) } }

4.2 调试技巧分享

通过Logcat过滤串口日志时,建议使用如下tag:

  • SerialPort:底层JNI调用日志
  • SerialPortUtil:工具类操作日志
  • SerialPortReceivedThread:数据接收日志

典型问题排查流程:

  1. 检查su权限是否正常
  2. 确认设备路径是否正确
  3. 验证波特率是否匹配
  4. 检查数据校验位设置
  5. 用示波器确认物理信号

5. 性能优化建议

5.1 数据缓冲区优化

原始方案每次读取都创建新byte数组,可以改为循环缓冲区:

private val mReceiveBuffer = ByteArray(1024) override fun run() { while (!isInterrupted) { val readCount = inputStream.read(mReceiveBuffer) if (readCount > 0) { val data = mReceiveBuffer.copyOfRange(0, readCount) mISerialPortDataListener.onDataReceived(data) } } }

5.2 异常处理增强

增加以下异常处理场景:

  • 设备拔出时的IO异常
  • 权限变化的SecurityException
  • 波特率不匹配时的数据乱码
  • 线程中断时的资源释放
fun open(device: File) { try { serialPort = SerialPort.newBuilder(device, 115200) .parity(0) // 无校验 .build() // ...初始化流和线程 } catch (e: SecurityException) { Log.e(TAG, "权限异常,请检查root权限") } catch (e: IOException) { Log.e(TAG, "设备可能已拔出") } }

在实际项目中,这套工具类已经稳定运行在超过5000台工业设备上,日均处理200万+条指令。关键是要做好线程安全和资源释放,特别是在Activity的onDestroy中必须调用stop方法。

http://www.jsqmd.com/news/562361/

相关文章:

  • K 小数问题
  • 【实战】从零到一:基于Docker的雷池WAF社区版部署与反向代理配置
  • STM32 IAP实战:用串口+Flash Loader Demo实现远程固件升级(附完整代码)
  • 程序员必须掌握的核心算法思想
  • 别只盯着GPU:用DELL R720搭建深度学习Server,这些‘古董’配件才是关键
  • SQLServer数据库设计实战:主键、外键和约束的最佳实践
  • 网络调试神器 Netcat for Windows:你的命令行网络瑞士军刀
  • 3-30午夜盘思
  • 校园自助图书借阅系统 Java 项目开发与源码分享
  • C#开发必备:5种获取EXE路径的方法对比(附性能测试)
  • 基于谐振ESO的永磁同步电机dq轴死区6次谐波补偿:从原理到实践
  • 深入解析亚马逊SP-API Reports模块:如何高效处理大规模数据报告
  • 研发采购一肩挑,我为何锁定这家?新能源场站测试仪选屏避坑指南 - 浴缸里的巡洋舰
  • DRM驱动模块详解:从Plane到Connector的硬件抽象指南(附回调函数解析)
  • Flutter开发必看:Dart语法里那些新手最容易踩的5个坑(附避坑代码)
  • 突破百度网盘限速壁垒:KinhDown让文件传输重获自由
  • ARMv8-A实战:手把手教你用QEMU+GDB调试Linux内核异常处理流程
  • Kaggle HR Dataset Clean Raw (2M Rows)
  • 别再让信号‘打架’了!手把手教你用ADS仿真搞定PCB阻抗匹配(附实战案例)
  • 前端监控:让你的网站问题无处遁形
  • 【T6/T3】通过账套备份文件快速识别畅捷通软件版本的实用技巧
  • Android ConstraintLayout实战:5分钟搞定复杂布局的Barrier与Guideline技巧
  • 老牌报表工具iReport复活指南:在Win10/Win11上从下载到运行的完整流程
  • 用友EPM vs 蓝科:合并报表选型深度对比 - 冠融盈科
  • 从电影帧率到无线通信:用生活化案例理解TDMA时分多址原理
  • 车载测试工程师技能进阶图谱:从协议解析到架构设计
  • Heltec ESP32 LoRa v3:轻松实现远距离无线通信的物联网开发板
  • 从官方Demo到自己的工程:手把手移植紫光PCIe DMA模块(附信号连接图)
  • 不只是游戏引擎:用Axmol 2.11.0的跨平台能力,快速构建一个轻量级多媒体演示App
  • 蓝科(LucaNet)怎么样?5家EPM厂商真实对比 - 冠融盈科