参考资料:
MODBUS TCP 03功能码报文解析
初识Modbus TCP-------------C#编写Modbus TCP客户端程序(一)
初识Modbus TCP-------------C#编写Modbus TCP客户端程序(二)
目前此上位机软件一共有四个版本:
上位机软件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的代码进行解释。
在进行代码解释之前,先对MODBUS TCP 03功能码的发送报文以及接收报文进行解释,这对后续理解代码有很大帮助。
MODBUS TCP 03功能码是用来读取寄存器数据的,收发报文例子如下;
客户端发送数据 1C 04 00 00 00 06 01 03 00 09 00 05
服务器端回送数据 1C 04 00 00 00 0D 01 03 0A 00 00 00 00 03 E7 00 00 00 00
如下图所示。
以下为退出按钮的事件函数,点击退出按钮后,程序就会退出。
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();
}
为了避免连接服务器发生超时掉线,我们这里做一个定时发送的函数,保证在掉线时间范围内连续向服务器发送数据,注意,需要在连接函数中增加 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);
}
接收信息函数一直在扫描服务器返回的报文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");
}
}
}
在显示信息函数中采用了“在线程里以安全方式调用控件”。正常的话会进入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];
}
}
发送函数会读取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);
}
用于将3区清空。
private void clear_Click(object sender, EventArgs e)
{
receive0x01.Text = "";
}
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 = "";
}
}
}
Modbus TCP上位机软件