设备心跳监控看板
大约 8 分钟约 2516 字
设备心跳监控看板
简介
设备心跳监控是上位机的核心功能之一,通过定时检测设备在线状态、关键参数和运行指标,构建实时监控看板。WPF 利用定时器、通知属性和 UI 虚拟化,可以构建实时刷新的设备状态监控界面,配合声光报警实现故障预警。
特点
设备状态模型
数据模型
/// <summary>
/// 设备状态模型
/// </summary>
public class DeviceStatus : ObservableObject
{
public string DeviceId { get; set; } = "";
public string DeviceName { get; set; } = "";
public string IpAddress { get; set; } = "";
public string DeviceType { get; set; } = "";
[ObservableProperty]
private DeviceState _state;
[ObservableProperty]
private DateTime _lastHeartbeat;
[ObservableProperty]
private double _temperature;
[ObservableProperty]
private double _cpuUsage;
[ObservableProperty]
private double _memoryUsage;
[ObservableProperty]
private int _errorCount;
[ObservableProperty]
private string _currentTask = "";
// 运行时长
public TimeSpan Uptime => DateTime.Now - LastHeartbeat;
// 状态颜色
public Brush StateBrush => State switch
{
DeviceState.Online => Brushes.Green,
DeviceState.Warning => Brushes.Orange,
DeviceState.Error => Brushes.Red,
DeviceState.Offline => Brushes.Gray,
_ => Brushes.Gray
};
// 状态文本
public string StateText => State switch
{
DeviceState.Online => "在线",
DeviceState.Warning => "警告",
DeviceState.Error => "故障",
DeviceState.Offline => "离线",
_ => "未知"
};
}
public enum DeviceState
{
Online,
Warning,
Error,
Offline
}心跳服务
设备心跳管理
/// <summary>
/// 设备心跳监控服务
/// </summary>
public class HeartbeatService : IDisposable
{
private readonly Dictionary<string, DeviceStatus> _devices = new();
private readonly Dictionary<string, CancellationTokenSource> _monitors = new();
private readonly ILogger<HeartbeatService> _logger;
public event Action<DeviceStatus>? DeviceStatusChanged;
public event Action<DeviceStatus, string>? AlarmTriggered;
public HeartbeatService(ILogger<HeartbeatService> logger)
{
_logger = logger;
}
// 注册设备
public void RegisterDevice(DeviceStatus device)
{
_devices[device.DeviceId] = device;
}
// 启动监控
public void StartMonitoring(int intervalMs = 5000)
{
foreach (var device in _devices.Values)
{
var cts = new CancellationTokenSource();
_monitors[device.DeviceId] = cts;
_ = MonitorDeviceAsync(device, intervalMs, cts.Token);
}
}
private async Task MonitorDeviceAsync(DeviceStatus device, int intervalMs, CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
// 心跳检测(实际项目中调用设备 API 或 Modbus/TCP)
var alive = await CheckDeviceAliveAsync(device.IpAddress);
var previousState = device.State;
device.LastHeartbeat = DateTime.Now;
if (alive)
{
// 读取设备参数
var parameters = await ReadDeviceParametersAsync(device);
device.Temperature = parameters.Temperature;
device.CpuUsage = parameters.CpuUsage;
device.MemoryUsage = parameters.MemoryUsage;
// 检查阈值
device.State = CheckThresholds(device);
}
else
{
device.State = DeviceState.Offline;
}
// 状态变化通知
if (previousState != device.State)
{
DeviceStatusChanged?.Invoke(device);
_logger.LogInformation("设备 {Name} 状态变化:{Old} → {New}",
device.DeviceName, previousState, device.State);
}
}
catch (Exception ex)
{
device.State = DeviceState.Error;
AlarmTriggered?.Invoke(device, $"通信异常:{ex.Message}");
}
await Task.Delay(intervalMs, ct);
}
}
// 检查参数阈值
private DeviceState CheckThresholds(DeviceStatus device)
{
if (device.Temperature > 80) return DeviceState.Error;
if (device.CpuUsage > 90) return DeviceState.Warning;
if (device.MemoryUsage > 90) return DeviceState.Warning;
return DeviceState.Online;
}
// Ping 检测设备
private async Task<bool> CheckDeviceAliveAsync(string ipAddress)
{
using var ping = new System.Net.NetworkInformation.Ping();
try
{
var reply = await ping.SendPingAsync(ipAddress, 3000);
return reply.Status == System.Net.NetworkInformation.IPStatus.Success;
}
catch
{
return false;
}
}
private async Task<(double Temperature, double CpuUsage, double MemoryUsage)>
ReadDeviceParametersAsync(DeviceStatus device)
{
// 实际项目中通过 Modbus/OPC UA/HTTP 获取
await Task.Delay(50);
return (Random.Shared.Next(30, 70), Random.Shared.Next(20, 80), Random.Shared.Next(30, 70));
}
public void StopMonitoring()
{
foreach (var cts in _monitors.Values)
cts.Cancel();
}
public void Dispose()
{
StopMonitoring();
foreach (var cts in _monitors.Values)
cts.Dispose();
}
}监控看板 UI
状态看板
<!-- 设备监控看板 -->
<ItemsControl ItemsSource="{Binding Devices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="8" Padding="12" CornerRadius="8"
Background="White" BorderThickness="2"
BorderBrush="{Binding StateBrush}">
<StackPanel>
<!-- 标题行 -->
<Grid Margin="0,0,0,8">
<TextBlock Text="{Binding DeviceName}" FontWeight="Bold" FontSize="14"/>
<Border HorizontalAlignment="Right"
Background="{Binding StateBrush}"
CornerRadius="4" Padding="6,2">
<TextBlock Text="{Binding StateText}" Foreground="White" FontSize="11"/>
</Border>
</Grid>
<!-- 参数 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="温度" Foreground="Gray" FontSize="12"/>
<ProgressBar Grid.Row="0" Grid.Column="1" Value="{Binding Temperature}" Maximum="100" Height="8" Margin="5,0"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="CPU" Foreground="Gray" FontSize="12"/>
<ProgressBar Grid.Row="1" Grid.Column="1" Value="{Binding CpuUsage}" Maximum="100" Height="8" Margin="5,0"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="内存" Foreground="Gray" FontSize="12"/>
<ProgressBar Grid.Row="2" Grid.Column="1" Value="{Binding MemoryUsage}" Maximum="100" Height="8" Margin="5,0"/>
</Grid>
<!-- 最后心跳 -->
<TextBlock Text="{Binding LastHeartbeat, StringFormat='最后心跳:{0:HH:mm:ss}'}"
Foreground="Gray" FontSize="10" Margin="0,8,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>报警系统
多级报警机制
/// <summary>
/// 报警规则引擎
/// </summary>
public class AlarmRuleEngine
{
private readonly List<AlarmRule> _rules = new();
private readonly ILogger<AlarmRuleEngine> _logger;
public event Action<AlarmEvent>? AlarmTriggered;
public event Action<AlarmEvent>? AlarmCleared;
public AlarmRuleEngine(ILogger<AlarmRuleEngine> logger)
{
_logger = logger;
}
// 注册报警规则
public void AddRule(AlarmRule rule)
{
_rules.Add(rule);
}
// 检查设备参数是否触发报警
public void Evaluate(DeviceStatus device)
{
foreach (var rule in _rules)
{
if (rule.DeviceType != null && rule.DeviceType != device.DeviceType)
continue;
var value = GetParameterValue(device, rule.ParameterName);
if (value == null) continue;
bool isAlarm = rule.Operator switch
{
AlarmOperator.GreaterThan => value.Value > rule.Threshold,
AlarmOperator.LessThan => value.Value < rule.Threshold,
AlarmOperator.Equal => Math.Abs(value.Value - rule.Threshold) < 0.001,
AlarmOperator.NotEqual => Math.Abs(value.Value - rule.Threshold) >= 0.001,
AlarmOperator.GreaterOrEqual => value.Value >= rule.Threshold,
AlarmOperator.LessOrEqual => value.Value <= rule.Threshold,
_ => false
};
if (isAlarm)
{
var alarmEvent = new AlarmEvent
{
DeviceId = device.DeviceId,
DeviceName = device.DeviceName,
RuleName = rule.Name,
Parameter = rule.ParameterName,
CurrentValue = value.Value,
Threshold = rule.Threshold,
Level = rule.Level,
Timestamp = DateTime.Now
};
AlarmTriggered?.Invoke(alarmEvent);
_logger.LogWarning("报警:{Device} {Param}={Value},阈值={Threshold}",
device.DeviceName, rule.ParameterName, value.Value, rule.Threshold);
}
}
}
private double? GetParameterValue(DeviceStatus device, string paramName)
{
return paramName.ToLower() switch
{
"temperature" => device.Temperature,
"cpu" => device.CpuUsage,
"memory" => device.MemoryUsage,
_ => null
};
}
}
public class AlarmRule
{
public string Name { get; set; } = "";
public string? DeviceType { get; set; }
public string ParameterName { get; set; } = "";
public AlarmOperator Operator { get; set; }
public double Threshold { get; set; }
public AlarmLevel Level { get; set; } = AlarmLevel.Warning;
}
public enum AlarmOperator
{
GreaterThan, LessThan, Equal, NotEqual, GreaterOrEqual, LessOrEqual
}
public enum AlarmLevel
{
Info, // 提示
Warning, // 警告
Error, // 故障
Critical // 严重
}
public class AlarmEvent
{
public string DeviceId { get; set; } = "";
public string DeviceName { get; set; } = "";
public string RuleName { get; set; } = "";
public string Parameter { get; set; } = "";
public double CurrentValue { get; set; }
public double Threshold { get; set; }
public AlarmLevel Level { get; set; }
public DateTime Timestamp { get; set; }
}报警通知与声光提示
/// <summary>
/// 报警通知服务 — 声音 + 弹窗 + 邮件
/// </summary>
public class AlarmNotificationService
{
private readonly ILogger<AlarmNotificationService> _logger;
private SoundPlayer? _alarmSound;
private bool _isAlarmPlaying;
public AlarmNotificationService(ILogger<AlarmNotificationService> logger)
{
_logger = logger;
}
// 播放报警声音
public void PlayAlarmSound(AlarmLevel level)
{
if (_isAlarmPlaying) return;
try
{
_isAlarmPlaying = true;
var soundPath = level switch
{
AlarmLevel.Critical => "Sounds/critical.wav",
AlarmLevel.Error => "Sounds/error.wav",
AlarmLevel.Warning => "Sounds/warning.wav",
_ => "Sounds/info.wav"
};
if (File.Exists(soundPath))
{
_alarmSound = new SoundPlayer(soundPath);
_alarmSound.PlayLooping();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "播放报警声音失败");
}
}
// 停止报警声音
public void StopAlarmSound()
{
_alarmSound?.Stop();
_isAlarmPlaying = false;
}
// 发送报警邮件
public async Task SendAlarmEmailAsync(AlarmEvent alarmEvent, string[] recipients)
{
try
{
var subject = $"[{alarmEvent.Level}] 设备报警:{alarmEvent.DeviceName}";
var body = $"设备:{alarmEvent.DeviceName}\n" +
$"参数:{alarmEvent.Parameter}\n" +
$"当前值:{alarmEvent.CurrentValue}\n" +
$"阈值:{alarmEvent.Threshold}\n" +
$"时间:{alarmEvent.Timestamp:yyyy-MM-dd HH:mm:ss}";
// 使用 SmtpClient 或第三方邮件服务发送
_logger.LogInformation("报警邮件已发送:{Subject}", subject);
}
catch (Exception ex)
{
_logger.LogError(ex, "发送报警邮件失败");
}
}
}报警历史记录
/// <summary>
/// 报警历史记录服务
/// </summary>
public class AlarmHistoryService
{
private readonly string _historyFilePath;
private readonly object _lock = new();
private readonly int _maxRecords = 10000;
public AlarmHistoryService(string dataDir)
{
Directory.CreateDirectory(dataDir);
_historyFilePath = Path.Combine(dataDir, "alarm_history.jsonl");
}
// 记录报警事件
public void RecordAlarm(AlarmEvent alarmEvent)
{
lock (_lock)
{
var line = JsonSerializer.Serialize(alarmEvent) + "\n";
File.AppendAllText(_historyFilePath, line);
}
}
// 查询报警历史
public List<AlarmEvent> QueryHistory(
string? deviceId = null,
AlarmLevel? level = null,
DateTime? startTime = null,
DateTime? endTime = null)
{
var records = new List<AlarmEvent>();
if (!File.Exists(_historyFilePath)) return records;
foreach (var line in File.ReadLines(_historyFilePath))
{
try
{
var evt = JsonSerializer.Deserialize<AlarmEvent>(line);
if (evt == null) continue;
if (deviceId != null && evt.DeviceId != deviceId) continue;
if (level != null && evt.Level != level) continue;
if (startTime != null && evt.Timestamp < startTime) continue;
if (endTime != null && evt.Timestamp > endTime) continue;
records.Add(evt);
}
catch { }
}
return records.OrderByDescending(r => r.Timestamp).Take(_maxRecords).ToList();
}
// 获取报警统计
public Dictionary<string, int> GetAlarmStatistics(DateTime start, DateTime end)
{
var records = QueryHistory(null, null, start, end);
return records
.GroupBy(r => r.Level.ToString())
.ToDictionary(g => g.Key, g => g.Count());
}
}优点
缺点
总结
设备心跳监控核心:定时 Ping/通信检测 → 更新状态模型 → UI 绑定自动刷新。状态模型用 ObservableObject 支持实时更新。阈值报警在 CheckThresholds 中统一判断。UI 用 UniformGrid 均匀排列设备卡片,状态颜色(绿/黄/红/灰)直观区分。大量设备建议分页或分组展示。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《设备心跳监控看板》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《设备心跳监控看板》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《设备心跳监控看板》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《设备心跳监控看板》最大的收益和代价分别是什么?
