ModbusTcp通信C#WPF开发测试(基于Nmodbus4库应用)

Easy系列PLC MODBUSTCP通信交换数据

Easy系列PLC MODBUSTCP通信交换数据-CSDN博客文章浏览阅读1k次,点赞18次,收藏15次。CODESYS MODBUS TCP通信请参考下面相关文章链接:CODESYS MODBUS TCP通信(禾川Q1 PLC作为MODBUS TCP从站)_codesys跟禾川通讯-CSDN博客禾川Q1 PLC MODBUS TCP 通信(PLC作为MODBUS TCP通信主站)禾川Q1 PLC MODBUS TCP通信(CODESYS平台完整配置+代码)-CSDN博客文章浏览阅读28次。_plc modbustcphttps://rxxw-control.blog.csdn.net/article/details/148965984?spm=1011.2415.3001.5331

更多工业通信相关内容,大家也可以查看BLIBLI上博主 "RXXW_Dor" 推出的工业通信视频课程。

1、网络拓扑

2、NModbus4包安装

最新的是NModbus包,函数和使用会有所不同,需要注意。这篇文章都是基于NModbus4库的使用和测试。

3、写线圈测试

本地回环测试IP地址固定为127.0.0.1,和第三方设备通信需要准确填写设备IP地址端口号已经MODBUS站号。

4、读线圈测试

5、MainWindow.xaml(界面代码)

XML 复制代码
<Window x:Class="ModbusNModbusDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ModbusTcp线圈读写(NModbus4)" Height="380" Width="520">
    <Grid Margin="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="85"/>
            <ColumnDefinition Width="120"/>
            <ColumnDefinition Width="95"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <!--IP、端口、从站ID-->
        <TextBlock Grid.Row="0" Grid.Column="0" Text="IP地址:" VerticalAlignment="Center"/>
        <TextBox x:Name="txtIp" Grid.Row="0" Grid.Column="1" Text="127.0.0.1" Margin="0,4"/>

        <TextBlock Grid.Row="0" Grid.Column="2" Text="端口:" VerticalAlignment="Center"/>
        <TextBox x:Name="txtPort" Grid.Row="0" Grid.Column="3" Text="502" Width="70" Margin="0,4"/>

        <TextBlock Grid.Row="1" Grid.Column="0" Text="从站ID:" VerticalAlignment="Center"/>
        <TextBox x:Name="txtSlaveId" Grid.Row="1" Grid.Column="1" Text="1" Margin="0,4"/>

        <!--线圈地址-->
        <TextBlock Grid.Row="2" Grid.Column="0" Text="线圈地址:" VerticalAlignment="Center"/>
        <TextBox x:Name="txtCoilAddr" Grid.Row="2" Grid.Column="1" Text="0" Margin="0,4"/>

        <!--批量数量-->
        <TextBlock Grid.Row="2" Grid.Column="2" Text="批量点数:" VerticalAlignment="Center"/>
        <TextBox x:Name="txtBatchCount" Grid.Row="2" Grid.Column="3" Text="8" Width="70" Margin="0,4"/>

        <!--连接/断开+状态-->
        <StackPanel Grid.Row="3" Grid.ColumnSpan="4" Orientation="Horizontal" Margin="0,8,0,8">
            <Button x:Name="BtnConnect" Content="连接PLC" Width="90" Click="BtnConnect_Click"/>
            <Button x:Name="BtnDisConnect" Content="断开连接" Width="90" Margin="8,0,0,0" Click="BtnDisConnect_Click"/>
            <TextBlock x:Name="tbConnState" Text="未连接" Foreground="Red" Margin="15,0,0,0" VerticalAlignment="Center"/>
        </StackPanel>

        <!--单线圈操作按钮-->
        <StackPanel Grid.Row="4" Grid.ColumnSpan="4" Orientation="Horizontal">
            <Button x:Name="BtnSetSingle" Content="线圈置ON" Width="95" Click="BtnSetSingle_Click"/>
            <Button x:Name="BtnResetSingle" Content="线圈置OFF" Width="95" Margin="8,0,0,0" Click="BtnResetSingle_Click"/>
            <Button x:Name="BtnReadSingle" Content="读取单线圈" Width="95" Margin="8,0,0,0" Click="BtnReadSingle_Click"/>
            <Button x:Name="BtnBatchWrite" Content="批量写线圈" Width="95" Margin="8,0,0,0" Click="BtnBatchWrite_Click"/>
        </StackPanel>

        <!--结果显示-->
        <TextBlock Grid.Row="5" Grid.ColumnSpan="4" x:Name="tbResult" Margin="0,12,0,0" FontSize="13"/>
    </Grid>
