Files
XplorePlane/XP.Hardware.MotionControl/Documents/AxisControlView_PLC_Communication.md
T

449 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 通讯次数
- 避免频繁写入导致的通讯拥塞
- 统一的错误处理和重试机制
---