打通射线源与相机的断链问题
This commit is contained in:
@@ -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)
|
||||||
|
└─ 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.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 ← ✅ 已订阅,缓存为 LatestDetectorFrame(volatile)
|
||||||
├─ CorrectionCompletedEvent ← 校正完成
|
├─ CorrectionCompletedEvent ← 未订阅(无需同步到 AppState)
|
||||||
└─ ErrorOccurredEvent ← 错误发生
|
└─ 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
|
```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 架构说明
|
```
|
||||||
|
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,22 +470,26 @@ 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
|
||||||
@@ -410,7 +499,11 @@ NavigationPropertyPanelViewModel
|
|||||||
│ 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()
|
||||||
|
|||||||
Reference in New Issue
Block a user