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