SSPC-Tester/SSPCTester.UI/Services/SafetyMonitorService.cs
2026-07-06 17:26:09 +08:00

299 lines
11 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 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 Timer? _timer;
private bool _handlingEmergency;
private bool _softwareEmergencyActive;
private bool _disposed;
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;
}
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;
if (!options.Enabled)
{
SetStatus(false, false, "设备故障", "PCI2312 安全监控未启用");
return;
}
int interval = Math.Clamp(options.PollIntervalMs, 50, 10_000);
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(interval));
SetStatus(false, false, "设备正常", "正在监控 PCI2312 急停输入");
}
public async Task TriggerSoftwareEmergencyAsync(CancellationToken ct = default)
{
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 通道已全部关断,电源输出和负载输入已发送关闭指令");
}
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);
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)
{
var options = _options.Pci2312;
await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false);
if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false))
{
const string message = "硬件急停按钮仍处于触发状态,无法执行状态重置。请先旋转或释放设备上的急停按钮,确认急停输入复位后再重试。";
SetStatus(true, IsAlarmOutputOn, "急停已触发", message);
return new SafetyResetResult(false, message);
}
await SetAlarmOutputAsync(false, ct).ConfigureAwait(false);
_softwareEmergencyActive = false;
_handlingEmergency = false;
SetStatus(false, false, "设备正常", "急停状态已重置,报警灯已关闭,正在监控 PCI2312 输入");
return new SafetyResetResult(true, "急停状态已重置。");
}
private void OnTimer(object? state)
{
if (_disposed) return;
_ = PollAsync();
}
private async Task PollAsync()
{
if (!await _gate.WaitAsync(0).ConfigureAwait(false)) return;
try
{
var options = _options.Pci2312;
if (!options.Enabled) return;
if (options.AutoConnect && !_safetyDio.IsConnected)
await _safetyDio.ConnectAsync().ConfigureAwait(false);
bool active = await IsEmergencyInputActiveAsync(options, CancellationToken.None).ConfigureAwait(false);
if (active)
{
await HandleEmergencyAsync().ConfigureAwait(false);
}
else if (_softwareEmergencyActive)
{
SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "等待状态重置,电源和负载不会自动恢复");
}
else if (IsEmergencyActive || IsAlarmOutputOn)
{
SetStatus(true, IsAlarmOutputOn, "急停已触发", $"DI{options.EmergencyInputChannel} 已复位,请点击状态重置关闭报警灯");
}
else
{
SetStatus(false, false, "设备正常", $"DI{options.EmergencyInputChannel} 未触发");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Safety monitor poll failed.");
SetStatus(IsEmergencyActive, IsAlarmOutputOn, "设备故障", ex.Message);
}
finally
{
_gate.Release();
}
}
private async Task HandleEmergencyAsync()
{
if (_handlingEmergency) return;
_handlingEmergency = true;
SetStatus(true, IsAlarmOutputOn, "急停已触发", "正在执行安全关断");
_log.Add(UiLogLevel.Error, "检测到急停输入接通,开始执行报警灯、电源、负载关断。");
try
{
await SetAlarmOutputAsync(true, CancellationToken.None).ConfigureAwait(false);
await TryShutdownSspcAsync().ConfigureAwait(false);
await TryShutdownPowerAsync().ConfigureAwait(false);
await TryShutdownLoadAsync().ConfigureAwait(false);
SetStatus(true, true, "急停已触发", "报警灯已接通SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
}
catch (Exception ex)
{
_logger.LogError(ex, "Emergency shutdown failed.");
_log.Add(UiLogLevel.Error, $"急停关断执行异常:{ex.Message}");
SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message);
_handlingEmergency = false;
}
}
private async Task TryShutdownSspcAsync()
{
try
{
if (!_sspc.IsConnected && _options.Sspc.AutoConnect)
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}");
}
}
private async Task TryShutdownPowerAsync()
{
try
{
if (!_power.IsConnected && _options.PowerSupply.AutoConnect)
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}");
}
}
private async Task TryShutdownLoadAsync()
{
try
{
if (!_load.IsConnected && _options.Load.AutoConnect)
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}");
}
}
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)
await _safetyDio.ConnectAsync(ct).ConfigureAwait(false);
}
private void SetStatus(bool emergencyActive, bool alarmOutputOn, string statusText, string detailText)
{
IsEmergencyActive = emergencyActive;
IsAlarmOutputOn = alarmOutputOn;
StatusText = statusText;
DetailText = detailText;
StatusChanged?.Invoke(this, EventArgs.Empty);
}
public void Dispose()
{
_disposed = true;
_timer?.Dispose();
_gate.Dispose();
}
}
public sealed record SafetyResetResult(bool Success, string Message);