初心 & 目标

一直想写一个高速、稳定、支持异步的串口收发库,但总是三点只能做到两点。这次阅读 Richard.HuHslCommunication 源码,看看大神的底层串口收发是怎么做的。

注:作者由于经济原因不再开源

预设目标:

  • 底层串口收发是怎么做的?
  • 如何实现的对多种协议的兼容?
  • 如何实现对软件的全自动更新升级?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

API 地址

INetMessage 用于定义解析规则和数据信息提取规则,由以下几个部分组成:

  • 头字节长度,检查头字节合法性,从头字节中提取出来的接下来需要接收数据的长度
  • 头字节的数据,内容字节的数据
  • 头字节的标识号
  • 发送的字节信息

下面以 ModbusTcpMessage 为示例,看看实现了 INetMessage 接口后的效果:

  1. namespace HslCommunication.Core.IMessage
  2. {
  3. /// <summary>
  4. /// Modbus-Tcp协议支持的消息解析类
  5. /// </summary>
  6. public class ModbusTcpMessage : INetMessage
  7. {
  8. /// <summary>
  9. /// 消息头的指令长度
  10. /// </summary>
  11. public int ProtocolHeadBytesLength
  12. {
  13. get { return 8; }
  14. }
  15. /// <summary>
  16. /// 从当前的头子节文件中提取出接下来需要接收的数据长度
  17. /// </summary>
  18. /// <returns>返回接下来的数据内容长度</returns>
  19. public int GetContentLengthByHeadBytes( )
  20. {
  21. /************************************************************************
  22. *
  23. * 说明:为了应对有些特殊的设备,在整个指令的开端会增加一个额外的数据的时候
  24. *
  25. ************************************************************************/
  26. if (HeadBytes?.Length >= ProtocolHeadBytesLength)
  27. {
  28. int length = HeadBytes[4] * 256 + HeadBytes[5];
  29. if (length == 0)
  30. {
  31. byte[] buffer = new byte[ProtocolHeadBytesLength - 1];
  32. for (int i = 0; i < buffer.Length; i++)
  33. {
  34. buffer[i] = HeadBytes[i + 1];
  35. }
  36. HeadBytes = buffer;
  37. return HeadBytes[5] * 256 + HeadBytes[6] - 1;
  38. }
  39. else
  40. {
  41. return length - 2;
  42. }
  43. }
  44. else
  45. {
  46. return 0;
  47. }
  48. }
  49. /// <summary>
  50. /// 检查头子节的合法性
  51. /// </summary>
  52. /// <param name="token">特殊的令牌,有些特殊消息的验证</param>
  53. /// <returns>是否成功的结果</returns>
  54. public bool CheckHeadBytesLegal( byte[] token )
  55. {
  56. if (HeadBytes == null) return false;
  57. if (SendBytes[0] != HeadBytes[0] || SendBytes[1] != HeadBytes[1]) return false;
  58. return HeadBytes[2] == 0x00 && HeadBytes[3] == 0x00;
  59. }
  60. /// <summary>
  61. /// 获取头子节里的消息标识
  62. /// </summary>
  63. /// <returns>消息标识</returns>
  64. public int GetHeadBytesIdentity( )
  65. {
  66. return HeadBytes[0] * 256 + HeadBytes[1];
  67. }
  68. /// <summary>
  69. /// 消息头字节
  70. /// </summary>
  71. public byte[] HeadBytes { get; set; }
  72. /// <summary>
  73. /// 消息内容字节
  74. /// </summary>
  75. public byte[] ContentBytes { get; set; }
  76. /// <summary>
  77. /// 发送的字节信息
  78. /// </summary>
  79. public byte[] SendBytes { get; set; }
  80. }
  81. }