初版本AxisControl(Viscom风格)控件。

This commit is contained in:
QI Mingxuan
2026-04-22 20:48:00 +08:00
parent 4390ad1e9f
commit 1279885924
16 changed files with 1656 additions and 0 deletions
@@ -45,6 +45,11 @@ namespace XP.Hardware.MotionControl.Abstractions
/// <returns>操作结果 | Operation result</returns>
MotionResult Stop();
/// <summary>设置 Jog 速度 | Set jog speed</summary>
/// <param name="speedPercent">速度百分比(0~100| Speed percentage (0~100)</param>
/// <returns>操作结果 | Operation result</returns>
MotionResult SetJogSpeed(double speedPercent);
/// <summary>从 PLC 更新状态 | Update status from PLC</summary>
void UpdateStatus();
}
@@ -42,6 +42,11 @@ namespace XP.Hardware.MotionControl.Abstractions
/// <returns>操作结果 | Operation result</returns>
MotionResult Stop();
/// <summary>设置 Jog 速度 | Set jog speed</summary>
/// <param name="speedPercent">速度百分比(0~100| Speed percentage (0~100)</param>
/// <returns>操作结果 | Operation result</returns>
MotionResult SetJogSpeed(double speedPercent);
/// <summary>从 PLC 更新状态 | Update status from PLC</summary>
void UpdateStatus();
}
@@ -70,6 +70,9 @@ namespace XP.Hardware.MotionControl.Abstractions
/// <inheritdoc/>
public abstract MotionResult Stop();
/// <inheritdoc/>
public abstract MotionResult SetJogSpeed(double speedPercent);
/// <inheritdoc/>
public abstract void UpdateStatus();
}
@@ -67,6 +67,9 @@ namespace XP.Hardware.MotionControl.Abstractions
/// <inheritdoc/>
public abstract MotionResult Stop();
/// <inheritdoc/>
public abstract MotionResult SetJogSpeed(double speedPercent);
/// <inheritdoc/>
public abstract void UpdateStatus();
}
@@ -95,6 +95,13 @@ namespace XP.Hardware.MotionControl.Implementations
return MotionResult.Ok();
}
/// <inheritdoc/>
public override MotionResult SetJogSpeed(double speedPercent)
{
_signalService.EnqueueWrite(_speedSignal, (float)speedPercent);
return MotionResult.Ok();
}
/// <inheritdoc/>
public override void UpdateStatus()
{
@@ -91,6 +91,14 @@ namespace XP.Hardware.MotionControl.Implementations
return MotionResult.Ok();
}
/// <inheritdoc/>
public override MotionResult SetJogSpeed(double speedPercent)
{
if (!_enabled) return MotionResult.Fail($"旋转轴 {_axisId} 已禁用,拒绝设置 Jog 速度命令");
_signalService.EnqueueWrite(_speedSignal, (float)speedPercent);
return MotionResult.Ok();
}
/// <inheritdoc/>
public override void UpdateStatus()
{
@@ -258,4 +258,47 @@ SourceZ → {3:F2}mm
DetectorZ → {4:F2}mm
Confirm to filll move matrix?</value>
</data>
<!-- AxisControlView localization resources -->
<data name="AC_AxisPositions" xml:space="preserve">
<value>Axis Positions</value>
</data>
<data name="AC_StageX" xml:space="preserve">
<value>Stage X</value>
</data>
<data name="AC_StageY" xml:space="preserve">
<value>Stage Y</value>
</data>
<data name="AC_SourceZ" xml:space="preserve">
<value>Source Z</value>
</data>
<data name="AC_DetectorZ" xml:space="preserve">
<value>Detector Z</value>
</data>
<data name="AC_DetectorSwing" xml:space="preserve">
<value>Detector Swing</value>
</data>
<data name="AC_StageRotation" xml:space="preserve">
<value>Stage Rotation</value>
</data>
<data name="AC_FixtureRotation" xml:space="preserve">
<value>Fixture Rotation</value>
</data>
<data name="AC_SafetyParams" xml:space="preserve">
<value>Safety Parameters</value>
</data>
<data name="AC_SafetyHeight" xml:space="preserve">
<value>Safety Height</value>
</data>
<data name="AC_CalibrationValue" xml:space="preserve">
<value>Calibration Value</value>
</data>
<data name="AC_Enable" xml:space="preserve">
<value>Enable</value>
</data>
<data name="AC_Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="AC_Restore" xml:space="preserve">
<value>Restore</value>
</data>
</root>
@@ -301,4 +301,61 @@ DetectorZ → {4:F2}mm
确认填入信息? | Confirm to filll move matrix?</value>
<comment>几何反算确认提示 | Geometry inverse confirmation message</comment>
</data>
<!-- AxisControlView 多语言资源 | AxisControlView localization resources -->
<data name="AC_AxisPositions" xml:space="preserve">
<value>轴位置</value>
<comment>轴位置分组标题 | Axis positions group title</comment>
</data>
<data name="AC_StageX" xml:space="preserve">
<value>载物台 X</value>
<comment>载物台 X 轴标签 | Stage X axis label</comment>
</data>
<data name="AC_StageY" xml:space="preserve">
<value>载物台 Y</value>
<comment>载物台 Y 轴标签 | Stage Y axis label</comment>
</data>
<data name="AC_SourceZ" xml:space="preserve">
<value>射线源 Z</value>
<comment>射线源 Z 轴标签 | Source Z axis label</comment>
</data>
<data name="AC_DetectorZ" xml:space="preserve">
<value>探测器 Z</value>
<comment>探测器 Z 轴标签 | Detector Z axis label</comment>
</data>
<data name="AC_DetectorSwing" xml:space="preserve">
<value>探测器摆动</value>
<comment>探测器摆动旋转轴标签 | Detector swing rotary axis label</comment>
</data>
<data name="AC_StageRotation" xml:space="preserve">
<value>载物台旋转</value>
<comment>载物台旋转轴标签 | Stage rotation axis label</comment>
</data>
<data name="AC_FixtureRotation" xml:space="preserve">
<value>夹具旋转</value>
<comment>夹具旋转轴标签 | Fixture rotation axis label</comment>
</data>
<data name="AC_SafetyParams" xml:space="preserve">
<value>安全参数</value>
<comment>安全参数分组标题 | Safety parameters group title</comment>
</data>
<data name="AC_SafetyHeight" xml:space="preserve">
<value>安全高度</value>
<comment>探测器安全高度限定值标签 | Safety height label</comment>
</data>
<data name="AC_CalibrationValue" xml:space="preserve">
<value>校准计算值</value>
<comment>校准自动计算值标签 | Calibration value label</comment>
</data>
<data name="AC_Enable" xml:space="preserve">
<value>使能</value>
<comment>使能开关标签 | Enable toggle label</comment>
</data>
<data name="AC_Save" xml:space="preserve">
<value>保存</value>
<comment>保存按钮文本 | Save button text</comment>
</data>
<data name="AC_Restore" xml:space="preserve">
<value>恢复</value>
<comment>恢复按钮文本 | Restore button text</comment>
</data>
</root>
@@ -258,4 +258,47 @@ SourceZ → {3:F2}mm
DetectorZ → {4:F2}mm
确认填入目标值信息?</value>
</data>
<!-- AxisControlView 多语言资源 | AxisControlView localization resources -->
<data name="AC_AxisPositions" xml:space="preserve">
<value>轴位置</value>
</data>
<data name="AC_StageX" xml:space="preserve">
<value>载物台 X</value>
</data>
<data name="AC_StageY" xml:space="preserve">
<value>载物台 Y</value>
</data>
<data name="AC_SourceZ" xml:space="preserve">
<value>射线源 Z</value>
</data>
<data name="AC_DetectorZ" xml:space="preserve">
<value>探测器 Z</value>
</data>
<data name="AC_DetectorSwing" xml:space="preserve">
<value>探测器摆动</value>
</data>
<data name="AC_StageRotation" xml:space="preserve">
<value>载物台旋转</value>
</data>
<data name="AC_FixtureRotation" xml:space="preserve">
<value>夹具旋转</value>
</data>
<data name="AC_SafetyParams" xml:space="preserve">
<value>安全参数</value>
</data>
<data name="AC_SafetyHeight" xml:space="preserve">
<value>安全高度</value>
</data>
<data name="AC_CalibrationValue" xml:space="preserve">
<value>校准计算值</value>
</data>
<data name="AC_Enable" xml:space="preserve">
<value>使能</value>
</data>
<data name="AC_Save" xml:space="preserve">
<value>保存</value>
</data>
<data name="AC_Restore" xml:space="preserve">
<value>恢复</value>
</data>
</root>
@@ -258,4 +258,47 @@ SourceZ → {3:F2}mm
DetectorZ → {4:F2}mm
確填入目標值資料?</value>
</data>
<!-- AxisControlView 多語言資源 | AxisControlView localization resources -->
<data name="AC_AxisPositions" xml:space="preserve">
<value>軸位置</value>
</data>
<data name="AC_StageX" xml:space="preserve">
<value>載物台 X</value>
</data>
<data name="AC_StageY" xml:space="preserve">
<value>載物台 Y</value>
</data>
<data name="AC_SourceZ" xml:space="preserve">
<value>射線源 Z</value>
</data>
<data name="AC_DetectorZ" xml:space="preserve">
<value>探測器 Z</value>
</data>
<data name="AC_DetectorSwing" xml:space="preserve">
<value>探測器擺動</value>
</data>
<data name="AC_StageRotation" xml:space="preserve">
<value>載物台旋轉</value>
</data>
<data name="AC_FixtureRotation" xml:space="preserve">
<value>夾具旋轉</value>
</data>
<data name="AC_SafetyParams" xml:space="preserve">
<value>安全參數</value>
</data>
<data name="AC_SafetyHeight" xml:space="preserve">
<value>安全高度</value>
</data>
<data name="AC_CalibrationValue" xml:space="preserve">
<value>校準計算值</value>
</data>
<data name="AC_Enable" xml:space="preserve">
<value>使能</value>
</data>
<data name="AC_Save" xml:space="preserve">
<value>保存</value>
</data>
<data name="AC_Restore" xml:space="preserve">
<value>恢復</value>
</data>
</root>
@@ -106,6 +106,24 @@ namespace XP.Hardware.MotionControl.Services
/// <returns>操作结果 | Operation result</returns>
MotionResult JogRotaryStop(RotaryAxisId axisId);
/// <summary>
/// 设置直线轴 Jog 速度 | Set linear axis jog speed
/// 根据速度百分比计算实际速度并写入 PLC | Calculates actual speed from percentage and writes to PLC
/// </summary>
/// <param name="axisId">直线轴标识 | Linear axis identifier</param>
/// <param name="speedPercent">速度百分比(0~100| Speed percentage (0~100)</param>
/// <returns>操作结果 | Operation result</returns>
MotionResult SetJogSpeed(AxisId axisId, double speedPercent);
/// <summary>
/// 设置旋转轴 Jog 速度 | Set rotary axis jog speed
/// 根据速度百分比计算实际速度并写入 PLC | Calculates actual speed from percentage and writes to PLC
/// </summary>
/// <param name="axisId">旋转轴标识 | Rotary axis identifier</param>
/// <param name="speedPercent">速度百分比(0~100| Speed percentage (0~100)</param>
/// <returns>操作结果 | Operation result</returns>
MotionResult SetJogRotarySpeed(RotaryAxisId axisId, double speedPercent);
#endregion
#region | Safety Door Control
@@ -488,6 +488,60 @@ namespace XP.Hardware.MotionControl.Services
return result;
}
/// <summary>
/// 设置直线轴 Jog 速度 | Set linear axis jog speed
/// 根据速度百分比计算实际速度并写入 PLC | Calculates actual speed from percentage and writes to PLC
/// </summary>
public MotionResult SetJogSpeed(AxisId axisId, double speedPercent)
{
var axis = _motionSystem.GetLinearAxis(axisId);
var actualSpeed = _config.DefaultVelocity * speedPercent / 100.0;
var result = axis.SetJogSpeed(actualSpeed);
if (result.Success)
{
_logger.Debug("直线轴 {AxisId} Jog 速度已设置,百分比={SpeedPercent}%,实际速度={ActualSpeed} | Linear axis {AxisId} jog speed set, percent={SpeedPercent}%, actual={ActualSpeed}",
axisId, speedPercent, actualSpeed);
}
else
{
_logger.Warn("直线轴 {AxisId} 设置 Jog 速度被拒绝:{Reason} | Linear axis {AxisId} set jog speed rejected: {Reason}", axisId, result.ErrorMessage);
}
return result;
}
/// <summary>
/// 设置旋转轴 Jog 速度 | Set rotary axis jog speed
/// 根据速度百分比计算实际速度并写入 PLC | Calculates actual speed from percentage and writes to PLC
/// </summary>
public MotionResult SetJogRotarySpeed(RotaryAxisId axisId, double speedPercent)
{
var axis = _motionSystem.GetRotaryAxis(axisId);
// 禁用轴检查 | Disabled axis check
if (!axis.Enabled)
{
_logger.Warn("旋转轴 {AxisId} 已禁用,拒绝设置 Jog 速度命令 | Rotary axis {AxisId} is disabled, set jog speed rejected", axisId);
return MotionResult.Fail($"旋转轴 {axisId} 已禁用 | Rotary axis {axisId} is disabled");
}
var actualSpeed = _config.DefaultVelocity * speedPercent / 100.0;
var result = axis.SetJogSpeed(actualSpeed);
if (result.Success)
{
_logger.Debug("旋转轴 {AxisId} Jog 速度已设置,百分比={SpeedPercent}%,实际速度={ActualSpeed} | Rotary axis {AxisId} jog speed set, percent={SpeedPercent}%, actual={ActualSpeed}",
axisId, speedPercent, actualSpeed);
}
else
{
_logger.Warn("旋转轴 {AxisId} 设置 Jog 速度被拒绝:{Reason} | Rotary axis {AxisId} set jog speed rejected: {Reason}", axisId, result.ErrorMessage);
}
return result;
}
#endregion
#region | Safety Door Control
@@ -0,0 +1,896 @@
using System;
using System.Collections.Generic;
using System.Windows;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using XP.Common.Controls;
using XP.Common.Logging.Interfaces;
using XP.Hardware.MotionControl.Abstractions;
using XP.Hardware.MotionControl.Abstractions.Enums;
using XP.Hardware.MotionControl.Abstractions.Events;
using XP.Hardware.MotionControl.Config;
using XP.Hardware.MotionControl.Services;
using XP.Hardware.Plc.Abstractions;
using XP.Common.Localization;
namespace XP.Hardware.MotionControl.ViewModels
{
/// <summary>
/// 轴控制面板 ViewModel | Axis Control Panel ViewModel
/// 集成摇杆、轴位置输入框、安全参数和使能控制 | Integrates joystick, axis position inputs, safety parameters and enable control
/// </summary>
public class AxisControlViewModel : BindableBase
{
private readonly IMotionControlService _motionControlService;
private readonly IMotionSystem _motionSystem;
private readonly IEventAggregator _eventAggregator;
private readonly MotionControlConfig _config;
private readonly IPlcService _plcService;
private readonly ILoggerService _logger;
#region | Internal State Tracking
/// <summary>直线轴 Jog 活跃状态 | Linear axis jog active states</summary>
private readonly Dictionary<AxisId, bool> _linearJogActive = new();
/// <summary>旋转轴 Jog 活跃状态 | Rotary axis jog active states</summary>
private readonly Dictionary<RotaryAxisId, bool> _rotaryJogActive = new();
/// <summary>输入框编辑冻结标志 | Input box editing freeze flags</summary>
private readonly Dictionary<string, bool> _editingFlags = new();
/// <summary>保存的轴位置数据 | Saved axis position data</summary>
private SavedPositions _savedPositions;
#endregion
#region | Constructor
public AxisControlViewModel(
IMotionControlService motionControlService,
IMotionSystem motionSystem,
IEventAggregator eventAggregator,
MotionControlConfig config,
IPlcService plcService,
ILoggerService logger)
{
_motionControlService = motionControlService ?? throw new ArgumentNullException(nameof(motionControlService));
_motionSystem = motionSystem ?? throw new ArgumentNullException(nameof(motionSystem));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_config = config ?? throw new ArgumentNullException(nameof(config));
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<AxisControlViewModel>();
// 监听 PLC 连接状态变化 | Listen for PLC connection status changes
_plcService.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(IPlcService.IsConnected))
OnPlcConnectionChanged();
};
// 初始化旋转轴输入框可见性 | Initialize rotary axis input box visibility
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;
// 初始化命令 | Initialize commands
ToggleEnableCommand = new DelegateCommand(ExecuteToggleEnable, () => IsPlcConnected);
SavePositionsCommand = new DelegateCommand(ExecuteSavePositions);
RestorePositionsCommand = new DelegateCommand(ExecuteRestorePositions, () => _savedPositions != null && IsPlcConnected);
// 初始化 Jog 状态跟踪字典 | Initialize jog state tracking dictionaries
foreach (AxisId axisId in Enum.GetValues(typeof(AxisId)))
_linearJogActive[axisId] = false;
foreach (RotaryAxisId axisId in Enum.GetValues(typeof(RotaryAxisId)))
_rotaryJogActive[axisId] = false;
// 初始化 PLC 连接状态 | Initialize PLC connection status
_isPlcConnected = _plcService.IsConnected;
// 订阅事件 | Subscribe to events
_eventAggregator.GetEvent<GeometryUpdatedEvent>().Subscribe(OnGeometryUpdated, ThreadOption.UIThread);
_eventAggregator.GetEvent<AxisStatusChangedEvent>().Subscribe(OnAxisStatusChanged, ThreadOption.UIThread);
// 初始化时主动刷新一次轴位置 | Refresh axis positions on initialization
try
{
StageXPosition = _motionSystem.GetLinearAxis(AxisId.StageX).ActualPosition;
StageYPosition = _motionSystem.GetLinearAxis(AxisId.StageY).ActualPosition;
SourceZPosition = _motionSystem.GetLinearAxis(AxisId.SourceZ).ActualPosition;
DetectorZPosition = _motionSystem.GetLinearAxis(AxisId.DetectorZ).ActualPosition;
DetectorSwingAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.DetectorSwing).ActualAngle;
StageRotationAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.StageRotation).ActualAngle;
FixtureRotationAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.FixtureRotation).ActualAngle;
}
catch { /* 轴未初始化时忽略 | Ignore when axes not initialized */ }
}
#endregion
#region | Joystick Output Properties
private double _dualJoystickOutputX;
/// <summary>双轴摇杆 X 轴输出 | Dual-axis joystick X output</summary>
public double DualJoystickOutputX
{
get => _dualJoystickOutputX;
set
{
if (SetProperty(ref _dualJoystickOutputX, value))
HandleDualJoystickOutput();
}
}
private double _dualJoystickOutputY;
/// <summary>双轴摇杆 Y 轴输出 | Dual-axis joystick Y output</summary>
public double DualJoystickOutputY
{
get => _dualJoystickOutputY;
set
{
if (SetProperty(ref _dualJoystickOutputY, value))
HandleDualJoystickOutput();
}
}
private MouseButtonType _dualJoystickActiveButton;
/// <summary>双轴摇杆当前激活按键 | Dual-axis joystick active mouse button</summary>
public MouseButtonType DualJoystickActiveButton
{
get => _dualJoystickActiveButton;
set
{
var oldValue = _dualJoystickActiveButton;
if (SetProperty(ref _dualJoystickActiveButton, value))
{
// 按键释放时停止所有双轴摇杆关联轴 | Stop all dual joystick axes when button released
if (value == MouseButtonType.None && oldValue != MouseButtonType.None)
StopDualJoystickAxes();
}
}
}
private double _singleJoystickOutputY;
/// <summary>单轴摇杆 Y 轴输出 | Single-axis joystick Y output</summary>
public double SingleJoystickOutputY
{
get => _singleJoystickOutputY;
set
{
if (SetProperty(ref _singleJoystickOutputY, value))
HandleSingleJoystickOutput();
}
}
private MouseButtonType _singleJoystickActiveButton;
/// <summary>单轴摇杆当前激活按键 | Single-axis joystick active mouse button</summary>
public MouseButtonType SingleJoystickActiveButton
{
get => _singleJoystickActiveButton;
set
{
var oldValue = _singleJoystickActiveButton;
if (SetProperty(ref _singleJoystickActiveButton, value))
{
// 按键释放时停止所有单轴摇杆关联轴 | Stop all single joystick axes when button released
if (value == MouseButtonType.None && oldValue != MouseButtonType.None)
StopSingleJoystickAxes();
}
}
}
#endregion
#region | Axis Position Properties
private double _stageXPosition;
/// <summary>载物台 X 轴位置 | Stage X axis position</summary>
public double StageXPosition { get => _stageXPosition; set => SetProperty(ref _stageXPosition, value); }
private double _stageYPosition;
/// <summary>载物台 Y 轴位置 | Stage Y axis position</summary>
public double StageYPosition { get => _stageYPosition; set => SetProperty(ref _stageYPosition, value); }
private double _sourceZPosition;
/// <summary>射线源 Z 轴位置 | Source Z axis position</summary>
public double SourceZPosition { get => _sourceZPosition; set => SetProperty(ref _sourceZPosition, value); }
private double _detectorZPosition;
/// <summary>探测器 Z 轴位置 | Detector Z axis position</summary>
public double DetectorZPosition { get => _detectorZPosition; set => SetProperty(ref _detectorZPosition, value); }
private double _detectorSwingAngle;
/// <summary>探测器摆动角度 | Detector swing angle</summary>
public double DetectorSwingAngle { get => _detectorSwingAngle; set => SetProperty(ref _detectorSwingAngle, value); }
private double _stageRotationAngle;
/// <summary>载物台旋转角度 | Stage rotation angle</summary>
public double StageRotationAngle { get => _stageRotationAngle; set => SetProperty(ref _stageRotationAngle, value); }
private double _fixtureRotationAngle;
/// <summary>夹具旋转角度 | Fixture rotation angle</summary>
public double FixtureRotationAngle { get => _fixtureRotationAngle; set => SetProperty(ref _fixtureRotationAngle, value); }
#endregion
#region | Rotary Axis Visibility
private Visibility _detectorSwingVisibility;
/// <summary>探测器摆动输入框可见性 | Detector swing input box visibility</summary>
public Visibility DetectorSwingVisibility { get => _detectorSwingVisibility; private set => SetProperty(ref _detectorSwingVisibility, value); }
private Visibility _stageRotationVisibility;
/// <summary>载物台旋转输入框可见性 | Stage rotation input box visibility</summary>
public Visibility StageRotationVisibility { get => _stageRotationVisibility; private set => SetProperty(ref _stageRotationVisibility, value); }
private Visibility _fixtureRotationVisibility;
/// <summary>夹具旋转输入框可见性 | Fixture rotation input box visibility</summary>
public Visibility FixtureRotationVisibility { get => _fixtureRotationVisibility; private set => SetProperty(ref _fixtureRotationVisibility, value); }
#endregion
#region | Safety Parameter Properties
private double _safetyHeight;
/// <summary>探测器安全高度限定值 | Detector safety height limit</summary>
public double SafetyHeight { get => _safetyHeight; set => SetProperty(ref _safetyHeight, value); }
private double _calibrationValue;
/// <summary>校准自动计算值 | Calibration auto-calculated value</summary>
public double CalibrationValue { get => _calibrationValue; set => SetProperty(ref _calibrationValue, value); }
#endregion
#region 使 | Enable and Status Properties
private bool _isJoystickEnabled = true;
/// <summary>摇杆使能状态 | Joystick enable state</summary>
public bool IsJoystickEnabled { get => _isJoystickEnabled; set => SetProperty(ref _isJoystickEnabled, value); }
private bool _isPlcConnected;
/// <summary>PLC 连接状态 | PLC connection status</summary>
public bool IsPlcConnected { get => _isPlcConnected; set => SetProperty(ref _isPlcConnected, value); }
private string _errorMessage;
/// <summary>错误提示信息 | Error message</summary>
public string ErrorMessage { get => _errorMessage; set => SetProperty(ref _errorMessage, value); }
#endregion
#region | Commands
/// <summary>切换使能开关命令 | Toggle enable switch command</summary>
public DelegateCommand ToggleEnableCommand { get; }
/// <summary>保存当前轴位置命令 | Save current axis positions command</summary>
public DelegateCommand SavePositionsCommand { get; }
/// <summary>恢复保存的轴位置命令 | Restore saved axis positions command</summary>
public DelegateCommand RestorePositionsCommand { get; }
#endregion
#region | Event Callbacks
/// <summary>
/// 几何参数更新回调,刷新轴实际位置 | Geometry updated callback, refresh axis actual positions
/// </summary>
private void OnGeometryUpdated(GeometryData data)
{
try
{
if (!IsEditing(nameof(StageXPosition)))
StageXPosition = _motionSystem.GetLinearAxis(AxisId.StageX).ActualPosition;
if (!IsEditing(nameof(StageYPosition)))
StageYPosition = _motionSystem.GetLinearAxis(AxisId.StageY).ActualPosition;
if (!IsEditing(nameof(SourceZPosition)))
SourceZPosition = _motionSystem.GetLinearAxis(AxisId.SourceZ).ActualPosition;
if (!IsEditing(nameof(DetectorZPosition)))
DetectorZPosition = _motionSystem.GetLinearAxis(AxisId.DetectorZ).ActualPosition;
if (!IsEditing(nameof(DetectorSwingAngle)))
DetectorSwingAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.DetectorSwing).ActualAngle;
if (!IsEditing(nameof(StageRotationAngle)))
StageRotationAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.StageRotation).ActualAngle;
if (!IsEditing(nameof(FixtureRotationAngle)))
FixtureRotationAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.FixtureRotation).ActualAngle;
}
catch { /* 轴未初始化时忽略 | Ignore when axes not initialized */ }
}
/// <summary>
/// 轴状态变化回调,更新对应轴位置 | Axis status changed callback, update corresponding axis position
/// </summary>
private void OnAxisStatusChanged(AxisStatusChangedData data)
{
try
{
switch (data.AxisId)
{
case AxisId.StageX:
if (!IsEditing(nameof(StageXPosition)))
StageXPosition = _motionSystem.GetLinearAxis(AxisId.StageX).ActualPosition;
break;
case AxisId.StageY:
if (!IsEditing(nameof(StageYPosition)))
StageYPosition = _motionSystem.GetLinearAxis(AxisId.StageY).ActualPosition;
break;
case AxisId.SourceZ:
if (!IsEditing(nameof(SourceZPosition)))
SourceZPosition = _motionSystem.GetLinearAxis(AxisId.SourceZ).ActualPosition;
break;
case AxisId.DetectorZ:
if (!IsEditing(nameof(DetectorZPosition)))
DetectorZPosition = _motionSystem.GetLinearAxis(AxisId.DetectorZ).ActualPosition;
break;
}
}
catch { /* 轴未初始化时忽略 | Ignore when axes not initialized */ }
}
#endregion
#region | Editing State Helpers
/// <summary>
/// 检查指定输入框是否正在编辑 | Check if specified input box is being edited
/// </summary>
/// <param name="propertyName">属性名称 | Property name</param>
/// <returns>是否正在编辑 | Whether editing</returns>
private bool IsEditing(string propertyName)
{
return _editingFlags.TryGetValue(propertyName, out var editing) && editing;
}
#endregion
#region | Exception Protection
/// <summary>
/// 安全执行:捕获异常避免 UI 崩溃 | Safe execution: catches exceptions to prevent UI crash
/// </summary>
private void SafeRun(Action action)
{
try { action(); }
catch (Exception ex)
{
_logger.Error(ex, "运动控制操作异常:{Message} | Motion control operation error: {Message}", ex.Message);
ErrorMessage = ex.Message;
}
}
#endregion
#region | Command State Refresh
/// <summary>
/// 刷新所有命令的 CanExecute 状态 | Refresh CanExecute state of all commands
/// </summary>
private void RaiseCommandCanExecuteChanged()
{
ToggleEnableCommand.RaiseCanExecuteChanged();
SavePositionsCommand.RaiseCanExecuteChanged();
RestorePositionsCommand.RaiseCanExecuteChanged();
}
/// <summary>
/// PLC 连接状态变化处理:断开时停止所有 Jog 并禁用操作,重连时清除错误
/// PLC connection state change handler: stop all jog and disable operations on disconnect, clear error on reconnect
/// </summary>
private void OnPlcConnectionChanged()
{
IsPlcConnected = _plcService.IsConnected;
RaiseCommandCanExecuteChanged();
if (!_plcService.IsConnected)
{
// PLC 断开:停止所有活跃的 Jog 操作 | PLC disconnected: stop all active jog operations
foreach (var axisId in _linearJogActive.Keys)
{
if (_linearJogActive[axisId])
{
_linearJogActive[axisId] = false;
_logger.Debug("PLC 断开,直线轴 Jog 已标记停止:{AxisId} | PLC disconnected, linear axis jog marked stopped: {AxisId}", axisId);
}
}
foreach (var axisId in _rotaryJogActive.Keys)
{
if (_rotaryJogActive[axisId])
{
_rotaryJogActive[axisId] = false;
_logger.Debug("PLC 断开,旋转轴 Jog 已标记停止:{AxisId} | PLC disconnected, rotary axis jog marked stopped: {AxisId}", axisId);
}
}
// 禁用摇杆 | Disable joystick
IsJoystickEnabled = false;
// 显示连接断开提示 | Show disconnection message
ErrorMessage = LocalizationHelper.Get("MC_PlcNotConnected");
_logger.Warn("PLC 连接断开,已停止所有 Jog 并禁用运动控制 | PLC disconnected, all jog stopped and motion control disabled");
}
else
{
// PLC 重连:清除错误信息(不自动启用摇杆,需用户手动开启)
// PLC reconnected: clear error message (don't auto-enable joystick, user must manually enable)
ErrorMessage = null;
_logger.Info("PLC 连接已恢复 | PLC connection restored");
}
}
#endregion
#region 使/ | Enable Toggle and Save/Restore Commands
/// <summary>
/// 切换摇杆使能状态,并发送使能状态到 PLC | Toggle joystick enable state and send to PLC
/// </summary>
private void ExecuteToggleEnable()
{
IsJoystickEnabled = !IsJoystickEnabled;
_logger.Info("摇杆使能状态切换:{Enabled} | Joystick enable toggled: {Enabled}", IsJoystickEnabled);
// TODO: 发送使能状态到 PLC(根据实际 PLC 信号定义)| Send enable state to PLC (based on actual PLC signal definition)
}
/// <summary>
/// 保存当前 6 个轴位置到内部变量 | Save current 6 axis positions to internal variable
/// </summary>
private void ExecuteSavePositions()
{
_savedPositions = new SavedPositions
{
StageX = StageXPosition,
StageY = StageYPosition,
SourceZ = SourceZPosition,
DetectorZ = DetectorZPosition,
DetectorSwing = DetectorSwingAngle,
StageRotation = StageRotationAngle,
FixtureRotation = FixtureRotationAngle
};
RestorePositionsCommand.RaiseCanExecuteChanged();
_logger.Info("轴位置已保存 | Axis positions saved");
}
/// <summary>
/// 从保存的数据恢复到输入框,并发送移动命令 | Restore saved data to input boxes and send move commands
/// </summary>
private void ExecuteRestorePositions()
{
if (_savedPositions == null) return;
// 恢复到输入框 | Restore to input boxes
StageXPosition = _savedPositions.StageX;
StageYPosition = _savedPositions.StageY;
SourceZPosition = _savedPositions.SourceZ;
DetectorZPosition = _savedPositions.DetectorZ;
DetectorSwingAngle = _savedPositions.DetectorSwing;
StageRotationAngle = _savedPositions.StageRotation;
FixtureRotationAngle = _savedPositions.FixtureRotation;
// 发送移动命令 | Send move commands
SafeRun(() =>
{
_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);
});
_logger.Info("轴位置已恢复并发送移动命令 | Axis positions restored and move commands sent");
}
#endregion
#region Jog | Joystick Jog Mapping Logic
/// <summary>
/// 处理双轴摇杆输出变化,根据当前激活按键映射到对应轴的 Jog 操作
/// Handle dual joystick output changes, map to corresponding axis jog based on active button
/// </summary>
private void HandleDualJoystickOutput()
{
switch (DualJoystickActiveButton)
{
case MouseButtonType.Left:
// 左键:X→StageX JogY→StageY Jog | Left button: X→StageX Jog, Y→StageY Jog
UpdateLinearJog(AxisId.StageX, DualJoystickOutputX);
UpdateLinearJog(AxisId.StageY, DualJoystickOutputY);
break;
case MouseButtonType.Right:
// 右键:X→DetectorSwing JogY→StageRotation 或 FixtureRotation Jog
// Right button: X→DetectorSwing Jog, Y→StageRotation or FixtureRotation Jog
UpdateRotaryJog(RotaryAxisId.DetectorSwing, DualJoystickOutputX);
var rotationAxisId = GetEnabledRotationAxisId();
if (rotationAxisId.HasValue)
UpdateRotaryJog(rotationAxisId.Value, DualJoystickOutputY);
break;
}
}
/// <summary>
/// 处理单轴摇杆输出变化,根据当前激活按键映射到对应轴的 Jog 操作
/// Handle single joystick output changes, map to corresponding axis jog based on active button
/// </summary>
private void HandleSingleJoystickOutput()
{
switch (SingleJoystickActiveButton)
{
case MouseButtonType.Left:
// 左键:Y→SourceZ Jog | Left button: Y→SourceZ Jog
UpdateLinearJog(AxisId.SourceZ, SingleJoystickOutputY);
break;
case MouseButtonType.Right:
// 右键:Y→DetectorZ Jog | Right button: Y→DetectorZ Jog
UpdateLinearJog(AxisId.DetectorZ, SingleJoystickOutputY);
break;
}
}
/// <summary>
/// 更新直线轴 Jog 状态:启动、更新速度或停止
/// Update linear axis jog state: start, update speed, or stop
/// </summary>
/// <param name="axisId">直线轴标识 | Linear axis identifier</param>
/// <param name="output">摇杆输出值(-1.0 ~ 1.0| Joystick output value (-1.0 ~ 1.0)</param>
private void UpdateLinearJog(AxisId axisId, double output)
{
if (output != 0)
{
var speedPercent = Math.Abs(output) * 100;
var positive = output > 0;
if (!_linearJogActive[axisId])
{
// 从零变为非零:先设速度再启动 Jog | Zero to non-zero: set speed then start jog
SafeRun(() =>
{
_motionControlService.SetJogSpeed(axisId, speedPercent);
_motionControlService.JogStart(axisId, positive);
_linearJogActive[axisId] = true;
_logger.Debug("直线轴 Jog 启动:{AxisId},方向={Direction},速度={Speed}% | Linear axis jog started: {AxisId}, direction={Direction}, speed={Speed}%",
axisId, positive ? "正向" : "反向", speedPercent);
});
}
else
{
// 已在 Jog 中:仅更新速度 | Already jogging: update speed only
SafeRun(() =>
{
_motionControlService.SetJogSpeed(axisId, speedPercent);
_logger.Debug("直线轴 Jog 速度更新:{AxisId},速度={Speed}% | Linear axis jog speed updated: {AxisId}, speed={Speed}%",
axisId, speedPercent);
});
}
}
else
{
// 从非零变为零:停止 Jog | Non-zero to zero: stop jog
if (_linearJogActive[axisId])
{
SafeRun(() =>
{
_motionControlService.JogStop(axisId);
_linearJogActive[axisId] = false;
_logger.Debug("直线轴 Jog 停止:{AxisId} | Linear axis jog stopped: {AxisId}", axisId);
});
}
}
}
/// <summary>
/// 更新旋转轴 Jog 状态:启动、更新速度或停止
/// Update rotary axis jog state: start, update speed, or stop
/// </summary>
/// <param name="axisId">旋转轴标识 | Rotary axis identifier</param>
/// <param name="output">摇杆输出值(-1.0 ~ 1.0| Joystick output value (-1.0 ~ 1.0)</param>
private void UpdateRotaryJog(RotaryAxisId axisId, double output)
{
if (output != 0)
{
var speedPercent = Math.Abs(output) * 100;
var positive = output > 0;
if (!_rotaryJogActive[axisId])
{
// 从零变为非零:先设速度再启动 Jog | Zero to non-zero: set speed then start jog
SafeRun(() =>
{
_motionControlService.SetJogRotarySpeed(axisId, speedPercent);
_motionControlService.JogRotaryStart(axisId, positive);
_rotaryJogActive[axisId] = true;
_logger.Debug("旋转轴 Jog 启动:{AxisId},方向={Direction},速度={Speed}% | Rotary axis jog started: {AxisId}, direction={Direction}, speed={Speed}%",
axisId, positive ? "正向" : "反向", speedPercent);
});
}
else
{
// 已在 Jog 中:仅更新速度 | Already jogging: update speed only
SafeRun(() =>
{
_motionControlService.SetJogRotarySpeed(axisId, speedPercent);
_logger.Debug("旋转轴 Jog 速度更新:{AxisId},速度={Speed}% | Rotary axis jog speed updated: {AxisId}, speed={Speed}%",
axisId, speedPercent);
});
}
}
else
{
// 从非零变为零:停止 Jog | Non-zero to zero: stop jog
if (_rotaryJogActive[axisId])
{
SafeRun(() =>
{
_motionControlService.JogRotaryStop(axisId);
_rotaryJogActive[axisId] = false;
_logger.Debug("旋转轴 Jog 停止:{AxisId} | Rotary axis jog stopped: {AxisId}", axisId);
});
}
}
}
/// <summary>
/// 停止双轴摇杆控制的所有轴 Jog | Stop all axes controlled by dual joystick
/// </summary>
private void StopDualJoystickAxes()
{
// 左键关联轴:StageX、StageY | Left button axes: StageX, StageY
UpdateLinearJog(AxisId.StageX, 0);
UpdateLinearJog(AxisId.StageY, 0);
// 右键关联轴:DetectorSwing、StageRotation/FixtureRotation | Right button axes: DetectorSwing, StageRotation/FixtureRotation
UpdateRotaryJog(RotaryAxisId.DetectorSwing, 0);
var rotationAxisId = GetEnabledRotationAxisId();
if (rotationAxisId.HasValue)
UpdateRotaryJog(rotationAxisId.Value, 0);
}
/// <summary>
/// 停止单轴摇杆控制的所有轴 Jog | Stop all axes controlled by single joystick
/// </summary>
private void StopSingleJoystickAxes()
{
// 左键关联轴:SourceZ | Left button axis: SourceZ
UpdateLinearJog(AxisId.SourceZ, 0);
// 右键关联轴:DetectorZ | Right button axis: DetectorZ
UpdateLinearJog(AxisId.DetectorZ, 0);
}
/// <summary>
/// 获取当前启用的旋转轴标识(StageRotation 或 FixtureRotation 二选一)
/// Get the currently enabled rotation axis ID (StageRotation or FixtureRotation, one of two)
/// </summary>
/// <returns>启用的旋转轴标识,若均未启用则返回 null | Enabled rotary axis ID, or null if none enabled</returns>
private RotaryAxisId? GetEnabledRotationAxisId()
{
if (_config.RotaryAxes.ContainsKey(RotaryAxisId.StageRotation)
&& _config.RotaryAxes[RotaryAxisId.StageRotation].Enabled)
return RotaryAxisId.StageRotation;
if (_config.RotaryAxes.ContainsKey(RotaryAxisId.FixtureRotation)
&& _config.RotaryAxes[RotaryAxisId.FixtureRotation].Enabled)
return RotaryAxisId.FixtureRotation;
return null;
}
#endregion
#region | Input Box Editing and Target Position
/// <summary>
/// 设置指定输入框的编辑状态 | Set editing state for specified input box
/// GotFocus 时设为 true 冻结实时更新,LostFocus 时设为 false 恢复更新
/// Set to true on GotFocus to freeze live updates, false on LostFocus to resume
/// </summary>
/// <param name="propertyName">属性名称 | Property name</param>
/// <param name="isEditing">是否正在编辑 | Whether editing</param>
public void SetEditing(string propertyName, bool isEditing)
{
_editingFlags[propertyName] = isEditing;
}
/// <summary>
/// 确认输入框编辑,发送目标位置移动命令 | Confirm input box edit, send target position move command
/// Enter 键触发,调用 MoveToTarget/MoveRotaryToTarget 后恢复实时更新
/// Triggered by Enter key, calls MoveToTarget/MoveRotaryToTarget then resumes live updates
/// </summary>
/// <param name="propertyName">属性名称 | Property name</param>
public void ConfirmPosition(string propertyName)
{
var value = GetPropertyValue(propertyName);
SafeRun(() =>
{
var result = SendMoveCommand(propertyName, value);
if (result.Success)
_logger.Info("目标位置已发送:{Property}={Value} | Target position sent: {Property}={Value}", propertyName, value);
else
_logger.Warn("目标位置发送失败:{Property}={Value},原因={Reason} | Target position send failed: {Property}={Value}, reason={Reason}", propertyName, value, result.ErrorMessage);
});
_editingFlags[propertyName] = false;
}
/// <summary>
/// 取消输入框编辑,恢复实时更新并显示当前实际值 | Cancel input box edit, resume live updates and show actual value
/// Escape 键或 LostFocus 触发 | Triggered by Escape key or LostFocus
/// </summary>
/// <param name="propertyName">属性名称 | Property name</param>
public void CancelEditing(string propertyName)
{
_editingFlags[propertyName] = false;
RestoreActualValue(propertyName);
}
/// <summary>
/// 步进移动:上下箭头改变数值并直接发送移动命令,不进入编辑冻结
/// Step move: arrow keys change value and send move command directly, without entering editing freeze
/// </summary>
/// <param name="propertyName">属性名称 | Property name</param>
/// <param name="delta">步进增量(默认 ±0.1| Step delta (default ±0.1)</param>
public void StepPosition(string propertyName, double delta)
{
var currentValue = GetPropertyValue(propertyName);
var newValue = currentValue + delta;
SetPropertyValue(propertyName, newValue);
SafeRun(() =>
{
var result = SendMoveCommand(propertyName, newValue);
if (!result.Success)
_logger.Warn("步进移动失败:{Property},原因={Reason} | Step move failed: {Property}, reason={Reason}", propertyName, result.ErrorMessage);
});
}
/// <summary>
/// 根据属性名称发送对应轴的移动命令 | Send move command for corresponding axis based on property name
/// </summary>
/// <param name="propertyName">属性名称 | Property name</param>
/// <param name="value">目标值 | Target value</param>
/// <returns>操作结果 | Operation result</returns>
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} | Unknown property name: {propertyName}");
}
}
/// <summary>
/// 根据属性名称获取当前绑定值 | Get current bound value by property name
/// </summary>
private double GetPropertyValue(string propertyName)
{
switch (propertyName)
{
case nameof(StageXPosition): return StageXPosition;
case nameof(StageYPosition): return StageYPosition;
case nameof(SourceZPosition): return SourceZPosition;
case nameof(DetectorZPosition): return DetectorZPosition;
case nameof(DetectorSwingAngle): return DetectorSwingAngle;
case nameof(StageRotationAngle): return StageRotationAngle;
case nameof(FixtureRotationAngle): return FixtureRotationAngle;
default: return 0;
}
}
/// <summary>
/// 根据属性名称设置绑定值 | Set bound value by property name
/// </summary>
private void SetPropertyValue(string propertyName, double value)
{
switch (propertyName)
{
case nameof(StageXPosition): StageXPosition = value; break;
case nameof(StageYPosition): StageYPosition = value; break;
case nameof(SourceZPosition): SourceZPosition = value; break;
case nameof(DetectorZPosition): DetectorZPosition = value; break;
case nameof(DetectorSwingAngle): DetectorSwingAngle = value; break;
case nameof(StageRotationAngle): StageRotationAngle = value; break;
case nameof(FixtureRotationAngle): FixtureRotationAngle = value; break;
}
}
/// <summary>
/// 恢复输入框为轴的当前实际值 | Restore input box to axis actual value
/// </summary>
private void RestoreActualValue(string propertyName)
{
try
{
switch (propertyName)
{
case nameof(StageXPosition):
StageXPosition = _motionSystem.GetLinearAxis(AxisId.StageX).ActualPosition;
break;
case nameof(StageYPosition):
StageYPosition = _motionSystem.GetLinearAxis(AxisId.StageY).ActualPosition;
break;
case nameof(SourceZPosition):
SourceZPosition = _motionSystem.GetLinearAxis(AxisId.SourceZ).ActualPosition;
break;
case nameof(DetectorZPosition):
DetectorZPosition = _motionSystem.GetLinearAxis(AxisId.DetectorZ).ActualPosition;
break;
case nameof(DetectorSwingAngle):
DetectorSwingAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.DetectorSwing).ActualAngle;
break;
case nameof(StageRotationAngle):
StageRotationAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.StageRotation).ActualAngle;
break;
case nameof(FixtureRotationAngle):
FixtureRotationAngle = _motionSystem.GetRotaryAxis(RotaryAxisId.FixtureRotation).ActualAngle;
break;
}
}
catch { /* 轴未初始化时忽略 | Ignore when axes not initialized */ }
}
#endregion
#region | Safety Parameter Logic
/// <summary>
/// 确认探测器安全高度限定值 | Confirm detector safety height limit value
/// Enter 键触发,保存当前值 | Triggered by Enter key, saves current value
/// </summary>
public void ConfirmSafetyHeight()
{
_logger.Info("探测器安全高度限定值已保存:{Value} | Detector safety height limit saved: {Value}", SafetyHeight);
}
/// <summary>
/// 确认校准自动计算值 | Confirm calibration auto-calculated value
/// Enter 键触发,保存当前值 | Triggered by Enter key, saves current value
/// </summary>
public void ConfirmCalibrationValue()
{
_logger.Info("校准自动计算值已保存:{Value} | Calibration auto-calculated value saved: {Value}", CalibrationValue);
}
#endregion
#region | Internal Data Classes
/// <summary>
/// 保存的轴位置数据 | Saved axis position data
/// </summary>
private class SavedPositions
{
public double StageX { get; set; }
public double StageY { get; set; }
public double SourceZ { get; set; }
public double DetectorZ { get; set; }
public double DetectorSwing { get; set; }
public double StageRotation { get; set; }
public double FixtureRotation { get; set; }
}
#endregion
}
}
@@ -0,0 +1,237 @@
<UserControl x:Class="XP.Hardware.MotionControl.Views.AxisControlView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:prism="http://prismlibrary.com/"
xmlns:controls="clr-namespace:XP.Common.Controls;assembly=XP.Common"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
HorizontalAlignment="Stretch" Background="White">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ========== 左侧区域:轴位置 + 安全参数 | Left: Axis Positions + Safety Params ========== -->
<StackPanel Grid.Column="0" Margin="0,0,8,0">
<!-- 轴位置输入框组 | Axis Position Inputs -->
<GroupBox Margin="0,0,0,6">
<GroupBox.Header>
<TextBlock Text="{loc:Localization AC_AxisPositions}" FontWeight="Bold" FontSize="12"/>
</GroupBox.Header>
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="80"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Stage X -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="{loc:Localization AC_StageX}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"/>
<telerik:RadNumericUpDown Grid.Row="0" Grid.Column="1" x:Name="NumStageX"
Value="{Binding StageXPosition, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
GotFocus="NumStageX_GotFocus" LostFocus="NumStageX_LostFocus" KeyDown="NumStageX_KeyDown"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
<!-- Stage Y -->
<TextBlock Grid.Row="1" Grid.Column="0" Text="{loc:Localization AC_StageY}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"/>
<telerik:RadNumericUpDown Grid.Row="1" Grid.Column="1" x:Name="NumStageY"
Value="{Binding StageYPosition, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
GotFocus="NumStageY_GotFocus" LostFocus="NumStageY_LostFocus" KeyDown="NumStageY_KeyDown"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
<!-- Source Z -->
<TextBlock Grid.Row="2" Grid.Column="0" Text="{loc:Localization AC_SourceZ}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"/>
<telerik:RadNumericUpDown Grid.Row="2" Grid.Column="1" x:Name="NumSourceZ"
Value="{Binding SourceZPosition, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
GotFocus="NumSourceZ_GotFocus" LostFocus="NumSourceZ_LostFocus" KeyDown="NumSourceZ_KeyDown"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
<!-- Detector Z -->
<TextBlock Grid.Row="3" Grid.Column="0" Text="{loc:Localization AC_DetectorZ}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"/>
<telerik:RadNumericUpDown Grid.Row="3" Grid.Column="1" x:Name="NumDetectorZ"
Value="{Binding DetectorZPosition, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
GotFocus="NumDetectorZ_GotFocus" LostFocus="NumDetectorZ_LostFocus" KeyDown="NumDetectorZ_KeyDown"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
<!-- Detector Swing(动态显示/隐藏)| Dynamic visibility -->
<TextBlock Grid.Row="4" Grid.Column="0" Text="{loc:Localization AC_DetectorSwing}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"
Visibility="{Binding DetectorSwingVisibility}"/>
<telerik:RadNumericUpDown Grid.Row="4" Grid.Column="1" x:Name="NumDetSwing"
Value="{Binding DetectorSwingAngle, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
GotFocus="NumDetSwing_GotFocus" LostFocus="NumDetSwing_LostFocus" KeyDown="NumDetSwing_KeyDown"
Visibility="{Binding DetectorSwingVisibility}"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
<!-- Stage Rotation(动态显示/隐藏)| Dynamic visibility -->
<TextBlock Grid.Row="5" Grid.Column="0" Text="{loc:Localization AC_StageRotation}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"
Visibility="{Binding StageRotationVisibility}"/>
<telerik:RadNumericUpDown Grid.Row="5" Grid.Column="1" x:Name="NumStageRot"
Value="{Binding StageRotationAngle, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
GotFocus="NumStageRot_GotFocus" LostFocus="NumStageRot_LostFocus" KeyDown="NumStageRot_KeyDown"
Visibility="{Binding StageRotationVisibility}"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
<!-- Fixture Rotation(动态显示/隐藏)| Dynamic visibility -->
<TextBlock Grid.Row="6" Grid.Column="0" Text="{loc:Localization AC_FixtureRotation}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"
Visibility="{Binding FixtureRotationVisibility}"/>
<telerik:RadNumericUpDown Grid.Row="6" Grid.Column="1" x:Name="NumFixtureRot"
Value="{Binding FixtureRotationAngle, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
GotFocus="NumFixtureRot_GotFocus" LostFocus="NumFixtureRot_LostFocus" KeyDown="NumFixtureRot_KeyDown"
Visibility="{Binding FixtureRotationVisibility}"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
</Grid>
</GroupBox>
<!-- 安全参数输入框组 | Safety Parameter Inputs -->
<GroupBox Margin="0,0,0,6">
<GroupBox.Header>
<TextBlock Text="{loc:Localization AC_SafetyParams}" FontWeight="Bold" FontSize="12"/>
</GroupBox.Header>
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="80"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 安全高度 | Safety Height -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="{loc:Localization AC_SafetyHeight}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"/>
<telerik:RadNumericUpDown Grid.Row="0" Grid.Column="1" x:Name="NumSafetyHeight"
Value="{Binding SafetyHeight, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
KeyDown="NumSafetyHeight_KeyDown"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
<!-- 校准计算值 | Calibration Value -->
<TextBlock Grid.Row="1" Grid.Column="0" Text="{loc:Localization AC_CalibrationValue}"
FontSize="12" VerticalAlignment="Center" Margin="0,2"/>
<telerik:RadNumericUpDown Grid.Row="1" Grid.Column="1" x:Name="NumCalibration"
Value="{Binding CalibrationValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ValueFormat="Numeric" NumberDecimalDigits="2" SmallChange="0.1" IsEditable="True"
KeyDown="NumCalibration_KeyDown"
Margin="4,2" telerik:StyleManager.Theme="Crystal"/>
</Grid>
</GroupBox>
<!-- 错误信息显示 | Error Message Display -->
<TextBlock Text="{Binding ErrorMessage}" FontSize="11" Foreground="#FFE53935"
TextWrapping="Wrap" Margin="0,2">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ErrorMessage}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding ErrorMessage}" Value="">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<!-- ========== 右侧区域:摇杆 + 操作按钮 | Right: Joysticks + Action Buttons ========== -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 摇杆区域 | Joystick Area -->
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- 单轴摇杆(SingleAxisY):Source_Z / Detector_Z | Single-axis joystick -->
<controls:VirtualJoystick x:Name="SingleJoystick"
JoystickMode="SingleAxisY"
Width="150" Height="200"
IsEnabled="{Binding IsJoystickEnabled}"
DefaultTopIcon="↑" DefaultBottomIcon="↓"
LeftButtonTopIcon="Src↑" LeftButtonBottomIcon="Src↓"
RightButtonTopIcon="Det↑" RightButtonBottomIcon="Det↓"
Margin="0,0,16,0"/>
<!-- 双轴摇杆(DualAxis):Stage X/Y + Swing/Rotation | Dual-axis joystick -->
<controls:VirtualJoystick x:Name="DualJoystick"
JoystickMode="DualAxis"
Width="200" Height="200"
IsEnabled="{Binding IsJoystickEnabled}"
DefaultTopIcon="↑" DefaultBottomIcon="↓" DefaultLeftIcon="←" DefaultRightIcon="→"
LeftButtonTopIcon="Y+" LeftButtonBottomIcon="Y-" LeftButtonLeftIcon="X-" LeftButtonRightIcon="X+"
RightButtonTopIcon="Rot+" RightButtonBottomIcon="Rot-" RightButtonLeftIcon="Swing-" RightButtonRightIcon="Swing+"/>
</StackPanel>
<!-- 操作按钮区域 | Action Buttons Area -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,0">
<!-- 使能开关 | Enable Toggle -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,12,0">
<TextBlock Text="{loc:Localization AC_Enable}" FontSize="12" VerticalAlignment="Center" Margin="0,0,6,0"/>
<telerik:RadToggleSwitchButton IsChecked="{Binding IsJoystickEnabled, Mode=TwoWay}"
Command="{Binding ToggleEnableCommand}"
telerik:StyleManager.Theme="Crystal"/>
</StackPanel>
<!-- 保存按钮 | Save Button -->
<telerik:RadButton Content="{loc:Localization AC_Save}"
Command="{Binding SavePositionsCommand}"
Width="70" Height="28" Margin="4,0" FontSize="12"
telerik:StyleManager.Theme="Crystal">
<telerik:RadButton.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FF62B8E0" Offset="1"/>
</LinearGradientBrush>
</telerik:RadButton.Background>
</telerik:RadButton>
<!-- 恢复按钮 | Restore Button -->
<telerik:RadButton Content="{loc:Localization AC_Restore}"
Command="{Binding RestorePositionsCommand}"
Width="70" Height="28" Margin="4,0" FontSize="12"
telerik:StyleManager.Theme="Crystal">
<telerik:RadButton.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FFFFD54F" Offset="1"/>
</LinearGradientBrush>
</telerik:RadButton.Background>
</telerik:RadButton>
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,234 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using XP.Common.Controls;
using XP.Hardware.MotionControl.ViewModels;
namespace XP.Hardware.MotionControl.Views
{
/// <summary>
/// 轴控制面板视图 | Axis Control Panel View
/// 集成摇杆、轴位置输入框、安全参数和使能控制的 UserControl
/// UserControl integrating joysticks, axis position inputs, safety parameters and enable control
/// </summary>
public partial class AxisControlView : UserControl
{
public AxisControlView()
{
InitializeComponent();
// 监听摇杆只读依赖属性变化,推送到 ViewModel | Listen to joystick read-only DP changes and push to ViewModel
var dualOutputXDesc = DependencyPropertyDescriptor.FromProperty(VirtualJoystick.OutputXProperty, typeof(VirtualJoystick));
var dualOutputYDesc = DependencyPropertyDescriptor.FromProperty(VirtualJoystick.OutputYProperty, typeof(VirtualJoystick));
var dualButtonDesc = DependencyPropertyDescriptor.FromProperty(VirtualJoystick.ActiveMouseButtonProperty, typeof(VirtualJoystick));
var singleOutputYDesc = DependencyPropertyDescriptor.FromProperty(VirtualJoystick.OutputYProperty, typeof(VirtualJoystick));
var singleButtonDesc = DependencyPropertyDescriptor.FromProperty(VirtualJoystick.ActiveMouseButtonProperty, typeof(VirtualJoystick));
dualOutputXDesc?.AddValueChanged(DualJoystick, (s, e) =>
{
if (ViewModel != null) ViewModel.DualJoystickOutputX = DualJoystick.OutputX;
});
dualOutputYDesc?.AddValueChanged(DualJoystick, (s, e) =>
{
if (ViewModel != null) ViewModel.DualJoystickOutputY = DualJoystick.OutputY;
});
dualButtonDesc?.AddValueChanged(DualJoystick, (s, e) =>
{
if (ViewModel != null) ViewModel.DualJoystickActiveButton = DualJoystick.ActiveMouseButton;
});
singleOutputYDesc?.AddValueChanged(SingleJoystick, (s, e) =>
{
if (ViewModel != null) ViewModel.SingleJoystickOutputY = SingleJoystick.OutputY;
});
singleButtonDesc?.AddValueChanged(SingleJoystick, (s, e) =>
{
if (ViewModel != null) ViewModel.SingleJoystickActiveButton = SingleJoystick.ActiveMouseButton;
});
}
/// <summary>
/// 获取当前 ViewModel 实例 | Get current ViewModel instance
/// </summary>
private AxisControlViewModel ViewModel => DataContext as AxisControlViewModel;
#region Stage X | Stage X Event Handlers
private void NumStageX_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel?.SetEditing(nameof(AxisControlViewModel.StageXPosition), true);
}
private void NumStageX_LostFocus(object sender, RoutedEventArgs e)
{
ViewModel?.CancelEditing(nameof(AxisControlViewModel.StageXPosition));
}
private void NumStageX_KeyDown(object sender, KeyEventArgs e)
{
HandleAxisKeyDown(nameof(AxisControlViewModel.StageXPosition), e);
}
#endregion
#region Stage Y | Stage Y Event Handlers
private void NumStageY_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel?.SetEditing(nameof(AxisControlViewModel.StageYPosition), true);
}
private void NumStageY_LostFocus(object sender, RoutedEventArgs e)
{
ViewModel?.CancelEditing(nameof(AxisControlViewModel.StageYPosition));
}
private void NumStageY_KeyDown(object sender, KeyEventArgs e)
{
HandleAxisKeyDown(nameof(AxisControlViewModel.StageYPosition), e);
}
#endregion
#region Source Z | Source Z Event Handlers
private void NumSourceZ_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel?.SetEditing(nameof(AxisControlViewModel.SourceZPosition), true);
}
private void NumSourceZ_LostFocus(object sender, RoutedEventArgs e)
{
ViewModel?.CancelEditing(nameof(AxisControlViewModel.SourceZPosition));
}
private void NumSourceZ_KeyDown(object sender, KeyEventArgs e)
{
HandleAxisKeyDown(nameof(AxisControlViewModel.SourceZPosition), e);
}
#endregion
#region Detector Z | Detector Z Event Handlers
private void NumDetectorZ_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel?.SetEditing(nameof(AxisControlViewModel.DetectorZPosition), true);
}
private void NumDetectorZ_LostFocus(object sender, RoutedEventArgs e)
{
ViewModel?.CancelEditing(nameof(AxisControlViewModel.DetectorZPosition));
}
private void NumDetectorZ_KeyDown(object sender, KeyEventArgs e)
{
HandleAxisKeyDown(nameof(AxisControlViewModel.DetectorZPosition), e);
}
#endregion
#region Detector Swing | Detector Swing Event Handlers
private void NumDetSwing_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel?.SetEditing(nameof(AxisControlViewModel.DetectorSwingAngle), true);
}
private void NumDetSwing_LostFocus(object sender, RoutedEventArgs e)
{
ViewModel?.CancelEditing(nameof(AxisControlViewModel.DetectorSwingAngle));
}
private void NumDetSwing_KeyDown(object sender, KeyEventArgs e)
{
HandleAxisKeyDown(nameof(AxisControlViewModel.DetectorSwingAngle), e);
}
#endregion
#region Stage Rotation | Stage Rotation Event Handlers
private void NumStageRot_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel?.SetEditing(nameof(AxisControlViewModel.StageRotationAngle), true);
}
private void NumStageRot_LostFocus(object sender, RoutedEventArgs e)
{
ViewModel?.CancelEditing(nameof(AxisControlViewModel.StageRotationAngle));
}
private void NumStageRot_KeyDown(object sender, KeyEventArgs e)
{
HandleAxisKeyDown(nameof(AxisControlViewModel.StageRotationAngle), e);
}
#endregion
#region Fixture Rotation | Fixture Rotation Event Handlers
private void NumFixtureRot_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel?.SetEditing(nameof(AxisControlViewModel.FixtureRotationAngle), true);
}
private void NumFixtureRot_LostFocus(object sender, RoutedEventArgs e)
{
ViewModel?.CancelEditing(nameof(AxisControlViewModel.FixtureRotationAngle));
}
private void NumFixtureRot_KeyDown(object sender, KeyEventArgs e)
{
HandleAxisKeyDown(nameof(AxisControlViewModel.FixtureRotationAngle), e);
}
#endregion
#region | Safety Parameter Key Events
private void NumSafetyHeight_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
ViewModel?.ConfirmSafetyHeight();
e.Handled = true;
}
}
private void NumCalibration_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
ViewModel?.ConfirmCalibrationValue();
e.Handled = true;
}
}
#endregion
#region | Common Key Handler
/// <summary>
/// 轴输入框通用键盘处理:Enter 确认,Escape 取消 | Common axis input key handler: Enter to confirm, Escape to cancel
/// </summary>
private void HandleAxisKeyDown(string propertyName, KeyEventArgs e)
{
if (ViewModel == null) return;
switch (e.Key)
{
case Key.Enter:
ViewModel.ConfirmPosition(propertyName);
e.Handled = true;
break;
case Key.Escape:
ViewModel.CancelEditing(propertyName);
e.Handled = true;
break;
}
}
#endregion
}
}