Files
XplorePlane/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs
T

1135 lines
53 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Windows;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using XP.Common.Controls.Joystick;
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);
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<GeometryUpdatedEvent>().Subscribe(OnGeometryUpdated, ThreadOption.UIThread);
_eventAggregator.GetEvent<AxisStatusChangedEvent>().Subscribe(OnAxisStatusChanged, ThreadOption.UIThread);
_eventAggregator.GetEvent<JoystickActiveEvent>().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;
/// <summary>双轴摇杆 X 轴输出 | Dual-axis joystick X output</summary>
public double DualJoystickOutputX
{
get => _dualJoystickOutputX;
set => SetProperty(ref _dualJoystickOutputX, value);
}
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 _swapMouseButtons;
/// <summary>是否交换摇杆左右键功能 | Whether to swap left and right joystick button functions</summary>
public bool SwapMouseButtons { get => _swapMouseButtons; set => SetProperty(ref _swapMouseButtons, value); }
private bool _isPlcConnected;
/// <summary>PLC 连接状态 | PLC connection status</summary>
public bool IsPlcConnected { get => _isPlcConnected; set => SetProperty(ref _isPlcConnected, value); }
private bool _isJoystickActive;
/// <summary>实体摇杆是否激活 | Whether physical joystick is active</summary>
public bool IsJoystickActive { get => _isJoystickActive; set => SetProperty(ref _isJoystickActive, value); }
private string _errorMessage;
/// <summary>错误提示信息 | Error message</summary>
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
/// <summary>是否有保存的位置数据 | Whether saved position data exists</summary>
public bool HasSavedPositions => _savedPositions != null;
#endregion
#region | Commands
/// <summary>切换使能开关命令 | Toggle enable switch command</summary>
public DelegateCommand ToggleEnableCommand { get; }
/// <summary>切换摇杆左右键功能命令 | Toggle joystick button swap command</summary>
public DelegateCommand ToggleSwapMouseButtonsCommand { get; }
/// <summary>保存当前轴位置命令 | Save current axis positions command</summary>
public DelegateCommand SavePositionsCommand { get; }
/// <summary>恢复保存的轴位置命令 | Restore saved axis positions command</summary>
public DelegateCommand RestorePositionsCommand { get; }
/// <summary>射线源与探测器Z轴锁定移动命令 | Source-Detector Z-axis lock move command</summary>
public DelegateCommand SZDZLockCommand { get; }
#endregion
#region | Event Callbacks
/// <summary>
/// 实体摇杆激活状态变化回调 | Physical joystick active status changed callback
/// </summary>
/// <param name="isActive">实体摇杆是否激活 | Whether physical joystick is active</param>
private void OnJoystickActiveChanged(bool isActive)
{
IsJoystickActive = isActive;
_logger.Debug("实体摇杆状态更新:{IsActive} | Physical joystick status updated: {IsActive}", isActive);
}
/// <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();
ToggleSwapMouseButtonsCommand.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 new List<AxisId>(_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<RotaryAxisId>(_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);
// 发送使能状态到 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);
}
}
/// <summary>
/// 切换左右键交换状态 | Toggle swap mouse buttons state
/// </summary>
private void ExecuteToggleSwapMouseButtons()
{
SwapMouseButtons = !SwapMouseButtons;
_logger.Info("摇杆左右键功能交换:{Enabled} | Joystick button swap toggled: {Enabled}", SwapMouseButtons);
}
/// <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();
RaisePropertyChanged(nameof(HasSavedPositions));
_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");
}
/// <summary>射线源与探测器Z轴锁定移动 | Source-Detector Z-axis lock move</summary>
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);
}
}
/// <summary>
/// 同步联动状态到 PLC(供视图加载时调用)| Sync linkage state to PLC (called when view loads)
/// </summary>
/// <returns>操作结果 | Operation result</returns>
public MotionResult SetSZDZLinkageToPlc()
{
return _motionControlService.SetSourceDetectorZLinkage(SZDZLock);
}
/// <summary>
/// 获取日志服务实例 | Get logger service instance
/// </summary>
public ILoggerService Logger => _logger;
#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);
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;
}
}
/// <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(() =>
{
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;
}
/// <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(() =>
{
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);
});
}
/// <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>
/// 发送射线源与探测器Z轴联动移动命令 | Send Source-Detector Z-axis linkage move command
/// 当SZDZLock=true时,保持两个轴的位移量相同
/// When SZDZLock=true, keeps the same displacement for both axes
/// </summary>
/// <param name="targetValue">目标位置 | Target position</param>
/// <returns>操作结果 | Operation result</returns>
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<string>();
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;
}
/// <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
}
}