打通射线源与相机的断链问题

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