15 KiB
15 KiB
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(正向/反向)
信号下发流程:
// 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(正向/反向)
信号下发流程:
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%):
var speedPercent = Math.Abs(output) * 100; // 0% ~ 100%
速度计算流程:
-
MotionControlService.SetJogSpeed 计算实际速度:
var actualSpeed = _config.DefaultVelocity * speedPercent / 100.0; // 例如:DefaultVelocity=100, speedPercent=50 → actualSpeed=50 mm/s -
PlcLinearAxis.SetJogSpeed 将实际速度写入 PLC:
_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 = 100mm/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
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
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):
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"按钮切换联动状态:
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 会同时移动两个轴,保持位移量相同:
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 信号下发:
-
SourceZ 移动:
MC_SourceZ_Target= targetValue(地址 10)MC_SourceZ_Speed= DefaultVelocity(地址 14)
-
DetectorZ 移动:
MC_DetZ_Target= currentDetectorZ + (targetValue - currentSourceZ)(地址 22)MC_DetZ_Speed= DefaultVelocity(地址 26)
3.3 联动 Jog | Linkage Jog
当 SZDZLock=true 时,单轴摇杆的左右键会同时控制两个轴:
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
点击"使能开关"按钮切换虚拟摇杆使能状态:
private void ExecuteToggleEnable()
{
IsJoystickEnabled = !IsJoystickEnabled;
_motionControlService.SetVirtualJoystickEnable(IsJoystickEnabled);
}
PLC 信号:
| 信号名 | 地址 | 类型 | 说明 |
|---|---|---|---|
MC_VirtualJoystick_Enable |
111 | bool | 虚拟摇杆使能(true=启用, false=禁用) |
5. 信号写入队列机制 | Signal Write Queue Mechanism
所有 PLC 写入操作通过 ISignalDataService.EnqueueWrite() 进入写入队列:
public interface ISignalDataService
{
bool EnqueueWrite(string signalName, object value);
bool EnqueueWriteBatch(Dictionary<string, object> writes);
}
队列处理流程:
- ViewModel 调用
EnqueueWrite(signalName, value) - 信号数据服务将写入请求加入队列
- PLC 服务在轮询周期(默认 100ms)中批量写入
- 写入成功后从队列移除
优势:
- 批量写入减少 PLC 通讯次数
- 避免频繁写入导致的通讯拥塞
- 统一的错误处理和重试机制