C#自定义实现ModbusTCP主站通讯

313 阅读8分钟
  • 摘要: 本文描述了如何使用C#实现ModbusTCP通讯功能,使用System.Net.Sockets库自定义实现。
  • 【原文】:mp.weixin.qq.com/s/z8qZk5dFI…
  • 【作者】:编程笔记in

  • 前言

    • Modbus TCP 是一种基于 TCP/IP 协议的工业通信协议。废话不多说,本文描述如何使用System.Net.Sockets库实现ModbusTCP通讯,通过Socket对象发送报文请求、接收响应数据实现ModbusTCP数据的读写功能,详细内容下面展开描述。

    • (一)案例功能

      • 如下案例程序实现了ModbusTCP常用的基本功能

        • 1、读取线圈(功能码:0x01)。
        • 2、读取离散输入(功能码:0x02)。
        • 3、读取保持寄存器(功能码:0x03)。
        • 4、读取输入寄存器(功能码:0x04)。
        • 5、写单个线圈(功能码:0x05)。
        • 6、写单个寄存器(功能码:0x06)。
        • 7、写多个线圈(功能码:0x0F)。
        • 8、写多个寄存器(功能码:0x10)。
      • 除了上面的功能外,还编写了简单的界面实现基本的交互功能,如连接、读取、写入、消息显示(显示读写的数据、报文)。

      • 报文的显示是通过触发事件返回,流程如下:

        • 1、ModbusTCP类中创建事件。
        • 2、在主窗体绑定事件。
        • 3、将数据(byte[])作为事件参数传递(读取或写入时)。
        • 4、窗体类接收到参数后显示(将数据转换通用报文格式)。
      /// <summary>
      /// 事件:请求报文
      /// </summary>
      public event EventHandler<ModbusMessageEvents> RequestMessage;
      /// <summary>
      /// 事件:响应报文
      /// </summary>
      public event EventHandler<ModbusMessageEvents> ResponseMessage;
      
    • (二)基本通讯流程

      • 1、建立TCP主站通讯连接到从站(IP:502端口)。
      • 2、主站发送Modbus请求帧。
      • 3、从站处理请求并返回响应帧。
    • (三)报文的基本组成

      • 1、读取数据|写单个数据写
        【事务标识符】【协议标识符】【剩余长度】【设备号】【功能码】【起始地址】【数据长度】
      • 2、写多个数据
        【事务标识符】【协议标识符】【剩余长度】【设备号】【功能码】【起始地址】【数据个数】【数据长度】
      • 如下方法为创建读取数据(也可用写单个数据)请求的方法
      /// <summary>
      /// 构建Modbus读取请求:
      /// </summary>
      /// <param name="funcCode">功能码</param>
      /// <param name="startAddress">起始地址</param>
      /// <param name="numberOfPoints">地址个数</param>
      /// <param name="unitId">单元ID</param>
      /// <returns>请求数组(byte)</returns>
      private byte[] BuildReadRequest(byte funcCode, ushort startAddress, ushort numberOfPoints, byte unitId = 1)
      {
          // 1、创建基本长度数组
          byte[] request = request = new byte[12];
          // 2、事务标识符
          request[0] = (byte)(_transactionId >> 8);
          request[1] = (byte)_transactionId;
          // 3、协议标识符 (0 for Modbus)
          request[2] = 0;
          request[3] = 0;
          // 4、剩余长度6:(unitId,功能代码,地址,长度)
          request[4] = 0;
          request[5] = 6;
          // 5、单元标识符:
          request[6] = unitId;
          // 6、功能码
          request[7] = funcCode;
          // 7、起始地址
          request[8] = (byte)(startAddress >> 8);
          request[9] = (byte)startAddress;
          // 8、数据长度
          request[10] = (byte)(numberOfPoints >> 8);
          request[11] = (byte)numberOfPoints;
          _transactionId++;
          return request;
      }
      

  • 运行环境

    • 操作系统:Win11
    • 编程软件:Visual Studio 2022
    • .Net版本:.Net Framework 4.8.0

一、预览

(一)运行效果

在这里插入图片描述

  • 二、代码

    • MainForm 设计

    界面如下图,添加一些通讯参数、读写、消息显示控件。读取和写入共用功能码、起始地址。读取时才需要数据长度。

