Files

420 lines
20 KiB
C#
Raw Permalink 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.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
{
/// <summary>
/// 运动控制操作面板 ViewModel | Motion Control Operation Panel ViewModel
/// 提供轴状态、几何参数、安全门状态的可绑定属性和操作命令(不含 Jog 调试)
/// Provides axis status, geometry, safety door properties and commands (excluding Jog debug)
/// </summary>
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<GeometryUpdatedEvent>().Subscribe(OnGeometryUpdated, ThreadOption.UIThread);
_eventAggregator.GetEvent<AxisStatusChangedEvent>().Subscribe(OnAxisStatusChanged, ThreadOption.UIThread);
_eventAggregator.GetEvent<DoorStatusChangedEvent>().Subscribe(OnDoorStatusChanged, ThreadOption.UIThread);
_eventAggregator.GetEvent<DoorInterlockChangedEvent>().Subscribe(OnDoorInterlockChanged, ThreadOption.UIThread);
// 订阅几何反算填入请求事件 | Subscribe to geometry apply request event
_eventAggregator.GetEvent<GeometryApplyRequestEvent>().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));
}
}
/// <summary>联锁未激活(用于开门按钮 IsEnabled| Not interlocked (for open door button IsEnabled)</summary>
public bool IsNotInterlocked => !_isInterlocked;
/// <summary>联锁状态文本(用于 UI 显示)| Interlock status text for UI display</summary>
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); }
/// <summary>PLC 是否已连接(从 PlcService 读取)| Whether PLC is connected (from PlcService)</summary>
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; }
/// <summary>一键将实际值填入目标值 | Copy actual values to target values</summary>
public DelegateCommand CopyActualToTargetCommand { get; }
/// <summary>打开调试窗口命令 | Open debug window command</summary>
public DelegateCommand OpenDebugWindowCommand { get; }
#endregion
#region | Command Execution
/// <summary>多轴联动移动(含二次确认)| Multi-axis coordinated move (with confirmation)</summary>
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, double>
{
{ 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);
}
/// <summary>所有轴回零(含二次确认)| Home all axes (with confirmation)</summary>
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();
}
/// <summary>
/// 几何反算:计算 SourceZ/DetectorZ 目标值并填入,其他轴用实际值填入
/// Geometry inverse: calculate SourceZ/DetectorZ targets and fill in, other axes use actual values
/// </summary>
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;
}
/// <summary>
/// 一键将所有轴实际值填入目标值 | Copy all actual values to target values
/// </summary>
private void ExecuteCopyActualToTarget()
{
SourceZTarget = SourceZActual;
DetectorZTarget = DetectorZActual;
StageXTarget = StageXActual;
StageYTarget = StageYActual;
DetSwingTarget = DetSwingActual;
StageRotTarget = StageRotActual;
FixtureRotTarget = FixtureRotActual;
}
/// <summary>打开调试窗口 | Open debug window</summary>
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;
}
/// <summary>
/// 收到几何反算填入请求,将计算结果填入目标值 | Receive geometry apply request, fill target values
/// </summary>
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
/// <summary>
/// 安全执行:捕获 PLC 异常避免 UI 崩溃 | Safe execution: catches PLC exceptions to prevent UI crash
/// </summary>
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 */ }
}
}
/// <summary>
/// 刷新所有命令的 CanExecute 状态 | Refresh CanExecute state of all commands
/// </summary>
private void RaiseCommandCanExecuteChanged()
{
MoveCommand.RaiseCanExecuteChanged();
StopCommand.RaiseCanExecuteChanged();
HomeCommand.RaiseCanExecuteChanged();
OpenDoorCommand.RaiseCanExecuteChanged();
CloseDoorCommand.RaiseCanExecuteChanged();
ApplyGeometryCommand.RaiseCanExecuteChanged();
CopyActualToTargetCommand.RaiseCanExecuteChanged();
}
#endregion
}
}