利用C#语言采用.Net Framework 4.5框架编写MODBUS TCP上位机软件

参考资料:
MODBUS TCP 03功能码报文解析
初识Modbus TCP-------------C#编写Modbus TCP客户端程序(一)
初识Modbus TCP-------------C#编写Modbus TCP客户端程序(二)


0. 软件描述

目前此上位机软件一共有四个版本:

  • 上位机软件v1.0版本功能:可以设置服务器的IP地址与端口号。客户端只能发送固定的报文,并接收服务器返回的报文,若要改变发送的报文,需要在程序中进行更改。

  • 上位机软件v1.1版本功能:在v1.0基础上增加了清空接收窗口的功能。

  • 上位机软件v1.2版本功能:在v1.1基础上,可以在客户端上发送任意报文。

  • 上位机软件v1.3版本功能:在v1.2基础上,增加了一个textbox,用于显示服务器返回报文中的部分有用信息。

  • 上位机软件v1.4版本功能:每间隔100ms,客户端自动向服务器发送一个03功能码报文,服务器接收到报文后会向客户端返回报文,客户端会将服务器返回报文进行解析,并将有用信息显示在v1.3增加的textbox中。(这就可以实现:上位机实时监控PLC某些参数(如压力、温度等)的变化)

由于V1.4集合了前面版本的各种功能,因此本文着重对V1.4的代码进行解释。

1. MODBUS TCP 报文解析

在进行代码解释之前,先对MODBUS TCP 03功能码的发送报文以及接收报文进行解释,这对后续理解代码有很大帮助。

MODBUS TCP 03功能码是用来读取寄存器数据的,收发报文例子如下;

客户端发送数据 1C 04 00 00 00 06 01 03 00 09 00 05

  • 1C 04代表交互标识, 00 00代表协议标识, 00 06代表数据长度为6个字节(数据长度计算起始点是06的后一位,报文是以16进制进行表示的,所以一个数字代表4位二进制,两个数字即8位二进制,即一个字节), 01代表设备地址, 03代表功能代码,00 09代表从%MW9(40010)开始,00 05代表读取数据长度"5"

服务器端回送数据 1C 04 00 00 00 0D 01 03 0A 00 00 00 00 03 E7 00 00 00 00

  • 1C 04代表交互标识, 00 00代表协议标识, 00 0D代表报文长度为13个字节, 01代表设备地址, 03代表功能代码,0A代表数据长度10个字节,03 E7代表%MW11(40012)=3*2^8+231=999

2. 上位机代码

2.1 设计器实现

如下图所示。

  • “服务器信息”区域用来填写PLC服务器的IP地址和端口号。
  • 1区用来填写客户端要发送的报文,填写之后点击发送,报文就会发送至服务器。
  • 2区用来显示从服务器返回报文中解析出来的有用信息。
  • 3区用来显示服务器返回的完整报文。
  • 4区是一个客户端发送报文的提示区,如果忘记客户端发送报文的格式,可以直接从4区复制对应功能码的实例发送报文,然后修改成自己想要的发送报文。

在这里插入图片描述

2.2 代码分块解析

2.2.1 退出按钮

以下为退出按钮的事件函数,点击退出按钮后,程序就会退出。

		private void exit_Click(object sender, EventArgs e)
		{
			Application.Exit();
		}

2.2.2 连接函数

		public void Connect()
		{
			byte[] data = new byte[1024];
			
			string ipadd = serverIP.Text.Trim();//将服务器 IP 地址存放在字符串 ipadd 中
			int port = Convert.ToInt32(serverPort.Text.Trim());//将端口号强制为 32 位整型,存放在 port 中

		
			//创建一个套接字
		
			IPEndPoint ie = new IPEndPoint(IPAddress.Parse(ipadd), port);
			newclient = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
		ProtocolType.Tcp);
		
			//将套接字与远程服务器地址相连
			try
			{
				newclient.Connect(ie);
				connect.Enabled = false;//使连接按钮变成虚的,无法点击
				Connected = true;
			}
			catch (SocketException e)
			{
				MessageBox.Show("连接服务器失败 " + e.Message);
				return;
			}
			
			ThreadStart myThreaddelegate = new ThreadStart(ReceiveMsg);
			myThread = new Thread(myThreaddelegate);
			myThread.Start();
			timersend.Enabled = true;
		}

2.2.3 连接按钮

点击连接按钮后,触发连接函数,在客户端和服务器之间建立连接。

		private void connect_Click_1(object sender, EventArgs e)
		{
			Connect();
		}

2.2.4 定时发送函数

为了避免连接服务器发生超时掉线,我们这里做一个定时发送的函数,保证在掉线时间范围内连续向服务器发送数据,注意,需要在连接函数中增加 timersend.Enabled = true;,在连接服务器的同时来触发定时发送。

