参考资料: 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的代码进行解释。
在进行代码解释之前,先对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上位机软件
|