# 硬件层及 UI 集成技术路线 ## 整体架构 采用 **DLL 直接引用 + Prism DI 容器手动注册 + AutoWireViewModel 自动装配** 的集成方式。 DLL 提供完整的 MVVM 三层(View / ViewModel / Service),主项目负责 DI 注册和 XAML 布局嵌入,数据通过注入的服务接口和 Prism EventAggregator 在两侧流动。 --- ## 分层说明 ### 1. DLL 引用层 `XP.Hardware.RaySource.dll` 放置于 `Libs/Hardware/` 目录,通过 `.csproj` 的 `` 引用。 DLL 内部包含: | 类型 | 名称 | 说明 | |------|------|------| | UserControl | `RaySourceOperateView` | 射线源操作界面 | | ViewModel | `RaySourceOperateViewModel` | 对应 ViewModel | | 服务接口/实现 | `IRaySourceService` / `RaySourceService` | 射线源业务逻辑 | | 工厂 | `IRaySourceFactory` / `RaySourceFactory` | 策略工厂,支持 Comet 160kv、Hamamatsu160kv | | 服务 | `IFilamentLifetimeService` | 灯丝寿命管理 | | 配置模型 | `RaySourceConfig` | 从 App.config 加载的配置 | --- ### 2. DI 注册层(App.xaml.cs → AppBootstrapper) 主项目在 `RegisterTypes()` 中**手动注册** DLL 内所有服务,未走 Prism 的 `ConfigureModuleCatalog` 自动模块加载,目的是避免模块加载顺序问题,确保 DryIoc 容器在 Shell 创建前已具备所有依赖。 ```csharp // 注册 ViewModel(供 ViewModelLocator 自动装配) containerRegistry.Register(); // 注册配置(从 App.config 读取 RaySource:xxx 键值) var raySourceConfig = XP.Hardware.RaySource.Config.ConfigLoader.LoadConfig(); containerRegistry.RegisterInstance(raySourceConfig); // 注册核心服务(全部单例) containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); ``` --- ### 3. XAML 嵌入层(MainWindow.xaml) 通过 XML 命名空间直接引用 DLL 中的 View: ```xml xmlns:views1="clr-namespace:XP.Hardware.RaySource.Views;assembly=XP.Hardware.RaySource" ``` 在主窗口右侧面板顶部(Grid.Row="0",固定高度 250px)放置控件: ```xml ``` 控件内部已设置 `prism:ViewModelLocator.AutoWireViewModel="True"`,Prism 按命名约定自动从 DI 容器解析 `RaySourceOperateViewModel` 并绑定为 DataContext。 --- ### 4. 数据传递路线 数据流分四条路径: **路径 A:配置数据(启动时,单向下行)** ``` App.config (RaySource:xxx 键值) → ConfigLoader.LoadConfig() → RaySourceConfig 实例 → 注入到 RaySourceService / RaySourceOperateViewModel ``` App.config 中的关键配置项: ```xml ``` **路径 B:用户操作(UI → DLL 服务层)** ``` RaySourceOperateView(按钮点击) → RaySourceOperateViewModel(Command 绑定) → IRaySourceService.SetVoltageAsync() / TurnOnAsync() / ... → IXRaySource(具体策略实现,如 Comet225) → 硬件通讯(B&R PVI / BR.AN.PviServices.dll) ``` **路径 C:状态回传(DLL 服务层 → UI)** ``` 硬件状态轮询(StatusPollingInterval = 500ms) → RaySourceService 内部更新 → RaySourceOperateViewModel 属性变更(INotifyPropertyChanged) → RaySourceOperateView 数据绑定自动刷新 同时: → AppStateService 订阅 IRaySourceService 事件 → 更新 RaySourceState(IsOn, Voltage, Power) → Dispatcher.BeginInvoke 调度到 UI 线程 → 其他 ViewModel 通过 IAppStateService 读取全局射线源状态 ``` `RaySourceState` 为不可变 record,定义于 `Models/StateModels.cs`: ```csharp public record RaySourceState( bool IsOn, // 开关状态 double Voltage, // 电压 (kV) double Power // 功率 (W) ) { public static readonly RaySourceState Default = new(false, 0, 0); } ``` **路径 D:跨模块事件通讯(Prism EventAggregator)** ``` DLL 内部发布事件(XP.Hardware.RaySource.Abstractions.Events): XrayStateChangedEvent — 射线开关状态变化 StatusUpdatedEvent — 实时电压/电流数据 ErrorOccurredEvent — 错误通知 OperationResultEvent — 操作结果回调 主项目任意 ViewModel 订阅示例: _eventAggregator.GetEvent().Subscribe(data => { ... }); ``` --- ### 5. 完整依赖关系 ``` RaySourceOperateView(DLL 中的 UserControl) └─ AutoWire → RaySourceOperateViewModel(DLL,主项目注册到 DI) ├─ IRaySourceService ← 单例,DLL 实现 ├─ RaySourceConfig ← App.config 加载 ├─ IFilamentLifetimeService ← 单例,DLL 实现 ├─ IEventAggregator ← Prism 内置 ├─ ILoggerService ← 主项目 LoggerServiceAdapter 适配 Serilog └─ ILocalizationService ← 主项目注册,DLL 消费 ``` --- ### 6. 资源释放 `App.OnExit()` 中显式从容器解析并释放资源: ```csharp var appStateService = bootstrapper.Container.Resolve(); appStateService?.Dispose(); var raySourceService = bootstrapper.Container.Resolve(); raySourceService?.Dispose(); ``` 确保硬件连接在应用退出时正确断开。 ### 7. --- # 硬件层 → AppState → UI 状态同步机制 ## 整体结论 | 硬件子系统 | 同步方式 | AppState 订阅 | 状态是否自动同步 | |-----------|---------|--------------|----------------| | 运动控制(Motion) | 事件驱动 + 轮询 | ✅ 已订阅 `GeometryUpdatedEvent` / `AxisStatusChangedEvent` | ✅ 自动同步 | | 射线源(RaySource) | 事件驱动 | ✅ 已订阅 `StatusUpdatedEvent` / `RaySourceStatusChangedEvent` / `VariablesConnectedEvent` | ✅ 自动同步 | | 探测器(Detector) | 事件驱动 | ✅ 已订阅 `StatusChangedEvent` / `ImageCapturedEvent` | ✅ 自动同步 | | 相机(Camera) | ViewModel 直连 + AppState 同步 | ✅ 连接/采集状态通过 `SyncCameraStateToAppState()` 推送 | ✅ 连接状态自动同步(帧数据不经过 AppState) | --- ## 一、运动控制 — 完整链路 ### 1.1 数据流 ``` PLC 硬件(B&R) └─ IPlcService.IsConnected(轮询前置检查) └─ MotionControlService.OnPollingTick() [System.Threading.Timer,周期 = PollingInterval ms] ├─ _motionSystem.UpdateAllStatus() ← 从 PLC 读取所有轴实际位置 ├─ GetCurrentGeometry() ← 正算 FOD / FDD / Magnification ├─ 发布 GeometryUpdatedEvent ─┐ └─ 发布 AxisStatusChangedEvent ─┤ ↓ AppStateService(构造时订阅两个事件) ├─ OnGeometryUpdated() └─ OnAxisStatusChanged() └─ TryRefreshMotionStateFromHardware() └─ BuildMotionStateSnapshot() ├─ 读取所有轴 ActualPosition / ActualAngle └─ SetMotionState() └─ 触发 MotionStateChanged 事件 └─ ViewModel 绑定自动刷新 UI ``` ### 1.2 关键实现细节 **轮询启动**:`MotionControlService.StartPolling()` 需在应用启动时显式调用,否则轮询不会运行。 **PLC 未连接时的保护**: ```csharp // MotionControlService.OnPollingTick() if (!_plcService.IsConnected) return; // 直接跳过,不报错 ``` **连续错误降频**: ```csharp if (_pollErrorCount > 3) { if (++_pollErrorCount % 50 != 0) return; // 每 50 次才尝试一次,防止日志刷屏 } ``` **AppStateService 初始化时的首次刷新**: ```csharp // AppStateService 构造函数末尾 private void SubscribeToExistingServices() { if (TryRefreshMotionStateFromHardware("initialization")) _logger.Info("AppStateService subscribed to motion hardware state"); else _logger.Warn("AppStateService could not initialize motion state from hardware"); } ``` PLC 未连接时 warn 但不崩溃,等待后续轮询事件触发再同步。 **`UpdateMotionState()` 的特殊行为**: ```csharp public void UpdateMotionState(MotionState newState) { // 优先从硬件层拉取最新快照,忽略外部传入值 if (TryRefreshMotionStateFromHardware("UpdateMotionState")) return; // 硬件不可用时才使用传入值(降级路径) SetMotionState(newState); } ``` 硬件连接后,外部调用 `UpdateMotionState()` 实际上会被硬件快照覆盖,硬件层始终是 `MotionState` 的唯一真实来源。 ### 1.3 事件清单 | 事件 | 发布方 | 订阅方 | 触发时机 | |------|--------|--------|---------| | `GeometryUpdatedEvent` | `MotionControlService` | `AppStateService`、`MotionControlViewModel`、`AxisControlViewModel` | 每次轮询 tick | | `AxisStatusChangedEvent` | `MotionControlService` | `AppStateService`、`MotionControlViewModel`、`AxisControlViewModel` | 轴状态发生变化时 | | `MotionErrorEvent` | `MotionControlService` | — | 轴状态变为 Error / Alarm | | `DoorStatusChangedEvent` | `MotionControlService` | `MotionControlViewModel` | 安全门状态变化 | | `DoorInterlockChangedEvent` | `MotionControlService` | — | 联锁状态变化 | | `GeometryApplyRequestEvent` | DebugWindow | `MotionControlViewModel` | 调试窗口发起几何反算请求 | --- ## 二、射线源 — ✅ 已打通 ### 2.1 当前状态 `AppStateService` 已在构造时订阅 `StatusUpdatedEvent`、`RaySourceStatusChangedEvent` 和 `VariablesConnectedEvent`,`RaySourceState` 自动同步。 ``` XP.Hardware.RaySource 发布的事件(AppState 层订阅情况): ├─ StatusUpdatedEvent ← ✅ 已订阅(主路径),映射为 RaySourceState ├─ RaySourceStatusChangedEvent ← ✅ 已订阅(补充路径),快速同步 IsOn 状态 ├─ VariablesConnectedEvent ← ✅ 已订阅,断开时重置 RaySourceState 为默认值 └─ OperationResultEvent ← 未订阅(操作结果由 ViewModel 层直接处理) ``` ### 2.2 数据流 ``` 射线源硬件(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) ``` ### 2.3 关键实现细节 **双路径设计**:`StatusUpdatedEvent` 是主路径,携带完整的电压、电流、开关状态;`RaySourceStatusChangedEvent` 是补充路径,仅在 `StatusUpdatedEvent` 尚未到达时快速同步 `IsOn` 字段,避免覆盖精确数据: ```csharp // OnRaySourceStatusChanged — 仅当 IsOn 状态发生变化时才更新 if (current.IsOn == isOn) return; UpdateRaySourceState(new RaySourceState(IsOn: isOn, Voltage: current.Voltage, Power: current.Power)); ``` **功率计算**:`Power(W) = ActualVoltage(kV) × ActualCurrent(μA) / 1000` **断连重置**:PVI 变量断开时将 `RaySourceState` 重置为 `Default(false, 0, 0)`,防止 UI 显示过期数据。 **Dispose 时取消订阅**:三个 token 均在 `Dispose()` 中取消。 --- ## 三、探测器 — ✅ 已打通 ### 3.1 当前状态 `AppStateService` 已在构造时订阅 `StatusChangedEvent` 和 `ImageCapturedEvent`,探测器状态和最新帧均自动同步。 ``` XP.Hardware.Detector 发布的事件(AppState 层订阅情况): ├─ StatusChangedEvent ← ✅ 已订阅,映射为 DetectorState ├─ ImageCapturedEvent ← ✅ 已订阅,缓存为 LatestDetectorFrame(volatile) ├─ CorrectionCompletedEvent ← 未订阅(无需同步到 AppState) └─ ErrorOccurredEvent ← 未订阅(无需同步到 AppState) ``` ### 3.2 数据流 ``` 探测器硬件 └─ 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 _detectorStatusChangedToken = _eventAggregator .GetEvent() .Subscribe(OnDetectorStatusChanged, ThreadOption.BackgroundThread); _detectorImageCapturedToken = _eventAggregator .GetEvent() .Subscribe(OnDetectorImageCaptured, ThreadOption.BackgroundThread); ``` **断连检测**:`OnDetectorStatusChanged` 在更新状态前记录 `wasConnected`,状态更新后若检测到从已连接变为断开,则发布 `DetectorDisconnectedEvent`: ```csharp bool wasConnected = _detectorState?.IsConnected ?? false; // ... 更新状态 ... if (wasConnected && !isConnected) { _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` 警告 | --- ## 四、相机 — ✅ 已打通(连接状态同步) ### 4.1 当前状态 相机图像流数据量大、帧率高,不适合经过 AppState 的不可变 record 替换机制,因此相机的图像帧仍由 `NavigationPropertyPanelViewModel` 直接持有并渲染。但**连接状态和采集状态**现已通过 `UpdateCameraState()` 同步到 `AppStateService`,供其他模块查询。 ``` ICameraController 事件(NavigationPropertyPanelViewModel 直接订阅): ├─ ImageGrabbed ← 直接渲染到 CameraImageSource(不经过 AppState,避免高频刷新) ├─ GrabError ← 更新 CameraStatusText └─ ConnectionLost ← 更新 IsCameraConnected + 同步 CameraState ✅ ``` ### 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, ...) ``` ### 4.3 关键实现细节 `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` 构造函数参数,无需修改注册代码。 --- ## 五、状态同步全景图 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ 硬件层 │ │ │ │ PLC ──→ MotionControlService ──→ GeometryUpdatedEvent ─────┐ │ │ └──→ AxisStatusChangedEvent ────┤ │ │ │ │ │ RaySource ──→ IRaySourceService ──→ StatusUpdatedEvent ────┤ │ │ └──→ RaySourceStatusChangedEvent ─┤│ │ └──→ VariablesConnectedEvent ────┤│ │ │ │ │ Detector ──→ IDetectorService ──→ StatusChangedEvent ────┤ │ │ └──→ ImageCapturedEvent ────┤ │ │ │ │ │ Camera ──→ ICameraController ──→ ConnectionLost / Grab ────┘ │ │ (via SyncCameraStateToAppState) │ └─────────────────────────────────────────────────────────────────────┘ │ 全部已接通 ✅ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ AppStateService │ │ │ │ MotionState ← 自动同步(轮询 + 事件驱动) ✅ │ │ RaySourceState ← 自动同步(StatusUpdatedEvent 等) ✅ │ │ DetectorState ← 自动同步(StatusChangedEvent) ✅ │ │ LatestDetectorFrame ← 自动缓存(ImageCapturedEvent) ✅ │ │ CameraState ← 连接/采集状态同步(帧数据不经过) ✅ │ └─────────────────────────────────────────────────────────────────────┘ │ ↓ PropertyChanged / StateChangedEvent ┌─────────────────────────────────────────────────────────────────────┐ │ ViewModel 层 │ │ │ │ MotionControlViewModel ← 同时订阅 GeometryUpdatedEvent(双路) │ │ CncNodeViewModel ← 通过 IAppStateService 读取快照 │ │ CncEditorViewModel ← 通过 IAppStateService 读取快照 │ │ ViewportPanelViewModel ← 订阅 DetectorStateChanged │ │ 订阅 DetectorDisconnectedEvent │ │ CncExecutionService ← 订阅 DetectorDisconnectedEvent │ │ NavigationPropertyPanelViewModel ← 直连 ICameraController │ │ + 推送 CameraState 到 AppState │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 六、待办事项 | 优先级 | 项目 | 涉及文件 | 状态 | |--------|------|---------|------| | 已完成 | 射线源事件订阅,打通 `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` | ✅ 已完成 |