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

news2025/4/26 7:33:42

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>
 /// 按位的方式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2326709.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IS-IS:单区域集成配置与多区域集成配置

一、IS-IS概述 IS-IS&#xff08;Intermediate System to Intermediate System&#xff09; 是一种链路状态内部网关协议&#xff08;IGP&#xff09;&#xff0c;设计用于自治系统&#xff08;AS&#xff09;内部的路由选择。最初由ISO为OSI模型的无连接网络服务&#xff08;…

API 安全之认证鉴权

作者&#xff1a;半天 前言 API 作为企业的重要数字资源&#xff0c;在给企业带来巨大便利的同时也带来了新的安全问题&#xff0c;一旦被攻击可能导致数据泄漏重大安全问题&#xff0c;从而给企业的业务发展带来极大的安全风险。正是在这样的背景下&#xff0c;OpenAPI 规范…

[测试] Google Test | 主流的 C 测试框架

目录 GoogleTest 2. 准备工作 3. 测试 4.怎么用 Attention is All You Need 写项目代码的时候 边写边测试 非常重要&#xff0c;这样可以帮助我们减少很多的问题。 这篇文章后面 主要以 GoogleTest 为例&#xff0c;进行介绍最近找了些 gtest 相关的资料,学习了下.后面主要…

OpenCV 图形API(3)高层次设计概览

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 描述 G-API 是一个异构框架&#xff0c;提供了统一的 API 来使用多个支持的后端编程图像处理流水线。 关键的设计理念是在指定使用哪些内核和设备时保持流…

碰一碰发视频网页版本开发的源码搭建指南

引言 在数字化信息快速传播的时代&#xff0c;近场通信&#xff08;NFC&#xff09;技术为信息交互带来了新的便捷方式。通过网页版本实现碰一碰发视频功能&#xff0c;能够让用户在浏览器环境中轻松实现视频分享&#xff0c;拓展了视频传播的途径。本文将详细介绍碰一碰发视频…

【含文档+PPT+源码】基于Python爬虫二手房价格预测与可视化系统的设计与实现

项目介绍 本课程演示的是一款基于Python爬虫二手房价格预测与可视化系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本、软件工具等所有资料 带你从零开始部署运行本套系统 该项…

Redis-16.在Java中操作Redis-Spring Data Redis使用方式-操作有序集合类型的数据

一. 操作有序集合类型的数据 package com.sky.test;import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.*;imp…

STM32智能手表——任务线程部分

RTOS和LVGL我没学过&#xff0c;但是应该能硬啃这个项目例程 ├─Application/User/Tasks # 用于存放任务线程的函数 │ ├─user_TaskInit.c # 初始化任务 │ ├─user_HardwareInitTask.c # 硬件初始化任务 │ ├─user_RunModeTasks.c…

SQL命令

一、表的创建 SQL MS Access、MySQL 和 SQL Server 数据类型 | 菜鸟教程 SQL Server 和 MySQL 中的 Date 函数 | 菜鸟教程 1.1、创建表 CREATE TABLE Citys (CityID int PRIMARY KEY,CityName varchar(255) );CREATE TABLE Per (PersonID int PRIMARY KEY, …

终端SSH连接工具SecureCRT安装和连接Linux

SecureCRT 9.5是一款集终端仿真与加密功能于一身的专业软件&#xff0c;其坚如磐石的安全性、高效的信息传输能力以及高度可定制的会话管理&#xff0c;使得它成为众多用户的首选。该软件不仅支持SSH2、SSH1、Telnet等多种协议&#xff0c;还提供了Relogin、Serial、TAPI、RAW等…

赛逸展2025“创新引擎”启动:限量席位,点亮科技绿色新征程

当今时代&#xff0c;科技革新与绿色发展已然成为推动社会进步的双引擎。2025第七届亚洲消费电子技术贸易展&#xff08;赛逸展&#xff09;敏锐捕捉这一趋势&#xff0c;重磅打造“科技创新专区”&#xff0c;并面向科技、绿色企业吹响限量招募号角。 这个独具特色的专区紧扣…

FPGA实现数码管显示分秒时间

目录 一. verilog实现 二. 烧录验证 三. 结果验证 使用开发板&#xff1a;DE2-115开发板 一. verilog实现 要实现分和秒&#xff0c;需要知道定时器的频率&#xff0c;通过查手册可知&#xff0c;我使用的开发板时钟为50hz&#xff0c;也就是时钟一个周期是2微秒。 5000000…

可视化开发:用Qt实现Excel级动态柱状图

Qt柱状图 QtChart 首先我们介绍一下 图表建立的基础&#xff1a;Qt Charts QtChart 是Qt框架的一个模块&#xff0c;专注与提供交互式数据可视化功能 俗话就是 用于用户轻松创建各种类型的图表和图形界面 它包含的图表类型有很多&#xff1a;折线图&#xff0c;饼图&#x…

从零实现Json-Rpc框架】- 项目实现 - 基于Dispatcher模块的RPC框架

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客仓库&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &…

kubekey -实现懒人一键部署K8S集群

kubekey -实现懒人一键部署K8S集群 操作步骤 官网&#xff1a; https://kubesphere.io/zh/ 一、执行以下命令快速创建一个 Kubernetes 集群。 Master节点 如果您访问 GitHub/Googleapis 受限&#xff0c;请登录 Linux 主机&#xff0c;执行以下命令设置下载区域。 [roottest ~]…

李宏毅机器学习笔记(1)—机器学习基本概念+深度学习基本概念

机器学习基本概念 1、获取模型 步骤 1.1、假定未知函数 带未知参数的函数 1.2、定义损失函数 真实值&#xff1a;label MAE MSE 几率分布&#xff0c;cross-entropy? 1.3、优化 单独考虑一个参数 让损失函数最小&#xff0c;找导数为零的点 单独考虑w&#xff0c;w…

数字IC后端项目常见问题之streamOut layermap和innovus drc violation

Q1&#xff1a;我需要将Innovus设计GDS导出到Virtuoso&#xff0c;但发现写出GDS的过程会报如下所示的警告。这里写出GDS使用的是Virtuoso (DFII) streamOut mapping文件&#xff01; Clock Gen模块Routing DRC&#xff0c;Timing分析及解决 streamOut tease.gds2 -mapFile cd…

短剧系统开发动漫短剧系统源码开发上线小程序app教程

一、市场规模与用户增长&#xff1a;突破677亿&#xff0c;Z世代成主力 整体扩张 2025年短剧市场预计同比增长15%&#xff0c;规模达677.9亿元&#xff0c;用户规模6.62亿&#xff08;占网民59.7%&#xff09;。动漫短剧作为细分领域&#xff0c;增速显著受益于二次元文化渗透&…

太阳能高杆路灯:照亮未来的新光

在全球能源转型进程加速以及可持续发展理念日益深入人心的背景下&#xff0c;太阳能高杆路灯作为融合新能源技术、智能控制技术与多功能集成特性的创新产品&#xff0c;正逐步革新传统路灯的格局。其不仅有效解决了传统路灯对电网供电的依赖问题&#xff0c;更为城市及乡村的照…

《C++Linux编程进阶:从0实现muduo 》-第8讲.C++面试如何高效获取线程ID

章节重点 在C面试时&#xff0c;经常被问到如果高效获取线程ID&#xff0c;但不少同学都不知道如何回答。 重点是通过__thread关键字。 重点内容 视频讲解&#xff1a;《CLinux编程进阶&#xff1a;从0实现muduo C网络框架系列》-第8讲. C面试如何高效获取线程ID 测试获取线…