# XP.Hardware.MotionControl 运动控制模块设计规划 > 文档版本:v1.0 > 创建日期:2026-04-10 > 适用项目:XplorePlane 平面CT工业检测系统 --- ## 1. 背景与硬件描述 本模块用于控制平面CT系统中的所有运动轴、旋转轴及安全防护门。 ### 1.1 直线轴(4轴) | 轴标识 | 说明 | 运动方向 | |--------|------|----------| | 轴1 SourceZ | 射线源Z方向运动 | 上下 | | 轴2 DetectorZ | 探测器Z方向运动 | 上下 | | 轴3 StageX | 载物台X方向运动 | 前后 | | 轴4 StageY | 载物台Y方向运动 | 左右 | ### 1.2 旋转轴(3轴) | 轴标识 | 说明 | 特点 | |--------|------|------| | 旋转轴1 DetectorSwing | 探测器在XZ平面的旋转摆动 | 有限角度,旋转中心在射线源Z方向延长线上 | | 旋转轴2 StageRotation | 载物台围绕XY中心的Z方向旋转 | 可选配置 | | 旋转轴3 FixtureRotation | 旋转夹具在载物台XY平面上围绕X方向的旋转 | 可选配置 | ### 1.3 安全门 防护门,具备状态机控制与互锁信号联动。 ### 1.4 几何关系 ``` FOD(焦点到旋转中心距离)= |SourceZ绝对坐标 - 旋转中心绝对Z坐标| FDD(焦点到探测器距离) = |SourceZ绝对坐标 - DetectorZ绝对坐标| 放大倍率 M = FDD / FOD ``` 只有确定了射线源、探测器、载物台旋转中心的绝对坐标,才能准确计算 FOD 和 FDD,进而控制放大倍率。 --- ## 2. 目录结构 ``` XP.Hardware.MotionControl/ ├── Abstractions/ │ ├── ILinearAxis.cs # 直线轴策略接口 │ ├── IRotaryAxis.cs # 旋转轴策略接口 │ ├── ISafetyDoor.cs # 安全门策略接口 │ ├── IMotionSystem.cs # 顶层系统管理接口 │ ├── LinearAxisBase.cs # 直线轴抽象基类 │ ├── RotaryAxisBase.cs # 旋转轴抽象基类 │ ├── SafetyDoorBase.cs # 安全门抽象基类 │ ├── MotionResult.cs # 统一操作结果类型 │ └── Enums/ │ ├── AxisStatus.cs # 轴状态枚举 │ ├── DoorStatus.cs # 门状态枚举 │ └── AxisId.cs # 轴标识枚举 ├── Implementations/ │ ├── PlcLinearAxis.cs # 基于PLC的直线轴实现 │ ├── PlcRotaryAxis.cs # 基于PLC的旋转轴实现 │ └── PlcSafetyDoor.cs # 基于PLC的安全门实现 ├── Services/ │ ├── IMotionControlService.cs # 运动控制业务服务接口 │ ├── MotionControlService.cs # 服务实现(含轮询线程、几何计算) │ └── GeometryCalculator.cs # FOD/FDD/放大倍率计算器 ├── Config/ │ ├── MotionControlConfig.cs # 配置实体(轴范围、原点、PLC信号名) │ └── ConfigLoader.cs # 配置加载器 ├── ViewModels/ │ └── MotionControlViewModel.cs # 操作面板 ViewModel ├── Views/ │ └── MotionControlView.xaml # 操作面板 UserControl(宽350) ├── Module/ │ └── MotionControlModule.cs # Prism 模块注册 ├── Documents/ │ └── MotionControl_Design.md # 本文档 └── Resources/ ├── Resources.resx ├── Resources.zh-CN.resx └── Resources.en-US.resx ``` --- ## 3. 核心抽象层设计 ### 3.1 三类硬件抽象继承关系 ``` ILinearAxis IRotaryAxis ISafetyDoor │ │ │ LinearAxisBase RotaryAxisBase SafetyDoorBase │ │ │ PlcLinearAxis PlcRotaryAxis PlcSafetyDoor ``` ### 3.2 ILinearAxis 接口职责 - 位置读取(实际值) - 目标位置写入 - Jog 正向 / Jog 反向(按住运动,松开停止) - 回零 - 停止 - 限位状态读取(正限位 / 负限位) - 轴状态(Idle / Moving / Homing / Error / Alarm) ### 3.3 IRotaryAxis 接口职责 - 角度读取(实际值) - 目标角度写入 - Jog 正向 / Jog 反向 - 回零 - 停止 - 角度软限位(MinAngle / MaxAngle,来自配置) - 轴状态 ### 3.4 ISafetyDoor 接口职责 - 状态机读取(Unknown / Opening / Open / Closing / Closed / Locked / Error) - 开门指令 - 关门指令 - 互锁信号读取(是否允许开门) ### 3.5 IMotionSystem 顶层接口职责 - 按 `AxisId` 枚举索引各直线轴(SourceZ、DetectorZ、StageX、StageY) - 按 `RotaryAxisId` 枚举索引各旋转轴(DetectorSwing、StageRotation、FixtureRotation) - 持有安全门实例 - 协调运动:开门前联锁检查 - 几何计算委托给 `GeometryCalculator` --- ## 4. 服务层设计 ### 4.1 IMotionControlService 封装业务规则,供 ViewModel 调用: - `StartPolling()` / `StopPolling()` — 启停 PLC 数据轮询 - `MoveToTarget(AxisId, double)` — 移动指定轴到目标位置 - `MoveAllToTarget(targets)` — 多轴联动移动 - `StopAll()` — 停止所有轴 - `HomeAll()` — 全部回零 - `JogStart(AxisId, direction)` / `JogStop(AxisId)` — Jog 控制 - `OpenDoor()` / `CloseDoor()` / `StopDoor()` — 门控制(含联锁前置检查) - `ApplyGeometry(fod, fdd)` — 根据 FOD/FDD 反算并移动 - `GetCurrentGeometry()` — 获取当前 FOD/FDD/M - `MoveStageByPixelOffset(dxPixels, dyPixels, executeMove)` — 根据图像像素偏移驱动载物台移动(自动计算放大比和物理距离) - `MoveToSafePosition()` — 所有轴移动到配置的安全点位置 - `MoveToTarget(axisId, target, speed, applyCompensation)` / `MoveAllToTarget(targets, applyCompensation)` — 定点移动,`applyCompensation=true` 时对 StageX/StageY 叠加坐标系补偿 - `ApplyPixelOffsetCompensation(dx, dy, mode)` — 记录像素偏移补偿并执行修正移动(默认覆盖) - `RecordPixelOffsetCompensation(dx, dy, mode)` — 仅记录补偿不移动(默认覆盖) - `GetStageCompensation()` / `SetStageCompensation(xMm, yMm)` / `ResetStageCompensation()` — 补偿值查询/设置/清零 > 坐标系补偿的完整设计见第 14 节。 ### 4.2 GeometryCalculator ```csharp // 正算:由轴位置计算几何参数 double CalcFOD(double sourceZAbs, double stageRotCenterZAbs) double CalcFDD(double sourceZAbs, double detectorZAbs) double CalcMagnification(double fdd, double fod) // 反算:由目标几何参数计算轴目标位置 (double sourceZTarget, double detectorZTarget) CalcAxisTargets(double targetFOD, double targetFDD) // 边界检查:目标值是否在轴 Min/Max 范围内 bool ValidateTargets(double sourceZTarget, double detectorZTarget) ``` 绝对坐标计算方式: ``` SourceZ绝对坐标 = SourceZ当前位置 + SourceZOrigin(配置) DetectorZ绝对坐标 = DetectorZ当前位置 + DetectorZOrigin(配置) 旋转中心绝对Z坐标 = StageRotationCenterZ(配置,固定值) ``` ### 4.3 轮询线程 在 `MotionControlService` 内部使用 `System.Threading.Timer`,周期约 100ms,仅在 PLC 已连接时激活。每次轮询通过 `ISignalDataService.GetValueByName()` 读取所有轴的实际位置和状态,更新到 ViewModel 的可绑定属性。 --- ## 5. 与 PLC 模块的依赖关系 MotionControl 通过依赖注入获取 `ISignalDataService`(来自 XP.Hardware.PLC),所有读写操作通过信号逻辑名称进行,不直接操作 PLC 地址。信号名称在配置文件中定义。 ``` XP.Hardware.MotionControl └── 依赖注入 → ISignalDataService(XP.Hardware.PLC) └── 读:GetValueByName("MC_SourceZ_Pos") └── 写:EnqueueWrite("MC_SourceZ_Target", value) ``` --- ## 6. 配置文件设计(App.config) ```xml 100 30 10 ``` --- ## 7. ViewModel 设计 ### 7.1 属性分组 ```csharp // 直线轴(每轴一组实际值 + 目标值) double SourceZActual / SourceZTarget double DetectorZActual / DetectorZTarget double StageXActual / StageXTarget double StageYActual / StageYTarget // 旋转轴(每轴一组,可选轴按配置控制 Visibility) double DetSwingActual / DetSwingTarget // 附带角度限制提示文本 double StageRotActual / StageRotTarget double FixtureRotActual / FixtureRotTarget // 几何信息(实时计算,只读显示) double FOD / FDD / Magnification // 几何反算输入 double TargetFOD / TargetFDD / TargetMagnification // 安全门 DoorStatus DoorStatus bool IsInterlocked // 状态 bool IsMoving bool IsPlcConnected // 调试模式(默认 false,折叠隐藏调试区域) bool IsDebugMode ``` ### 7.2 命令列表 ```csharp // 主操作命令 ICommand MoveCommand // 执行运动到各轴目标位置 ICommand StopCommand // 停止所有轴 ICommand HomeCommand // 全部回零 ICommand OpenDoorCommand ICommand CloseDoorCommand ICommand StopDoorCommand ICommand ApplyGeometryCommand // 根据 FOD/FDD/M 反算并移动 // 调试命令(Jog,按住触发,松开停止) ICommand JogPlusCommand // 参数:AxisId ICommand JogMinusCommand // 参数:AxisId ICommand JogStopCommand // 参数:AxisId ``` --- ## 8. View 布局设计(宽 350px,UserControl) ``` ┌─────────────────────────────────────┐ 350px │ [安全门状态] ● 已关闭 [互锁: 正常] │ ├─────────────────────────────────────┤ │ 直线轴 │ │ 源Z [实际: 123.4mm] [▲目标▼] │ │ 探Z [实际: 456.7mm] [▲目标▼] │ │ 台X [实际: 12.3mm] [▲目标▼] │ │ 台Y [实际: -5.6mm] [▲目标▼] │ ├─────────────────────────────────────┤ │ 旋转轴 │ │ 探摆 [实际: 15.0°] [▲目标▼] │ │ 限制: -45° ~ +45° │ │ 台转 [实际: 180.0°] [▲目标▼] │ │ 夹具 [实际: 0.0°] [▲目标▼] │ ├─────────────────────────────────────┤ │ 几何信息 │ │ FOD: 150.0mm FDD: 450.0mm M: 3.0│ │ [目标FOD▲▼] [目标FDD▲▼] [应用] │ ├─────────────────────────────────────┤ │ [开始运动] [停止运动] [回零] │ │ [开门] [关门] │ ├─────────────────────────────────────┤ │ ▼ 调试模式(默认折叠) │ │ 源Z [+Jog][-Jog] 探Z [+][-] │ │ 台X [+Jog][-Jog] 台Y [+][-] │ │ 探摆 [+Jog][-Jog] 台转[+][-] │ │ 夹具 [+Jog][-Jog] │ └─────────────────────────────────────┘ ``` --- ## 9. FOD/FDD 反算逻辑 用户输入目标 FOD 和 FDD(或放大倍率 M),系统自动计算各轴目标位置: ``` // 已知:StageRotationCenterZ(旋转中心绝对Z坐标,来自配置,固定值) // 输入 FOD + FDD: SourceZ_target = StageRotationCenterZ - FOD DetectorZ_target = SourceZ_target + FDD // 输入 FOD + M(M = FDD/FOD): FDD = M * FOD DetectorZ_target = SourceZ_target + FDD // 边界检查: if SourceZ_target < SourceZ.Min || SourceZ_target > SourceZ.Max → 拒绝并提示 if DetectorZ_target < DetectorZ.Min || DetectorZ_target > DetectorZ.Max → 拒绝并提示 ``` --- ## 10. 关键安全机制 参考旧项目 `ControlDoor.openDoor()` 和 `_130kvMove` 的实现经验: | 机制 | 说明 | 对标旧代码 | |------|------|-----------| | 运动中防重入 | `IsMoving` 标志位,运动中禁止再次触发运动命令 | `fdcRun` 标志位 | | 开门联锁检查 | 开门前通过 `IEventAggregator` 检查是否有扫描进行中 | `ControlDoor.openDoor()` 前置逻辑 | | 射线源联锁 | 开门前需确认射线源已关闭(暂不实现,预留扩展点,见下方说明) | `zeroXray2Correct()` 等待归零 | | 可选轴隐藏 | `StageRotation` 和 `FixtureRotation` 通过配置 `Enabled` 控制 UI 可见性 | 无对应,新增 | | Jog 安全 | Jog 操作通过 `MouseDown/Up` 事件控制,松开立即停止 | `radButton3_MouseDown/Up` | --- ## 11. Prism 模块注册 ```csharp // MotionControlModule.RegisterTypes() containerRegistry.RegisterSingleton(); // 配置(单例) containerRegistry.RegisterSingleton(); // 系统管理(单例) containerRegistry.RegisterSingleton(); // 服务(单例) // MotionControlModule.OnInitialized() // 注册多语言资源到 Fallback Chain // 启动 PLC 轮询(若 PLC 已连接) ``` --- --- ## 12. 关于射线源联锁的扩展说明(暂不实现) ### 12.1 什么是事件聚合器(EventAggregator) Prism 的 `IEventAggregator` 是一个**发布/订阅总线**,用于在模块之间传递消息,而不需要两个模块直接互相引用。 类比理解:就像一个广播电台,`RaySource` 模块是"电台"(发布者),`MotionControl` 模块是"收音机"(订阅者),两者只通过"频道"(事件类型)通信,彼此不知道对方的存在。 ``` XP.Hardware.RaySource XP.Hardware.MotionControl │ │ │ 发布 RaySourceStatusChangedEvent │ └──────────→ EventAggregator ──────→ 订阅并响应 ``` ### 12.2 如何订阅(当需要实现时) **第一步:RaySource 模块已有的事件定义** ```csharp // XP.Hardware.RaySource/Abstractions/Events/RaySourceStatusChangedEvent.cs // 该事件已存在,携带射线源当前状态 public class RaySourceStatusChangedEvent : PubSubEvent { } ``` **第二步:MotionControlService 中注入并订阅** ```csharp public class MotionControlService : IMotionControlService { private readonly IEventAggregator _eventAggregator; private bool _isRaySourceOn = false; // 缓存射线源状态 public MotionControlService(IEventAggregator eventAggregator, ...) { _eventAggregator = eventAggregator; // 订阅射线源状态变化事件 _eventAggregator .GetEvent() .Subscribe(OnRaySourceStatusChanged, ThreadOption.BackgroundThread); } private void OnRaySourceStatusChanged(RaySourceStatus status) { // 根据状态判断射线是否开启 _isRaySourceOn = (status == RaySourceStatus.XRayOn); } public async Task OpenDoor() { // 开门前检查射线源 if (_isRaySourceOn) { _logger.Warn("射线源未关闭,禁止开门 | Ray source is on, door open blocked"); return; } // ... 继续开门逻辑 } } ``` **第三步:注意事项** - 订阅时选择 `ThreadOption.BackgroundThread`,避免在 UI 线程处理硬件状态 - 模块卸载时需要取消订阅,防止内存泄漏:`GetEvent<...>().Unsubscribe(OnRaySourceStatusChanged)` - 两个模块之间**不需要互相引用**,只需要共同引用定义事件的程序集(或在各自模块内定义相同结构的事件) ### 12.3 预留扩展点 当需要实现射线源联锁时,只需在 `MotionControlService.OpenDoor()` 方法开头加入上述订阅逻辑即可,不需要修改任何其他代码。 --- ## 13. 事件定义(Prism EventAggregator) ``` Abstractions/Events/ ├── AxisStatusChangedEvent.cs # 轴状态变化事件(AxisId + AxisStatus) ├── DoorStatusChangedEvent.cs # 门状态变化事件(DoorStatus) ├── GeometryUpdatedEvent.cs # 几何参数更新事件(FOD + FDD + M) └── MotionErrorEvent.cs # 运动错误事件(AxisId + 错误信息) ``` --- ## 13. 实现优先级建议 | 优先级 | 内容 | |--------|------| | P0 | 配置实体、枚举定义、MotionResult | | P0 | ILinearAxis / IRotaryAxis / ISafetyDoor 接口 + 抽象基类 | | P1 | PlcLinearAxis / PlcRotaryAxis / PlcSafetyDoor 实现 | | P1 | GeometryCalculator | | P1 | IMotionControlService + MotionControlService(含轮询) | | P2 | MotionControlViewModel | | P2 | MotionControlView.xaml(宽350 UserControl) | | P3 | MotionControlModule 注册 | | P3 | 多语言资源文件 | --- ## 14. 图像坐标系补偿设计 | Coordinate Compensation Design ### 14.1 功能目标 主框架在坐标匹配过程中会计算出工件位置的图像像素偏移 `(dX, dY)`(可为负)。本模块负责: 1. 将像素偏移换算为载物台物理位移(mm),记录为**补偿值**(初始 `(0,0)`); 2. 执行修正移动,使工件对齐; 3. 后续任意"移动到名义位置"的指令,可选择自动叠加该补偿值,保证工件持续对齐。 ### 14.2 补偿值模型 - **单位**:物理量 mm(Stage 坐标系),**放大比无关**。像素偏移随放大比变化,但对应的物理错位是固定的,因此存 mm 而非像素。 - **作用范围**:仅 `StageX / StageY`,其余轴不补偿。 - **存储位置**:`MotionControlService`(单例)私有字段 `_compXmm / _compYmm`,用 `_compLock` 保护跨线程(轮询线程 / UI 线程)读写。 - **生命周期**:随软件运行保持;`HomeAll` 回零**不清补偿**;仅 `ResetStageCompensation()` 显式清零(换工件/维护时由主框架调用)。 ### 14.3 记录模式(CompensationMode) | 模式 | 语义 | 适用场景 | |------|------|----------| | `Replace`(默认)| `comp = 本次换算值` | 每次从同一名义参考位置测量、每位置只记录一次的一次性对位 | | `Accumulate` | `comp += 本次换算值` | 在同一位置迭代修正残差的闭环逼近 | > 注意:`ApplyPixelOffsetCompensation` 的移动为**相对移动**。覆盖模式假设"同参考位置、每位置一次";若在同一位置连续修正而不回参考点,请改用 `Accumulate`,否则物理移动会重复叠加。 ### 14.4 像素→mm 换算 复用像素偏移换算逻辑(私有方法 `TryConvertPixelToStageMm`),与 `MoveStageByPixelOffset` 一致: ``` mmPerPixelX = DetectorPixelSizeX / M mmPerPixelY = DetectorPixelSizeY / M dxMm = dxPixels × mmPerPixelX × PixelDirectionX dyMm = dyPixels × mmPerPixelY × PixelDirectionY ``` 其中 `M` 为当前放大倍率(由 `GetCurrentGeometry()` 实时计算)。 ### 14.5 典型调用流程 ```csharp // 主框架坐标匹配完成后:一步完成"记录补偿 + 修正移动"(默认覆盖) _mc.ApplyPixelOffsetCompensation(dxPixels, dyPixels); // 其他类库定点运动并叠加补偿(仅 StageX/StageY 生效,补偿后仍过边界校验) _mc.MoveAllToTarget(new Dictionary { { AxisId.StageX, 100 }, { AxisId.StageY, 50 } }, applyCompensation: true); // 换工件时清零补偿 _mc.ResetStageCompensation(); ``` 补偿值变化时发布 `StageCompensationChangedEvent(StageCompensationData(Xmm, Ymm))`,供 UI 显示当前补偿量。 ### 14.6 无需新增配置 补偿功能复用现有几何配置(`DetectorPixelSizeX/Y`、`PixelDirectionX/Y`)与实时放大比,不引入新的 App.config 参数。