项目背景
某制造企业需要对生产线进行数字化改造,要求开发一套上位机监控系统,实时采集设备状态、监控生产进度、记录异常报警,并提供数据追溯功能。本项目采用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数据进行测试。
- 详细的日志记录是排查问题的关键。
这个项目让我深刻理解了工控软件开发的特殊性,为后续的职业发展打下了坚实基础。