前语

本文是安卓串口通讯的第 5 篇文章。本来这篇文章不在计划内,可是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通讯时接纳数据或许会呈现分包的状况有所了解。

其实关于串口通讯会或许会呈现分包早有耳闻,可是我自己实践运用时一向没有遇到过,或许准确的说,尽管遇到过,可是并没有特意的去处理:

分包?不便是传过来的数据不完好嘛,那我把这个数据丢了,等一个完好的数据不就得了。

亦或许,之前运用的都是极少量的数据,一次读取的数据只要 1 byte ,所以很少呈现数据包不完好的状况。

何为分包?

严厉意义上来说,其实并不存在分包的概念。

由于由于串口通讯的特性,它并不知道不知道也无法知道所谓的 “包” 是什么,它只知道你给了数据给它,他就尽或许的把数据发出去。

由于串口通讯时运用的是流式传输,也便是说,一切数据都是以流的形式进行发送、读取,也不存在所谓的“包”的概念。

所谓的“包”仅仅咱们在应用层人为的规则了多少长度的数据或许满足什么样格式的数据为一个“包”。

而为了最大程度的削减通讯时的请求次数,在处理数据流时,通常会尽或许多的读取数据,然后缓存起来(即所谓的缓冲数据),直至到达设置的某个巨细或超过某个时间没有读取到新的数据。

例如,咱们人为的规则了一个数据包为 10 字节,PLC 或 其他串口设备发送时将这 10 个字节的数据接连的发送出来。可是安卓设备或其他主机在接纳时,由于上面所说的原因,或许会先读到 4 字节的数据,再读到 6 字节的数据。也便是说,咱们需求的完好数据不会在一次读取中读到,而是被拆分成了不同的“数据包”,此即所谓的 “分包”:

安卓与串口通信-数据分包的处理

怎样处理分包?

其实谜底就在谜面上,经过上面对分包呈现的原因进行简略的解释之后,相信大伙关于怎样处理分包问题现已有了自己的答案。

处理分包的核心原理说起来十分简略,无非便是把咱们需求的完好的数据包从多次读取到的数据中取出来,再拼成咱们需求的完好数据包即可。

问题在于,咱们应该怎样才能知晓读取到数据归于哪个数据包呢?咱们又该怎样知道数据包是否现已完好了呢?

这就取决于咱们在运用串口通讯时界说的协议了。

一般来说,为了处理分包问题,咱们常用的界说协议的办法有以下几种:

  1. 规则一切数据为固定长度。
  2. 为一个完好的数据规则一个终止字符,读到这个字符表明本次数据包已完好。
  3. 在每个数据包之前添加一个字符,用于表明后续发送的数据包长度。

固定数据包长度

固定数据长度指咱们规则每次通讯时发送的数据包长度都是固定的长度,假如实践长度缺乏规则的长度则运用某些特殊字符如 \0 填充剩下的长度。

关于这种状况,十分好处理,只要咱们每次读取数据时都判断读取到的数据长度,假如数据长度没有到达契合的固定长度,则以为读取数据不完好,就接着读取,直至数据长度契合:

val resultByte = mutableListOf<Byte>()
private fun getFullData(count: Int = 0, dataSize: Int = 20): ByteArray {
    val buffer = ByteArray(1024)
    val readLen = usbSerialPort.read(buffer, 2000)
    for (i in 0 until readLen) {
        resultByte.add(buffer[i])
    }
    // 判断数据长度是否契合
    return if (resultByte.size == dataSize) {
        resultByte.toByteArray()
    } else {
        if (count < 10) {
            getFullData(count + 1, dataSize)
        }
        else {
            // 超时
            return ByteArray(0)
        }
    }
}

可是这种办法也有一个显着的缺点,那便是运用场景局限性特别强,只适合于主机发送请求,从机器回应的这种场景,由于假如是在从机不断的发送数据,而主机或许在某个时间段读取,也或许一向轮询读取的状况下,光靠数据长度判断是不可靠的,由于咱们无法确保咱们读到的指定长度的数据一定便是同一个完好数据,有或许参杂了上一次的数据或许下一次的数据,而一旦读取错一次,就意味着今后每次读取的数据都是错的。

添加完毕符

为了处理上述办法导致的局限性,咱们可以给每一帧数据添加一个完毕符号,通常来说咱们会规则 \r\n 即 CRLF (0x0D 0x0A)为完毕符号。