</Window>

6、MainWindow.xaml.cs(逻辑代码)

cs 复制代码
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Windows;
using Modbus.Device;

namespace ModbusNModbusDemo
{
    public partial class MainWindow : Window
    {
        private TcpClient _modbusTcpClient;
        private IModbusMaster _modbusMaster;
        public bool IsConnected => _modbusTcpClient != null && _modbusTcpClient.Connected;

        public MainWindow()
        {
            InitializeComponent();
            tbConnState.Text = "未连接";
            tbConnState.Foreground = System.Windows.Media.Brushes.Red;
        }

        #region 连接断开【异步】
        private async void BtnConnect_Click(object sender, RoutedEventArgs e)
        {
            if (IsConnected)
            {
                MessageBox.Show("已经处于连接状态!");
                return;
            }
            if (!CheckParams(out string ip, out int port, out byte slaveId, out _)) return;
            try
            {
                // 异步连接不卡UI
                _modbusTcpClient = new TcpClient();
                await _modbusTcpClient.ConnectAsync(ip, port);
                _modbusMaster = ModbusIpMaster.CreateIp(_modbusTcpClient);

                tbConnState.Text = "连接成功";
                tbConnState.Foreground = System.Windows.Media.Brushes.Green;
                MessageBox.Show("ModbusTCP连接建立成功");
            }
            catch (Exception ex)
            {
                CloseConnect();
                MessageBox.Show("连接失败:" + ex.Message);
            }
        }

        private void BtnDisConnect_Click(object sender, RoutedEventArgs e)
        {
            CloseConnect();
            MessageBox.Show("已断开通信连接");
        }

        void CloseConnect()
        {
            if (_modbusTcpClient != null)
            {
                _modbusTcpClient.Close();
                _modbusTcpClient.Dispose();
                _modbusTcpClient = null;
            }
            _modbusMaster = null;
            tbConnState.Text = "未连接";
            tbConnState.Foreground = System.Windows.Media.Brushes.Red;
        }
        #endregion

        #region 单线圈操作 异步
        private async void BtnSetSingle_Click(object sender, RoutedEventArgs e)
        {
            if (!CheckConnectState()) return;
            await WriteSingleCoil(true);
        }
        private async void BtnResetSingle_Click(object sender, RoutedEventArgs e)
        {
            if (!CheckConnectState()) return;
            await WriteSingleCoil(false);
        }

        async Task WriteSingleCoil(bool state)
        {
            if (!CheckParams(out _, out _, out byte slaveId, out ushort addr)) return;
            try
            {
                // 同步Modbus方法套Task.Run脱离UI线程
                await Task.Run(() =>
                {
                    _modbusMaster.WriteSingleCoil(slaveId, addr, state);
                });
                MessageBox.Show($"线圈{addr} {(state ? "置位成功ON" : "复位成功OFF")}");
            }
            catch (Exception ex)
            {
                MessageBox.Show("写入异常:" + ex.Message);
                CloseConnect();
            }
        }

        private async void BtnReadSingle_Click(object sender, RoutedEventArgs e)
        {
            if (!CheckConnectState()) return;
            if (!CheckParams(out _, out _, out byte slaveId, out ushort addr)) return;
            try
            {
                bool[] resArr = await Task.Run(() =>
                {
                    return _modbusMaster.ReadCoils(slaveId, addr, 1);
                });
                tbResult.Text = $"线圈{addr} 状态:{(resArr[0] ? "ON" : "OFF")}";
            }
            catch (Exception ex)
            {
                tbResult.Text = "读取失败:" + ex.Message;
                CloseConnect();
            }
        }
        #endregion

        #region 批量写线圈
        private async void BtnBatchWrite_Click(object sender, RoutedEventArgs e)
        {
            if (!CheckConnectState()) return;
            if (!CheckParams(out _, out _, out byte slaveId, out ushort startAddr)) return;
            if (!ushort.TryParse(txtBatchCount.Text, out ushort count) || count < 1)
            {
                MessageBox.Show("批量点数填写错误");
                return;
            }
            bool[] batchData = new bool[count];
            for (int i = 0; i < count; i++)
                batchData[i] = i % 2 == 0;

            try
            {
                await Task.Run(() =>
                {
                    _modbusMaster.WriteMultipleCoils(slaveId, startAddr, batchData);
                });
                MessageBox.Show($"从地址{startAddr}开始,共{count}个线圈批量写入完成!");
            }
            catch (Exception ex)
            {
                MessageBox.Show("批量写入异常:" + ex.Message);
                CloseConnect();
            }
        }
        #endregion

