- 摘要: 本文描述了如何使用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; } - 1、读取数据|写单个数据写
-
-
运行环境
- 操作系统: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,也可以自行修改封装实现更多功能…
-
最后
- 项目地址:gitee.com/incodenotes…
- 往期案例:关注微信公众号【编程笔记in】,案例代码地址后台点击菜单【案例】
- 如果你觉得这篇文章对你有帮助,不妨点个赞再走呗!
- 如有其他疑问,欢迎评论区留言讨论!
- 也可以关注微信公众号 [编程笔记in] ,一起交流学习!