在这里插入图片描述

  • (一)MainForm代码

    public partial class MainForm : Form
    {
        ModbusTcp modbusTcp;
    
        #region 界面初始化、加载、初始化参数
        public MainForm()
        {
            InitializeComponent();
            this.CenterToParent();
            rtbx_Message.ForeColor = Color.Gray;
        }
        private void MainForm_Load(object sender, EventArgs e)
        {
            Initialize();
        }
        private void Initialize()
        {
            modbusTcp = new ModbusTcp();
            modbusTcp.RequestMessage += ModbusTcp_RequestMessage;
            modbusTcp.ResponseMessage += ModbusTcp_ResponseMessage;
            //控件
            ControlStateUpdate();
            string[] funcCodes = {  "01_读取线圈","02_读取离散输入",  
                                    "03_读取保持寄存器", "04_读取输入寄存器",
                                    "05_写单个线圈", "06_写单个寄存器",
                                    "15_写多个线圈", "16_写多个寄存器" };
            cbx_FuncCode.DataSource = funcCodes;
        }
        #endregion
    
        #region 事件方法
        private void btn_Connect_Click(object sender, EventArgs e)
        {
            try
            {
                if (modbusTcp == null) return;
                if (!modbusTcp.IsConnected)
                {
                    modbusTcp.Connect(tbx_IPAddress.Text);
                    MessageUpdate("连接成功...", Color.Green);
                    ControlStateUpdate();
                }
                else
                {
                    modbusTcp.Disconnect();
                    ControlStateUpdate();
                    MessageUpdate("断开连接...", Color.Red);
                }
            }
            catch (Exception ex)
            {
                MessageUpdate(ex.Message, Color.Red);
                modbusTcp.Disconnect();
                ControlStateUpdate();
            }
        }
        private void btn_ReadData_Click(object sender, EventArgs e)
        {
            if (!modbusTcp.IsConnected)
            {
                MessageUpdate("ModbusTCP未连接!!!请连接后再尝试读取数据...",Color.Red);
                return;
            }
            bool[] coils = null;
            ushort[] registers = null;
            switch (modbusTcp.FuncCode)
            {
                case "01":
                   coils = modbusTcp.ReadCoils(modbusTcp.DataModel.ReadStartAddress, modbusTcp.DataModel.ReadDataLength);
                    MessageUpdate($"[{modbusTcp.DataModel.ReadStartAddress}][{ArrayToString(coils)}]",
                       Color.Blue, $"# 读取 线圈 >");
                    break;
                case "02":
                    coils = modbusTcp.ReadDiscreteInputs(modbusTcp.DataModel.ReadStartAddress, modbusTcp.DataModel.ReadDataLength);
                    MessageUpdate($"[{modbusTcp.DataModel.ReadStartAddress}][{ArrayToString(coils)}]",
                       Color.Blue, $"# 读取 离散输入>");
    
                    break;
                case "03":
                    registers = modbusTcp.ReadHoldingRegisters(modbusTcp.DataModel.ReadStartAddress, modbusTcp.DataModel.ReadDataLength);
                    MessageUpdate($"[{modbusTcp.DataModel.ReadStartAddress}][{ArrayToString(registers)}]",
                        Color.Blue, $"# 读取 保持寄存器 >");
                    break;
                case "04":
                    registers = modbusTcp.ReadInputRegisters(modbusTcp.DataModel.ReadStartAddress, modbusTcp.DataModel.ReadDataLength);
                    MessageUpdate($"[{modbusTcp.DataModel.ReadStartAddress}][{ArrayToString(registers)}]",
                        Color.Blue, $"# 读取 输入寄存器 >");
                    break;
            }
        }
        private void btn_SendData_Click(object sender, EventArgs e)
        {
            byte[] responseData = null;
            switch (modbusTcp.FuncCode)
            {
                case "05":
                    string coil = rtbx_SendData.Text.Trim().ToLower();
                    if (coil.Equals("true")|| coil.Equals("false"))
                    {
                        responseData = modbusTcp.WriteSingleCoil(modbusTcp.DataModel.ReadStartAddress, coil.Equals("true")? true:false);
                        MessageUpdate($"{coil}", Color.Green, $"# 写入 单个线圈 >");
                    }
                    else
                        MessageUpdate($"请输入有效数值类型...如:true 或 false", Color.Red, $"# 写入数据 >");
                    break;
                case "06":
                    string register = rtbx_SendData.Text.Trim().ToLower();
                    if (ushort.TryParse(register, out ushort result))
                    {
                        responseData = modbusTcp.WriteSingleRegister(modbusTcp.DataModel.ReadStartAddress, result);
                        MessageUpdate($"{register}", Color.Green, $"# 写入 单个寄存器 >");
                    }
                    else
                        MessageUpdate($"请输入有效数值类型...如:0,1,2,3...", Color.Red, $"# 写入数据 >");
                    break;
                case "15":
                    responseData = modbusTcp.WriteMultipleCoils(modbusTcp.DataModel.ReadStartAddress, ParseArray<bool>(rtbx_SendData.Text));
                    MessageUpdate($"{ArrayToString(ParseArray<bool>(rtbx_SendData.Text))}", Color.Green, $"# 写入 多个线圈 >");
                    break;
                case "16":
                    responseData = modbusTcp.WriteMultipleRegisters(modbusTcp.DataModel.ReadStartAddress, ParseArray<ushort>(rtbx_SendData.Text));
                    MessageUpdate($"{ArrayToString(ParseArray<ushort>(rtbx_SendData.Text))}", Color.Green, $"# 写入 寄存器 >");
                    break;
            }
        }
        private void btn_ClearMessage_Click(object sender, EventArgs e)
        {
            rtbx_Message.Clear();
        }
        private void btn_ClearSendData_Click(object sender, EventArgs e)
        {
            rtbx_SendData.Clear();
        }
        private void rtbx_Message_TextChanged(object sender, EventArgs e)
        {
            label_RecvLine.Text = $"row:{rtbx_Message.Lines.Length}";
        }
        /// <summary>
        /// 响应报文
        /// </summary>
        private void ModbusTcp_ResponseMessage(object sender, ModbusMessageEvents args)
        {
            if (checkBox_PrintResponseMessage.Checked)
            {
                ModbusMessageEvents message = args as ModbusMessageEvents;
                MessageUpdate($"{ArrayToHex<byte>(message.Message)}", Color.Blue, $"# 接收:响应报文 >");
            }
        }
        /// <summary>
        /// 请求报文
        /// </summary>
        private void ModbusTcp_RequestMessage(object sender, ModbusMessageEvents args)
        {
            if (checkBox_PrintRequestMessage.Checked)
            {
                ModbusMessageEvents message = args as ModbusMessageEvents;
                MessageUpdate($"{ArrayToHex<byte>(message.Message)}", Color.Green, $"# 发送:请求报文 >");
            }
        }
        #endregion
    
        #region 数据转换
        /// <summary>
        /// 数组转字符串
        /// </summary>
        private string ArrayToString<T>(T[] values, string sep = " ")
        {
            return string.Join(sep, values.Select(r => Convert.ToString(r)).ToArray());
        }
        /// <summary>
        /// 数组转HEX字符串
        /// </summary>
        private string ArrayToHex<T>(T[] values, string sep = " ") where T : struct, IConvertible
        {
            return string.Join(sep, values.Select(b => b.ToInt32(null).ToString("X2")));
        }
        /// <summary>
        /// 字符串转换数组
        /// </summary>
        private T[] ParseArray<T>(string input)
        {
            string[] items = input.Trim('[', ']').Split(',');
            return items.Select(item => (T)Convert.ChangeType(item.Trim(), typeof(T))).ToArray();
        }
        #endregion
    
        #region 控件状态更新
        /// <summary>
        /// 控件状态更新:根据连接状态
        /// </summary>
        private void ControlStateUpdate()
        {
            tbx_IPAddress.Enabled = modbusTcp.IsConnected ? false : true;
            tbx_Port.Enabled = modbusTcp.IsConnected ? false : true;
            btn_ConnectState.BackColor = modbusTcp.IsConnected ? Color.Green : Color.Red;
            btn_Connect.Invoke(new Action(() => {
                btn_Connect.Text = modbusTcp.IsConnected ? "关闭" : "连接";
            }));
            btn_ReadData.Invoke(new Action(() => {
                btn_ReadData.Enabled = modbusTcp.IsConnected;
                cbx_FuncCode_SelectedIndexChanged(null, null);
            }));
            btn_SendData.Invoke(new Action(() => {
                btn_SendData.Enabled = modbusTcp.IsConnected;
                cbx_FuncCode_SelectedIndexChanged(null,null);
            }));
        }
        #endregion
    
        #region 操作消息更新
        /// <summary>
        /// 操作消息更新
        /// </summary>
        private void MessageUpdate(string data, Color color, string appendText = null, int maxLineNum = 1000, bool isAppendTime = true)
        {
            // 空数据检查
            if (string.IsNullOrEmpty(data)) return;
            // 线程安全调用
            if (rtbx_Message.InvokeRequired)
            {
                rtbx_Message.BeginInvoke(new Action(() => MessageUpdate(data, color, appendText, maxLineNum, isAppendTime)));
                return;
            }
            lock (rtbx_Message)
            {
                rtbx_Message.SuspendLayout(); // 暂停重绘提高性能
                try
                {
    
                    if (rtbx_Message.Lines.Length > maxLineNum)
                    {
                        rtbx_Message.Clear();
                    }
                    if (isAppendTime)
                    {
                        rtbx_Message.AppendText($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}]:");
                    }
                    if (!string.IsNullOrEmpty(appendText))
                    {
                        rtbx_Message.AppendText($"{appendText}{Environment.NewLine}");
                    }
                    else
                    {
                        rtbx_Message.AppendText($"{Environment.NewLine}");
                    }
                    int startIndex = rtbx_Message.TextLength;
                    rtbx_Message.ScrollToCaret();
                    rtbx_Message.SelectionStart = rtbx_Message.TextLength;
                    rtbx_Message.AppendText($"{data}{Environment.NewLine}");
                    SetTextColor(rtbx_Message, startIndex, data.Length, color);
                }
                finally
                {
                    rtbx_Message.ResumeLayout(); // 恢复重绘
                }
            }
        }
        /// <summary>
        /// 设置文本框指定范围内的文本颜色
        /// </summary>
        private void SetTextColor(RichTextBox rtb, int startIndex, int length, Color color)
        {
            rtb.Invoke(new Action(() => {
                // 保存当前选择状态
                int originalStart = rtb.SelectionStart;
                int originalLength = rtb.SelectionLength;
                // 设置新选择范围
                rtb.Select(startIndex, length);
                // 更改选中文本的颜色
                rtb.SelectionColor = color;
                // 恢复原始选择状态
                rtb.Select(originalStart, originalLength);
            }));
        }
    
        #endregion
    
        #region 参数变更
        private void nudx_StartAddress_ValueChanged(object sender, EventArgs e)
        {
            if (ushort.TryParse(nudx_StartAddress.Value.ToString(),out ushort result))
            {
                modbusTcp.DataModel.ReadStartAddress = result;
            }
            else
            {
                nudx_StartAddress.Value = modbusTcp.DataModel.ReadStartAddress;
            }
        }
        private void nudx_DataLength_ValueChanged(object sender, EventArgs e)
        {
            if (ushort.TryParse(nudx_DataLength.Value.ToString(), out ushort result))
            {
                modbusTcp.DataModel.ReadDataLength = result;
            }
            else
            {
                nudx_DataLength.Value = modbusTcp.DataModel.ReadDataLength;
            }
        }
        private void cbx_FuncCode_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (cbx_FuncCode == null || cbx_FuncCode.SelectedItem==null) return;
            modbusTcp.FuncCode = cbx_FuncCode.SelectedItem.ToString().Split('_')[0];
            modbusTcp.DataModel.FuncCode = (ModbusFunctionCode)byte.Parse(modbusTcp.FuncCode);
            if (modbusTcp.IsConnected && (modbusTcp.FuncCode.Equals("05")
                || modbusTcp.FuncCode.Equals("06")
                || modbusTcp.FuncCode.Equals("15")
                || modbusTcp.FuncCode.Equals("16")))
            {
                btn_SendData.Enabled = true;
                btn_ReadData.Enabled = false;
            }
            else
            {
                btn_SendData.Enabled = false;
                btn_ReadData.Enabled = true;
            }
        }
        #endregion
    }
    
  • (二)ModbusTCP 类

    • 创建基本的连接、断开、读取、写入功能。以及创建构建请求、响应、验证等方法。
    public class ModbusTcp
    {
        #region 字段|属性|事件
        /// <summary>
        /// 事件:请求报文
        /// </summary>
        public event EventHandler<ModbusMessageEvents> RequestMessage;
        /// <summary>
        /// 事件:响应报文
        /// </summary>
        public event EventHandler<ModbusMessageEvents> ResponseMessage;
    
        private ushort _transactionId = 0;
        private bool isConnected = false;
        private Socket _socket;
        private IPAddress ipAddress;
        private IPEndPoint remoteEP;
        public int ReceiveTimeout { get; set; } = 3000;
        public int SendTimeout { get; set; } = 2000;
        public bool IsConnected { get => isConnected; }
        public DataModel DataModel { get; private set; } = new DataModel();
        public string FuncCode { get; set; } = "01";
    
        #endregion
    
        #region 连接|断开
        public void Connect(string address, int port = 502)
        {
            try
            {
                Disconnect();
                ipAddress = IPAddress.Parse(address);
                remoteEP = new IPEndPoint(ipAddress, port);
                _socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                _socket.ReceiveTimeout = ReceiveTimeout;
                _socket.SendTimeout = SendTimeout;
                _socket.Connect(remoteEP);
                isConnected = true;
            }
            catch (Exception ex)
            {
                isConnected = false;
                throw new Exception($"创建连接失败...,Exception :{ex.Message}");
            }
        }
        public void Disconnect()
        {
            try
            {
                if (IsConnected)
                {
                    _socket?.Shutdown(SocketShutdown.Both);
                    _socket?.Close();
                    isConnected = false;
                }
            }
            catch (Exception ex)
            {
                throw new Exception($"{ex.Message}");
            }
        }
        #endregion
    
        #region 读取
        /// <summary>
        /// 读取线圈(功能码0x01 )
        /// </summary>
        public bool[] ReadCoils(ushort startAddress, ushort numberOfPoints, byte unitId = 1)
        {
            _socket.ReceiveTimeout = ReceiveTimeout;
            try
            {
                // 发送请求
                byte[] request = BuildReadRequest(0x01, startAddress, numberOfPoints, unitId);
                _socket.Send(request);
                // 请求报文
                OnRequestMessage(new ModbusMessageEvents(request));
                byte[] response = ResponseParse(numberOfPoints, 0x01);
                // 响应报文
                OnResponseMessage(new ModbusMessageEvents(response));
                // 验证功能码
                ValidateResponse(0x01, response, request);
                // 解析响应的内容(线圈状态按位存储)
                bool[] coils = new bool[numberOfPoints];
                for (int i = 0; i < numberOfPoints; i++)
                {
                    int byteIndex = 9 + i / 8;
                    int bitIndex = i % 8;
                    coils[i] = (response[byteIndex] & (1 << bitIndex)) != 0;
                }
                return coils;
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                throw new TimeoutException($"读取操作超时({ReceiveTimeout}ms)", ex);
            }
        }
        /// <summary>
        /// 读取离散输入(功能码0x02 )
        /// </summary>
        public bool[] ReadDiscreteInputs(ushort startAddress, ushort numberOfPoints, byte unitId = 1)
        {
            _socket.ReceiveTimeout = ReceiveTimeout;
            try
            {
                // 发送请求
                byte[] request = BuildReadRequest(0x02, startAddress, numberOfPoints, unitId);
                _socket.Send(request);
                // 请求报文
                OnRequestMessage(new ModbusMessageEvents(request));
                byte[] response = ResponseParse(numberOfPoints, 0x02);
                // 响应报文
                OnResponseMessage(new ModbusMessageEvents(response));
                // 验证功能码
                ValidateResponse(0x02, response, request);
                // 解析响应的内容(线圈状态按位存储)
                bool[] coils = new bool[numberOfPoints];
                for (int i = 0; i < numberOfPoints; i++)
                {
                    int byteIndex = 9 + i / 8;
                    int bitIndex = i % 8;
                    coils[i] = (response[byteIndex] & (1 << bitIndex)) != 0;
                }
                return coils;
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                throw new TimeoutException($"读取操作超时({ReceiveTimeout}ms)", ex);
            }
        }
        /// <summary>
        /// 读取保持寄存器(功能码0x03 )
        /// </summary>
        public ushort[] ReadHoldingRegisters(ushort startAddress, ushort numberOfPoints, byte unitId = 1)
        {
            _socket.ReceiveTimeout = ReceiveTimeout;
            try
            {
                //发送请求
                byte[] request = BuildReadRequest(0x03, startAddress, numberOfPoints, unitId);
                _socket.Send(request);
                //请求报文
                OnRequestMessage(new ModbusMessageEvents(request));
                //响应接收
                byte[] response = ResponseParse(numberOfPoints, 0x03);
                //响应报文
                OnResponseMessage(new ModbusMessageEvents(response));
                //验证响应
                ValidateResponse(0x03, response,request);
                //解析响应的内容
                ushort[] registers = new ushort[numberOfPoints];
                //根据读取的寄存器个数遍历解析实际值。
                for (int i = 0; i < numberOfPoints; i++)
                {
                    int offset = 9 + i * 2;
                    registers[i] = (ushort)((response[offset] << 8) | response[offset + 1]);
                }
                //返回结果
                return registers;
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                throw new TimeoutException($"读取操作超时({ReceiveTimeout}ms)", ex);
            }
        }
        /// <summary>
        /// 读取输入寄存器(功能码0x04 )
        /// </summary>
        public ushort[] ReadInputRegisters(ushort startAddress, ushort numberOfPoints, byte unitId = 1)
        {
            _socket.ReceiveTimeout = ReceiveTimeout;
            try
            {
                //发送请求
                byte[] request = BuildReadRequest(0x04, startAddress, numberOfPoints, unitId);
                _socket.Send(request);
                //请求报文
                OnRequestMessage(new ModbusMessageEvents(request));
                //响应接收
                byte[] response = ResponseParse(numberOfPoints,0x04);
                //响应报文
                OnResponseMessage(new ModbusMessageEvents(response));
                //验证响应
                ValidateResponse(0x04, response, request);
                //解析响应的内容
                ushort[] registers = new ushort[numberOfPoints];
                //根据读取的寄存器个数遍历解析实际值。
                for (int i = 0; i < numberOfPoints; i++)
                {
                    int offset = 9 + i * 2;
                    registers[i] = (ushort)((response[offset] << 8) | response[offset + 1]);
                }
                //返回结果
                return registers;
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                throw new TimeoutException($"读取操作超时({ReceiveTimeout}ms)", ex);
            }
        }
        #endregion
    
        #region 写入
    
        /// <summary>
        /// 写入单个线圈(功能码0x05 )
        /// </summary>
        public byte[] WriteSingleCoil(ushort startAddress, bool inputValue, byte unitId = 1)
        {
            _socket.ReceiveTimeout = ReceiveTimeout;
            try
            {
                // 发送请求
                byte[] request = BuildWritequest(0x05, startAddress, (inputValue ? (ushort)0xFF00 : (ushort)0x0000), unitId);
                _socket.Send(request);
                // 请求报文
                OnRequestMessage(new ModbusMessageEvents(request,true));
                // 读取响应
                byte[] response = ResponseParse(1, 0x05);
                // 响应报文
                OnResponseMessage(new ModbusMessageEvents(response, true));
                return response;
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                throw new TimeoutException($"读取操作超时({ReceiveTimeout}ms)", ex);
            }
        }
        /// <summary>
        /// 写入单个寄存器(功能码0x06 )
        /// </summary>
        public byte[] WriteSingleRegister(ushort startAddress, ushort inputValue, byte unitId = 1)
        {
            _socket.ReceiveTimeout = ReceiveTimeout;
            try
            {
                // 发送请求
                byte[] request = BuildWritequest(0x06, startAddress, inputValue, unitId);
                _socket.Send(request);
                // 请求报文
                OnRequestMessage(new ModbusMessageEvents(request, true));
                // 读取响应
                byte[] response = ResponseParse(1, 0x06);
                // 响应报文
                OnResponseMessage(new ModbusMessageEvents(response, true));
                return response;
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                throw new TimeoutException($"读取操作超时({ReceiveTimeout}ms)", ex);
            }
        }
        /// <summary>
        /// 写入多个线圈(功能码0x0F )
        /// </summary>
        public byte[] WriteMultipleCoils(ushort startAddress, bool[] data, byte unitId = 1)
        {
            _socket.ReceiveTimeout = ReceiveTimeout;
            try
            {
                int byteCount = (data.Length + 7) / 8;
                byte[] byteArray = new byte[byteCount];
    
                // 将bool数组转换为字节数组
                for (int i = 0; i < data.Length; i++)
                {
                    if (data[i])
                    {
                        byteArray[i / 8] |= (byte)(1 << (i % 8));
                    }
                }
                // 发送请求
                byte[] request = BuildMutiWriteRequest(0x0F, startAddress, byteArray, (ushort)data.Length, unitId);
    
                _socket.Send(request);
                // 请求报文
                OnRequestMessage(new ModbusMessageEvents(request,true));
                byte[] response = ResponseParse(1, 0x0F);
                // 响应报文
                OnResponseMessage(new ModbusMessageEvents(response, true));
                return response;
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                throw new TimeoutException($"读取操作超时({ReceiveTimeout}ms)", ex);
            }
        }
        /// <summary>
        /// 写入多个寄存器(功能码0x10)
        /// </summary>
        public byte[] WriteMultipleRegisters(ushort startAddress, ushort[] data, byte unitId = 1)
        {
            _socket.ReceiveTimeout = ReceiveTimeout;
            try
            {
                int byteCount = (data.Length*2);
                byte[] byteArray = new byte[byteCount];
                // 将ushort数组转换为字节数组
                for (int i = 0; i < data.Length; i++)
                {
                    byteArray[2 * i] = (byte)(data[i] >> 8);
                    byteArray[1 + 2 * i] = (byte)data[i];
                }
                // 发送请求
                byte[] request = BuildMutiWriteRequest(0x10, startAddress, byteArray, (ushort)data.Length, unitId);
    
                _socket.Send(request);
                // 请求报文
                OnRequestMessage(new ModbusMessageEvents(request, true));
                byte[] response = ResponseParse(1, 0x10);
                // 响应报文
                OnResponseMessage(new ModbusMessageEvents(response, true));
                return response;
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                throw new TimeoutException($"读取操作超时({ReceiveTimeout}ms)", ex);
            }
        }
        #endregion
    
        #region 请求构建|响应|验证方法...
        /// <summary>
        /// 构建写入多个请求
        /// </summary>
        /// <param name="functionCode">功能码</param>
        /// <param name="startAddress">起始地址</param>
        /// <param name="dataLengthOrValue">数据</param>
        /// <param name="additionalData">数据个数</param>
        /// <param name="unitId">单元ID(设备地址、站号、从站ID)</param>
        private byte[] BuildMutiWriteRequest(byte funcCode, ushort startAddress, byte[] data, ushort number,byte unitId = 1)
        {
            int dataLength = data.Length;
            // 1、创建基本长度数组
            byte[] request = new byte[13 + dataLength];
            // 2、事务标识符
            request[0] = (byte)(_transactionId >> 8);
            request[1] = (byte)_transactionId;
            // 3、协议标识符 (0 for Modbus)
            request[2] = 0;
            request[3] = 0;
            // 4、从下标5开始的剩余长度:unitId,功能代码,起始地址,数据长度 + 附加数据
            uint length = (ushort)(7 + dataLength);
            request[4] = (byte)(length >> 8);
            request[5] = (byte)length;
            // 5、单元标识符:
            request[6] = unitId;
            // 6、功能码
            request[7] = funcCode;
            // 7、起始地址
            request[8] = (byte)(startAddress >> 8);
            request[9] = (byte)startAddress;
            // 8、数据个数
            request[10] = (byte)(number >> 8);
            request[11] = (byte)number;
            // 9、设置剩余数据长度 
            request[12] = (byte)(dataLength);
            // 10、 写多个寄存器|线圈
            Array.Copy(data, 0, request, 13, dataLength);
            _transactionId++;
            return request;
        }
        /// <summary>
        /// 构建写入请求
        /// </summary>
        private byte[] BuildWritequest(byte funcCode, ushort startAddress, ushort data, byte unitId = 1)
        {
            // 1、创建基本长度数组
            byte[] request = request = new byte[12];
            // 2、事务标识符
            request[0] = (byte)(_transactionId >> 8);
            request[1] = (byte)_transactionId;
            // 3、协议标识符 (0 for Modbus)
            request[2] = 0;
            request[3] = 0;
            // 4、剩余长度6:(unitId,功能代码,地址,长度)
            request[4] = 0;
            request[5] = 6;
            // 5、单元标识符:
            request[6] = unitId;
            // 6、功能码
            request[7] = (byte)funcCode;
            // 7、起始地址
            request[8] = (byte)(startAddress >> 8);
            request[9] = (byte)startAddress;
            // 8、数据
            request[10] = (byte)(data >> 8);
            request[11] = (byte)data;
            _transactionId++;
            return request;
        }
        /// <summary>
        /// 构建Modbus读取请求:
        /// </summary>
        /// <param name="funcCode">功能码</param>
        /// <param name="startAddress">起始地址</param>
        /// <param name="numberOfPoints">地址个数</param>
        /// <param name="unitId">单元ID</param>
        /// <returns>请求数组(byte)</returns>
        private byte[] BuildReadRequest(byte funcCode, ushort startAddress, ushort numberOfPoints, byte unitId = 1)
        {
            // 1、创建基本长度数组
            byte[] request = request = new byte[12];
            // 2、事务标识符
            request[0] = (byte)(_transactionId >> 8);
            request[1] = (byte)_transactionId;
            // 3、协议标识符 (0 for Modbus)
            request[2] = 0;
            request[3] = 0;
            // 4、剩余长度6:(unitId,功能代码,地址,长度)
            request[4] = 0;
            request[5] = 6;
            // 5、单元标识符:
            request[6] = unitId;
            // 6、功能码
            request[7] = funcCode;
            // 7、起始地址
            request[8] = (byte)(startAddress >> 8);
            request[9] = (byte)startAddress;
            // 8、数据长度
            request[10] = (byte)(numberOfPoints >> 8);
            request[11] = (byte)numberOfPoints;
            _transactionId++;
            return request;
        }
        /// <summary>
        /// 解析响应数据
        /// </summary>
        private byte[] ResponseParse(ushort number,byte funcCode)
        {
            byte[] response = null;
            int byteCount = 0;
            //根据功能码 设置响应长度 并创建字节数组(长度为响应长度)。
            switch ((ModbusFunctionCode)funcCode)
            {
                // 读取类功能码:
                //MBAP头(7) + 功能码(1) + 字节数(1) + 数据(byteCount)
                case ModbusFunctionCode.ReadCoils:
                case ModbusFunctionCode.ReadDiscreteInputs:
                    byteCount = (number + 7) / 8;
                    response = new byte[9 + byteCount]; 
                    break;
                // MBAP头(7) + 功能码(1) + 字节数(1) + 数据(2*number)
                case ModbusFunctionCode.ReadHoldingRegisters:
                case ModbusFunctionCode.ReadInputRegisters:
                    byteCount = 9 + 2 * number;
                    response = new byte[byteCount];
                    break;
    
                // 写入类功能码(单条):
                //MBAP头(7) + 功能码(1) + 地址(2) + 值(2)
                case ModbusFunctionCode.WriteSingleCoil:
                case ModbusFunctionCode.WriteSingleRegister:
    
                    response = new byte[12];
                    break;
                // 写入类功能码(多条):
                //MBAP头(7) + 功能码(1) + 起始地址(2) + 数量(2)
                case ModbusFunctionCode.WriteMultipleCoils:
                case ModbusFunctionCode.WriteMultipleRegisters:
                    response = new byte[12];
                    break;
            }
            _socket.Receive(response);
            return response;
        }
        /// <summary>
        /// 验证响应数据有效性
        /// </summary>
        private void ValidateResponse( byte funcCode,byte[] response, byte[] request = null)
        {
            if (response.Length < 9)
                throw new InvalidDataException("响应长度不足");
            // 检查异常标志(功能码最高位为1)
            byte actualFunctionCode = response[7];
            if ((actualFunctionCode & 0x80) != 0)
            {
                throw new ModbusException( $"Modbus异常响应: 功能码 0x{actualFunctionCode:X2}, 异常码 0x{response[8]:X2}");
            }
            // 检查功能码是否匹配
            if (actualFunctionCode != funcCode)
            {
                throw new InvalidDataException($"功能码不匹配,期望 0x{funcCode:X2},实际 0x{actualFunctionCode:X2}");
            }
            // 验证事务ID和协议ID是否匹配
            if (request!=null)
            {
                if (response[0] != request[0] || response[1] != request[1] || response[2] != 0 || response[3] != 0)
                {
                    throw new ModbusException("事务ID或协议ID不匹配");
                }
            }
        }
    
        #endregion
    
        #region 事件触发
        /// <summary>
        /// 请求报文
        /// </summary>
        private void OnRequestMessage(ModbusMessageEvents paramater)
        {
            RequestMessage?.Invoke(this, paramater);
        }
        /// <summary>
        /// 响应报文
        /// </summary>
        private void OnResponseMessage(ModbusMessageEvents paramater)
        {
            ResponseMessage?.Invoke(this, paramater);
        }
        #endregion
    }
    public class DataModel
     {
         public ModbusFunctionCode FuncCode { get; set; } = ModbusFunctionCode.ReadHoldingRegisters;
         public ushort ReadStartAddress { get; set; } = 0;
         public ushort ReadDataLength { get; set; } = 1;
         public ushort WriteStartAddress { get; set; } = 0;
         public ushort WriteDataLength { get; set; } = 1;
     }
    

  • (三)其他

    • 报文请求响应消息,功能码类型、自定义异常。
    /// <summary>
    /// 报文事件
    /// </summary>
    public class ModbusMessageEvents
    {
        /// <summary>
        /// 操作:读|写
        /// </summary>
        public bool Operate { get; }  = false;
        /// <summary>
        /// 消息内容:报文
        /// </summary>
        public byte[] Message { get;}
        public ModbusMessageEvents(byte[] message) 
        {
            Message = message;
        }
        public ModbusMessageEvents(byte[] message, bool operate)
        {
            Message = message;
            Operate = operate;
        }
    }
    /// <summary>
    /// 功能码
    /// </summary>
    public enum ModbusFunctionCode:byte
    {
        ReadCoils = 0x01,
        ReadDiscreteInputs = 0x02,
        ReadHoldingRegisters = 0x03,
        ReadInputRegisters = 0x04,
        WriteSingleCoil = 0x05,
        WriteSingleRegister = 0x06,
        WriteMultipleCoils = 0x0F,
        WriteMultipleRegisters = 0x10
    }
    /// <summary>
    /// 自定义Modbus异常类
    /// </summary>
    public class ModbusException : Exception
    {
        public ModbusException(string message) : base(message) { }
        public ModbusException(string message, Exception inner) : base(message, inner) { }
    }
    

  • 结语

    • 至此本案例展示了如何使用Scoket实现ModbusTCP通讯功能。大概就是基于TCP通讯,将数据按照指定MBAP格式填入发送即可。最后稍微做一些数据验证处理就可以了,通过案例学习编程是ge有趣的。
    • 案例存在不少缺陷,比如没有做基本的连接检测功能,可能不知道连接是否断开,看后续完善吧。
    • 如果感兴趣的话可下载案例体验、测试程序bug,也可以自行修改封装实现更多功能…

image.png


  • 最后

    • 项目地址gitee.com/incodenotes…
    • 往期案例:关注微信公众号【编程笔记in】,案例代码地址后台点击菜单【案例
    • 如果你觉得这篇文章对你有帮助,不妨点个赞再走呗!
    • 如有其他疑问,欢迎评论区留言讨论!
    • 也可以关注微信公众号  [编程笔记in]  ,一起交流学习!