        #region 辅助方法
        bool CheckConnectState()
        {
            if (!IsConnected)
            {
                MessageBox.Show("请先建立ModbusTCP连接!");
                return false;
            }
            return true;
        }

        bool CheckParams(out string ip, out int port, out byte slaveId, out ushort coilAddr)
        {
            ip = txtIp.Text.Trim();
            port = 0; slaveId = 0; coilAddr = 0;
            if (!int.TryParse(txtPort.Text, out port) || port < 1 || port > 65535)
            {
                MessageBox.Show("端口数字错误");
                return false;
            }
            if (!byte.TryParse(txtSlaveId.Text, out slaveId) || slaveId < 1)
            {
                MessageBox.Show("从站ID范围1~247");
                return false;
            }
            if (!ushort.TryParse(txtCoilAddr.Text, out coilAddr))
            {
                MessageBox.Show("线圈地址非负整数");
                return false;
            }
            return true;
        }
        #endregion
    }
}

7、启用轮询

我们更换了一个界面,界面里总共2个设备。在同一个端口下RS485总线挂接了2个从站设备。

8、监控的RS485总线数据帧

9、对应界面代码(MainWindow.xaml)

XML 复制代码
<Window x:Class="ModbusNModbusDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="卷绕机数据监控" Height="720" Width="980">
    <Grid Margin="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--顶部:公用IP+连接断开日志按钮-->
        <StackPanel Grid.Row="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,12">
            <TextBlock Text="IP地址:" FontSize="13" VerticalAlignment="Center"/>
            <TextBox x:Name="txtGlobalIp" Width="160" Text="192.168.1.125" Margin="6,2"/>
            <Button x:Name="BtnConnect" Content="通信连接" Width="110" Click="BtnConnect_Click" Margin="15,0,8,0"/>
            <Button x:Name="BtnDisConnect" Content="通信断开" Width="110" Click="BtnDisConnect_Click" Margin="0,0,8,0"/>
            <Button x:Name="BtnLogOpen" Content="日志查看" Width="110" Click="BtnOpenLogFolder_Click"/>
        </StackPanel>

        <!--主体左右双从站布局-->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <!--左侧从站1区域-->
            <Grid Grid.Column="0">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
                    <TextBlock Text="端口号:" VerticalAlignment="Center"/>
                    <TextBox x:Name="txtPort1" Width="90" Text="10010" Margin="5,0"/>
                    <TextBlock Text="ModbusID:" VerticalAlignment="Center" Margin="12,0,0,0"/>
                    <TextBox x:Name="txtSlaveId1" Width="60" Text="1" Margin="5,0"/>
                </StackPanel>

                <!--从站1参数监视框-->
                <GroupBox Grid.Row="1" Header="监视参数" Margin="0,0,6,8">
                    <Grid Margin="8">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="90"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Row="0" Grid.Column="0" Text="油轮频率" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS1_Freq" Grid.Row="0" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="0" Grid.Column="2" Foreground="Red" Text=" 41519"/>

                        <TextBlock Grid.Row="1" Grid.Column="0" Text="油轮加速时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS1_AccTime" Grid.Row="1" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="1" Grid.Column="2" Foreground="Red" Text=" 41533"/>

                        <TextBlock Grid.Row="2" Grid.Column="0" Text="GR1初始加速时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS1_GR1" Grid.Row="2" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="2" Grid.Column="2" Foreground="Red" Text=" 41534"/>

                        <TextBlock Grid.Row="3" Grid.Column="0" Text="GR2初始加速时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS1_GR2" Grid.Row="3" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="3" Grid.Column="2" Foreground="Red" Text=" 41535"/>

                        <TextBlock Grid.Row="4" Grid.Column="0" Text="GR3初始加速时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS1_GR3" Grid.Row="4" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="4" Grid.Column="2" Foreground="Red" Text=" 41536"/>

                        <TextBlock Grid.Row="5" Grid.Column="0" Text="爬升时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS1_Climb" Grid.Row="5" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="5" Grid.Column="2" Foreground="Red" Text=" 41568"/>

                        <TextBlock Grid.Row="6" Grid.Column="0" Text="备用1" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS1_Res1" Grid.Row="6" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="6" Grid.Column="2" Foreground="Red" Text=" 41569"/>

                        <TextBlock Grid.Row="7" Grid.Column="0" Text="备用2" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS1_Res2" Grid.Row="7" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="7" Grid.Column="2" Foreground="Red" Text=" 41570"/>
                    </Grid>
                </GroupBox>

                <!--从站1 IO状态面板-->
                <GroupBox Grid.Row="2" Header="监视状态">
                    <Grid Margin="6">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right">
                            <TextBlock Text="运行状态(M300)" FontSize="13"/>
                            <Ellipse x:Name="ledS1_M300" Width="16" Height="16" Fill="Green" Margin="6,0,0,0"/>
                        </StackPanel>
                        <Grid Grid.Row="1">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="8"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Border Grid.Column="2" Background="Black" Width="1"/>
                            <TextBlock x:Name="tbS1_XLeft" Grid.Column="0" FontSize="13"/>
                            <TextBlock x:Name="tbS1_XRight" Grid.Column="1" FontSize="13"/>
                            <TextBlock x:Name="tbS1_YLeft" Grid.Column="3" FontSize="13"/>
                            <TextBlock x:Name="tbS1_YRight" Grid.Column="4" FontSize="13"/>
                        </Grid>
                    </Grid>
                </GroupBox>
            </Grid>

            <!--右侧从站2区域-->
            <Grid Grid.Column="1">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
                    <TextBlock Text="端口号:" VerticalAlignment="Center"/>
                    <TextBox x:Name="txtPort2" Width="90" Text="10010" Margin="5,0"/>
                    <TextBlock Text="ModbusID:" VerticalAlignment="Center" Margin="12,0,0,0"/>
                    <TextBox x:Name="txtSlaveId2" Width="60" Text="2" Margin="5,0"/>
                </StackPanel>

                <GroupBox Grid.Row="1" Header="监视参数" Margin="6,0,0,8">
                    <Grid Margin="8">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="90"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Row="0" Grid.Column="0" Text="油轮频率" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS2_Freq" Grid.Row="0" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="0" Grid.Column="2" Foreground="Red" Text=" 41519"/>

                        <TextBlock Grid.Row="1" Grid.Column="0" Text="油轮加速时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS2_AccTime" Grid.Row="1" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="1" Grid.Column="2" Foreground="Red" Text=" 41533"/>

                        <TextBlock Grid.Row="2" Grid.Column="0" Text="GR1初始加速时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS2_GR1" Grid.Row="2" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="2" Grid.Column="2" Foreground="Red" Text=" 41534"/>

                        <TextBlock Grid.Row="3" Grid.Column="0" Text="GR2初始加速时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS2_GR2" Grid.Row="3" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="3" Grid.Column="2" Foreground="Red" Text=" 41535"/>

                        <TextBlock Grid.Row="4" Grid.Column="0" Text="GR3初始加速时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS2_GR3" Grid.Row="4" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="4" Grid.Column="2" Foreground="Red" Text=" 41536"/>

                        <TextBlock Grid.Row="5" Grid.Column="0" Text="爬升时间" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS2_Climb" Grid.Row="5" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="5" Grid.Column="2" Foreground="Red" Text=" 41568"/>

                        <TextBlock Grid.Row="6" Grid.Column="0" Text="备用1" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS2_Res1" Grid.Row="6" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="6" Grid.Column="2" Foreground="Red" Text=" 41569"/>

                        <TextBlock Grid.Row="7" Grid.Column="0" Text="备用2" HorizontalAlignment="Right"/>
                        <TextBox x:Name="tbS2_Res2" Grid.Row="7" Grid.Column="1" Background="#FFFCF000" IsReadOnly="True" Text="0.00"/>
                        <TextBlock Grid.Row="7" Grid.Column="2" Foreground="Red" Text=" 41570"/>
                    </Grid>
                </GroupBox>

                <GroupBox Grid.Row="2" Header="监视状态">
                    <Grid Margin="6">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right">
                            <TextBlock Text="运行状态(M300)" FontSize="13"/>
                            <Ellipse x:Name="ledS2_M300" Width="16" Height="16" Fill="Green" Margin="6,0,0,0"/>
                        </StackPanel>
                        <Grid Grid.Row="1">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="8"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Border Grid.Column="2" Background="Black" Width="1"/>
                            <TextBlock x:Name="tbS2_XLeft" Grid.Column="0" FontSize="13"/>
                            <TextBlock x:Name="tbS2_XRight" Grid.Column="1" FontSize="13"/>
                            <TextBlock x:Name="tbS2_YLeft" Grid.Column="3" FontSize="13"/>
                            <TextBlock x:Name="tbS2_YRight" Grid.Column="4" FontSize="13"/>
                        </Grid>
                    </Grid>
                </GroupBox>
            </Grid>
        </Grid>
    </Grid>
