串口通讯模块
📋 模块概述
串口通讯模块提供与外部串口设备的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