分享

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

 吴敬锐 2022-12-03 发布于广东

参考资料:
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上位机软件

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多