所以,咱们在读取数据时会循环读取,直至读取到完毕符号,则咱们以为本次读取完毕,现已获得了一个完好的数据包:

val resultByte = mutableListOf<Byte>()
private fun getFullData(): ByteArray {
    var isFindEnd = false
    while (!isFindEnd) {
        val buffer = ByteArray(1024)
        val readLen = usbSerialPort.read(buffer, 2000)
        if (readLen != 0) {
            for (i in 0 until readLen) {
                resultByte.add(buffer[i])
            }
            if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) {
                isFindEnd = true
            }
        }
    }
    return resultByte.toByteArray()
}

可是这个办法明显也有一个缺陷,那便是假如是单次距离读取或许轮询时第一次读取数据有或许也是不完好的数据。

由于咱们尽管读取到了完毕符号,可是并不意味着这次读取的便是完好的数据,或许前面还有数据咱们并没有读到。

不过这种办法可以确保轮询时只要第一次读取数据有或许不完好,可是后续的数据都是完好的。

仅仅单次距离读取的话就无法确保读取到的是完好数据了。

在最初添加数据包长度

和添加完毕符类似,咱们也可以在数据包最初添加一个特殊字符,然后在后面紧跟着一个指定长度(1byte)字符指定接下来的数据包长度有多长。

这样,咱们就可以在解析时首要查找这个开始符号,查找到之后则以为一个新的数据包开始了,然后读取之后 1byte 的字符,获取到这个数据包的长度,接下里依照这个这个指定长度,循环读取直到长度契合即可。

具体读取办法其实便是上面两种办法的结合,所以这里我就不贴代码了。

最好的状况

最方便的处理数据分包的办法当然是在数据中既包括固定数据头、固定数据尾、甚至连数据长度都是固定的。

例如某款温度传感器,发送的是数据格包为固定 10 位长度,且有完毕符 CRLF,并且数据包最初有且只要 -+、 (0x2B 0x2D 0x20)三种状况,那么咱们在接纳数据时就可以这么写:

val resultByte = mutableListOf<Byte>()
val READ_WAIT_MILLIS = 2000
private fun getFullData(count: Int = 0, dataSize: Int = 14): ByteArray {
    var isFindStar = false
    var isFindEnd = false
    while (!isFindStar) { // 查找帧头
        val buffer = ByteArray(1024)
        val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
        if (readLen != 0) {
            if (buffer.first() == 0x2B.toByte() || buffer.first() == 0x2D.toByte() || buffer.first() == 0x20.toByte()) {
                isFindStar = true
                for (i in 0 until readLen) { // 有帧头,把这次结果存入
                    resultByte.add(buffer[i])
                }
            }
        }
    }
    while (!isFindEnd) { // 查找帧尾
        val buffer = ByteArray(1024)
        val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
        if (readLen != 0) {
            for (i in 0 until readLen) { // 先把结果存入
                resultByte.add(buffer[i])
            }
            if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) { // 是帧尾, 完毕查找
                isFindEnd = true
            }
        }
    }
    // 判断数据长度是否契合
    return if (resultByte.size == dataSize) {
        resultByte.toByteArray()
    } else {
        if (count < 10) {
            getFullData(count + 1, dataSize)
        }
        else {
            return ByteArray(0)
        }
    }

粘包呢?

上面咱们只说了分包状况,可是在实践运用过程中,还有或许会呈现粘包的现象。

粘包,望文生义便是不同的数据包在一次读取中混合到了一块。

假如想要处理粘包的问题也很简略,类似于处理分包,也是需求咱们在界说协议时给出可以区别不同数据包的办法,这样咱们依照协议解析即可。

总结

其实串口通讯中的分包或许粘包处理起来并不难,问题首要在于串口通讯一般都是每个硬件设备厂商或许传感器厂商自己界说一套通讯协议,而有的厂商界说的协议比较“不考虑”实践,没有给出任何可以区别不同数据包的标志,这就会导致咱们在接入这些设备时无法正常的解分出数据包。

可是也并不是说就没有办法去解析,而是需求咱们具体状况具体分析,比如温度传感器,尽管通讯协议中没有给出数据头、数据尾、数据长度等信息,可是其实它回来的数据格式几乎都是固定的,咱们只要依照这个固定格式去解析即可。