diff --git a/XP.Hardware.MotionControl/Abstractions/Events/JoystickActiveEvent.cs b/XP.Hardware.MotionControl/Abstractions/Events/JoystickActiveEvent.cs new file mode 100644 index 0000000..ee9fc93 --- /dev/null +++ b/XP.Hardware.MotionControl/Abstractions/Events/JoystickActiveEvent.cs @@ -0,0 +1,11 @@ +using Prism.Events; + +namespace XP.Hardware.MotionControl.Abstractions.Events +{ + /// + /// 实体摇杆激活状态变化事件 | Physical Joystick Active Status Changed Event + /// + public class JoystickActiveEvent : PubSubEvent + { + } +} diff --git a/XP.Hardware.MotionControl/Abstractions/IJoystick.cs b/XP.Hardware.MotionControl/Abstractions/IJoystick.cs new file mode 100644 index 0000000..8502cca --- /dev/null +++ b/XP.Hardware.MotionControl/Abstractions/IJoystick.cs @@ -0,0 +1,17 @@ +using XP.Hardware.MotionControl.Abstractions.Enums; + +namespace XP.Hardware.MotionControl.Abstractions +{ + /// + /// 摇杆接口 | Joystick Interface + /// 定义实体摇杆输入状态的读取能力 | Defines physical joystick input status reading capability + /// + public interface IJoystick + { + /// 实体摇杆输入是否激活 | Whether physical joystick input is active + bool IsJoystickActive { get; } + + /// 从 PLC 更新状态 | Update status from PLC + void UpdateStatus(); + } +} diff --git a/XP.Hardware.MotionControl/Abstractions/IMotionSystem.cs b/XP.Hardware.MotionControl/Abstractions/IMotionSystem.cs index b6962f0..c5553f7 100644 --- a/XP.Hardware.MotionControl/Abstractions/IMotionSystem.cs +++ b/XP.Hardware.MotionControl/Abstractions/IMotionSystem.cs @@ -22,6 +22,9 @@ namespace XP.Hardware.MotionControl.Abstractions /// 安全门实例 | Safety door instance ISafetyDoor SafetyDoor { get; } + /// 摇杆实例 | Joystick instance + IJoystick Joystick { get; } + /// 轴复位实例 | Axis reset instance IAxisReset AxisReset { get; } diff --git a/XP.Hardware.MotionControl/Documents/AxisControlView_PLC_Communication.md b/XP.Hardware.MotionControl/Documents/AxisControlView_PLC_Communication.md new file mode 100644 index 0000000..3240fd5 --- /dev/null +++ b/XP.Hardware.MotionControl/Documents/AxisControlView_PLC_Communication.md @@ -0,0 +1,449 @@ +# AxisControlView PLC 通信机制 | AxisControlView PLC Communication + +## 概述 | Overview + +AxisControlView 是运动控制模块的核心用户控件,通过 AxisControlViewModel 与 PLC 进行双向通信。本文档详细说明摇杆 Jog 操作、轴位置输入框操作以及射线源/探测器Z轴联动状态下的 PLC 信号下发机制。 + +## 通信架构 | Communication Architecture + +``` +AxisControlView (XAML) + ↓ (数据绑定) +AxisControlViewModel (C#) + ↓ (调用) +IMotionControlService + ↓ (调用) +ISignalDataService + ↓ (写入队列) +PLC (DB31, WriteCommon 组) +``` + +## 1. 摇杆 Jog 操作 | Joystick Jog Operation + +### 1.1 双轴摇杆(圆形)| Dual Axis Joystick (Circular) + +**左键拖拽(默认模式)**: +- X 轴输出 → StageX Jog(正向/反向) +- Y 轴输出 → StageY Jog(正向/反向) + +**右键拖拽(默认模式)**: +- X 轴输出 → DetectorSwing Jog(正向/反向) +- Y 轴输出 → StageRotation/FixtureRotation Jog(正向/反向) + +**信号下发流程**: + +```csharp +// 1. 摇杆输出变化触发 HandleDualJoystickOutput() +private void HandleDualJoystickOutput() +{ + switch (DualJoystickActiveButton) + { + case MouseButtonType.Left: + UpdateLinearJog(AxisId.StageX, DualJoystickOutputX); + UpdateLinearJog(AxisId.StageY, DualJoystickOutputY); + break; + case MouseButtonType.Right: + UpdateRotaryJog(RotaryAxisId.DetectorSwing, DualJoystickOutputX); + var rotationAxisId = GetEnabledRotationAxisId(); + if (rotationAxisId.HasValue) + UpdateRotaryJog(rotationAxisId.Value, DualJoystickOutputY); + break; + } +} + +// 2. UpdateLinearJog/UpdateRotaryJog 处理 Jog 启动/速度更新/停止 +private void UpdateLinearJog(AxisId axisId, double output) +{ + if (output != 0) + { + var speedPercent = Math.Abs(output) * 100; + var positive = output > 0; + + if (!_linearJogActive[axisId]) + { + // 首次非零:设置速度并启动 Jog + _motionControlService.SetJogSpeed(axisId, speedPercent); + _motionControlService.JogStart(axisId, positive); + _linearJogActive[axisId] = true; + } + else + { + // 已在 Jog:仅更新速度 + _motionControlService.SetJogSpeed(axisId, speedPercent); + } + } + else + { + // 松开鼠标:停止 Jog + if (_linearJogActive[axisId]) + { + _motionControlService.JogStop(axisId); + _linearJogActive[axisId] = false; + } + } +} + +// 3. IMotionControlService 调用 ISignalDataService.EnqueueWrite 写入 PLC +public MotionResult JogStart(AxisId axisId, bool positive) +{ + var signalName = positive ? MotionSignalNames.SourceZ_JogPos : MotionSignalNames.SourceZ_JogNeg; + var result = _signalService.EnqueueWrite(signalName, true); + return result ? MotionResult.Ok() : MotionResult.Fail("Jog 启动写入失败"); +} +``` + +**PLC 信号地址**: + +| 轴 | 信号名 | 地址 | 说明 | +|----|--------|------|------| +| StageX | `MC_StageX_JogPos` | 42 | 正向 Jog(0:缺省, 1:点动) | +| StageX | `MC_StageX_JogNeg` | 43 | 反向 Jog(0:缺省, 1:点动) | +| StageY | `MC_StageY_JogPos` | 54 | 正向 Jog(0:缺省, 1:点动) | +| StageY | `MC_StageY_JogNeg` | 55 | 反向 Jog(0:缺省, 1:点动) | +| DetectorSwing | `MC_DetSwing_JogPos` | 66 | 正向 Jog(0:缺省, 1:点动) | +| DetectorSwing | `MC_DetSwing_JogNeg` | 67 | 反向 Jog(0:缺省, 1:点动) | +| StageRotation | `MC_StageRot_JogPos` | 78 | 正向 Jog(0:缺省, 1:点动) | +| StageRotation | `MC_StageRot_JogNeg` | 79 | 反向 Jog(0:缺省, 1:点动) | +| FixtureRotation | `MC_FixRot_JogPos` | 90 | 正向 Jog(0:缺省, 1:点动) | +| FixtureRotation | `MC_FixRot_JogNeg` | 91 | 反向 Jog(0:缺省, 1:点动) | + +### 1.2 单轴摇杆(腰圆)| Single Axis Joystick (Oval) + +**左键拖拽(默认模式)**: +- Y 轴输出 → SourceZ Jog(正向/反向) +- 若 SZDZLock=true,同时 → DetectorZ Jog(正向/反向) + +**右键拖拽(默认模式)**: +- Y 轴输出 → DetectorZ Jog(正向/反向) +- 若 SZDZLock=true,同时 → SourceZ Jog(正向/反向) + +**信号下发流程**: + +```csharp +private void HandleSingleJoystickOutput() +{ + switch (SingleJoystickActiveButton) + { + case MouseButtonType.Left: + UpdateLinearJog(AxisId.SourceZ, SingleJoystickOutputY); + if (_szdzLock) + UpdateLinearJog(AxisId.DetectorZ, SingleJoystickOutputY); + break; + case MouseButtonType.Right: + UpdateLinearJog(AxisId.DetectorZ, SingleJoystickOutputY); + if (_szdzLock) + UpdateLinearJog(AxisId.SourceZ, SingleJoystickOutputY); + break; + } +} +``` + +**PLC 信号地址**: + +| 轴 | 信号名 | 地址 | 说明 | +|----|--------|------|------| +| SourceZ | `MC_SourceZ_JogPos` | 18 | 正向 Jog(0:缺省, 1:点动) | +| SourceZ | `MC_SourceZ_JogNeg` | 19 | 反向 Jog(0:缺省, 1:点动) | +| DetectorZ | `MC_DetZ_JogPos` | 30 | 正向 Jog(0:缺省, 1:点动) | +| DetectorZ | `MC_DetZ_JogNeg` | 31 | 反向 Jog(0:缺省, 1:点动) | + +### 1.3 Jog 速度控制 | Jog Speed Control + +摇杆输出值(-1.0 ~ 1.0)映射为速度百分比(0% ~ 100%): + +```csharp +var speedPercent = Math.Abs(output) * 100; // 0% ~ 100% +``` + +**速度计算流程**: + +1. **MotionControlService.SetJogSpeed** 计算实际速度: + ```csharp + var actualSpeed = _config.DefaultVelocity * speedPercent / 100.0; + // 例如:DefaultVelocity=100, speedPercent=50 → actualSpeed=50 mm/s + ``` + +2. **PlcLinearAxis.SetJogSpeed** 将实际速度写入 PLC: + ```csharp + _signalService.EnqueueWrite(_speedSignal, (float)speedPercent); + // 注意:参数名 speedPercent 实际接收的是 actualSpeed(已计算的实际速度值) + ``` + +**PLC 信号地址**: + +| 轴 | 信号名 | 地址 | 说明 | +|----|--------|------|------| +| SourceZ | `MC_SourceZ_Speed` | 14 | 速度值(mm/s,由 DefaultVelocity * speedPercent / 100 计算) | +| DetectorZ | `MC_DetZ_Speed` | 26 | 速度值(mm/s,由 DefaultVelocity * speedPercent / 100 计算) | +| StageX | `MC_StageX_Speed` | 38 | 速度值(mm/s,由 DefaultVelocity * speedPercent / 100 计算) | +| StageY | `MC_StageY_Speed` | 50 | 速度值(mm/s,由 DefaultVelocity * speedPercent / 100 计算) | +| DetectorSwing | `MC_DetSwing_Speed` | 62 | 速度值(度/s,由 DefaultVelocity * speedPercent / 100 计算) | +| StageRotation | `MC_StageRot_Speed` | 74 | 速度值(度/s,由 DefaultVelocity * speedPercent / 100 计算) | +| FixtureRotation | `MC_FixRot_Speed` | 86 | 速度值(度/s,由 DefaultVelocity * speedPercent / 100 计算) | + +**示例**: +- `DefaultVelocity = 100` mm/s +- 摇杆输出 30% → PLC 接收速度 = 100 * 30% = 30 mm/s +- 摇杆输出 100% → PLC 接收速度 = 100 * 100% = 100 mm/s + +## 2. 轴位置输入框操作 | Axis Position Input Box Operation + +### 2.1 编辑状态管理 | Editing State Management + +- **GotFocus**:`SetEditing(propertyName, true)` → 冻结实时更新 +- **LostFocus/Escape**:`CancelEditing(propertyName)` → 恢复实时更新,显示实际值 +- **Enter**:`ConfirmPosition(propertyName)` → 发送目标位置移动命令 + +### 2.2 移动命令下发流程 | Move Command Flow + +```csharp +public void ConfirmPosition(string propertyName) +{ + var value = GetPropertyValue(propertyName); + SafeRun(() => + { + MotionResult result; + // 如果射线源与探测器Z轴联动,且操作的是SourceZ或DetectorZ,则同时发送两个轴 + if (SZDZLock && (propertyName == nameof(SourceZPosition) || propertyName == nameof(DetectorZPosition))) + { + result = SendSourceDetectorZMoveCommand(value); + } + else + { + result = SendMoveCommand(propertyName, value); + } + if (result.Success) + _logger.Info("目标位置已发送:{Property}={Value}", propertyName, value); + else + _logger.Warn("目标位置发送失败:{Property}={Value},原因={Reason}", propertyName, value, result.ErrorMessage); + }); + _editingFlags[propertyName] = false; +} + +private MotionResult SendMoveCommand(string propertyName, double value) +{ + switch (propertyName) + { + case nameof(StageXPosition): + return _motionControlService.MoveToTarget(AxisId.StageX, value); + case nameof(SourceZPosition): + return _motionControlService.MoveToTarget(AxisId.SourceZ, value); + // ... 其他轴 + } +} +``` + +### 2.3 单轴移动 | Single Axis Move + +```csharp +public MotionResult MoveToTarget(AxisId axisId, double targetPosition, int? speed = null) +{ + var axis = _motionSystem.GetLinearAxis(axisId); + var actualSpeed = speed ?? _config.DefaultVelocity; + + // 边界检查(使用配置中的 Min/Max) + if (targetPosition < config.Min || targetPosition > config.Max) + return MotionResult.Fail("目标位置超出范围"); + + // 写入目标位置和速度 + var targetResult = _signalService.EnqueueWrite(signalName, targetPosition); + var speedResult = _signalService.EnqueueWrite(speedSignalName, actualSpeed); + + // 启动移动(写入目标位置会自动触发移动) + return targetResult && speedResult ? MotionResult.Ok() : MotionResult.Fail("移动命令写入失败"); +} +``` + +**PLC 信号地址**: + +| 轴 | 目标位置信号 | 地址 | 速度信号 | 地址 | +|----|-------------|------|---------|------| +| SourceZ | `MC_SourceZ_Target` | 10 | `MC_SourceZ_Speed` | 14 | +| DetectorZ | `MC_DetZ_Target` | 22 | `MC_DetZ_Speed` | 26 | +| StageX | `MC_StageX_Target` | 34 | `MC_StageX_Speed` | 38 | +| StageY | `MC_StageY_Target` | 46 | `MC_StageY_Speed` | 50 | +| DetectorSwing | `MC_DetSwing_Target` | 58 | `MC_DetSwing_Speed` | 62 | +| StageRotation | `MC_StageRot_Target` | 70 | `MC_StageRot_Speed` | 74 | +| FixtureRotation | `MC_FixRot_Target` | 82 | `MC_FixRot_Speed` | 86 | + +### 2.4 步进移动 | Step Move + +上下箭头键触发 `StepPosition(propertyName, delta)`: + +```csharp +public void StepPosition(string propertyName, double delta) +{ + var currentValue = GetPropertyValue(propertyName); + var newValue = currentValue + delta; + SetPropertyValue(propertyName, newValue); + + // 直接发送移动命令,不进入编辑冻结 + if (SZDZLock && (propertyName == nameof(SourceZPosition) || propertyName == nameof(DetectorZPosition))) + { + result = SendSourceDetectorZMoveCommand(newValue); + } + else + { + result = SendMoveCommand(propertyName, newValue); + } +} +``` + +## 3. 射线源/探测器Z轴联动 | Source-Detector Z-axis Linkage + +### 3.1 联动使能 | Linkage Enable + +点击"SZDZLock"按钮切换联动状态: + +```csharp +private void ExecuteSZDZLock() +{ + SZDZLock = !SZDZLock; + var result = _motionControlService.SetSourceDetectorZLinkage(SZDZLock); + + if (!result.Success) + { + // 设置失败,恢复状态 + SZDZLock = !SZDZLock; + MessageBox.Show(result.ErrorMessage); + } +} +``` + +**PLC 信号**: + +| 信号名 | 地址 | 类型 | 说明 | +|--------|------|------|------| +| `MC_SourceDetZ_Linkage_Enable` | 101 | bool | 联动使能(true=启用, false=禁用) | + +### 3.2 联动移动 | Linkage Move + +当 SZDZLock=true 时,移动 SourceZ 或 DetectorZ 会同时移动两个轴,保持位移量相同: + +```csharp +private MotionResult SendSourceDetectorZMoveCommand(double targetValue) +{ + // 1. 检查联动是否在配置中启用 + if (!_config.SourceDetectorZLinkage.Enabled) + return MotionResult.Fail("射线源与探测器Z轴联动未启用"); + + // 2. 检查两个轴是否都在空闲状态 + var sourceZAxis = _motionSystem.GetLinearAxis(AxisId.SourceZ); + var detectorZAxis = _motionSystem.GetLinearAxis(AxisId.DetectorZ); + + if (sourceZAxis.Status == AxisStatus.Moving) + return MotionResult.Fail("射线源Z轴正在运动中,无法联动移动"); + + if (detectorZAxis.Status == AxisStatus.Moving) + return MotionResult.Fail("探测器Z轴正在运动中,无法联动移动"); + + // 3. 计算位移量和目标位置 + var currentSourceZ = sourceZAxis.ActualPosition; + var currentDetectorZ = detectorZAxis.ActualPosition; + var sourceDelta = targetValue - currentSourceZ; + var targetDetectorZ = currentDetectorZ + sourceDelta; + + // 4. 边界检查(使用配置中的 Min/Max) + if (targetValue < sourceConfig.Min || targetValue > sourceConfig.Max) + return MotionResult.Fail("射线源Z轴目标位置超出范围"); + + if (targetDetectorZ < detectorConfig.Min || targetDetectorZ > detectorConfig.Max) + return MotionResult.Fail("探测器Z轴目标位置超出范围"); + + // 5. 同时发送两个轴的移动命令 + var sourceResult = sourceZAxis.MoveToTarget(targetValue, _config.DefaultVelocity); + var detectorResult = detectorZAxis.MoveToTarget(targetDetectorZ, _config.DefaultVelocity); + + return sourceResult.Success && detectorResult.Success + ? MotionResult.Ok() + : (sourceResult.Success ? detectorResult : sourceResult); +} +``` + +**PLC 信号下发**: + +1. **SourceZ 移动**: + - `MC_SourceZ_Target` = targetValue(地址 10) + - `MC_SourceZ_Speed` = DefaultVelocity(地址 14) + +2. **DetectorZ 移动**: + - `MC_DetZ_Target` = currentDetectorZ + (targetValue - currentSourceZ)(地址 22) + - `MC_DetZ_Speed` = DefaultVelocity(地址 26) + +### 3.3 联动 Jog | Linkage Jog + +当 SZDZLock=true 时,单轴摇杆的左右键会同时控制两个轴: + +```csharp +private void HandleSingleJoystickOutput() +{ + switch (SingleJoystickActiveButton) + { + case MouseButtonType.Left: + UpdateLinearJog(AxisId.SourceZ, SingleJoystickOutputY); + if (_szdzLock) + UpdateLinearJog(AxisId.DetectorZ, SingleJoystickOutputY); + break; + case MouseButtonType.Right: + UpdateLinearJog(AxisId.DetectorZ, SingleJoystickOutputY); + if (_szdzLock) + UpdateLinearJog(AxisId.SourceZ, SingleJoystickOutputY); + break; + } +} +``` + +**PLC 信号下发**: + +- **SourceZ Jog**: + - `MC_SourceZ_JogPos`/`MC_SourceZ_JogNeg` = true(地址 18/19) + - `MC_SourceZ_Speed` = speedPercent(地址 14) + +- **DetectorZ Jog**: + - `MC_DetZ_JogPos`/`MC_DetZ_JogNeg` = true(地址 30/31) + - `MC_DetZ_Speed` = speedPercent(地址 26) + +## 4. 虚拟摇杆使能 | Virtual Joystick Enable + +点击"使能开关"按钮切换虚拟摇杆使能状态: + +```csharp +private void ExecuteToggleEnable() +{ + IsJoystickEnabled = !IsJoystickEnabled; + _motionControlService.SetVirtualJoystickEnable(IsJoystickEnabled); +} +``` + +**PLC 信号**: + +| 信号名 | 地址 | 类型 | 说明 | +|--------|------|------|------| +| `MC_VirtualJoystick_Enable` | 111 | bool | 虚拟摇杆使能(true=启用, false=禁用) | + +## 5. 信号写入队列机制 | Signal Write Queue Mechanism + +所有 PLC 写入操作通过 `ISignalDataService.EnqueueWrite()` 进入写入队列: + +```csharp +public interface ISignalDataService +{ + bool EnqueueWrite(string signalName, object value); + bool EnqueueWriteBatch(Dictionary writes); +} +``` + +**队列处理流程**: + +1. ViewModel 调用 `EnqueueWrite(signalName, value)` +2. 信号数据服务将写入请求加入队列 +3. PLC 服务在轮询周期(默认 100ms)中批量写入 +4. 写入成功后从队列移除 + +**优势**: + +- 批量写入减少 PLC 通讯次数 +- 避免频繁写入导致的通讯拥塞 +- 统一的错误处理和重试机制 + +--- \ No newline at end of file diff --git a/XP.Hardware.MotionControl/Documents/GUIDENCE.md b/XP.Hardware.MotionControl/Documents/GUIDENCE.md index c32b6c5..a4ddf6d 100644 --- a/XP.Hardware.MotionControl/Documents/GUIDENCE.md +++ b/XP.Hardware.MotionControl/Documents/GUIDENCE.md @@ -21,7 +21,80 @@ protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) --- -## 2. 获取轴状态和位置 | Reading Axis Status and Position +## 2. 界面集成 | UI Integration + +### AxisControlView 控件使用 | AxisControlView Usage + +`AxisControlView` 是运动控制模块的核心用户控件,提供完整的轴控制和调试功能。 + +**XAML 引用 | XAML Reference** + +```xml + + + + + + + + + + +``` + +**自动 ViewModel 绑定 | Auto ViewModel Binding** + +控件使用 Prism 的 `ViewModelLocator.AutoWireViewModel="True"` 自动绑定 `AxisControlViewModel`,无需手动设置 `DataContext`。 + +**ViewModel 属性说明 | ViewModel Properties** + +| 属性 | 类型 | 说明 | +|------|------|------| +| `StageXPosition` | double | 载物台 X 轴位置(mm),范围由配置决定 | +| `StageYPosition` | double | 载物台 Y 轴位置(mm),范围由配置决定 | +| `SourceZPosition` | double | 射线源 Z 轴位置(mm),范围由配置决定 | +| `DetectorZPosition` | double | 探测器 Z 轴位置(mm),范围由配置决定 | +| `DetectorSwingAngle` | double | 探测器摆动角度(度),范围 -45~45 | +| `StageRotationAngle` | double | 载物台旋转角度(度),范围 -360~360 | +| `FixtureRotationAngle` | double | 夹具旋转角度(度),范围 -90~90 | +| `IsJoystickEnabled` | bool | 摇杆使能开关(默认 false) | +| `SwapMouseButtons` | bool | 摇杆左右键功能切换(默认 false) | +| `SZDZLock` | bool | 射线源/探测器Z轴锁定移动(默认 false) | +| `HasSavedPositions` | bool | 是否有已保存的位置快照 | + +**交互方式 | Interaction Modes** + +1. **手动输入位置** + - 直接编辑 RadNumericUpDown 控件 + - Enter 键确认并移动轴 + - Escape 键取消修改,恢复原值 + +2. **单轴摇杆(SourceZ/DetectorZ)** + - 左键拖拽:正向 Jog + - 右键拖拽:反向 Jog + - 松开鼠标:停止 Jog + +3. **双轴摇杆(StageX/Y + Rotation)** + - 默认模式(SwapMouseButtons=false): + - 左键拖拽:控制 StageX/Y(X轴=左右,Y轴=上下) + - 右键拖拽:控制 StageRotation(X轴=旋转) + - 切换模式(SwapMouseButtons=true): + - 左键拖拽:控制 StageRotation(X轴=旋转) + - 右键拖拽:控制 DetectorSwing(X轴=摆动,Y轴=摆动) + +4. **快捷按钮** + - **使能开关**:启用/禁用摇杆控制(图标切换) + - **摇杆模式**:切换左右键功能(图标切换) + - **SZDZ锁定**:锁定 SourceZ/DetectorZ 同步移动(图标切换) + - **保存位置**:保存当前所有轴位置到快照 + - **恢复位置**:恢复到上次保存的位置快照 + +--- + +## 3. 获取轴状态和位置 | Reading Axis Status and Position 通过 DI 注入 `IMotionSystem`: @@ -154,7 +227,7 @@ public class YourController --- -## 4. 几何计算 | Geometry Calculation +## 5. 几何计算 | Geometry Calculation ```csharp public class YourGeometryUser @@ -207,7 +280,7 @@ public class YourGeometryUser --- -## 5. 事件订阅 | Event Subscription +## 6. 事件订阅 | Event Subscription 通过 Prism `IEventAggregator` 被动接收状态变化: @@ -260,35 +333,86 @@ public class YourMonitor --- -## 6. PLC 信号定义 | PLC Signal Definitions +## 7. PLC 信号定义 | PLC Signal Definitions -信号名称硬编码在 `MotionSignalNames.cs` 中,信号地址定义在 `PlcAddrDfn.xml`。 +信号名称硬编码在 `MotionSignalNames.cs` 中,信号地址定义在 `PlcAddrDfn.xml`(位于 `XplorePlane/bin/Debug/net8.0-windows/win-x64/`)。 -每个轴包含以下信号(以 SourceZ 为例): +### 7.1 写入信号(WriteCommon,DB31) -| 信号名 | 方向 | 类型 | 说明 | +| 信号名 | 类型 | 地址 | 说明 | |--------|------|------|------| -| `MC_SourceZ_Pos` | Read | single | 实际位置 | -| `MC_SourceZ_Target` | Write | single | 目标位置 | -| `MC_SourceZ_Speed` | Write | single | 运动速度 | -| `MC_SourceZ_JogPos` | Write | byte | 正向 Jog | -| `MC_SourceZ_JogNeg` | Write | byte | 反向 Jog | -| `MC_SourceZ_Home` | Write | byte | 回零 | -| `MC_SourceZ_Stop` | Write | byte | 停止 | +| `MC_SourceZ_Target` | single | 10 | 射线源Z目标位置(mm) | +| `MC_SourceZ_Speed` | single | 14 | 射线源Z运动速度 | +| `MC_SourceZ_JogPos` | byte | 18 | 射线源Z正向Jog | +| `MC_SourceZ_JogNeg` | byte | 19 | 射线源Z反向Jog | +| `MC_SourceZ_Home` | byte | 20 | 射线源Z回零 | +| `MC_SourceZ_Stop` | byte | 21 | 射线源Z停止 | +| `MC_DetZ_Target` | single | 22 | 探测器Z目标位置(mm) | +| `MC_DetZ_Speed` | single | 26 | 探测器Z运动速度 | +| `MC_DetZ_JogPos` | byte | 30 | 探测器Z正向Jog | +| `MC_DetZ_JogNeg` | byte | 31 | 探测器Z反向Jog | +| `MC_DetZ_Home` | byte | 32 | 探测器Z回零 | +| `MC_DetZ_Stop` | byte | 33 | 探测器Z停止 | +| `MC_StageX_Target` | single | 34 | 载物台X目标位置(mm) | +| `MC_StageX_Speed` | single | 38 | 载物台X运动速度 | +| `MC_StageX_JogPos` | byte | 42 | 载物台X正向Jog | +| `MC_StageX_JogNeg` | byte | 43 | 载物台X反向Jog | +| `MC_StageX_Home` | byte | 44 | 载物台X回零 | +| `MC_StageX_Stop` | byte | 45 | 载物台X停止 | +| `MC_StageY_Target` | single | 46 | 载物台Y目标位置(mm) | +| `MC_StageY_Speed` | single | 50 | 载物台Y运动速度 | +| `MC_StageY_JogPos` | byte | 54 | 载物台Y正向Jog | +| `MC_StageY_JogNeg` | byte | 55 | 载物台Y反向Jog | +| `MC_StageY_Home` | byte | 56 | 载物台Y回零 | +| `MC_StageY_Stop` | byte | 57 | 载物台Y停止 | +| `MC_DetSwing_Target` | single | 58 | 探测器摆动目标角度(度) | +| `MC_DetSwing_Speed` | single | 62 | 探测器摆动运动速度 | +| `MC_DetSwing_JogPos` | byte | 66 | 探测器摆动正向Jog | +| `MC_DetSwing_JogNeg` | byte | 67 | 探测器摆动反向Jog | +| `MC_DetSwing_Home` | byte | 68 | 探测器摆动回零 | +| `MC_DetSwing_Stop` | byte | 69 | 探测器摆动停止 | +| `MC_StageRot_Target` | single | 70 | 载物台旋转目标角度(度) | +| `MC_StageRot_Speed` | single | 74 | 载物台旋转运动速度 | +| `MC_StageRot_JogPos` | byte | 78 | 载物台旋转正向Jog | +| `MC_StageRot_JogNeg` | byte | 79 | 载物台旋转反向Jog | +| `MC_StageRot_Home` | byte | 80 | 载物台旋转回零 | +| `MC_StageRot_Stop` | byte | 81 | 载物台旋转停止 | +| `MC_FixRot_Target` | single | 82 | 夹具旋转目标角度(度) | +| `MC_FixRot_Speed` | single | 86 | 夹具旋转运动速度 | +| `MC_FixRot_JogPos` | byte | 90 | 夹具旋转正向Jog | +| `MC_FixRot_JogNeg` | byte | 91 | 夹具旋转反向Jog | +| `MC_FixRot_Home` | byte | 92 | 夹具旋转回零 | +| `MC_FixRot_Stop` | byte | 93 | 夹具旋转停止 | +| `MC_Door_Open` | byte | 94 | 安全门开门 | +| `MC_Door_Close` | byte | 95 | 安全门关门 | +| `MC_Door_Stop` | byte | 96 | 安全门停止 | +| `MC_SourceDetZ_Linkage_Enable` | bool | 101 | 射线源与探测器Z轴联动使能 | +| `MC_VirtualJoystick_Enable` | bool | 111 | 虚拟摇杆使能 | -安全门信号: +### 7.2 读取信号(ReadCommon,DB31) -| 信号名 | 方向 | 说明 | -|--------|------|------| -| `MC_Door_Open` | Write | 开门 | -| `MC_Door_Close` | Write | 关门 | -| `MC_Door_Stop` | Write | 停门 | -| `MC_Door_Status` | Read | 门状态(int: 0-6) | -| `MC_Door_Interlock` | Read | 联锁信号 | +| 信号名 | 类型 | 地址 | 说明 | +|--------|------|------|------| +| `MC_SourceZ_Pos` | single | 100 | 射线源Z实际位置(mm) | +| `MC_DetZ_Pos` | single | 104 | 探测器Z实际位置(mm) | +| `MC_StageX_Pos` | single | 108 | 载物台X实际位置(mm) | +| `MC_StageY_Pos` | single | 112 | 载物台Y实际位置(mm) | +| `MC_DetSwing_Angle` | single | 116 | 探测器摆动实际角度(度) | +| `MC_StageRot_Angle` | single | 120 | 载物台旋转实际角度(度) | +| `MC_FixRot_Angle` | single | 124 | 夹具旋转实际角度(度) | +| `MC_Door_Status` | byte | 128 | 安全门状态(0:未知,1:开门中,2:已开,3:关门中,4:已关,5:已锁定,6:故障) | +| `MC_Door_Interlock` | byte | 130 | 安全门联锁信号(0:无联锁,10:联锁有效) | +| `MC_Joystick_Active` | bool | 110 | 实体摇杆输入激活 | + +### 7.3 信号类型说明 | Signal Type Notes + +- **single**:32位浮点数(4字节) +- **byte**:8位无符号整数(1字节) +- **bool**:布尔值(1字节,0=false, 非0=true) --- -## 7. 安全机制 | Safety Mechanisms +## 8. 安全机制 | Safety Mechanisms | 机制 | 说明 | |------|------| @@ -303,7 +427,7 @@ public class YourMonitor --- -## 8. 轮询机制 | Polling Mechanism +## 9. 轮询机制 | Polling Mechanism `MotionControlService` 使用 `System.Threading.Timer` 以配置的 `PollingInterval`(默认 100ms)周期执行: @@ -319,4 +443,4 @@ public class YourMonitor --- -**最后更新 | Last Updated**: 2026-04-14 +**最后更新 | Last Updated**: 2026-05-08 diff --git a/XP.Hardware.MotionControl/Documents/README.md b/XP.Hardware.MotionControl/Documents/README.md index ed26742..323b65d 100644 --- a/XP.Hardware.MotionControl/Documents/README.md +++ b/XP.Hardware.MotionControl/Documents/README.md @@ -14,7 +14,7 @@ XP.Hardware.MotionControl 是 XplorePlane 平面CT工业检测系统的核心运 - 安全防护门控制(联锁检查、状态机) - FOD/FDD/放大倍率几何正算与反算 - 多轴联动移动(原子性边界检查) -- Jog 点动调试(MouseDown/MouseUp 安全控制) +- Jog 点动调试(虚拟摇杆控制) - 100ms 周期 PLC 状态轮询 - 基于 Prism EventAggregator 的跨模块事件通讯 - 可配置的轴启用/禁用(FixtureRotation 等可选轴) @@ -23,6 +23,56 @@ XP.Hardware.MotionControl 是 XplorePlane 平面CT工业检测系统的核心运 --- +## 界面组件 | UI Components + +### AxisControlView(核心界面控件) + +`AxisControlView.xaml` 是运动控制模块的核心用户控件,提供完整的轴控制和调试功能: + +``` +XP.Hardware.MotionControl/Views/ +├── AxisControlView.xaml # 核心轴控制面板(350px+ UserControl) +└── AxisControlView.xaml.cs +``` + +**功能组成 | Features** + +| 区域 | 组件 | 功能说明 | +|------|------|----------| +| **标题栏** | TextBlock | 显示"运动控制"标题 | +| **左侧** | RadNumericUpDown | 7个轴位置输入框(StageX/Y/SourceZ/DetectorZ/DetectorSwing/StageRotation/FixtureRotation) | +| **右侧** | VirtualJoystick | 单轴摇杆(Z轴)+ 双轴摇杆(XY+旋转) | +| **底部** | RadButton | 使能开关、摇杆模式切换、SZDZ锁定、保存/恢复位置 | + +**ViewModel 绑定 | ViewModel Bindings** + +| 属性 | 类型 | 说明 | +|------|------|------| +| `StageXPosition` / `StageYPosition` | double | 载物台 X/Y 轴位置(mm) | +| `SourceZPosition` / `DetectorZPosition` | double | 射线源/探测器 Z 轴位置(mm) | +| `DetectorSwingAngle` | double | 探测器摆动角度(度) | +| `StageRotationAngle` | double | 载物台旋转角度(度) | +| `FixtureRotationAngle` | double | 夹具旋转角度(度) | +| `IsJoystickEnabled` | bool | 摇杆使能开关 | +| `SwapMouseButtons` | bool | 摇杆左右键功能切换 | +| `SZDZLock` | bool | 射线源/探测器Z轴锁定移动 | +| `SavedPositions` | SavedPositions | 已保存的位置快照 | + +**交互方式 | Interaction Modes** + +1. **手动输入**:直接编辑 RadNumericUpDown,Enter 确认,Escape 取消 +2. **单轴摇杆**:拖拽单轴摇杆控制 SourceZ/DetectorZ 轴 Jog +3. **双轴摇杆**: + - 左键拖拽:控制 StageX/Y 轴(X轴=左右,Y轴=上下) + - 右键拖拽:控制 StageRotation/DetectorSwing 轴(X轴=旋转,Y轴=摆动) +4. **快捷按钮**: + - 使能开关:启用/禁用摇杆控制 + - 摇杆模式:切换左右键功能(左键XY/右键旋转 vs 左键旋转/右键摆动) + - SZDZ锁定:锁定 SourceZ/DetectorZ 同步移动 + - 保存/恢复:保存当前所有轴位置或恢复到上次保存位置 + +--- + ## 框架架构 | Architecture ``` @@ -56,17 +106,18 @@ XP.Hardware.MotionControl/ │ ├── IMotionControlService.cs # 业务服务接口 │ ├── MotionControlService.cs # 服务实现(轮询、事件、日志) │ └── GeometryCalculator.cs # 几何计算器 +├── ViewModels/ # 视图模型 +│ ├── AxisControlViewModel.cs # AxisControlView ViewModel +│ └── MotionControlViewModel.cs # MotionControlView ViewModel ├── Config/ # 配置层 │ ├── MotionControlConfig.cs # 配置实体 │ ├── ConfigLoader.cs # 配置加载器 │ └── MotionSignalNames.cs # PLC 信号名称常量 -├── ViewModels/ # 视图模型 -│ ├── MotionControlViewModel.cs # 操作面板 ViewModel -│ └── MotionDebugViewModel.cs # 调试窗口 ViewModel ├── Views/ # WPF 视图 -│ ├── MotionControlView.xaml # 操作面板(350px UserControl) -│ ├── MotionDebugWindow.xaml # Jog 调试窗口 -│ └── MotionControlView.xaml.cs +│ ├── AxisControlView.xaml # 核心轴控制面板(350px+) +│ ├── AxisControlView.xaml.cs +│ ├── MotionControlView.xaml # 简化版操作面板 +│ └── MotionDebugWindow.xaml # Jog 调试窗口 ├── Module/ │ └── MotionControlModule.cs # Prism 模块注册 ├── Resources/ # 多语言资源 @@ -123,17 +174,22 @@ XP.Hardware.MotionControl/ | `MotionControl:DetectorZ:Min/Max/Origin` | 0/600/0 | 探测器Z轴范围和原点(mm) | | `MotionControl:StageX:Min/Max/Origin` | -150/150/0 | 载物台X轴范围和原点(mm) | | `MotionControl:StageY:Min/Max/Origin` | -150/150/0 | 载物台Y轴范围和原点(mm) | -| `MotionControl:DetectorSwing:Min/Max/Enabled` | -45/45/true | 探测器摆动范围和启用 | -| `MotionControl:StageRotation:Min/Max/Enabled` | -360/360/true | 载物台旋转范围和启用 | -| `MotionControl:FixtureRotation:Min/Max/Enabled` | -90/90/false | 夹具旋转范围和启用 | +| `MotionControl:DetectorSwing:Min/Max/Origin/Enabled` | -45/45/0/true | 探测器摆动范围、原点和启用 | +| `MotionControl:StageRotation:Min/Max/Origin/Enabled` | -360/360/0/true | 载物台旋转范围、原点和启用 | +| `MotionControl:FixtureRotation:Min/Max/Origin/Enabled` | -90/90/0/false | 夹具旋转范围、原点和启用 | | `MotionControl:Geometry:SourceZOrigin` | 0 | 射线源Z原点偏移(mm) | | `MotionControl:Geometry:DetectorZOrigin` | 600 | 探测器Z原点偏移(mm) | -| `MotionControl:Geometry:StageRotationCenterZ` | 300 | 旋转中心Z坐标(mm) | +| `MotionControl:Geometry:StageRotationCenterZ` | 0 | 旋转中心Z坐标(mm,固定值) | +| `MotionControl:Geometry:SwingPivotOffset` | 0 | 探测器摆动旋转中心Z偏移(mm) | +| `MotionControl:Geometry:SwingRadius` | 0 | 探测器摆动半径(mm) | | `MotionControl:PollingInterval` | 100 | 轮询周期(ms) | | `MotionControl:DefaultVelocity` | 100 | 默认速度 | +| `MotionControl:SourceDetectorZLinkage:Enabled` | false | 射线源与探测器Z轴联动使能 | +| `MotionControl:SourceDetectorZLinkage:TriggerThreshold` | 1.0 | 联动触发的位置变化阈值(mm) | +| `MotionControl:SourceDetectorZLinkage:SpeedPercent` | 100 | 联动移动速度百分比(0-100) | PLC 信号名称硬编码在 `MotionSignalNames.cs` 中,信号定义在 `PlcAddrDfn.xml` 的 ReadCommon/WriteCommon 组。 --- -**最后更新 | Last Updated**: 2026-04-14 +**最后更新 | Last Updated**: 2026-05-08 diff --git a/XP.Hardware.MotionControl/Implementations/PlcJoystick.cs b/XP.Hardware.MotionControl/Implementations/PlcJoystick.cs new file mode 100644 index 0000000..37cef0e --- /dev/null +++ b/XP.Hardware.MotionControl/Implementations/PlcJoystick.cs @@ -0,0 +1,32 @@ +using System; +using XP.Hardware.MotionControl.Abstractions; +using XP.Hardware.MotionControl.Config; +using XP.Hardware.Plc.Abstractions; + +namespace XP.Hardware.MotionControl.Implementations +{ + /// + /// 基于 PLC 的实体摇杆实现 | PLC-based Physical Joystick Implementation + /// 信号名称硬编码在 MotionSignalNames 中 | Signal names hardcoded in MotionSignalNames + /// + public class PlcJoystick : IJoystick + { + private readonly ISignalDataService _signalService; + private bool _isJoystickActive; + + public PlcJoystick(ISignalDataService signalService) + { + _signalService = signalService ?? throw new ArgumentNullException(nameof(signalService)); + } + + /// + public bool IsJoystickActive => _isJoystickActive; + + /// + public void UpdateStatus() + { + // 读取实体摇杆输入激活信号 | Read physical joystick input active signal + _isJoystickActive = _signalService.GetValueByName(MotionSignalNames.Joystick_Active) == 10; + } + } +} diff --git a/XP.Hardware.MotionControl/Implementations/PlcMotionSystem.cs b/XP.Hardware.MotionControl/Implementations/PlcMotionSystem.cs index d95c55a..3f22b23 100644 --- a/XP.Hardware.MotionControl/Implementations/PlcMotionSystem.cs +++ b/XP.Hardware.MotionControl/Implementations/PlcMotionSystem.cs @@ -16,6 +16,7 @@ namespace XP.Hardware.MotionControl.Implementations private readonly Dictionary _linearAxes = new(); private readonly Dictionary _rotaryAxes = new(); private readonly ISafetyDoor _safetyDoor; + private readonly IJoystick _joystick; private readonly IAxisReset _axisReset; public PlcMotionSystem(MotionControlConfig config, ISignalDataService signalService) @@ -70,6 +71,9 @@ namespace XP.Hardware.MotionControl.Implementations // 创建安全门 | Create safety door _safetyDoor = new PlcSafetyDoor(signalService); + // 创建摇杆 | Create joystick + _joystick = new PlcJoystick(signalService); + // 创建轴复位 | Create axis reset _axisReset = new PlcAxisReset(signalService); } @@ -77,6 +81,8 @@ namespace XP.Hardware.MotionControl.Implementations /// public ISafetyDoor SafetyDoor => _safetyDoor; /// + public IJoystick Joystick => _joystick; + /// public IAxisReset AxisReset => _axisReset; /// public IReadOnlyDictionary LinearAxes => _linearAxes; @@ -103,6 +109,7 @@ namespace XP.Hardware.MotionControl.Implementations foreach (var axis in _linearAxes.Values) axis.UpdateStatus(); foreach (var axis in _rotaryAxes.Values) axis.UpdateStatus(); _safetyDoor.UpdateStatus(); + _joystick.UpdateStatus(); _axisReset.UpdateStatus(); } } diff --git a/XP.Hardware.MotionControl/Services/MotionControlService.cs b/XP.Hardware.MotionControl/Services/MotionControlService.cs index de58499..97fe3a1 100644 --- a/XP.Hardware.MotionControl/Services/MotionControlService.cs +++ b/XP.Hardware.MotionControl/Services/MotionControlService.cs @@ -131,6 +131,9 @@ namespace XP.Hardware.MotionControl.Services var oldDoorStatus = _motionSystem.SafetyDoor.Status; var oldInterlocked = _motionSystem.SafetyDoor.IsInterlocked; + // 缓存旧的摇杆状态 | Cache old joystick status + var oldJoystickActive = _motionSystem.Joystick.IsJoystickActive; + // 更新所有轴和门状态 | Update all axis and door status _motionSystem.UpdateAllStatus(); @@ -185,6 +188,15 @@ namespace XP.Hardware.MotionControl.Services _logger.Info("门联锁状态变化:{IsInterlocked} | Door interlock status changed: {IsInterlocked}", _motionSystem.SafetyDoor.IsInterlocked); } + + // 检测实体摇杆状态变化 | Detect physical joystick status changes + if (oldJoystickActive != _motionSystem.Joystick.IsJoystickActive) + { + _eventAggregator.GetEvent() + .Publish(_motionSystem.Joystick.IsJoystickActive); + _logger.Info("实体摇杆状态变化:{IsJoystickActive} | Physical joystick status changed: {IsJoystickActive}", + _motionSystem.Joystick.IsJoystickActive); + } // 轮询成功,重置错误计数 | Poll succeeded, reset error count _pollErrorCount = 0; } diff --git a/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs b/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs index bd3272b..8e314d7 100644 --- a/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs +++ b/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs @@ -100,6 +100,7 @@ namespace XP.Hardware.MotionControl.ViewModels // 订阅事件 | Subscribe to events _eventAggregator.GetEvent().Subscribe(OnGeometryUpdated, ThreadOption.UIThread); _eventAggregator.GetEvent().Subscribe(OnAxisStatusChanged, ThreadOption.UIThread); + _eventAggregator.GetEvent().Subscribe(OnJoystickActiveChanged, ThreadOption.UIThread); // 初始化时主动刷新一次轴位置 | Refresh axis positions on initialization try @@ -265,6 +266,10 @@ namespace XP.Hardware.MotionControl.ViewModels /// PLC 连接状态 | PLC connection status public bool IsPlcConnected { get => _isPlcConnected; set => SetProperty(ref _isPlcConnected, value); } + private bool _isJoystickActive; + /// 实体摇杆是否激活 | Whether physical joystick is active + public bool IsJoystickActive { get => _isJoystickActive; set => SetProperty(ref _isJoystickActive, value); } + private string _errorMessage; /// 错误提示信息 | Error message public string ErrorMessage { get => _errorMessage; set => SetProperty(ref _errorMessage, value); } @@ -302,6 +307,16 @@ namespace XP.Hardware.MotionControl.ViewModels #region 事件回调 | Event Callbacks + /// + /// 实体摇杆激活状态变化回调 | Physical joystick active status changed callback + /// + /// 实体摇杆是否激活 | Whether physical joystick is active + private void OnJoystickActiveChanged(bool isActive) + { + IsJoystickActive = isActive; + _logger.Debug("实体摇杆状态更新:{IsActive} | Physical joystick status updated: {IsActive}", isActive); + } + /// /// 几何参数更新回调,刷新轴实际位置 | Geometry updated callback, refresh axis actual positions /// @@ -566,6 +581,20 @@ namespace XP.Hardware.MotionControl.ViewModels MessageBoxButton.OK, MessageBoxImage.Error); } } + + /// + /// 同步联动状态到 PLC(供视图加载时调用)| Sync linkage state to PLC (called when view loads) + /// + /// 操作结果 | Operation result + public MotionResult SetSZDZLinkageToPlc() + { + return _motionControlService.SetSourceDetectorZLinkage(SZDZLock); + } + + /// + /// 获取日志服务实例 | Get logger service instance + /// + public ILoggerService Logger => _logger; #endregion #region 摇杆 Jog 映射逻辑 | Joystick Jog Mapping Logic @@ -798,7 +827,16 @@ namespace XP.Hardware.MotionControl.ViewModels var value = GetPropertyValue(propertyName); SafeRun(() => { - var result = SendMoveCommand(propertyName, value); + MotionResult result; + // 如果射线源与探测器Z轴联动,且操作的是SourceZ或DetectorZ,则同时发送两个轴 + if (SZDZLock && (propertyName == nameof(SourceZPosition) || propertyName == nameof(DetectorZPosition))) + { + result = SendSourceDetectorZMoveCommand(value); + } + else + { + result = SendMoveCommand(propertyName, value); + } if (result.Success) _logger.Info("目标位置已发送:{Property}={Value} | Target position sent: {Property}={Value}", propertyName, value); else @@ -831,7 +869,16 @@ namespace XP.Hardware.MotionControl.ViewModels SetPropertyValue(propertyName, newValue); SafeRun(() => { - var result = SendMoveCommand(propertyName, newValue); + MotionResult result; + // 如果射线源与探测器Z轴联动,且操作的是SourceZ或DetectorZ,则同时步进两个轴 + if (SZDZLock && (propertyName == nameof(SourceZPosition) || propertyName == nameof(DetectorZPosition))) + { + result = SendSourceDetectorZMoveCommand(newValue); + } + else + { + result = SendMoveCommand(propertyName, newValue); + } if (!result.Success) _logger.Warn("步进移动失败:{Property},原因={Reason} | Step move failed: {Property}, reason={Reason}", propertyName, result.ErrorMessage); }); @@ -866,6 +913,96 @@ namespace XP.Hardware.MotionControl.ViewModels } } + /// + /// 发送射线源与探测器Z轴联动移动命令 | Send Source-Detector Z-axis linkage move command + /// 当SZDZLock=true时,保持两个轴的位移量相同 + /// When SZDZLock=true, keeps the same displacement for both axes + /// + /// 目标位置 | Target position + /// 操作结果 | Operation result + private MotionResult SendSourceDetectorZMoveCommand(double targetValue) + { + // 1. 检查联动是否在配置中启用 | Check if linkage is enabled in config + if (!_config.SourceDetectorZLinkage.Enabled) + { + _logger.Warn("射线源与探测器Z轴联动未在配置中启用 | Source-Detector Z-axis linkage not enabled in config"); + return MotionResult.Fail("射线源与探测器Z轴联动未启用 | Source-Detector Z-axis linkage not enabled"); + } + + // 2. 获取两个轴并检查是否都在空闲状态 | Get both axes and check if both are idle + var sourceZAxis = _motionSystem.GetLinearAxis(AxisId.SourceZ); + var detectorZAxis = _motionSystem.GetLinearAxis(AxisId.DetectorZ); + + if (sourceZAxis.Status == AxisStatus.Moving) + { + _logger.Warn("射线源Z轴正在运动中,拒绝联动移动命令 | Source Z-axis is moving, linkage move command rejected"); + return MotionResult.Fail("射线源Z轴正在运动中,无法联动移动 | Source Z-axis is moving, linkage move rejected"); + } + + if (detectorZAxis.Status == AxisStatus.Moving) + { + _logger.Warn("探测器Z轴正在运动中,拒绝联动移动命令 | Detector Z-axis is moving, linkage move command rejected"); + return MotionResult.Fail("探测器Z轴正在运动中,无法联动移动 | Detector Z-axis is moving, linkage move rejected"); + } + + // 3. 计算位移量和目标位置 | Calculate displacement and target positions + var currentSourceZ = sourceZAxis.ActualPosition; + var currentDetectorZ = detectorZAxis.ActualPosition; + var sourceDelta = targetValue - currentSourceZ; + var targetDetectorZ = currentDetectorZ + sourceDelta; + + // 4. 边界检查(使用配置中的 Min/Max)| Boundary check (use Min/Max from config) + if (_config.LinearAxes.TryGetValue(AxisId.SourceZ, out var sourceConfig) && + _config.LinearAxes.TryGetValue(AxisId.DetectorZ, out var detectorConfig)) + { + var errors = new List(); + + if (targetValue < sourceConfig.Min || targetValue > sourceConfig.Max) + { + errors.Add($"射线源Z轴目标位置 {targetValue} 超出范围 [{sourceConfig.Min}, {sourceConfig.Max}] | " + + $"Source Z-axis target {targetValue} out of range [{sourceConfig.Min}, {sourceConfig.Max}]"); + } + + if (targetDetectorZ < detectorConfig.Min || targetDetectorZ > detectorConfig.Max) + { + errors.Add($"探测器Z轴目标位置 {targetDetectorZ} 超出范围 [{detectorConfig.Min}, {detectorConfig.Max}] | " + + $"Detector Z-axis target {targetDetectorZ} out of range [{detectorConfig.Min}, {detectorConfig.Max}]"); + } + + if (errors.Count > 0) + { + var allErrors = string.Join("; ", errors); + _logger.Warn("联动移动边界检查失败:{Errors} | Linkage move boundary check failed: {Errors}", allErrors); + return MotionResult.Fail(allErrors); + } + } + else + { + _logger.Error(null, "未找到射线源Z或探测器Z轴配置 | Source Z or Detector Z axis config not found"); + return MotionResult.Fail("轴配置缺失 | Axis config missing"); + } + + // 5. 所有检查通过,同时发送两个轴的移动命令 | All checks passed, send move commands to both axes simultaneously + var sourceResult = sourceZAxis.MoveToTarget(targetValue, _config.DefaultVelocity); + var detectorResult = detectorZAxis.MoveToTarget(targetDetectorZ, _config.DefaultVelocity); + + // 6. 返回结果(任一失败都记录日志)| Return result (log if any fails) + if (!sourceResult.Success) + { + _logger.Warn("射线源Z轴移动失败:{Reason} | Source Z-axis move failed: {Reason}", sourceResult.ErrorMessage); + return sourceResult; + } + + if (!detectorResult.Success) + { + _logger.Warn("探测器Z轴移动失败:{Reason} | Detector Z-axis move failed: {Reason}", detectorResult.ErrorMessage); + return detectorResult; + } + + _logger.Info("射线源与探测器Z轴联动移动成功 | Source-Detector Z-axis linkage move successful"); + return sourceResult; + } + /// /// 根据属性名称获取当前绑定值 | Get current bound value by property name /// diff --git a/XP.Hardware.MotionControl/Views/AxisControlView.xaml.cs b/XP.Hardware.MotionControl/Views/AxisControlView.xaml.cs index 58a421b..b8fdcf3 100644 --- a/XP.Hardware.MotionControl/Views/AxisControlView.xaml.cs +++ b/XP.Hardware.MotionControl/Views/AxisControlView.xaml.cs @@ -18,6 +18,9 @@ namespace XP.Hardware.MotionControl.Views { InitializeComponent(); + // 监听视图 Loaded 事件,在视图加载时同步联动状态到 PLC | Listen to view Loaded event, sync linkage state to PLC on view load + Loaded += AxisControlView_Loaded; + // 监听摇杆只读依赖属性变化,推送到 ViewModel | Listen to joystick read-only DP changes and push to ViewModel var dualOutputXDesc = DependencyPropertyDescriptor.FromProperty(VirtualJoystick.OutputXProperty, typeof(VirtualJoystick)); var dualOutputYDesc = DependencyPropertyDescriptor.FromProperty(VirtualJoystick.OutputYProperty, typeof(VirtualJoystick)); @@ -230,5 +233,25 @@ namespace XP.Hardware.MotionControl.Views } #endregion + + #region 视图加载事件 | View Loaded Event + + private void AxisControlView_Loaded(object sender, RoutedEventArgs e) + { + // 视图加载时同步联动状态到 PLC | Sync linkage state to PLC when view is loaded + var viewModel = ViewModel; + if (viewModel != null && viewModel.SZDZLock) + { + // 调用服务设置联动 | Call service to set linkage + var result = viewModel.SetSZDZLinkageToPlc(); + if (!result.Success) + { + // 记录日志但不阻塞加载 | Log but don't block loading + viewModel.Logger.Warn("视图加载时同步联动状态失败:{Reason} | Failed to sync linkage state on view load: {Reason}", result.ErrorMessage); + } + } + } + + #endregion } }