C#实现ModbusRTU详解【四】—— 通讯Demo

前言

通过前面的三篇文章,我们已经基本了解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
{
    /// <summary>
    /// 自定义串口消息接收事件委托
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public delegate void ReceiveDataEventHandler(object sender, ReceiveDataEventArg e);

    class SerialPortHelper
    {
        /// <summary>
        /// 自定义串口消息接收事件
        /// </summary>
        public event ReceiveDataEventHandler ReceiveDataEvent;

        //串口字段
        private SerialPort serialPort;

        /// <summary>
        /// 构造函数
        /// </summary>
        public SerialPortHelper()
        {
            serialPort = new SerialPort();
        }

        /// <summary>
        /// 串口状态
        /// </summary>
        public bool Status { get => serialPort.IsOpen; }

        /// <summary>
        /// 获取当前计算机所有的串行端口名
        /// </summary>
        /// <returns></returns>
        public static string[] GetPortArray()
        {
            return SerialPort.GetPortNames();
        }

        /// <summary>
        /// 串口参数
        /// </summary>
        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;
        }

        /// <summary>
        /// 打开串口
        /// </summary>
        public void Open()
        {
            //打开串口
            serialPort.Open();
        }

        /// <summary>
        /// 关闭串口
        /// </summary>
        public void Close()
        {
            serialPort.Close();
        }

        /// <summary>
        /// 发送数据
        /// </summary>
        /// <param name="data">要发送的数据</param>
        public void SendDataMethod(byte[] data)
        {
            //获取串口状态,true为已打开,false为未打开
            bool isOpen = serialPort.IsOpen;

            if (!isOpen)
            {
                Open();
            }

            //发送字节数组
            //参数1:包含要写入端口的数据的字节数组。
            //参数2:参数中从零开始的字节偏移量,从此处开始将字节复制到端口。
            //参数3:要写入的字节数。 
            serialPort.Write(data, 0, data.Length);
        }

        /// <summary>
        /// 发送数据
        /// </summary>
        /// <param name="data">要发送的数据</param>
        public void SendDataMethod(string data)
        {
            //获取串口状态,true为已打开,false为未打开
            bool isOpen = serialPort.IsOpen;

            if (!isOpen)
            {
                Open();
            }

            //直接发送字符串
            serialPort.Write(data);
        }

        /// <summary>
        /// 串口接收到数据触发此方法进行数据读取
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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类:

/// <summary>
/// 串口接收数据事件的参数
/// </summary>
public class ReceiveDataEventArg : EventArgs
{
    /// <summary>
    /// 串口接收到的数据
    /// </summary>
    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
{
    /// <summary>
    /// CRC16校验码计算
    /// </summary>
    /// <param name="data">要计算的报文</param>
    /// <returns></returns>
    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 };
    }

    /// <summary>
    /// CRC16校验码计算
    /// </summary>
    /// <param name="data">要计算的报文</param>
    /// <returns></returns>
    public static byte[] CRC16(List<byte> 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
{
    /// <summary>
    /// 生成读取报文
    /// </summary>
    /// <param name="slaveStation">从站地址</param>
    /// <param name="mode">读取模式</param>
    /// <param name="startAdr">起始地址</param>
    /// <param name="length">读取长度</param>
    /// <returns></returns>
    public static byte[] MessageGeneration(int slaveStation, ReadMode mode, short startAdr, short length)
    {
        return GetReadMessage(slaveStation, mode, startAdr, length);
    }

    /// <summary>
    /// 生成写入报文
    /// </summary>
    /// <param name="slaveStation">从站地址</param>
    /// <param name="mode">写入模式</param>
    /// <param name="startAdr">起始地址</param>
    /// <param name="value">写入值</param>
    /// <returns></returns>
    //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<bool>)value);
    //        case WriteMode.Write03s:
    //            return GetArrayDataWriteMessage(slaveStation, startAdr, (IEnumerable<short>)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<bool>)value),
        WriteMode.Write03s => GetArrayDataWriteMessage(slaveStation, startAdr, (IEnumerable<short>)value),
        _ => throw new ArgumentOutOfRangeException(nameof(mode), $"Not expected WriteMode value: {mode}"),
    };

    /// <summary>
    /// 获取读取数据请求报文
    /// </summary>
    /// <param name="slaveStation">从站地址</param>
    /// <param name="mode">读取模式</param>
    /// <param name="startAdr">起始地址</param>
    /// <param name="length">读取长度</param>
    /// <returns></returns>
    private static byte[] GetReadMessage(int slaveStation, ReadMode mode, short startAdr, short length)
    {
        //定义临时字节列表
        List<byte> temp = new List<byte>();

        //依次放入头两位字节(站地址和读取模式)
        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();
    }

    /// <summary>
    /// 获取写入单个线圈的报文
    /// </summary>
    /// <param name="slaveStation">从站地址</param>
    /// <param name="startAdr">线圈地址</param>
    /// <param name="value">写入值</param>
    /// <returns>写入单个线圈的报文</returns>
    private static byte[] GetSingleBoolWriteMessage(int slaveStation, short startAdr, bool value)
    {
        //创建字节列表
        List<byte> temp = new List<byte>();

        //插入站地址及功能码
        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();
    }

    /// <summary>
    /// 获取写入单个寄存器的报文
    /// </summary>
    /// <param name="slaveStation">从站地址</param>
    /// <param name="startAdr">寄存器地址</param>
    /// <param name="value">写入值</param>
    /// <returns>写入单个寄存器的报文</returns>
    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();
    }

    /// <summary>
    /// 获取写入多个线圈的报文
    /// </summary>
    /// <param name="slaveStation">从站地址</param>
    /// <param name="startAdr">起始地址</param>
    /// <param name="value">写入值</param>
    /// <returns>写入多个线圈的报文</returns>
    private static byte[] GetArrayBoolWriteMessage(int slaveStation, short startAdr, IEnumerable<bool> value)
    {
        //定义报文临时存储字节集合
        List<byte> tempList = new List<byte>();

        //插入从站地址
        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<byte> valueTemp = new List<byte>();

        //由于一个字节只有八个位,所以如果需要写入的值超过了八个,
        //则需要生成一个新的字节用以存储,
        //所以循环截取输入的值,然后生成对应的写入值字节
        for (int i = 0; i < value.Count(); i += 8)
        {
            //写入值字节临时字节集合
            List<bool> 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();
    }

    /// <summary>
    /// 获取写入多个寄存器的报文
    /// </summary>
    /// <param name="slaveStation">从站地址</param>
    /// <param name="startAdr">起始地址</param>
    /// <param name="value">写入值</param>
    /// <returns>写入多个寄存器的报文</returns>
    private static byte[] GetArrayDataWriteMessage(int slaveStation, short startAdr, IEnumerable<short> value)
    {
        //定义报文临时存储字节集合
        List<byte> tempList = new List<byte>();

        //插入从站地址
        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<byte> valueBytes = new List<byte>();

        //将需要插入的每个值转换为字节数组,
        //并根据计算机大小端存储方式进行高低字节转换
        //然后插入到值的字节集合中
        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();
    }

    /// <summary>
    /// 反转顺序并生成字节
    /// </summary>
    /// <param name="data">位数据</param>
    /// <returns></returns>
    private static byte GetBitArray(IEnumerable<bool> 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
{
    /// <summary>
    /// 解析线圈数据
    /// </summary>
    /// <param name="receiveMsg">接收到的报文</param>
    /// <returns></returns>
    public static BitArray GetCoil(byte[] receiveMsg)
    {
        //获取线圈状态
        BitArray bitArray = new BitArray(receiveMsg.Skip(3).Take(Convert.ToInt32(receiveMsg[2])).ToArray());

        return bitArray;
    }

    /// <summary>
    /// 解析寄存器数据
    /// </summary>
    /// <param name="receiveMsg">接收到的报文</param>
    /// <returns></returns>
    public static List<short> GetRegister(byte[] receiveMsg)
    {
        List<short> result = new List<short>();
        //获取字节数
        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;
    }
}


窗体

通讯建立

窗体通过按钮点击,打开串口连接。

/// <summary>
/// 打开或者关闭串口连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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 = "打开串口";
    }
}

这里其实就是简单的打开和关闭串口的连接,如果串口未打开,则禁用读写按钮。

模式切换

根据读写模式的下拉列表的不同,修改窗体的存储读写模式的字段的值。

/// <summary>
/// 读写模式切换事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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 ? "写入" : "读取";
}

