fix: unify hardware fault state and UI alerts

This commit is contained in:
zhengxuan.zhang
2026-08-04 11:02:41 +08:00
parent d7f8f7c3f5
commit 98d0a5b64b
3 changed files with 220 additions and 47 deletions
+18 -7
View File
@@ -3,13 +3,22 @@ using System;
namespace XplorePlane.Models.Main
{
/// <summary>系统操作模式</summary>
public enum OperationMode
public enum OperationMode
{
Idle, // 空闲
Scanning, // 扫描
CTAcquire, // CT 采集
RecipeRun // 配方执行中
}
RecipeRun // 配方执行中
}
/// <summary>统一硬件故障来源。</summary>
public enum HardwareFaultSource
{
None,
Motion,
Detector,
RaySource
}
/// <summary>配方执行状态</summary>
public enum RecipeExecutionStatus
@@ -74,10 +83,12 @@ namespace XplorePlane.Models.Main
}
/// <summary>系统整体状态(不可变)</summary>
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);
}
+198 -36
View File
@@ -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<HardwareFaultSource, string> _hardwareFaults = new();
// ── 状态字段(通过 Interlocked.Exchange 原子替换,线程安全)──
private MotionState _motionState = MotionState.Default; // 运动轴状态
@@ -272,9 +282,16 @@ namespace XplorePlane.Services.AppState
.GetEvent<RaySourceStatusChangedEvent>()
.Subscribe(OnRaySourceStatusChanged, ThreadOption.BackgroundThread);
_raySourceVariablesConnectedToken = _eventAggregator
.GetEvent<VariablesConnectedEvent>()
.Subscribe(OnRaySourceVariablesConnected, ThreadOption.BackgroundThread);
_raySourceVariablesConnectedToken = _eventAggregator
.GetEvent<VariablesConnectedEvent>()
.Subscribe(OnRaySourceVariablesConnected, ThreadOption.BackgroundThread);
_motionErrorToken = _eventAggregator.GetEvent<MotionErrorEvent>()
.Subscribe(OnMotionError, ThreadOption.BackgroundThread);
_detectorErrorToken = _eventAggregator.GetEvent<DetectorErrorOccurredEvent>()
.Subscribe(OnDetectorError, ThreadOption.BackgroundThread);
_raySourceErrorToken = _eventAggregator.GetEvent<RaySourceErrorOccurredEvent>()
.Subscribe(OnRaySourceError, ThreadOption.BackgroundThread);
SubscribeToExistingServices();
_logger.Info("AppStateService initialized");
@@ -501,10 +518,17 @@ namespace XplorePlane.Services.AppState
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Unsubscribe(_raySourceStatusChangedToken);
}
if (_raySourceVariablesConnectedToken is not null)
{
_eventAggregator.GetEvent<VariablesConnectedEvent>().Unsubscribe(_raySourceVariablesConnectedToken);
}
if (_raySourceVariablesConnectedToken is not null)
{
_eventAggregator.GetEvent<VariablesConnectedEvent>().Unsubscribe(_raySourceVariablesConnectedToken);
}
if (_motionErrorToken is not null)
_eventAggregator.GetEvent<MotionErrorEvent>().Unsubscribe(_motionErrorToken);
if (_detectorErrorToken is not null)
_eventAggregator.GetEvent<DetectorErrorOccurredEvent>().Unsubscribe(_detectorErrorToken);
if (_raySourceErrorToken is not null)
_eventAggregator.GetEvent<RaySourceErrorOccurredEvent>().Unsubscribe(_raySourceErrorToken);
MotionStateChanged = null;
RaySourceStateChanged = null;
@@ -556,11 +580,48 @@ namespace XplorePlane.Services.AppState
}
/// <summary>轴状态变更事件回调:尝试从硬件刷新完整运动状态快照。</summary>
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);
}
/// <summary>几何参数更新事件回调:缓存几何数据并刷新运动状态。</summary>
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");
}
}
/// <summary>
/// 将硬件故障统一写入 AppState、日志和主界面状态栏。
/// 同一来源同一消息只提示一次,避免硬件轮询造成告警洪泛。
/// </summary>
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<StatusBarMessageEvent>().Publish(
new StatusBarMessagePayload(displayMessage, IsError: true, DurationMs: 0));
}
}
private void ClearHardwareFault(HardwareFaultSource source)
{
if (_disposed || source == HardwareFaultSource.None) return;
bool cleared;
KeyValuePair<HardwareFaultSource, string>[] 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<StatusBarMessageEvent>().Publish(
new StatusBarMessagePayload($"{GetHardwareFaultName(source)}故障已恢复", IsError: false, DurationMs: 5000));
}
private static string GetHardwareFaultName(HardwareFaultSource source) => source switch
{
HardwareFaultSource.Motion => "运动控制",
HardwareFaultSource.Detector => "探测器",
HardwareFaultSource.RaySource => "射线源",
_ => "硬件"
};
/// <summary>
/// 从硬件层采集各轴实际位置和几何参数,生成完整 MotionState 快照。
@@ -794,4 +956,4 @@ namespace XplorePlane.Services.AppState
RaiseOnDispatcher(old, newState, MotionStateChanged, nameof(MotionState));
}
}
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
# AppState 与事件流问题审计
> 审计日期:2026-08-03
> 审计日期:2026-08-03P1-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` 窗口生命周期清理。