using System; using System.Collections.Generic; using System.Linq; using System.Windows; using Prism.Commands; using Prism.Events; using Prism.Mvvm; 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.MotionControl.Views; using XP.Hardware.Plc.Abstractions; namespace XP.Hardware.MotionControl.ViewModels { /// /// 运动控制操作面板 ViewModel | Motion Control Operation Panel ViewModel /// 提供轴状态、几何参数、安全门状态的可绑定属性和操作命令(不含 Jog 调试) /// Provides axis status, geometry, safety door properties and commands (excluding Jog debug) /// public class MotionControlViewModel : BindableBase { private readonly IMotionControlService _motionControlService; private readonly IMotionSystem _motionSystem; private readonly IEventAggregator _eventAggregator; private readonly MotionControlConfig _config; private readonly IPlcService _plcService; private Window _debugWindow; #region 构造函数 | Constructor public MotionControlViewModel( IMotionControlService motionControlService, IMotionSystem motionSystem, IEventAggregator eventAggregator, MotionControlConfig config, IPlcService plcService) { _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)); // 监听 PLC 连接状态变化 | Listen for PLC connection status changes _plcService.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(IPlcService.IsConnected)) { RaisePropertyChanged(nameof(IsPlcConnected)); RaiseCommandCanExecuteChanged(); } }; // 初始化可选轴可见性 | Initialize optional axis visibility 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; // 初始化命令(通过 SafeRun 包裹,防止 PLC 异常崩溃)| Initialize commands (wrapped with SafeRun to prevent PLC exception crash) MoveCommand = new DelegateCommand(() => SafeRun(ExecuteMove), () => IsPlcConnected); StopCommand = new DelegateCommand(() => SafeRun(() => _motionControlService.StopAll()), () => IsPlcConnected); HomeCommand = new DelegateCommand(() => SafeRun(ExecuteHome), () => IsPlcConnected); OpenDoorCommand = new DelegateCommand(() => SafeRun(() => _motionControlService.OpenDoor()), () => IsPlcConnected); CloseDoorCommand = new DelegateCommand(() => SafeRun(() => _motionControlService.CloseDoor()), () => IsPlcConnected); ApplyGeometryCommand = new DelegateCommand(() => SafeRun(ExecuteApplyGeometry), () => IsPlcConnected); CopyActualToTargetCommand = new DelegateCommand(ExecuteCopyActualToTarget); OpenDebugWindowCommand = new DelegateCommand(ExecuteOpenDebugWindow); // 订阅事件 | Subscribe to events _eventAggregator.GetEvent().Subscribe(OnGeometryUpdated, ThreadOption.UIThread); _eventAggregator.GetEvent().Subscribe(OnAxisStatusChanged, ThreadOption.UIThread); _eventAggregator.GetEvent().Subscribe(OnDoorStatusChanged, ThreadOption.UIThread); _eventAggregator.GetEvent().Subscribe(OnDoorInterlockChanged, ThreadOption.UIThread); // 订阅几何反算填入请求事件 | Subscribe to geometry apply request event _eventAggregator.GetEvent().Subscribe(OnGeometryApplyRequest, ThreadOption.UIThread); // 初始化时主动刷新一次轴位置和门状态 | Refresh axis positions and door status on initialization try { SourceZActual = _motionSystem.GetLinearAxis(AxisId.SourceZ).ActualPosition; DetectorZActual = _motionSystem.GetLinearAxis(AxisId.DetectorZ).ActualPosition; StageXActual = _motionSystem.GetLinearAxis(AxisId.StageX).ActualPosition; StageYActual = _motionSystem.GetLinearAxis(AxisId.StageY).ActualPosition; DetSwingActual = _motionSystem.GetRotaryAxis(RotaryAxisId.DetectorSwing).ActualAngle; StageRotActual = _motionSystem.GetRotaryAxis(RotaryAxisId.StageRotation).ActualAngle; FixtureRotActual = _motionSystem.GetRotaryAxis(RotaryAxisId.FixtureRotation).ActualAngle; DoorStatus = _motionSystem.SafetyDoor.Status; IsInterlocked = _motionSystem.SafetyDoor.IsInterlocked; } catch { } } #endregion #region 直线轴属性 | Linear Axis Properties private double _sourceZActual; public double SourceZActual { get => _sourceZActual; set => SetProperty(ref _sourceZActual, value); } private double _sourceZTarget; public double SourceZTarget { get => _sourceZTarget; set => SetProperty(ref _sourceZTarget, value); } private double _detectorZActual; public double DetectorZActual { get => _detectorZActual; set => SetProperty(ref _detectorZActual, value); } private double _detectorZTarget; public double DetectorZTarget { get => _detectorZTarget; set => SetProperty(ref _detectorZTarget, value); } private double _stageXActual; public double StageXActual { get => _stageXActual; set => SetProperty(ref _stageXActual, value); } private double _stageXTarget; public double StageXTarget { get => _stageXTarget; set => SetProperty(ref _stageXTarget, value); } private double _stageYActual; public double StageYActual { get => _stageYActual; set => SetProperty(ref _stageYActual, value); } private double _stageYTarget; public double StageYTarget { get => _stageYTarget; set => SetProperty(ref _stageYTarget, value); } #endregion #region 旋转轴属性 | Rotary Axis Properties private double _detSwingActual; public double DetSwingActual { get => _detSwingActual; set => SetProperty(ref _detSwingActual, value); } private double _detSwingTarget; public double DetSwingTarget { get => _detSwingTarget; set => SetProperty(ref _detSwingTarget, value); } private double _stageRotActual; public double StageRotActual { get => _stageRotActual; set => SetProperty(ref _stageRotActual, value); } private double _stageRotTarget; public double StageRotTarget { get => _stageRotTarget; set => SetProperty(ref _stageRotTarget, value); } private double _fixtureRotActual; public double FixtureRotActual { get => _fixtureRotActual; set => SetProperty(ref _fixtureRotActual, value); } private double _fixtureRotTarget; public double FixtureRotTarget { get => _fixtureRotTarget; set => SetProperty(ref _fixtureRotTarget, value); } #endregion #region 几何参数属性 | Geometry Properties private double _fod; public double FOD { get => _fod; private set => SetProperty(ref _fod, value); } private double _fdd; public double FDD { get => _fdd; private set => SetProperty(ref _fdd, value); } private double _magnification; public double Magnification { get => _magnification; private set => SetProperty(ref _magnification, value); } private double _targetFOD; public double TargetFOD { get => _targetFOD; set => SetProperty(ref _targetFOD, value); } private double _targetFDD; public double TargetFDD { get => _targetFDD; set => SetProperty(ref _targetFDD, value); } private double _targetMagnification; public double TargetMagnification { get => _targetMagnification; set => SetProperty(ref _targetMagnification, value); } #endregion #region 安全门和状态属性 | Safety Door and Status Properties private DoorStatus _doorStatus; public DoorStatus DoorStatus { get => _doorStatus; private set => SetProperty(ref _doorStatus, value); } private bool _isInterlocked; public bool IsInterlocked { get => _isInterlocked; private set { SetProperty(ref _isInterlocked, value); RaisePropertyChanged(nameof(IsNotInterlocked)); RaisePropertyChanged(nameof(InterlockStatusText)); } } /// 联锁未激活(用于开门按钮 IsEnabled)| Not interlocked (for open door button IsEnabled) public bool IsNotInterlocked => !_isInterlocked; /// 联锁状态文本(用于 UI 显示)| Interlock status text for UI display public string InterlockStatusText => _isInterlocked ? XP.Common.Localization.LocalizationHelper.Get("MC_Interlocked") : XP.Common.Localization.LocalizationHelper.Get("MC_NotInterlocked"); private bool _isMoving; public bool IsMoving { get => _isMoving; private set => SetProperty(ref _isMoving, value); } /// PLC 是否已连接(从 PlcService 读取)| Whether PLC is connected (from PlcService) public bool IsPlcConnected => _plcService.IsConnected; #endregion #region 可选轴可见性 | Optional Axis Visibility private Visibility _stageRotationVisibility; public Visibility StageRotationVisibility { get => _stageRotationVisibility; private set => SetProperty(ref _stageRotationVisibility, value); } private Visibility _fixtureRotationVisibility; public Visibility FixtureRotationVisibility { get => _fixtureRotationVisibility; private set => SetProperty(ref _fixtureRotationVisibility, value); } #endregion #region 命令 | Commands public DelegateCommand MoveCommand { get; } public DelegateCommand StopCommand { get; } public DelegateCommand HomeCommand { get; } public DelegateCommand OpenDoorCommand { get; } public DelegateCommand CloseDoorCommand { get; } public DelegateCommand ApplyGeometryCommand { get; } /// 一键将实际值填入目标值 | Copy actual values to target values public DelegateCommand CopyActualToTargetCommand { get; } /// 打开调试窗口命令 | Open debug window command public DelegateCommand OpenDebugWindowCommand { get; } #endregion #region 命令执行 | Command Execution /// 多轴联动移动(含二次确认)| Multi-axis coordinated move (with confirmation) private void ExecuteMove() { var result = MessageBox.Show( XP.Common.Localization.LocalizationHelper.Get("MC_MoveConfirm"), XP.Common.Localization.LocalizationHelper.Get("MC_Move"), MessageBoxButton.OKCancel, MessageBoxImage.Question); if (result != MessageBoxResult.OK) return; var targets = new Dictionary { { AxisId.SourceZ, SourceZTarget }, { AxisId.DetectorZ, DetectorZTarget }, { AxisId.StageX, StageXTarget }, { AxisId.StageY, StageYTarget } }; _motionControlService.MoveAllToTarget(targets); _motionControlService.MoveRotaryToTarget(RotaryAxisId.DetectorSwing, DetSwingTarget); if (StageRotationVisibility == Visibility.Visible) _motionControlService.MoveRotaryToTarget(RotaryAxisId.StageRotation, StageRotTarget); if (FixtureRotationVisibility == Visibility.Visible) _motionControlService.MoveRotaryToTarget(RotaryAxisId.FixtureRotation, FixtureRotTarget); } /// 所有轴回零(含二次确认)| Home all axes (with confirmation) private void ExecuteHome() { var result = MessageBox.Show( XP.Common.Localization.LocalizationHelper.Get("MC_HomeConfirm"), XP.Common.Localization.LocalizationHelper.Get("MC_Home"), MessageBoxButton.OKCancel, MessageBoxImage.Question); if (result != MessageBoxResult.OK) return; _motionControlService.HomeAll(); } /// /// 几何反算:计算 SourceZ/DetectorZ 目标值并填入,其他轴用实际值填入 /// Geometry inverse: calculate SourceZ/DetectorZ targets and fill in, other axes use actual values /// private void ExecuteApplyGeometry() { var (sourceZTarget, detectorZTarget, error) = _motionControlService.CalculateGeometryTargets(TargetFOD, TargetFDD); if (error != null) { MessageBox.Show(error, XP.Common.Localization.LocalizationHelper.Get("MC_Geometry_Title"), MessageBoxButton.OK, MessageBoxImage.Warning); return; } // 计算得到的轴填入目标值 | Fill calculated axis targets SourceZTarget = sourceZTarget; DetectorZTarget = detectorZTarget; // 不需要移动的轴用实际值填入 | Non-calculated axes use actual values StageXTarget = StageXActual; StageYTarget = StageYActual; DetSwingTarget = DetSwingActual; StageRotTarget = StageRotActual; FixtureRotTarget = FixtureRotActual; } /// /// 一键将所有轴实际值填入目标值 | Copy all actual values to target values /// private void ExecuteCopyActualToTarget() { SourceZTarget = SourceZActual; DetectorZTarget = DetectorZActual; StageXTarget = StageXActual; StageYTarget = StageYActual; DetSwingTarget = DetSwingActual; StageRotTarget = StageRotActual; FixtureRotTarget = FixtureRotActual; } /// 打开调试窗口 | Open debug window private void ExecuteOpenDebugWindow() { if (_debugWindow != null && _debugWindow.IsLoaded) { _debugWindow.Activate(); return; } _debugWindow = new MotionDebugWindow { Owner = Application.Current.MainWindow, ShowInTaskbar = true }; _debugWindow.Closed += (s, e) => _debugWindow = null; _debugWindow.Show(); } #endregion #region 事件回调 | Event Callbacks private void OnGeometryUpdated(GeometryData data) { FOD = data.FOD; FDD = data.FDD; Magnification = data.Magnification; // 每次轮询都刷新所有轴的实际位置 | Refresh all axis actual positions on every poll try { SourceZActual = _motionSystem.GetLinearAxis(AxisId.SourceZ).ActualPosition; DetectorZActual = _motionSystem.GetLinearAxis(AxisId.DetectorZ).ActualPosition; StageXActual = _motionSystem.GetLinearAxis(AxisId.StageX).ActualPosition; StageYActual = _motionSystem.GetLinearAxis(AxisId.StageY).ActualPosition; DetSwingActual = _motionSystem.GetRotaryAxis(RotaryAxisId.DetectorSwing).ActualAngle; StageRotActual = _motionSystem.GetRotaryAxis(RotaryAxisId.StageRotation).ActualAngle; FixtureRotActual = _motionSystem.GetRotaryAxis(RotaryAxisId.FixtureRotation).ActualAngle; DoorStatus = _motionSystem.SafetyDoor.Status; } catch { /* 轴未初始化时忽略 | Ignore when axes not initialized */ } } private void OnAxisStatusChanged(AxisStatusChangedData data) { UpdateLinearAxisActual(data.AxisId); UpdateRotaryAxesActual(); UpdateDoorStatus(); IsMoving = _motionSystem.LinearAxes.Values.Any(a => a.Status == AxisStatus.Moving || a.Status == AxisStatus.Homing) || _motionSystem.RotaryAxes.Values.Any(a => a.Status == AxisStatus.Moving || a.Status == AxisStatus.Homing); } private void OnDoorStatusChanged(DoorStatus status) { DoorStatus = status; IsInterlocked = _motionSystem.SafetyDoor.IsInterlocked; } private void OnDoorInterlockChanged(bool isInterlocked) { IsInterlocked = isInterlocked; } /// /// 收到几何反算填入请求,将计算结果填入目标值 | Receive geometry apply request, fill target values /// private void OnGeometryApplyRequest(GeometryApplyRequestData data) { SourceZTarget = data.SourceZTarget; DetectorZTarget = data.DetectorZTarget; StageXTarget = data.StageXActual; StageYTarget = data.StageYActual; DetSwingTarget = data.DetSwingActual; StageRotTarget = data.StageRotActual; FixtureRotTarget = data.FixtureRotActual; } #endregion #region 状态更新辅助 | Status Update Helpers private void UpdateLinearAxisActual(AxisId axisId) { switch (axisId) { case AxisId.SourceZ: SourceZActual = _motionSystem.GetLinearAxis(AxisId.SourceZ).ActualPosition; break; case AxisId.DetectorZ: DetectorZActual = _motionSystem.GetLinearAxis(AxisId.DetectorZ).ActualPosition; break; case AxisId.StageX: StageXActual = _motionSystem.GetLinearAxis(AxisId.StageX).ActualPosition; break; case AxisId.StageY: StageYActual = _motionSystem.GetLinearAxis(AxisId.StageY).ActualPosition; break; } } private void UpdateRotaryAxesActual() { DetSwingActual = _motionSystem.GetRotaryAxis(RotaryAxisId.DetectorSwing).ActualAngle; StageRotActual = _motionSystem.GetRotaryAxis(RotaryAxisId.StageRotation).ActualAngle; FixtureRotActual = _motionSystem.GetRotaryAxis(RotaryAxisId.FixtureRotation).ActualAngle; } private void UpdateDoorStatus() { DoorStatus = _motionSystem.SafetyDoor.Status; IsInterlocked = _motionSystem.SafetyDoor.IsInterlocked; } #endregion #region 异常保护 | Exception Protection /// /// 安全执行:捕获 PLC 异常避免 UI 崩溃 | Safe execution: catches PLC exceptions to prevent UI crash /// private void SafeRun(Action action) { try { action(); } catch (Exception ex) { try { System.Windows.MessageBox.Show(ex.Message, "MotionControl", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning); } catch { /* 防止 MessageBox 本身异常 | Prevent MessageBox itself from throwing */ } } } /// /// 刷新所有命令的 CanExecute 状态 | Refresh CanExecute state of all commands /// private void RaiseCommandCanExecuteChanged() { MoveCommand.RaiseCanExecuteChanged(); StopCommand.RaiseCanExecuteChanged(); HomeCommand.RaiseCanExecuteChanged(); OpenDoorCommand.RaiseCanExecuteChanged(); CloseDoorCommand.RaiseCanExecuteChanged(); ApplyGeometryCommand.RaiseCanExecuteChanged(); CopyActualToTargetCommand.RaiseCanExecuteChanged(); } #endregion } }