龙空技术网

一款实用的c#实现的串口通讯框架(SerialIo)

中年农码工 1463

前言:

当前朋友们对“通过串口数据容错算法”大体比较注重,朋友们都需要学习一些“通过串口数据容错算法”的相关文章。那么小编也在网络上收集了一些有关“通过串口数据容错算法””的相关内容,希望咱们能喜欢,姐妹们一起来了解一下吧!

前言

本项目核心在于实现通讯协议解析,目前例程仅实现了 一对一通讯的解决方案,多设备的(比如 485通讯)从理论上是可以实现的,后期有机会再从框架层去处理。

项目介绍

项目名称为 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实现截图

结束语

  这个框架不太复杂,步骤有一些繁琐,但代码量很少,共享出来也希望给需要的人一些思路,同时也希望能提出一些建议,能更好的改进;

代码已托管至:gitee

标签: #通过串口数据容错算法