</Window>

10、对应逻辑代码(MainWindow.xaml.cs)

cs 复制代码
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using Modbus.Device;

namespace ModbusNModbusDemo
{
    /// <summary>轮询状态枚举:状态机四段流转控制轮询步骤</summary>
    public enum PollState
    {
        /// <summary>空闲:未连接/停止轮询</summary>
        Idle,
        /// <summary>采集从站1数据</summary>
        PollSlaveOne,
        /// <summary>采集从站2数据</summary>
        PollSlaveTwo,
        /// <summary>一轮采集完成延时等待</summary>
        LoopDelay
    }

    public partial class MainWindow : Window
    {
        #region 全局变量与常量定义
        private TcpClient _modbusTcpClient;             //ModbusTcp客户端对象
        private IModbusMaster _modbusMaster;            //NModbus主站通信实例
        private CancellationTokenSource _pollCts;      //轮询任务取消令牌
        private Task _pollTask;                         //后台轮询任务

        /// <summary>当前运行轮询状态,配合状态机切换</summary>
        private PollState _curState = PollState.Idle;

        /// <summary>轮询锁定从站ID,连接成功后固定不再修改</summary>
        private byte _slaveId1 = 1;
        private byte _slaveId2 = 2;

        /// <summary>单条Modbus指令超时时间(毫秒)</summary>
        private const int CmdTimeoutMs = 1000;
        /// <summary>一轮所有从站采集完毕后的间隔延时(毫秒)</summary>
        private const int LoopWaitMs = 200;

