当前位置: 首页 > news >正文

C# TCP - 串口转发 - 实践

C# TCP - 串口转发 - 实践

C# TCP - 串口转发服务器代码笔记

一、项目概述

该项目是一个基于 C# WinForms 的TCP 服务器与串口通信结合的转发系统,核心功能是实现 “TCP 客户端 ↔ 服务器 ↔ 串口设备” 之间的双向数据转发,适用于需要通过网络远程控制串口设备或读取串口设备数据的场景(如工业控制、物联网设备监控等)。

二、核心功能模块

1. 配置管理模块

1.1 功能说明
  • 自动加载并显示本机 IPv4 地址、串口列表及通信参数(波特率、数据位等)

  • 支持从配置文件(App.config)读取历史配置,实现 “配置持久化”

  • 提供配置保存功能,修改后需重启服务器生效

1.2 关键代码解析
// 1. 绑定串口参数(波特率、数据位等)
private void BindData()
{   // 加载本机IPv4地址   string hostName = Dns.GetHostName();   IPAddress[] addresses = Dns.GetHostAddresses(hostName);   foreach (IPAddress address in addresses)   {       if (address.AddressFamily == AddressFamily.InterNetwork) // 筛选IPv4           IP = address.ToString();   }
​   // 从配置文件读取历史配置(优先级:配置文件 > 自动获取)   txtIP.Text = IP != ConfigurationManager.AppSettings["IP"] ? IP : ConfigurationManager.AppSettings["IP"];   txtPort.Text = ConfigurationManager.AppSettings["Port"];   cbbPortNames.Text = ConfigurationManager.AppSettings["PortName"];   // ... 其他参数(波特率、数据位等)同理
}
​
// 2. 保存配置到App.config
private void btnSave_Click(object sender, EventArgs e)
{   // 输入校验(非空判断)   if (string.IsNullOrWhiteSpace(txtIP.Text))   {       MessageBox.Show("服务器IP不能为空!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Error);       return;   }
​   // 写入配置文件   Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);   config.AppSettings.Settings["IP"].Value = txtIP.Text.Trim();   config.AppSettings.Settings["Port"].Value = txtPort.Text.Trim();   // ... 其他参数同理   config.Save(); // 保存配置   MessageBox.Show("保存配置成功!\n请重新启动服务器!", "提示");
}
1.3 注意事项
  • 配置文件需提前在项目中创建,需包含IPPortPortName等关键字段

  • 保存配置后需重启服务器,配置才会生效

2. 服务器启停模块

2.1 功能说明
  • 启动:同时初始化 TCP 服务器和串口,禁用配置修改控件,更新状态指示(绿色 = 运行)

  • 停止:关闭 TCP 服务器和串口,启用配置修改控件,更新状态指示(红色 = 停止)

