24 KiB
硬件层及 UI 集成技术路线
整体架构
采用 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 创建前已具备所有依赖。
// 注册 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:
xmlns:views1="clr-namespace:XP.Hardware.RaySource.Views;assembly=XP.Hardware.RaySource"
在主窗口右侧面板顶部(Grid.Row="0",固定高度 250px)放置控件:
<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 中的关键配置项:
<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(按钮点击)
→ 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:
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. 完整依赖关系
RaySourceOperateView(DLL 中的 UserControl)
└─ AutoWire → RaySourceOperateViewModel(DLL,主项目注册到 DI)
├─ IRaySourceService ← 单例,DLL 实现
├─ RaySourceConfig ← App.config 加载
├─ IFilamentLifetimeService ← 单例,DLL 实现
├─ IEventAggregator ← Prism 内置
├─ ILoggerService ← 主项目 LoggerServiceAdapter 适配 Serilog
└─ ILocalizationService ← 主项目注册,DLL 消费
6. 资源释放
App.OnExit() 中显式从容器解析并释放资源:
var appStateService = bootstrapper.Container.Resolve<IAppStateService>();
appStateService?.Dispose();
var raySourceService = bootstrapper.Container.Resolve<IRaySourceService>();
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 未连接时的保护:
// MotionControlService.OnPollingTick()
if (!_plcService.IsConnected) return; // 直接跳过,不报错
连续错误降频:
if (_pollErrorCount > 3)
{
if (++_pollErrorCount % 50 != 0) return; // 每 50 次才尝试一次,防止日志刷屏
}
AppStateService 初始化时的首次刷新:
// 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() 的特殊行为:
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 字段,避免覆盖精确数据:
// 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 关键实现细节
订阅均在后台线程,避免阻塞采集链路:
_detectorStatusChangedToken = _eventAggregator
.GetEvent<StatusChangedEvent>()
.Subscribe(OnDetectorStatusChanged, ThreadOption.BackgroundThread);
_detectorImageCapturedToken = _eventAggregator
.GetEvent<ImageCapturedEvent>()
.Subscribe(OnDetectorImageCaptured, ThreadOption.BackgroundThread);
断连检测:OnDetectorStatusChanged 在更新状态前记录 wasConnected,状态更新后若检测到从已连接变为断开,则发布 DetectorDisconnectedEvent:
bool wasConnected = _detectorState?.IsConnected ?? false;
// ... 更新状态 ...
if (wasConnected && !isConnected)
{
_eventAggregator.GetEvent<DetectorDisconnectedEvent>().Publish();
_logger.Warn("探测器已断连,发布 DetectorDisconnectedEvent");
}
最新帧缓存:LatestDetectorFrame 为 volatile 字段,任意线程可安全读取,无需加锁:
private volatile ImageCapturedEventArgs _latestDetectorFrame;
public ImageCapturedEventArgs LatestDetectorFrame => _latestDetectorFrame;
Dispose 时取消订阅:
_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 警告 |
四、相机 — ✅ 已打通(连接状态同步)
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() 是统一的同步入口,在所有状态变更节点调用:
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 构造函数参数,无需修改注册代码。
五、状态同步全景图
┌─────────────────────────────────────────────────────────────────────┐
│ 硬件层 │
│ │
│ 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 |
✅ 已完成 |