        /// <summary>保持寄存器逻辑地址数组(UI地址=数组值+40001,已剔除40001偏移)</summary>
        private readonly ushort[] _regAddrArr = { 1518, 1532, 1533, 1534, 1535, 1567, 1568, 1569 };

        /// <summary>DI离散输入UI起始地址10001(X0),代码内部读取需-10001做地址偏移</summary>
        private const ushort DiStart = 10001;
        private const ushort DiCount = 16;
        /// <summary>线圈输出UI起始地址11537(Y起始),代码内部读取需-10001做地址偏移</summary>
        private const ushort CoilStart = 11537;
        private const ushort CoilCount = 16;

        //IO默认全OFF初始文本
        private string _defXLeft, _defXRight, _defYLeft, _defYRight;

        //寄存器显示缓存:通信异常时保留上次有效值,不刷新为0
        private string[] _s1RegBuf = new string[8];
        private string[] _s2RegBuf = new string[8];
        //DI、线圈文本缓存:通信异常沿用旧文本
        private string _s1XLeftBuf, _s1XRightBuf, _s1YLeftBuf, _s1YRightBuf;
        private string _s2XLeftBuf, _s2XRightBuf, _s2YLeftBuf, _s2YRightBuf;
        #endregion

        #region 窗体初始化、日志相关方法
        public MainWindow()
        {
            InitializeComponent();
            CreateLogDir();
            InitDefaultIoText();
            InitUiDefaultValue();
        }

        /// <summary>创建日志保存文件夹ModbusLog</summary>
        private void CreateLogDir()
        {
            if (!Directory.Exists("ModbusLog")) Directory.CreateDirectory("ModbusLog");
        }

        /// <summary>生成X/Y点位默认OFF字符串,用于界面初始化赋值</summary>
        private void InitDefaultIoText()
        {
            _defXLeft = ""; _defXRight = ""; _defYLeft = ""; _defYRight = "";
            for (int i = 0; i < 8; i++) _defXLeft += $"X{i}: OFF{Environment.NewLine}";
            for (int i = 8; i < 16; i++) _defXRight += $"X{i + 2}: OFF{Environment.NewLine}";
            for (int i = 0; i < 8; i++) _defYLeft += $"Y{i}: OFF{Environment.NewLine}";
            for (int i = 8; i < 16; i++) _defYRight += $"Y{i + 2}: OFF{Environment.NewLine}";

            //初始化缓存为默认OFF文本
            _s1XLeftBuf = _defXLeft; _s1XRightBuf = _defXRight;
            _s1YLeftBuf = _defYLeft; _s1YRightBuf = _defYRight;
            _s2XLeftBuf = _defXLeft; _s2XRightBuf = _defXRight;
            _s2YLeftBuf = _defYLeft; _s2YRightBuf = _defYRight;
        }

