fix: unify hardware fault state and UI alerts
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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` 窗口生命周期清理。
|
||||
|
||||
Reference in New Issue
Block a user