using System.Diagnostics; using System.IO; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SSPCTester.Devices.Config; using SSPCTester.Devices.Interfaces; namespace SSPCTester.UI.Services; public sealed class SafetyMonitorService : IDisposable { private readonly ISafetyDio _safetyDio; private readonly IPowerSupply _power; private readonly ILoad _load; private readonly ISspc _sspc; private readonly DeviceOptions _options; private readonly UiLogSink _log; private readonly ILogger _logger; private readonly SemaphoreSlim _gate = new(1, 1); private readonly string _traceLogPath; private readonly object _traceLock = new(); private Timer? _timer; private bool _handlingEmergency; private bool _softwareEmergencyActive; private bool _hardwareEmergencyLatched; private bool _disposed; private long _pollSequence; private long _pollGateBusyCount; private string? _lastStatusSnapshot; private string? _lastPollSnapshot; public SafetyMonitorService( ISafetyDio safetyDio, IPowerSupply power, ILoad load, ISspc sspc, IOptions options, UiLogSink log, ILogger logger) { _safetyDio = safetyDio; _power = power; _load = load; _sspc = sspc; _options = options.Value; _log = log; _logger = logger; string logDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SSPCTester", "Logs"); Directory.CreateDirectory(logDir); _traceLogPath = Path.Combine(logDir, $"safety-trace-{DateTime.Now:yyyyMMdd-HHmmss}.log"); Trace($"Safety trace started. UseMock={_options.Pci2312.UseMock}, AutoConnect={_options.Pci2312.AutoConnect}, Enabled={_options.Pci2312.Enabled}, PollIntervalMs={_options.Pci2312.PollIntervalMs}"); } public bool IsEmergencyActive { get; private set; } public bool IsAlarmOutputOn { get; private set; } public string StatusText { get; private set; } = "设备故障"; public string DetailText { get; private set; } = "设备安全监控未启动"; public event EventHandler? StatusChanged; public void Start() { if (_timer is not null) return; var options = _options.Pci2312; Trace($"Start() called. Enabled={options.Enabled}, AutoConnect={options.AutoConnect}, DeviceId={options.DeviceId}, DI={options.EmergencyInputChannel}, DO={options.AlarmOutputChannel}, PollIntervalMs={options.PollIntervalMs}"); if (!options.Enabled) { SetStatus(false, false, "设备故障", "PCI2312 安全监控未启用"); return; } int interval = Math.Clamp(options.PollIntervalMs, 50, 10000); _timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(interval)); SetStatus(false, false, "设备正常", "正在监控 PCI2312 急停输入"); _log.Add(UiLogLevel.Info, $"安全观测日志已启用:{_traceLogPath}"); } public async Task TriggerSoftwareEmergencyAsync(CancellationToken ct = default) { var sw = Stopwatch.StartNew(); Trace("TriggerSoftwareEmergencyAsync enter."); await _gate.WaitAsync(ct).ConfigureAwait(false); try { _softwareEmergencyActive = true; _handlingEmergency = true; SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "正在执行软件急停安全关断"); _log.Add(UiLogLevel.Error, "软件触发报警急停,开始执行报警灯、电源、负载关断。"); await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false); await SetAlarmOutputAsync(true, ct).ConfigureAwait(false); await TryShutdownSspcAsync().ConfigureAwait(false); await TryShutdownPowerAsync().ConfigureAwait(false); await TryShutdownLoadAsync().ConfigureAwait(false); SetStatus(true, true, "软件急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令"); Trace($"TriggerSoftwareEmergencyAsync success in {sw.ElapsedMilliseconds}ms."); } catch (Exception ex) { _softwareEmergencyActive = false; _handlingEmergency = false; _logger.LogError(ex, "Software emergency shutdown failed."); _log.Add(UiLogLevel.Error, $"软件急停执行异常:{ex.Message}"); SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message); Trace($"TriggerSoftwareEmergencyAsync failed in {sw.ElapsedMilliseconds}ms. {ex.GetType().Name}: {ex.Message}"); throw; } finally { _gate.Release(); } } public async Task ResetAsync(CancellationToken ct = default) { await _gate.WaitAsync(ct).ConfigureAwait(false); try { return await ResetCoreAsync(ct).ConfigureAwait(false); } finally { _gate.Release(); } } private async Task ResetCoreAsync(CancellationToken ct) { Trace("ResetCoreAsync enter."); var options = _options.Pci2312; await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false); if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false)) { const string message = "硬件急停按钮仍处于触发状态,无法执行状态重置。请先旋转或释放设备上的急停按钮,确认急停输入复位后再重试。"; SetStatus(true, IsAlarmOutputOn, "急停已触发", message); Trace("ResetCoreAsync blocked: emergency input still active."); return new SafetyResetResult(false, message); } await SetAlarmOutputAsync(false, ct).ConfigureAwait(false); _softwareEmergencyActive = false; _hardwareEmergencyLatched = false; _handlingEmergency = false; SetStatus(false, false, "设备正常", "急停状态已重置,报警灯已关闭,正在监控 PCI2312 输入"); Trace("ResetCoreAsync success."); return new SafetyResetResult(true, "急停状态已重置。"); } private void OnTimer(object? state) { if (_disposed) return; _ = PollAsync(); } private async Task PollAsync() { if (!await _gate.WaitAsync(0).ConfigureAwait(false)) { long skipped = Interlocked.Increment(ref _pollGateBusyCount); if (skipped % 20 == 0) Trace($"PollAsync skipped by gate contention. skipped={skipped}"); return; } try { long seq = Interlocked.Increment(ref _pollSequence); var options = _options.Pci2312; if (!options.Enabled) return; if (options.AutoConnect && !_safetyDio.IsConnected) { Trace($"Poll#{seq}: attempting auto connect for safety DIO."); await _safetyDio.ConnectAsync().ConfigureAwait(false); } bool active = await IsEmergencyInputActiveAsync(options, CancellationToken.None).ConfigureAwait(false); string snapshot = $"poll={seq},active={active},handling={_handlingEmergency},soft={_softwareEmergencyActive},latched={_hardwareEmergencyLatched},emergency={IsEmergencyActive},alarm={IsAlarmOutputOn},dioConnected={_safetyDio.IsConnected}"; if (!string.Equals(_lastPollSnapshot, snapshot, StringComparison.Ordinal)) { _lastPollSnapshot = snapshot; Trace($"Poll snapshot: {snapshot}"); } if (active) { _hardwareEmergencyLatched = true; Trace($"Poll#{seq}: branch=ACTIVE -> HandleEmergencyAsync"); await HandleEmergencyAsync().ConfigureAwait(false); } else if (_hardwareEmergencyLatched) { Trace($"Poll#{seq}: branch=HardwareLatchedClear -> ClearHardwareEmergencyAsync"); await ClearHardwareEmergencyAsync(options).ConfigureAwait(false); } else if (_softwareEmergencyActive) { Trace($"Poll#{seq}: branch=SoftwareActiveWaitReset"); SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "等待状态重置,电源和负载不会自动恢复"); } else if (IsEmergencyActive || IsAlarmOutputOn) { Trace($"Poll#{seq}: branch=CleanupByStatus -> ClearHardwareEmergencyAsync"); await ClearHardwareEmergencyAsync(options).ConfigureAwait(false); } else { SetStatus(false, false, "设备正常", $"DI{options.EmergencyInputChannel} 未触发"); } } catch (Exception ex) { _logger.LogWarning(ex, "Safety monitor poll failed."); SetStatus(IsEmergencyActive, IsAlarmOutputOn, "设备故障", ex.Message); Trace($"PollAsync failed. {ex.GetType().Name}: {ex.Message}"); } finally { _gate.Release(); } } private async Task HandleEmergencyAsync() { if (_handlingEmergency) { Trace("HandleEmergencyAsync skipped because _handlingEmergency=true."); return; } var sw = Stopwatch.StartNew(); _handlingEmergency = true; SetStatus(true, IsAlarmOutputOn, "急停已触发", "正在执行安全关断"); _log.Add(UiLogLevel.Error, "检测到急停输入接通,开始执行报警灯、电源、负载关断。"); Trace("HandleEmergencyAsync begin."); try { var step = Stopwatch.StartNew(); await SetAlarmOutputAsync(true, CancellationToken.None).ConfigureAwait(false); Trace($"HandleEmergencyAsync step SetAlarmOutputAsync OK in {step.ElapsedMilliseconds}ms."); step.Restart(); await TryShutdownSspcAsync().ConfigureAwait(false); Trace($"HandleEmergencyAsync step TryShutdownSspcAsync done in {step.ElapsedMilliseconds}ms."); step.Restart(); await TryShutdownPowerAsync().ConfigureAwait(false); Trace($"HandleEmergencyAsync step TryShutdownPowerAsync done in {step.ElapsedMilliseconds}ms."); step.Restart(); await TryShutdownLoadAsync().ConfigureAwait(false); Trace($"HandleEmergencyAsync step TryShutdownLoadAsync done in {step.ElapsedMilliseconds}ms."); SetStatus(true, true, "急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令"); Trace($"HandleEmergencyAsync success in {sw.ElapsedMilliseconds}ms."); } catch (Exception ex) { _logger.LogError(ex, "Emergency shutdown failed."); _log.Add(UiLogLevel.Error, $"急停关断执行异常:{ex.Message}"); SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message); _handlingEmergency = false; Trace($"HandleEmergencyAsync failed in {sw.ElapsedMilliseconds}ms. {ex.GetType().Name}: {ex.Message}"); } } private async Task ClearHardwareEmergencyAsync(Pci2312Options options) { var sw = Stopwatch.StartNew(); Trace("ClearHardwareEmergencyAsync begin."); if (IsAlarmOutputOn) await SetAlarmOutputAsync(false, CancellationToken.None).ConfigureAwait(false); _handlingEmergency = false; _softwareEmergencyActive = false; _hardwareEmergencyLatched = false; SetStatus(false, false, "设备正常", $"DI{options.EmergencyInputChannel} 已复位,硬件急停已解除,报警灯已关闭,设备输出不会自动恢复"); _log.Add(UiLogLevel.Info, "硬件急停输入已复位,软件急停状态和报警灯已自动解除。"); Trace($"ClearHardwareEmergencyAsync success in {sw.ElapsedMilliseconds}ms."); } private async Task TryShutdownSspcAsync() { try { if (!_sspc.IsConnected && _options.Sspc.AutoConnect) { Trace("TryShutdownSspcAsync: auto-connecting SSPC."); await _sspc.ConnectAsync().ConfigureAwait(false); } if (_sspc.IsConnected) { for (int ch = 1; ch <= _sspc.ChannelCount; ch++) { try { await _sspc.TurnOffAsync(ch).ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "急停关断 SSPC CH{Ch} 失败。", ch); } } _log.Add(UiLogLevel.Warning, $"SSPC 全部 {_sspc.ChannelCount} 个通道已发送关断指令。"); } else { _log.Add(UiLogLevel.Warning, "SSPC 未连接,跳过通道关断。"); } } catch (Exception ex) { _logger.LogWarning(ex, "SSPC 批量关断失败。"); _log.Add(UiLogLevel.Error, $"SSPC 通道批量关断失败:{ex.Message}"); Trace($"TryShutdownSspcAsync failed. {ex.GetType().Name}: {ex.Message}"); } } private async Task TryShutdownPowerAsync() { try { if (!_power.IsConnected && _options.PowerSupply.AutoConnect) { Trace("TryShutdownPowerAsync: auto-connecting power supply."); await _power.ConnectAsync().ConfigureAwait(false); } if (_power.IsConnected) await _power.OutputAsync(false).ConfigureAwait(false); _log.Add(UiLogLevel.Warning, "UDP5080 电源输出已发送关闭指令。"); } catch (Exception ex) { _logger.LogWarning(ex, "Power shutdown failed."); _log.Add(UiLogLevel.Error, $"UDP5080 电源关闭失败:{ex.Message}"); Trace($"TryShutdownPowerAsync failed. {ex.GetType().Name}: {ex.Message}"); } } private async Task TryShutdownLoadAsync() { try { if (!_load.IsConnected && _options.Load.AutoConnect) { Trace("TryShutdownLoadAsync: auto-connecting load."); await _load.ConnectAsync().ConfigureAwait(false); } if (_load.IsConnected) await _load.InputAsync(false).ConfigureAwait(false); _log.Add(UiLogLevel.Warning, "IT8702P 负载输入已发送关闭指令。"); } catch (Exception ex) { _logger.LogWarning(ex, "Load shutdown failed."); _log.Add(UiLogLevel.Error, $"IT8702P 负载关闭失败:{ex.Message}"); Trace($"TryShutdownLoadAsync failed. {ex.GetType().Name}: {ex.Message}"); } } private async Task IsEmergencyInputActiveAsync(Pci2312Options options, CancellationToken ct) { bool value = await _safetyDio.ReadInputAsync(options.EmergencyInputChannel, ct).ConfigureAwait(false); return options.EmergencyInputActiveHigh ? value : !value; } private async Task SetAlarmOutputAsync(bool on, CancellationToken ct) { var options = _options.Pci2312; bool outputValue = options.AlarmOutputActiveHigh ? on : !on; await _safetyDio.WriteOutputAsync(options.AlarmOutputChannel, outputValue, ct).ConfigureAwait(false); IsAlarmOutputOn = on; } private async Task EnsureSafetyDioConnectedAsync(CancellationToken ct) { var options = _options.Pci2312; if (!options.Enabled) throw new InvalidOperationException("PCI2312 安全输入输出未启用,无法控制报警灯。"); if (!_safetyDio.IsConnected) { Trace("EnsureSafetyDioConnectedAsync: connecting safety DIO."); await _safetyDio.ConnectAsync(ct).ConfigureAwait(false); } } private void SetStatus(bool emergencyActive, bool alarmOutputOn, string statusText, string detailText) { string snapshot = $"emergency={emergencyActive},alarm={alarmOutputOn},status={statusText},detail={detailText}"; if (!string.Equals(_lastStatusSnapshot, snapshot, StringComparison.Ordinal)) { _lastStatusSnapshot = snapshot; Trace($"SetStatus changed: {snapshot}"); } IsEmergencyActive = emergencyActive; IsAlarmOutputOn = alarmOutputOn; StatusText = statusText; DetailText = detailText; StatusChanged?.Invoke(this, EventArgs.Empty); } public void Dispose() { Trace("Dispose() called. Safety monitor disposed."); _disposed = true; _timer?.Dispose(); _gate.Dispose(); } private void Trace(string message) { string line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [T{Environment.CurrentManagedThreadId}] {message}{Environment.NewLine}"; try { lock (_traceLock) { File.AppendAllText(_traceLogPath, line); } } catch { // Temporary diagnostic trace should never break runtime behavior. } } } public sealed record SafetyResetResult(bool Success, string Message);