实时数据监控:1秒一次的生死线
这是「从零搭建工业控制系统」系列第5篇。上篇聊完Modbus通信,这篇说怎么把设备数据搬到屏幕上。
从"点一下刷一次"说起
最早做设备监控界面的时候,我写了个"刷新"按钮。操作员想看当前压力,点一下,发一次Modbus请求,显示出来。
后来被现场工程师骂了一顿。
“我盯着压力表看的时候,还得一直点按钮?万一我点慢了,压力超了不知道怎么办?”
确实。工业现场的数据监控必须是自动、持续、实时的。你不可能让人盯着手动刷新。所以定时轮询是必须的,但怎么轮询、轮询频率多少、数据怎么更新到UI,这里面有不少门道。
轮询方案选型
WPF里定时执行有三种选择:
| 方案 | 线程 | 适用场景 |
|---|---|---|
DispatcherTimer | UI线程 | 需要直接操作UI控件 |
System.Threading.Timer | 线程池 | 后台数据采集 |
Task.Delay循环 | 线程池 | 需要精确控制间隔 |
我项目里选的是 System.Threading.Timer。原因很简单:数据采集是后台操作,不应该占用UI线程。采集完数据后通过 ObservableProperty 自动通知UI更新。

压力监控服务:完整实现
项目里有个 ChamberPressureMonitorService,专门监控腔体压力。核心逻辑就是定时读寄存器、判断阈值、更新属性。
public partial class ChamberPressureMonitorService : ObservableObject, IDisposable
{
private readonly IDeviceCmdService _deviceCmdService;
private readonly Timer _monitorTimer;
// 1秒轮询一次
private const int MONITOR_INTERVAL_MS = 1000;
[ObservableProperty]
private double currentPressureMTorr;
[ObservableProperty]
private double pps1ThresholdMTorr;
[ObservableProperty]
private bool isPressureAboveThreshold;
[ObservableProperty]
private bool isDataValid;
[ObservableProperty]
private DateTime lastUpdateTime;
[ObservableProperty]
private bool isServiceRunning;
public ChamberPressureMonitorService(IDeviceCmdService deviceCmdService)
{
_deviceCmdService = deviceCmdService;
InitializePPS1Threshold();
// 创建定时器,但不立即启动
_monitorTimer = new Timer(OnMonitorTick, null, Timeout.Infinite, Timeout.Infinite);
}
}
几个设计要点:
① 继承 ObservableObject — 这个服务本身就是数据源,属性变化时直接通知绑定的UI。不需要中间层转发。
② Timeout.Infinite 初始化 — Timer创建后不立即跑。等设备连接成功后再调 StartMonitoring()。
③ 监控间隔1秒 — 工业设备不需要太快。压力变化是秒级的,100ms轮询除了给设备添堵没别的好处。
启动和停止
public void StartMonitoring()
{
if (IsServiceRunning)
{
_logger.Warn("压力监控服务已在运行");
return;
}
// 立即执行一次,不用等第一个周期
UpdatePressureData();
_monitorTimer.Change(MONITOR_INTERVAL_MS, MONITOR_INTERVAL_MS);
IsServiceRunning = true;
}
public void StopMonitoring()
{
if (!IsServiceRunning) return;
_monitorTimer.Change(Timeout.Infinite, Timeout.Infinite);
IsServiceRunning = false;
}
启动时立即执行一次 — 这是个细节。如果不立即执行,启动后1秒内UI显示的都是初始值0。操作员看到0可能会以为设备坏了。
数据读取:从寄存器到属性
定时器回调里做的事很简单:找到对应寄存器,读值,更新属性。
private void UpdatePressureData()
{
try
{
// 从压力真空计对应的模拟量输入寄存器读取(点位地址略)
var pressureRegister = _deviceCmdService?.AnalogIOInputRegisters?
.FirstOrDefault(ai => ai.IoCode == AnalogIOInputCodes.PressureGauge.ToIoCode());
if (pressureRegister != null)
{
// 注意:用FlowValue不用Value
// Value是原始寄存器值,FlowValue是转换后的物理量
CurrentPressureMTorr = pressureRegister.FlowValue;
// 阈值判断
IsPressureAboveThreshold = CurrentPressureMTorr >= Pps1ThresholdMTorr;
IsDataValid = true;
LastUpdateTime = DateTime.Now;
}
else
{
HandleDataUnavailable();
}
}
catch (Exception ex)
{
_logger.Error($"更新压力数据失败: {ex.Message}", ex);
HandleDataUnavailable();
}
}
这里有个坑我踩过:一开始我用的是 pressureRegister.Value,结果数值完全不对。后来才发现 Modbus 寄存器返回的是 ushort(0-65535),需要经过工程量转换才是实际压力值。FlowValue 才是转换后的。
数据不可用时的安全策略
设备断线、传感器故障、通信超时——这些情况都必须处理。我的做法是"安全默认":
private void HandleDataUnavailable()
{
IsDataValid = false;
LastUpdateTime = DateTime.Now;
// 安全策略:数据不可用时认为压力未达标
IsPressureAboveThreshold = false;
// 重置为安全默认值
CurrentPressureMTorr = 0;
_logger.Warn("压力数据不可用,采用安全策略");
}
为什么设为0而不是保持上一个值? 因为我宁可让安全控制服务看到"压力不达标"从而禁止操作,也不能让它看到一个过时的"正常"值而放行。工业系统的安全原则是:不确定就不允许。
健康检查
光有定时器还不够。万一定时器在跑,但数据早就过期了呢?比如设备断线后Timer还在空转,IsDataValid虽然设了false,但外部怎么知道这个服务还"活着"?
public bool IsServiceHealthy()
{
if (!IsServiceRunning) return false;
// 超过10秒没更新认为不健康
var timeSinceLastUpdate = DateTime.Now - LastUpdateTime;
return timeSinceLastUpdate.TotalSeconds < 10 && IsDataValid;
}
10秒是10个轮询周期。正常情况下1秒更新一次,如果10秒没更新,要么通信断了,要么定时器卡死了。外部可以定期调这个方法判断服务状态。

