跳到主要内容

串口通讯模块

📋 模块概述

串口通讯模块提供与外部串口设备的UART通信能力,基于第三方串口库NormalSerial封装实现,支持异步数据接收和线程安全操作。

核心类: android.znhaas.util.SerialPort

依赖库: com.vi.vioserial.NormalSerial

🎯 主要功能

  • ✅ 串口打开/关闭
  • ✅ 异步数据接收
  • ✅ 数据发送
  • ✅ 线程安全操作
  • ✅ 状态检查(防止重复开启/关闭)
  • ✅ 异常处理和日志记录

🏗️ 架构设计

线程安全设计

使用AtomicBoolean确保关闭操作的线程安全:

class SerialPort : OnNormalDataListener {
private val isClosing = AtomicBoolean(false)

// 底层状态检查
private fun isNativeOpen(): Boolean {
return try {
NormalSerial.instance().isOpen
} catch (e: Exception) {
Log.w(TAG, "检查底层状态异常: ${e.message}")
false
}
}
}

数据监听机制

实现OnNormalDataListener接口接收异步数据:

override fun onNormalDataReceived(data: ByteArray?, size: Int) {
if (data != null && size > 0) {
val hexString = bytesToHex(data, size)
val asciiString = String(data, 0, size, Charsets.UTF_8)

Log.d(TAG, "收到数据 HEX: $hexString")
Log.d(TAG, "收到数据 ASCII: $asciiString")

onDataReceived?.invoke(asciiString)
}
}

📚 核心API

1. 打开串口

open(onDataReceived: (String) -> Unit, portStr: String = "/dev/ttyHS1", ibaudRate: Int = 9600): Boolean

打开串口并注册数据接收回调。

参数:

  • onDataReceived: 数据接收回调函数,参数为接收到的字符串
  • portStr: 串口设备路径,默认"/dev/ttyHS1"
  • ibaudRate: 波特率,默认9600

返回值:

  • true: 打开成功
  • false: 打开失败

常用波特率:

  • 9600 (默认)
  • 19200
  • 38400
  • 57600
  • 115200

使用示例:

val serialPort = SerialPort()

val success = serialPort.open(
onDataReceived = { data ->
Log.d(TAG, "收到串口数据: $data")
handleSerialData(data)
},
portStr = "/dev/ttyHS1",
ibaudRate = 115200
)

if (success) {
Log.d(TAG, "串口打开成功")
} else {
Log.e(TAG, "串口打开失败")
}

自动重复开启防护:

// 第一次打开
serialPort.open({ data -> Log.d(TAG, data) }) // 成功

// 重复调用会被自动跳过
serialPort.open({ data -> Log.d(TAG, data) }) // 输出警告: "串口已连接,跳过重复开启"

2. 发送数据

send(data: String): Boolean

发送字符串数据。

参数:

  • data: 要发送的字符串

返回值:

  • true: 发送成功
  • false: 发送失败

使用示例:

// 发送文本命令
val success = serialPort.send("COMMAND:START\n")
if (success) {
Log.d(TAG, "命令发送成功")
} else {
Log.e(TAG, "命令发送失败")
}

// 发送JSON数据
val json = JSONObject().apply {
put("type", "status")
put("value", "online")
}.toString()
serialPort.send(json)

send(data: ByteArray): Boolean

发送字节数据。

参数:

  • data: 要发送的字节数组

返回值:

  • true: 发送成功
  • false: 发送失败

使用示例:

// 发送二进制数据
val hexData = byteArrayOf(0xAA.toByte(), 0x55.toByte(), 0x01, 0x02, 0x03)
serialPort.send(hexData)

// 发送字符串转字节
val textData = "HELLO".toByteArray(Charsets.UTF_8)
serialPort.send(textData)

3. 关闭串口

close()

关闭串口连接并清理监听器。

特性:

  • 只移除监听器,不关闭底层连接
  • 防止重复关闭
  • 线程安全

使用示例:

// 正常关闭
serialPort.close()

// 多次调用安全
serialPort.close() // 自动跳过,输出: "串口已关闭或正在关闭中"

4. 状态检查

isOpen(): Boolean

检查串口是否已打开。

返回值:

  • true: 串口已打开
  • false: 串口未打开

使用示例:

if (serialPort.isOpen()) {
// 串口已打开,可以发送数据
serialPort.send("DATA")
} else {
// 串口未打开,需要先打开
serialPort.open({ data -> Log.d(TAG, data) })
}

🔧 完整使用示例

基础串口通信

class SerialCommunicationActivity : AppCompatActivity() {

private val serialPort = SerialPort()
private val TAG = "SerialComm"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_serial_comm)

// 打开串口
openSerialPort()

// 发送按钮
sendButton.setOnClickListener {
sendData()
}
}

private fun openSerialPort() {
val success = serialPort.open(
onDataReceived = { data ->
runOnUiThread {
// 更新UI显示接收到的数据
receivedDataTextView.append(data + "\n")
Log.d(TAG, "收到数据: $data")

// 处理接收到的数据
handleReceivedData(data)
}
},
portStr = "/dev/ttyHS1",
ibaudRate = 115200
)

if (success) {
statusTextView.text = "串口已打开"
statusTextView.setTextColor(Color.GREEN)
sendButton.isEnabled = true
} else {
statusTextView.text = "串口打开失败"
statusTextView.setTextColor(Color.RED)
sendButton.isEnabled = false
}
}

