- 初心 & 目标
- 项目结构
- 实现协议兼容
- IByteTransform">数据转换接口 IByteTransform
- INetMessage">数据信息提取接口 INetMessage
初心 & 目标
一直想写一个高速、稳定、支持异步的串口收发库,但总是三点只能做到两点。这次阅读 Richard.Hu 的 HslCommunication 源码,看看大神的底层串口收发是怎么做的。
注:作者由于经济原因不再开源
预设目标:
- 底层串口收发是怎么做的?
- 如何实现的对多种协议的兼容?
- 如何实现对软件的全自动更新升级?HslCommunication.Enthernet.NetSoftUpdateServer
项目结构
实现协议兼容
数据转换接口 IByteTransform
作者介绍:
设备的 BCL 类型的读写,本质是基于 byte 数组和 C# 基础类型的转换,但是这里有个问题,不同的 PLC,modbus 协议对于转换的格式不是固定的,有可能是一样的,有可能不是一样的,所以又抽象出来一个 IByteTransform 接口。
这个接口集成到了下面的设备交互的基类 NetworkDeviceBase 里,这个基类实现了一些基础的类型的数据读写。
所以到这里可以看到,从NetworkDeviceBase 类继承出去的设备类(大部分的设备通信协议都是从这个继承出去的),其基本的读写代码都是一致的,关于解析协议,通信的底层都是封装完毕。
IByteTransform 由三部分构成:
- 定义了一组从 byte[] 提取数据的方法,例如
float TransSingle( byte[] buffer, int index );
- 定义了一组数据转化为 byte[] 的方法,例如
byte[] TransByte( int[] values );
- 存储数据解析格式的 DataFormat 属性
- 很形象的 ABCD、BADC、CDAB 和 DCBA
- ABCD:对等转换,字节无需颠倒,例如三菱PLC,Hsl 通信协议
- DCBA:颠倒转换,字节需完全颠倒,例如西门子 PLC
- CDAB:以 2 字节为单位颠倒转换,例如 Modbus 协议
添加新的协议后,都需要实现 IByteTransform 接口,以定义该协议的数据转换方法和数据解析格式。
数据信息提取接口 INetMessage
INetMessage 用于定义解析规则和数据信息提取规则,由以下几个部分组成:
- 头字节长度,检查头字节合法性,从头字节中提取出来的接下来需要接收数据的长度
- 头字节的数据,内容字节的数据
- 头字节的标识号
- 发送的字节信息
下面以 ModbusTcpMessage 为示例,看看实现了 INetMessage 接口后的效果:
namespace HslCommunication.Core.IMessage
{
/// <summary>
/// Modbus-Tcp协议支持的消息解析类
/// </summary>
public class ModbusTcpMessage : INetMessage
{
/// <summary>
/// 消息头的指令长度
/// </summary>
public int ProtocolHeadBytesLength
{
get { return 8; }
}
/// <summary>
/// 从当前的头子节文件中提取出接下来需要接收的数据长度
/// </summary>
/// <returns>返回接下来的数据内容长度</returns>
public int GetContentLengthByHeadBytes( )
{
/************************************************************************
*
* 说明:为了应对有些特殊的设备,在整个指令的开端会增加一个额外的数据的时候
*
************************************************************************/
if (HeadBytes?.Length >= ProtocolHeadBytesLength)
{
int length = HeadBytes[4] * 256 + HeadBytes[5];
if (length == 0)
{
byte[] buffer = new byte[ProtocolHeadBytesLength - 1];
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = HeadBytes[i + 1];
}
HeadBytes = buffer;
return HeadBytes[5] * 256 + HeadBytes[6] - 1;
}
else
{
return length - 2;
}
}
else
{
return 0;
}
}
/// <summary>
/// 检查头子节的合法性
/// </summary>
/// <param name="token">特殊的令牌,有些特殊消息的验证</param>
/// <returns>是否成功的结果</returns>
public bool CheckHeadBytesLegal( byte[] token )
{
if (HeadBytes == null) return false;
if (SendBytes[0] != HeadBytes[0] || SendBytes[1] != HeadBytes[1]) return false;
return HeadBytes[2] == 0x00 && HeadBytes[3] == 0x00;
}
/// <summary>
/// 获取头子节里的消息标识
/// </summary>
/// <returns>消息标识</returns>
public int GetHeadBytesIdentity( )
{
return HeadBytes[0] * 256 + HeadBytes[1];
}
/// <summary>
/// 消息头字节
/// </summary>
public byte[] HeadBytes { get; set; }
/// <summary>
/// 消息内容字节
/// </summary>
public byte[] ContentBytes { get; set; }
/// <summary>
/// 发送的字节信息
/// </summary>
public byte[] SendBytes { get; set; }
}
}