打通射线源与相机的断链问题
This commit is contained in:
@@ -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<StatusUpdatedEvent>()
|
||||
.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<XP.Hardware.Detector.Abstractions.Events.StatusChangedEvent>()
|
||||
.Subscribe(OnDetectorStatusChanged);
|
||||
_detectorStatusChangedToken = _eventAggregator
|
||||
.GetEvent<StatusChangedEvent>()
|
||||
.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;
|
||||
UpdateDetectorState(new DetectorState(
|
||||
IsConnected: status.IsConnected,
|
||||
IsAcquiring: status.IsAcquiring,
|
||||
FrameRate: status.FrameRate,
|
||||
Resolution: status.Resolution));
|
||||
_eventAggregator.GetEvent<DetectorDisconnectedEvent>().Publish();
|
||||
_logger.Warn("探测器已断连,发布 DetectorDisconnectedEvent");
|
||||
}
|
||||
```
|
||||
|
||||
**最新帧缓存**:`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
|
||||
└─ 直接持有 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<NavigationPropertyPanelViewModel>()` 注册,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` | ✅ 已完成 |
|
||||
|
||||
|
||||
|
||||
@@ -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<ImageCapturedEvent>()
|
||||
.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();
|
||||
_logger.Info("AppStateService initialized");
|
||||
}
|
||||
@@ -302,6 +320,21 @@ namespace XplorePlane.Services.AppState
|
||||
_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;
|
||||
RaySourceStateChanged = null;
|
||||
DetectorStateChanged = null;
|
||||
@@ -420,6 +453,66 @@ namespace XplorePlane.Services.AppState
|
||||
_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)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -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<NavigationPropertyPanelViewModel>();
|
||||
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
|
||||
|
||||
/// <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
|
||||
|
||||
public void Dispose()
|
||||
|
||||
Reference in New Issue
Block a user