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

804 lines
29 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
↓ (调用)
IMotionSystem → PlcLinearAxis / PlcRotaryAxis
↓ (调用)
ISignalDataService.EnqueueWrite()
↓ (写入队列)
PLC (DB31, WriteCommon 组)
```
**关键分层说明**
- `AxisControlViewModel` 调用 `IMotionControlService` 的业务方法
- `IMotionControlService` 委托给 `IMotionSystem` 获取轴实例(`PlcLinearAxis` / `PlcRotaryAxis`
- 轴实例内部通过 `ISignalDataService.EnqueueWrite()` 写入 PLC 信号
- ViewModel 不直接操作信号,所有信号写入由轴实现层完成
## 1. 摇杆 Jog 操作 | Joystick Jog Operation
### 1.1 双轴摇杆(圆形)| Dual Axis Joystick (Circular)
**左键拖拽(默认模式)**
- X 轴输出 → StageX Jog(正向/反向)
- Y 轴输出 → StageY Jog(正向/反向)
**右键拖拽(默认模式)**
- X 轴输出 → DetectorSwing Jog(正向/反向)
- Y 轴输出 → StageRotation/FixtureRotation Jog(正向/反向)
> 注:当 `SwapMouseButtons=true` 时,左右键功能互换。
**信号下发流程**
```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 处理 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. MotionControlService.JogStart 委托给轴实现
public MotionResult JogStart(AxisId axisId, bool positive)
{
var axis = _motionSystem.GetLinearAxis(axisId);
// Homing 状态检查
if (axis.Status == AxisStatus.Homing)
return MotionResult.Fail("轴正在回零,拒绝 Jog 命令");
return axis.JogStart(positive);
}
// 4. PlcLinearAxis.JogStart 写入 PLC 信号
public override MotionResult JogStart(bool positive)
{
if (Status == AxisStatus.Homing)
return MotionResult.Fail("轴正在回零,拒绝 Jog 命令");
var signal = positive ? _jogPosSignal : _jogNegSignal;
_signalService.EnqueueWrite(signal, true);
return MotionResult.Ok();
}
```
**旋转轴 Jog 流程类似**`UpdateRotaryJog``SetJogRotarySpeed``JogRotaryStart`),额外包含禁用轴检查。
**PLC 信号地址(直线轴 Jog**
| 轴 | 信号名 | 地址 | 说明 |
|----|--------|------|------|
| 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:点动) |
| 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:点动) |
**PLC 信号地址(旋转轴 Jog**
| 轴 | 信号名 | 地址 | 说明 |
|----|--------|------|------|
| 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;
}
}
```
### 1.3 按键释放自动停止 | Button Release Auto-Stop
当摇杆按键从非 None 变为 None 时,自动停止所有关联轴的 Jog:
```csharp
// DualJoystickActiveButton setter 中
if (value == MouseButtonType.None && oldValue != MouseButtonType.None)
StopDualJoystickAxes();
// StopDualJoystickAxes 停止所有双轴摇杆关联轴
private void StopDualJoystickAxes()
{
UpdateLinearJog(AxisId.StageX, 0);
UpdateLinearJog(AxisId.StageY, 0);
UpdateRotaryJog(RotaryAxisId.DetectorSwing, 0);
var rotationAxisId = GetEnabledRotationAxisId();
if (rotationAxisId.HasValue)
UpdateRotaryJog(rotationAxisId.Value, 0);
}
```
单轴摇杆同理(`StopSingleJoystickAxes` 停止 SourceZ 和 DetectorZ)。
### 1.4 Jog 速度控制 | Jog Speed Control
摇杆输出值(-1.0 ~ 1.0)映射为速度百分比(0% ~ 100%):
```csharp
var speedPercent = Math.Abs(output) * 100; // 0% ~ 100%
```
**速度计算流程**
1. **MotionControlService.SetJogSpeed** 计算实际速度:
```csharp
public MotionResult SetJogSpeed(AxisId axisId, double speedPercent)
{
var axis = _motionSystem.GetLinearAxis(axisId);
var actualSpeed = _config.DefaultVelocity * speedPercent / 100.0;
return axis.SetJogSpeed(actualSpeed);
}
```
2. **PlcLinearAxis.SetJogSpeed** 将实际速度写入 PLC
```csharp
public override MotionResult SetJogSpeed(double speedPercent)
{
// 注意:参数名为 speedPercent,但实际接收的是已计算的速度值(mm/s 或 度/s)
_signalService.EnqueueWrite(_speedSignal, (float)speedPercent);
return MotionResult.Ok();
}
```
**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 / 100 = 30 mm/s
- 摇杆输出 100% → PLC 接收速度 = 100 × 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(StageYPosition):
return _motionControlService.MoveToTarget(AxisId.StageY, value);
case nameof(SourceZPosition):
return _motionControlService.MoveToTarget(AxisId.SourceZ, value);
case nameof(DetectorZPosition):
return _motionControlService.MoveToTarget(AxisId.DetectorZ, value);
case nameof(DetectorSwingAngle):
return _motionControlService.MoveRotaryToTarget(RotaryAxisId.DetectorSwing, value);
case nameof(StageRotationAngle):
return _motionControlService.MoveRotaryToTarget(RotaryAxisId.StageRotation, value);
case nameof(FixtureRotationAngle):
return _motionControlService.MoveRotaryToTarget(RotaryAxisId.FixtureRotation, value);
default:
return MotionResult.Fail($"未知的属性名称:{propertyName}");
}
}
```
### 2.3 单轴移动 | Single Axis Move
```csharp
// MotionControlService.MoveToTarget — 业务层
public MotionResult MoveToTarget(AxisId axisId, double target, double? speed = null)
{
var axis = _motionSystem.GetLinearAxis(axisId);
// 运动中防重入检查
if (axis.Status == AxisStatus.Moving)
return MotionResult.Fail("轴正在运动中,拒绝重复命令");
// 委托给轴实现(轴内部包含边界检查)
return axis.MoveToTarget(target, speed ?? _config.DefaultVelocity);
}
// PlcLinearAxis.MoveToTarget — 轴实现层
public override MotionResult MoveToTarget(double target, double? speed = null)
{
if (!ValidateTarget(target))
return MotionResult.Fail($"目标位置 {target} 超出范围 [{_min}, {_max}]");
if (Status == AxisStatus.Moving)
return MotionResult.Fail("轴正在运动中,拒绝重复命令");
if (speed.HasValue)
_signalService.EnqueueWrite(_speedSignal, (float)speed.Value);
_signalService.EnqueueWrite(_writeSignal, (float)target);
Status = AxisStatus.Moving;
return MotionResult.Ok();
}
```
**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);
SafeRun(() =>
{
MotionResult result;
if (SZDZLock && (propertyName == nameof(SourceZPosition) || propertyName == nameof(DetectorZPosition)))
{
result = SendSourceDetectorZMoveCommand(newValue);
}
else
{
result = SendMoveCommand(propertyName, newValue);
}
if (!result.Success)
_logger.Warn("步进移动失败:{Property},原因={Reason}", propertyName, result.ErrorMessage);
});
}
```
## 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, ...);
}
}
// MotionControlService.SetSourceDetectorZLinkage
public MotionResult SetSourceDetectorZLinkage(bool enabled)
{
var config = _config.SourceDetectorZLinkage;
if (!config.Enabled)
return MotionResult.Fail("射线源与探测器Z轴联动未启用");
var result = _signalService.EnqueueWrite(MotionSignalNames.SourceDetZ_Linkage_Enable, (bool)enabled);
return result ? MotionResult.Ok() : MotionResult.Fail("联动使能写入失败");
}
```
另外提供公开方法 `SetSZDZLinkageToPlc()` 供外部直接同步当前联动状态到 PLC:
```csharp
public MotionResult SetSZDZLinkageToPlc()
{
return _motionControlService.SetSourceDetectorZLinkage(SZDZLock);
}
```
**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 (_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轴目标位置超出范围");
if (targetDetectorZ < detectorConfig.Min || targetDetectorZ > detectorConfig.Max)
errors.Add("探测器Z轴目标位置超出范围");
if (errors.Count > 0)
return MotionResult.Fail(string.Join("; ", errors));
}
// 5. 同时发送两个轴的移动命令(通过轴实现层写入 PLC)
var sourceResult = sourceZAxis.MoveToTarget(targetValue, _config.DefaultVelocity);
var detectorResult = detectorZAxis.MoveToTarget(targetDetectorZ, _config.DefaultVelocity);
if (!sourceResult.Success) return sourceResult;
if (!detectorResult.Success) return detectorResult;
return MotionResult.Ok();
}
```
**PLC 信号下发**
1. **SourceZ 移动**
- `MC_SourceZ_Speed` = DefaultVelocity(地址 14
- `MC_SourceZ_Target` = targetValue(地址 10
2. **DetectorZ 移动**
- `MC_DetZ_Speed` = DefaultVelocity(地址 26
- `MC_DetZ_Target` = currentDetectorZ + (targetValue - currentSourceZ)(地址 22
### 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_Speed` = actualSpeed(地址 14
- `MC_SourceZ_JogPos`/`MC_SourceZ_JogNeg` = true(地址 18/19
- **DetectorZ Jog**
- `MC_DetZ_Speed` = actualSpeed(地址 26
- `MC_DetZ_JogPos`/`MC_DetZ_JogNeg` = true(地址 30/31
## 4. 虚拟摇杆使能 | Virtual Joystick Enable
点击"使能开关"按钮切换虚拟摇杆使能状态:
```csharp
private void ExecuteToggleEnable()
{
IsJoystickEnabled = !IsJoystickEnabled;
var result = _motionControlService.SetVirtualJoystickEnable(IsJoystickEnabled);
if (!result.Success)
{
MessageBox.Show(result.ErrorMessage, ...);
}
}
// MotionControlService.SetVirtualJoystickEnable
public MotionResult SetVirtualJoystickEnable(bool enabled)
{
var result = _signalService.EnqueueWrite(MotionSignalNames.VirtualJoystick_Enable, (bool)enabled);
return result ? MotionResult.Ok() : MotionResult.Fail("虚拟摇杆使能写入失败");
}
```
**PLC 信号**
| 信号名 | 地址 | 类型 | 说明 |
|--------|------|------|------|
| `MC_VirtualJoystick_Enable` | 111 | bool | 虚拟摇杆使能(true=启用, false=禁用) |
## 5. 鼠标按键交换 | Mouse Button Swap
点击"交换按键"按钮切换摇杆左右键功能映射:
```csharp
private void ExecuteToggleSwapMouseButtons()
{
SwapMouseButtons = !SwapMouseButtons;
}
```
此功能为纯 UI 层逻辑,不涉及 PLC 信号写入。当 `SwapMouseButtons=true` 时,摇杆控件内部交换左右键的功能映射。
## 6. 保存/恢复轴位置 | Save/Restore Axis Positions
### 6.1 保存当前位置 | Save Current Positions
```csharp
private void ExecuteSavePositions()
{
_savedPositions = new SavedPositions
{
StageX = StageXPosition,
StageY = StageYPosition,
SourceZ = SourceZPosition,
DetectorZ = DetectorZPosition,
DetectorSwing = DetectorSwingAngle,
StageRotation = StageRotationAngle,
FixtureRotation = FixtureRotationAngle
};
}
```
### 6.2 恢复保存的位置 | Restore Saved Positions
恢复时会同时向所有轴发送移动命令:
```csharp
private void ExecuteRestorePositions()
{
if (_savedPositions == null) return;
// 恢复到输入框
StageXPosition = _savedPositions.StageX;
StageYPosition = _savedPositions.StageY;
SourceZPosition = _savedPositions.SourceZ;
DetectorZPosition = _savedPositions.DetectorZ;
DetectorSwingAngle = _savedPositions.DetectorSwing;
StageRotationAngle = _savedPositions.StageRotation;
FixtureRotationAngle = _savedPositions.FixtureRotation;
// 发送移动命令(通过 MotionControlService → PlcLinearAxis/PlcRotaryAxis → PLC
_motionControlService.MoveToTarget(AxisId.StageX, _savedPositions.StageX);
_motionControlService.MoveToTarget(AxisId.StageY, _savedPositions.StageY);
_motionControlService.MoveToTarget(AxisId.SourceZ, _savedPositions.SourceZ);
_motionControlService.MoveToTarget(AxisId.DetectorZ, _savedPositions.DetectorZ);
_motionControlService.MoveRotaryToTarget(RotaryAxisId.DetectorSwing, _savedPositions.DetectorSwing);
_motionControlService.MoveRotaryToTarget(RotaryAxisId.StageRotation, _savedPositions.StageRotation);
_motionControlService.MoveRotaryToTarget(RotaryAxisId.FixtureRotation, _savedPositions.FixtureRotation);
}
```
**PLC 信号下发**:恢复时向所有 7 个轴写入目标位置和速度信号(参见第 2.3 节信号地址表)。
## 7. 安全参数 | Safety Parameters
ViewModel 提供以下安全参数属性:
| 属性 | 说明 |
|------|------|
| `SafetyHeight` | 探测器安全高度限定值 |
| `CalibrationValue` | 校准自动计算值 |
```csharp
public void ConfirmSafetyHeight()
{
_logger.Info("探测器安全高度限定值已保存:{Value}", SafetyHeight);
}
public void ConfirmCalibrationValue()
{
_logger.Info("校准自动计算值已保存:{Value}", CalibrationValue);
}
```
> 注:当前实现仅记录日志,未写入 PLC 信号。后续可能扩展为写入 PLC 安全参数区域。
## 8. PLC 断连安全处理 | PLC Disconnection Safety
当 PLC 连接断开时,ViewModel 自动执行以下安全操作:
```csharp
private void OnPlcConnectionChanged()
{
IsPlcConnected = _plcService.IsConnected;
if (!_plcService.IsConnected)
{
// 1. 停止所有活跃的 Jog 操作(标记为停止)
foreach (var axisId in _linearJogActive.Keys)
if (_linearJogActive[axisId])
_linearJogActive[axisId] = false;
foreach (var axisId in _rotaryJogActive.Keys)
if (_rotaryJogActive[axisId])
_rotaryJogActive[axisId] = false;
// 2. 禁用摇杆
IsJoystickEnabled = false;
// 3. 显示连接断开提示
ErrorMessage = LocalizationHelper.Get("MC_PlcNotConnected");
}
else
{
// PLC 重连:清除错误信息(不自动启用摇杆,需用户手动开启)
ErrorMessage = null;
}
}
```
**安全策略**
- PLC 断连时立即标记所有 Jog 为停止状态
- 禁用虚拟摇杆,防止用户误操作
- PLC 重连后不自动恢复使能,需用户手动确认后开启
## 9. 实体摇杆状态 | Physical Joystick Status
通过 `JoystickActiveEvent` 事件订阅实体摇杆的激活状态:
```csharp
_eventAggregator.GetEvent<JoystickActiveEvent>().Subscribe(OnJoystickActiveChanged, ThreadOption.UIThread);
private void OnJoystickActiveChanged(bool isActive)
{
IsJoystickActive = isActive;
}
```
**PLC 信号(读取)**
| 信号名 | 说明 |
|--------|------|
| `MC_Joystick_Active` | 实体摇杆输入激活状态(由 PLC 轮询读取) |
## 10. 旋转轴可见性控制 | Rotary Axis Visibility Control
根据配置动态控制旋转轴输入框的可见性:
```csharp
DetectorSwingVisibility = _config.RotaryAxes.ContainsKey(RotaryAxisId.DetectorSwing)
&& _config.RotaryAxes[RotaryAxisId.DetectorSwing].Enabled
? Visibility.Visible : Visibility.Collapsed;
StageRotationVisibility = _config.RotaryAxes.ContainsKey(RotaryAxisId.StageRotation)
&& _config.RotaryAxes[RotaryAxisId.StageRotation].Enabled
? Visibility.Visible : Visibility.Collapsed;
FixtureRotationVisibility = _config.RotaryAxes.ContainsKey(RotaryAxisId.FixtureRotation)
&& _config.RotaryAxes[RotaryAxisId.FixtureRotation].Enabled
? Visibility.Visible : Visibility.Collapsed;
```
此功能为纯 UI 层逻辑,不涉及 PLC 信号。
## 11. 信号写入队列机制 | Signal Write Queue Mechanism
所有 PLC 写入操作通过 `ISignalDataService.EnqueueWrite()` 进入写入队列:
```csharp
public interface ISignalDataService
{
bool EnqueueWrite(string signalName, object value);
bool EnqueueWriteBatch(Dictionary<string, object> writes);
T GetValueByName<T>(string signalName);
}
```
**队列处理流程**
1. 轴实现层(`PlcLinearAxis`/`PlcRotaryAxis`)调用 `EnqueueWrite(signalName, value)`
2. 信号数据服务将写入请求加入队列
3. PLC 服务在轮询周期(默认 100ms)中批量写入
4. 写入成功后从队列移除
**优势**
- 批量写入减少 PLC 通讯次数
- 避免频繁写入导致的通讯拥塞
- 统一的错误处理和重试机制
## 12. 完整 PLC 信号名称常量 | Complete PLC Signal Name Constants
定义在 `MotionSignalNames.cs` 中:
| 分类 | 常量名 | 信号名 | 方向 |
|------|--------|--------|------|
| 联动 | `SourceDetZ_Linkage_Enable` | `MC_SourceDetZ_Linkage_Enable` | 写入 |
| 使能 | `VirtualJoystick_Enable` | `MC_VirtualJoystick_Enable` | 写入 |
| 状态 | `Joystick_Active` | `MC_Joystick_Active` | 读取 |
| SourceZ | `SourceZ_Pos` | `MC_SourceZ_Pos` | 读取 |
| SourceZ | `SourceZ_Target` | `MC_SourceZ_Target` | 写入 |
| SourceZ | `SourceZ_Speed` | `MC_SourceZ_Speed` | 写入 |
| SourceZ | `SourceZ_JogPos` | `MC_SourceZ_JogPos` | 写入 |
| SourceZ | `SourceZ_JogNeg` | `MC_SourceZ_JogNeg` | 写入 |
| SourceZ | `SourceZ_Home` | `MC_SourceZ_Home` | 写入 |
| SourceZ | `SourceZ_Stop` | `MC_SourceZ_Stop` | 写入 |
| DetectorZ | `DetZ_Pos` | `MC_DetZ_Pos` | 读取 |
| DetectorZ | `DetZ_Target` | `MC_DetZ_Target` | 写入 |
| DetectorZ | `DetZ_Speed` | `MC_DetZ_Speed` | 写入 |
| DetectorZ | `DetZ_JogPos` | `MC_DetZ_JogPos` | 写入 |
| DetectorZ | `DetZ_JogNeg` | `MC_DetZ_JogNeg` | 写入 |
| DetectorZ | `DetZ_Home` | `MC_DetZ_Home` | 写入 |
| DetectorZ | `DetZ_Stop` | `MC_DetZ_Stop` | 写入 |
| StageX | `StageX_Pos` | `MC_StageX_Pos` | 读取 |
| StageX | `StageX_Target` | `MC_StageX_Target` | 写入 |
| StageX | `StageX_Speed` | `MC_StageX_Speed` | 写入 |
| StageX | `StageX_JogPos` | `MC_StageX_JogPos` | 写入 |
| StageX | `StageX_JogNeg` | `MC_StageX_JogNeg` | 写入 |
| StageX | `StageX_Home` | `MC_StageX_Home` | 写入 |
| StageX | `StageX_Stop` | `MC_StageX_Stop` | 写入 |
| StageY | `StageY_Pos` | `MC_StageY_Pos` | 读取 |
| StageY | `StageY_Target` | `MC_StageY_Target` | 写入 |
| StageY | `StageY_Speed` | `MC_StageY_Speed` | 写入 |
| StageY | `StageY_JogPos` | `MC_StageY_JogPos` | 写入 |
| StageY | `StageY_JogNeg` | `MC_StageY_JogNeg` | 写入 |
| StageY | `StageY_Home` | `MC_StageY_Home` | 写入 |
| StageY | `StageY_Stop` | `MC_StageY_Stop` | 写入 |
| DetSwing | `DetSwing_Angle` | `MC_DetSwing_Angle` | 读取 |
| DetSwing | `DetSwing_Target` | `MC_DetSwing_Target` | 写入 |
| DetSwing | `DetSwing_Speed` | `MC_DetSwing_Speed` | 写入 |
| DetSwing | `DetSwing_JogPos` | `MC_DetSwing_JogPos` | 写入 |
| DetSwing | `DetSwing_JogNeg` | `MC_DetSwing_JogNeg` | 写入 |
| DetSwing | `DetSwing_Home` | `MC_DetSwing_Home` | 写入 |
| DetSwing | `DetSwing_Stop` | `MC_DetSwing_Stop` | 写入 |
| StageRot | `StageRot_Angle` | `MC_StageRot_Angle` | 读取 |
| StageRot | `StageRot_Target` | `MC_StageRot_Target` | 写入 |
| StageRot | `StageRot_Speed` | `MC_StageRot_Speed` | 写入 |
| StageRot | `StageRot_JogPos` | `MC_StageRot_JogPos` | 写入 |
| StageRot | `StageRot_JogNeg` | `MC_StageRot_JogNeg` | 写入 |
| StageRot | `StageRot_Home` | `MC_StageRot_Home` | 写入 |
| StageRot | `StageRot_Stop` | `MC_StageRot_Stop` | 写入 |
| FixRot | `FixRot_Angle` | `MC_FixRot_Angle` | 读取 |
| FixRot | `FixRot_Target` | `MC_FixRot_Target` | 写入 |
| FixRot | `FixRot_Speed` | `MC_FixRot_Speed` | 写入 |
| FixRot | `FixRot_JogPos` | `MC_FixRot_JogPos` | 写入 |
| FixRot | `FixRot_JogNeg` | `MC_FixRot_JogNeg` | 写入 |
| FixRot | `FixRot_Home` | `MC_FixRot_Home` | 写入 |
| FixRot | `FixRot_Stop` | `MC_FixRot_Stop` | 写入 |
| 安全门 | `Door_Open` | `MC_Door_Open` | 写入 |
| 安全门 | `Door_Close` | `MC_Door_Close` | 写入 |
| 安全门 | `Door_Stop` | `MC_Door_Stop` | 写入 |
| 安全门 | `Door_Status` | `MC_Door_Status` | 读取 |
| 安全门 | `Door_Interlock` | `MC_Door_Interlock` | 读取 |
| 轴复位 | `Axis_Reset` | `MC_Axis_Reset` | 写入 |
| 轴复位 | `Axis_ResetDone` | `MC_Axis_ResetDone` | 读取 |
---