软件项目··36 分钟阅读

上位机开发实战:生产线监控系统

从需求分析到系统架构,完整记录一个WPF上位机监控系统的开发过程,展示工控项目的实际落地

WPF上位机监控系统工控项目C#

项目背景

某制造企业需要对生产线进行数字化改造,要求开发一套上位机监控系统,实时采集设备状态、监控生产进度、记录异常报警,并提供数据追溯功能。本项目采用C# + WPF + SQL Server技术栈,开发周期3个月。

需求分析

功能需求

1. **设备状态监控**:实时显示各设备的运行/停止/故障状态

2. **数据采集**:采集温度、压力、产量等工艺参数

3. **报警管理**:设备故障、参数超限时及时报警,支持声光提醒

4. **生产记录**:记录每件产品的生产信息(配方、时间、操作员等)

5. **数据查询**:支持按时间、设备、产品编号查询历史数据

6. **报表生成**:生成日报、月报等生产统计报表

技术需求

  • 通信方式:西门子S7-1200 PLC(以太网)
  • 数据库:SQL Server 2019
  • 界面要求:简洁直观,支持1920x1080分辨率
  • 响应时间:界面刷新间隔≤500ms

系统架构设计

整体架构

```

┌─────────────────┐

│ WPF界面层 │ ← 实时显示、用户交互

└────────┬────────┘

┌────────▼────────┐

│ 业务逻辑层 │ ← 数据处理、报警逻辑

└────────┬────────┘

┌────────▼────────┐

│ 通信层 │ ← PLC通信、数据采集

└────────┬────────┘

┌────────▼────────┐

│ 数据存储层 │ ← SQL Server数据库

└─────────────────┘

```

模块划分

```csharp

// 主程序结构

public partial class App : Application

{

protected override void OnStartup(StartupEventArgs e)

{

base.OnStartup(e);

// 启动IOC容器

var container = new ContainerBuilder()

.RegisterType<PlcService>().As<IPlcService>().SingleInstance()

.RegisterType<DataService>().As<IDataService>().SingleInstance()

.RegisterType<AlarmService>().As<IAlarmService>().SingleInstance()

.Build();

// 启动主窗口

var mainWindow = container.Resolve<MainWindow>();

mainWindow.Show();

}

}

```

核心功能实现

1. PLC通信模块

```csharp

// PLC通信服务

public class PlcService : IPlcService, IDisposable

{

private readonly SiemensPlcClient _plc = new SiemensPlcClient();

private Timer _readTimer; // 定时器读取PLC

public async Task InitializeAsync()

{

// 连接PLC

bool connected = await _plc.ConnectAsync("192.168.1.100", 102);

if (!connected)

{

throw new Exception("PLC连接失败");

}

// 启动定时读取(500ms间隔)

_readTimer = new Timer(async _ => await ReadAllDataAsync(), null, 0, 500);

}

private async Task ReadAllDataAsync()

{

try

{

// 并行读取多个数据块,提高效率

await Task.WhenAll(

ReadDeviceStatusAsync(),

ReadProcessParametersAsync(),

ReadProductionCountAsync()

);

}

catch (Exception ex)

{

// 记录错误,但不中断读取

Log.Error($"PLC数据读取失败: {ex.Message}");

}

}

private async Task ReadDeviceStatusAsync()

{

// 读取设备运行状态

var status = await _plc.ReadDbBoolAsync(1, 0, 0); // DB1.DBX0.0

await NotifyDataChanged("Device1Status", status);

}

private async Task ReadProcessParametersAsync()

{

// 读取工艺参数

var temperature = await _plc.ReadDbRealAsync(1, 10);

var pressure = await _plc.ReadDbRealAsync(1, 14);

var speed = await _plc.ReadDbIntAsync(1, 18);

await NotifyDataChanged("Temperature", temperature);

await NotifyDataChanged("Pressure", pressure);

await NotifyDataChanged("Speed", speed);

}

}

```

2. 报警管理模块