气体流量采集:事件驱动模式
压力监控是"服务自己持有数据,UI直接绑定"。气体流量采集用了另一种模式——事件驱动:
public class FlowDataCollectorService : IDisposable
{
private Timer _collectionTimer;
public int CollectionIntervalMs { get; set; } = 1000;
// 数据采集完成事件
public event EventHandler<FlowData> DataCollected;
public void StartCollection(int intervalMs = 1000)
{
if (_isCollecting) return;
CollectionIntervalMs = intervalMs;
_collectionTimer = new Timer(CollectionTimerCallback, null, 0, intervalMs);
_isCollecting = true;
}
private void CollectionTimerCallback(object state)
{
// 采集所有MFC流量数据
var data = CollectFromAllChannels();
DataCollected?.Invoke(this, data);
}
}
区别在于:
- 压力监控:数据存在Service的属性里,UI直接绑定
CurrentPressureMTorr - 流量采集:数据通过事件抛出去,订阅者自己决定怎么处理
两种模式各有适用场景。压力只有一个值,绑属性简单直接。流量有12路MFC,数据量大,用事件更灵活。

UI更新:ObservableProperty的魔力
用 CommunityToolkit.Mvvm 的 [ObservableProperty],数据更新到UI是自动的:
<!-- XAML绑定 -->
<TextBlock Text="{Binding CurrentPressureMTorr, StringFormat={}{0:F1} mTorr}" />
<Border Background="{Binding IsPressureAboveThreshold, Converter={StaticResource BoolToColorConverter}}" />
<TextBlock Text="{Binding LastUpdateTime, StringFormat={}{0:HH:mm:ss}}" />
C#里改了 CurrentPressureMTorr,UI上的数字立刻变。不需要手动调 Dispatcher.Invoke,不需要手动触发 PropertyChanged。源生成器帮你搞定了。
但有个线程问题要注意:System.Threading.Timer 的回调在线程池线程上跑,不是UI线程。ObservableProperty 内部会触发 PropertyChanged,WPF的绑定引擎会自动 marshal 到 UI 线程,所以大多数情况没问题。
但如果你的属性setter里有UI相关的逻辑(比如打开对话框),就必须手动切线程:
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
// UI相关操作
}));
多服务协调
实际项目里不止一个监控服务。我项目里有:
ChamberPressureMonitorService— 腔体压力,1秒PumpPressureMonitorService— 泵口压力,1秒FlowDataCollectorService— 气体流量,1秒ChamberSafetyControlService— 安全控制,有自己的定时器
这些服务不能各跑各的,得有人协调。这个角色由 DeviceCmdService 担当——设备连接成功后启动监控,断开时停止监控。
// 设备连接成功后
_chamberPressureMonitor.StartMonitoring();
_rpsPressureMonitor.StartMonitoring();
_gasFlowCollector.StartCollection(1000);
// 设备断开时
_chamberPressureMonitor.StopMonitoring();
_rpsPressureMonitor.StopMonitoring();
_gasFlowCollector.StopCollection();
统一启停比让每个服务自己监听连接状态更可控。出了问题只看一个地方。

轮询踩坑清单
| 坑 | 现象 | 解决 |
|---|---|---|
| 用DispatcherTimer | UI卡顿 | 改用Threading.Timer |
| 轮询间隔太短 | 设备通信口打满 | 1秒足够,最快500ms |
| 不处理数据不可用 | 断线后UI显示过期值 | 设安全默认值+IsDataValid |
| 不做健康检查 | 服务假死不知道 | 检查LastUpdateTime超时 |
| 用原始寄存器值 | 数值不对 | 用转换后的FlowValue |
| 多服务各自监听 | 启停不一致 | 统一由协调者管理 |
本篇小结
| 知识点 | 关键做法 |
|---|---|
| 定时轮询 | System.Threading.Timer + 1秒间隔 |
| 数据绑定 | [ObservableProperty] 驱动UI自动更新 |
| 安全策略 | 数据不可用时设安全默认值 |
| 健康检查 | 检查LastUpdateTime是否超时 |
| 两种模式 | 属性绑定(单值) vs 事件驱动(批量数据) |
| 多服务协调 | 统一启停,由DeviceCmdService管理 |
实时监控的核心不是"快",而是"稳"。1秒一次的可靠更新,比100毫秒一次的偶尔丢包强得多。
下期预告
第6篇:配方数据结构设计
数据能读了,设备能控了。下一篇进入工艺执行——怎么把操作步骤写成配置文件,配方数据怎么组织,PPS参数怎么映射。
:实时数据监控与UI更新&spm=1001.2101.3001.5002&articleId=163832661&d=1&t=3&u=b57795a43da64b8399b0e3fbd76a6123)
1810

被折叠的 条评论
为什么被折叠?



