420 lines
20 KiB
C#
420 lines
20 KiB
C#
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
|
||
}
|
||
}
|