通过前面的三篇文章,我们已经基本了解ModbusRTU是个什么东西,以及如何通过C#生成需要的八种常用的通讯报文了,接下来我们就需要完整地实现ModbusRTU通讯了。
传送门:
C#实现ModbusRTU详解【一】—— 简介及仿真配置
C#实现ModbusRTU详解【二】—— 生成读取报文
C#实现ModbusRTU详解【三】—— 生成写入报文
接下来我们将会使用前面生成的读写报文,实现完整的ModbusRTU通讯。
第一步:我们需要确定使用什么样的程序来完成我们的通讯Demo,本文示例将使用Winform,也可根据个人喜好选择WPF或者其它项目类型。确定好之后创建对应的项目,并完成窗体的UI设计(功能优先)。窗体上应有通讯参数设定、通讯开关、报文参数设定、读写模式选择、报文收发显示、报文发送按钮以及接收到的报文的解析等内容。
第二步:通过前面的文章,我们已经知道了,ModbusRTU通讯一般是以串口为通讯介质的,所以我们需要一个串口通讯类,它应该包括串口的参数设定、打开与关闭、收发数据的方法或事件。
第三步:引入我们已经写好的报文生成方法,以及解析报文的方法,并根据实际需要进行结构调整。
第四步:创建所需的窗体事件,并在窗体事件中调用已经添加完成的通讯方法和报文生成方法。
第五步:测试程序是否能正常运行,并根据出现的问题修改程序,并尽量优化使用体验。
明确了思路之后,我们就可以开始搭建我们的ModbusRTU Demo了。
新建一个Winform程序,根据自己喜好创建布局,参考如下:
然后在项目中创建两个文件夹,分别用以存放通讯类及报文类,如下图所示:
根据C# 串口通讯这篇文章,我们可以得知C#如何搭建串口通讯,此处不再赘述搭建方法,以下为SerialPortHelper的代码:
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModbusRTUDemo.Communication
{
///
/// 自定义串口消息接收事件委托
///
///
///
public delegate void ReceiveDataEventHandler(object sender, ReceiveDataEventArg e);
class SerialPortHelper
{
///
/// 自定义串口消息接收事件
///
public event ReceiveDataEventHandler ReceiveDataEvent;
//串口字段
private SerialPort serialPort;
///
/// 构造函数
///
public SerialPortHelper()
{
serialPort = new SerialPort();
}
///
/// 串口状态
///
public bool Status { get => serialPort.IsOpen; }
///
/// 获取当前计算机所有的串行端口名
///
///
public static string[] GetPortArray()
{
return SerialPort.GetPortNames();
}
///
/// 串口参数
///
public void SetSerialPort(string portName, int baudrate, Parity parity, int databits, StopBits stopBits)
{
//端口名
serialPort.PortName = portName;
//波特率
serialPort.BaudRate = baudrate;
//奇偶校验
serialPort.Parity = parity;
//数据位
serialPort.DataBits = databits;
//停止位
serialPort.StopBits = stopBits;
//串口接收数据事件
serialPort.DataReceived += ReceiveDataMethod;
}
///
/// 打开串口
///
public void Open()
{
//打开串口
serialPort.Open();
}
///
/// 关闭串口
///
public void Close()
{
serialPort.Close();
}
///
/// 发送数据
///
/// 要发送的数据
public void SendDataMethod(byte[] data)
{
//获取串口状态,true为已打开,false为未打开
bool isOpen = serialPort.IsOpen;
if (!isOpen)
{
Open();
}
//发送字节数组
//参数1:包含要写入端口的数据的字节数组。
//参数2:参数中从零开始的字节偏移量,从此处开始将字节复制到端口。
//参数3:要写入的字节数。
serialPort.Write(data, 0, data.Length);
}
///
/// 发送数据
///
/// 要发送的数据
public void SendDataMethod(string data)
{
//获取串口状态,true为已打开,false为未打开
bool isOpen = serialPort.IsOpen;
if (!isOpen)
{
Open();
}
//直接发送字符串
serialPort.Write(data);
}
///
/// 串口接收到数据触发此方法进行数据读取
///
///
///
private void ReceiveDataMethod(object sender, SerialDataReceivedEventArgs e)
{
ReceiveDataEventArg arg = new ReceiveDataEventArg();
//读取串口缓冲区的字节数据
arg.Data = new byte[serialPort.BytesToRead];
serialPort.Read(arg.Data, 0, serialPort.BytesToRead);
//触发自定义消息接收事件,把串口数据发送出去
if (ReceiveDataEvent != null && arg.Data.Length != 0)
{
ReceiveDataEvent.Invoke(null, arg);
}
}
}
}
ReceiveDataEventArg类:
///
/// 串口接收数据事件的参数
///
public class ReceiveDataEventArg : EventArgs
{
///
/// 串口接收到的数据
///
public byte[] Data { get; set; }
}
通讯的实现都是常规操作,但是有一个需要注意的地方,为了满足这个类的通用性,我们为通讯方法创建了一个自定义的消息接收事件。因为我们的SerialPort类的DataReceived事件的参数是不带接收到的数据的,所以当触发DataReceived事件时,需要手动去从缓冲区去获取接收的数据。因此我们自定义了一个消息接收的事件,当串口接收到数据触发DataReceived事件时,我们在DataReceived触发的方法里去获取串口接收到的数据,然后再通过自定义事件把这个数据用参数传递出去,这样的好处就是使用这个通讯类的时候,我们不需要关注如何去获取数据。
首先还是我们的读写模式的枚举类型和校验码类:
ReadMode:
public enum ReadMode
{
Read01 = 0x01,
Read02 = 0x02,
Read03 = 0x03,
Read04 = 0x04,
}
WriteMode:
public enum WriteMode
{
Write01 = 0x05,
Write03 = 0x06,
Write01s = 0x0F,
Write03s = 0x10
}
CheckSum:
class CheckSum
{
///
/// CRC16校验码计算
///
/// 要计算的报文
///
public static byte[] CRC16(byte[] data)
{
int len = data.Length;
if (len > 0)
{
ushort crc = 0xFFFF;
for (int i = 0; i < len; i++)
{
crc = (ushort)(crc ^ (data[i]));
for (int j = 0; j < 8; j++)
{
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
}
byte hi = (byte)((crc & 0xFF00) >> 8); //高位置
byte lo = (byte)(crc & 0x00FF); //低位置
return BitConverter.IsLittleEndian ? new byte[] { lo, hi } : new byte[] { hi, lo };
}
return new byte[] { 0, 0 };
}
///
/// CRC16校验码计算
///
/// 要计算的报文
///
public static byte[] CRC16(List data)
{
int len = data.Count;
if (len > 0)
{
ushort crc = 0xFFFF;
for (int i = 0; i < len; i++)
{
crc = (ushort)(crc ^ (data[i]));
for (int j = 0; j < 8; j++)
{
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
}
byte hi = (byte)((crc & 0xFF00) >> 8); //高位置
byte lo = (byte)(crc & 0x00FF); //低位置
return BitConverter.IsLittleEndian ? new byte[] { lo, hi } : new byte[] { hi, lo };
}
return new byte[] { 0, 0 };
}
}
接着就是报文生成的类(MessageGenerationModule):
class MessageGenerationModule
{
///
/// 生成读取报文
///
/// 从站地址
/// 读取模式
/// 起始地址
/// 读取长度
///
public static byte[] MessageGeneration(int slaveStation, ReadMode mode, short startAdr, short length)
{
return GetReadMessage(slaveStation, mode, startAdr, length);
}
///
/// 生成写入报文
///
/// 从站地址
/// 写入模式
/// 起始地址
/// 写入值
///
//public static byte[] MessageGeneration(int slaveStation, WriteMode mode, short startAdr, object value)
//{
// //C# 8.0以下版本的写法:
// switch (mode)
// {
// case WriteMode.Write01:
// return GetSingleBoolWriteMessage(slaveStation, startAdr, (bool)value);
// case WriteMode.Write03:
// return GetSingleDataWriteMessage(slaveStation, startAdr, Convert.ToInt16(value));
// case WriteMode.Write01s:
// return GetArrayBoolWriteMessage(slaveStation, startAdr, (IEnumerable)value);
// case WriteMode.Write03s:
// return GetArrayDataWriteMessage(slaveStation, startAdr, (IEnumerable)value);
// default:
// return null;
// }
//}
public static byte[] MessageGeneration(int slaveStation, WriteMode mode, short startAdr, object value) => mode switch
{
//C# 8.0开始支持此写法,具体可查阅微软官方文档,switch表达式
WriteMode.Write01 => GetSingleBoolWriteMessage(slaveStation, startAdr, (bool)value),
WriteMode.Write03 => GetSingleDataWriteMessage(slaveStation, startAdr, Convert.ToInt16(value)),
WriteMode.Write01s => GetArrayBoolWriteMessage(slaveStation, startAdr, (IEnumerable)value),
WriteMode.Write03s => GetArrayDataWriteMessage(slaveStation, startAdr, (IEnumerable)value),
_ => throw new ArgumentOutOfRangeException(nameof(mode), $"Not expected WriteMode value: {mode}"),
};
///
/// 获取读取数据请求报文
///
/// 从站地址
/// 读取模式
/// 起始地址
/// 读取长度
///
private static byte[] GetReadMessage(int slaveStation, ReadMode mode, short startAdr, short length)
{
//定义临时字节列表
List temp = new List();
//依次放入头两位字节(站地址和读取模式)
temp.Add((byte)slaveStation);
temp.Add((byte)mode);
//获取起始地址及读取长度
byte[] start = BitConverter.GetBytes(startAdr);
byte[] count = BitConverter.GetBytes(length);
//判断系统是否为小端存储
//如果为true,BitConverter.GetBytes方法会返回低字节在前,高字节在后的字节数组,
//而ModbusRTU则需要高字节在前,低字节在后,所以需要做一次反转操作。
if (BitConverter.IsLittleEndian)
{
Array.Reverse(start);
Array.Reverse(count);
}
//依次放入起始地址和读取长度
temp.AddRange(start);
temp.AddRange(count);
//获取校验码并在最后放入
temp.AddRange(CheckSum.CRC16(temp));
return temp.ToArray();
}
///
/// 获取写入单个线圈的报文
///
/// 从站地址
/// 线圈地址
/// 写入值
/// 写入单个线圈的报文
private static byte[] GetSingleBoolWriteMessage(int slaveStation, short startAdr, bool value)
{
//创建字节列表
List temp = new List();
//插入站地址及功能码
temp.Add((byte)slaveStation);
temp.Add(0x05);
//获取线圈地址
byte[] start = BitConverter.GetBytes(startAdr);
//根据计算机大小端存储方式进行高低字节转换
if (BitConverter.IsLittleEndian) Array.Reverse(start);
//插入线圈地址
temp.Add(start[0]);
temp.Add(start[1]);
//插入写入值
temp.Add((byte)(value ? 0xFF : 0x00));
temp.Add(0x00);
//转换为字节数组
byte[] result = temp.ToArray();
//计算校验码并拼接,返回最后的报文结果
return result.Concat(CheckSum.CRC16(temp)).ToArray();
}
///
/// 获取写入单个寄存器的报文
///
/// 从站地址
/// 寄存器地址
/// 写入值
/// 写入单个寄存器的报文
private static byte[] GetSingleDataWriteMessage(int slaveStation, short startAdr, short value)
{
//从站地址
byte station = (byte)slaveStation;
//功能码
byte type = 0x06;
//寄存器地址
byte[] start = BitConverter.GetBytes(startAdr);
//值
byte[] valueBytes = BitConverter.GetBytes(value);
//根据计算机大小端存储方式进行高低字节转换
if (BitConverter.IsLittleEndian)
{
Array.Reverse(start);
Array.Reverse(valueBytes);
}
//拼接报文
byte[] result = new byte[] { station, type };
result = result.Concat(start.Concat(valueBytes).ToArray()).ToArray();
//计算校验码并拼接,返回最后的报文结果
return result.Concat(CheckSum.CRC16(result)).ToArray();
}
///
/// 获取写入多个线圈的报文
///
/// 从站地址
/// 起始地址
/// 写入值
/// 写入多个线圈的报文
private static byte[] GetArrayBoolWriteMessage(int slaveStation, short startAdr, IEnumerable value)
{
//定义报文临时存储字节集合
List tempList = new List();
//插入从站地址
tempList.Add((byte)slaveStation);
//插入功能码
tempList.Add(0x0F);
//获取起始地址
byte[] start = BitConverter.GetBytes(startAdr);
//获取写入线圈数量
byte[] length = BitConverter.GetBytes(Convert.ToInt16(value.Count()));
//根据计算机大小端存储方式进行高低字节转换
if (BitConverter.IsLittleEndian)
{
Array.Reverse(start);
Array.Reverse(length);
}
//插入起始地址
tempList.Add(start[0]);
tempList.Add(start[1]);
//插入写入线圈数量
tempList.Add(length[0]);
tempList.Add(length[1]);
//定义写入值字节集合
List valueTemp = new List();
//由于一个字节只有八个位,所以如果需要写入的值超过了八个,
//则需要生成一个新的字节用以存储,
//所以循环截取输入的值,然后生成对应的写入值字节
for (int i = 0; i < value.Count(); i += 8)
{
//写入值字节临时字节集合
List temp = value.Skip(i).Take(8).ToList();
//剩余位不足八个,则把剩下的所有位都放到同一个字节里
if (temp.Count != 8)
{
//取余获取剩余的位的数量
int m = value.Count() % 8;
//截取位放入临时字节集合中
temp = value.Skip(i).Take(m).ToList();
}
//获取位生成的写入值字节
byte tempByte = GetBitArray(temp);
//将生成的写入值字节拼接到写入值字节集合中
valueTemp.Add(tempByte);
}
//获取写入值的字节数
byte bytecount = (byte)valueTemp.Count;
//插入写入值的字节数
tempList.Add(bytecount);
//插入值字节集合
tempList.AddRange(valueTemp);
//根据报文字节集合计算CRC16校验码,并拼接到最后,然后转换为字节数组并返回
return tempList.Concat(CheckSum.CRC16(tempList)).ToArray();
}
///
/// 获取写入多个寄存器的报文
///
/// 从站地址
/// 起始地址
/// 写入值
/// 写入多个寄存器的报文
private static byte[] GetArrayDataWriteMessage(int slaveStation, short startAdr, IEnumerable value)
{
//定义报文临时存储字节集合
List tempList = new List();
//插入从站地址
tempList.Add((byte)slaveStation);
//插入功能码
tempList.Add(0x10);
//获取起始地址
byte[] start = BitConverter.GetBytes(startAdr);
//获取写入值的数量
byte[] length = BitConverter.GetBytes(Convert.ToInt16(value.Count()));
//根据计算机大小端存储方式进行高低字节转换
if (BitConverter.IsLittleEndian)
{
Array.Reverse(start);
Array.Reverse(length);
}
//插入起始地址
tempList.AddRange(start);
//插入写入值数量
tempList.AddRange(length);
//创建写入值字节集合
List valueBytes = new List();
//将需要插入的每个值转换为字节数组,
//并根据计算机大小端存储方式进行高低字节转换
//然后插入到值的字节集合中
foreach (var item in value)
{
byte[] temp = BitConverter.GetBytes(item);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(temp);
}
valueBytes.AddRange(temp);
}
//获取写入值的字节数
byte count = Convert.ToByte(valueBytes.Count);
//插入写入值的字节数
tempList.Add(count);
//插入写入值字节集合
tempList.AddRange(valueBytes);
//根据报文字节集合计算CRC16校验码,并拼接到最后,然后转换为字节数组并返回
return tempList.Concat(CheckSum.CRC16(tempList)).ToArray();
}
///
/// 反转顺序并生成字节
///
/// 位数据
///
private static byte GetBitArray(IEnumerable data)
{
//把位数据集合反转
data.Reverse();
//定义初始字节,值为0000 0000
byte temp = 0x00;
//循环计数
int index = 0;
//循环位集合
foreach (bool item in data)
{
//判断每一位的数据,为true则左移一个1到对应的位置
if (item) temp = (byte)(temp | (0x01 << index));
//计数+1
index++;
}
//返回最后使用位数据集合生成的二进制字节
return temp;
}
}
这里需要注意,我们所有生成报文的方法,都是通过MessageGeneration这个方法进行调用,这样在使用时,我们只需要传入不同的参数,就可以实现对不同方法的调用,并根据读写模式的不同使用不同的重载方法。而具体生成方法需要修改的时候,基本不需要修改已经调用了MessageGeneration方法的代码。
最后是解析接收从站响应报文的方法,此处不添加浮点数等数据类型的解析。如果需要浮点数处理,可以参考前面的读取报文生成的文章。
AnalysisMessage:
class AnalysisMessage
{
///
/// 解析线圈数据
///
/// 接收到的报文
///
public static BitArray GetCoil(byte[] receiveMsg)
{
//获取线圈状态
BitArray bitArray = new BitArray(receiveMsg.Skip(3).Take(Convert.ToInt32(receiveMsg[2])).ToArray());
return bitArray;
}
///
/// 解析寄存器数据
///
/// 接收到的报文
///
public static List GetRegister(byte[] receiveMsg)
{
List result = new List();
//获取字节数
int count = Convert.ToInt32(receiveMsg[2]);
int index = 0;
for (int i = 3; i < count + 3; i += 2)
{
index++;
//每个地址所属的字节数组
byte[] temp = new byte[] { receiveMsg[i + 1], receiveMsg[i] };
//获取整型结果
result.Add(BitConverter.ToInt16(temp, 0));
}
return result;
}
}
窗体通过按钮点击,打开串口连接。
///
/// 打开或者关闭串口连接
///
///
///
private void btnCon_Click(object sender, EventArgs e)
{
if (!conHelper.Status)
{
//串口号
string port = cbxPort.SelectedItem.ToString();
//波特率
int baudrate = (int)cbxBaudRate.SelectedItem;
//奇偶校验
Parity parity = GetSelectedParity();
//数据位
int databits = (int)cbxDataBits.SelectedItem;
//停止位
StopBits stopBits = GetSelectedStopBits();
//设定串口参数
conHelper.SetSerialPort(port, baudrate, parity, databits, stopBits);
//打开串口
conHelper.Open();
Thread.Sleep(200);
//刷新状态
tbxStatus.Text = conHelper.Status ? "连接成功" : "未连接";
//启用读写按钮
btnRW.Enabled = true;
btnCon.Text = "关闭串口";
}
else
{
//关闭串口
conHelper.Close();
tbxStatus.Text = conHelper.Status ? "连接成功" : "未连接";
//禁用读写按钮
btnRW.Enabled = false;
btnCon.Text = "打开串口";
}
}
这里其实就是简单的打开和关闭串口的连接,如果串口未打开,则禁用读写按钮。
根据读写模式的下拉列表的不同,修改窗体的存储读写模式的字段的值。
///
/// 读写模式切换事件
///
///
///
private void cbxMode_SelectedIndexChanged(object sender, EventArgs e)
{
//更新状态字段
GetReadWriteMode();
//计数复位
nudCount.Value = 1;
//清空输入值
tbxValue.Clear();
//是否显示提示文本
labTip.Visible = isSingleData ? false : true;
//是否可输入值
tbxValue.Enabled = isWrite ? true : false;
//是否可修改计数
nudCount.Enabled = isWrite ? false : true;
//读写按钮显示文本
btnRW.Text = isWrite ? "写入" : "读取";
}
///
/// 读写按钮事件
///
///
///
private void btnRW_Click(object sender, EventArgs e)
{
//生成的报文
byte[] message = null;
//从站地址
short station = (short)nudStation.Value;
//起始地址
short stratAdr = (short)nudAddress.Value;
//读写数量
short count = (short)nudCount.Value;
if (isWrite)
{
//生成写入报文
WriteMode mode = (WriteMode)readWriteMode;
//生成单个或多个值的写入报文
if (isSingleData)
{
//判断是否输入单个值
if (tbxValue.Text.IndexOf(",") != -1)
{
MessageBox.Show("输入值过多");
return;
}
//生成写入单个值的写入报文
if (isCoil)
{
//生成写入单个线圈的报文
bool value = false;
if (string.Equals(tbxValue.Text, "True", StringComparison.OrdinalIgnoreCase) || tbxValue.Text == "1")
{
value = true;
}
else if (string.Equals(tbxValue.Text, "False", StringComparison.OrdinalIgnoreCase) || tbxValue.Text == "0")
{
value = false;
}
else
{
MessageBox.Show("输入值只能是1、0或者true、false");
return;
}
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, value);
}
else
{
//生成写入单个寄存器的报文
try
{
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, short.Parse(tbxValue.Text));
}
catch (Exception)
{
MessageBox.Show("输入有误");
return;
}
}
}
else
{
//输入值数组
string[] arr = tbxValue.Text.Split(",");
if (isCoil)
{
//生成写入多个线圈的报文
List value = new List();
for (int i = 0; i < arr.Length; i++)
{
bool temp = false;
if (string.Equals(arr[i], "True", StringComparison.OrdinalIgnoreCase) || arr[i] == "1")
{
temp = true;
}
else if (string.Equals(tbxValue.Text, "False", StringComparison.OrdinalIgnoreCase) || arr[i] == "0")
{
temp = false;
}
else
{
MessageBox.Show("输入值只能是1、0或者true、false");
return;
}
value.Add(temp);
}
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, value);
}
else
{
//生成写入多个寄存器的报文
try
{
List value = new List();
for (int i = 0; i < arr.Length; i++)
{
value.Add(short.Parse(arr[i]));
}
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, value);
}
catch (Exception)
{
MessageBox.Show("输入有误");
return;
}
}
}
}
else
{
//生成读取报文
ReadMode mode = (ReadMode)readWriteMode;
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, count);
}
//发送报文
conHelper.SendDataMethod(message);
//将发送的报文显示在窗体中
string msgStr = "";
for (int i = 0; i < message.Length; i++)
{
msgStr += message[i].ToString("X2") + " ";
}
rbxSendMsg.Text = msgStr;
}
根据选择的模式不一样,以及输入的值的不同,我们做一次初步的解析,生成对应的写入值或者调用对应的读取方法,来获取读写报文,并通过串口发送出去,然后在窗口中显示我们所发送的报文。
///
/// 接收消息的事件
///
///
/// 事件参数
private void ReceiveDataEvent(object sender, ReceiveDataEventArg e)
{
//在窗体上显示接收到的报文
string msgStr = "";
for (int i = 0; i < e.Data.Length; i++)
{
msgStr += e.Data[i].ToString("X2") + " ";
}
rbxRecMsg.Invoke(new Action(() => { rbxRecMsg.Text = msgStr; }));
//如果是读取数据,则对接收到的消息进行解析
if (!isWrite)
{
string result = "";
if (isCoil)
{
BitArray bitArray = AnalysisMessage.GetCoil(e.Data);
int count = Convert.ToInt32(nudCount.Value);
for (int i = 0; i < count; i++)
{
result += bitArray[i].ToString() + ",";
}
}
else
{
List list = AnalysisMessage.GetRegister(e.Data);
list.ForEach(m => { result += m.ToString() + ","; });
}
tbxValue.Invoke(new Action(() => { tbxValue.Text = result.Remove(result.LastIndexOf(","), 1); }));
}
}
using ModbusRTUDemo.Communication;
using ModbusRTUDemo.Message;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO.Ports;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
namespace ModbusRTUDemo
{
public partial class DemoForm : Form
{
#region Field
///
/// 串口类
///
SerialPortHelper conHelper = new SerialPortHelper();
///
/// 是否为写入模式
///
private bool isWrite = false;
///
/// 是否读写线圈
///
private bool isCoil = true;
///
/// 是否读写单个值
///
private bool isSingleData = true;
///
/// 读写模式
///
private object readWriteMode = null;
#endregion
#region Ctor
public DemoForm()
{
InitializeComponent();
}
#endregion
#region FormEvent
///
/// 窗体加载事件
///
///
///
private void DemoForm_Load(object sender, EventArgs e)
{
//设置可选串口
cbxPort.Items.AddRange(SerialPortHelper.GetPortArray());
//设置可选波特率
cbxBaudRate.Items.AddRange(new object[] { 9600, 19200 });
//设置可选奇偶校验
cbxParity.Items.AddRange(new object[] { "None", "Odd", "Even", "Mark", "Space" });
//设置可选数据位
cbxDataBits.Items.AddRange(new object[] { 5, 6, 7, 8 });
//设置可选停止位
cbxStopBits.Items.AddRange(new object[] { 1, 1.5, 2 });
//设置读写模式
cbxMode.Items.AddRange(new object[] {
"读取输出线圈",
"读取离散输入",
"读取保持型寄存器",
"读取输入寄存器",
"写入单个线圈",
"写入多个线圈",
"写入单个寄存器",
"写入多个寄存器"
});
//设置默认选中项
cbxPort.SelectedIndex = 1;
cbxBaudRate.SelectedIndex = 0;
cbxParity.SelectedIndex = 0;
cbxDataBits.SelectedIndex = 3;
cbxStopBits.SelectedIndex = 0;
cbxMode.SelectedIndex = 0;
//显示连接状态
tbxStatus.Text = conHelper.Status ? "连接成功" : "未连接";
//从站地址默认为1
nudStation.Value = 1;
//设置为默认输入法,即为英文半角
tbxValue.ImeMode = ImeMode.Disable;
//初始化禁用读写按钮(未打开串口连接)
btnRW.Enabled = false;
//注册接收消息的事件
conHelper.ReceiveDataEvent += ReceiveDataEvent;
}
///
/// 打开或者关闭串口连接
///
///
///
private void btnCon_Click(object sender, EventArgs e)
{
if (!conHelper.Status)
{
//串口号
string port = cbxPort.SelectedItem.ToString();
//波特率
int baudrate = (int)cbxBaudRate.SelectedItem;
//奇偶校验
Parity parity = GetSelectedParity();
//数据位
int databits = (int)cbxDataBits.SelectedItem;
//停止位
StopBits stopBits = GetSelectedStopBits();
//设定串口参数
conHelper.SetSerialPort(port, baudrate, parity, databits, stopBits);
//打开串口
conHelper.Open();
Thread.Sleep(200);
//刷新状态
tbxStatus.Text = conHelper.Status ? "连接成功" : "未连接";
//启用读写按钮
btnRW.Enabled = true;
btnCon.Text = "关闭串口";
}
else
{
//关闭串口
conHelper.Close();
tbxStatus.Text = conHelper.Status ? "连接成功" : "未连接";
//禁用读写按钮
btnRW.Enabled = false;
btnCon.Text = "打开串口";
}
}
///
/// 读写按钮事件
///
///
///
private void btnRW_Click(object sender, EventArgs e)
{
//生成的报文
byte[] message = null;
//从站地址
short station = (short)nudStation.Value;
//起始地址
short stratAdr = (short)nudAddress.Value;
//读写数量
short count = (short)nudCount.Value;
if (isWrite)
{
//生成写入报文
WriteMode mode = (WriteMode)readWriteMode;
//生成单个或多个值的写入报文
if (isSingleData)
{
//判断是否输入单个值
if (tbxValue.Text.IndexOf(",") != -1)
{
MessageBox.Show("输入值过多");
return;
}
//生成写入单个值的写入报文
if (isCoil)
{
//生成写入单个线圈的报文
bool value = false;
if (string.Equals(tbxValue.Text, "True", StringComparison.OrdinalIgnoreCase) || tbxValue.Text == "1")
{
value = true;
}
else if (string.Equals(tbxValue.Text, "False", StringComparison.OrdinalIgnoreCase) || tbxValue.Text == "0")
{
value = false;
}
else
{
MessageBox.Show("输入值只能是1、0或者true、false");
return;
}
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, value);
}
else
{
//生成写入单个寄存器的报文
try
{
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, short.Parse(tbxValue.Text));
}
catch (Exception)
{
MessageBox.Show("输入有误");
return;
}
}
}
else
{
//输入值数组
string[] arr = tbxValue.Text.Split(",");
if (isCoil)
{
//生成写入多个线圈的报文
List value = new List();
for (int i = 0; i < arr.Length; i++)
{
bool temp = false;
if (string.Equals(arr[i], "True", StringComparison.OrdinalIgnoreCase) || arr[i] == "1")
{
temp = true;
}
else if (string.Equals(tbxValue.Text, "False", StringComparison.OrdinalIgnoreCase) || arr[i] == "0")
{
temp = false;
}
else
{
MessageBox.Show("输入值只能是1、0或者true、false");
return;
}
value.Add(temp);
}
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, value);
}
else
{
//生成写入多个寄存器的报文
try
{
List value = new List();
for (int i = 0; i < arr.Length; i++)
{
value.Add(short.Parse(arr[i]));
}
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, value);
}
catch (Exception)
{
MessageBox.Show("输入有误");
return;
}
}
}
}
else
{
//生成读取报文
ReadMode mode = (ReadMode)readWriteMode;
message = MessageGenerationModule.MessageGeneration(station, mode, stratAdr, count);
}
//发送报文
conHelper.SendDataMethod(message);
//将发送的报文显示在窗体中
string msgStr = "";
for (int i = 0; i < message.Length; i++)
{
msgStr += message[i].ToString("X2") + " ";
}
rbxSendMsg.Text = msgStr;
}
///
/// 读写模式切换事件
///
///
///
private void cbxMode_SelectedIndexChanged(object sender, EventArgs e)
{
//更新状态字段
GetReadWriteMode();
//计数复位
nudCount.Value = 1;
//清空输入值
tbxValue.Clear();
//是否显示提示文本
labTip.Visible = isSingleData ? false : true;
//是否可输入值
tbxValue.Enabled = isWrite ? true : false;
//是否可修改计数
nudCount.Enabled = isWrite ? false : true;
//读写按钮显示文本
btnRW.Text = isWrite ? "写入" : "读取";
}
///
/// 根据输入值的数量同步刷新窗体计数
///
///
///
private void tbxValue_TextChanged(object sender, EventArgs e)
{
nudCount.Value = Regex.Matches(tbxValue.Text, ",").Count + 1;
}
#endregion
#region Event
///
/// 接收消息的事件
///
///
/// 事件参数
private void ReceiveDataEvent(object sender, ReceiveDataEventArg e)
{
//在窗体上显示接收到的报文
string msgStr = "";
for (int i = 0; i < e.Data.Length; i++)
{
msgStr += e.Data[i].ToString("X2") + " ";
}
rbxRecMsg.Invoke(new Action(() => { rbxRecMsg.Text = msgStr; }));
//如果是读取数据,则对接收到的消息进行解析
if (!isWrite)
{
string result = "";
if (isCoil)
{
BitArray bitArray = AnalysisMessage.GetCoil(e.Data);
int count = Convert.ToInt32(nudCount.Value);
for (int i = 0; i < count; i++)
{
result += bitArray[i].ToString() + ",";
}
}
else
{
List list = AnalysisMessage.GetRegister(e.Data);
list.ForEach(m => { result += m.ToString() + ","; });
}
tbxValue.Invoke(new Action(() => { tbxValue.Text = result.Remove(result.LastIndexOf(","), 1); }));
}
}
#endregion
#region Methods
///
/// 获取窗体选中的奇偶校验
///
///
private Parity GetSelectedParity()
{
switch (cbxParity.SelectedItem.ToString())
{
case "Odd":
return Parity.Odd;
case "Even":
return Parity.Even;
case "Mark":
return Parity.Mark;
case "Space":
return Parity.Space;
case "None":
default:
return Parity.None;
}
}
///
/// 获取窗体选中的停止位
///
///
private StopBits GetSelectedStopBits()
{
switch (Convert.ToDouble(cbxStopBits.SelectedItem))
{
case 1:
return StopBits.One;
case 1.5:
return StopBits.OnePointFive;
case 2:
return StopBits.Two;
default:
return StopBits.One;
}
}
///
/// 根据选中的读写模式更新字段值
///
private void GetReadWriteMode()
{
switch (cbxMode.SelectedItem.ToString())
{
case "读取输出线圈":
default:
isWrite = false;
isSingleData = false;
isCoil = true;
readWriteMode = ReadMode.Read01;
break;
case "读取离散输入":
isWrite = false;
isSingleData = false;
isCoil = true;
readWriteMode = ReadMode.Read02;
break;
case "读取保持型寄存器":
isWrite = false;
isSingleData = false;
isCoil = false;
readWriteMode = ReadMode.Read03;
break;
case "读取输入寄存器":
isWrite = false;
isSingleData = false;
isCoil = false;
readWriteMode = ReadMode.Read04;
break;
case "写入单个线圈":
isWrite = true;
isSingleData = true;
isCoil = true;
readWriteMode = WriteMode.Write01;
break;
case "写入多个线圈":
isWrite = true;
isSingleData = false;
isCoil = true;
readWriteMode = WriteMode.Write01s;
break;
case "写入单个寄存器":
isWrite = true;
isSingleData = true;
isCoil = false;
readWriteMode = WriteMode.Write03;
break;
case "写入多个寄存器":
isWrite = true;
isSingleData = false;
isCoil = false;
readWriteMode = WriteMode.Write03s;
break;
}
}
#endregion
}
}
至此,我们的ModbusRTU的Winform的Demo就已经完成了,可以看到,八种报文的测试都是没有问题的。在实际使用的时候,我们可以选择像本文介绍的方法,自己创建连接并生成发送报文,也可以使用现有的类库,下一篇将会介绍一个现有的ModbusRTU的类库的使用。