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
}
}