前言

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

其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使用时一直没有遇到过,或者准确的说,虽然遇到过,但是并没有特意的去处理:

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

亦或者,之前使用的都是极少量的数据,一次读取的数据只有 1 byte ,所以很少出现数据包不完整的情况。

作者:equationl 链接:https://juejin.cn/post/7240248679515979835

何为分包?

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

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

因为串口通信时使用的是流式传输,也就是说,所有数据都是以流的形式进行发送、读取,也不存在所谓的“包”的概念。

所谓的“包”只是我们在应用层人为的规定了多少长度的数据或者满足什么样格式的数据为一个“包”。

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

例如,我们人为的规定了一个数据包为 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 = falsewhile (!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 = falsevar isFindEnd = falsewhile (!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 = truefor (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)}}

粘包呢?

上面我们只说了分包情况,但是在实际使用过程中,还有可能会出现粘包的现象。

粘包,顾名思义就是不同的数据包在一次读取中混合到了一块。

如果想要解决粘包的问题也很简单,类似于解决分包,也是需要我们在定义协议时给出能够区分不同数据包的方式,这样我们按照协议解析即可。

总结

其实串口通信中的分包或者粘包解决起来并不难,问题主要在于串口通信一般都是每个硬件设备厂商或者传感器厂商自己定义一套通信协议,而有的厂商定义的协议比较“不考虑”实际,没有给出任何能够区分不同数据包的标志,这就会导致我们在接入这些设备时无法正常的解析出数据包。

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

关注我获取更多知识或者投稿

安卓与串口通信-数据分包的处理相关推荐

  1. (RS485 232串口通信数据解析实用干货(1)

    文章目录 (RS485 232串口通信数据解析实用干货(1) 新的改变 功能快捷键 合理的创建标题,有助于目录的生成 如何改变文本的样式 插入链接与图片 如何插入一段漂亮的代码片 生成一个适合你的列表 ...

  2. C#串口通信数据不完整

    下面代码是我使用C#开发串口通信写的代码,在实现串口通信过程中,在数据接收线程中, 未加这条语句Thread.Sleep(50);我使用串口工具调试 ,串口通信正常,但是在与实际单片机通信过程中,出现 ...

  3. 串口---串口通信数据位长度对传输数据的影响

    文章来源:https://blog.csdn.net/petershina/article/details/8612357 针对串口通信,关于设置数据位长度对通信的影响,如图: 在串口数据通信中,会看 ...

  4. 串口通信数据位长度对传输数据的影响

    http://blog.csdn.net/petershina/article/details/8612357 针对串口通信,关于设置数据位长度对通信的影响,如图: 在串口数据通信中,会看到串口参数设 ...

  5. 安卓USB串口通信 Arduino

    OkUSB,一个简洁的Android串口通信框架. 功能简介 支持设置波特率 支持设置数据位 支持设置停止位 支持设置校验位 支持DTS和RTS 支持串口连接状态监听 文件:url80.ctfile. ...

  6. stm32 串口通信数据移位寄存器_STM32串口接RS485丢码问题已解决*_*

    static void Rs4851Send(unsigned char *p,unsigned char length) { ///TIM_SetCounter(TIM(RS4851_TIM_NO) ...

  7. labview接收串口通信数据不正常的解决办法

    在昨天我写了采用挨个数发送的方法获得了正常的上位机数据. 但是在上位机中,数据显示并不正常. 如:1234会依次显示1234:1:2:3:4: 我的解决办法是:在接收到的数据之后加一个选择结构,如果接 ...

  8. CH340与Android串口通信

    CH340与Android串口通信 为何要将CH340的ATD+Eclipse上的安卓工程移植到AndroidStudio 移植的具体步骤 CH340串口通信驱动函数 通信过程中重难点 还存在的问题 ...

  9. 串口通信学习(GPS模块)2021.5.10

    GPS串口通信学习实践 2021.5.10 1.串口通信简介 1.1 波特率 1.2 数据位 1.3 停止位 1.4 奇偶校验位 2.GPS模块串口通信配置 2.1 驱动安装 2.2 插入GPS模块 ...

最新文章

  1. 在计算机网络俗称网上邻居上能看到自己,能看到自己计算机,看不到网上邻居的标准答案...
  2. viewsource和viewparsed_(Summary)Developer Tools:IE9的F12,Chrome的Ctrl+Shift+J比较
  3. shell 当中的比较运算
  4. win10下安装Pyspider
  5. bldc 原理 方波控制_正弦波驱动BLDC原理
  6. gitee中打开的index.html中图片不显示_typora + gitee + zsh 实现全免费个人云笔记
  7. PSINS捷联惯导更新算法
  8. 关于CSDN登录提示手机号验证的问题
  9. Dataset之LFW:LFW人脸数据库的简介、安装、使用方法之详细攻略
  10. 用计算机找到自己的另一半,如何找到自己的另一半
  11. 密码生成器(C语言实现)
  12. 关于计算机教学的论文,关于计算机教学论文.docx
  13. 短信推广的规则,你知道多少?
  14. win7防火墙开启ping
  15. 白菜一斤八毛用计算机怎么算,6毛一斤的大白菜,一买好几颗,12种做法换着吃...
  16. 【Swift】LeedCode 宝石与石头
  17. shell判断所输整数是否为质数
  18. 微信公众号开发---微信开发学习路线(及供参考)
  19. 老板与管理者的区别(zt)
  20. vue axios 拦截器配置与封装

热门文章

  1. Fetch的理解和使用
  2. 转转登录一直显示服务器错误,[转]已成功与服务器建立连接但是在登录过程中发生错误。provid...
  3. JQuery 渐变显示隐藏动画 ( fadeIn(),fadeOut(),fadeTo() )
  4. mysql排序 latin,MySQL排序规则:latin1_swedish_ci Vs utf8_general_ci
  5. 汇报演示领导都说好,只因用了Smartbi幻灯片这个功能
  6. 微服务分布式事务处理
  7. 写给程序猿们的交互设计
  8. 【小迪安全】web安全|渗透测试|网络安全 | 学习笔记-10
  9. sqlserver里面增加约束条件
  10. ghost android x86,[小兵系统]GHOST WIN7 X86/X64 V2016.05 纯净版