一、效果展示
二、 VS2022
界面设计
引用NModbus4
三、代码展示
ViewModels、MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;using CommunityToolkit.Mvvm.Input;using Modbus.Device;using System;using System.Collections.Generic;using System.Collections.ObjectModel;using System.IO;using System.IO.Ports;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Windows.documents;using System.Windows.Media;using System.Windows.Media.Imaging;using Wpf.Ui.Controls;namespace WpfHomeModbusDemo.ViewModels{ public partial class MainWindowViewModel: ObservableObject { public MainWindowViewModel() { //获取串口列表 portNameList = new ObservableCollection<string>(SerialPort.GetPortNames()); //初始化波特率 数据位 校验位 停止位 列表 BaudRateList = new ObservableCollection<int> { 4800, 9600, 19200, 38400, 57600, 115200 }; DataBitsList = new ObservableCollection<int> { 7, 8 }; ParityList = new ObservableCollection<Parity>(Enum.GetValues(typeof(Parity)).Cast<Parity>()); StopBitsList = new ObservableCollection<StopBits>(Enum.GetValues(typeof(StopBits)).Cast<StopBits>()); //设置串口默认值 SelectedBaudRate = 9600; SelectedDataBits = 8; SelectedParity = Parity.None; SelectedStopBits = StopBits.One; //设置默认媒体图片 MediaImage = new BitmapImage(new Uri("pack://application:,,,/Images/image.jpg")); } //串口列表 [ObservableProperty] private ObservableCollection<string> portNameList; //波特率列表 [ObservableProperty] private ObservableCollection<int> baudRateList; //数据位列表 [ObservableProperty] private ObservableCollection<int> dataBitsList; [ObservableProperty] private ObservableCollection<Parity> parityList; [ObservableProperty] private ObservableCollection<StopBits> stopBitsList; //串口属性 [ObservableProperty] private string selectedPort; [ObservableProperty] private int selectedBaudRate; [ObservableProperty] private int selectedDataBits; [ObservableProperty] private Parity selectedParity; [ObservableProperty] private StopBits selectedStopBits; [ObservableProperty] private bool isConnected; private SerialPort serialPort; private IModbusSerialMaster master; //连接串口 [RelayCommand] private void Connect() { if (IsConnected == true) { Open(); StartReading(); } else { Close(); } } //打开连接 private bool Open() { try { serialPort = new SerialPort(); serialPort.PortName = SelectedPort; serialPort.BaudRate = SelectedBaudRate; serialPort.DataBits = SelectedDataBits; serialPort.Parity = SelectedParity; serialPort.StopBits = SelectedStopBits; serialPort.Open(); master = ModbusSerialMaster.CreateRtu(serialPort); master.Transport.ReadTimeout = 2000; //读超时 master.Transport.Retries = 3; //重试次数 var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "提示", Content = "串口打开成功", }; uiMessageBox.ShowDialogAsync(true); return true; } catch (UnauthorizedAccessException) { var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "错误", Content = "无法访问串口,可能串口被其他程序占用", }; uiMessageBox.ShowDialogAsync(false); return false; } catch (IOException ex) { var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "错误", Content = "串口连接错误: " + ex.Message, }; uiMessageBox.ShowDialogAsync(false); return false; } catch (Exception ex) { var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "错误", Content = "打开串口时发生未知错误: " + ex.Message }; uiMessageBox.ShowDialogAsync(false); return false; } } //关闭连接 private void Close() { try { if (serialPort.IsOpen) { serialPort.Close(); var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "提示", Content = "串口已关闭!" }; uiMessageBox.ShowDialogAsync(false); } } catch (Exception ex) { var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "错误", Content = "关闭串口时发生错误: " + ex.Message }; uiMessageBox.ShowDialogAsync(false); } } //检查连接 private void CheckConnection() { if (master == null || !IsConnected) { throw new InvalidOperationException("串口未打开或已断开,请检查连接!"); } } [ObservableProperty] private bool mainLightIsChecked; [ObservableProperty] private bool deskLampIsChecked; [ObservableProperty] private bool tvIsChecked; [ObservableProperty] private bool pS5IsChecked; [ObservableProperty] private ImageSource mediaImage; //主厅灯光数值 [ObservableProperty] private string mainLight; //数据读取 private async void StartReading() { while (true) { // 如果没有连接,则停止循环 if (!IsConnected) { break; } Read(); // 执行读取数据 await Task.Delay(1000); // 每隔1秒读取一次 } } private void Read() { if (master == null) { //var uiMessageBox = new Wpf.Ui.Controls.MessageBox //{ // Title = "提示", // Content = "串口未打开或已断开,请检查连接!", //}; //uiMessageBox.ShowDialogAsync(false); return; } //客厅部分 byte slaveAddress = 1; //从站地址 ushort startAddress = 0; //起始寄存器地址 ushort numberOfPoints = 4; //读取寄存器数量 // 读取从站信息 CheckConnection(); // 01 输入状态 try { bool[] LivingRoom = master.ReadCoils(slaveAddress, startAddress, numberOfPoints); //主厅灯 MainLightIsChecked = LivingRoom[0]; //氛围灯 DeskLampIsChecked = LivingRoom[1]; //电视 TvIsChecked = LivingRoom[2]; //PS5 PS5IsChecked = LivingRoom[3]; } catch (Modbus.SlaveException ex) { HandleModbusException(ex); } // 04 输入寄存器 try { //04 ushort[] register = master.ReadHoldingRegisters(slaveAddress, startAddress, numberOfPoints); MainLight = register[0]!.ToString(); } catch (Modbus.SlaveException ex) { HandleModbusException(ex); } } // 处理 Modbus 错误 private void HandleModbusException(Modbus.SlaveException ex) { var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "提示", Content = "未知错误", }; switch (ex.SlaveExceptionCode) { case 1: uiMessageBox.Content = "异常代码 1: 非法功能码!"; uiMessageBox.ShowDialogAsync(); break; case 2: uiMessageBox.Content = "异常代码 2: 非法数据地址!"; uiMessageBox.ShowDialogAsync(); break; case 3: uiMessageBox.Content = "异常代码 3: 非法数据值!"; uiMessageBox.ShowDialogAsync(); break; case 4: uiMessageBox.Content = "异常代码 4: 从设备故障!"; uiMessageBox.ShowDialogAsync(); break; case 5: uiMessageBox.Content = "异常代码 5: 硬件故障!"; uiMessageBox.ShowDialogAsync(); break; case 6: uiMessageBox.Content = "异常代码 6: 从设备忙!"; uiMessageBox.ShowDialogAsync(); break; case 7: uiMessageBox.Content = "异常代码 7: 内存错误!"; uiMessageBox.ShowDialogAsync(); break; default: uiMessageBox.Content = "未知异常代码: " + ex.SlaveExceptionCode; uiMessageBox.ShowDialogAsync(); break; } Close(); } [RelayCommand] private void WriteLighting() { //写入数据 if (master == null) { var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "提示", Content = "串口未打开或已断开,请检查连接!", }; uiMessageBox.ShowDialogAsync(false); return; } byte slaveAddress = 1; ushort coilAddress = 0; //可能事件与IsChecked冲突,这里暂时不需要取反 //bool value = !MainLightIsChecked; bool value = MainLightIsChecked; master.WriteSingleCoil(slaveAddress, coilAddress, value); ushort registerAddress = 0; ushort valueToWrite = 0; try { master.WriteSingleRegister(slaveAddress, registerAddress, valueToWrite); } catch (Modbus.SlaveException ex) { HandleModbusException(ex); } // 可选:写完后读回来验证 //ushort[] readBack = master.ReadHoldingRegisters(slaveAddress, registerAddress, 1); //MainLight = readBack[0].ToString(); } }}MainWindow.xaml
<Window x:Class="WpfHomeModbusDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:WpfHomeModbusDemo.Controls" xmlns:converters="clr-namespace:WpfHomeModbusDemo.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:WpfHomeModbusDemo" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:viewmodels="clr-namespace:WpfHomeModbusDemo.ViewModels" Title="" Width="1200" Height="750" d:DataContext="{d:DesignInstance viewmodels:MainWindowViewModel}" mc:Ignorable="d"> <Window.Resources> <converters:ProgressRingVisibilityConverter x:Key="ProgressRingVisibilityConverter" /> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.25*" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid Grid.Column="0" Background="#111517"> <DockPanel MinWidth="150" Margin="25" HorizontalAlignment="Center" LastChildFill="False"> <StackPanel DockPanel.Dock="Top"> <StackPanel Orientation="Vertical"> <TextBlock Foreground="White" Text="PortName" /> <ComboBox Margin="0,10,0,0" ItemsSource="{Binding PortNameList}" SelectedItem="{Binding SelectedPort, Mode=TwoWay}" /> </StackPanel> <StackPanel Margin="0,20,0,0" Orientation="Vertical"> <TextBlock Foreground="White" Text="BaudRate" /> <ComboBox Margin="0,10,0,0" ItemsSource="{Binding BaudRateList}" SelectedItem="{Binding SelectedBaudRate, Mode=TwoWay}" /> </StackPanel> <StackPanel Margin="0,20,0,0" Orientation="Vertical"> <TextBlock Foreground="White" Text="DataBits" /> <ComboBox Margin="0,10,0,0" ItemsSource="{Binding DataBitsList}" SelectedItem="{Binding SelectedDataBits, Mode=TwoWay}" /> </StackPanel> <StackPanel Margin="0,20,0,0" Orientation="Vertical"> <TextBlock Foreground="White" Text="Parity" /> <ComboBox Margin="0,10,0,0" ItemsSource="{Binding ParityList}" SelectedItem="{Binding SelectedParity, Mode=TwoWay}" /> </StackPanel> <StackPanel Margin="0,20,0,0" Orientation="Vertical"> <TextBlock Foreground="White" Text="StopBits" /> <ComboBox Margin="0,10,0,0" ItemsSource="{Binding StopBitsList}" SelectedItem="{Binding SelectedStopBits, Mode=TwoWay}" /> </StackPanel> <ui:ToggleSwitch x:Name="SerialToggle" Margin="0,50,0,0" Command="{Binding ConnectCommand}" IsChecked="{Binding IsConnected, Mode=TwoWay}" OffContent="关" onContent="开" /> </StackPanel> </DockPanel> </Grid> <Grid Grid.Column="1" Background="#151a1b"> <UniformGrid Margin="30" Columns="3" Rows="2"> <DockPanel Margin="0,0,10,10"> <TextBlock DockPanel.Dock="Top" FontFamily="微软雅黑" FontSize="24" FontWeight="Bold" Foreground="#bcbebf" Text="客厅" /> <UniformGrid Margin="0,10,0,0" Columns="2" Rows="2"> <controls:DeviceToggleButton Margin="0,0,5,5" DeviceName="主厅" DeviceType="灯光" Devicevalue="{Binding MainLight}" Icon="{DynamicResource Lighting}" Command="{Binding WriteLightingCommand}" IsChecked="{Binding MainLightIsChecked}"> <ui:ProgressRing Width="32" Height="32" Foreground="#b6b6b6" Progress="{Binding Devicevalue, RelativeSource={RelativeSource AncestorType=controls:DeviceToggleButton}}" Visibility="{Binding Devicevalue, RelativeSource={RelativeSource AncestorType=controls:DeviceToggleButton}, Converter={StaticResource ProgressRingVisibilityConverter}}" /> </controls:DeviceToggleButton> <controls:DeviceToggleButton Margin="5,0,0,5" DeviceName="氛围" DeviceType="灯光" Icon="{DynamicResource DeskLamp}" IsChecked="{Binding DeskLampIsChecked}" /> <controls:DeviceToggleButton Margin="0,5,5,0" DeviceName="电视" DeviceType="媒体" Icon="{DynamicResource Tv}" IsChecked="{Binding TvIsChecked}" /> <controls:DeviceToggleButton Margin="5,5,0,0" DeviceName="游戏机" DeviceType="媒体" Icon="{DynamicResource PS5}" IsChecked="{Binding PS5IsChecked}" /> </UniformGrid> </DockPanel> <DockPanel Margin="10,0,10,10"> <TextBlock DockPanel.Dock="Top" FontFamily="微软雅黑" FontSize="24" FontWeight="Bold" Foreground="#bcbebf" Text="工作室" /> <UniformGrid Margin="0,10,0,0" Columns="2" Rows="2"> <controls:DeviceToggleButton Margin="0,0,5,5" DeviceName="工作" DeviceType="灯光" Icon="{DynamicResource Lighting}" /> <controls:DeviceToggleButton Margin="5,0,0,5" DeviceName="射灯" DeviceType="灯光" Icon="{DynamicResource Spotlight}" /> <controls:DeviceToggleButton Margin="0,5,5,0" DeviceName="电脑" DeviceType="媒体" Icon="{DynamicResource Computer}" /> <controls:DeviceToggleButton Margin="5,5,0,0" DeviceName="音响" DeviceType="媒体" Icon="{DynamicResource Speaker}" /> </UniformGrid> </DockPanel> <DockPanel Margin="10,0,0,10"> <TextBlock DockPanel.Dock="Top" FontFamily="微软雅黑" FontSize="24" FontWeight="Bold" Foreground="#bcbebf" Text="卧室" /> <UniformGrid Margin="0,10,0,0" Columns="2" Rows="2"> <controls:DeviceToggleButton Margin="0,0,5,5" DeviceName="床头灯(左)" DeviceType="灯光" Icon="{DynamicResource BedsideLamp}" /> <controls:DeviceToggleButton Margin="5,0,0,5" DeviceName="床头灯(右)" DeviceType="灯光" Icon="{DynamicResource BedsideLamp}" /> <controls:DeviceToggleButton Margin="0,5,5,0" DeviceName="风扇" DeviceType="家居" Icon="{DynamicResource Fan}" /> <controls:DeviceToggleButton Margin="5,5,0,0" DeviceName="电视" DeviceType="媒体" Icon="{DynamicResource Tv}" /> </UniformGrid> </DockPanel> <DockPanel Margin="0,10,10,0"> <TextBlock DockPanel.Dock="Top" FontFamily="微软雅黑" FontSize="24" FontWeight="Bold" Foreground="#bcbebf" Text="媒体" /> <Border Margin="0,10,0,0" Background="Transparent" ClipToBounds="True" CornerRadius="10" DockPanel.Dock="Bottom"> <Image Source="{Binding MediaImage}" Stretch="UniformToFill" /> </Border> </DockPanel> <DockPanel Margin="10,10,10,0"> <TextBlock DockPanel.Dock="Top" FontFamily="微软雅黑" FontSize="24" FontWeight="Bold" Foreground="#bcbebf" Text="灯光" /> <UniformGrid Margin="0,10,0,0" Columns="2" Rows="2"> <controls:DeviceToggleButton Margin="0,0,5,5" DeviceName="卫生间" DeviceType="灯光" Icon="{DynamicResource Bathroom}" /> <controls:DeviceToggleButton Margin="5,0,0,5" DeviceName="厨房" DeviceType="灯光" Icon="{DynamicResource Kitchen}" /> <controls:DeviceToggleButton Margin="0,5,5,0" DeviceName="衣帽间" DeviceType="灯光" Icon="{DynamicResource WalkInCloset}" /> <controls:DeviceToggleButton Margin="5,5,0,0" DeviceName="大厅" DeviceType="灯光" Icon="{DynamicResource Spotlight}" /> </UniformGrid> </DockPanel> <DockPanel Margin="10,10,0,0"> <TextBlock DockPanel.Dock="Top" FontFamily="微软雅黑" FontSize="24" FontWeight="Bold" Foreground="#bcbebf" Text="家" /> <UniformGrid Margin="0,10,0,0" Columns="2" Rows="2"> <controls:DeviceToggleButton Margin="0,0,5,5" DeviceName="温度计" DeviceType="家居" Icon="{DynamicResource Thermometer}" /> <controls:DeviceToggleButton Margin="5,0,0,5" DeviceName="插座能耗" DeviceType="家居" Icon="{DynamicResource EnergyConsumption}" /> <controls:DeviceToggleButton Margin="0,5,5,0" DeviceName="空调" DeviceType="家居" Icon="{DynamicResource AirConditioner}" /> <controls:DeviceToggleButton Margin="5,5,0,0" DeviceName="Yao" DeviceType="成员" Icon="{DynamicResource Man}" /> </UniformGrid> </DockPanel> </UniformGrid> </Grid> </Grid></Window>MainWindow.xaml.cs
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Windows;using System.Windows.Controls;using System.Windows.Data;using System.Windows.documents;using System.Windows.Input;using System.Windows.Media;using System.Windows.Media.Imaging;using System.Windows.Navigation;using System.Windows.Shapes;using WpfHomeModbusDemo.ViewModels;namespace WpfHomeModbusDemo{ /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new MainWindowViewModel(); } }}学员招募
C#上位机编程一对一教学+就位指导,有兴趣的私信。
