diff --git a/XplorePlane/Models/Main/StateModels.cs b/XplorePlane/Models/Main/StateModels.cs index 08d60366..88e5ce16 100644 --- a/XplorePlane/Models/Main/StateModels.cs +++ b/XplorePlane/Models/Main/StateModels.cs @@ -3,13 +3,22 @@ using System; namespace XplorePlane.Models.Main { /// 系统操作模式 - public enum OperationMode + public enum OperationMode { Idle, // 空闲 Scanning, // 扫描 CTAcquire, // CT 采集 - RecipeRun // 配方执行中 - } + RecipeRun // 配方执行中 + } + + /// 统一硬件故障来源。 + public enum HardwareFaultSource + { + None, + Motion, + Detector, + RaySource + } /// 配方执行状态 public enum RecipeExecutionStatus @@ -74,10 +83,12 @@ namespace XplorePlane.Models.Main } /// 系统整体状态(不可变) - public record SystemState( - OperationMode OperationMode, // 当前操作模式 - bool HasError, // 是否存在错误 - string ErrorMessage) // 错误信息 + public record SystemState( + OperationMode OperationMode, // 当前操作模式 + bool HasError, // 是否存在错误 + string ErrorMessage, // 错误信息 + HardwareFaultSource FaultSource = HardwareFaultSource.None, + DateTime? FaultedAtUtc = null) // 错误信息 { public static readonly SystemState Default = new(OperationMode.Idle, false, string.Empty); } diff --git a/XplorePlane/Services/AppState/AppStateService.cs b/XplorePlane/Services/AppState/AppStateService.cs index 90970861..72cbd1b1 100644 --- a/XplorePlane/Services/AppState/AppStateService.cs +++ b/XplorePlane/Services/AppState/AppStateService.cs @@ -1,7 +1,9 @@ using Prism.Events; -using Prism.Mvvm; -using System; -using System.ComponentModel; +using Prism.Mvvm; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using System.Threading; using System.Windows; using System.Windows.Threading; @@ -16,8 +18,11 @@ using XP.Hardware.MotionControl.Abstractions.Events; using XP.Hardware.MotionControl.Services; using XP.Hardware.RaySource.Abstractions.Enums; using XP.Hardware.RaySource.Abstractions.Events; -using XP.Hardware.RaySource.Services; -using XplorePlane.Events; +using XP.Hardware.RaySource.Services; +using XplorePlane.Events; +using XplorePlane.Models.Main; +using DetectorErrorOccurredEvent = XP.Hardware.Detector.Abstractions.Events.ErrorOccurredEvent; +using RaySourceErrorOccurredEvent = XP.Hardware.RaySource.Abstractions.Events.ErrorOccurredEvent; namespace XplorePlane.Services.AppState { @@ -155,10 +160,15 @@ namespace XplorePlane.Services.AppState private readonly SubscriptionToken _detectorImageCapturedToken; // 探测器图像采集订阅 private readonly SubscriptionToken _raySourceStatusUpdatedToken; // 射线源全量状态更新订阅 private readonly SubscriptionToken _raySourceStatusChangedToken; // 射线源三态变更订阅 - private readonly SubscriptionToken _raySourceVariablesConnectedToken; // 射线源 PVI 变量连接状态订阅 + private readonly SubscriptionToken _raySourceVariablesConnectedToken; // 射线源 PVI 变量连接状态订阅 + private readonly SubscriptionToken _motionErrorToken; + private readonly SubscriptionToken _detectorErrorToken; + private readonly SubscriptionToken _raySourceErrorToken; - private bool _disposed; // 是否已释放 - private GeometryData _latestGeometry; // 最新几何参数缓存 + private bool _disposed; // 是否已释放 + private GeometryData _latestGeometry; // 最新几何参数缓存 + private readonly object _hardwareFaultGate = new(); + private readonly System.Collections.Generic.Dictionary _hardwareFaults = new(); // ── 状态字段(通过 Interlocked.Exchange 原子替换,线程安全)── private MotionState _motionState = MotionState.Default; // 运动轴状态 @@ -272,9 +282,16 @@ namespace XplorePlane.Services.AppState .GetEvent() .Subscribe(OnRaySourceStatusChanged, ThreadOption.BackgroundThread); - _raySourceVariablesConnectedToken = _eventAggregator - .GetEvent() - .Subscribe(OnRaySourceVariablesConnected, ThreadOption.BackgroundThread); + _raySourceVariablesConnectedToken = _eventAggregator + .GetEvent() + .Subscribe(OnRaySourceVariablesConnected, ThreadOption.BackgroundThread); + + _motionErrorToken = _eventAggregator.GetEvent() + .Subscribe(OnMotionError, ThreadOption.BackgroundThread); + _detectorErrorToken = _eventAggregator.GetEvent() + .Subscribe(OnDetectorError, ThreadOption.BackgroundThread); + _raySourceErrorToken = _eventAggregator.GetEvent() + .Subscribe(OnRaySourceError, ThreadOption.BackgroundThread); SubscribeToExistingServices(); _logger.Info("AppStateService initialized"); @@ -501,10 +518,17 @@ namespace XplorePlane.Services.AppState _eventAggregator.GetEvent().Unsubscribe(_raySourceStatusChangedToken); } - if (_raySourceVariablesConnectedToken is not null) - { - _eventAggregator.GetEvent().Unsubscribe(_raySourceVariablesConnectedToken); - } + if (_raySourceVariablesConnectedToken is not null) + { + _eventAggregator.GetEvent().Unsubscribe(_raySourceVariablesConnectedToken); + } + + if (_motionErrorToken is not null) + _eventAggregator.GetEvent().Unsubscribe(_motionErrorToken); + if (_detectorErrorToken is not null) + _eventAggregator.GetEvent().Unsubscribe(_detectorErrorToken); + if (_raySourceErrorToken is not null) + _eventAggregator.GetEvent().Unsubscribe(_raySourceErrorToken); MotionStateChanged = null; RaySourceStateChanged = null; @@ -556,11 +580,48 @@ namespace XplorePlane.Services.AppState } /// 轴状态变更事件回调:尝试从硬件刷新完整运动状态快照。 - private void OnAxisStatusChanged(AxisStatusChangedData _) - { - if (_disposed) return; - TryRefreshMotionStateFromHardware("axis-status-changed"); - } + private void OnAxisStatusChanged(AxisStatusChangedData data) + { + if (_disposed) return; + + if (data.Status is AxisStatus.Error or AxisStatus.Alarm) + { + PublishHardwareFault( + HardwareFaultSource.Motion, + $"轴 {data.AxisId} 进入 {data.Status} 状态"); + } + else + { + ClearHardwareFault(HardwareFaultSource.Motion); + } + + TryRefreshMotionStateFromHardware("axis-status-changed"); + } + + private void OnMotionError(MotionErrorData data) + { + if (_disposed || data is null) return; + PublishHardwareFault( + HardwareFaultSource.Motion, + $"轴 {data.AxisId}:{data.ErrorMessage}"); + } + + private void OnDetectorError(DetectorResult result) + { + if (_disposed || result is null || result.IsSuccess) return; + PublishHardwareFault( + HardwareFaultSource.Detector, + result.ErrorMessage ?? "探测器发生未知错误", + result.Exception); + } + + private void OnRaySourceError(string message) + { + if (_disposed) return; + PublishHardwareFault( + HardwareFaultSource.RaySource, + string.IsNullOrWhiteSpace(message) ? "射线源发生未知错误" : message); + } /// 几何参数更新事件回调:缓存几何数据并刷新运动状态。 private void OnGeometryUpdated(GeometryData geometry) @@ -631,17 +692,26 @@ namespace XplorePlane.Services.AppState bool isConnected = status != DetectorStatus.Uninitialized && status != DetectorStatus.Error; bool isAcquiring = status == DetectorStatus.Acquiring; - var newState = new DetectorState( - IsConnected: isConnected, + var newState = new DetectorState( + IsConnected: isConnected, IsAcquiring: isAcquiring, FrameRate: frameRate, Resolution: resolution, Binning: binning, FrameAvg: frameAvg, WorkMode: workMode, - Sensitivity: sensitivity); - - UpdateDetectorState(newState); + Sensitivity: sensitivity); + + UpdateDetectorState(newState); + + if (status == DetectorStatus.Error) + { + PublishHardwareFault(HardwareFaultSource.Detector, "探测器状态为 Error"); + } + else if (isConnected) + { + ClearHardwareFault(HardwareFaultSource.Detector); + } _logger.Info( "探测器状态已同步:{Status} → IsConnected={IsConnected} IsAcquiring={IsAcquiring} | " + @@ -716,16 +786,108 @@ namespace XplorePlane.Services.AppState { if (_disposed) return; - if (!isConnected) - { - UpdateRaySourceState(RaySourceState.Default); - _logger.Warn("射线源 PVI 变量已断开,RaySourceState 已重置 | RaySource PVI variables disconnected, RaySourceState reset"); - } - else - { - _logger.Info("射线源 PVI 变量已连接 | RaySource PVI variables connected"); - } - } + if (!isConnected) + { + UpdateRaySourceState(RaySourceState.Default); + PublishHardwareFault(HardwareFaultSource.RaySource, "射线源变量连接已断开"); + _logger.Warn("射线源 PVI 变量已断开,RaySourceState 已重置 | RaySource PVI variables disconnected, RaySourceState reset"); + } + else + { + ClearHardwareFault(HardwareFaultSource.RaySource); + _logger.Info("射线源 PVI 变量已连接 | RaySource PVI variables connected"); + } + } + + /// + /// 将硬件故障统一写入 AppState、日志和主界面状态栏。 + /// 同一来源同一消息只提示一次,避免硬件轮询造成告警洪泛。 + /// + private void PublishHardwareFault( + HardwareFaultSource source, + string message, + Exception exception = null) + { + if (_disposed || source == HardwareFaultSource.None) return; + + var normalized = string.IsNullOrWhiteSpace(message) ? "未知硬件故障" : message.Trim(); + bool shouldNotify; + lock (_hardwareFaultGate) + { + shouldNotify = !_hardwareFaults.TryGetValue(source, out var previous) || + !string.Equals(previous, normalized, StringComparison.Ordinal); + _hardwareFaults[source] = normalized; + } + + var displayMessage = $"{GetHardwareFaultName(source)}:{normalized}"; + var state = _systemState; + if (shouldNotify || !state.HasError || state.FaultSource != source || state.ErrorMessage != displayMessage) + { + UpdateSystemState(state with + { + HasError = true, + ErrorMessage = displayMessage, + FaultSource = source, + FaultedAtUtc = DateTime.UtcNow + }); + } + + if (shouldNotify) + { + if (exception is null) + _logger.Error(null, $"硬件故障 [{source}] {normalized}"); + else + _logger.Error(exception, $"硬件故障 [{source}] {normalized}"); + } + + if (shouldNotify) + { + _eventAggregator.GetEvent().Publish( + new StatusBarMessagePayload(displayMessage, IsError: true, DurationMs: 0)); + } + } + + private void ClearHardwareFault(HardwareFaultSource source) + { + if (_disposed || source == HardwareFaultSource.None) return; + + bool cleared; + KeyValuePair[] remaining; + lock (_hardwareFaultGate) + { + cleared = _hardwareFaults.Remove(source); + remaining = _hardwareFaults.ToArray(); + } + if (!cleared) return; + + var next = remaining.Length == 0 + ? _systemState with + { + HasError = false, + ErrorMessage = string.Empty, + FaultSource = HardwareFaultSource.None, + FaultedAtUtc = null + } + : _systemState with + { + HasError = true, + ErrorMessage = $"{GetHardwareFaultName(remaining[0].Key)}:{remaining[0].Value}", + FaultSource = remaining[0].Key + }; + + UpdateSystemState(next); + _logger.Info("硬件故障已恢复 [{Source}] | Hardware fault cleared: {Source}", source, source); + _eventAggregator.GetEvent().Publish( + new StatusBarMessagePayload($"{GetHardwareFaultName(source)}故障已恢复", IsError: false, DurationMs: 5000)); + } + + private static string GetHardwareFaultName(HardwareFaultSource source) => source switch + { + HardwareFaultSource.Motion => "运动控制", + HardwareFaultSource.Detector => "探测器", + HardwareFaultSource.RaySource => "射线源", + _ => "硬件" + }; /// /// 从硬件层采集各轴实际位置和几何参数,生成完整 MotionState 快照。 @@ -794,4 +956,4 @@ namespace XplorePlane.Services.AppState RaiseOnDispatcher(old, newState, MotionStateChanged, nameof(MotionState)); } } -} \ No newline at end of file +} diff --git a/doc/fix/AppState与事件流问题审计.md b/doc/fix/AppState与事件流问题审计.md index a43b8639..22de1147 100644 --- a/doc/fix/AppState与事件流问题审计.md +++ b/doc/fix/AppState与事件流问题审计.md @@ -1,6 +1,6 @@ # AppState 与事件流问题审计 -> 审计日期:2026-08-03 +> 审计日期:2026-08-03;P1-6 修复完成:2026-08-04 > 审计范围:硬件事件、`AppStateService`、调试面板、探测器帧队列、BGA 向导及窗口生命周期。 > 本文最初用于记录代码审阅结论;2026-08-04 更新:第一、二阶段(P0-1、P0-2、P1-1、P1-2)已修复并通过编译验证,其余问题仍待处理,详见第 4、5 节。 @@ -19,7 +19,7 @@ | P1-3 | `_latestGeometry` 跨线程读写缺少明确同步 | **确认存在,风险待量化** | [`AppStateService.cs:161`](../../XplorePlane/Services/AppState/AppStateService.cs#L161)、[`AppStateService.cs:566-571`](../../XplorePlane/Services/AppState/AppStateService.cs#L566) | | P1-4 | 探测器断连状态判断不是单一原子事务 | **部分确认** | [`AppStateService.cs:579-585`](../../XplorePlane/Services/AppState/AppStateService.cs#L579) 在后台线程读取 `_detectorState`;最终状态更新仍通过原子替换,但 `wasConnected` 与后续更新之间可能交错 | | P1-5 | `CncExecutionStateChanged` 没有主项目订阅方 | **确认存在** | `AppStateService` 有发布事件,但当前主项目未发现 `+=` 订阅;部分界面改读 `MainViewportService.IsCncRunning`,形成双来源风险 | -| P1-6 | 硬件错误事件没有进入统一 `SystemState` | **确认存在** | `MotionErrorEvent`、探测器 `ErrorOccurredEvent`、射线源 `ErrorOccurredEvent` 在硬件层有发布,但主项目未发现对应统一订阅;射线源自身 ViewModel 的局部订阅不等于全局故障状态闭环 | +| P1-6 | 硬件错误事件没有进入统一 `SystemState` | **已修复(2026-08-04)** | `AppStateService` 统一订阅运动、探测器和射线源错误事件,更新 `SystemState`,记录日志,并发布 `StatusBarMessageEvent`;同源同消息去重,恢复事件清除对应故障 | | P1-7 | `OperationMode` 始终为 `Idle`,权限守卫可能失效 | **确认存在** | 当前唯一写入点 [`RecipeService.cs:225`](../../XplorePlane/Services/Recipe/RecipeService.cs#L225) 仍写入 `OperationMode.Idle`;[`PermissionService.cs:159`](../../XplorePlane/Services/Security/PermissionService.cs#L159) 依赖该字段 | | P2-1 | 标定矩阵/画面联动没有外部调用方 | **基本确认** | `UpdateCalibrationMatrix`、`RequestLinkedView`、`UpdateLinkedViewState` 的调用主要位于 `AppStateService` 自身,未发现业务入口;需结合实际 UI 需求决定是接入还是删除 | | P2-2 | `WindowLauncherService.ShowOrActivateVisible` 关闭后保留窗口引用 | **确认存在** | [`WindowLauncherService.cs:91-106`](../../XplorePlane/Services/Main/WindowLauncherService.cs#L91) 没有像 `ShowOrActivate` 一样挂载 `Closed` 清理逻辑;接口注释也明确记录了该历史行为 | @@ -72,7 +72,7 @@ - [x] `_acquireQueue` 有明确消费者或明确的淘汰/禁用策略,运行时内存曲线可解释。(P1-1 已修复:移除队列本体,改为纯计数) - [x] BGA 流水线输出缺失时有可见错误,不再静默返回;编译器不再报告该方法的不可达代码。(P0-1 已修复) - [ ] CNC 执行中权限判断、主界面状态和执行服务使用同一个状态来源。(P1-5/P1-7,第三阶段,未开始) -- [ ] 硬件故障能够进入统一状态、日志和 UI 告警链路。(P1-6,第三阶段,未开始) +- [x] 硬件故障能够进入统一状态、日志和 UI 告警链路。(P1-6 已修复) 第一、二阶段的四项已完成并通过 `dotnet build` 主项目编译验证;`XplorePlane.Tests` 因历史遗留的命名空间引用问题(`XplorePlane.Services.Logging`/`Diagnostics`/`Dialogs` 缺失)当前无法编译运行,与本次改动无关(改动前 `git stash` 验证同样报错),因此本轮未新增/运行自动化测试,也未验证 P1-3/P1-4/P2-3 等标注为“待验证”的问题。 @@ -81,7 +81,7 @@ - P1-3 `_latestGeometry` 跨线程同步风险待量化。 - P1-4 探测器断连状态判断的原子性待确认。 - P1-5 `CncExecutionStateChanged` 统一事实源。 -- P1-6 硬件错误事件接入统一 `SystemState`。 +- ~~P1-6 硬件错误事件接入统一 `SystemState`。~~ 已完成:运动轴 Error/Alarm、探测器错误/错误状态、射线源错误/变量断连均已接入统一状态、日志和状态栏告警链路。 - P1-7 `OperationMode` 恒为 `Idle` 的权限守卫失效风险。 - P2-1 标定矩阵/画面联动子系统去留决策。 - P2-2 `WindowLauncherService.ShowOrActivateVisible` 窗口生命周期清理。