private fun sendData() {
val data = inputEditText.text.toString()
if (data.isEmpty()) {
Toast.makeText(this, "请输入数据", Toast.LENGTH_SHORT).show()
return
}

val success = serialPort.send(data + "\n")
if (success) {
Log.d(TAG, "数据发送成功: $data")
inputEditText.text.clear()
} else {
Toast.makeText(this, "数据发送失败", Toast.LENGTH_SHORT).show()
}
}

private fun handleReceivedData(data: String) {
// 解析接收到的数据
when {
data.startsWith("TEMP:") -> {
val temp = data.substring(5).trim().toFloatOrNull()
temp?.let {
temperatureTextView.text = "${it}°C"
}
}
data.startsWith("STATUS:") -> {
val status = data.substring(7).trim()
deviceStatusTextView.text = status
}
data.startsWith("ERROR:") -> {
val error = data.substring(6).trim()
Log.e(TAG, "设备错误: $error")
Toast.makeText(this, "设备错误: $error", Toast.LENGTH_LONG).show()
}
}
}

override fun onDestroy() {
super.onDestroy()
serialPort.close()
}
}

与串口协议配合使用

H07项目中串口通讯常与SerialPortProtocol协同工作:

class SerialProtocolManager(private val context: Context) {

private val serialPort = SerialPort()
private val protocol = SerialPortProtocol()
private val logUtil by lazy { LogUtil.getInstance(context) }

fun initialize(): Boolean {
return serialPort.open(
onDataReceived = { data ->
// 使用协议解析器解析数据
protocol.parseData(data)
},
portStr = "/dev/ttyHS1",
ibaudRate = 115200
)
}

fun sendCommand(command: String): Boolean {
// 使用协议格式化命令
val formattedCommand = protocol.formatCommand(command)
return serialPort.send(formattedCommand)
}

fun sendStartRecord(): Boolean {
return sendCommand("RECORD:START")
}

fun sendStopRecord(): Boolean {
return sendCommand("RECORD:STOP")
}

fun sendTakePhoto(): Boolean {
return sendCommand("PHOTO:TAKE")
}

fun cleanup() {
serialPort.close()
}
}

在Service中使用

class SerialPortService : Service() {

private val serialPort = SerialPort()
private val handler = Handler(Looper.getMainLooper())
private val TAG = "SerialPortService"

override fun onCreate() {
super.onCreate()
initializeSerialPort()
startPeriodicCheck()
}

private fun initializeSerialPort() {
val success = serialPort.open(
onDataReceived = { data ->
handleSerialData(data)
},
portStr = "/dev/ttyHS1",
ibaudRate = 115200
)

if (success) {
Log.d(TAG, "串口服务初始化成功")
} else {
Log.e(TAG, "串口服务初始化失败")
}
}

private fun handleSerialData(data: String) {
Log.d(TAG, "串口数据: $data")

// 广播数据给其他组件
val intent = Intent("com.znhaas.SERIAL_DATA")
intent.putExtra("data", data)
sendBroadcast(intent)
}

private fun startPeriodicCheck() {
handler.postDelayed(object : Runnable {
override fun run() {
// 定期检查串口状态
if (!serialPort.isOpen()) {
Log.w(TAG, "串口已关闭,尝试重新打开")
initializeSerialPort()
} else {
// 发送心跳
serialPort.send("HEARTBEAT\n")
}

handler.postDelayed(this, 10000) // 每10秒检查一次
}
}, 10000)
}

override fun onBind(intent: Intent?): IBinder? = null

override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
serialPort.close()
}
}

⚠️ 注意事项

1. 权限要求

串口访问可能需要root权限或系统权限:

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

2. 设备路径

不同硬件平台的串口设备路径可能不同:

平台常见路径
通用Android/dev/ttyS0, /dev/ttyS1
高通平台/dev/ttyHS0, /dev/ttyHS1
MTK平台/dev/ttyMT0, /dev/ttyMT1
RK平台/dev/ttyS0, /dev/ttyS1

3. 波特率选择

确保波特率与外部设备一致:

// 低速设备(如传感器)
serialPort.open({ data -> }, ibaudRate = 9600)

// 高速设备(如GPS模块)
serialPort.open({ data -> }, ibaudRate = 115200)

4. 数据格式

接收到的数据可能包含控制字符,需要适当处理:

serialPort.open({ data ->
// 去除控制字符
val cleanData = data.trim().replace("\r", "").replace("\n", "")

// 或者保留换行符用于分隔命令
val lines = data.split("\n")
lines.forEach { line ->
if (line.isNotEmpty()) {
processCommand(line)
}
}
})

5. 线程安全

数据接收回调可能在子线程中执行,更新UI需要切换到主线程:

serialPort.open({ data ->
// ✅ 正确: 切换到主线程
runOnUiThread {
textView.text = data
}

// ❌ 错误: 直接更新UI(可能崩溃)
// textView.text = data
})

6. 资源释放

必须在适当时机关闭串口:

// Activity
override fun onDestroy() {
super.onDestroy()
serialPort.close()
}

// Fragment
override fun onDestroyView() {
super.onDestroyView()
serialPort.close()
}

// Service
override fun onDestroy() {
super.onDestroy()
serialPort.close()
}

7. 数据编码

默认使用UTF-8编码,如需其他编码需手动转换:

// 发送GBK编码
val gbkData = "中文数据".toByteArray(Charset.forName("GBK"))
serialPort.send(gbkData)

// 接收GBK编码
serialPort.open({ data ->
// data已自动转为UTF-8字符串
// 如果原始数据是GBK,需要重新解码
})

🔗 相关资源

  • 源码位置: app/src/main/java/android/znhaas/util/SerialPort.kt
  • 协议解析: android.znhaas.util.SerialPortProtocol
  • 依赖库: com.vi.vioserial.NormalSerial

📚 扩展阅读