一款简单实用的串口通讯框架(SerialIo)
- 前言
大龄程序员失业状态,前几天面试了一家与医疗设备为主的公司并录取;因该单位涉及串口通讯方面技术,自己曾做过通讯相关的一些项目,涉及Socket的较多,也使用SuperSocket做过一些项目,入职前做了一些准备工作,按照SuperSocket设计思路写了一套串口通讯的框架,最后入职后发现曾一再确定的双休问题不能实现;技术负责人对我的工作安排只是跟着几个年轻程序员熟悉及维护已有程序,入职前所说的某一个新项目也已经有人在做,而且翻看他们的代码质量也很一般,,综合考虑还是放弃了这份工作,5年前开始一直都在技术管理岗位上,实在不能接受连工作安排都没具体计划的领导;辞职后再把这个套东西整理了一下,做个开源项目发布吧,同时也希望有更好的串口解析框架欢迎告知我来学习。
- 项目介绍
项目名称为 ZhCun.SerialIO
一款串口通讯框架,更容易的处理协议解析,内部实现了粘包、分包、冗余错误包的处理实现; 它可更方便的业务逻辑的处理,将通讯、协议解析、业务逻辑 完全独立开来,更容易扩展和修改代码逻辑; 本项目参考了SuperSocket中协议解析的设计原理,可外部 命令(Command)类, 对应协议中命令字灵活实现逻辑。
例如: 协议格式:[2字节 固定头 0xaa,0xbb] + [1字节 长度 len] [1字节 命令字] [1字节 设备号] [N字节 data] [2字节 CRC校验码]
命令数据: AA BB 09 01 05 00 01 2B 56
可以处理以下几种(粘包、分包、容错)情况:
1. AA BB 09 01 05 00 01 2B 56 AA BB 09 01 05 00 01 2B 56 AA BB 09 01 05 00 01 可以不考虑粘包的处理,会分成3条协议交给Command来处理(后面说明)
2. 00 00 00 AA BB 09 01 05 00 01 2B 56 00 00 00 标记为红色的为错误数据,这些数据会被自动过滤掉
3. 连续收到(延时接收了)多个半包,串口缓存问题可能导致延时收到数据
AA BB 09 01 05
00 01 2B 56
这种情况会等待下次处理,如果之后再没有收到正确的数据会丢弃前部分,后面正确的数据会正常处理
代码目录:
- 设计思路及实现
ISerialServer 是实现串口通讯的接口,接收数据、发送数据、打开与关闭串口的实现;SerialCore 是 ISerialServer 通讯的核心实现
public interface ISerialServer : IDisposable
{
/// <summary>
/// 当连接状态改变时触发
/// </summary>
event Action<ConnectChangedEvent> ConnectChanged;
/// <summary>
/// 当读数据完成时触发
/// </summary>
event Action<ReadWriteEvent> DataReadOver;
/// <summary>
/// 当下发数据时触发
/// </summary>
event Action<ReadWriteEvent> DataWriteOver;
/// <summary>
/// 当前服务的配置
/// </summary>
SerialOption Option { get; } void Write(byte[] data); void Write(byte[] data, int offset, int count); void Write(byte[] data, int offset, int count, int sendTimes, int interval); void Write(IWriteModel model); /// <summary>
/// 开始监听串口
/// </summary>
void Start(SerialOption option);
/// <summary>
/// 默认参数及指定串口开始服务
/// </summary>
void Start(string portName, int baudRate = 9600);
}
SerialServerBase 继承了 SerialCore ,它主要实现了 命令处理器 及 过滤处理器,这套框架的核心就是协议的解析及命令的处理;
它扩展了两个重要属性 :Filters 和 Commands 分别是协议过滤器和命令处理器,与 SerialCore 分开是为了满足不需要过滤器和命令处理器的情况
构造函数中调用载入过滤器 LoadFilters() 与 载入命令处理器 的两个方法,该方法应该由应用程序来实现子类并加入用户自定义的 Filters 和 Commands
public SerialServerBase()
{
Filters = new List<IReceiveFilter>();
Commands = new List<ICommand>();
LoadFilters();
LoadCommands();
Filters.ForEach(s => s.OnFilterFinish = ReceiveFilterAction);
}
/// <summary>
/// 过滤器
/// </summary>
protected List<IReceiveFilter> Filters { get; }
/// <summary>
/// 命令处理器
/// </summary>
protected List<ICommand> Commands { get; }
/// <summary>
/// 加载过滤器,子类需 Filters.Add
/// </summary>
protected virtual void LoadFilters() { }
/// <summary>
/// 加载命令处理器
/// </summary>
protected virtual void LoadCommands() { }
SerialServerBase 重写了 OnDataReadOver 和 ReceiveFilterAction,分别来处理协议解析和命令处理
/// <summary>
/// 接收到数据后交给过滤器来处理协议
/// </summary>
protected override void OnDataReadOver(byte[] data, int offset, int count)
{
foreach (var filter in Filters)
{
filter.Filter(this, data, offset, count, false, out _);
} base.OnDataReadOver(data, offset, count);
}
/// <summary>
/// 接收数据解析完成后触发
/// </summary>
protected virtual void ReceiveFilterAction(PackageInfo package)
{
if (Commands == null || Commands.Count == 0) return; var cmd = Commands.Find(s => s.Key == package.Key);
if (cmd != null)
{
cmd.Execute(this, package);
}
}
IReceiveFilter 接收过滤器定义,它实现解析的核心功能,处理粘包、分包都是在 它的实现类 ReceiveBaseFilter 中,Filter 方法实现分包粘包的处理,代码如下
/// <summary>
/// 过滤协议,粘包、分包的处理
/// </summary>
public virtual void Filter(IBufferReceive recBuffer, byte[] data, int offset, int count, bool isBuffer, out int rest)
{
if (!isBuffer && recBuffer.HasReceiveBuffer())
{
recBuffer.SetReceiveBuffer(data, offset, count);
Filter(recBuffer, recBuffer.ReceiveBuffer, 0, recBuffer.ReceiveOffset, true, out rest);
return;
} if (isBuffer && count < MinLength)
{
rest = 0;
return;
} if (!isBuffer && recBuffer.ReceiveOffset + count < MinLength)
{
//等下一次接收后处理
recBuffer.SetReceiveBuffer(data, offset, count);
rest = 0;
return;
} rest = 0; if (!FindHead(data, offset, count, out int headOffset))
{
//未找到包头丢弃
recBuffer.RestReceiveBuffer();
FilterFinish(1, data, offset, count);
return;
} if (count - (headOffset - offset) < MinLength)
{
// 从包头位置小于最小长度(半包情况),注意:解析了一半,不做解析完成处理
recBuffer.SetReceiveBuffer(data, headOffset, count - (headOffset - offset));
return;
} int dataLen = GetDataLength(data, headOffset, count - (headOffset - offset));
if (dataLen <= 0)
{
//错误的长度 丢弃
recBuffer.RestReceiveBuffer();
FilterFinish(2, data, offset, count);
return;
} if (dataLen > count - (headOffset - offset))
{
//半(分)包情况,等下次接收后合并
if (!isBuffer) recBuffer.SetReceiveBuffer(data, headOffset, count - (headOffset - offset));
return;
} rest = count - (dataLen + (headOffset - offset)); FilterFinish(0, data, headOffset, dataLen); recBuffer.RestReceiveBuffer(); if (rest > 0)
{
Filter(recBuffer, data, headOffset + dataLen, rest, false, out rest);
return;
}
}
核心解析先介绍这么多,下面举例说明下如何应用及使用过程
以上介绍示例的协议来举例
协议说明: [2字节 固定头 0xaa,0xbb] + [1字节 长度 len] [1字节 命令字] [1字节 设备号] [N字节 data] [2字节 CRC校验码]
步骤:
1. 创建过滤器 ,应用层的过滤器只需要设置 包头,获取数据包长度、命令字的实现,简单几行代码即可方便实现过滤的整个过程;
代码如下:
public class FHDemoFilter : FixedHeadFilter
{
static byte[] Head = new byte[] { 0xaa, 0xbb }; public FHDemoFilter()
: base(Head, 6)
{ } //[0xaa,0xbb] [len] [cmd] [DevId] [data] [crc-1,crc-2] protected override int GetDataLength(byte[] data, int offset, int count)
{
//数据包长度 第3个字节
return data[offset + 2];
} protected override int GetPackageKey(byte[] data, int offset, int count)
{
//命令字 第4个字节
return data[offset + 3];
}
}
2. 创建命令处理器 ,命令处理器 由 ICommand 派生,需要指明 Key (即:GetPackageKey 获取的命令字),然后一个 执行的逻辑方法 Execute ,这里将 接收到的数据包与发送的数据包封装了实体对象,更方便处理data的解析及发送包的封装;
定义一个抽象的 CmdBase 它的派生类来实现具体 命令 的业务逻辑,CmdBase 会将协议生成一个实体对象给派生类
public abstract class CmdBase<TReadModel> : ICommand
where TReadModel : R_Base, new()
{
public abstract int Key { get; } public abstract string CmdName { get; } /// <summary>
/// 执行响应逻辑
/// </summary>
public abstract void ExecuteHandle(ISerialServer server, TReadModel rep); public virtual void Execute(ISerialServer server, PackageInfo package)
{
var rModel = new TReadModel();
var r = rModel.Analy(package.Body, package.BodyOffset, package.BodyCount);
if (r == 0)
{
ExecuteHandle(server, rModel);
}
else
{
LogPrint.Print($"解析key={Key} 异常,error code: {r}");
}
}
}
CmdBase 将创建的 R_Base 实例 交给派生类处理,R_Base 封装了解析数据包内容及校验的实现,派生类只需要将 Data 数据再次解析即可
R_Base 使用了 BytesAnalyHelper 字节解析工具,它能更方便和灵活的来按顺序解析协议;
public abstract class R_Base : BaseProtocol
{
/// <summary>
/// 解析数据对象
/// </summary>
protected BytesAnalyHelper AnalyObj { get; private set; }
/// <summary>
/// 解析消息体, 0 正常, 1 校验码 错误,2 解析异常
/// </summary>
protected abstract int AnalyBody(BytesAnalyHelper analy); /// <summary>
/// 解析协议, 0 成功 1 校验码错误 9 异常
/// </summary>
public int Analy(byte[] data, int offset, int count)
{
try
{
var crc = GetCRC(data, offset, count - 2); //校验码方法内去除
var crcBytes = BitConverter.GetBytes(crc);
if (crcBytes[0] != data[offset + count - 1] || crcBytes[1] != data[offset + count - 2])
{
return 1;
}
//[0xaa,0xbb] [len] [cmd] [DevId] [data] [crc-1,crc-2]
AnalyObj = new BytesAnalyHelper(data, offset, count, 4); // 跳过包头部分
DevId = AnalyObj.GetByte(); // 取 DevId
var r = AnalyBody(AnalyObj);
return r;
}
catch
{
//LogHelper.LogObj.Error($"解析数据包发生异常.", ex);
return 9;
}
}
}
举例解析 data 为一个文本的实现,只需要用哪种编码转换即可,直接赋值给 Text
public class R_Text : R_Base
{
public string Text { set; get; } protected override int AnalyBody(BytesAnalyHelper analy)
{
Text = analy.GetString(analy.NotAnalyCount - 2);
return 0;
}
}
,然后CmdText 的命令处理器就可以得到这个对象,来进行对应的业务逻辑处理
public class CmdText : CmdBase<R_Text>
{
public override int Key => 0x02; public override string CmdName => "Text"; public override void ExecuteHandle(ISerialServer server, R_Text rep)
{
LogPrint.Print($"[Text]: {rep.Text}");
//回复消息
var w = new W_TextRe();
w.DevId = rep.DevId;
server.Write(w); //.. to do something
}
}
以上就是实现的主要部分,最终调用 SerialServer 派生实例的 Start 方法即可;
最后附上,demo实现截图
- 结束语
这个框架不太复杂,步骤有一些繁琐,但代码量很少,共享出来也希望给需要的人一些思路,同时也希望能提出一些建议,能更好的改进;
看到这里如果有 年龄 35+ 的程序员,欢迎交流一下都在做什么?
代码已托管至:gitee
一款简单实用的串口通讯框架(SerialIo)的更多相关文章
- 推荐一款开源的C#TCP通讯框架
原来收费的TCP通讯框架开源了,这是一款国外的开源TCP通信框架,使用了一段时间,感觉不错,介绍给大家 框架名称是networkcomms 作者开发了5年多,目前已经停止开发,对于中小型的应用场景,够 ...
- 简单的Java串口通讯应答示例
java串口通讯第一次使用,找的资料都比较麻烦,一时没有理出头绪,自己在示例的基础上整理了一个简单的应答示例,比较简陋,但演示了java串口通讯的基本过程. package com.garfield. ...
- 简单实用的Android ORM框架TigerDB
TigerDB是一个简单的Android ORM框架,它能让你一句话实现数据库的增删改查,同时支持实体对象的持久化和自动映射,同时你也不必关心表结构的变化,因为它会自动检测新增字段来更新你的表结构. ...
- 一款简单实用的jQuery图片画廊插件
图片画廊 今天分享一个自己实现的jQuery 图片画廊插件. 看一下效果图: 点击图片时: 在线演示地址:http://www.jr93.top/photoGallery/photoGallery.h ...
- 简单实用的纯CSS百分比圆形进度条插件
percircle是一款简单实用的纯CSS百分比圆形进度条插件.你不需要做任何设置,只需要按该圆形进度条插件提供的标准HTML结构来编写代码,就可以生成一个漂亮的百分比圆形进度条. 首先要做的就是引入 ...
- 基于jQuery简单实用的Tabs选项卡插件
jQuery庞大的插件库总是让人欢喜让人忧,如何从庞大的插件库里挑出适合自己的插件,总是让很多缺少经验的朋友头疼的事!今天为大家推荐几款简单实用的Tabs选项卡插件,推荐理由:简单易用灵活,样式美观, ...
- 纯css3简单实用的checkbox复选框和radio单选框
昨天为大家分享了一款很炫的checkbox复选框和radio单选框,今天再给大家带来一款简单实用的checkbox复选框和radio单选框.界面清淅.舒服.先给大家来张效果图: 在线预览 源码下载 ...
- C#串口通讯
本文提供一个用C#实现串口通讯实例,亲自编写,亲测可用! 开发环境:VS2008+.net FrameWork3.5(实际上2.0应该也可以) 第一步 创建一个WinForm窗体,拉入一些界面元素 重 ...
- 简单实用的原生PHP分页类
一款简单实用的原生PHP分页类,分页按钮样式简洁美观,页码多的时候显示“...”,也是挺多网站用的效果 核心分页代码 include_once("config.php"); req ...
随机推荐
- 【spring 注解驱动开发】扩展原理
尚学堂spring 注解驱动开发学习笔记之 - 扩展原理 扩展原理 1.扩展原理-BeanFactoryPostProcessor BeanFactoryPostProcessor * 扩展原理: * ...
- 【java web】过滤器filter
一.过滤器简介 过滤器filter依赖于servlet容器 所谓过滤器顾名思义是用来过滤的,Java的过滤器能够为我们提供系统级别的过滤,也就是说,能过滤所有的web请求, 这一点,是拦截器无法做到的 ...
- Mysql---C#在cmd中使用mysqldump导出sql文件
一.概述 本文描述了在C#中利用mysqldump工具导出sql文件. 二.代码片段 CmdHelper类代码如下: public class CmdHelper { public static st ...
- protected访问权限
Java中protected方法访问权限的问题 protected 修饰的成员变量或方法,只能在同包或子类可访问; package 1 public class TestPackage { prote ...
- 2020年秋游戏开发-Gluttonous Snake
此作业要求参考https://edu.cnblogs.com/campus/nenu/2020Fall/homework/11577 GitHub地址为https://github.com/15011 ...
- Python的GPU编程实例——近邻表计算
技术背景 GPU加速是现代工业各种场景中非常常用的一种技术,这得益于GPU计算的高度并行化.在Python中存在有多种GPU并行优化的解决方案,包括之前的博客中提到的cupy.pycuda和numba ...
- Jmeter的初体验--安装
准备工作 安装JMeter前需要安装配置好Java 一.安装 1.直接在官网下载安装即可,下载地址:http://jmeter.apache.org/download_jmeter.cgi,(Wind ...
- 证明:(a,[b,c]) = [(a,b),(a,c)]
这题是潘承洞.潘承彪所著<初等数论>(第三版)第一章第5节里一个例题,书中采用算术基本定理证明,并指出要直接用第4节的方法来证是较困难的. 现采用第4节的方法(即最大公约数理论里的几个常用 ...
- PXC 5.7.14 安装部署
http://www.dbhelp.net/2017/01/06/pxc-5-7-14-%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2-pxc-install.html PX ...
- vue引用 element-ui 的 el-aside -> el-menu -> el-menu-item 路由侧边栏跳转链接,接上动态路由
npm 下载 npm i element-ui -S Vue main.js //引入element-ui的包 import ElementUI from 'element-ui'; //引入el ...