using MainShell.Hardware; using MainShell.Parameter; using MainShell.Common; using MwFramework.Device; using System; using System.Collections.Generic; using System.Linq; namespace MainShell.Motion.Safety { public sealed class MotionSafetyContext { public MotionSafetyContext(MotionRequestKind requestKind, IReadOnlyCollection axes, IReadOnlyCollection requests, string source, int? timeoutMilliseconds) { RequestKind = requestKind; Axes = axes ?? Array.Empty(); Requests = requests ?? Array.Empty(); Source = source; TimeoutMilliseconds = timeoutMilliseconds; } public MotionRequestKind RequestKind { get; } public IReadOnlyCollection Axes { get; } public IReadOnlyCollection Requests { get; } public string Source { get; } public int? TimeoutMilliseconds { get; } public bool IsBatch => Requests.Count > 1; } public sealed class MotionSafetyCheckResult { public MotionSafetyCheckResult(bool isPassed, string ruleName, string message, string severity = "Block", int? alarmId = null, string recoveryHint = null, IEnumerable affectedAxes = null, MessageKey messageKey = MessageKey.None, object[] messageArgs = null) { IsPassed = isPassed; RuleName = ruleName; Message = message; Severity = severity; AlarmId = alarmId; RecoveryHint = recoveryHint; AffectedAxes = (affectedAxes ?? Enumerable.Empty()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); MessageKey = messageKey; MessageArgs = messageArgs ?? Array.Empty(); } public bool IsPassed { get; } public string RuleName { get; } public string Message { get; } public string Severity { get; } public int? AlarmId { get; } public string RecoveryHint { get; } public IReadOnlyCollection AffectedAxes { get; } public MessageKey MessageKey { get; } public object[] MessageArgs { get; } public static MotionSafetyCheckResult Passed(string ruleName) { return new MotionSafetyCheckResult(true, ruleName, null); } public static MotionSafetyCheckResult Blocked(string ruleName, string message, int? alarmId = null, string recoveryHint = null, IEnumerable affectedAxes = null, MessageKey messageKey = MessageKey.None, params object[] messageArgs) { return new MotionSafetyCheckResult(false, ruleName, message, "Block", alarmId, recoveryHint, affectedAxes, messageKey, messageArgs); } } public interface IMotionSafetyCheck { MotionSafetyCheckResult Check(MotionSafetyContext context); } public sealed class StagePlatformSafetyOptions { public StagePlatformSafetyOptions(double maxPlaneSpread, double maxTravelPerStep) { MaxPlaneSpread = maxPlaneSpread; MaxTravelPerStep = maxTravelPerStep; } public double MaxPlaneSpread { get; } public double MaxTravelPerStep { get; } } internal sealed class MotionRequestDuplicateAxisCheck : IMotionSafetyCheck { public MotionSafetyCheckResult Check(MotionSafetyContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var duplicateAxisNames = context.Requests .Select(x => x.AxisName) .Where(x => !string.IsNullOrWhiteSpace(x)) .GroupBy(x => x, StringComparer.OrdinalIgnoreCase) .Where(x => x.Count() > 1) .Select(x => x.Key) .ToArray(); if (duplicateAxisNames.Length == 0) { return MotionSafetyCheckResult.Passed(nameof(MotionRequestDuplicateAxisCheck)); } return MotionSafetyCheckResult.Blocked( nameof(MotionRequestDuplicateAxisCheck), string.Format("Duplicate axis requests detected: {0}.", string.Join(", ", duplicateAxisNames)), recoveryHint: "请检查批量运动请求,确保同一批次中每根轴只出现一次。", affectedAxes: duplicateAxisNames, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { string.Format("重复轴请求: {0}", string.Join(", ", duplicateAxisNames)) }); } } internal sealed class MotionBatchSameSourceCheck : IMotionSafetyCheck { public MotionSafetyCheckResult Check(MotionSafetyContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var nonEmptySources = context.Requests .Select(x => x.Source) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (nonEmptySources.Length <= 1) { return MotionSafetyCheckResult.Passed(nameof(MotionBatchSameSourceCheck)); } return MotionSafetyCheckResult.Blocked( nameof(MotionBatchSameSourceCheck), string.Format("Batch requests must share the same source. Sources: {0}.", string.Join(", ", nonEmptySources)), recoveryHint: "请确保一个批量运动由同一个机构动作统一发起。", messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { string.Format("批量动作来源不一致: {0}", string.Join(", ", nonEmptySources)) }); } } internal sealed class EmergencyStopReleasedCheck : IMotionSafetyCheck { private readonly MotionSafetyStateProvider _stateProvider; public EmergencyStopReleasedCheck(MotionSafetyStateProvider stateProvider) { _stateProvider = stateProvider ?? throw new ArgumentNullException(nameof(stateProvider)); } public MotionSafetyCheckResult Check(MotionSafetyContext context) { if (_stateProvider.IsEmergencyStopReleased()) { return MotionSafetyCheckResult.Passed(nameof(EmergencyStopReleasedCheck)); } return MotionSafetyCheckResult.Blocked( nameof(EmergencyStopReleasedCheck), "Emergency stop is active or safety circuit is not ready.", recoveryHint: "请释放急停并检查安全回路状态后再执行运动。", affectedAxes: context != null ? context.Axes.Select(x => x != null ? x.Name : null) : null, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { "急停未释放或安全回路未就绪" }); } } internal sealed class SafetyDoorClosedCheck : IMotionSafetyCheck { private readonly MotionSafetyStateProvider _stateProvider; public SafetyDoorClosedCheck(MotionSafetyStateProvider stateProvider) { _stateProvider = stateProvider ?? throw new ArgumentNullException(nameof(stateProvider)); } public MotionSafetyCheckResult Check(MotionSafetyContext context) { if (_stateProvider.CurrentMachineMode != MachineMode.Auto) { return MotionSafetyCheckResult.Passed(nameof(SafetyDoorClosedCheck)); } if (_stateProvider.AreSafetyDoorsClosed()) { return MotionSafetyCheckResult.Passed(nameof(SafetyDoorClosedCheck)); } return MotionSafetyCheckResult.Blocked( nameof(SafetyDoorClosedCheck), "Safety door is open and shield is not active.", recoveryHint: "请关闭安全门,或确认门屏蔽状态是否符合当前操作模式。", affectedAxes: context != null ? context.Axes.Select(x => x != null ? x.Name : null) : null, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { "安全门未关闭" }); } } internal sealed class AutoFlowModeCheck : IMotionSafetyCheck { private readonly MotionSafetyStateProvider _stateProvider; public AutoFlowModeCheck(MotionSafetyStateProvider stateProvider) { _stateProvider = stateProvider ?? throw new ArgumentNullException(nameof(stateProvider)); } public MotionSafetyCheckResult Check(MotionSafetyContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (!_stateProvider.IsAutoWorkflowSource(context.Source)) { return MotionSafetyCheckResult.Passed(nameof(AutoFlowModeCheck)); } if (_stateProvider.CurrentMachineMode == MainShell.Common.MachineMode.Auto) { return MotionSafetyCheckResult.Passed(nameof(AutoFlowModeCheck)); } return MotionSafetyCheckResult.Blocked( nameof(AutoFlowModeCheck), string.Format("Motion source '{0}' requires Auto mode. Current mode: {1}.", context.Source, _stateProvider.CurrentMachineMode), recoveryHint: "请切换到自动模式,或改由手动安全入口执行该动作。", affectedAxes: context.Axes.Select(x => x != null ? x.Name : null), messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { string.Format("动作来源 {0} 需要自动模式", context.Source) }); } } internal sealed class StageVacuumReadyCheck : IMotionSafetyCheck { private readonly MotionSafetyStateProvider _stateProvider; private const string StagePlaneTag = "StagePlaneMove"; public StageVacuumReadyCheck(MotionSafetyStateProvider stateProvider) { _stateProvider = stateProvider ?? throw new ArgumentNullException(nameof(stateProvider)); } public MotionSafetyCheckResult Check(MotionSafetyContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (!context.Requests.Any(ContainsStagePlaneTag)) { return MotionSafetyCheckResult.Passed(nameof(StageVacuumReadyCheck)); } if (_stateProvider.AreStageVacuumsReady()) { return MotionSafetyCheckResult.Passed(nameof(StageVacuumReadyCheck)); } return MotionSafetyCheckResult.Blocked( nameof(StageVacuumReadyCheck), "Stage vacuum signals are not ready for plane movement.", recoveryHint: "请检查平台真空吸附状态,确认基板已吸稳后再移动平台。", affectedAxes: new[] { AxisName.Axis_Stage_Z7, AxisName.Axis_Stage_Z8, AxisName.Axis_Stage_Z9 }, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { "平台真空未就绪" }); } private static bool ContainsStagePlaneTag(MotionMoveRequest request) { return request != null && request.Tags != null && request.Tags.Contains(StagePlaneTag, StringComparer.OrdinalIgnoreCase); } } internal sealed class BondHeadSafePositionCheck : IMotionSafetyCheck { private readonly MotionSafetyStateProvider _stateProvider; private const string StagePlaneTag = "StagePlaneMove"; public BondHeadSafePositionCheck(MotionSafetyStateProvider stateProvider) { _stateProvider = stateProvider ?? throw new ArgumentNullException(nameof(stateProvider)); } public MotionSafetyCheckResult Check(MotionSafetyContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (!context.Requests.Any(ContainsStagePlaneTag)) { return MotionSafetyCheckResult.Passed(nameof(BondHeadSafePositionCheck)); } var unsafeSignals = _stateProvider.GetUnsafeBondHeadSignals(); if (unsafeSignals == null || unsafeSignals.Count == 0) { return MotionSafetyCheckResult.Passed(nameof(BondHeadSafePositionCheck)); } return MotionSafetyCheckResult.Blocked( nameof(BondHeadSafePositionCheck), string.Format("Bond head safe position signals are not ready: {0}.", string.Join(", ", unsafeSignals)), recoveryHint: "请确认 BondHead 已回到安全位,再执行平台联动。", affectedAxes: new[] { AxisName.Axis_Stage_Z7, AxisName.Axis_Stage_Z8, AxisName.Axis_Stage_Z9 }, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { string.Format("BondHead 安全位信号未就绪: {0}", string.Join(", ", unsafeSignals)) }); } private static bool ContainsStagePlaneTag(MotionMoveRequest request) { return request != null && request.Tags != null && request.Tags.Contains(StagePlaneTag, StringComparer.OrdinalIgnoreCase); } } internal sealed class StagePlatformPlaneMoveSafetyCheck : IMotionSafetyCheck { private const string StagePlaneTag = "StagePlaneMove"; private readonly HardwareManager _hardware; private readonly StagePlatformSafetyOptions _options; public StagePlatformPlaneMoveSafetyCheck(HardwareManager hardware, StagePlatformSafetyOptions options) { _hardware = hardware ?? throw new ArgumentNullException(nameof(hardware)); _options = options ?? throw new ArgumentNullException(nameof(options)); } public MotionSafetyCheckResult Check(MotionSafetyContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var requests = context.Requests.Where(ContainsStagePlaneTag).ToArray(); if (requests.Length == 0) { return MotionSafetyCheckResult.Passed(nameof(StagePlatformPlaneMoveSafetyCheck)); } if (requests.Length != 3) { return MotionSafetyCheckResult.Blocked( nameof(StagePlatformPlaneMoveSafetyCheck), "Stage plane move requires exactly 3 axis requests.", recoveryHint: "请确认平台联动请求完整包含 Z7、Z8、Z9 三根轴。", affectedAxes: new[] { AxisName.Axis_Stage_Z7, AxisName.Axis_Stage_Z8, AxisName.Axis_Stage_Z9 }, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { "平台联动请求数量不正确" }); } var expectedAxes = new[] { AxisName.Axis_Stage_Z7, AxisName.Axis_Stage_Z8, AxisName.Axis_Stage_Z9 }; var requestMap = requests.ToDictionary(x => x.AxisName, x => x, StringComparer.OrdinalIgnoreCase); if (expectedAxes.Any(x => !requestMap.ContainsKey(x))) { return MotionSafetyCheckResult.Blocked( nameof(StagePlatformPlaneMoveSafetyCheck), "Stage plane move must include Stage-Z7, Stage-Z8 and Stage-Z9.", recoveryHint: "请检查平台三轴联动动作构建逻辑。", affectedAxes: expectedAxes, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { "平台联动缺少必要轴" }); } var currentPositions = new Dictionary(StringComparer.OrdinalIgnoreCase) { { AxisName.Axis_Stage_Z7, GetAxisPosition(_hardware.Axis_Stage_Z7, nameof(_hardware.Axis_Stage_Z7)) }, { AxisName.Axis_Stage_Z8, GetAxisPosition(_hardware.Axis_Stage_Z8, nameof(_hardware.Axis_Stage_Z8)) }, { AxisName.Axis_Stage_Z9, GetAxisPosition(_hardware.Axis_Stage_Z9, nameof(_hardware.Axis_Stage_Z9)) } }; var targetPositions = expectedAxes.Select(x => requestMap[x].TargetPosition).ToArray(); var currentSpread = currentPositions.Values.Max() - currentPositions.Values.Min(); var targetSpread = targetPositions.Max() - targetPositions.Min(); var maxTravelDelta = expectedAxes.Max(x => Math.Abs(requestMap[x].TargetPosition - currentPositions[x])); if (targetSpread > _options.MaxPlaneSpread) { return MotionSafetyCheckResult.Blocked( nameof(StagePlatformPlaneMoveSafetyCheck), string.Format("Stage plane target spread {0:F3} exceeds limit {1:F3}.", targetSpread, _options.MaxPlaneSpread), recoveryHint: "请缩小三轴目标高度差,或先执行中间整平动作。", affectedAxes: expectedAxes, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { string.Format("平台目标姿态差超限: {0:F3}", targetSpread) }); } if (currentSpread > _options.MaxPlaneSpread) { return MotionSafetyCheckResult.Blocked( nameof(StagePlatformPlaneMoveSafetyCheck), string.Format("Stage plane current spread {0:F3} exceeds limit {1:F3}.", currentSpread, _options.MaxPlaneSpread), recoveryHint: "请先执行平台整平或回安全平面动作。", affectedAxes: expectedAxes, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { string.Format("平台当前姿态差超限: {0:F3}", currentSpread) }); } if (maxTravelDelta > _options.MaxTravelPerStep) { return MotionSafetyCheckResult.Blocked( nameof(StagePlatformPlaneMoveSafetyCheck), string.Format("Stage plane travel delta {0:F3} exceeds single-step limit {1:F3}. Consider a safe intermediate move.", maxTravelDelta, _options.MaxTravelPerStep), recoveryHint: "请改为分步走位,先到安全中转位再到目标位。", affectedAxes: expectedAxes, messageKey: MessageKey.ProcessFailedWithReason, messageArgs: new object[] { string.Format("平台单步移动量超限: {0:F3}", maxTravelDelta) }); } return MotionSafetyCheckResult.Passed(nameof(StagePlatformPlaneMoveSafetyCheck)); } private static bool ContainsStagePlaneTag(MotionMoveRequest request) { return request != null && request.Tags != null && request.Tags.Contains(StagePlaneTag, StringComparer.OrdinalIgnoreCase); } private static double GetAxisPosition(IAxis axis, string axisPropertyName) { if (axis == null) { throw new InvalidOperationException(string.Format("Axis '{0}' is not available.", axisPropertyName)); } return axis.State != null ? axis.State.ActualPos : axis.GetPositionImmediate(); } } }