diff --git a/XplorePlane/Doc/硬件层及UI集成技术路线.md b/XplorePlane/Doc/硬件层及UI集成技术路线.md deleted file mode 100644 index ede56ae..0000000 Binary files a/XplorePlane/Doc/硬件层及UI集成技术路线.md and /dev/null differ diff --git a/XplorePlane/Doc/硬件层及UI集成技术路线.txt b/XplorePlane/Doc/硬件层及UI集成技术路线.txt new file mode 100644 index 0000000..6e25384 --- /dev/null +++ b/XplorePlane/Doc/硬件层及UI集成技术路线.txt @@ -0,0 +1,430 @@ +# RaySourceOperateView 集成技术路线 + +## 整体架构 + +采用 **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) | 事件定义存在 | ❌ 未订阅任何事件 | ⚠️ 需手动推送 | +| 探测器(Detector) | 事件定义存在 | ❌ 未订阅任何事件 | ⚠️ 需手动推送 | +| 相机(Camera) | ViewModel 直连 | ❌ 不经过 AppState | ⚠️ `CameraState` 为空壳 | + +--- + +## 一、运动控制 — 完整链路 + +### 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` 持有 `IRaySourceService` 引用,但**没有订阅任何射线源事件**。`RaySourceState` 只能通过外部显式调用 `UpdateRaySourceState()` 手动推送,硬件状态变化不会自动反映到 AppState。 + +``` +XP.Hardware.RaySource 发布的事件(均无人在 AppState 层订阅): + ├─ StatusUpdatedEvent ← 实时电压 / 电流 / 功率数据 + ├─ RaySourceStatusChangedEvent ← 三态状态(On / Off / Fault) + ├─ VariablesConnectedEvent ← PLC 变量连接状态 + └─ OperationResultEvent ← 操作执行结果 +``` + +### 2.2 当前 `RaySourceState` 的写入路径 + +目前只有两处会更新 `RaySourceState`: + +1. `CncProgramService.CreateReferencePointNode()` — 创建参考点时读取一次快照 +2. 任何直接调用 `appStateService.UpdateRaySourceState()` 的地方(目前代码中无此调用) + +### 2.3 修复方案 + +在 `AppStateService` 构造时订阅 `StatusUpdatedEvent`,将实时数据映射到 `RaySourceState`: + +```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)); +} +``` + +同时在 `Dispose()` 中取消订阅,并在 `AppStateService` 的字段和 `Dispose` 方法中补充对应的 `SubscriptionToken`。 + +--- + +## 三、探测器 — 断链(待补全) + +### 3.1 现状 + +与射线源情况相同,`AppStateService` 没有订阅任何探测器事件,`DetectorState` 是静态默认值。 + +``` +XP.Hardware.Detector 发布的事件(均无人在 AppState 层订阅): + ├─ StatusChangedEvent ← 探测器连接 / 采集状态变化 + ├─ ImageCapturedEvent ← 图像采集完成 + ├─ CorrectionCompletedEvent ← 校正完成 + └─ ErrorOccurredEvent ← 错误发生 +``` + +### 3.2 修复方案 + +在 `AppStateService` 构造时订阅 `StatusChangedEvent`: + +```csharp +_detectorStatusToken = _eventAggregator + .GetEvent() + .Subscribe(OnDetectorStatusChanged); + +private void OnDetectorStatusChanged(DetectorStatus status) +{ + if (_disposed) return; + UpdateDetectorState(new DetectorState( + IsConnected: status.IsConnected, + IsAcquiring: status.IsAcquiring, + FrameRate: status.FrameRate, + Resolution: status.Resolution)); +} +``` + +--- + +## 四、相机 — 独立直连,不经过 AppState + +### 4.1 现状 + +相机完全绕过 `AppStateService`,由 `NavigationPropertyPanelViewModel` 直接持有 `ICamera` 引用并订阅硬件事件: + +``` +NavigationPropertyPanelViewModel + └─ 直接持有 ICamera 引用 + ├─ _camera.ImageGrabbed += OnCameraImageGrabbed + ├─ _camera.GrabError += OnCameraGrabError + └─ _camera.ConnectionLost += OnCameraConnectionLost + └─ IsCameraConnected 属性直接驱动 UI 命令可用性 +``` + +`AppStateService.CameraState` 字段目前是空壳,没有任何地方向它写入真实数据。 + +### 4.2 架构说明 + +相机的独立直连是有意为之的设计——相机图像流数据量大、帧率高,不适合经过 AppState 的不可变 record 替换机制。当前图像通过 `ManualImageLoadedEvent` 在模块间传递,与状态管理解耦。 + +如需将相机连接状态纳入 AppState 统一管理(例如供其他模块查询),可在 `OnCameraConnectionLost` / `ConnectCamera` 中补充调用 `_appStateService.UpdateCameraState()`,仅同步连接状态,不同步图像帧。 + +--- + +## 五、状态同步全景图 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 硬件层 │ +│ │ +│ PLC ──→ MotionControlService ──→ GeometryUpdatedEvent ─────┐ │ +│ └──→ AxisStatusChangedEvent ────┤ │ +│ │ │ +│ RaySource ──→ IRaySourceService ──→ StatusUpdatedEvent ─ ─ ┤ │ +│ └──→ RaySourceStatusChangedEvent ─┤│ +│ │ │ +│ Detector ──→ IDetectorService ──→ StatusChangedEvent ─ ─ ┤ │ +│ │ │ +│ Camera ──→ ICamera ──→ ImageGrabbed / ConnectionLost ─ ─ ┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ 已接通 ✅ │ 断链 ⚠️ + ↓ ↓ +┌─────────────────────────────────────────────────────────────────────┐ +│ AppStateService │ +│ │ +│ MotionState ← 自动同步(轮询 + 事件驱动) ✅ │ +│ RaySourceState ← 需手动推送 / 待补订阅 ⚠️ │ +│ DetectorState ← 需手动推送 / 待补订阅 ⚠️ │ +│ CameraState ← 空壳,相机走独立直连路径 ⚠️ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ↓ PropertyChanged / StateChangedEvent +┌─────────────────────────────────────────────────────────────────────┐ +│ ViewModel 层 │ +│ │ +│ MotionControlViewModel ← 同时订阅 GeometryUpdatedEvent(双路) │ +│ CncNodeViewModel ← 通过 IAppStateService 读取快照 │ +│ CncEditorViewModel ← 通过 IAppStateService 读取快照 │ +│ NavigationPropertyPanelViewModel ← 直连 ICamera(独立路径) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + + +## 六、待办事项 + +| 优先级 | 项目 | 涉及文件 | +|--------|------|---------| +| 高 | 补充射线源事件订阅,打通 `RaySourceState` 自动同步 | `AppStateService.cs` | +| 高 | 补充探测器事件订阅,打通 `DetectorState` 自动同步 | `AppStateService.cs` | +| 中 | 确认 `StartPolling()` 在应用启动流程中被调用 | `App.xaml.cs` / `AppBootstrapper` | +| 低 | 评估是否将相机连接状态纳入 `CameraState` 统一管理 | `NavigationPropertyPanelViewModel.cs`、`AppStateService.cs` | + + +