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

news2025/2/1 12:56:34

前言

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

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

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

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

何为分包?

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

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

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

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

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

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

1.gif

怎么处理分包?

其实谜底就在谜面上,通过上面对分包出现的原因进行简单的解释之后,相信大伙对于怎么解决分包问题已经有了自己的答案。

解决分包的核心原理说起来非常简单,无非就是把我们需要的完整的数据包从多次读取到的数据中取出来,再拼成我们需要的完整数据包即可。

问题在于,我们应该怎么才能知晓读取到数据属于哪个数据包呢?我们又该怎么知道数据包是否已经完整了呢?

这就取决于我们在使用串口通信时定义的协议了。

一般来说,为了解决分包问题,我们常用的定义协议的方法有以下几种:

  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)
        }
    }

粘包呢?

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

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

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

总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/631336.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

编译tolua——3、以pbc为例子,添加第三方库

目录 1、编译工具和环境说明 2、基础编译tolua 3、以pbc为例子&#xff0c;添加第三方库 4、更新luaJit 大家好&#xff0c;我是阿赵。 之前分享过怎样正常编译基础版本的tolua。这次用添加pbc为例&#xff0c;看看怎样往tolua里面添加其他的第三方库。知道了方法之后&#xf…

【郭东白架构课 模块二:创造价值】31 |节点六: 如何组织阶段性的价值交付?

你好&#xff0c;我是郭东白。上节课我们讲了为什么要做阶段性的价值交付&#xff0c;以及进入阶段性价值交付环节的准备工作。有了这些学习基础&#xff0c;这节课我们就可以进行阶段性价值交付了。 在交付的过程中&#xff0c;主要有三部分工作&#xff1a;目标分解、定义交…

SLAM十四讲——ch4实践(李群李代数)

视觉SLAM14讲----ch4的操作及避坑 一、ch4的实践的准备工作二、各个实践操作1. Sophus的基本使用方法2. 例子&#xff1a;评估轨迹误差 三、遇到的问题 一、ch4的实践的准备工作 确保已经有Sophus库&#xff0c;Sophus库是一个较好的李代数库。 注意&#xff1a; 开始时slamb…

MySQL 数据库实用指南:测试数据准备、SQL语句规范与基本操作

前言 欢迎来到小K的MySQL专栏&#xff0c;本节将为大家准备MySQL测试数据、以及带来SQL语句规范、数据库的基本操作的详细讲解~✨文末送书&#xff0c;小K赠书活动第二期 目录 前言一、准备测试数据二、SQL语句规范三、数据库的基本操作四、总结&#xff1a;文末赠书 一、准备测…

Linux之进程间通信——system V(共享内存、消息队列、信号量等)

文章目录 前言一、共享内存1.共享内存的基本原理2.共享内存的创建3.共享内存的控制参数返回值共享内存的内核数据结构 4.共享内存的关联参数 5.共享内存的去关联6.查看IPC资源7.查看共享内存8.删除共享内存 二、实现进程间通信&#xff08;代码&#xff09;三、共享内存的特点四…

【Newman+Jenkins】实施接口自动化测试

一、是什么Newman Newman就是纽曼手机这个经典牌子&#xff0c;哈哈&#xff0c;开玩笑啦。。。别当真&#xff0c;简单地说Newman就是命令行版的Postman&#xff0c;查看官网地址。 Newman可以使用Postman导出的collection文件直接在命令行运行&#xff0c;把Postman界面化运…

软件测试—冒烟测试

1. 核心 冒烟测试就是完成一个新版本的开发后&#xff0c;对该版本最基本的功能进行测试&#xff0c;保证基本的功能和流程能走通。 如果不通过&#xff0c;则打回开发那边重新开发&#xff1b; 如果通过测试&#xff0c;才会进行下一步的测试(功能测试&#xff0c;集成测试…

【ThreadLocal为什么可能内存泄漏?】 —— 每天一点小知识

&#x1f4a7; T h r e a d L o c a l 为什么可能内存泄漏&#xff1f; \color{#FF1493}{ThreadLocal为什么可能内存泄漏&#xff1f;} ThreadLocal为什么可能内存泄漏&#xff1f;&#x1f4a7; &#x1f337; 仰望天空&#xff0c;妳我亦是行人.✨ &#x1f984; 个…

渗透测试综合实验

