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 { /// /// 轴控制面板 ViewModel | Axis Control Panel ViewModel /// 集成摇杆、轴位置输入框、安全参数和使能控制 | Integrates joystick, axis position inputs, safety parameters and enable control /// 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 /// 直线轴 Jog 活跃状态 | Linear axis jog active states private readonly Dictionary _linearJogActive = new(); /// 旋转轴 Jog 活跃状态 | Rotary axis jog active states private readonly Dictionary _rotaryJogActive = new(); /// 输入框编辑冻结标志 | Input box editing freeze flags private readonly Dictionary _editingFlags = new(); /// 保存的轴位置数据 | Saved axis position data 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(); // 监听 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); ToggleSwapMouseButtonsCommand = new DelegateCommand(ExecuteToggleSwapMouseButtons); SavePositionsCommand = new DelegateCommand(ExecuteSavePositions, () => IsPlcConnected); RestorePositionsCommand = new DelegateCommand(ExecuteRestorePositions, () => _savedPositions != null && IsPlcConnected); // 射线源与探测器Z轴联动命令 | Source-Detector Z-axis linkage command SZDZLockCommand = new DelegateCommand(() => SafeRun(ExecuteSZDZLock), () => 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().Subscribe(OnGeometryUpdated, ThreadOption.UIThread); _eventAggregator.GetEvent().Subscribe(OnAxisStatusChanged, ThreadOption.UIThread); _eventAggregator.GetEvent().Subscribe(OnJoystickActiveChanged, 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; /// 双轴摇杆 X 轴输出 | Dual-axis joystick X output public double DualJoystickOutputX { get => _dualJoystickOutputX; set => SetProperty(ref _dualJoystickOutputX, value); } private double _dualJoystickOutputY; /// 双轴摇杆 Y 轴输出 | Dual-axis joystick Y output public double DualJoystickOutputY { get => _dualJoystickOutputY; set { if (SetProperty(ref _dualJoystickOutputY, value)) HandleDualJoystickOutput(); } } private MouseButtonType _dualJoystickActiveButton; /// 双轴摇杆当前激活按键 | Dual-axis joystick active mouse button 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; /// 单轴摇杆 Y 轴输出 | Single-axis joystick Y output public double SingleJoystickOutputY { get => _singleJoystickOutputY; set { if (SetProperty(ref _singleJoystickOutputY, value)) HandleSingleJoystickOutput(); } } private MouseButtonType _singleJoystickActiveButton; /// 单轴摇杆当前激活按键 | Single-axis joystick active mouse button 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; /// 载物台 X 轴位置 | Stage X axis position public double StageXPosition { get => _stageXPosition; set => SetProperty(ref _stageXPosition, value); } private double _stageYPosition; /// 载物台 Y 轴位置 | Stage Y axis position public double StageYPosition { get => _stageYPosition; set => SetProperty(ref _stageYPosition, value); } private double _sourceZPosition; /// 射线源 Z 轴位置 | Source Z axis position public double SourceZPosition { get => _sourceZPosition; set => SetProperty(ref _sourceZPosition, value); } private double _detectorZPosition; /// 探测器 Z 轴位置 | Detector Z axis position public double DetectorZPosition { get => _detectorZPosition; set => SetProperty(ref _detectorZPosition, value); } private double _detectorSwingAngle; /// 探测器摆动角度 | Detector swing angle public double DetectorSwingAngle { get => _detectorSwingAngle; set => SetProperty(ref _detectorSwingAngle, value); } private double _stageRotationAngle; /// 载物台旋转角度 | Stage rotation angle public double StageRotationAngle { get => _stageRotationAngle; set => SetProperty(ref _stageRotationAngle, value); } private double _fixtureRotationAngle; /// 夹具旋转角度 | Fixture rotation angle public double FixtureRotationAngle { get => _fixtureRotationAngle; set => SetProperty(ref _fixtureRotationAngle, value); } #endregion #region 旋转轴可见性 | Rotary Axis Visibility private Visibility _detectorSwingVisibility; /// 探测器摆动输入框可见性 | Detector swing input box visibility public Visibility DetectorSwingVisibility { get => _detectorSwingVisibility; private set => SetProperty(ref _detectorSwingVisibility, value); } private Visibility _stageRotationVisibility; /// 载物台旋转输入框可见性 | Stage rotation input box visibility public Visibility StageRotationVisibility { get => _stageRotationVisibility; private set => SetProperty(ref _stageRotationVisibility, value); } private Visibility _fixtureRotationVisibility; /// 夹具旋转输入框可见性 | Fixture rotation input box visibility public Visibility FixtureRotationVisibility { get => _fixtureRotationVisibility; private set => SetProperty(ref _fixtureRotationVisibility, value); } #endregion #region 安全参数属性 | Safety Parameter Properties private double _safetyHeight; /// 探测器安全高度限定值 | Detector safety height limit public double SafetyHeight { get => _safetyHeight; set => SetProperty(ref _safetyHeight, value); } private double _calibrationValue; /// 校准自动计算值 | Calibration auto-calculated value public double CalibrationValue { get => _calibrationValue; set => SetProperty(ref _calibrationValue, value); } #endregion #region 使能与状态属性 | Enable and Status Properties private bool _isJoystickEnabled = true; /// 摇杆使能状态 | Joystick enable state public bool IsJoystickEnabled { get => _isJoystickEnabled; set => SetProperty(ref _isJoystickEnabled, value); } private bool _swapMouseButtons; /// 是否交换摇杆左右键功能 | Whether to swap left and right joystick button functions public bool SwapMouseButtons { get => _swapMouseButtons; set => SetProperty(ref _swapMouseButtons, value); } private bool _isPlcConnected; /// PLC 连接状态 | PLC connection status public bool IsPlcConnected { get => _isPlcConnected; set => SetProperty(ref _isPlcConnected, value); } private bool _isJoystickActive; /// 实体摇杆是否激活 | Whether physical joystick is active public bool IsJoystickActive { get => _isJoystickActive; set => SetProperty(ref _isJoystickActive, value); } private string _errorMessage; /// 错误提示信息 | Error message public string ErrorMessage { get => _errorMessage; set => SetProperty(ref _errorMessage, value); } // 射线源与探测器Z轴联动状态 | Source-Detector Z-axis linkage state private bool _szdzLock; public bool SZDZLock { get => _szdzLock; set => SetProperty(ref _szdzLock, value); } #endregion #region 保存位置状态 | Saved Positions State /// 是否有保存的位置数据 | Whether saved position data exists public bool HasSavedPositions => _savedPositions != null; #endregion #region 命令 | Commands /// 切换使能开关命令 | Toggle enable switch command public DelegateCommand ToggleEnableCommand { get; } /// 切换摇杆左右键功能命令 | Toggle joystick button swap command public DelegateCommand ToggleSwapMouseButtonsCommand { get; } /// 保存当前轴位置命令 | Save current axis positions command public DelegateCommand SavePositionsCommand { get; } /// 恢复保存的轴位置命令 | Restore saved axis positions command public DelegateCommand RestorePositionsCommand { get; } /// 射线源与探测器Z轴锁定移动命令 | Source-Detector Z-axis lock move command public DelegateCommand SZDZLockCommand { get; } #endregion #region 事件回调 | Event Callbacks /// /// 实体摇杆激活状态变化回调 | Physical joystick active status changed callback /// /// 实体摇杆是否激活 | Whether physical joystick is active private void OnJoystickActiveChanged(bool isActive) { IsJoystickActive = isActive; _logger.Debug("实体摇杆状态更新:{IsActive} | Physical joystick status updated: {IsActive}", isActive); } /// /// 几何参数更新回调,刷新轴实际位置 | Geometry updated callback, refresh axis actual positions /// 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 */ } } /// /// 轴状态变化回调,更新对应轴位置 | Axis status changed callback, update corresponding axis position /// 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 /// /// 检查指定输入框是否正在编辑 | Check if specified input box is being edited /// /// 属性名称 | Property name /// 是否正在编辑 | Whether editing private bool IsEditing(string propertyName) { return _editingFlags.TryGetValue(propertyName, out var editing) && editing; } #endregion #region 异常保护 | Exception Protection /// /// 安全执行:捕获异常避免 UI 崩溃 | Safe execution: catches exceptions to prevent UI crash /// 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 /// /// 刷新所有命令的 CanExecute 状态 | Refresh CanExecute state of all commands /// private void RaiseCommandCanExecuteChanged() { ToggleEnableCommand.RaiseCanExecuteChanged(); ToggleSwapMouseButtonsCommand.RaiseCanExecuteChanged(); SavePositionsCommand.RaiseCanExecuteChanged(); RestorePositionsCommand.RaiseCanExecuteChanged(); } /// /// PLC 连接状态变化处理:断开时停止所有 Jog 并禁用操作,重连时清除错误 /// PLC connection state change handler: stop all jog and disable operations on disconnect, clear error on reconnect /// private void OnPlcConnectionChanged() { IsPlcConnected = _plcService.IsConnected; RaiseCommandCanExecuteChanged(); if (!_plcService.IsConnected) { // PLC 断开:停止所有活跃的 Jog 操作 | PLC disconnected: stop all active jog operations foreach (var axisId in new List(_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 new List(_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 /// /// 切换摇杆使能状态,并发送使能状态到 PLC | Toggle joystick enable state and send to PLC /// private void ExecuteToggleEnable() { IsJoystickEnabled = !IsJoystickEnabled; _logger.Info("摇杆使能状态切换:{Enabled} | Joystick enable toggled: {Enabled}", IsJoystickEnabled); // 发送使能状态到 PLC(根据实际 PLC 信号定义)| Send enable state to PLC (based on actual PLC signal definition) try { // 设置虚拟摇杆使能 | Set virtual joystick enable var result = _motionControlService.SetVirtualJoystickEnable(IsJoystickEnabled); if (!result.Success) { // 写入失败,回滚状态 | Write failed, rollback state IsJoystickEnabled = !IsJoystickEnabled; MessageBox.Show(result.ErrorMessage, XP.Common.Localization.LocalizationHelper.Get("MC_Error"), MessageBoxButton.OK, MessageBoxImage.Warning); } } catch (Exception ex) { // 异常时回滚状态 | Rollback state on exception IsJoystickEnabled = !IsJoystickEnabled; MessageBox.Show(ex.Message, XP.Common.Localization.LocalizationHelper.Get("MC_Error"), MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// 切换左右键交换状态 | Toggle swap mouse buttons state /// private void ExecuteToggleSwapMouseButtons() { SwapMouseButtons = !SwapMouseButtons; _logger.Info("摇杆左右键功能交换:{Enabled} | Joystick button swap toggled: {Enabled}", SwapMouseButtons); } /// /// 保存当前 6 个轴位置到内部变量 | Save current 6 axis positions to internal variable /// private void ExecuteSavePositions() { _savedPositions = new SavedPositions { StageX = StageXPosition, StageY = StageYPosition, SourceZ = SourceZPosition, DetectorZ = DetectorZPosition, DetectorSwing = DetectorSwingAngle, StageRotation = StageRotationAngle, FixtureRotation = FixtureRotationAngle }; RestorePositionsCommand.RaiseCanExecuteChanged(); RaisePropertyChanged(nameof(HasSavedPositions)); _logger.Info("轴位置已保存 | Axis positions saved"); } /// /// 从保存的数据恢复到输入框,并发送移动命令 | Restore saved data to input boxes and send move commands /// 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"); } /// 射线源与探测器Z轴锁定移动 | Source-Detector Z-axis lock move private void ExecuteSZDZLock() { try { // 切换联动状态 | Toggle linkage state SZDZLock = !SZDZLock; // 调用服务设置联动 | Call service to set linkage var result = _motionControlService.SetSourceDetectorZLinkage(SZDZLock); if (!result.Success) { // 如果设置失败,恢复状态 | If set failed, restore state SZDZLock = !SZDZLock; MessageBox.Show(result.ErrorMessage, XP.Common.Localization.LocalizationHelper.Get("MC_Error"), MessageBoxButton.OK, MessageBoxImage.Warning); } } catch (Exception ex) { // 异常时恢复状态 | Restore state on exception SZDZLock = !SZDZLock; MessageBox.Show(ex.Message, XP.Common.Localization.LocalizationHelper.Get("MC_Error"), MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// 同步联动状态到 PLC(供视图加载时调用)| Sync linkage state to PLC (called when view loads) /// /// 操作结果 | Operation result public MotionResult SetSZDZLinkageToPlc() { return _motionControlService.SetSourceDetectorZLinkage(SZDZLock); } /// /// 获取日志服务实例 | Get logger service instance /// public ILoggerService Logger => _logger; #endregion #region 摇杆 Jog 映射逻辑 | Joystick Jog Mapping Logic /// /// 处理双轴摇杆输出变化,根据当前激活按键映射到对应轴的 Jog 操作 /// Handle dual joystick output changes, map to corresponding axis jog based on active button /// private void HandleDualJoystickOutput() { switch (DualJoystickActiveButton) { case MouseButtonType.Left: // 左键:X→StageX Jog,Y→StageY Jog | Left button: X→StageX Jog, Y→StageY Jog UpdateLinearJog(AxisId.StageX, DualJoystickOutputX); UpdateLinearJog(AxisId.StageY, DualJoystickOutputY); break; case MouseButtonType.Right: // 右键:X→DetectorSwing Jog,Y→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; } } /// /// 处理单轴摇杆输出变化,根据当前激活按键映射到对应轴的 Jog 操作 /// Handle single joystick output changes, map to corresponding axis jog based on active button /// private void HandleSingleJoystickOutput() { switch (SingleJoystickActiveButton) { case MouseButtonType.Left: // 左键:Y→SourceZ Jog | Left button: Y→SourceZ Jog UpdateLinearJog(AxisId.SourceZ, SingleJoystickOutputY); if (_szdzLock) { UpdateLinearJog(AxisId.DetectorZ, SingleJoystickOutputY); } break; case MouseButtonType.Right: // 右键:Y→DetectorZ Jog | Right button: Y→DetectorZ Jog UpdateLinearJog(AxisId.DetectorZ, SingleJoystickOutputY); if (_szdzLock) { UpdateLinearJog(AxisId.SourceZ, SingleJoystickOutputY); } break; } } /// /// 更新直线轴 Jog 状态:启动、更新速度或停止 /// Update linear axis jog state: start, update speed, or stop /// /// 直线轴标识 | Linear axis identifier /// 摇杆输出值(-1.0 ~ 1.0)| Joystick output value (-1.0 ~ 1.0) 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); }); } } } /// /// 更新旋转轴 Jog 状态:启动、更新速度或停止 /// Update rotary axis jog state: start, update speed, or stop /// /// 旋转轴标识 | Rotary axis identifier /// 摇杆输出值(-1.0 ~ 1.0)| Joystick output value (-1.0 ~ 1.0) 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); }); } } } /// /// 停止双轴摇杆控制的所有轴 Jog | Stop all axes controlled by dual joystick /// 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); } /// /// 停止单轴摇杆控制的所有轴 Jog | Stop all axes controlled by single joystick /// private void StopSingleJoystickAxes() { // 左键关联轴:SourceZ | Left button axis: SourceZ UpdateLinearJog(AxisId.SourceZ, 0); // 右键关联轴:DetectorZ | Right button axis: DetectorZ UpdateLinearJog(AxisId.DetectorZ, 0); } /// /// 获取当前启用的旋转轴标识(StageRotation 或 FixtureRotation 二选一) /// Get the currently enabled rotation axis ID (StageRotation or FixtureRotation, one of two) /// /// 启用的旋转轴标识,若均未启用则返回 null | Enabled rotary axis ID, or null if none enabled 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 /// /// 设置指定输入框的编辑状态 | 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 /// /// 属性名称 | Property name /// 是否正在编辑 | Whether editing public void SetEditing(string propertyName, bool isEditing) { _editingFlags[propertyName] = isEditing; } /// /// 确认输入框编辑,发送目标位置移动命令 | Confirm input box edit, send target position move command /// Enter 键触发,调用 MoveToTarget/MoveRotaryToTarget 后恢复实时更新 /// Triggered by Enter key, calls MoveToTarget/MoveRotaryToTarget then resumes live updates /// /// 属性名称 | Property name 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, propertyName); } else { 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; } /// /// 取消输入框编辑,恢复实时更新并显示当前实际值 | Cancel input box edit, resume live updates and show actual value /// Escape 键或 LostFocus 触发 | Triggered by Escape key or LostFocus /// /// 属性名称 | Property name public void CancelEditing(string propertyName) { _editingFlags[propertyName] = false; RestoreActualValue(propertyName); } /// /// 步进移动:上下箭头改变数值并直接发送移动命令,不进入编辑冻结 /// Step move: arrow keys change value and send move command directly, without entering editing freeze /// /// 属性名称 | Property name /// 步进增量(默认 ±0.1)| Step delta (default ±0.1) public void StepPosition(string propertyName, double delta) { var currentValue = GetPropertyValue(propertyName); var newValue = currentValue + delta; SetPropertyValue(propertyName, newValue); SafeRun(() => { MotionResult result; // 如果射线源与探测器Z轴联动,且操作的是SourceZ或DetectorZ,则同时步进两个轴 if (SZDZLock && (propertyName == nameof(SourceZPosition) || propertyName == nameof(DetectorZPosition))) { result = SendSourceDetectorZMoveCommand(newValue, propertyName); } else { result = SendMoveCommand(propertyName, newValue); } if (!result.Success) _logger.Warn("步进移动失败:{Property},原因={Reason} | Step move failed: {Property}, reason={Reason}", propertyName, result.ErrorMessage); }); } /// /// 根据属性名称发送对应轴的移动命令 | Send move command for corresponding axis based on property name /// /// 属性名称 | Property name /// 目标值 | Target value /// 操作结果 | Operation result 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}"); } } /// /// 发送射线源与探测器Z轴联动移动命令 | Send Source-Detector Z-axis linkage move command /// 当SZDZLock=true时,保持两个轴的位移量相同 /// When SZDZLock=true, keeps the same displacement for both axes /// /// 目标位置 | Target position /// 操作结果 | Operation result private MotionResult SendSourceDetectorZMoveCommand(double targetValue, string sourcePropertyName = null) { // 1. 检查联动是否在配置中启用 | Check if linkage is enabled in config if (!_config.SourceDetectorZLinkage.Enabled) { _logger.Warn("射线源与探测器Z轴联动未在配置中启用 | Source-Detector Z-axis linkage not enabled in config"); return MotionResult.Fail("射线源与探测器Z轴联动未启用 | Source-Detector Z-axis linkage not enabled"); } // 2. 获取两个轴并检查是否都在空闲状态 | Get both axes and check if both are idle var sourceZAxis = _motionSystem.GetLinearAxis(AxisId.SourceZ); var detectorZAxis = _motionSystem.GetLinearAxis(AxisId.DetectorZ); if (sourceZAxis.Status == AxisStatus.Moving) { _logger.Warn("射线源Z轴正在运动中,拒绝联动移动命令 | Source Z-axis is moving, linkage move command rejected"); return MotionResult.Fail("射线源Z轴正在运动中,无法联动移动 | Source Z-axis is moving, linkage move rejected"); } if (detectorZAxis.Status == AxisStatus.Moving) { _logger.Warn("探测器Z轴正在运动中,拒绝联动移动命令 | Detector Z-axis is moving, linkage move command rejected"); return MotionResult.Fail("探测器Z轴正在运动中,无法联动移动 | Detector Z-axis is moving, linkage move rejected"); } // 3. 根据编辑来源计算位移量和目标位置 | Calculate displacement and targets based on edit source var currentSourceZ = sourceZAxis.ActualPosition; var currentDetectorZ = detectorZAxis.ActualPosition; double targetSourceZ; double targetDetectorZ; if (sourcePropertyName == nameof(DetectorZPosition)) { // 用户编辑的是探测器Z:以探测器Z为基准计算位移 | User edited DetectorZ: calculate delta from DetectorZ var delta = targetValue - currentDetectorZ; targetDetectorZ = targetValue; targetSourceZ = currentSourceZ + delta; } else { // 用户编辑的是射线源Z(默认):以射线源Z为基准计算位移 | User edited SourceZ (default): calculate delta from SourceZ var delta = targetValue - currentSourceZ; targetSourceZ = targetValue; targetDetectorZ = currentDetectorZ + delta; } // 4. 边界检查(使用配置中的 Min/Max)| Boundary check (use Min/Max from config) if (_config.LinearAxes.TryGetValue(AxisId.SourceZ, out var sourceConfig) && _config.LinearAxes.TryGetValue(AxisId.DetectorZ, out var detectorConfig)) { var errors = new List(); if (targetSourceZ < sourceConfig.Min || targetSourceZ > sourceConfig.Max) { errors.Add($"射线源Z轴目标位置 {targetSourceZ} 超出范围 [{sourceConfig.Min}, {sourceConfig.Max}] | " + $"Source Z-axis target {targetSourceZ} out of range [{sourceConfig.Min}, {sourceConfig.Max}]"); } if (targetDetectorZ < detectorConfig.Min || targetDetectorZ > detectorConfig.Max) { errors.Add($"探测器Z轴目标位置 {targetDetectorZ} 超出范围 [{detectorConfig.Min}, {detectorConfig.Max}] | " + $"Detector Z-axis target {targetDetectorZ} out of range [{detectorConfig.Min}, {detectorConfig.Max}]"); } if (errors.Count > 0) { var allErrors = string.Join("; ", errors); _logger.Warn("联动移动边界检查失败:{Errors} | Linkage move boundary check failed: {Errors}", allErrors); return MotionResult.Fail(allErrors); } } else { _logger.Error(null, "未找到射线源Z或探测器Z轴配置 | Source Z or Detector Z axis config not found"); return MotionResult.Fail("轴配置缺失 | Axis config missing"); } // 5. 所有检查通过,同时发送两个轴的移动命令 | All checks passed, send move commands to both axes simultaneously var sourceResult = sourceZAxis.MoveToTarget(targetSourceZ, _config.DefaultVelocity); var detectorResult = detectorZAxis.MoveToTarget(targetDetectorZ, _config.DefaultVelocity); // 6. 返回结果(任一失败都记录日志)| Return result (log if any fails) if (!sourceResult.Success) { _logger.Warn("射线源Z轴移动失败:{Reason} | Source Z-axis move failed: {Reason}", sourceResult.ErrorMessage); return sourceResult; } if (!detectorResult.Success) { _logger.Warn("探测器Z轴移动失败:{Reason} | Detector Z-axis move failed: {Reason}", detectorResult.ErrorMessage); return detectorResult; } _logger.Info("射线源与探测器Z轴联动移动成功 | Source-Detector Z-axis linkage move successful"); return sourceResult; } /// /// 根据属性名称获取当前绑定值 | Get current bound value by property name /// 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; } } /// /// 根据属性名称设置绑定值 | Set bound value by property name /// 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; } } /// /// 恢复输入框为轴的当前实际值 | Restore input box to axis actual value /// 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 /// /// 确认探测器安全高度限定值 | Confirm detector safety height limit value /// Enter 键触发,保存当前值 | Triggered by Enter key, saves current value /// public void ConfirmSafetyHeight() { _logger.Info("探测器安全高度限定值已保存:{Value} | Detector safety height limit saved: {Value}", SafetyHeight); } /// /// 确认校准自动计算值 | Confirm calibration auto-calculated value /// Enter 键触发,保存当前值 | Triggered by Enter key, saves current value /// public void ConfirmCalibrationValue() { _logger.Info("校准自动计算值已保存:{Value} | Calibration auto-calculated value saved: {Value}", CalibrationValue); } #endregion #region 内部数据类 | Internal Data Classes /// /// 保存的轴位置数据 | Saved axis position data /// 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 } }