449 lines
15 KiB
Markdown
449 lines
15 KiB
Markdown
# 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<string, object> writes);
|
||
}
|
||
```
|
||
|
||
**队列处理流程**:
|
||
|
||
1. ViewModel 调用 `EnqueueWrite(signalName, value)`
|
||
2. 信号数据服务将写入请求加入队列
|
||
3. PLC 服务在轮询周期(默认 100ms)中批量写入
|
||
4. 写入成功后从队列移除
|
||
|
||
**优势**:
|
||
|
||
- 批量写入减少 PLC 通讯次数
|
||
- 避免频繁写入导致的通讯拥塞
|
||
- 统一的错误处理和重试机制
|
||
|
||
--- |