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 控件说明

控件类型 控件名称 用途
TextBox txtIP 显示 / 输入服务器 IP
TextBox txtPort 显示 / 输入 TCP 端口
ComboBox cbbPortNames 选择串口名称(如 COM3)
ComboBox cbbBaudRate 选择波特率(如 9600、115200)
ComboBox cbbDataBits 选择数据位(5-8)
ComboBox cbbStopBits 选择停止位(One、Two 等)
ComboBox cbbParity 选择校验位(None、Odd 等)
Button btnStart 启动 / 停止服务器
Button btnSave 保存配置到 App.config
TextBox txtSendByte 统计转发到串口的字节数
TextBox txtReceiveByte 统计从串口接收的字节数
Panel panelSerialPort 串口状态指示(绿 = 运行,红 = 停)
Panel panelNetwork TCP 服务器状态指示
Panel panelReceive TCP 数据接收状态闪烁
Panel panelSend 串口数据转发状态闪烁

四、常见问题与解决方案

  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,          // 从站地址:1
        0x06,          // 功能码: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. 遍历数据字节,计算CRC
    for (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 控件说明

控件类型 控件名称 用途
TextBox txtIP 输入服务器 IP 地址(如 192.168.1.100)
TextBox txtPort 输入服务器 TCP 端口(如 9999)
Button btnConnOrClose 建立 / 断开 TCP 连接
CheckBox cbRealTimeRead 勾选启用实时读取设备数据
TextBox txtReadData1 显示读取的寄存器 1 数值
TextBox txtReadData2 显示读取的寄存器 2 数值
TextBox txtSetData1 输入要写入寄存器 1 的数值
TextBox txtSetData2 输入要写入寄存器 2 的数值
Button button1 触发写入寄存器 1
Button button2 触发写入寄存器 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 表示错误),提示具体错误

相关推荐
苦逼大学生被编程薄纱2 小时前
C++ 容器学习系列|vector 核心知识全解析,铺垫下一期模拟实现
开发语言·c++·学习
ajassi20002 小时前
开源 C# 快速开发(四)自定义控件--波形图
开发语言·开源·c#
DKPT2 小时前
JVM堆大小如何设置?
java·开发语言·jvm·笔记·学习
追烽少年x2 小时前
C# MVVM模式和Qt中MVC模式的比较
c#
江公望2 小时前
Qt容器QList、QLinkedList、QVector特性浅谈
开发语言·qt
€8113 小时前
Java入门级教程21——Java 缓存技术、RMI远程方法调用、多线程分割大文件
java·开发语言·java缓存代理模式的实现·java rmi远程方法调用·多线程分割大文件
路弥行至3 小时前
C语言入门教程 | 第四讲:深入理解数制与码制,掌握基本数据类型的奥秘
服务器·c语言·开发语言·经验分享·笔记·其他·入门教程
青柠编程3 小时前
基于Spring Boot与SSM的中药实验管理系统架构设计
java·开发语言·数据库
远远远远子3 小时前
C++ --1 perparation
开发语言·c++