也可以将想要定时发送给服务器的报文填进data内,这样就可以实现定时发送功能。

		private void timersend_Tick(object sender, EventArgs e)
		{
			int isecond = 1000;//以毫秒为单位
			timersend.Interval = isecond;//1 秒触发一次
			byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00,0x00, 0x00, 0x03 };//这行代码是定时(每一秒)向客户端发送 01 功能码请求。
			
			newclient.Send(data);
		}

2.2.5 接收信息函数

接收信息函数一直在扫描服务器返回的报文data。为了说明接收信息函数的工作原理,举一个例子。

这里举例,当服务器返回的报文为00 01 00 00 00 09 01 03 06 00 05 00 06 00 07

首先,函数内部创建了一个大小为1024的byte类型名为data的数组。然后通过代码newclient.Receive(data);将服务器返回报文存储进data数组内部,接着读取数组内的第六位数值09并将其赋值给名为“length”的整形数据,这是因为在服务器返回的报文中,第六位指的是09后面的报文长度,单位字节,由于不同报文长度不同,因此必须定义报文长度为变量,并且由于报文的第六位一定是报文长度信息,所以可以直接读取报文第六位数据。然后定义一个名为datashow的数组,将其长度定义为恰好是接收报文的总长度。然后通过一个for循环,将data数组内的接收报文赋值给datashow数组。接着把数组转换为16进制字符串,便于后续将其显示在上位机中。最后判断报文的第八位是否为预设数据中的任意一个,由于报文第八位保存的信息是功能代码信息,所以判断接收报文是否符合标准,例子中的第八位是03,为03功能码,符合标准,调用showMsg01函数进行显示,如果接收报文不符合标准,则不予显示。

		public void ReceiveMsg()
		{
			while (true)
			{
				byte[] data = new byte[1024];
				newclient.Receive(data);
				int length = data[5];
				Byte[] datashow = new byte[length + 6];
				for (int i = 0; i <= length + 5; i++)
					datashow[i] = data[i];
				string stringdata = BitConverter.ToString(datashow);//把数组转换成 16 进制字符串
				
				if (data[7] == 0x01 || data[7] == 0x02 || data[7] == 0x03 || data[7] == 0x05|| data[7] == 0x06 || data[7] == 0x0F || data[7] == 0x10)
				{
					showMsg01(stringdata + "\r\n");
				}
			}
		}
		

2.2.6 显示信息函数

在显示信息函数中采用了“在线程里以安全方式调用控件”。正常的话会进入else内部。接下来,依然采用2.2.5的例子来进行工作原理讲解。

当服务器返回的报文为00 01 00 00 00 09 01 03 06 00 05 00 06 00 07

receive0x01.AppendText(msg);代码将报文直接显示到3区域中,如下图所示。

string[] data = msg.Split('-');代码将msg-为分割标准进行分割并存储在string数组data中。然后通过代码int length = Convert.ToInt32(data[5]);取得接收报文的报文长度信息。最后,通过代码information.Text = data[6 + length - 1];将报文中的最后一位07显示到区域2中。
在这里插入图片描述

		public void showMsg01(string msg)
		{
			//在线程里以安全方式调用控件
			if (receive0x01.InvokeRequired)
			{
				MyInvoke _myinvoke = new MyInvoke(showMsg01);
				receive0x01.Invoke(_myinvoke, new object[] { msg });
			}
			else
			{
				receive0x01.AppendText(msg);
				
				string[] data = msg.Split('-');
				int length = Convert.ToInt32(data[5]);
				information.Text = data[6 + length - 1];
			}
		}

2.2.7 发送函数

发送函数会读取1区内用户填写的要发送的报文,然后将其发送至服务器。为了说明发送函数的工作原理,举一个例子。

例如,客户端发送报文为000100000006010300000003。这里注意,每个字节之间不能加空格。
在这里插入图片描述
首先,创建一个名为data的长度为1024的byte类型数组。然后通过for循环,将报文中每两个数为一组存进data数组中。然后读取data数组的第六位,获取报文长度。接着创建一个空数组datashow,其长度刚好为发送报文的总长度。然后通过for循环,将data中的报文内容复制到datashow中。最后将其发送至服务器。

		private void send01_Click(object sender, EventArgs e)
		{
			byte[] data = new byte[1024];
			for (int i = 0; i < (trans.Text.Length - trans.Text.Length % 2) / 2; i++)
				data[i] = Convert.ToByte(trans.Text.Substring(i * 2, 2), 16);
			int length = data[5];
			byte[] datashow = new byte[length + 6];
			for (int i = 0; i <= length + 5; i++)
				datashow[i] = data[i];
				
			newclient.Send(datashow);
		}

