Android 串口通信

814 阅读4分钟

串口通信,在 Android 手机端 APP 开发中用的很少,因为绝大多数 APP 都是使用 HTTP 与后台通信获取数据的,但是在智能家居,人脸识别门禁,自动售货机,和单片机通信等场景,基本上都离不开串口通信,就比如自动售货机,Android 应用收到付款成功后,发送串口指令,控制货道进行出货。

什么是串口

串口叫做串行接口,是指数据一位一位地顺序传送,通信线路简单,只要一对传输线就可以实现双向通信,相当于一个管道,在硬件层面有三根跳线,Tx 线,Rx 线和地线,这个管道传输的数据是串行的,有顺序的。串口通信就是通信双方按位进行,遵守时序的一种通信方式。

1.png

串口的通信方式

  • 单工模式:只支持数据在一个方向上传输。
  • 半双工模式:可以发送和接收数据,但不能同时进行发送和接收,也就是同一时间只有一个动作发生。
  • 全双工模式:数据可以同时往两个方向传输,在同一时间可以同时进行发送和接收数据,实现双向通信。

编码

引入依赖

implementation 'com.licheedev:android-serialport:2.1.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.activity:activity-ktx:1.5.1'

这里以测温为例,有些场所的人脸识别 Android 终端设备是带有测温功能的,能实时显示屏幕前人的体温,这个是怎么做到的呢?其实就是利用串口通信,简单地说,就是测温相关的硬件模块把测得的温度发送给 Android 设备,Android 接收后解析数据进行显示。

private var serialPort: SerialPort? = null
private var outputStream: OutputStream? = null
private var inputStream: InputStream? = null
private var isStopRead = false 

开启串口,SerialPort 接收两个参数 - 串口地址和波特率,串口地址就是串口文件所在地,波特率表示每秒钟传送的 bit 的个数,这俩参数都会在硬件说明上标注,我们根据说明书填写即可。

try {
    serialPort = SerialPort(File("/dev/ttyS3"), 115200) //串口地址,波特率
    outputStream = serialPort?.outputStream
    inputStream = serialPort?.inputStream
} catch (e: Exception) {
    e.printStackTrace()
}

你也可以配置数据位,停止位和校验位,不过一般都用默认的,无需配置。

serialPort = SerialPort.newBuilder(File("/dev/ttyS3"), 115200) //串口地址,波特率
    .dataBits(dataBits) // 数据位:通信中数据位的参数,默认8,可选值为5-8
    .stopBits(stopBits) // 停止位:单个包的最后一位,表示传输的结束,默认1位
    .parity(parity) //一种简单的检错方式,0指无校验,1指奇校验,2指偶校验
    .build()

打开之后,我们就可以开始读取温度数据了

    private suspend fun readTemperature() {
        isStopRead = false
        try {
            inputStream?.let {
                if (it.available() > 0) { //清除缓存
                    it.skip(it.available().toLong())
                }
                //开启循环,不断读取数据,直到协程喊停。
                while (!isStopRead) {
                    if (it.available() <= 0) { //说明没有可用字节
                        delay(200)
                        continue
                    }
                    val tempBuffer = ByteArray(231)
                    val tempSize: Int = it.read(tempBuffer)
                    Log.i(tag, "Read Length: $tempSize")
                    //对数据进行解析,这个根据交互的实际情况出发。
                    val tempCollect = String(tempBuffer) //此数据长这样:{36.35}{36.35}{36.35}.......
                    val temperature = tempCollect.substring(1, 6) //拿到第一个温度,设置上去即可。
                    withContext(Dispatchers.Main) {
                        findViewById<TextView>(R.id.result).text = temperature
                    }
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

开启协程,执行读取任务

        lifecycleScope.launch(Dispatchers.IO) {
            readTemperature()
        }

记得不用的时候关一下就行了

private fun closePort() {
    try {
        isStopRead = true
        inputStream?.close()
        outputStream?.close()
        serialPort?.close()
        serialPort = null
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

简单的测温模块这样就完成了。当然,如果你要给串口发送命令的话,看下面代码

//给串口发送命令
private suspend fun sendCommand(hexStr: String) {
    try {
        outputStream?.also {
            it.write(hexStr2bytes(hexStr))
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

//把十六进制表示的字节数组字符串,转换成十六进制字节数组
private fun hexStr2bytes(hex: String): ByteArray {
    val len = (hex.length / 2)
    val result = ByteArray(len)
    val achar = hex.uppercase(Locale.ROOT).toCharArray()
    for (i in 0 until len) {
        val pos = i * 2
        result[i] = (hexChar2byte(achar[pos]) shl 4 or hexChar2byte(achar[pos + 1])).toByte()
    }
    return result
}

// 把十六进制字符转成字节
private fun hexChar2byte(c: Char) = when (c) {
    '0' -> 0
    '1' -> 1
    '2' -> 2
    '3' -> 3
    '4' -> 4
    '5' -> 5
    '6' -> 6
    '7' -> 7
    '8' -> 8
    '9' -> 9
    'a', 'A' -> 10
    'b', 'B' -> 11
    'c', 'C' -> 12
    'd', 'D' -> 13
    'e', 'E' -> 14
    'f', 'F' -> 15
    else -> -1
}