2.2 关键代码解析
// 1. 启动服务器(TCP+串口)
private void StartServer()
{   // 初始化串口   serialPort1.PortName = cbbPortNames.Text;   serialPort1.BaudRate = int.Parse(cbbBaudRate.Text);   serialPort1.DataBits = int.Parse(cbbDataBits.Text);   serialPort1.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cbbStopBits.Text); // 枚举转换   serialPort1.Parity = (Parity)Enum.Parse(typeof(Parity), cbbParity.Text);   serialPort1.Open(); // 打开串口
​   // 初始化TCP服务器   server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);   int port = int.Parse(ConfigurationManager.AppSettings["Port"]);   IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, port); // 监听所有网卡的指定端口   server.Bind(ipEndPoint); // 绑定IP和端口   server.Listen(100); // 最大监听队列100
​   // 更新UI状态   btnStart.Text = "停止";   txtIP.Enabled = false; // 禁用配置修改   panelSerialPort.BackColor = Color.Lime; // 串口运行(绿色)   panelNetwork.BackColor = Color.Lime; // 网络运行(绿色)
}
​
// 2. 停止服务器
private void StopServer()
{   server.Close(); // 关闭TCP服务器   serialPort1.Close(); // 关闭串口
​   // 恢复UI状态   btnStart.Text = "启动";   txtIP.Enabled = true; // 启用配置修改   panelSerialPort.BackColor = Color.Red; // 串口停止(红色)   panelNetwork.BackColor = Color.Red; // 网络停止(红色)
}
​
// 3. 启停触发按钮
private void btnStart_Click(object sender, EventArgs e)
{   try   {       if (!serialPort1.IsOpen) // 未启动 → 启动       {           StartServer();             AcceptData(); // 启动数据接收任务       }       else // 已启动 → 停止       {           CacelAcceptData(); // 取消数据接收任务           StopServer();              }   }   catch (Exception ex)   {       MessageBox.Show(ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);   }
}
2.3 注意事项
  • 启动前需确保串口未被其他程序占用,TCP 端口未被占用

  • 枚举转换(StopBits/Parity)需保证下拉框值与枚举名一致(如 “One” 对应StopBits.One

3. 数据转发模块

3.1 核心流程
  1. TCP 客户端 → 服务器 → 串口设备:服务器接收 TCP 客户端数据,通过串口转发给设备

  2. 串口设备 → 服务器 → TCP 客户端:服务器接收串口设备应答数据,广播转发给所有 TCP 客户端

3.2 关键代码解析
3.2.1 接收 TCP 客户端数据并转发到串口
private void AcceptData()
{   cts1 = new CancellationTokenSource(); // 任务取消令牌(用于停止时中断任务)   Task.Run(() => // 异步任务(避免阻塞UI)   {       while (!cts1.IsCancellationRequested)       {           // 1. 接收TCP客户端连接           Socket client = server.Accept(); // 阻塞等待新连接           string clientKey = client.RemoteEndPoint.ToString(); // 客户端标识(IP:端口)                      // 去重:移除已存在的相同客户端           if (clients.ContainsKey(clientKey))               clients.Remove(clientKey);           clients.Add(clientKey, client); // 加入客户端字典
​           // 2. 接收该客户端的持续数据           cts2 = new CancellationTokenSource();           Task.Run(() =>           {               while (!cts2.IsCancellationRequested)               {                   byte[] buffer = new byte[client.Available]; // 根据可用数据长度创建缓冲区                   int len = client.Receive(buffer); // 接收客户端数据
​                   if (len > 0)                   {                       // 转发到串口                       serialPort1.Write(buffer, 0, len);
​                       // 异步更新UI(数据统计、状态闪烁)                       Invoke(new Action(async () =>                       {                           txtSendByte.Text = (int.Parse(txtSendByte.Text) + len).ToString(); // 发送字节数统计                           panelReceive.BackColor = Color.Lime; // 接收状态闪烁(绿色)                           await Task.Delay(70); // 闪烁时长                           panelReceive.BackColor = Color.Gray;                       }));                   }               }           }, cts2.Token);       }   }, cts1.Token);
}
3.2.2 接收串口数据并广播到所有 TCP 客户端
// 串口数据接收事件(设备应答数据)
private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{   byte[] buffer = new byte[serialPort1.BytesToRead]; // 缓冲区长度=可用数据长度   int len = serialPort1.Read(buffer, 0, buffer.Length); // 读取串口数据
​   if (len > 0)   {       // 广播到所有TCP客户端       foreach (var dict in clients)       {           Socket client = dict.Value;           if (client != null && client.Connected) // 确保客户端连接正常           {               client.Send(buffer); // 转发数据
​               // 异步更新UI(接收字节数统计、状态闪烁)               Invoke(new Action(async () =>               {                   txtReceiveByte.Text = (int.Parse(txtReceiveByte.Text) + len).ToString(); // 接收字节数统计                   panelSend.BackColor = Color.Lime; // 发送状态闪烁(绿色)                   await Task.Delay(70);                   panelSend.BackColor = Color.Gray;               }));           }       }   }
}
3.3 注意事项
  • 数据缓冲区长度使用client.Available/serialPort1.BytesToRead,避免内存浪费

  • 跨线程更新 UI 需使用Invoke(WinForms 控件线程安全限制)

  • 客户端管理使用Dictionary<string, Socket>,键为客户端IP:端口,便于去重和广播

4. 安全与异常处理模块

4.1 功能说明
  • 任务取消:通过CancellationTokenSource安全中断异步数据接收任务

  • 窗体关闭保护:服务器运行时禁止关闭窗体,避免资源泄漏

  • 输入校验:保存配置前检查关键参数非空

4.2 关键代码解析
// 1. 取消数据接收任务
private void CacelAcceptData()
{   cts2?.Cancel(); // 取消单个客户端数据接收任务   cts1?.Cancel(); // 取消客户端连接监听任务
}
​
// 2. 窗体关闭保护
private void Server_FormClosing(object sender, FormClosingEventArgs e)
{   if (serialPort1.IsOpen) // 服务器运行中 → 禁止关闭   {       e.Cancel = true; // 取消关闭操作       MessageBox.Show("服务器正在运行中,请停止服务器后,再关闭!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);   }
}

三、UI 控件说明

控件类型控件名称用途
TextBoxtxtIP显示 / 输入服务器 IP
TextBoxtxtPort显示 / 输入 TCP 端口
ComboBoxcbbPortNames选择串口名称(如 COM3)
ComboBoxcbbBaudRate选择波特率(如 9600、115200)
ComboBoxcbbDataBits选择数据位(5-8)
ComboBoxcbbStopBits选择停止位(One、Two 等)
ComboBoxcbbParity选择校验位(None、Odd 等)
ButtonbtnStart启动 / 停止服务器
ButtonbtnSave保存配置到 App.config
TextBoxtxtSendByte统计转发到串口的字节数
TextBoxtxtReceiveByte统计从串口接收的字节数
PanelpanelSerialPort串口状态指示(绿 = 运行,红 = 停)
PanelpanelNetworkTCP 服务器状态指示
PanelpanelReceiveTCP 数据接收状态闪烁
PanelpanelSend串口数据转发状态闪烁

四、常见问题与解决方案

  1. 串口打开失败

    • 原因:串口被其他程序占用、串口名称选择错误

    • 解决方案:关闭占用串口的程序,重新选择正确的串口

  2. TCP 端口绑定失败

    • 原因:端口被其他程序占用、端口号超出范围(0-65535)

    • 解决方案:更换未占用的端口,确保端口号合法

  3. 跨线程更新 UI 报错

    • 原因:WinForms 控件不允许非 UI 线程直接修改

    • 解决方案:使用Invoke(new Action(() => { ... }))包裹 UI 更新代码

  4. 配置保存后不生效

    • 原因:配置需重启服务器加载

    • 解决方案:保存后关闭服务器,重新启动

  5. 客户端连接后无法接收数据

    • 原因:客户端未正确连接、数据缓冲区长度不足

    • 解决方案:检查客户端 IP 和端口是否正确,确保缓冲区长度足够(建议使用固定长度缓冲区如 1024,避免client.Available=0时缓冲区为空)

五、扩展建议

  1. 增加日志功能:记录客户端连接 / 断开、数据转发详情,便于问题排查

  2. 客户端心跳检测:定期检测客户端连接状态,移除断开的客户端

  3. 数据格式解析:支持自定义协议(如帧头 + 数据 + 校验位),过滤无效数据

  4. 多串口支持:扩展为多串口转发,适配多个设备

  5. UI 优化:增加客户端列表显示(当前连接的客户端 IP: 端口),支持手动断开指定客户端

C# TCP 客户端(Modbus 协议)代码笔记

一、项目概述

该客户端是基于 C# WinForms + TCP 协议 + Modbus-RTU 协议 的设备通信工具,核心功能是与前文的 “TCP - 串口转发服务器” 交互,实现对串口设备的 数据读取(Modbus 功能码 03)数据写入(Modbus 功能码 06),适用于工业设备(如传感器、控制器)的远程监控与控制场景。

二、核心技术栈

  1. 网络通信Socket 类实现 TCP 客户端,支持异步连接、发送、接收

  2. 协议处理:Modbus-RTU 协议(功能码 03 读保持寄存器、功能码 06 写单个寄存器)

  3. 数据校验:CRC16 循环冗余校验(确保 Modbus 报文完整性)

  4. 异步编程Task + CancellationTokenSource 实现非阻塞通信,避免 UI 卡顿

三、核心功能模块

1. TCP 连接管理模块

1.1 功能说明
  • 建立连接:根据输入的服务器 IP 和端口,异步创建 TCP 连接

  • 断开连接:关闭 Socket,释放资源,恢复 UI 可编辑状态

  • 状态控制:连接成功后禁用 IP / 端口输入,切换按钮文本为 “断开”

1.2 关键代码解析

csharp

private async void btnConnOrClose_Click(object sender, EventArgs e)
{   try   {       if (btnConnOrClose.Text == "连接")       {           // 1. 初始化TCP Socket(IPv4、流式传输、TCP协议)           client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);                      // 2. 异步连接服务器(避免阻塞UI)           // 解析IP和端口:IPAddress.Parse(txtIP.Text) → 转换为IP地址对象;int.Parse(txtPort.Text) → 转换为端口号           await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text)));                      // 3. 更新UI状态(连接成功)           btnConnOrClose.Text = "断开"; // 按钮文本切换为“断开”           txtIP.Enabled = false; // 禁用IP输入           txtPort.Enabled = false; // 禁用端口输入       }       else       {           // 1. 断开连接:先关闭连接,再释放Socket           client.Disconnect(false); // false = 不允许后续重用Socket           client.Close(); // 关闭Socket,释放资源           client = null; // 置空,避免空引用                      // 2. 恢复UI状态(断开成功)           btnConnOrClose.Text = "连接"; // 按钮文本切换为“连接”           txtIP.Enabled = true; // 启用IP输入           txtPort.Enabled = true; // 启用端口输入       }   }   catch (Exception ex)   {       // 异常处理(如IP格式错误、端口占用、服务器未启动等)       MessageBox.Show(ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);       return;   }
​   // 连接成功后,启动数据接收任务(持续接收服务器转发的设备应答)   await ReceiveMessage();
}
1.3 注意事项
  • 异步连接:使用 ConnectAsync 而非同步 Connect,避免 UI 卡死

  • 异常捕获:需处理 FormatException(IP / 端口格式错误)、SocketException(连接失败)等

  • 资源释放:断开时必须调用 Close(),否则会导致 Socket 资源泄漏

2. Modbus 数据读取模块(功能码 03)

2.1 功能说明
  • 实时读取:勾选 “实时读取” 后,每 3 秒自动发送 Modbus 读指令(功能码 03)

  • 报文构造:生成包含 “从站地址、功能码、寄存器地址、寄存器数量、CRC 校验” 的完整 Modbus 报文

  • 数据解析:接收设备应答报文后,解析寄存器值并显示到 UI

2.2 关键代码解析
2.2.1 实时读取触发(复选框事件)

csharp

private void cbRealTimeRead_CheckedChanged(object sender, EventArgs e)
{   if (cbRealTimeRead.Checked)   {       // 校验连接状态:未连接则提示       if (client == null || !client.Connected)       {           MessageBox.Show("先建立连接,再读取!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);           cbRealTimeRead.Checked = false; // 取消勾选           return;       }
​       // 启动数据发送任务(每3秒发一次读指令)       _ = SendMessage(); // 用_忽略Task返回值,避免编译器警告   }   else   {       // 取消实时读取:终止发送任务       cts1?.Cancel(); // cts1是发送任务的取消令牌   }
}
2.2.2 构造 Modbus 读报文并发送

csharp

private Task SendMessage()
{   // 初始化取消令牌(用于终止实时发送任务)   cts1 = new CancellationTokenSource();      return Task.Run(async () =>   {       while (!cts1.IsCancellationRequested) // 任务未取消则循环       {           // 1. 构造Modbus读指令核心报文(功能码03)           // 格式:[从站地址(1字节)][功能码(1字节)][起始寄存器地址高8位(1字节)][起始寄存器地址低8位(1字节)][读取寄存器数量高8位(1字节)][读取寄存器数量低8位(1字节)]           byte[] modbusCore = new byte[6] {                0x01,          // 从站地址:1(默认设备地址)               0x03,          // 功能码:03(读保持寄存器)               0x00, 0x00,    // 起始寄存器地址:0x0000(从第0个寄存器开始读)               0x00, 0x02     // 读取寄存器数量:0x0002(读2个寄存器)           };
​           // 2. 计算CRC16校验(Modbus-RTU必须加CRC,确保报文无差错)           byte[] crc = CRC16(modbusCore);
​           // 3. 组装完整报文(核心报文 + CRC校验)           byte[] fullPacket = new byte[8]; // 6字节核心 + 2字节CRC = 8字节           Array.Copy(modbusCore, 0, fullPacket, 0, modbusCore.Length); // 复制核心报文           Array.Copy(crc, 0, fullPacket, modbusCore.Length, crc.Length); // 复制CRC
​           // 4. 发送报文到服务器(由服务器转发给串口设备)           client.Send(fullPacket);
​           // 5. 延迟3秒(避免频繁发送,减轻设备压力)           await Task.Delay(3000, cts1.Token); // 传入取消令牌,支持延迟中取消       }   }, cts1.Token);
}
2.2.3 接收并解析设备应答报文

csharp

private Task ReceiveMessage()
{// 初始化取消令牌(用于终止接收任务)cts2 = new CancellationTokenSource();return Task.Run(async () =>{while (!cts2.IsCancellationRequested) // 任务未取消则循环{// 1. 创建缓冲区(长度=当前可用数据长度,避免内存浪费)byte[] buffer = new byte[client.Available];// 2. 接收服务器转发的设备应答数据int receiveLen = client.Receive(buffer); // 返回实际接收的字节数// 3. 校验应答报文长度(Modbus读2个寄存器的应答应为9字节:1+1+1+2*2+2)// 格式:[从站地址][功能码][字节数][寄存器1高8位][寄存器1低8位][寄存器2高8位][寄存器2低8位][CRC高8位][CRC低8位]if (receiveLen == 9){// 跨线程更新UI(WinForms控件不允许非UI线程直接修改)Invoke(new Action(() =>{// 解析寄存器1值:高8位*256 + 低8位(16位无符号整数)int reg1Value = buffer[3] * 256 + buffer[4];txtReadData1.Text = reg1Value.ToString(); // 显示到UI// 解析寄存器2值:同理int reg2Value = buffer[5] * 256 + buffer[6];txtReadData2.Text = reg2Value.ToString(); // 显示到UI}));}// 4. 延迟1秒(降低CPU占用)await Task.Delay(1000, cts2.Token);}}, cts2.Token);
}

3. Modbus 数据写入模块(功能码 06)

3.1 功能说明
  • 单寄存器写入:根据输入的数值,构造 Modbus 写指令(功能码 06),写入指定寄存器

  • 输入校验:确保输入为合法整数,避免无效指令发送

  • 报文构造:包含 “从站地址、功能码、目标寄存器地址、写入值、CRC 校验”

3.2 关键代码解析(以写入寄存器 1 为例)

csharp

private void button1_Click(object sender, EventArgs e)
{// 1. 输入校验:确保输入是合法整数bool isInt = int.TryParse(txtSetData1.Text, out int writeValue);if (!isInt){MessageBox.Show("输入正确格式的数据!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);return;}// 2. 拆分写入值为高低8位(Modbus寄存器是16位,需分高低字节传输)byte highByte = (byte)(writeValue / 256); // 高8位:数值除以256取整byte lowByte = (byte)(writeValue % 256);  // 低8位:数值除以256取余// 3. 构造Modbus写指令核心报文(功能码06)// 格式:[从站地址(1字节)][功能码(1字节)][目标寄存器地址高8位(1字节)][目标寄存器地址低8位(1字节)][写入值高8位(1字节)][写入值低8位(1字节)]byte[] modbusCore = new byte[6] { 0x01,          // 从站地址:10x06,          // 功能码:06(写单个保持寄存器)0x00, 0x00,    // 目标寄存器地址:0x0000(写入第0个寄存器)highByte, lowByte // 写入值的高低8位};// 4. 计算CRC16校验byte[] crc = CRC16(modbusCore);// 5. 组装完整报文(核心报文 + CRC)byte[] fullPacket = new byte[8];Array.Copy(modbusCore, 0, fullPacket, 0, modbusCore.Length);Array.Copy(crc, 0, fullPacket, modbusCore.Length, crc.Length);// 6. 发送报文(前提:已建立连接)if (client != null && client.Connected)client.Send(fullPacket);
}
3.3 写入寄存器 2 的差异

目标寄存器地址 不同:将 0x00, 0x00 改为 0x00, 0x01,对应写入第 1 个寄存器,其余逻辑完全一致(代码见 button2_Click 方法)。

4. CRC16 校验模块

4.1 功能说明

Modbus-RTU 协议要求所有报文末尾必须添加 2 字节 CRC16 校验,用于检测报文在传输过程中是否出现差错(如丢包、错码)。该模块实现标准的 CRC16 算法(多项式 0xA001,初始值 0xFFFF)。

4.2 关键代码解析

csharp

private static byte[] CRC16(byte[] data)
{int dataLen = data.Length;if (dataLen == 0) // 空数据返回空校验return new byte[] { 0, 0 };ushort crc = 0xFFFF; // 初始值:0xFFFF// 1. 遍历数据字节,计算CRCfor (int i = 0; i < dataLen; i++){crc = (ushort)(crc ^ data[i]); // 当前CRC与数据字节异或// 2. 每字节循环8位(处理每一位)for (int j = 0; j < 8; j++){// 若最低位为1:右移1位后与多项式0xA001异或;否则仅右移1位crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);}}// 3. CRC结果高低位交换(Modbus-RTU要求小端序:低字节在前,高字节在后)byte crcLow = (byte)(crc & 0x00FF);  // 低8位byte crcHigh = (byte)((crc & 0xFF00) >> 8); // 高8位return new byte[] { crcLow, crcHigh }; // 返回小端序CRC
}

四、UI 控件说明

控件类型控件名称用途
TextBoxtxtIP输入服务器 IP 地址(如 192.168.1.100)
TextBoxtxtPort输入服务器 TCP 端口(如 9999)
ButtonbtnConnOrClose建立 / 断开 TCP 连接
CheckBoxcbRealTimeRead勾选启用实时读取设备数据
TextBoxtxtReadData1显示读取的寄存器 1 数值
TextBoxtxtReadData2显示读取的寄存器 2 数值
TextBoxtxtSetData1输入要写入寄存器 1 的数值
TextBoxtxtSetData2输入要写入寄存器 2 的数值
Buttonbutton1触发写入寄存器 1
Buttonbutton2触发写入寄存器 2

五、常见问题与解决方案

  1. 连接失败,提示 “无法连接到远程服务器”

    • 原因:服务器未启动、IP / 端口输入错误、网络不通(防火墙拦截)

    • 解决方案:确认服务器已启动,检查 IP 和端口是否与服务器一致,关闭防火墙或开放对应端口

  2. 实时读取无数据,UI 无显示

    • 原因:Modbus 报文格式错误(从站地址、寄存器地址错误)、CRC 校验错误、服务器未转发数据

    • 解决方案:

      • 检查从站地址是否与设备匹配(默认 0x01,若设备地址不同需修改)

      • 用串口工具(如 SSCOM)抓取报文,验证 CRC 是否正确

      • 确认服务器已正常连接串口设备

  3. 写入数据后,设备无响应

    • 原因:写入值超出寄存器范围(如 16 位寄存器最大 65535)、目标寄存器地址错误

    • 解决方案:确认写入值在设备寄存器允许范围内,检查目标寄存器地址是否与设备手册一致

  4. UI 卡顿

    • 原因:未使用异步编程,同步发送 / 接收阻塞 UI 线程

    • 解决方案:确保所有网络操作(ConnectSendReceive)使用异步方法(ConnectAsyncSendAsync),并用Task.Run将循环逻辑放入后台线程

  5. 取消实时读取后,任务仍在运行

    • 原因:未正确调用CancellationTokenSource.Cancel(),或取消后未释放令牌

    • 解决方案:确保cbRealTimeRead取消勾选时,调用cts1?.Cancel(),且任务循环中检查cts1.IsCancellationRequested

六、扩展建议

  1. 增加报文日志:记录发送 / 接收的原始字节(如01 03 00 00 00 02 D4 0B),便于调试协议问题

  2. 支持多从站:增加从站地址输入框,支持同时与多个地址的设备通信

  3. 批量读写:扩展 Modbus 功能码(如功能码 16 批量写寄存器),支持一次写入多个寄存器

  4. 数据格式转换:支持十进制 / 十六进制显示切换,适配不同设备的数据格式

  5. 断线重连:增加自动重连机制,服务器断开后无需手动重新连接

  6. 错误处理增强:解析 Modbus 异常响应(如功能码 + 0x80 表示错误),提示具体错误

http://www.hskmm.com/?act=detail&tid=21349

相关文章:

  • 【研发规范】Git 提交(commit)、CodeReview规范
  • PCIE 各个管脚的作用是什么?
  • Windows 11 局域网打印机共享设置
  • DailyPaper-2025-9-29
  • gpd winmax2 fedora42 睡眠秒唤醒问题
  • 国企人力资源管理系统怎么选?内行人推荐这8款,功能、服务双保障
  • spring service注入命名规则
  • 完整教程:基于岗课赛证的中职物联网专业“综合布线课程”教学解决方案
  • tensorflow加载和预处理信息
  • linux查询磁盘空间,查询指定目录的空间 df命令
  • 轻松规划房贷:用好公积金贷款,让梦想之家触手可及
  • milvus使用的etcd空间整理
  • 本土化战略赋能:Gitee如何领跑中国DevOps黄金赛道
  • 打印机错误0x0000709,问题排查和修复指南
  • k8s使用的etcd空间清理
  • MyBatis 与 JPA 的核心对比
  • 2025.9.29 测试
  • 深度学习(CVAE)
  • c# aot orm 框架测试 mysql
  • 洛谷题单指南-进阶数论-P2303 [SDOI2012] Longge 的问题
  • PK-2877电流互感器在高频脉冲电源模块测试中的应用方案
  • VC++ 使用OpenSSL创建RSA密钥PEM档案
  • CF1699D Almost Triple Deletions
  • QMT回测模式为什么要在副图进行
  • DSA:DeepSeek Sparse Attention
  • 荒野猎手出击!启明智显ZX7981PO:专治各种恶劣环境的5G插卡路由器
  • AWS CDK重构功能发布:安全重构基础设施即代码
  • 开发即时通社交软件APP首选系统,可定制开发,可提供源码
  • 死锁的处理策略-死锁的检测和解除
  • springboot3 mybatis 数据库操控入门与实战