文章目录 一、前期交互二、信息搜集三、威胁建模五、渗透攻击1.弱口令攻击2.SQL注入3.不安全文件上传 六、后渗透攻击利用1.蚁剑安装2.一句话木马利用 七、漏洞报告 一、前期交互 二、信息搜集 使用nmap收集端口、域名、后台信息 目标IP nmap -O -sV IP 三、威胁建模 寻…

基于javaweb jsp+servlet实验室设备管理系统的设计与实现

一.项目介绍 本系统分为 超级管理员、老师、学生三类角色 超级管理员&#xff1a;通知管理、维护用户信息、实验室管理&#xff08;负责维护实验室、预约实验室&#xff09;、设备管理&#xff08;维护技术参数、维护运行数据、维护电子文档&#xff09;、设备维修管理&am…

第5章 总体设计

第5章 总体设计 总体设计是决定”怎样做”。也就是概括的说&#xff0c;系统应该如何实现&#xff0c;因此总体设计也被称作概要设计。 5.1 设计过程 例题 5.2 设计原理 5.2.1 模块化 模块是由边界元素限定的相邻程序元素&#xff08;例如&#xff0c;数据说明&#xff0c;…

【Spring Boot】Spring Boot特点及重要策略,含安装步骤详细讲解

前言 Spring Boot是由Pivotal团队提供的全新框架&#xff0c;其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置&#xff0c;从而使开发人员不再需要定义样板化的配置。通过这种方式&#xff0c;Spring Boot致力于在蓬勃发展的快速应…

Matplotlib 绘制多图

Matplotlib 绘制多图 我们可以使用 pyplot 中的 subplot() 和 subplots() 方法来绘制多个子图。 subplot() 方法在绘图时需要指定位置&#xff0c;subplots() 方法可以一次生成多个&#xff0c;在调用时只需要调用生成对象的 ax 即可。 subplot subplot(nrows, ncols, inde…

微服务_Nacos

简介 Nacos&#xff08;全称为“动态服务发现、配置和服务管理平台”&#xff09;是阿里巴巴开源的一款云原生服务发现和配置管理平台&#xff0c;支持多种语言和多种环境&#xff0c;包括Kubernetes、Docker、Spring Cloud等常见的云原生环境。它提供了服务发现、配置管理、服…

MFC的定义和实际操作方法

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天从另一个角度来看一下MFC。 完整的应用一般由四个类组成&#xff1a;CWinApp应用类&#xff0c;CFrameWnd窗口框架类&#xff0c;CDocument文档类&#xff0c;CView视类 过程&#xff1a;CWinApp创建CF…

算法刷题-链表-反转链表

反转链表 206.反转链表思路C代码双指针法递归法其他语言版本使用虚拟头结点解决链表翻转使用栈解决反转链表的问题 反转链表的写法很简单&#xff0c;一些同学甚至可以背下来但过一阵就忘了该咋写&#xff0c;主要是因为没有理解真正的反转过程。 206.反转链表 力扣题目链接 …

【Java基础篇】方法的使用(方法的使用以及形参实参的关系)

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a;Java.SE&#xff0c;本专栏主要讲解运算符&#xff0c;程序逻辑控制&#xff0c;方法的使用&…

【线程安全问题】线程互斥与线程同步技术

在达内Windows/Win32编程专栏中&#xff0c;我们已经介绍过线程同步与线程互斥技术&#xff0c;包括了原子锁&#xff0c;互斥体&#xff0c;事件和信号量。但是与海哥讲的线程同步与线程互斥技术不太一样&#xff0c;这篇文章来带领大家学习线程同步与线程互斥技术&#xff0c…

新手运行bert,pycharm不识别conda安装的python环境

提示No module named numpy/tensorflow conda list是有这些包的 pycharm识别不出interpreter的package 改成scripts下的python.exe就能识别出numpy和tensorflow了 改完interpreter之后出现过importerror: dll load failed&#xff0c;在environment variables里加了这些就不报错…

06. Web大前端时代之:HTML5+CSS3入门系列~HTML5 画布

我们先看看画布的魅力&#xff1a; 初始画布 canvas默认是宽300px&#xff0c;高150px; 绘制步骤 1.定义一个id <canvas id"canvasOne" width"300" height"300"></canvas> 2.获取canvas对象 var canvasObj document.getEleme…