```csharp

public class AlarmService : IAlarmService

{

private readonly IDataService _dataService;

public event EventHandler<AlarmEventArgs> AlarmRaised;

// 报警规则配置

private readonly Dictionary<string, AlarmRule> _alarmRules = new()

{

{ "Temperature", new AlarmRule { MaxValue = 80, MinValue = 20, Level = AlarmLevel.Warning } },

{ "Pressure", new AlarmRule { MaxValue = 10, MinValue = 0.5, Level = AlarmLevel.Warning } },

{ "Device1Status", new AlarmRule { EqualsValue = false, Level = AlarmLevel.Error } }

};

public async Task CheckAlarmsAsync(string paramName, object value)

{

if (_alarmRules.TryGetValue(paramName, out var rule))

{

bool alarmTriggered = false;

string message = "";

// 检查上限

if (rule.MaxValue.HasValue && Convert.ToDouble(value) > rule.MaxValue)

{

alarmTriggered = true;

message = $"{paramName}超过上限: {value} > {rule.MaxValue}";

}

// 检查下限

else if (rule.MinValue.HasValue && Convert.ToDouble(value) < rule.MinValue)

{

alarmTriggered = true;

message = $"{paramName}低于下限: {value} < {rule.MinValue}";

}

// 检查等于值

else if (rule.EqualsValue.HasValue && Convert.ToBoolean(value) != rule.EqualsValue)

{

alarmTriggered = true;

message = $"{paramName}状态异常: {value}";

}

if (alarmTriggered)

{

var alarm = new Alarm

{

Time = DateTime.Now,

Parameter = paramName,

Value = value.ToString(),

Message = message,

Level = rule.Level

};

// 触发报警事件

AlarmRaised?.Invoke(this, new AlarmEventArgs(alarm));

// 写入数据库

await _dataService.SaveAlarmAsync(alarm);

// 发出声光报警

PlayAlarmSound(rule.Level);

}

}

}

private void PlayAlarmSound(AlarmLevel level)

{

// 根据报警级别播放不同音效

var soundFile = level == AlarmLevel.Error ? "error.wav" : "warning.wav";

using var player = new SoundPlayer(soundFile);

player.Play();

}

}

```

3. 实时数据绑定

```csharp

// 实时数据显示(使用MVVM模式)

public class RealTimeDisplayViewModel : INotifyPropertyChanged

{

private double _temperature;

public double Temperature

{

get => _temperature;

set

{

_temperature = value;

OnPropertyChanged();

// 自动检查报警

_alarmService.CheckAlarmsAsync(nameof(Temperature), value);

}

}

private bool _deviceStatus;

public bool DeviceStatus

{

get => _deviceStatus;

set

{

_deviceStatus = value;

OnPropertyChanged();

// 设备状态改变时触发报警检查

_alarmService.CheckAlarmsAsync(nameof(DeviceStatus), value);

}

}

// 实现INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged([CallerMemberName] string name = null)

{

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

}

}

```

4. WPF界面实现

