从零搭建工业控制系统(二十一):状态管理系统——中心状态对象设计

状态管理系统:中心状态对象设计

这是「从零搭建工业控制系统」系列第21篇。前面讲了消息通信,这篇讲状态管理------设备状态散落在各处怎么管,怎么跟Modbus寄存器映射,怎么绑定到UI。


问题:状态散落各处

工业系统有大量状态------阀门开关、压力读数、温度值、连接状态、运行状态......一开始这些状态散落在各个Service和ViewModel里。

问题很快出现:

  • 阀门状态在ModbusService里,压力在PressureMonitor里,温度在TraceMonitor里
  • 命令执行器要检查互锁条件,得从四五个地方拿状态
  • UI状态面板要显示所有状态,绑定了七八个不同的ViewModel
  • 状态来源不一致------A服务读到的阀门状态和B服务读到的不一样(因为读取时机不同)

后来我做了一个中心状态对象:所有子系统状态集中在一个类里,统一管理,统一更新,统一绑定。


中心状态对象设计

csharp 复制代码
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寄存器读取。映射关系在轮询服务里:

csharp 复制代码
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都能拿到同一个实例:

csharp 复制代码
containerRegistry.RegisterSingleton<SubsystemStatus>();

ViewModel注入后直接绑定:

csharp 复制代码
public partial class StatusViewModel : ObservableObject
{
    private readonly SubsystemStatus _status;

    public SubsystemStatus Status => _status;

    public StatusViewModel(SubsystemStatus status)
    {
        _status = status;
    }
}

XAML里绑定:

xml 复制代码
<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都更新。


互锁预检

命令执行前用状态对象做互锁预检:

csharp 复制代码
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,需要自己处理:

csharp 复制代码
// 后台线程
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篇:线程安全与并发控制

状态管理讲完了,下篇讲并发------多线程怎么安全操作设备,怎么避免死锁,怎么做互斥闸门。

相关推荐
俊昭喜喜里3 小时前
C#中的func<>
java·前端·c#
highreport3 小时前
net报表工具对比:HighReport 与 FastReport
java·c#
reasonsummer3 小时前
【办公类-146-05】20260901《一分园、二分园四大教育》(优化版:标题excle+复制AI文字+Python占位符录入)
开发语言·数据库·c#
CSDN_RTKLIB3 小时前
C#可选参数时重载
c#
古少侠4 小时前
用 DS随心转 把 AI 画的 Mermaid 流程图转成 Word 里的高清图
开发语言·c#·word
智者知已应修善业5 小时前
C#用一个循环[等效]取出字符串中的数字
网络·c#·web·string·语言
CSDN_RTKLIB5 小时前
自己的溯源函数注意点
c#
A_nanda5 小时前
C# 调用 .NET(Core)Web 服务混用:常见问题与 .NET 6~10 正确写法
c#·.net
北域码匠15 小时前
高通滤波算法深度解析(High-Pass Filter)
stm32·算法·c#·数字信号处理·嵌入式开发·滤波算法·高通滤波