状态管理系统:中心状态对象设计
这是「从零搭建工业控制系统」系列第21篇。前面讲了消息通信,这篇讲状态管理——设备状态散落在各处怎么管,怎么跟Modbus寄存器映射,怎么绑定到UI。
问题:状态散落各处
工业系统有大量状态——阀门开关、压力读数、温度值、连接状态、运行状态……一开始这些状态散落在各个Service和ViewModel里。
问题很快出现:
- 阀门状态在ModbusService里,压力在PressureMonitor里,温度在TraceMonitor里
- 命令执行器要检查互锁条件,得从四五个地方拿状态
- UI状态面板要显示所有状态,绑定了七八个不同的ViewModel
- 状态来源不一致——A服务读到的阀门状态和B服务读到的不一样(因为读取时机不同)
后来我做了一个中心状态对象:所有子系统状态集中在一个类里,统一管理,统一更新,统一绑定。

中心状态对象设计
public class SubsystemStatus : ObservableObject
{
// 连接状态
[ObservableProperty] private bool isDeviceConnected;
[ObservableProperty] private bool isModbusOnline;
// 腔体状态
[ObservableProperty] private double chamberPressure;
[ObservableProperty] private bool isChamberVacuum;
[ObservableProperty] private bool isChamberCoverClosed;
// 阀门状态
[ObservableProperty] private bool inletValveOpen;
[ObservableProperty] private bool outletValveOpen;
// 加热器状态
[ObservableProperty] private double heater1Temperature;
[ObservableProperty] private double heater2Temperature;
[ObservableProperty] private bool heater1Enabled;
// 运行状态
[ObservableProperty] private string currentWorkflow = "空闲";
[ObservableProperty] private int currentStep;
[ObservableProperty] private int totalSteps;
}
所有状态属性集中在一个类。用 [ObservableProperty] 自动生成INPC通知——属性变了UI自动更新。
Modbus寄存器到状态的映射
状态不是凭空来的,来自Modbus寄存器读取。映射关系在轮询服务里:
private async Task PollAndUpdateStatusAsync()
{
// 读取线圈状态
var inletCoil = await modbusService.ReadCoilAsync(DigitalIO, InletValveAddress);
_status.InletValveOpen = inletCoil;
// 读取寄存器
var pressureRaw = await modbusService.ReadRegisterAsync(AnalogIO, PressureAddress);
_status.ChamberPressure = pressureRaw * 0.001; // 原始值转工程值
// 读取温度
var tempRaw = await modbusService.ReadInputRegisterAsync(DigitalIO, Temp1Address);
_status.Heater1Temperature = tempRaw * 0.1;
}
每次轮询读到的值直接赋给状态对象的属性。[ObservableProperty] 自动触发PropertyChanged,UI绑定自动更新。

后台轮询频率
轮询频率是个权衡:
| 频率 | 优点 | 缺点 |
|---|---|---|
| 100ms | 状态实时性好 | Modbus通信压力大 |
| 500ms | 通信压力小 | 状态延迟0.5秒 |
| 1000ms | 压力最小 | 操作员觉得"卡" |
实际用500ms。工业设备状态变化没那么快,500ms足够。温度变化慢,用1秒轮询;阀门状态变化快,用200ms轮询。不同状态用不同频率。

UI绑定
状态对象注册为单例,所有ViewModel都能拿到同一个实例:
containerRegistry.RegisterSingleton<SubsystemStatus>();
ViewModel注入后直接绑定:
public partial class StatusViewModel : ObservableObject
{
private readonly SubsystemStatus _status;
public SubsystemStatus Status => _status;
public StatusViewModel(SubsystemStatus status)
{
_status = status;
}
}
XAML里绑定:
<StackPanel>
<TextBlock Text="{Binding Status.ChamberPressure, StringFormat={}{0:F1} Torr}" />
<TextBlock Text="{Binding Status.Heater1Temperature, StringFormat={}{0:F0} °C}" />
<Ellipse Fill="{Binding Status.InletValveOpen, Converter={StaticResource BoolToColorConverter}}" />
</StackPanel>
一个状态对象,N个UI元素绑定。改一处,所有绑定的UI都更新。
互锁预检
命令执行前用状态对象做互锁预检:
public async Task<CommandResult> OpenInletValveAsync()
{
// 互锁:腔体盖必须关闭
if (!_status.IsChamberCoverClosed)
return CommandResult.Fail("INTERLOCK", "腔体盖未关闭,不能开进气阀");
// 互锁:压力不能太高
if (_status.ChamberPressure > 700)
return CommandResult.Fail("INTERLOCK", "压力过高,不能开进气阀");
// 执行命令...
}
互锁条件从状态对象读,不从Modbus实时读——轮询服务负责更新状态,命令执行器只读状态。 职责分离。

跨线程更新
轮询在后台线程,状态属性更新会触发PropertyChanged,UI线程需要处理这个通知。
[ObservableProperty] 生成的代码内部会自动处理跨线程问题——如果PropertyChanged事件在非UI线程触发,WPF的绑定引擎会自动切换到UI线程更新界面。
但如果你手动触发PropertyChanged,需要自己处理:
// 后台线程
Application.Current.Dispatcher.Invoke(() =>
{
_status.ChamberPressure = newValue;
});
或者用 Dispatcher.BeginInvoke 异步切换。
踩坑记录
坑1:状态更新太频繁卡UI
500ms轮询一次,每次更新十几个属性,每个属性变化都触发PropertyChanged。UI线程每秒处理几十个绑定更新,界面卡顿。解决办法:批量更新——先读完所有寄存器,最后一次性赋值。减少PropertyChanged触发次数。
坑2:状态不一致
轮询读到阀门开,但命令执行器同时读了一次Modbus发现阀门关。两个来源不一致。解决办法:命令执行器不从Modbus读状态,只从状态对象读。 状态对象是唯一真相来源。
坑3:跨线程异常
后台线程更新ObservableProperty时报跨线程异常。原因是某些属性的类型不是线程安全的(如ObservableCollection)。解决办法:用Dispatcher切换到UI线程更新。
本篇小结
| 概念 | 关键做法 |
|---|---|
| 中心状态对象 | 所有状态集中一个类 |
| ObservableProperty | 自动INPC通知 |
| Modbus映射 | 轮询读寄存器→赋值状态属性 |
| 轮询频率 | 不同状态不同频率 |
| UI绑定 | 单例状态对象,多处绑定 |
| 互锁预检 | 从状态对象读,不从Modbus实时读 |
| 批量更新 | 读完所有寄存器再一次性赋值 |
状态管理的核心:单一真相来源。所有状态集中管理,所有人从同一个地方读,避免不一致。
下期预告
第22篇:线程安全与并发控制
状态管理讲完了,下篇讲并发——多线程怎么安全操作设备,怎么避免死锁,怎么做互斥闸门。
:状态管理系统——中心状态对象设计&spm=1001.2101.3001.5002&articleId=164153704&d=1&t=3&u=05a9392c3c5b431a8a277a2a8168a520)
1654

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



