当前位置: 首页 > news >正文

基于modbusTcp连接Modbus Slave本地模拟服务通讯(C#编写ModbusTcp类库)(一)

C#编写ModbusTcp类库,模拟plc进行本地通信测试

Modbus是一个应用层协议,常用于工业自动化设备之间的通信,主要有两种传输方式:RTU和TCP。

常见的功能码包括读取线圈(01)、读取离散输入(02)、读保持寄存器(03)、读输入寄存器(04)、写单个线圈(05)、写单个寄存器(06)、写多个线圈(15)、写多个寄存器(16)等。类库需要支持这些基本操作。

一、协议基础:

  • Modbus TCP 使用 TCP/IP 协议,默认端口 ‌502‌。

  • 数据帧格式:事务标识符(2字节) + 协议标识符(2字节) + 长度(2字节) + 单元标识符(1字节) + Modbus PDU(功能码 + 数据)。
    在这里插入图片描述

二、常用功能码‌:

  • 03 功能码‌:读取保持寄存器(Read Holding Registers)。
  • 06 功能码‌:写单个寄存器(Write Single Register)。

三、工具准备‌:

  • Modbus Slave 模拟器‌:如 Modbus Slave ,用于模拟从站设备。下载地址: 模拟器下载地址
  • 配置从站的 IP(如 127.0.0.1)、端口(502)、寄存器地址和初始值。
    在这里插入图片描述

四、C# 实现步骤‌:

  • 使用 TcpClient 建立 TCP 连接。
  • 构造 Modbus 请求报文并发送。
  • 接收响应报文并解析数据。
  • 处理异常和超时。
    在这里插入图片描述

五、代码实现:

  1. 建立数据连接:
/// <summary>
/// 连接
/// </summary>
/// <returns></returns>
protected override Result Connect()
{
   
    var result = new Result();
    socket?.SafeClose();
    socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    try
    {
   
        //超时时间设置
        socket.ReceiveTimeout = timeout;
        socket.SendTimeout = timeout;

        //连接
        IAsyncResult connectResult = socket.BeginConnect(ipEndPoint, null, null);
        //阻塞当前线程           
        if (!connectResult.AsyncWaitHandle.WaitOne(timeout))
            throw new TimeoutException("连接超时");
        socket.EndConnect(connectResult);
    }
    catch (Exception ex)
    {
   
        socket?.SafeClose();
        result.IsSucceed = false;
        result.Err = ex.Message;
        result.ErrCode = 408;
        result.Exception = ex;
    }
    return result.EndTime();
}

2.断开连接

public static void SafeClose(this Socket socket)
{
   
    try
    {
   
        if (socket?.Connected ?? false) socket?.Shutdown(SocketShutdown.Both);//正常关闭连接
    }
    catch {
    }

    try
    {
   
        socket?.Close();
    }
    catch {
    }
}

3.读取数据

/// <summary>
/// 读取数据
/// </summary>
/// <param name="address">寄存器起始地址</param>
/// <param name="stationNumber">站号</param>
/// <param name="functionCode">功能码</param>
/// <param name="readLength">读取长度</param>
/// <param name="byteFormatting">大小端转换</param>
/// <returns></returns>
public Result<byte[]> Read(string address, byte stationNumber = 1, byte functionCode = 3, ushort readLength = 1, bool byteFormatting = true)
{
   
    var result = new Result<byte[]>();

    if (!socket?.Connected ?? true)
    {
   
        var conentResult = Connect();
        if (!conentResult.IsSucceed)
        {
   
            conentResult.Err = $"读取 地址:{
     address} 站号:{
     stationNumber} 功能码:{
     functionCode} 失败。{
      conentResult.Err}";
            return result.SetErrInfo(conentResult);
        }
    }
    try
    {
   
        var chenkHead = GetCheckHead(functionCode);
        //1 获取命令(组装报文)
        byte[] command = GetReadCommand(address, stationNumber, functionCode, readLength, chenkHead);
        result.Requst = string.Join(" ", command.Select(t => t.ToString("X2")));
        //获取响应报文
        var sendResult = SendPackageReliable(command);
        if (!sendResult.IsSucceed)
        {
   
            sendResult.Err = $"读取 地址:{
     address} 站号:{
     stationNumber} 功能码:{
     functionCode} 失败。{
      sendResult.Err}";
            return result.SetErrInfo(sendResult).EndTime();
        }
        var dataPackage = sendResult.Value;
        byte[] resultBuffer = new byte[dataPackage.Length - 9];
        Array.Copy(dataPackage, 9, resultBuffer, 0, resultBuffer.Length);
        result.Response = string.Join(" ", dataPackage.Select(t => t.ToString("X2")));
        //4 获取响应报文数据(字节数组形式)             
        if (byteFormatting)
            result.Value = resultBuffer.Reverse().ToArray().ByteFormatting(format);
        else
            result.Value = resultBuffer.Reverse().ToArray();

        if (chenkHead[0] != dataPackage[0] || chenkHead[1] != dataPackage[1])
        {
   
            result.IsSucceed = false;
            result.Err = $"读取 地址:{
     address} 站号:{
     stationNumber} 功能码:{
     functionCode} 失败。响应结果校验失败";
            socket?.SafeClose();
        }
        else if (ModbusHelper.VerifyFunctionCode(functionCode, dataPackage[7]))
        {
   
            result.IsSucceed = false;
            result.Err = ModbusHelper.ErrMsg(dataPackage[8]);
        }
    }
    catch (SocketException ex)
    {
   
        result.IsSucceed = false;
        if (ex.SocketErrorCode == SocketError.TimedOut)
        {
   
            result.Err = $"读取 地址:{
     address} 站号:{
     stationNumber} 功能码:{
     functionCode} 失败。连接超时";
            socket?.SafeClose();
        }
        else
        {
   
            result.Err = $"读取 地址:{
     address} 站号:{
     stationNumber} 功能码:{
     functionCode} 失败。{
      ex.Message}";
        }
    }
    finally
    {
   
        if (isAutoOpen) Dispose();
    }
    return result.EndTime();
}


