diff --git a/XplorePlane/Doc/硬件层及UI集成技术路线.txt b/XplorePlane/Doc/硬件层及UI集成技术路线.md similarity index 52% rename from XplorePlane/Doc/硬件层及UI集成技术路线.txt rename to XplorePlane/Doc/硬件层及UI集成技术路线.md index 6e25384..6a2624a 100644 --- a/XplorePlane/Doc/硬件层及UI集成技术路线.txt +++ b/XplorePlane/Doc/硬件层及UI集成技术路线.md @@ -1,4 +1,4 @@ -# RaySourceOperateView 集成技术路线 +# 硬件层及 UI 集成技术路线 ## 整体架构 @@ -181,9 +181,9 @@ raySourceService?.Dispose(); | 硬件子系统 | 同步方式 | AppState 订阅 | 状态是否自动同步 | |-----------|---------|--------------|----------------| | 运动控制(Motion) | 事件驱动 + 轮询 | ✅ 已订阅 `GeometryUpdatedEvent` / `AxisStatusChangedEvent` | ✅ 自动同步 | -| 射线源(RaySource) | 事件定义存在 | ❌ 未订阅任何事件 | ⚠️ 需手动推送 | -| 探测器(Detector) | 事件定义存在 | ❌ 未订阅任何事件 | ⚠️ 需手动推送 | -| 相机(Camera) | ViewModel 直连 | ❌ 不经过 AppState | ⚠️ `CameraState` 为空壳 | +| 射线源(RaySource) | 事件驱动 | ✅ 已订阅 `StatusUpdatedEvent` / `RaySourceStatusChangedEvent` / `VariablesConnectedEvent` | ✅ 自动同步 | +| 探测器(Detector) | 事件驱动 | ✅ 已订阅 `StatusChangedEvent` / `ImageCapturedEvent` | ✅ 自动同步 | +| 相机(Camera) | ViewModel 直连 + AppState 同步 | ✅ 连接/采集状态通过 `SyncCameraStateToAppState()` 推送 | ✅ 连接状态自动同步(帧数据不经过 AppState) | --- @@ -269,110 +269,195 @@ public void UpdateMotionState(MotionState newState) --- -## 二、射线源 — 断链(待补全) +## 二、射线源 — ✅ 已打通 -### 2.1 现状 +### 2.1 当前状态 -`AppStateService` 持有 `IRaySourceService` 引用,但**没有订阅任何射线源事件**。`RaySourceState` 只能通过外部显式调用 `UpdateRaySourceState()` 手动推送,硬件状态变化不会自动反映到 AppState。 +`AppStateService` 已在构造时订阅 `StatusUpdatedEvent`、`RaySourceStatusChangedEvent` 和 `VariablesConnectedEvent`,`RaySourceState` 自动同步。 ``` -XP.Hardware.RaySource 发布的事件(均无人在 AppState 层订阅): - ├─ StatusUpdatedEvent ← 实时电压 / 电流 / 功率数据 - ├─ RaySourceStatusChangedEvent ← 三态状态(On / Off / Fault) - ├─ VariablesConnectedEvent ← PLC 变量连接状态 - └─ OperationResultEvent ← 操作执行结果 +XP.Hardware.RaySource 发布的事件(AppState 层订阅情况): + ├─ StatusUpdatedEvent ← ✅ 已订阅(主路径),映射为 RaySourceState + ├─ RaySourceStatusChangedEvent ← ✅ 已订阅(补充路径),快速同步 IsOn 状态 + ├─ VariablesConnectedEvent ← ✅ 已订阅,断开时重置 RaySourceState 为默认值 + └─ OperationResultEvent ← 未订阅(操作结果由 ViewModel 层直接处理) ``` -### 2.2 当前 `RaySourceState` 的写入路径 +### 2.2 数据流 -目前只有两处会更新 `RaySourceState`: +``` +射线源硬件(B&R PVI) + └─ IRaySourceService(XP.Hardware.RaySource) + ├─ 发布 StatusUpdatedEvent(SystemStatusData,500ms 轮询) + │ └─ AppStateService.OnRaySourceStatusUpdated() [BackgroundThread] + │ ├─ IsOn = data.IsXRayOn + │ ├─ Voltage = data.ActualVoltage (kV) + │ ├─ Power = ActualVoltage × ActualCurrent / 1000 (W) + │ └─ UpdateRaySourceState() → 触发 RaySourceStateChanged → ViewModel 刷新 UI + │ + ├─ 发布 RaySourceStatusChangedEvent(RaySourceStatus 枚举) + │ └─ AppStateService.OnRaySourceStatusChanged() [BackgroundThread] + │ └─ 仅在 IsOn 变化时更新,保留当前 Voltage/Power 值 + │ + └─ 发布 VariablesConnectedEvent(bool) + └─ AppStateService.OnRaySourceVariablesConnected() [BackgroundThread] + └─ 断开时:UpdateRaySourceState(RaySourceState.Default) +``` -1. `CncProgramService.CreateReferencePointNode()` — 创建参考点时读取一次快照 -2. 任何直接调用 `appStateService.UpdateRaySourceState()` 的地方(目前代码中无此调用) +### 2.3 关键实现细节 -### 2.3 修复方案 - -在 `AppStateService` 构造时订阅 `StatusUpdatedEvent`,将实时数据映射到 `RaySourceState`: +**双路径设计**:`StatusUpdatedEvent` 是主路径,携带完整的电压、电流、开关状态;`RaySourceStatusChangedEvent` 是补充路径,仅在 `StatusUpdatedEvent` 尚未到达时快速同步 `IsOn` 字段,避免覆盖精确数据: ```csharp -// AppStateService 构造函数中补充 -_raySourceStatusToken = _eventAggregator - .GetEvent() - .Subscribe(OnRaySourceStatusUpdated); - -// 处理方法 -private void OnRaySourceStatusUpdated(SystemStatusData data) -{ - if (_disposed) return; - UpdateRaySourceState(new RaySourceState( - IsOn: data.IsOn, - Voltage: data.Voltage, - Power: data.Power)); -} +// OnRaySourceStatusChanged — 仅当 IsOn 状态发生变化时才更新 +if (current.IsOn == isOn) return; +UpdateRaySourceState(new RaySourceState(IsOn: isOn, Voltage: current.Voltage, Power: current.Power)); ``` -同时在 `Dispose()` 中取消订阅,并在 `AppStateService` 的字段和 `Dispose` 方法中补充对应的 `SubscriptionToken`。 +**功率计算**:`Power(W) = ActualVoltage(kV) × ActualCurrent(μA) / 1000` + +**断连重置**:PVI 变量断开时将 `RaySourceState` 重置为 `Default(false, 0, 0)`,防止 UI 显示过期数据。 + +**Dispose 时取消订阅**:三个 token 均在 `Dispose()` 中取消。 --- -## 三、探测器 — 断链(待补全) +## 三、探测器 — ✅ 已打通 -### 3.1 现状 +### 3.1 当前状态 -与射线源情况相同,`AppStateService` 没有订阅任何探测器事件,`DetectorState` 是静态默认值。 +`AppStateService` 已在构造时订阅 `StatusChangedEvent` 和 `ImageCapturedEvent`,探测器状态和最新帧均自动同步。 ``` -XP.Hardware.Detector 发布的事件(均无人在 AppState 层订阅): - ├─ StatusChangedEvent ← 探测器连接 / 采集状态变化 - ├─ ImageCapturedEvent ← 图像采集完成 - ├─ CorrectionCompletedEvent ← 校正完成 - └─ ErrorOccurredEvent ← 错误发生 +XP.Hardware.Detector 发布的事件(AppState 层订阅情况): + ├─ StatusChangedEvent ← ✅ 已订阅,映射为 DetectorState + ├─ ImageCapturedEvent ← ✅ 已订阅,缓存为 LatestDetectorFrame(volatile) + ├─ CorrectionCompletedEvent ← 未订阅(无需同步到 AppState) + └─ ErrorOccurredEvent ← 未订阅(无需同步到 AppState) ``` -### 3.2 修复方案 +### 3.2 数据流 -在 `AppStateService` 构造时订阅 `StatusChangedEvent`: +``` +探测器硬件 + └─ IDetectorService(XP.Hardware.Detector) + ├─ 发布 StatusChangedEvent(DetectorStatus 枚举) + │ └─ AppStateService.OnDetectorStatusChanged() [BackgroundThread] + │ ├─ 映射 DetectorStatus → DetectorState(IsConnected, IsAcquiring, ...) + │ ├─ UpdateDetectorState() + │ │ └─ 触发 DetectorStateChanged 事件 → ViewModel 刷新 UI + │ └─ 若从已连接变为断开:发布 DetectorDisconnectedEvent + │ ├─ CncExecutionService 订阅 → 取消当前 CNC 执行 + │ └─ ViewportPanelViewModel 订阅 → 弹出断连警告对话框 + │ + └─ 发布 ImageCapturedEvent(ImageCapturedEventArgs) + └─ AppStateService.OnDetectorImageCaptured() [BackgroundThread] + └─ volatile 写 _latestDetectorFrame + └─ 上层通过 LatestDetectorFrame 属性按需读取(CNC 执行、图像处理等) +``` +### 3.3 关键实现细节 + +**订阅均在后台线程**,避免阻塞采集链路: ```csharp -_detectorStatusToken = _eventAggregator - .GetEvent() - .Subscribe(OnDetectorStatusChanged); +_detectorStatusChangedToken = _eventAggregator + .GetEvent() + .Subscribe(OnDetectorStatusChanged, ThreadOption.BackgroundThread); -private void OnDetectorStatusChanged(DetectorStatus status) +_detectorImageCapturedToken = _eventAggregator + .GetEvent() + .Subscribe(OnDetectorImageCaptured, ThreadOption.BackgroundThread); +``` + +**断连检测**:`OnDetectorStatusChanged` 在更新状态前记录 `wasConnected`,状态更新后若检测到从已连接变为断开,则发布 `DetectorDisconnectedEvent`: +```csharp +bool wasConnected = _detectorState?.IsConnected ?? false; +// ... 更新状态 ... +if (wasConnected && !isConnected) { - if (_disposed) return; - UpdateDetectorState(new DetectorState( - IsConnected: status.IsConnected, - IsAcquiring: status.IsAcquiring, - FrameRate: status.FrameRate, - Resolution: status.Resolution)); + _eventAggregator.GetEvent().Publish(); + _logger.Warn("探测器已断连,发布 DetectorDisconnectedEvent"); } ``` +**最新帧缓存**:`LatestDetectorFrame` 为 `volatile` 字段,任意线程可安全读取,无需加锁: +```csharp +private volatile ImageCapturedEventArgs _latestDetectorFrame; +public ImageCapturedEventArgs LatestDetectorFrame => _latestDetectorFrame; +``` + +**Dispose 时取消订阅**: +```csharp +_eventAggregator.GetEvent().Unsubscribe(_detectorStatusChangedToken); +_eventAggregator.GetEvent().Unsubscribe(_detectorImageCapturedToken); +``` + +### 3.4 DetectorDisconnectedEvent 下游链路 + +`DetectorDisconnectedEvent` 是无载荷 Prism 事件,定义于 `XplorePlane/Events/DetectorDisconnectedEvent.cs`: + +| 订阅方 | 线程选项 | 行为 | +|--------|---------|------| +| `CncExecutionService` | `BackgroundThread` | 取消 `_executionCts`,中止当前 CNC 执行 | +| `ViewportPanelViewModel` | `UIThread` | 若 CNC 正在运行,弹出 `MessageBox` 警告 | + --- -## 四、相机 — 独立直连,不经过 AppState +## 四、相机 — ✅ 已打通(连接状态同步) -### 4.1 现状 +### 4.1 当前状态 -相机完全绕过 `AppStateService`,由 `NavigationPropertyPanelViewModel` 直接持有 `ICamera` 引用并订阅硬件事件: +相机图像流数据量大、帧率高,不适合经过 AppState 的不可变 record 替换机制,因此相机的图像帧仍由 `NavigationPropertyPanelViewModel` 直接持有并渲染。但**连接状态和采集状态**现已通过 `UpdateCameraState()` 同步到 `AppStateService`,供其他模块查询。 ``` -NavigationPropertyPanelViewModel - └─ 直接持有 ICamera 引用 - ├─ _camera.ImageGrabbed += OnCameraImageGrabbed - ├─ _camera.GrabError += OnCameraGrabError - └─ _camera.ConnectionLost += OnCameraConnectionLost - └─ IsCameraConnected 属性直接驱动 UI 命令可用性 +ICameraController 事件(NavigationPropertyPanelViewModel 直接订阅): + ├─ ImageGrabbed ← 直接渲染到 CameraImageSource(不经过 AppState,避免高频刷新) + ├─ GrabError ← 更新 CameraStatusText + └─ ConnectionLost ← 更新 IsCameraConnected + 同步 CameraState ✅ ``` -`AppStateService.CameraState` 字段目前是空壳,没有任何地方向它写入真实数据。 +### 4.2 数据流 -### 4.2 架构说明 +``` +ICameraController(XP.Camera) + └─ NavigationPropertyPanelViewModel(直接持有 _camera) + ├─ ConnectCamera() / OnCameraReady() + │ └─ IsCameraConnected = true → SyncCameraStateToAppState() + │ └─ AppStateService.UpdateCameraState(IsConnected=true, IsStreaming=false, ...) + │ + ├─ DisconnectCamera() + │ └─ IsCameraConnected = false → SyncCameraStateToAppState() + │ └─ AppStateService.UpdateCameraState(IsConnected=false, IsStreaming=false, ...) + │ + ├─ StartGrab() / StopGrab() + │ └─ IsCameraGrabbing = true/false → SyncCameraStateToAppState() + │ └─ AppStateService.UpdateCameraState(IsStreaming=true/false, ...) + │ + └─ OnCameraConnectionLost() + └─ IsCameraConnected = false → SyncCameraStateToAppState() + └─ AppStateService.UpdateCameraState(IsConnected=false, IsStreaming=false, ...) +``` -相机的独立直连是有意为之的设计——相机图像流数据量大、帧率高,不适合经过 AppState 的不可变 record 替换机制。当前图像通过 `ManualImageLoadedEvent` 在模块间传递,与状态管理解耦。 +### 4.3 关键实现细节 -如需将相机连接状态纳入 AppState 统一管理(例如供其他模块查询),可在 `OnCameraConnectionLost` / `ConnectCamera` 中补充调用 `_appStateService.UpdateCameraState()`,仅同步连接状态,不同步图像帧。 +`SyncCameraStateToAppState()` 是统一的同步入口,在所有状态变更节点调用: + +```csharp +private void SyncCameraStateToAppState() +{ + _appStateService.UpdateCameraState(new CameraState( + IsConnected: IsCameraConnected, + IsStreaming: IsCameraGrabbing, + CurrentFrame: null, // 帧数据不经过 AppState,避免高频触发 UI 刷新 + Width: IsCameraConnected ? ImageWidth : 0, + Height: IsCameraConnected ? ImageHeight : 0, + FrameRate: 0)); +} +``` + +**设计决策**:`CurrentFrame` 始终为 null,帧数据仍由 ViewModel 直接持有。`CameraState` 只承载连接/采集状态和图像尺寸,供其他模块(如 CNC 执行、状态栏)查询相机是否可用。 + +**DI 注册**:`NavigationPropertyPanelViewModel` 已通过 `RegisterSingleton()` 注册,DryIoc 自动解析新增的 `IAppStateService` 构造函数参数,无需修改注册代码。 --- @@ -385,32 +470,40 @@ NavigationPropertyPanelViewModel │ PLC ──→ MotionControlService ──→ GeometryUpdatedEvent ─────┐ │ │ └──→ AxisStatusChangedEvent ────┤ │ │ │ │ -│ RaySource ──→ IRaySourceService ──→ StatusUpdatedEvent ─ ─ ┤ │ +│ RaySource ──→ IRaySourceService ──→ StatusUpdatedEvent ────┤ │ │ └──→ RaySourceStatusChangedEvent ─┤│ +│ └──→ VariablesConnectedEvent ────┤│ │ │ │ -│ Detector ──→ IDetectorService ──→ StatusChangedEvent ─ ─ ┤ │ +│ Detector ──→ IDetectorService ──→ StatusChangedEvent ────┤ │ +│ └──→ ImageCapturedEvent ────┤ │ │ │ │ -│ Camera ──→ ICamera ──→ ImageGrabbed / ConnectionLost ─ ─ ┘ │ +│ Camera ──→ ICameraController ──→ ConnectionLost / Grab ────┘ │ +│ (via SyncCameraStateToAppState) │ └─────────────────────────────────────────────────────────────────────┘ - │ 已接通 ✅ │ 断链 ⚠️ - ↓ ↓ + │ 全部已接通 ✅ + ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ AppStateService │ │ │ -│ MotionState ← 自动同步(轮询 + 事件驱动) ✅ │ -│ RaySourceState ← 需手动推送 / 待补订阅 ⚠️ │ -│ DetectorState ← 需手动推送 / 待补订阅 ⚠️ │ -│ CameraState ← 空壳,相机走独立直连路径 ⚠️ │ +│ MotionState ← 自动同步(轮询 + 事件驱动) ✅ │ +│ RaySourceState ← 自动同步(StatusUpdatedEvent 等) ✅ │ +│ DetectorState ← 自动同步(StatusChangedEvent) ✅ │ +│ LatestDetectorFrame ← 自动缓存(ImageCapturedEvent) ✅ │ +│ CameraState ← 连接/采集状态同步(帧数据不经过) ✅ │ └─────────────────────────────────────────────────────────────────────┘ │ ↓ PropertyChanged / StateChangedEvent ┌─────────────────────────────────────────────────────────────────────┐ │ ViewModel 层 │ │ │ -│ MotionControlViewModel ← 同时订阅 GeometryUpdatedEvent(双路) │ -│ CncNodeViewModel ← 通过 IAppStateService 读取快照 │ -│ CncEditorViewModel ← 通过 IAppStateService 读取快照 │ -│ NavigationPropertyPanelViewModel ← 直连 ICamera(独立路径) │ +│ MotionControlViewModel ← 同时订阅 GeometryUpdatedEvent(双路) │ +│ CncNodeViewModel ← 通过 IAppStateService 读取快照 │ +│ CncEditorViewModel ← 通过 IAppStateService 读取快照 │ +│ ViewportPanelViewModel ← 订阅 DetectorStateChanged │ +│ 订阅 DetectorDisconnectedEvent │ +│ CncExecutionService ← 订阅 DetectorDisconnectedEvent │ +│ NavigationPropertyPanelViewModel ← 直连 ICameraController │ +│ + 推送 CameraState 到 AppState │ └─────────────────────────────────────────────────────────────────────┘ ``` @@ -419,12 +512,14 @@ NavigationPropertyPanelViewModel ## 六、待办事项 -| 优先级 | 项目 | 涉及文件 | -|--------|------|---------| -| 高 | 补充射线源事件订阅,打通 `RaySourceState` 自动同步 | `AppStateService.cs` | -| 高 | 补充探测器事件订阅,打通 `DetectorState` 自动同步 | `AppStateService.cs` | -| 中 | 确认 `StartPolling()` 在应用启动流程中被调用 | `App.xaml.cs` / `AppBootstrapper` | -| 低 | 评估是否将相机连接状态纳入 `CameraState` 统一管理 | `NavigationPropertyPanelViewModel.cs`、`AppStateService.cs` | +| 优先级 | 项目 | 涉及文件 | 状态 | +|--------|------|---------|------| +| 已完成 | 射线源事件订阅,打通 `RaySourceState` 自动同步 | `AppStateService.cs` | ✅ 已完成 | +| 已完成 | 探测器事件订阅,打通 `DetectorState` 自动同步 + `LatestDetectorFrame` 缓存 | `AppStateService.cs` | ✅ 已完成 | +| 已完成 | 探测器断连事件(`DetectorDisconnectedEvent`)下游链路 | `CncExecutionService.cs`、`ViewportPanelViewModel.cs` | ✅ 已完成 | +| 已完成 | 手动模式下实时按钮切换回 `LiveDetector` 模式 | `MainViewportService.cs` | ✅ 已完成 | +| 中 | 确认 `StartPolling()` 在应用启动流程中被调用 | `App.xaml.cs` / `AppBootstrapper` | ✅ 已确认 | +| 低 | 评估是否将相机连接状态纳入 `CameraState` 统一管理 | `NavigationPropertyPanelViewModel.cs`、`AppStateService.cs` | ✅ 已完成 | diff --git a/XplorePlane/Services/AppState/AppStateService.cs b/XplorePlane/Services/AppState/AppStateService.cs index 26fe1e8..96c3a7b 100644 --- a/XplorePlane/Services/AppState/AppStateService.cs +++ b/XplorePlane/Services/AppState/AppStateService.cs @@ -13,6 +13,8 @@ using XP.Hardware.MotionControl.Abstractions; using XP.Hardware.MotionControl.Abstractions.Enums; 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 XplorePlane.Models; @@ -38,6 +40,9 @@ namespace XplorePlane.Services.AppState private readonly SubscriptionToken _geometryUpdatedToken; private readonly SubscriptionToken _detectorStatusChangedToken; private readonly SubscriptionToken _detectorImageCapturedToken; + private readonly SubscriptionToken _raySourceStatusUpdatedToken; + private readonly SubscriptionToken _raySourceStatusChangedToken; + private readonly SubscriptionToken _raySourceVariablesConnectedToken; private bool _disposed; private GeometryData _latestGeometry; @@ -123,6 +128,19 @@ namespace XplorePlane.Services.AppState .GetEvent() .Subscribe(OnDetectorImageCaptured, ThreadOption.BackgroundThread); + // ── 射线源状态事件订阅(后台线程)── + _raySourceStatusUpdatedToken = _eventAggregator + .GetEvent() + .Subscribe(OnRaySourceStatusUpdated, ThreadOption.BackgroundThread); + + _raySourceStatusChangedToken = _eventAggregator + .GetEvent() + .Subscribe(OnRaySourceStatusChanged, ThreadOption.BackgroundThread); + + _raySourceVariablesConnectedToken = _eventAggregator + .GetEvent() + .Subscribe(OnRaySourceVariablesConnected, ThreadOption.BackgroundThread); + SubscribeToExistingServices(); _logger.Info("AppStateService initialized"); } @@ -302,6 +320,21 @@ namespace XplorePlane.Services.AppState _eventAggregator.GetEvent().Unsubscribe(_detectorImageCapturedToken); } + if (_raySourceStatusUpdatedToken is not null) + { + _eventAggregator.GetEvent().Unsubscribe(_raySourceStatusUpdatedToken); + } + + if (_raySourceStatusChangedToken is not null) + { + _eventAggregator.GetEvent().Unsubscribe(_raySourceStatusChangedToken); + } + + if (_raySourceVariablesConnectedToken is not null) + { + _eventAggregator.GetEvent().Unsubscribe(_raySourceVariablesConnectedToken); + } + MotionStateChanged = null; RaySourceStateChanged = null; DetectorStateChanged = null; @@ -420,6 +453,66 @@ namespace XplorePlane.Services.AppState _latestDetectorFrame = args; } + /// + /// 射线源全量状态更新回调(主路径)。 + /// StatusUpdatedEvent 携带实际电压、电流和开关状态,是 RaySourceState 的主要数据来源。 + /// Power(W) = ActualVoltage(kV) × ActualCurrent(μA) / 1000 + /// + private void OnRaySourceStatusUpdated(SystemStatusData data) + { + if (_disposed || data == null) return; + + double power = data.ActualVoltage * data.ActualCurrent / 1000.0; + UpdateRaySourceState(new RaySourceState( + IsOn: data.IsXRayOn, + Voltage: data.ActualVoltage, + Power: power)); + } + + /// + /// 射线源三态状态变更回调(补充路径)。 + /// RaySourceStatusChangedEvent 仅携带枚举状态,用于在 StatusUpdatedEvent 尚未到达时 + /// 快速同步 IsOn 字段,电压和功率保持当前值不变。 + /// + private void OnRaySourceStatusChanged(RaySourceStatus status) + { + if (_disposed) return; + + bool isOn = status == RaySourceStatus.Opened; + var current = _raySourceState; + + // 仅当 IsOn 状态发生变化时才更新,避免覆盖 StatusUpdatedEvent 写入的精确数据 + if (current.IsOn == isOn) return; + + UpdateRaySourceState(new RaySourceState( + IsOn: isOn, + Voltage: current.Voltage, + Power: current.Power)); + + _logger.Info( + "射线源状态变更:{Status} → IsOn={IsOn} | RaySource status changed: {Status} → IsOn={IsOn}", + status, isOn); + } + + /// + /// 射线源 PVI 变量连接状态变更回调。 + /// 断开时将 RaySourceState 重置为默认值(IsOn=false, Voltage=0, Power=0)。 + /// + private void OnRaySourceVariablesConnected(bool isConnected) + { + 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"); + } + } + private bool TryRefreshMotionStateFromHardware(string reason) { try diff --git a/XplorePlane/ViewModels/Main/NavigationPropertyPanelViewModel.cs b/XplorePlane/ViewModels/Main/NavigationPropertyPanelViewModel.cs index 6253851..8785474 100644 --- a/XplorePlane/ViewModels/Main/NavigationPropertyPanelViewModel.cs +++ b/XplorePlane/ViewModels/Main/NavigationPropertyPanelViewModel.cs @@ -6,6 +6,8 @@ using System.Collections.ObjectModel; using System.Windows; using System.Windows.Media.Imaging; using XP.Camera; +using XplorePlane.Models; +using XplorePlane.Services.AppState; namespace XplorePlane.ViewModels { @@ -16,6 +18,7 @@ namespace XplorePlane.ViewModels { private static readonly ILogger _logger = Log.ForContext(); private readonly ICameraController _camera; + private readonly IAppStateService _appStateService; private volatile bool _liveViewRunning; private bool _disposed; @@ -161,9 +164,10 @@ namespace XplorePlane.ViewModels #endregion Commands - public NavigationPropertyPanelViewModel(ICameraController camera) + public NavigationPropertyPanelViewModel(ICameraController camera, IAppStateService appStateService) { _camera = camera ?? throw new ArgumentNullException(nameof(camera)); + _appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService)); ConnectCameraCommand = new DelegateCommand(ConnectCamera, () => !IsCameraConnected); DisconnectCameraCommand = new DelegateCommand(DisconnectCamera, () => IsCameraConnected); @@ -198,6 +202,7 @@ namespace XplorePlane.ViewModels IsCameraConnected = true; CameraStatusText = "已连接"; RefreshCameraParams(); + SyncCameraStateToAppState(); StartGrab(); IsLiveViewEnabled = true; } @@ -217,12 +222,14 @@ namespace XplorePlane.ViewModels CameraStatusText = $"已连接: {info.ModelName} (SN: {info.SerialNumber})"; _logger.Information("Camera connected: {ModelName}", info.ModelName); RefreshCameraParams(); + SyncCameraStateToAppState(); } catch (Exception ex) { _logger.Error(ex, "Failed to connect camera"); CameraStatusText = $"连接失败: {ex.Message}"; IsCameraConnected = false; + SyncCameraStateToAppState(); } } @@ -246,6 +253,7 @@ namespace XplorePlane.ViewModels IsCameraGrabbing = false; CameraStatusText = "未连接"; CameraImageSource = null; + SyncCameraStateToAppState(); _logger.Information("Camera disconnected"); } } @@ -257,6 +265,7 @@ namespace XplorePlane.ViewModels _camera.StartGrabbing(); IsCameraGrabbing = true; CameraStatusText = "采集中..."; + SyncCameraStateToAppState(); // 如果已勾选实时,自动启动 Live View if (IsLiveViewEnabled) @@ -279,6 +288,7 @@ namespace XplorePlane.ViewModels _camera.StopGrabbing(); IsCameraGrabbing = false; CameraStatusText = "已停止采集"; + SyncCameraStateToAppState(); } catch (Exception ex) { @@ -402,11 +412,33 @@ namespace XplorePlane.ViewModels IsCameraGrabbing = false; CameraStatusText = "连接已断开"; CameraImageSource = null; + SyncCameraStateToAppState(); }); } #endregion Camera Event Handlers + #region AppState Sync + + /// + /// 将当前相机连接/采集状态同步到 AppStateService.CameraState。 + /// 仅同步连接状态、采集状态和图像尺寸,不同步帧数据(避免高频触发 UI 刷新)。 + /// + private void SyncCameraStateToAppState() + { + if (_appStateService == null) return; + + _appStateService.UpdateCameraState(new CameraState( + IsConnected: IsCameraConnected, + IsStreaming: IsCameraGrabbing, + CurrentFrame: null, + Width: IsCameraConnected ? ImageWidth : 0, + Height: IsCameraConnected ? ImageHeight : 0, + FrameRate: 0)); + } + + #endregion AppState Sync + #region IDisposable public void Dispose()