2.2.8 清空函数

用于将3区清空。

		private void clear_Click(object sender, EventArgs e)
		{
			receive0x01.Text = "";
		}

2.3 完整代码

using System;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using System.Text;

namespace Modbus_TCP_Client
{
	public partial class Form1 : Form
	{
		public Socket newclient;
		public bool Connected;
		public Thread myThread;
		public delegate void MyInvoke(string str);
		public Form1()
		{
			InitializeComponent();
		}
		
		private void exit_Click(object sender, EventArgs e)
		{
			Application.Exit();
		}
		
		public void Connect()
		{
			byte[] data = new byte[1024];
			
			string ipadd = serverIP.Text.Trim();//将服务器 IP 地址存放在字符串 ipadd 中
			int port = Convert.ToInt32(serverPort.Text.Trim());//将端口号强制为 32 位整型,存放在 port 中
		
		
			//创建一个套接字
		
			IPEndPoint ie = new IPEndPoint(IPAddress.Parse(ipadd), port);
			newclient = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
		ProtocolType.Tcp);
		
			//将套接字与远程服务器地址相连
			try
			{
				newclient.Connect(ie);
				connect.Enabled = false;//使连接按钮变成虚的,无法点击
				Connected = true;
			}
			catch (SocketException e)
			{
				MessageBox.Show("连接服务器失败 " + e.Message);
				return;
			}
			
			ThreadStart myThreaddelegate = new ThreadStart(ReceiveMsg);
			myThread = new Thread(myThreaddelegate);
			myThread.Start();
			timersend.Enabled = true;
		}
		
		private void connect_Click_1(object sender, EventArgs e)
		{
			Connect();
		}
		
		private void timersend_Tick(object sender, EventArgs e)
		{
			int isecond = 1000;//以毫秒为单位
			timersend.Interval = isecond;//1 秒触发一次
			byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00,0x00, 0x00, 0x03 };//这行代码是定时(每一秒)向客户端发送 01 功能码请求。
			
			newclient.Send(data);
		}
		
		public void ReceiveMsg()
		{
			while (true)
			{
				byte[] data = new byte[1024];
				newclient.Receive(data);
				int length = data[5];
				Byte[] datashow = new byte[length + 6];
				for (int i = 0; i <= length + 5; i++)
					datashow[i] = data[i];
				string stringdata = BitConverter.ToString(datashow);//把数组转换成 16 进制字符串
				
				if (data[7] == 0x01 || data[7] == 0x02 || data[7] == 0x03 || data[7] == 0x05|| data[7] == 0x06 || data[7] == 0x0F || data[7] == 0x10)
				{
					showMsg01(stringdata + "\r\n");
				}
			}
		}
		
		private void send01_Click(object sender, EventArgs e)
		{
			byte[] data = new byte[1024];
			for (int i = 0; i < (trans.Text.Length - trans.Text.Length % 2) / 2; i++)
				data[i] = Convert.ToByte(trans.Text.Substring(i * 2, 2), 16);
			int length = data[5];
			byte[] datashow = new byte[length + 6];
			for (int i = 0; i <= length + 5; i++)
				datashow[i] = data[i];
				
			newclient.Send(datashow);
		}
		
		public void showMsg01(string msg)
		{
			//在线程里以安全方式调用控件
			if (receive0x01.InvokeRequired)
			{
				MyInvoke _myinvoke = new MyInvoke(showMsg01);
				receive0x01.Invoke(_myinvoke, new object[] { msg });
			}
			else
			{
				receive0x01.AppendText(msg);
				
				string[] data = msg.Split('-');
				int length = Convert.ToInt32(data[5]);
				information.Text = data[6 + length - 1];
			}
		}
		
		private void clear_Click(object sender, EventArgs e)
		{
			receive0x01.Text = "";
		}
	}
}

3. 代码下载

Modbus TCP上位机软件

  • 9
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
c# tcplistener 是一个用于在TCP网络上监听连接的类。它提供了一些简单的方法,可以在阻塞同步模式下侦听和接受传入的连接请求。TcpListener可以使用TcpClient或Socket来连接。你可以使用IPEndPoint、本地IP地址和端口号或者只使用端口号来创建TcpListener。如果将本地IP地址设为Any,将本地端口号设为0(如果希望基础服务提供程序为您分配这些值),那么可以使用LocalEndpoint来标识已指定的信息。通过使用TcpListener,上位机可以作为服务端监听来自机器人的连接请求。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C#,winform,Tcp通信源码使用TcpListener和TcpClient 源码](https://download.csdn.net/download/l726972012/85241896)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [c#_TcpListener&TcpClient](https://blog.csdn.net/qq_48705696/article/details/116501621)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吮指原味张

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值