/// <summary>
/// 获取随机校验头
/// </summary>
/// <returns></returns>
private byte[] GetCheckHead(int seed)
{
   
    var random = new Random(DateTime.Now.Millisecond + seed);
    return new byte[] {
    (byte)random.Next(255), (byte)random.Next(255) };
}

/// <summary>
/// 获取读取命令
/// </summary>
/// <param name="address">寄存器起始地址</param>
/// <param name="stationNumber">站号</param>
/// <param name="functionCode">功能码</param>
/// <param name="length">读取长度</param>
/// <returns></returns>
public byte[] GetReadCommand(string address, byte stationNumber, byte functionCode, ushort length, byte[] check = null)
{
   
    var readAddress = ushort.Parse(address?.Trim());
    if (plcAddresses) readAddress = (ushort)(Convert.ToUInt16(address?.Trim().Substring(1)) - 1);

    byte[] buffer = new byte[12];
    buffer[0] = check?[0] ?? 0x19;
    buffer[1] = check?[1] ?? 0xB2;//Client发出的检验信息
    buffer[2] = 0x00;
    buffer[3] = 0x00;//表示tcp/ip 的协议的Modbus的协议
    buffer[4] = 0x00;
    buffer[5] = 0x06;//表示的是该字节以后的字节长度

    buffer[6] = stationNumber;  //站号
    buffer[7] = functionCode;   //功能码
    buffer[8] = BitConverter.GetBytes(readAddress)[1];
    buffer[9] = BitConverter.GetBytes(readAddress)[0];//寄存器地址
    buffer[10] = BitConverter.GetBytes(length)[1];
    buffer[11] = BitConverter.GetBytes(length)[0];//表示request 寄存器的长度(寄存器个数)
    return buffer;
}