        /// <summary>初始界面:寄存器默认0.00,IO全部OFF</summary>
        private void InitUiDefaultValue()
        {
            for (int i = 0; i < 8; i++)
            {
                _s1RegBuf[i] = "0.00";
                _s2RegBuf[i] = "0.00";
            }
            tbS1_XLeft.Text = _defXLeft;
            tbS1_XRight.Text = _defXRight;
            tbS1_YLeft.Text = _defYLeft;
            tbS1_YRight.Text = _defYRight;

            tbS2_XLeft.Text = _defXLeft;
            tbS2_XRight.Text = _defXRight;
            tbS2_YLeft.Text = _defYLeft;
            tbS2_YRight.Text = _defYRight;
        }

        /// <summary>分级日志写入:按日期生成txt日志,区分发送/接收/异常/系统</summary>
        private void WriteLog(string logType, string msg)
        {
            try
            {
                string fname = $"{DateTime.Now:yyyy-MM-dd}.txt";
                string path = Path.Combine("ModbusLog", fname);
                string line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]【{logType}】{msg}{Environment.NewLine}";
                File.AppendAllText(path, line);
            }
            catch { }
        }

        /// <summary>打开当日日志文档按钮事件</summary>
        private void BtnOpenLogFolder_Click(object sender, RoutedEventArgs e)
        {
            string fname = $"{DateTime.Now:yyyy-MM-dd}.txt";
            string path = Path.Combine("ModbusLog", fname);
            if (File.Exists(path))
                System.Diagnostics.Process.Start("notepad.exe", path);
            else
                MessageBox.Show("今日无日志");
        }
        #endregion

        #region 连接、断开按钮逻辑
        /// <summary>连接按钮:建立TCP+Modbus主站,启动轮询状态机</summary>
        private async void BtnConnect_Click(object sender, RoutedEventArgs e)
        {
            if (_curState != PollState.Idle)
            {
                MessageBox.Show("已处于连接轮询状态");
                return;
            }
            string ip = txtGlobalIp.Text.Trim();
            if (!int.TryParse(txtPort1.Text, out int port))
            {
                MessageBox.Show("端口格式错误");
                return;
            }
            //连接瞬间锁定从站ID,后续轮询不再更改
            byte.TryParse(txtSlaveId1.Text, out _slaveId1);
            byte.TryParse(txtSlaveId2.Text, out _slaveId2);

            try
            {
                _modbusTcpClient = new TcpClient { SendTimeout = CmdTimeoutMs, ReceiveTimeout = CmdTimeoutMs };
                await _modbusTcpClient.ConnectAsync(ip, port);
                _modbusMaster = ModbusIpMaster.CreateIp(_modbusTcpClient);
                WriteLog("系统", $"TCP连接成功 IP:{ip} Port:{port} S1ID:{_slaveId1} S2ID:{_slaveId2}");
                MessageBox.Show("连接成功,启动轮询");
                StartStateMachinePoll();
            }
            catch (Exception ex)
            {
                WriteLog("异常", $"连接失败:{ex.Message}");
                CloseConn();
                MessageBox.Show("连接失败:" + ex.Message);
            }
        }

        /// <summary>断开按钮:停止轮询、关闭通信链路</summary>
        private void BtnDisConnect_Click(object sender, RoutedEventArgs e)
        {
            CloseConn();
            WriteLog("系统", "手动断开通信,停止轮询,界面数据保持当前值");
            MessageBox.Show("已断开");
        }

        /// <summary>断开链路,停止轮询,界面数据不恢复默认</summary>
        private void CloseConn()
        {
            _curState = PollState.Idle;
            _pollCts?.Cancel();
            _pollCts?.Dispose();
            _pollCts = null;
            _modbusTcpClient?.Close();
            _modbusTcpClient?.Dispose();
            _modbusTcpClient = null;
            _modbusMaster = null;
        }
        #endregion

        #region 状态机轮询主体逻辑
        /// <summary>启动轮询状态机,后台循环依次采集从站1→从站2→延时等待</summary>
        private void StartStateMachinePoll()
        {
            _pollCts = new CancellationTokenSource();
            _curState = PollState.PollSlaveOne;
            _pollTask = Task.Run(async () =>
            {
                while (!_pollCts.Token.IsCancellationRequested && _curState != PollState.Idle)
                {
                    switch (_curState)
                    {
                        case PollState.PollSlaveOne:
                            await PollSingleSlave(_slaveId1, true);
                            _curState = PollState.PollSlaveTwo;
                            break;
                        case PollState.PollSlaveTwo:
                            await PollSingleSlave(_slaveId2, false);
                            _curState = PollState.LoopDelay;
                            break;
                        case PollState.LoopDelay:
                            await Task.Delay(LoopWaitMs, _pollCts.Token);
                            _curState = PollState.PollSlaveOne;
                            break;
                    }
                }
            }, _pollCts.Token);
        }

