报警记录与历史趋势
大约 8 分钟约 2532 字
报警记录与历史趋势
简介
报警管理是工控上位机的必备功能,包括实时报警监控、报警确认、历史记录查询和报警统计分析。历史趋势用于展示关键参数随时间的变化曲线,帮助操作员判断设备运行状态和工艺稳定性。WPF 配合 OxyPlot 图表库可以构建专业的报警和趋势界面。
特点
报警数据模型
报警记录
/// <summary>
/// 报警记录模型
/// </summary>
public class AlarmRecord : ObservableObject
{
public long Id { get; set; }
[ObservableProperty]
private AlarmLevel _level;
[ObservableProperty]
private string _deviceName = "";
[ObservableProperty]
private string _alarmCode = "";
[ObservableProperty]
private string _alarmMessage = "";
[ObservableProperty]
private DateTime _triggerTime;
[ObservableProperty]
private DateTime? _ackTime;
[ObservableProperty]
private DateTime? _recoverTime;
[ObservableProperty]
private string _ackUser = "";
[ObservableProperty]
private double _triggerValue;
[ObservableProperty]
private double _thresholdValue;
[ObservableProperty]
private string _unit = "";
// 是否已确认
public bool IsAcknowledged => AckTime.HasValue;
// 是否已恢复
public bool IsRecovered => RecoverTime.HasValue;
// 持续时长
public string Duration
{
get
{
var end = RecoverTime ?? DateTime.Now;
var span = end - TriggerTime;
return span.TotalHours >= 1
? $"{span.TotalHours:F1}h"
: span.TotalMinutes >= 1
? $"{span.TotalMinutes:F1}min"
: $"{span.TotalSeconds:F0}s";
}
}
// 级别颜色
public Brush LevelBrush => Level switch
{
AlarmLevel.Info => Brushes.Blue,
AlarmLevel.Warning => Brushes.Orange,
AlarmLevel.Error => Brushes.Red,
AlarmLevel.Critical => Brushes.DarkRed,
_ => Brushes.Gray
};
// 状态文本
public string StatusText => (IsAcknowledged, IsRecovered) switch
{
(false, false) => "未确认",
(true, false) => "已确认",
(true, true) => "已恢复",
_ => "未知"
};
}
public enum AlarmLevel
{
Info = 0,
Warning = 1,
Error = 2,
Critical = 3
}报警管理服务
报警触发与确认
/// <summary>
/// 报警管理服务
/// </summary>
public class AlarmService
{
private readonly IAlarmRepository _repository;
private readonly ILogger<AlarmService> _logger;
// 活跃报警(未恢复)
public ObservableCollection<AlarmRecord> ActiveAlarms { get; } = new();
public event Action<AlarmRecord>? AlarmTriggered;
public event Action<AlarmRecord>? AlarmRecovered;
public AlarmService(IAlarmRepository repository, ILogger<AlarmService> logger)
{
_repository = repository;
_logger = logger;
}
// 触发报警
public async Task TriggerAsync(AlarmLevel level, string deviceName,
string alarmCode, string message, double triggerValue, double thresholdValue, string unit = "")
{
// 检查是否已有相同报警(去重)
var existing = ActiveAlarms.FirstOrDefault(a =>
a.DeviceName == deviceName && a.AlarmCode == alarmCode && !a.IsRecovered);
if (existing != null) return; // 已存在相同报警
var alarm = new AlarmRecord
{
Level = level,
DeviceName = deviceName,
AlarmCode = alarmCode,
AlarmMessage = message,
TriggerTime = DateTime.Now,
TriggerValue = triggerValue,
ThresholdValue = thresholdValue,
Unit = unit
};
await _repository.InsertAsync(alarm);
ActiveAlarms.Add(alarm);
AlarmTriggered?.Invoke(alarm);
_logger.LogWarning("报警触发:[{Level}] {Device} - {Message} (值={Value}{Unit}, 阈值={Threshold}{Unit})",
level, deviceName, message, triggerValue, unit, thresholdValue, unit);
}
// 确认报警
public async Task AcknowledgeAsync(long alarmId, string user)
{
var alarm = ActiveAlarms.FirstOrDefault(a => a.Id == alarmId);
if (alarm != null && !alarm.IsAcknowledged)
{
alarm.AckTime = DateTime.Now;
alarm.AckUser = user;
await _repository.UpdateAsync(alarm);
}
}
// 确认所有报警
public async Task AcknowledgeAllAsync(string user)
{
foreach (var alarm in ActiveAlarms.Where(a => !a.IsAcknowledged))
{
await AcknowledgeAsync(alarm.Id, user);
}
}
// 恢复报警
public async Task RecoverAsync(string deviceName, string alarmCode)
{
var alarm = ActiveAlarms.FirstOrDefault(a =>
a.DeviceName == deviceName && a.AlarmCode == alarmCode && !a.IsRecovered);
if (alarm != null)
{
alarm.RecoverTime = DateTime.Now;
await _repository.UpdateAsync(alarm);
ActiveAlarms.Remove(alarm);
AlarmRecovered?.Invoke(alarm);
}
}
// 查询历史报警
public async Task<List<AlarmRecord>> QueryHistoryAsync(
DateTime startTime, DateTime endTime,
AlarmLevel? minLevel = null, string? deviceName = null)
{
return await _repository.QueryAsync(startTime, endTime, minLevel, deviceName);
}
// 报警统计
public async Task<Dictionary<AlarmLevel, int>> GetStatisticsAsync(DateTime date)
{
var alarms = await _repository.QueryAsync(date, date.AddDays(1));
return alarms.GroupBy(a => a.Level)
.ToDictionary(g => g.Key, g => g.Count());
}
}阈值检测
自动报警检测
/// <summary>
/// 阈值检测器
/// </summary>
public class ThresholdMonitor
{
private readonly AlarmService _alarmService;
private readonly List<ThresholdRule> _rules;
public ThresholdMonitor(AlarmService alarmService)
{
_alarmService = alarmService;
_rules = new List<ThresholdRule>();
}
// 添加规则
public void AddRule(string deviceName, string parameterName, string alarmCode,
double warningThreshold, double errorThreshold, string unit = "")
{
_rules.Add(new ThresholdRule
{
DeviceName = deviceName,
ParameterName = parameterName,
AlarmCode = alarmCode,
WarningThreshold = warningThreshold,
ErrorThreshold = errorThreshold,
Unit = unit
});
}
// 检测值
public async Task CheckAsync(string deviceName, string parameterName, double value)
{
var rules = _rules.Where(r => r.DeviceName == deviceName && r.ParameterName == parameterName);
foreach (var rule in rules)
{
if (value >= rule.ErrorThreshold)
{
await _alarmService.TriggerAsync(AlarmLevel.Error, deviceName, rule.AlarmCode,
$"{parameterName} 超限", value, rule.ErrorThreshold, rule.Unit);
}
else if (value >= rule.WarningThreshold)
{
await _alarmService.TriggerAsync(AlarmLevel.Warning, deviceName, rule.AlarmCode,
$"{parameterName} 预警", value, rule.WarningThreshold, rule.Unit);
}
else
{
await _alarmService.RecoverAsync(deviceName, rule.AlarmCode);
}
}
}
}
public class ThresholdRule
{
public string DeviceName { get; set; } = "";
public string ParameterName { get; set; } = "";
public string AlarmCode { get; set; } = "";
public double WarningThreshold { get; set; }
public double ErrorThreshold { get; set; }
public string Unit { get; set; } = "";
}历史趋势
OxyPlot 趋势图
/// <summary>
/// 历史趋势 ViewModel
/// </summary>
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
public class TrendViewModel : ObservableObject
{
[ObservableProperty]
private PlotModel? _trendModel;
[ObservableProperty]
private DateTime _startTime = DateTime.Now.AddHours(-1);
[ObservableProperty]
private DateTime _endTime = DateTime.Now;
public async Task LoadTrendAsync(string deviceName, string parameterName)
{
var data = await _historyService.GetHistoryDataAsync(
deviceName, parameterName, StartTime, EndTime);
var model = new PlotModel
{
Title = $"{deviceName} - {parameterName}",
Background = OxyColors.White
};
// 时间轴
model.Axes.Add(new DateTimeAxis
{
Position = AxisPosition.Bottom,
Title = "时间",
StringFormat = "HH:mm:ss",
IntervalType = DateTimeIntervalType.Minutes
});
// 数值轴
model.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = parameterName
});
// 数据线
var series = new LineSeries
{
Title = parameterName,
Color = OxyColors.Blue,
StrokeThickness = 1
};
foreach (var point in data)
{
series.Points.Add(new DataPoint(
DateTimeAxis.ToDouble(point.Timestamp),
point.Value));
}
model.Series.Add(series);
TrendModel = model;
}
}<!-- 趋势图显示 -->
<oxy:PlotView Model="{Binding TrendModel}"
Width="800" Height="400"/>多参数叠加趋势
/// <summary>
/// 多参数趋势对比 ViewModel
/// </summary>
public class MultiTrendViewModel : ObservableObject
{
[ObservableProperty]
private PlotModel? _multiTrendModel;
[ObservableProperty]
private ObservableCollection<ParameterSelection> _availableParameters = new();
private static readonly OxyColor[] SeriesColors =
{
OxyColors.Blue, OxyColors.Red, OxyColors.Green,
OxyColors.Orange, OxyColors.Purple
};
public async Task LoadMultiTrendAsync(string deviceName, string[] parameterNames)
{
var model = new PlotModel
{
Title = $"{deviceName} - 多参数趋势",
Background = OxyColors.White,
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.TopRight
};
model.Axes.Add(new DateTimeAxis
{
Position = AxisPosition.Bottom,
Title = "时间",
StringFormat = "HH:mm:ss",
IntervalType = DateTimeIntervalType.Auto
});
model.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "数值",
Key = "Value"
});
for (int i = 0; i < parameterNames.Length; i++)
{
var param = parameterNames[i];
var data = await _historyService.GetHistoryDataAsync(
deviceName, param, DateTime.Now.AddHours(-1), DateTime.Now);
var series = new LineSeries
{
Title = param,
Color = SeriesColors[i % SeriesColors.Length],
StrokeThickness = 1.5,
YAxisKey = "Value"
};
foreach (var point in data)
{
series.Points.Add(new DataPoint(
DateTimeAxis.ToDouble(point.Timestamp),
point.Value));
}
model.Series.Add(series);
}
MultiTrendModel = model;
}
}
public class ParameterSelection
{
public string Name { get; set; } = "";
public bool IsSelected { get; set; }
public string Unit { get; set; } = "";
}报警统计图表
/// <summary>
/// 报警统计 ViewModel — 展示报警分布和趋势
/// </summary>
public class AlarmStatisticsViewModel : ObservableObject
{
[ObservableProperty]
private PlotModel? _levelDistributionModel;
[ObservableProperty]
private PlotModel? _dailyTrendModel;
[ObservableProperty]
private DateTime _selectedDate = DateTime.Today;
public async Task LoadStatisticsAsync(DateTime date)
{
var alarms = await _alarmService.QueryHistoryAsync(date, date.AddDays(1));
// 报警级别饼图
var pieModel = new PlotModel { Title = "报警级别分布", Background = OxyColors.White };
var pieSeries = new PieSeries
{
InsideLabelFormat = "{1}: {0}",
OutsideLabelFormat = "",
StrokeThickness = 1
};
var groups = alarms.GroupBy(a => a.Level);
foreach (var group in groups)
{
pieSeries.Slices.Add(new PieSlice(
group.Key.ToString(),
group.Count())
{
Fill = group.Key switch
{
AlarmLevel.Info => OxyColors.Blue,
AlarmLevel.Warning => OxyColors.Orange,
AlarmLevel.Error => OxyColors.Red,
AlarmLevel.Critical => OxyColors.DarkRed,
_ => OxyColors.Gray
}
});
}
pieModel.Series.Add(pieSeries);
LevelDistributionModel = pieModel;
// 报警数量趋势(按小时)
var trendModel = new PlotModel { Title = "报警趋势(按小时)", Background = OxyColors.White };
trendModel.Axes.Add(new LinearAxis
{
Position = AxisPosition.Bottom,
Title = "小时",
Minimum = 0,
Maximum = 24
});
trendModel.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "报警数量"
});
var hourlyData = alarms
.GroupBy(a => a.TriggerTime.Hour)
.Select(g => new { Hour = g.Key, Count = g.Count() })
.OrderBy(x => x.Hour);
var lineSeries = new LineSeries
{
Title = "报警数量",
Color = OxyColors.Red,
StrokeThickness = 2
};
foreach (var item in hourlyData)
{
lineSeries.Points.Add(new DataPoint(item.Hour, item.Count));
}
trendModel.Series.Add(lineSeries);
DailyTrendModel = trendModel;
}
}报警声音通知
/// <summary>
/// 报警声音通知服务
/// </summary>
public class AlarmSoundService : IDisposable
{
private readonly DispatcherTimer _soundTimer;
private bool _isPlaying;
private AlarmLevel? _currentHighestLevel;
public AlarmSoundService()
{
_soundTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(3)
};
_soundTimer.Tick += OnSoundTimerTick;
}
public void OnAlarmTriggered(AlarmLevel level)
{
// 只有错误和紧急级别触发声音
if (level < AlarmLevel.Error) return;
_currentHighestLevel = level;
if (!_isPlaying)
{
_isPlaying = true;
_soundTimer.Start();
PlayAlertSound(level);
}
}
public void StopAlarm()
{
_isPlaying = false;
_soundTimer.Stop();
_currentHighestLevel = null;
}
private void OnSoundTimerTick(object? sender, EventArgs e)
{
if (_currentHighestLevel.HasValue)
PlayAlertSound(_currentHighestLevel.Value);
}
private void PlayAlertSound(AlarmLevel level)
{
try
{
var soundFile = level switch
{
AlarmLevel.Error => "alarm_error.wav",
AlarmLevel.Critical => "alarm_critical.wav",
_ => "alarm_warning.wav"
};
var player = new System.Media.SoundPlayer(soundFile);
player.Play();
}
catch (Exception ex)
{
// 声音播放失败不应影响主流程
System.Diagnostics.Debug.WriteLine($"报警声音播放失败: {ex.Message}");
}
}
public void Dispose()
{
_soundTimer.Stop();
}
}<!-- 报警声音开关按钮 -->
<ToggleButton Content="报警声音"
IsChecked="{Binding IsSoundEnabled}"
Margin="5"/>优点
缺点
总结
报警管理核心流程:阈值检测 → 触发报警 → 确认报警 → 恢复报警。报警去重避免同一报警重复触发。分级处理:Info 记录、Warning 提示、Error 停机、Critical 紧急。历史趋势用 OxyPlot 绑定 PlotModel 显示实时曲线。报警数据定期归档,避免数据库膨胀。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《报警记录与历史趋势》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《报警记录与历史趋势》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《报警记录与历史趋势》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《报警记录与历史趋势》最大的收益和代价分别是什么?
