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

发布时间:2026/9/4 7:42:53
从零搭建工业控制系统(二十一):状态管理系统——中心状态对象设计 状态管理系统中心状态对象设计这是「从零搭建工业控制系统」系列第21篇。前面讲了消息通信这篇讲状态管理——设备状态散落在各处怎么管怎么跟Modbus寄存器映射怎么绑定到UI。问题状态散落各处工业系统有大量状态——阀门开关、压力读数、温度值、连接状态、运行状态……一开始这些状态散落在各个Service和ViewModel里。问题很快出现阀门状态在ModbusService里压力在PressureMonitor里温度在TraceMonitor里命令执行器要检查互锁条件得从四五个地方拿状态UI状态面板要显示所有状态绑定了七八个不同的ViewModel状态来源不一致——A服务读到的阀门状态和B服务读到的不一样因为读取时机不同后来我做了一个中心状态对象所有子系统状态集中在一个类里统一管理统一更新统一绑定。中心状态对象设计publicclassSubsystemStatus:ObservableObject{// 连接状态[ObservableProperty]privateboolisDeviceConnected;[ObservableProperty]privateboolisModbusOnline;// 腔体状态[ObservableProperty]privatedoublechamberPressure;[ObservableProperty]privateboolisChamberVacuum;[ObservableProperty]privateboolisChamberCoverClosed;// 阀门状态[ObservableProperty]privateboolinletValveOpen;[ObservableProperty]privatebooloutletValveOpen;// 加热器状态[ObservableProperty]privatedoubleheater1Temperature;[ObservableProperty]privatedoubleheater2Temperature;[ObservableProperty]privateboolheater1Enabled;// 运行状态[ObservableProperty]privatestringcurrentWorkflow空闲;[ObservableProperty]privateintcurrentStep;[ObservableProperty]privateinttotalSteps;}所有状态属性集中在一个类。用[ObservableProperty]自动生成INPC通知——属性变了UI自动更新。Modbus寄存器到状态的映射状态不是凭空来的来自Modbus寄存器读取。映射关系在轮询服务里privateasyncTaskPollAndUpdateStatusAsync(){// 读取线圈状态varinletCoilawaitmodbusService.ReadCoilAsync(DigitalIO,InletValveAddress);_status.InletValveOpeninletCoil;// 读取寄存器varpressureRawawaitmodbusService.ReadRegisterAsync(AnalogIO,PressureAddress);_status.ChamberPressurepressureRaw*0.001;// 原始值转工程值// 读取温度vartempRawawaitmodbusService.ReadInputRegisterAsync(DigitalIO,Temp1Address);_status.Heater1TemperaturetempRaw*0.1;}每次轮询读到的值直接赋给状态对象的属性。[ObservableProperty]自动触发PropertyChangedUI绑定自动更新。后台轮询频率轮询频率是个权衡频率优点缺点100ms状态实时性好Modbus通信压力大500ms通信压力小状态延迟0.5秒1000ms压力最小操作员觉得卡实际用500ms。工业设备状态变化没那么快500ms足够。温度变化慢用1秒轮询阀门状态变化快用200ms轮询。不同状态用不同频率。UI绑定状态对象注册为单例所有ViewModel都能拿到同一个实例containerRegistry.RegisterSingletonSubsystemStatus();ViewModel注入后直接绑定publicpartialclassStatusViewModel:ObservableObject{privatereadonlySubsystemStatus_status;publicSubsystemStatusStatus_status;publicStatusViewModel(SubsystemStatusstatus){_statusstatus;}}XAML里绑定StackPanelTextBlockText{Binding Status.ChamberPressure, StringFormat{}{0:F1} Torr}/TextBlockText{Binding Status.Heater1Temperature, StringFormat{}{0:F0} °C}/EllipseFill{Binding Status.InletValveOpen, Converter{StaticResource BoolToColorConverter}}//StackPanel一个状态对象N个UI元素绑定。改一处所有绑定的UI都更新。互锁预检命令执行前用状态对象做互锁预检publicasyncTaskCommandResultOpenInletValveAsync(){// 互锁腔体盖必须关闭if(!_status.IsChamberCoverClosed)returnCommandResult.Fail(INTERLOCK,腔体盖未关闭不能开进气阀);// 互锁压力不能太高if(_status.ChamberPressure700)returnCommandResult.Fail(INTERLOCK,压力过高不能开进气阀);// 执行命令...}互锁条件从状态对象读不从Modbus实时读——轮询服务负责更新状态命令执行器只读状态。职责分离。跨线程更新轮询在后台线程状态属性更新会触发PropertyChangedUI线程需要处理这个通知。[ObservableProperty]生成的代码内部会自动处理跨线程问题——如果PropertyChanged事件在非UI线程触发WPF的绑定引擎会自动切换到UI线程更新界面。但如果你手动触发PropertyChanged需要自己处理// 后台线程Application.Current.Dispatcher.Invoke((){_status.ChamberPressurenewValue;});或者用Dispatcher.BeginInvoke异步切换。踩坑记录坑1状态更新太频繁卡UI500ms轮询一次每次更新十几个属性每个属性变化都触发PropertyChanged。UI线程每秒处理几十个绑定更新界面卡顿。解决办法批量更新——先读完所有寄存器最后一次性赋值。减少PropertyChanged触发次数。坑2状态不一致轮询读到阀门开但命令执行器同时读了一次Modbus发现阀门关。两个来源不一致。解决办法命令执行器不从Modbus读状态只从状态对象读。状态对象是唯一真相来源。坑3跨线程异常后台线程更新ObservableProperty时报跨线程异常。原因是某些属性的类型不是线程安全的如ObservableCollection。解决办法用Dispatcher切换到UI线程更新。本篇小结概念关键做法中心状态对象所有状态集中一个类ObservableProperty自动INPC通知Modbus映射轮询读寄存器→赋值状态属性轮询频率不同状态不同频率UI绑定单例状态对象多处绑定互锁预检从状态对象读不从Modbus实时读批量更新读完所有寄存器再一次性赋值状态管理的核心单一真相来源。所有状态集中管理所有人从同一个地方读避免不一致。下期预告第22篇线程安全与并发控制状态管理讲完了下篇讲并发——多线程怎么安全操作设备怎么避免死锁怎么做互斥闸门。