        /// <summary>单个从站全量采集:寄存器+DI输入+线圈输出</summary>
        /// <param name="sid">目标从站ID</param>
        /// <param name="isSlave1">true=从站1,false=从站2,区分UI缓存赋值</param>
        private async Task PollSingleSlave(byte sid, bool isSlave1)
        {
            await Task.Run(() =>
            {
                ushort[] regRes = new ushort[_regAddrArr.Length];
                bool regReadOk = true;
                bool[] diRes = null;
                bool[] coilRes = null;

                #region 1.逐个读取8路保持寄存器03,返回原始整数÷100转为两位小数
                if (_curState == PollState.Idle) return;
                WriteLog("发送", $"从站{sid}【03寄存器】开始逐个发送采集帧,共{_regAddrArr.Length}个地址");
                for (int i = 0; i < _regAddrArr.Length; i++)
                {
                    ushort addr = _regAddrArr[i];
                    try
                    {
                        WriteLog("发送", $"从站{sid}单点读寄存器逻辑地址:{addr}(UI标签={addr + 40001})");
                        var tmp = _modbusMaster.ReadHoldingRegisters(sid, addr, 1);
                        regRes[i] = tmp[0];
                        WriteLog("接收", $"从站{sid}逻辑{addr}原始整数:{tmp[0]},折算:{tmp[0] / 100.0:F2}");
                    }
                    catch (Exception ex)
                    {
                        WriteLog("异常", $"从站{sid}逻辑地址{addr}(UI{addr + 40001})读取异常:{ex.Message}");
                        regReadOk = false;
                    }
                }
                if (regReadOk)
                    WriteLog("接收", $"从站{sid}【03寄存器】全部读取正常");
                #endregion

                #region 2.读取DI输入02(x0-x15),UI10001需减10001偏移得到底层位地址
                if (_curState == PollState.Idle) return;
                WriteLog("发送", $"从站{sid}【02 DI】发送帧 起始:{DiStart} 点数:{DiCount}");
                try
                {
                    ushort diRealAddr = DiStart - 10001;
                    diRes = _modbusMaster.ReadInputs(sid, diRealAddr, DiCount);
                    WriteLog("接收", $"从站{sid}【02 DI】正常返回16点位");
                }
                catch (Exception ex)
                {
                    WriteLog("异常", $"从站{sid}【02 DI异常】{ex.Message}");
                }
                #endregion

                #region 3.读取线圈01,UI地址减10001偏移
                if (_curState == PollState.Idle) return;
                WriteLog("发送", $"从站{sid}【01 Coil】发送帧 起始:{CoilStart} 点数:{CoilCount}");
                try
                {
                    ushort coilRealAddr = CoilStart - 10001;
                    coilRes = _modbusMaster.ReadCoils(sid, coilRealAddr, CoilCount);
                    WriteLog("接收", $"从站{sid}【01 Coil】正常返回16点位");
                }
                catch (Exception ex)
                {
                    WriteLog("异常", $"从站{sid}【01 Coil异常】{ex.Message}");
                }
                #endregion

                #region UI刷新:成功数值÷100保留2位小数;异常沿用缓存原值
                Application.Current.Dispatcher.Invoke(() =>
                {
                    string[] buf = isSlave1 ? _s1RegBuf : _s2RegBuf;
                    if (regReadOk)
                    {
                        for (int i = 0; i < regRes.Length; i++)
                        {
                            double val = regRes[i] / 100.0;
                            buf[i] = val.ToString("0.00");
                        }
                    }

                    string xLText = "", xRText = "";
                    string yLText = "", yRText = "";

                    //DI拼接,ON后面带*
                    if (diRes != null)
                    {
                        for (int i = 0; i < 8; i++)
                        {
                            if (diRes[i])
                                xLText += $"X{i}: ON *{Environment.NewLine}";
                            else
                                xLText += $"X{i}: OFF{Environment.NewLine}";
                        }
                        for (int i = 8; i < 16; i++)
                        {
                            if (diRes[i])
                                xRText += $"X{i + 2}: ON *{Environment.NewLine}";
                            else
                                xRText += $"X{i + 2}: OFF{Environment.NewLine}";
                        }
                        if (isSlave1) { _s1XLeftBuf = xLText; _s1XRightBuf = xRText; }
                        else { _s2XLeftBuf = xLText; _s2XRightBuf = xRText; }
                    }
                    else
                    {
                        xLText = isSlave1 ? _s1XLeftBuf : _s2XLeftBuf;
                        xRText = isSlave1 ? _s1XRightBuf : _s2XRightBuf;
                    }

                    //线圈拼接,ON后面带*
                    if (coilRes != null)
                    {
                        for (int i = 0; i < 8; i++)
                        {
                            if (coilRes[i])
                                yLText += $"Y{i}: ON *{Environment.NewLine}";
                            else
                                yLText += $"Y{i}: OFF{Environment.NewLine}";
                        }
                        for (int i = 8; i < 16; i++)
                        {
                            if (coilRes[i])
                                yRText += $"Y{i + 2}: ON *{Environment.NewLine}";
                            else
                                yRText += $"Y{i + 2}: OFF{Environment.NewLine}";
                        }
                        if (isSlave1) { _s1YLeftBuf = yLText; _s1YRightBuf = yRText; }
                        else { _s2YLeftBuf = yLText; _s2YRightBuf = yRText; }
                    }
                    else
                    {
                        yLText = isSlave1 ? _s1YLeftBuf : _s2YLeftBuf;
                        yRText = isSlave1 ? _s1YRightBuf : _s2YRightBuf;
                    }

                    //赋值
                    if (isSlave1)
                    {
                        tbS1_Freq.Text = buf[0];
                        tbS1_AccTime.Text = buf[1];
                        tbS1_GR1.Text = buf[2];
                        tbS1_GR2.Text = buf[3];
                        tbS1_GR3.Text = buf[4];
                        tbS1_Climb.Text = buf[5];
                        tbS1_Res1.Text = buf[6];
                        tbS1_Res2.Text = buf[7];

                        tbS1_XLeft.Text = _s1XLeftBuf;
                        tbS1_XRight.Text = _s1XRightBuf;
                        tbS1_YLeft.Text = _s1YLeftBuf;
                        tbS1_YRight.Text = _s1YRightBuf;
                    }
                    else
                    {
                        tbS2_Freq.Text = buf[0];
                        tbS2_AccTime.Text = buf[1];
                        tbS2_GR1.Text = buf[2];
                        tbS2_GR2.Text = buf[3];
                        tbS2_GR3.Text = buf[4];
                        tbS2_Climb.Text = buf[5];
                        tbS2_Res1.Text = buf[6];
                        tbS2_Res2.Text = buf[7];

                        tbS2_XLeft.Text = _s2XLeftBuf;
                        tbS2_XRight.Text = _s2XRightBuf;
                        tbS2_YLeft.Text = _s2YLeftBuf;
                        tbS2_YRight.Text = _s2YRightBuf;
                    }
                });
                #endregion
            });
        }
        #endregion
    }
}