```xml

<!-- 实时监控界面 XAML -->

<Window x:Class="UpperComputer.Views.MonitorWindow">

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="Auto"/>

<RowDefinition Height="*"/>

</Grid.RowDefinitions>

<!-- 顶部状态栏 -->

<Border Grid.Row="0" Background="#2E3440" Padding="10">

<StackPanel Orientation="Horizontal">

<TextBlock Text="设备状态:" Foreground="White" Margin="0,0,10 <Ellipse Width,0"/>

="15" Height="15" Fill="{Binding DeviceStatus, Converter={StaticResource BoolToColorConverter}}"/>

<TextBlock Text="{Binding DeviceStatusText}" Foreground="White" Margin="10,0,0,0"/>

</StackPanel>

</Border>

<!-- 主内容区 -->

<Grid Grid.Row="1">

<Grid.ColumnDefinitions>

<ColumnDefinition Width="*"/>

<ColumnDefinition Width="*"/>

</Grid.ColumnDefinitions>

<!-- 左侧:工艺参数 -->

<StackPanel Grid.Column="0" Margin="20">

<TextBlock Text="工艺参数" FontSize="20" FontWeight="Bold" Margin="0,0,0,20"/>

<TextBlock Text="温度 (℃)" FontSize="14"/>

<TextBlock Text="{Binding Temperature, StringFormat={}{0:F2}}" FontSize="32" Foreground="Red"/>

<TextBlock Text="压力 (MPa)" FontSize="14" Margin="0,20,0,0"/>

<TextBlock Text="{Binding Pressure, StringFormat={}{0:F2}}" FontSize="32" Foreground="Blue"/>

</StackPanel>

<!-- 右侧:报警信息 -->

<StackPanel Grid.Column="1" Margin="20">

<TextBlock Text="当前报警" FontSize="20" FontWeight="Bold" Margin="0,0,0,20"/>

<ListView ItemsSource="{Binding CurrentAlarms}">

<ListView.ItemTemplate>

<DataTemplate>

<Border Background="Red" CornerRadius="5" Padding="10" Margin="0,5">

<TextBlock Text="{Binding Message}" Foreground="White"/>

</Border>

</DataTemplate>

</ListView.ItemTemplate>

</ListView>

</StackPanel>

</Grid>

</Grid>

</Window>

```

5. 数据查询与报表

```csharp

public class ReportService

{

private readonly IDataService _dataService;

// 生成日报表

public async Task<ProductionReport> GenerateDailyReportAsync(DateTime date)

{

var report = new ProductionReport

{

Date = date.Date,

StartTime = date,

EndTime = date.AddDays(1)

};

// 统计产量

report.TotalOutput = await _dataService.GetProductionCountAsync(date);

report.QualifiedCount = await _dataService.GetQualifiedCountAsync(date);

report.DefectCount = report.TotalOutput - report.QualifiedCount;

report.QualificationRate = (double)report.QualifiedCount / report.TotalOutput * 100;

// 统计设备运行时间

report.OperatingHours = await _dataService.GetOperatingHoursAsync(date);

// 计算平均工艺参数

report.AvgTemperature = await _dataService.GetAverageValueAsync("Temperature", date);

report.AvgPressure = await _dataService.GetAverageValueAsync("Pressure", date);

return report;

}

// 导出Excel报表

public async Task ExportToExcelAsync(ProductionReport report, string filePath)

{

using var workbook = new ExcelWorkbook();

var worksheet = workbook.AddWorksheet("日报");

worksheet.Cell(1, 1).Value = "生产日报";

worksheet.Cell(3, 1).Value = "日期";

worksheet.Cell(3, 2).Value = report.Date.ToString("yyyy-MM-dd");

worksheet.Cell(4, 1).Value = "总产量";

worksheet.Cell(4, 2).Value = report.TotalOutput;

worksheet.Cell(5, 1).Value = "合格数";

worksheet.Cell(5, 2).Value = report.QualifiedCount;

await workbook.SaveAsAsync(filePath);

}

}

```

项目总结

技术收获

1. **WPF应用开发**:掌握了MVVM模式、数据绑定、样式定制等核心技能。

2. **工业通信**:深入理解了PLC通信协议,学会了处理通信异常和断线重连。

3. **多线程编程**:使用Task异步处理数据采集,保证界面响应流畅。

4. **数据库设计**:设计了合理的数据库表结构,支持快速查询和报表生成。

遇到的挑战与解决方案

1. **界面卡顿**:初期直接在线程中更新UI导致卡顿,改为使用Dispatcher异步更新。

2. **PLC断线**:实现断线检测和自动重连机制,提高系统稳定性。

3. **数据量大**:采用数据库索引和分页查询,提升查询性能。

经验总结

  • 工控项目优先考虑稳定性,通信异常处理是重中之重。
  • 使用MVVM模式可以显著提高代码的可维护性。
  • 自动化测试在工控项目中同样重要,可以模拟PLC数据进行测试。
  • 详细的日志记录是排查问题的关键。

这个项目让我深刻理解了工控软件开发的特殊性,为后续的职业发展打下了坚实基础。