/// <summary>
/// 发送报文,并获取响应报文(如果网络异常,会自动进行一次重试)
/// TODO 重试机制应改成用户主动设置
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
public Result<byte[]> SendPackageReliable(byte[] command)
{
   
    try
    {
   
        var result = SendPackageSingle(command);
        if (!result.IsSucceed)
        {
   
            WarningLog?.Invoke(result.Err, result.Exception);
            //如果出现异常,则进行一次重试         
            var conentResult = Connect();
            if (!conentResult.IsSucceed)
                return new Result<byte[]>(conentResult);

            return SendPackageSingle(command);
        }
        else
            return result;
    }
    catch (Exception ex)
    {
   
        try
        {
   
            WarningLog?.Invoke(ex.Message, ex);
            //如果出现异常,则进行一次重试                
            var conentResult = Connect();
            if (!conentResult.IsSucceed)
                return new Result<byte[]>(conentResult);

            return SendPackageSingle(command);
        }
        catch (Exception ex2)
        {
   
            Result<byte[]> result = new Result<byte[]>();
            result.IsSucceed = false;
            result.Err = ex2.Message;
            result.AddErr2List();
            return result.EndTime();
        }
    }
}


4.其他类型数据读取

 /// <summary>
 /// 读取Int16类型数据
 /// </summary>
 /// <param name="address">寄存器起始地址</param>
 /// <param name="stationNumber">站号</param>
 /// <param name="functionCode">功能码</param>
 /// <returns></returns>
 public Result<short> ReadInt16(string address, byte stationNumber = 1, byte functionCode = 3)
 {
   
     var readResut = Read(address, stationNumber, functionCode);
     var result = new Result<short>(readResut);
     if (result.IsSucceed)
         result.Value = BitConverter.ToInt16(readResut.Value, 0);
     return result.EndTime();
 }

 /// <summary>
 /// 按位的方式

相关文章:

  • VMware Workstation Pro下载链接
  • 【图像去噪】论文复现:灵感源自MAE!进一步解决BSN的局限性,破坏真实噪声的空间相关性!AMSNet的Pytorch源码复现,跑通源码,原理详解!
  • SQL Server:数据库镜像端点检查
  • 图解AUTOSAR_SWS_CANStateManager
  • STM32 FATFS - 在spi的SD卡中运行fatfs
  • 招标采购管理系统智能化亮点应用场景举例
  • 基于Spring Boot的平面设计课程在线学习平台系统的设计与实现(LW+源码+讲解)
  • MySQL五十题
  • C 语言测验
  • 《Linux运维总结:基于银河麒麟V10+ARM64架构CPU二进制部署单实例rabbitmq3.10.25》
  • windows使用nvm管理node版本
  • 3. 费曼学习法?
  • 性能比拼: Pingora vs Nginx (My NEW Favorite Proxy)
  • 蓝桥杯 刷题对应的题解
  • stm32 can 遥控帧的问题
  • 全局曝光与卷帘曝光
  • 海康摄像头通过Web插件进行预览播放和控制
  • IP 地址规划中的子网划分:/18 网络容纳 64 个 C 段(/24)的原理与应用解析
  • SpringCould微服务架构之Docker(8)
  • 面基:Java项目中跟钉钉接口对接,如何确保数据传输安全性和稳定性
  • 巴基斯坦称成功拦截印度导弹,空军所有资产安全
  • 第四轮伊美核谈判将于11日在阿曼举行
  • 2025年度上海市住房城乡建设管理委工程系列中级职称评审工作启动
  • 央行:下阶段将实施好适度宽松的货币政策
  • 美众议院通过法案将“墨西哥湾”更名为“美国湾”
  • 保证断电、碰撞等事故中车门系统能够开启!隐藏式门把手将迎来强制性国家标准