11、日志文件

从日志上可以看出单个数据帧的响应时间大概40MS

12、运行界面

相关推荐
何中应1 小时前
Nexus如何设置端口号
java·服务器·maven·nexus
.小小陈.1 小时前
应用层协议 HTTP 全解析:从基础到实战
网络·网络协议·http
Irissgwe1 小时前
10、NAT、代理服务、内网穿透
网络·frp·内网穿透·nat·代理服务器·反向代理·正向代理
小此方1 小时前
Re:Linux系统篇(二十七)进程篇·十二:从零构建属于你的自定义 Shell 解释器
linux·运维·服务器
Shadow(⊙o⊙)1 小时前
mkfifo()命名管道-FIFO客户端 服务端模拟。*System V消息队列、信号量(信号灯)。
linux·运维·服务器·开发语言·c++
网络研究院1 小时前
AI安全格局:前沿模型、智能体AI和AI编码工具如何重塑网络安全与关键基础设施韧性
网络·人工智能·安全·模型·威胁
10WTW011 小时前
计网实验 协议分析--ARP协议
网络
酉鬼女又兒1 小时前
零基础入门计算机网络:点对点协议PPP、媒体接入控制基本概念、静态划分信道技术、CSMA/CD与CSMA/CA协议全面详解
服务器·网络·网络协议·计算机网络·职场和发展·求职招聘·媒体
Shadow(⊙o⊙)2 小时前
System V共享内存详解,shm系列接口,三种共享内存删除机制。System V通信缺点分析
linux·运维·服务器·开发语言·网络·c++