读写事件

/// <summary>
/// 读写按钮事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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<bool> value = new List<bool>();
                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<short> value = new List<short>();
                    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;
}

根据选择的模式不一样,以及输入的值的不同,我们做一次初步的解析,生成对应的写入值或者调用对应的读取方法,来获取读写报文,并通过串口发送出去,然后在窗口中显示我们所发送的报文。

消息接收事件

/// <summary>
/// 接收消息的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e">事件参数</param>
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<short> 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

        /// <summary>
        /// 串口类
        /// </summary>
        SerialPortHelper conHelper = new SerialPortHelper();

        /// <summary>
        /// 是否为写入模式
        /// </summary>
        private bool isWrite = false;

        /// <summary>
        /// 是否读写线圈
        /// </summary>
        private bool isCoil = true;

        /// <summary>
        /// 是否读写单个值
        /// </summary>
        private bool isSingleData = true;

        /// <summary>
        /// 读写模式
        /// </summary>
        private object readWriteMode = null;

        #endregion

        #region Ctor

        public DemoForm()
        {
            InitializeComponent();
        }

        #endregion

        #region FormEvent

        /// <summary>
        /// 窗体加载事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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;
        }

        /// <summary>
        /// 打开或者关闭串口连接
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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 = "打开串口";
            }
        }

        /// <summary>
        /// 读写按钮事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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<bool> value = new List<bool>();
                        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<short> value = new List<short>();
                            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;
        }


        /// <summary>
        /// 读写模式切换事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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 ? "写入" : "读取";
        }

        /// <summary>
        /// 根据输入值的数量同步刷新窗体计数
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void tbxValue_TextChanged(object sender, EventArgs e)
        {
            nudCount.Value = Regex.Matches(tbxValue.Text, ",").Count + 1;
        }

        #endregion

        #region Event

        /// <summary>
        /// 接收消息的事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e">事件参数</param>
        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<short> 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

        /// <summary>
        /// 获取窗体选中的奇偶校验
        /// </summary>
        /// <returns></returns>
        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;
            }
        }

        /// <summary>
        /// 获取窗体选中的停止位
        /// </summary>
        /// <returns></returns>
        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;
            }
        }

        /// <summary>
        /// 根据选中的读写模式更新字段值
        /// </summary>
        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的类库的使用。

  • 51
    点赞
  • 256
    收藏
    觉得还不错? 一键收藏
  • 29
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值