运动控制:新增射线源与探测器 Z 轴联动移动功能,支持同步位移控制,新增实体摇杆实现 PLC 摇杆状态监控和事件发布,IMotionSystem 接口新增 Joystick 属性,更新文档说明联动功能使用方法。

This commit is contained in:
QI Mingxuan
2026-05-09 11:30:05 +08:00
parent fa8ad29862
commit f4a2856b92
11 changed files with 910 additions and 39 deletions
@@ -0,0 +1,11 @@
using Prism.Events;
namespace XP.Hardware.MotionControl.Abstractions.Events
{
/// <summary>
/// 实体摇杆激活状态变化事件 | Physical Joystick Active Status Changed Event
/// </summary>
public class JoystickActiveEvent : PubSubEvent<bool>
{
}
}
@@ -0,0 +1,17 @@
using XP.Hardware.MotionControl.Abstractions.Enums;
namespace XP.Hardware.MotionControl.Abstractions
{
/// <summary>
/// 摇杆接口 | Joystick Interface
/// 定义实体摇杆输入状态的读取能力 | Defines physical joystick input status reading capability
/// </summary>
public interface IJoystick
{
/// <summary>实体摇杆输入是否激活 | Whether physical joystick input is active</summary>
bool IsJoystickActive { get; }
/// <summary>从 PLC 更新状态 | Update status from PLC</summary>
void UpdateStatus();
}
}
@@ -22,6 +22,9 @@ namespace XP.Hardware.MotionControl.Abstractions
/// <summary>安全门实例 | Safety door instance</summary>
ISafetyDoor SafetyDoor { get; }
/// <summary>摇杆实例 | Joystick instance</summary>
IJoystick Joystick { get; }
/// <summary>轴复位实例 | Axis reset instance</summary>
IAxisReset AxisReset { get; }
@@ -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 | 正向 Jog0:缺省, 1:点动) |
| StageX | `MC_StageX_JogNeg` | 43 | 反向 Jog0:缺省, 1:点动) |
| StageY | `MC_StageY_JogPos` | 54 | 正向 Jog0:缺省, 1:点动) |
| StageY | `MC_StageY_JogNeg` | 55 | 反向 Jog0:缺省, 1:点动) |
| DetectorSwing | `MC_DetSwing_JogPos` | 66 | 正向 Jog0:缺省, 1:点动) |
| DetectorSwing | `MC_DetSwing_JogNeg` | 67 | 反向 Jog0:缺省, 1:点动) |
| StageRotation | `MC_StageRot_JogPos` | 78 | 正向 Jog0:缺省, 1:点动) |
| StageRotation | `MC_StageRot_JogNeg` | 79 | 反向 Jog0:缺省, 1:点动) |
| FixtureRotation | `MC_FixRot_JogPos` | 90 | 正向 Jog0:缺省, 1:点动) |
| FixtureRotation | `MC_FixRot_JogNeg` | 91 | 反向 Jog0:缺省, 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 | 正向 Jog0:缺省, 1:点动) |
| SourceZ | `MC_SourceZ_JogNeg` | 19 | 反向 Jog0:缺省, 1:点动) |
| DetectorZ | `MC_DetZ_JogPos` | 30 | 正向 Jog0:缺省, 1:点动) |
| DetectorZ | `MC_DetZ_JogNeg` | 31 | 反向 Jog0:缺省, 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<string, object> writes);
}
```
**队列处理流程**
1. ViewModel 调用 `EnqueueWrite(signalName, value)`
2. 信号数据服务将写入请求加入队列
3. PLC 服务在轮询周期(默认 100ms)中批量写入
4. 写入成功后从队列移除
**优势**
- 批量写入减少 PLC 通讯次数
- 避免频繁写入导致的通讯拥塞
- 统一的错误处理和重试机制
---
+149 -25
View File
@@ -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
<UserControl x:Class="YourView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:motion="clr-namespace:XP.Hardware.MotionControl.Views;assembly=XP.Hardware.MotionControl">
<Grid>
<!-- 基本使用 | Basic usage -->
<motion:AxisControlView />
<!-- 设置最小宽度 | Set minimum width -->
<motion:AxisControlView MinWidth="400" />
</Grid>
</UserControl>
```
**自动 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轴=上下)
- 右键拖拽:控制 StageRotationX轴=旋转)
- 切换模式(SwapMouseButtons=true):
- 左键拖拽:控制 StageRotationX轴=旋转)
- 右键拖拽:控制 DetectorSwingX轴=摆动,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 写入信号(WriteCommonDB31
| 信号名 | 方向 | 类型 | 说明 |
| 信号名 | 类型 | 地址 | 说明 |
|--------|------|------|------|
| `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 读取信号(ReadCommonDB31
| 信号名 | 方向 | 说明 |
|--------|------|------|
| `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
+68 -12
View File
@@ -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. **手动输入**:直接编辑 RadNumericUpDownEnter 确认,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
@@ -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
{
/// <summary>
/// 基于 PLC 的实体摇杆实现 | PLC-based Physical Joystick Implementation
/// 信号名称硬编码在 MotionSignalNames 中 | Signal names hardcoded in MotionSignalNames
/// </summary>
public class PlcJoystick : IJoystick
{
private readonly ISignalDataService _signalService;
private bool _isJoystickActive;
public PlcJoystick(ISignalDataService signalService)
{
_signalService = signalService ?? throw new ArgumentNullException(nameof(signalService));
}
/// <inheritdoc/>
public bool IsJoystickActive => _isJoystickActive;
/// <inheritdoc/>
public void UpdateStatus()
{
// 读取实体摇杆输入激活信号 | Read physical joystick input active signal
_isJoystickActive = _signalService.GetValueByName<byte>(MotionSignalNames.Joystick_Active) == 10;
}
}
}
@@ -16,6 +16,7 @@ namespace XP.Hardware.MotionControl.Implementations
private readonly Dictionary<AxisId, ILinearAxis> _linearAxes = new();
private readonly Dictionary<RotaryAxisId, IRotaryAxis> _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
/// <inheritdoc/>
public ISafetyDoor SafetyDoor => _safetyDoor;
/// <inheritdoc/>
public IJoystick Joystick => _joystick;
/// <inheritdoc/>
public IAxisReset AxisReset => _axisReset;
/// <inheritdoc/>
public IReadOnlyDictionary<AxisId, ILinearAxis> 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();
}
}
@@ -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<JoystickActiveEvent>()
.Publish(_motionSystem.Joystick.IsJoystickActive);
_logger.Info("实体摇杆状态变化:{IsJoystickActive} | Physical joystick status changed: {IsJoystickActive}",
_motionSystem.Joystick.IsJoystickActive);
}
// 轮询成功,重置错误计数 | Poll succeeded, reset error count
_pollErrorCount = 0;
}
@@ -100,6 +100,7 @@ namespace XP.Hardware.MotionControl.ViewModels
// 订阅事件 | Subscribe to events
_eventAggregator.GetEvent<GeometryUpdatedEvent>().Subscribe(OnGeometryUpdated, ThreadOption.UIThread);
_eventAggregator.GetEvent<AxisStatusChangedEvent>().Subscribe(OnAxisStatusChanged, ThreadOption.UIThread);
_eventAggregator.GetEvent<JoystickActiveEvent>().Subscribe(OnJoystickActiveChanged, ThreadOption.UIThread);
// 初始化时主动刷新一次轴位置 | Refresh axis positions on initialization
try
@@ -265,6 +266,10 @@ namespace XP.Hardware.MotionControl.ViewModels
/// <summary>PLC 连接状态 | PLC connection status</summary>
public bool IsPlcConnected { get => _isPlcConnected; set => SetProperty(ref _isPlcConnected, value); }
private bool _isJoystickActive;
/// <summary>实体摇杆是否激活 | Whether physical joystick is active</summary>
public bool IsJoystickActive { get => _isJoystickActive; set => SetProperty(ref _isJoystickActive, value); }
private string _errorMessage;
/// <summary>错误提示信息 | Error message</summary>
public string ErrorMessage { get => _errorMessage; set => SetProperty(ref _errorMessage, value); }
@@ -302,6 +307,16 @@ namespace XP.Hardware.MotionControl.ViewModels
#region | Event Callbacks
/// <summary>
/// 实体摇杆激活状态变化回调 | Physical joystick active status changed callback
/// </summary>
/// <param name="isActive">实体摇杆是否激活 | Whether physical joystick is active</param>
private void OnJoystickActiveChanged(bool isActive)
{
IsJoystickActive = isActive;
_logger.Debug("实体摇杆状态更新:{IsActive} | Physical joystick status updated: {IsActive}", isActive);
}
/// <summary>
/// 几何参数更新回调,刷新轴实际位置 | Geometry updated callback, refresh axis actual positions
/// </summary>
@@ -566,6 +581,20 @@ namespace XP.Hardware.MotionControl.ViewModels
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 同步联动状态到 PLC(供视图加载时调用)| Sync linkage state to PLC (called when view loads)
/// </summary>
/// <returns>操作结果 | Operation result</returns>
public MotionResult SetSZDZLinkageToPlc()
{
return _motionControlService.SetSourceDetectorZLinkage(SZDZLock);
}
/// <summary>
/// 获取日志服务实例 | Get logger service instance
/// </summary>
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
}
}
/// <summary>
/// 发送射线源与探测器Z轴联动移动命令 | Send Source-Detector Z-axis linkage move command
/// 当SZDZLock=true时,保持两个轴的位移量相同
/// When SZDZLock=true, keeps the same displacement for both axes
/// </summary>
/// <param name="targetValue">目标位置 | Target position</param>
/// <returns>操作结果 | Operation result</returns>
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<string>();
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;
}
/// <summary>
/// 根据属性名称获取当前绑定值 | Get current bound value by property name
/// </summary>
@@ -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
}
}