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

29 KiB
Raw Blame History

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 时,左右键功能互换。

信号下发流程

// 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 流程类似UpdateRotaryJogSetJogRotarySpeedJogRotaryStart),额外包含禁用轴检查。

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(正向/反向)

信号下发流程

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:

// 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%):

var speedPercent = Math.Abs(output) * 100;  // 0% ~ 100%

速度计算流程

  1. MotionControlService.SetJogSpeed 计算实际速度:

    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

    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

  • GotFocusSetEditing(propertyName, true) → 冻结实时更新
  • LostFocus/EscapeCancelEditing(propertyName) → 恢复实时更新,显示实际值
  • EnterConfirmPosition(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(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

// 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)

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"按钮切换联动状态:

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:

public MotionResult SetSZDZLinkageToPlc()
{
    return _motionControlService.SetSourceDetectorZLinkage(SZDZLock);
}

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 (_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 时,单轴摇杆的左右键会同时控制两个轴:

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

点击"使能开关"按钮切换虚拟摇杆使能状态:

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

点击"交换按键"按钮切换摇杆左右键功能映射:

private void ExecuteToggleSwapMouseButtons()
{
    SwapMouseButtons = !SwapMouseButtons;
}

此功能为纯 UI 层逻辑,不涉及 PLC 信号写入。当 SwapMouseButtons=true 时,摇杆控件内部交换左右键的功能映射。

6. 保存/恢复轴位置 | Save/Restore Axis Positions

6.1 保存当前位置 | Save Current Positions

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

恢复时会同时向所有轴发送移动命令:

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 校准自动计算值
public void ConfirmSafetyHeight()
{
    _logger.Info("探测器安全高度限定值已保存:{Value}", SafetyHeight);
}

public void ConfirmCalibrationValue()
{
    _logger.Info("校准自动计算值已保存:{Value}", CalibrationValue);
}

注:当前实现仅记录日志,未写入 PLC 信号。后续可能扩展为写入 PLC 安全参数区域。

8. PLC 断连安全处理 | PLC Disconnection Safety

当 PLC 连接断开时,ViewModel 自动执行以下安全操作:

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 事件订阅实体摇杆的激活状态:

_eventAggregator.GetEvent<JoystickActiveEvent>().Subscribe(OnJoystickActiveChanged, ThreadOption.UIThread);

private void OnJoystickActiveChanged(bool isActive)
{
    IsJoystickActive = isActive;
}

PLC 信号(读取)

信号名 说明
MC_Joystick_Active 实体摇杆输入激活状态(由 PLC 轮询读取)

10. 旋转轴可见性控制 | Rotary Axis Visibility Control

根据配置动态控制旋转轴输入框的可见性:

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() 进入写入队列:

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 读取