C#上位机软件实战:实时通讯与智能参数配置
·
【智能设备开发】C#上位机软件实战:实时通讯与智能参数配置
一、引言
随着物联网(IoT)的不断发展,智能设备在工业自动化、环境监测、新能源等领域得到广泛应用。作为智能设备的核心配套软件,上位机需要实现稳定实时通讯和灵活参数配置两大核心能力。
本文将使用 C# + WPF(也可扩展至 .NET MAUI)开发一套实用型智能设备上位机,重点实现实时通讯与智能参数配置功能,代码遵循生产级规范,可直接用于实际项目。
二、系统设计与架构
核心设计原则:
- 轻量且可扩展
- 异步通讯优先
- 配置动态化(支持保存/加载)
- 高稳定性(断线重连、参数校验)
主要模块:
- 实时通讯模块(串口 + Modbus TCP)
- 参数配置模块(动态UI + 持久化)
- 数据监控与报警模块
- 数据存储模块
三、实时通讯:与设备的数据交互
智能设备常见的通讯方式包括串口(RS232/RS485)和TCP/IP(Modbus TCP)。
1. 通讯服务抽象(推荐)
public interface IDeviceCommService : IAsyncDisposable
{
Task<bool> ConnectAsync(DeviceConfig config);
Task<Dictionary<string, object>> ReadParametersAsync();
Task<bool> WriteParameterAsync(string paramName, object value);
bool IsConnected { get; }
event EventHandler<DeviceDataEventArgs>? DataReceived;
}
2. Modbus TCP 实现
public class ModbusTcpCommService : IDeviceCommService
{
private ModbusTcpMaster? _master;
private readonly ILogger _logger;
public async Task<bool> ConnectAsync(DeviceConfig config)
{
try
{
_master = new ModbusTcpMaster(config.IpAddress, config.Port);
await _master.ConnectAsync();
_logger.Information("设备连接成功 {Ip}:{Port}", config.IpAddress, config.Port);
return true;
}
catch (Exception ex)
{
_logger.Error(ex, "连接失败");
return false;
}
}
public async Task<Dictionary<string, object>> ReadParametersAsync()
{
var result = new Dictionary<string, object>();
var values = await _master!.ReadHoldingRegistersAsync(1, 1000, 20);
// 根据地址映射解析参数
result["Temperature"] = values[0] / 10.0;
result["Humidity"] = values[1] / 10.0;
result["Setpoint"] = values[2];
return result;
}
}
3. 串口通讯(带粘包处理)
推荐直接复用之前提供的 SerialPortFrameParser 类实现稳定帧解析。
四、智能参数配置模块
1. 参数配置模型
public class DeviceParameter
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public object Value { get; set; } = null!;
public object? DefaultValue { get; set; }
public double? Min { get; set; }
public double? Max { get; set; }
public string Unit { get; set; } = "";
public bool IsReadOnly { get; set; }
}
2. 参数配置界面(WPF示例)
<!-- ParameterConfigView.xaml -->
<DataGrid ItemsSource="{Binding Parameters}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="参数名称" Binding="{Binding Description}"/>
<DataGridTextColumn Header="当前值" Binding="{Binding Value}"/>
<DataGridTextColumn Header="单位" Binding="{Binding Unit}"/>
<DataGridTemplateColumn Header="操作">
<DataTemplate>
<Button Content="写入" Command="{Binding WriteCommand}"/>
</DataTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
ViewModel实现:
public partial class ParameterConfigViewModel : ObservableObject
{
private readonly IDeviceCommService _commService;
[ObservableProperty] private ObservableCollection<DeviceParameter> parameters = new();
[RelayCommand]
private async Task WriteParameter(DeviceParameter param)
{
if (await _commService.WriteParameterAsync(param.Name, param.Value))
{
Log.Information("参数写入成功: {Name} = {Value}", param.Name, param.Value);
}
}
public async Task LoadParametersAsync()
{
var data = await _commService.ReadParametersAsync();
// 映射到 Parameters 集合
}
}
五、数据监控与报警
- 使用 OxyPlot 实现实时曲线监控
- 建立阈值规则引擎,异常时自动报警并记录日志
六、数据存储与导出
推荐使用 ClosedXML 一键导出Excel,或 SQLite 进行本地历史数据存储。
七、部署与优化建议
- 使用 .NET 9 Native AOT 发布单文件exe
- 支持配置文件(JSON)保存常用设备参数
- 增加断线自动重连机制
结语:实时通讯是上位机的基础,智能参数配置是提升设备可管理性的关键。通过C#强大的异步能力和组件生态,我们可以快速开发出稳定、易用的智能设备上位机软件。
本方案可与之前的 Modbus实战、串口粘包解析、ValueTask优化 等内容无缝结合,形成完整上位机开发技术栈。
需要我继续补充以下任意部分吗?
- 完整 ParameterConfigView + ViewModel 代码
- 实时通讯 + 参数配置 完整 Demo 结构
- 报警联动 实现示例
- Native AOT 发布配置
1. 实时通讯 + 参数配置 完整 Demo 工程结构
SmartDeviceUpperMonitor.WPF/
├── SmartDeviceUpperMonitor/
│ ├── App.xaml
│ ├── App.xaml.cs # DI 配置 + Serilog 初始化
│ ├── MainWindow.xaml # 主界面 (TabControl)
│ │
│ ├── Core/
│ │ ├── Models/
│ │ │ ├── DeviceConfig.cs
│ │ │ ├── DeviceParameter.cs
│ │ │ └── AlarmEventArgs.cs
│ │ └── Events/
│ │
│ ├── Interfaces/
│ │ ├── IDeviceCommService.cs
│ │ └── IAlarmService.cs
│ │
│ ├── Services/
│ │ ├── Comm/
│ │ │ ├── ModbusTcpCommService.cs
│ │ │ └── SerialCommService.cs
│ │ ├── ParameterConfigService.cs
│ │ ├── AlarmService.cs
│ │ └── ExportService.cs
│ │
│ ├── ViewModels/
│ │ ├── MainViewModel.cs
│ │ ├── DashboardViewModel.cs
│ │ └── ParameterConfigViewModel.cs # 参数配置核心
│ │
│ ├── Views/
│ │ ├── DashboardView.xaml
│ │ └── ParameterConfigView.xaml # 参数配置界面
│ │
│ └── Configuration/
│ └── AppSettings.json
│
├── SmartDeviceUpperMonitor.Core/ # 共享实体(可选)
└── packages/
2. 完整 ParameterConfigView + ViewModel 代码
ParameterConfigView.xaml
<UserControl x:Class="SmartDeviceUpperMonitor.Views.ParameterConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 工具栏 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,12">
<Button Content="读取所有参数" Width="140" Margin="5"
Command="{Binding ReadAllParametersCommand}"/>
<Button Content="保存为默认配置" Width="140" Margin="5"
Command="{Binding SaveAsDefaultCommand}"/>
<Button Content="应用所有修改" Width="140" Margin="5"
Command="{Binding ApplyAllCommand}" Style="{StaticResource AccentButton}"/>
</StackPanel>
<!-- 参数列表 -->
<DataGrid Grid.Row="1" ItemsSource="{Binding Parameters}"
AutoGenerateColumns="False" CanUserSortColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="参数名称" Binding="{Binding Description}" Width="180" IsReadOnly="True"/>
<DataGridTextColumn Header="当前值" Binding="{Binding Value}" Width="120"/>
<DataGridTextColumn Header="单位" Binding="{Binding Unit}" Width="80" IsReadOnly="True"/>
<DataGridTextColumn Header="范围" Binding="{Binding RangeDisplay}" Width="120" IsReadOnly="True"/>
<DataGridTemplateColumn Header="操作" Width="100">
<DataTemplate>
<Button Content="写入" Padding="8,4"
Command="{Binding DataContext.WriteParameterCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
ParameterConfigViewModel.cs(使用 CommunityToolkit.Mvvm)
public partial class ParameterConfigViewModel : ObservableObject
{
private readonly IDeviceCommService _commService;
private readonly ILogger<ParameterConfigViewModel> _logger;
[ObservableProperty] private ObservableCollection<DeviceParameter> parameters = new();
[ObservableProperty] private bool isConnected;
[ObservableProperty] private bool isBusy;
public ParameterConfigViewModel(IDeviceCommService commService, ILogger<ParameterConfigViewModel> logger)
{
_commService = commService;
_logger = logger;
}
[RelayCommand]
private async Task ReadAllParametersAsync()
{
IsBusy = true;
try
{
var data = await _commService.ReadParametersAsync();
Parameters.Clear();
foreach (var kv in data)
{
Parameters.Add(new DeviceParameter
{
Name = kv.Key,
Description = GetDescription(kv.Key),
Value = kv.Value,
Unit = GetUnit(kv.Key)
});
}
_logger.Information("成功读取 {Count} 个设备参数", data.Count);
}
catch (Exception ex)
{
_logger.Error(ex, "读取参数失败");
}
finally
{
IsBusy = false;
}
}
[RelayCommand]
private async Task WriteParameterAsync(DeviceParameter param)
{
if (param == null) return;
try
{
bool success = await _commService.WriteParameterAsync(param.Name, param.Value);
if (success)
_logger.Information("参数写入成功 → {Name} = {Value}", param.Description, param.Value);
}
catch (Exception ex)
{
_logger.Error(ex, "写入参数失败");
}
}
[RelayCommand]
private async Task ApplyAllAsync()
{
foreach (var param in Parameters)
await WriteParameterAsync(param);
}
// 辅助方法:根据参数名返回描述和单位(可改为配置化)
private string GetDescription(string key) => key switch
{
"Temperature" => "目标温度",
"Humidity" => "湿度设定",
_ => key
};
private string GetUnit(string key) => key switch
{
"Temperature" => "°C",
"Humidity" => "%RH",
_ => ""
};
}
3. 报警联动 实现示例
AlarmService.cs
public class AlarmService
{
private readonly List<AlarmRule> _rules = new();
private readonly IEventAggregator _eventAggregator;
private readonly ILogger _logger;
public void AddRule(AlarmRule rule) => _rules.Add(rule);
public void Evaluate(DeviceData data)
{
foreach (var rule in _rules.Where(r => r.Enabled))
{
if (rule.DeviceId != data.DeviceId) continue;
double value = data.GetValue(rule.Parameter);
if ((rule.ThresholdHigh.HasValue && value > rule.ThresholdHigh) ||
(rule.ThresholdLow.HasValue && value < rule.ThresholdLow))
{
var alarm = new AlarmEventArgs
{
DeviceId = data.DeviceId,
Parameter = rule.Parameter,
Value = value,
Severity = rule.Severity,
Description = $"{rule.Parameter} 超出阈值 ({value})"
};
_eventAggregator.GetEvent<AlarmRaisedEvent>().Publish(alarm);
_logger.Warning("报警触发: {Desc}", alarm.Description);
}
}
}
}
4. Native AOT 发布配置(.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<!-- Native AOT 核心配置 -->
<PublishAot>true</PublishAot>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<OptimizationPreference>Speed</OptimizationPreference>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>
</Project>
发布命令:
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishAot=true
1. 优化后的 Demo 工程结构(推荐最终版)
SmartDeviceUpperMonitor.WPF/
├── SmartDeviceUpperMonitor/ # 主项目
│ ├── App.xaml
│ ├── App.xaml.cs # DI 配置 + Serilog + 启动逻辑
│ ├── MainWindow.xaml # 主界面布局(TabControl)
│ ├── MainWindow.xaml.cs
│ │
│ ├── Core/ # 领域核心
│ │ ├── Models/ # DeviceConfig、DeviceParameter、AlarmEventArgs
│ │ ├── Enums/
│ │ └── Events/ # 自定义事件(如 AlarmRaisedEvent)
│ │
│ ├── Interfaces/ # 接口定义层
│ │ ├── IDeviceCommService.cs
│ │ ├── IParameterService.cs
│ │ └── IAlarmService.cs
│ │
│ ├── Services/ # 业务实现
│ │ ├── Comm/
│ │ │ ├── ModbusTcpCommService.cs
│ │ │ └── SerialCommService.cs
│ │ ├── ParameterService.cs
│ │ ├── AlarmService.cs
│ │ └── LoggingService.cs
│ │
│ ├── ViewModels/ # MVVM
│ │ ├── MainViewModel.cs
│ │ ├── DashboardViewModel.cs
│ │ └── ParameterConfigViewModel.cs
│ │
│ ├── Views/ # 界面
│ │ ├── DashboardView.xaml
│ │ └── ParameterConfigView.xaml
│ │
│ ├── Controls/ # 自定义控件(可选)
│ ├── Configuration/ # 配置
│ │ └── appsettings.json
│ └── Helpers/
│
├── SmartDeviceUpperMonitor.Core/ # 可复用核心类库(推荐)
├── SmartDeviceUpperMonitor.Tests/
└── README.md
2. 完整 App.xaml.cs(DI 配置)
public partial class App : Application
{
public static IServiceProvider Services { get; private set; } = null!;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 配置日志
LoggingService.Configure();
// 配置依赖注入
var services = new ServiceCollection();
ConfigureServices(services);
Services = services.BuildServiceProvider();
var mainWindow = Services.GetRequiredService<MainWindow>();
mainWindow.Show();
}
private static void ConfigureServices(IServiceCollection services)
{
// 配置绑定
services.AddSingleton<IConfiguration>(new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build());
// 服务注册
services.AddSingleton<IDeviceCommService, ModbusTcpCommService>();
services.AddSingleton<IAlarmService, AlarmService>();
services.AddSingleton<IParameterService, ParameterService>();
// ViewModels
services.AddTransient<MainViewModel>();
services.AddTransient<DashboardViewModel>();
services.AddTransient<ParameterConfigViewModel>();
// 主窗口
services.AddTransient<MainWindow>();
}
}
3. MainWindow.xaml 布局(推荐主界面)
<Window x:Class="SmartDeviceUpperMonitor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="智能设备上位机 - 实时通讯与参数配置"
Height="720" Width="1280"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 顶部状态栏 -->
<DockPanel Grid.Row="0" Background="#2C3E50" Height="40">
<TextBlock Text="智能设备上位机" Foreground="White" FontSize="16" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="15,0"/>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right" Margin="0,0,15,0">
<TextBlock x:Name="ConnectionStatus" Text="已断开" Foreground="#E74C3C"
VerticalAlignment="Center" Margin="10,0"/>
<Button Content="连接设备" Padding="12,6" Margin="8,0"
Command="{Binding ConnectCommand}"/>
</StackPanel>
</DockPanel>
<!-- Tab 控制区域 -->
<TabControl Grid.Row="1" Margin="8" Style="{StaticResource ModernTabControl}">
<!-- Tab 1: 实时监控 -->
<TabItem Header="实时监控">
<ContentControl Content="{Binding DashboardView}"/>
</TabItem>
<!-- Tab 2: 参数配置(核心) -->
<TabItem Header="参数配置">
<ContentControl Content="{Binding ParameterConfigView}"/>
</TabItem>
<!-- Tab 3: 报警中心 -->
<TabItem Header="报警中心">
<ContentControl Content="{Binding AlarmCenterView}"/>
</TabItem>
<!-- Tab 4: 历史数据 -->
<TabItem Header="历史数据">
<ContentControl Content="{Binding HistoryView}"/>
</TabItem>
</TabControl>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
MainViewModel.cs(简化版)
public partial class MainViewModel : ObservableObject
{
public DashboardViewModel DashboardView { get; }
public ParameterConfigViewModel ParameterConfigView { get; }
public MainViewModel(DashboardViewModel dashboardVM, ParameterConfigViewModel paramVM)
{
DashboardView = dashboardVM;
ParameterConfigView = paramVM;
}
[RelayCommand]
private async Task ConnectAsync() { /* ... */ }
}
优化说明:
- 采用依赖注入 + MVVM 标准结构
MainWindow作为 Shell,使用 TabControl 组织功能模块- 各功能模块独立为 View + ViewModel,便于维护和扩展
- 支持后续轻松扩展到 .NET MAUI
需要我继续补充以下任意内容吗?
- DashboardView.xaml + 实时曲线 完整代码
- appsettings.json 配置示例
- AlarmCenterView 报警中心界面
1. DashboardView.xaml
<UserControl x:Class="SmartDeviceUpperMonitor.Views.DashboardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:oxy="http://oxyplot.org/wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题栏 + 控制按钮 -->
<DockPanel Grid.Row="0" Margin="0,0,0,12">
<TextBlock Text="实时数据监控" FontSize="18" FontWeight="SemiBold"
VerticalAlignment="Center"/>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Button Content="开始采集" Margin="5" Padding="12,6"
Command="{Binding StartAcquisitionCommand}"/>
<Button Content="停止采集" Margin="5" Padding="12,6"
Command="{Binding StopAcquisitionCommand}"/>
<Button Content="清除曲线" Margin="5" Padding="12,6"
Command="{Binding ClearChartCommand}"/>
</StackPanel>
</DockPanel>
<!-- OxyPlot 实时曲线 -->
<oxy:PlotView Grid.Row="1"
Model="{Binding TrendModel}"
Background="Transparent"
Margin="0,8"
Controller="{Binding PlotController}"
Width="Auto" Height="Auto"/>
</Grid>
</UserControl>
2. DashboardViewModel.cs(完整版)
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using System;
using System.Threading;
using System.Threading.Tasks;
public partial class DashboardViewModel : ObservableObject
{
private readonly IDeviceCommService _commService;
private readonly IAlarmService _alarmService;
private CancellationTokenSource? _cts;
// OxyPlot 模型
public PlotModel TrendModel { get; } = new PlotModel
{
Title = "实时趋势监控",
Background = OxyColors.Transparent
};
private readonly LineSeries _temperatureSeries = new LineSeries
{
Title = "温度 (°C)",
Color = OxyColors.OrangeRed,
StrokeThickness = 2.5,
MarkerType = MarkerType.None
};
private readonly LineSeries _humiditySeries = new LineSeries
{
Title = "湿度 (%RH)",
Color = OxyColors.DodgerBlue,
StrokeThickness = 2.5,
MarkerType = MarkerType.None
};
public PlotController PlotController { get; } = new PlotController();
public DashboardViewModel(IDeviceCommService commService, IAlarmService alarmService)
{
_commService = commService;
_alarmService = alarmService;
InitializeChart();
}
private void InitializeChart()
{
// X轴(时间)
var dateAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
StringFormat = "HH:mm:ss",
Title = "时间",
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
// Y轴(温度)
var tempAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = "温度 (°C)",
MajorGridlineStyle = LineStyle.Solid
};
TrendModel.Axes.Add(dateAxis);
TrendModel.Axes.Add(tempAxis);
TrendModel.Series.Add(_temperatureSeries);
TrendModel.Series.Add(_humiditySeries);
}
[RelayCommand]
private async Task StartAcquisitionAsync()
{
if (_cts != null) return;
_cts = new CancellationTokenSource();
try
{
while (!_cts.Token.IsCancellationRequested)
{
var data = await _commService.ReadParametersAsync();
double temp = Convert.ToDouble(data.GetValueOrDefault("Temperature", 0));
double humidity = Convert.ToDouble(data.GetValueOrDefault("Humidity", 0));
var now = DateTime.Now;
// 更新曲线
_temperatureSeries.Points.Add(new DataPoint(DateTimeAxis.ToDouble(now), temp));
_humiditySeries.Points.Add(new DataPoint(DateTimeAxis.ToDouble(now), humidity));
// 滚动窗口(保持曲线性能)
if (_temperatureSeries.Points.Count > 1200)
{
_temperatureSeries.Points.RemoveAt(0);
_humiditySeries.Points.RemoveAt(0);
}
// 轻量刷新
TrendModel.InvalidatePlot(false);
// 报警判断
_alarmService.Evaluate(new DeviceData
{
DeviceId = "MainDevice",
Values = data
});
await Task.Delay(80, _cts.Token); // ≈12.5Hz
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
// 日志记录
}
}
[RelayCommand]
private void StopAcquisition()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
[RelayCommand]
private void ClearChart()
{
_temperatureSeries.Points.Clear();
_humiditySeries.Points.Clear();
TrendModel.InvalidatePlot(true);
}
}
使用说明:
- 在
MainViewModel中注入DashboardViewModel并绑定到 Tab 内容。 - 确保项目已安装
OxyPlot.WpfNuGet 包。 - 可根据需要增加更多曲线(如压力、流量等)。
需要我继续补充以下内容吗?
AlarmCenterView.xaml+ ViewModel(报警列表)appsettings.json配置示例MainViewModel.cs完整代码(协调各子 ViewModel)
更多推荐

所有评论(0)