SSPC-Tester/SSPCTester.UI/Services/SafetyMonitorService.cs
2026-07-12 12:07:41 +08:00

427 lines
17 KiB
C#
Raw 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.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<SafetyMonitorService> _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<DeviceOptions> options,
UiLogSink log,
ILogger<SafetyMonitorService> 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<SafetyResetResult> ResetAsync(CancellationToken ct = default)
{
await _gate.WaitAsync(ct).ConfigureAwait(false);
try
{
return await ResetCoreAsync(ct).ConfigureAwait(false);
}
finally
{
_gate.Release();
}
}
private async Task<SafetyResetResult> 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<bool> 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);