e9596b013a
补充射线源和PLC类库所需配置和信号地址定义文件。
804 lines
29 KiB
Markdown
804 lines
29 KiB
Markdown
# 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 | 正向 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:点动) |
|
||
| 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:点动) |
|
||
|
||
**PLC 信号地址(旋转轴 Jog)**:
|
||
|
||
| 轴 | 信号名 | 地址 | 说明 |
|
||
|----|--------|------|------|
|
||
| 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;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 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` | 读取 |
|
||
|
||
---
|