更新硬件集成文档

This commit is contained in:
zhengxuan.zhang
2026-05-06 23:26:55 +08:00
parent d56caf1ab5
commit d079e9357b
2 changed files with 430 additions and 0 deletions
Binary file not shown.
@@ -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` 的 `<Reference HintPath>` 引用。
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<XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel>();
// 注册配置(从 App.config 读取 RaySource:xxx 键值)
var raySourceConfig = XP.Hardware.RaySource.Config.ConfigLoader.LoadConfig();
containerRegistry.RegisterInstance(raySourceConfig);
// 注册核心服务(全部单例)
containerRegistry.RegisterSingleton<IRaySourceFactory, RaySourceFactory>();
containerRegistry.RegisterSingleton<IRaySourceService, RaySourceService>();
containerRegistry.RegisterSingleton<IFilamentLifetimeService, FilamentLifetimeService>();
```
---
### 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
<views1:RaySourceOperateView Grid.Row="0" Grid.ColumnSpan="2" />
```
控件内部已设置 `prism:ViewModelLocator.AutoWireViewModel="True"`Prism 按命名约定自动从 DI 容器解析 `RaySourceOperateViewModel` 并绑定为 DataContext。
---
### 4. 数据传递路线
数据流分四条路径:
**路径 A:配置数据(启动时,单向下行)**
```
App.config (RaySource:xxx 键值)
→ ConfigLoader.LoadConfig()
→ RaySourceConfig 实例
→ 注入到 RaySourceService / RaySourceOperateViewModel
```
App.config 中的关键配置项:
```xml
<add key="RaySource:PlcIpAddress" value="192.168.1.100" />
<add key="RaySource:MinVoltage" value="20" />
<add key="RaySource:MaxVoltage" value="225" />
<add key="RaySource:StatusPollingInterval" value="500" />
<add key="RaySource:EnableAutoStatusMonitoring" value="true" />
```
**路径 B:用户操作(UI → DLL 服务层)**
```
RaySourceOperateView(按钮点击)
→ RaySourceOperateViewModelCommand 绑定)
→ 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<StatusUpdatedEvent>().Subscribe(data => { ... });
```
---
### 5. 完整依赖关系
```
RaySourceOperateViewDLL 中的 UserControl
└─ AutoWire → RaySourceOperateViewModelDLL,主项目注册到 DI
├─ IRaySourceService ← 单例,DLL 实现
├─ RaySourceConfig ← App.config 加载
├─ IFilamentLifetimeService ← 单例,DLL 实现
├─ IEventAggregator ← Prism 内置
├─ ILoggerService ← 主项目 LoggerServiceAdapter 适配 Serilog
└─ ILocalizationService ← 主项目注册,DLL 消费
```
---
### 6. 资源释放
`App.OnExit()` 中显式从容器解析并释放资源:
```csharp
var appStateService = bootstrapper.Container.Resolve<IAppStateService>();
appStateService?.Dispose();
var raySourceService = bootstrapper.Container.Resolve<IRaySourceService>();
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<StatusUpdatedEvent>()
.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<XP.Hardware.Detector.Abstractions.Events.StatusChangedEvent>()
.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` |