# 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(); 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().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 writes); T GetValueByName(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` | 读取 | ---