2026-07-12 12:07:41 +08:00
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using System.IO;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
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;
|
2026-07-06 17:26:09 +08:00
|
|
|
|
private readonly ISspc _sspc;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private readonly DeviceOptions _options;
|
|
|
|
|
|
private readonly UiLogSink _log;
|
|
|
|
|
|
private readonly ILogger<SafetyMonitorService> _logger;
|
|
|
|
|
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
private readonly string _traceLogPath;
|
|
|
|
|
|
private readonly object _traceLock = new();
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private Timer? _timer;
|
|
|
|
|
|
private bool _handlingEmergency;
|
2026-07-03 14:16:15 +08:00
|
|
|
|
private bool _softwareEmergencyActive;
|
2026-07-09 10:47:46 +08:00
|
|
|
|
private bool _hardwareEmergencyLatched;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private bool _disposed;
|
2026-07-12 12:07:41 +08:00
|
|
|
|
private long _pollSequence;
|
|
|
|
|
|
private long _pollGateBusyCount;
|
|
|
|
|
|
private string? _lastStatusSnapshot;
|
|
|
|
|
|
private string? _lastPollSnapshot;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
|
|
|
|
|
|
public SafetyMonitorService(
|
|
|
|
|
|
ISafetyDio safetyDio,
|
|
|
|
|
|
IPowerSupply power,
|
|
|
|
|
|
ILoad load,
|
2026-07-06 17:26:09 +08:00
|
|
|
|
ISspc sspc,
|
2026-07-01 20:04:27 +08:00
|
|
|
|
IOptions<DeviceOptions> options,
|
|
|
|
|
|
UiLogSink log,
|
|
|
|
|
|
ILogger<SafetyMonitorService> logger)
|
|
|
|
|
|
{
|
|
|
|
|
|
_safetyDio = safetyDio;
|
|
|
|
|
|
_power = power;
|
|
|
|
|
|
_load = load;
|
2026-07-06 17:26:09 +08:00
|
|
|
|
_sspc = sspc;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
_options = options.Value;
|
|
|
|
|
|
_log = log;
|
|
|
|
|
|
_logger = logger;
|
2026-07-12 12:07:41 +08:00
|
|
|
|
|
|
|
|
|
|
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}");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsEmergencyActive { get; private set; }
|
|
|
|
|
|
public bool IsAlarmOutputOn { get; private set; }
|
2026-07-02 01:50:02 +08:00
|
|
|
|
public string StatusText { get; private set; } = "设备故障";
|
2026-07-01 20:04:27 +08:00
|
|
|
|
public string DetailText { get; private set; } = "设备安全监控未启动";
|
|
|
|
|
|
public event EventHandler? StatusChanged;
|
|
|
|
|
|
|
|
|
|
|
|
public void Start()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_timer is not null) return;
|
|
|
|
|
|
|
|
|
|
|
|
var options = _options.Pci2312;
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"Start() called. Enabled={options.Enabled}, AutoConnect={options.AutoConnect}, DeviceId={options.DeviceId}, DI={options.EmergencyInputChannel}, DO={options.AlarmOutputChannel}, PollIntervalMs={options.PollIntervalMs}");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (!options.Enabled)
|
|
|
|
|
|
{
|
2026-07-02 01:50:02 +08:00
|
|
|
|
SetStatus(false, false, "设备故障", "PCI2312 安全监控未启用");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 10:47:46 +08:00
|
|
|
|
int interval = Math.Clamp(options.PollIntervalMs, 50, 10000);
|
2026-07-01 20:04:27 +08:00
|
|
|
|
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(interval));
|
2026-07-02 01:50:02 +08:00
|
|
|
|
SetStatus(false, false, "设备正常", "正在监控 PCI2312 急停输入");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
_log.Add(UiLogLevel.Info, $"安全观测日志已启用:{_traceLogPath}");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 14:16:15 +08:00
|
|
|
|
public async Task TriggerSoftwareEmergencyAsync(CancellationToken ct = default)
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
|
|
Trace("TriggerSoftwareEmergencyAsync enter.");
|
2026-07-03 14:16:15 +08:00
|
|
|
|
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);
|
2026-07-06 17:26:09 +08:00
|
|
|
|
await TryShutdownSspcAsync().ConfigureAwait(false);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
await TryShutdownPowerAsync().ConfigureAwait(false);
|
|
|
|
|
|
await TryShutdownLoadAsync().ConfigureAwait(false);
|
2026-07-06 17:26:09 +08:00
|
|
|
|
SetStatus(true, true, "软件急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"TriggerSoftwareEmergencyAsync success in {sw.ElapsedMilliseconds}ms.");
|
2026-07-03 14:16:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
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);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"TriggerSoftwareEmergencyAsync failed in {sw.ElapsedMilliseconds}ms. {ex.GetType().Name}: {ex.Message}");
|
2026-07-03 14:16:15 +08:00
|
|
|
|
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)
|
2026-07-01 20:04:27 +08:00
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace("ResetCoreAsync enter.");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
var options = _options.Pci2312;
|
2026-07-03 14:16:15 +08:00
|
|
|
|
await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false);
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false))
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
const string message = "硬件急停按钮仍处于触发状态,无法执行状态重置。请先旋转或释放设备上的急停按钮,确认急停输入复位后再重试。";
|
|
|
|
|
|
SetStatus(true, IsAlarmOutputOn, "急停已触发", message);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace("ResetCoreAsync blocked: emergency input still active.");
|
2026-07-03 14:16:15 +08:00
|
|
|
|
return new SafetyResetResult(false, message);
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await SetAlarmOutputAsync(false, ct).ConfigureAwait(false);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_softwareEmergencyActive = false;
|
2026-07-09 10:47:46 +08:00
|
|
|
|
_hardwareEmergencyLatched = false;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
_handlingEmergency = false;
|
2026-07-03 14:16:15 +08:00
|
|
|
|
SetStatus(false, false, "设备正常", "急停状态已重置,报警灯已关闭,正在监控 PCI2312 输入");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace("ResetCoreAsync success.");
|
2026-07-03 14:16:15 +08:00
|
|
|
|
return new SafetyResetResult(true, "急停状态已重置。");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnTimer(object? state)
|
|
|
|
|
|
{
|
2026-07-02 01:50:02 +08:00
|
|
|
|
if (_disposed) return;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
_ = PollAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task PollAsync()
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-07-01 20:04:27 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
long seq = Interlocked.Increment(ref _pollSequence);
|
2026-07-01 20:04:27 +08:00
|
|
|
|
var options = _options.Pci2312;
|
|
|
|
|
|
if (!options.Enabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (options.AutoConnect && !_safetyDio.IsConnected)
|
2026-07-12 12:07:41 +08:00
|
|
|
|
{
|
|
|
|
|
|
Trace($"Poll#{seq}: attempting auto connect for safety DIO.");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
await _safetyDio.ConnectAsync().ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
}
|
2026-07-01 20:04:27 +08:00
|
|
|
|
|
|
|
|
|
|
bool active = await IsEmergencyInputActiveAsync(options, CancellationToken.None).ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (active)
|
2026-07-02 01:50:02 +08:00
|
|
|
|
{
|
2026-07-09 10:47:46 +08:00
|
|
|
|
_hardwareEmergencyLatched = true;
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"Poll#{seq}: branch=ACTIVE -> HandleEmergencyAsync");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
await HandleEmergencyAsync().ConfigureAwait(false);
|
2026-07-02 01:50:02 +08:00
|
|
|
|
}
|
2026-07-09 10:47:46 +08:00
|
|
|
|
else if (_hardwareEmergencyLatched)
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"Poll#{seq}: branch=HardwareLatchedClear -> ClearHardwareEmergencyAsync");
|
2026-07-09 10:47:46 +08:00
|
|
|
|
await ClearHardwareEmergencyAsync(options).ConfigureAwait(false);
|
|
|
|
|
|
}
|
2026-07-03 14:16:15 +08:00
|
|
|
|
else if (_softwareEmergencyActive)
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"Poll#{seq}: branch=SoftwareActiveWaitReset");
|
2026-07-03 14:16:15 +08:00
|
|
|
|
SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "等待状态重置,电源和负载不会自动恢复");
|
|
|
|
|
|
}
|
2026-07-02 01:50:02 +08:00
|
|
|
|
else if (IsEmergencyActive || IsAlarmOutputOn)
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"Poll#{seq}: branch=CleanupByStatus -> ClearHardwareEmergencyAsync");
|
2026-07-09 10:47:46 +08:00
|
|
|
|
await ClearHardwareEmergencyAsync(options).ConfigureAwait(false);
|
2026-07-02 01:50:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
SetStatus(false, false, "设备正常", $"DI{options.EmergencyInputChannel} 未触发");
|
|
|
|
|
|
}
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(ex, "Safety monitor poll failed.");
|
2026-07-02 01:50:02 +08:00
|
|
|
|
SetStatus(IsEmergencyActive, IsAlarmOutputOn, "设备故障", ex.Message);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"PollAsync failed. {ex.GetType().Name}: {ex.Message}");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_gate.Release();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task HandleEmergencyAsync()
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
if (_handlingEmergency)
|
|
|
|
|
|
{
|
|
|
|
|
|
Trace("HandleEmergencyAsync skipped because _handlingEmergency=true.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var sw = Stopwatch.StartNew();
|
2026-07-01 20:04:27 +08:00
|
|
|
|
_handlingEmergency = true;
|
2026-07-02 01:50:02 +08:00
|
|
|
|
SetStatus(true, IsAlarmOutputOn, "急停已触发", "正在执行安全关断");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
_log.Add(UiLogLevel.Error, "检测到急停输入接通,开始执行报警灯、电源、负载关断。");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace("HandleEmergencyAsync begin.");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
var step = Stopwatch.StartNew();
|
2026-07-01 20:04:27 +08:00
|
|
|
|
await SetAlarmOutputAsync(true, CancellationToken.None).ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"HandleEmergencyAsync step SetAlarmOutputAsync OK in {step.ElapsedMilliseconds}ms.");
|
|
|
|
|
|
|
|
|
|
|
|
step.Restart();
|
2026-07-06 17:26:09 +08:00
|
|
|
|
await TryShutdownSspcAsync().ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"HandleEmergencyAsync step TryShutdownSspcAsync done in {step.ElapsedMilliseconds}ms.");
|
|
|
|
|
|
|
|
|
|
|
|
step.Restart();
|
2026-07-01 20:04:27 +08:00
|
|
|
|
await TryShutdownPowerAsync().ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"HandleEmergencyAsync step TryShutdownPowerAsync done in {step.ElapsedMilliseconds}ms.");
|
|
|
|
|
|
|
|
|
|
|
|
step.Restart();
|
2026-07-01 20:04:27 +08:00
|
|
|
|
await TryShutdownLoadAsync().ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"HandleEmergencyAsync step TryShutdownLoadAsync done in {step.ElapsedMilliseconds}ms.");
|
2026-07-06 17:26:09 +08:00
|
|
|
|
SetStatus(true, true, "急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"HandleEmergencyAsync success in {sw.ElapsedMilliseconds}ms.");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError(ex, "Emergency shutdown failed.");
|
|
|
|
|
|
_log.Add(UiLogLevel.Error, $"急停关断执行异常:{ex.Message}");
|
2026-07-02 01:50:02 +08:00
|
|
|
|
SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message);
|
|
|
|
|
|
_handlingEmergency = false;
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"HandleEmergencyAsync failed in {sw.ElapsedMilliseconds}ms. {ex.GetType().Name}: {ex.Message}");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 10:47:46 +08:00
|
|
|
|
private async Task ClearHardwareEmergencyAsync(Pci2312Options options)
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
|
|
Trace("ClearHardwareEmergencyAsync begin.");
|
2026-07-09 10:47:46 +08:00
|
|
|
|
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, "硬件急停输入已复位,软件急停状态和报警灯已自动解除。");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"ClearHardwareEmergencyAsync success in {sw.ElapsedMilliseconds}ms.");
|
2026-07-09 10:47:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
private async Task TryShutdownSspcAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_sspc.IsConnected && _options.Sspc.AutoConnect)
|
2026-07-12 12:07:41 +08:00
|
|
|
|
{
|
|
|
|
|
|
Trace("TryShutdownSspcAsync: auto-connecting SSPC.");
|
2026-07-06 17:26:09 +08:00
|
|
|
|
await _sspc.ConnectAsync().ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
}
|
2026-07-06 17:26:09 +08:00
|
|
|
|
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}");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"TryShutdownSspcAsync failed. {ex.GetType().Name}: {ex.Message}");
|
2026-07-06 17:26:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private async Task TryShutdownPowerAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_power.IsConnected && _options.PowerSupply.AutoConnect)
|
2026-07-12 12:07:41 +08:00
|
|
|
|
{
|
|
|
|
|
|
Trace("TryShutdownPowerAsync: auto-connecting power supply.");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
await _power.ConnectAsync().ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
}
|
2026-07-01 20:04:27 +08:00
|
|
|
|
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}");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"TryShutdownPowerAsync failed. {ex.GetType().Name}: {ex.Message}");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task TryShutdownLoadAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_load.IsConnected && _options.Load.AutoConnect)
|
2026-07-12 12:07:41 +08:00
|
|
|
|
{
|
|
|
|
|
|
Trace("TryShutdownLoadAsync: auto-connecting load.");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
await _load.ConnectAsync().ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
}
|
2026-07-01 20:04:27 +08:00
|
|
|
|
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}");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace($"TryShutdownLoadAsync failed. {ex.GetType().Name}: {ex.Message}");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 14:16:15 +08:00
|
|
|
|
private async Task EnsureSafetyDioConnectedAsync(CancellationToken ct)
|
|
|
|
|
|
{
|
|
|
|
|
|
var options = _options.Pci2312;
|
|
|
|
|
|
if (!options.Enabled)
|
|
|
|
|
|
throw new InvalidOperationException("PCI2312 安全输入输出未启用,无法控制报警灯。");
|
|
|
|
|
|
|
|
|
|
|
|
if (!_safetyDio.IsConnected)
|
2026-07-12 12:07:41 +08:00
|
|
|
|
{
|
|
|
|
|
|
Trace("EnsureSafetyDioConnectedAsync: connecting safety DIO.");
|
2026-07-03 14:16:15 +08:00
|
|
|
|
await _safetyDio.ConnectAsync(ct).ConfigureAwait(false);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
}
|
2026-07-03 14:16:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private void SetStatus(bool emergencyActive, bool alarmOutputOn, string statusText, string detailText)
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
string snapshot = $"emergency={emergencyActive},alarm={alarmOutputOn},status={statusText},detail={detailText}";
|
|
|
|
|
|
if (!string.Equals(_lastStatusSnapshot, snapshot, StringComparison.Ordinal))
|
|
|
|
|
|
{
|
|
|
|
|
|
_lastStatusSnapshot = snapshot;
|
|
|
|
|
|
Trace($"SetStatus changed: {snapshot}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
IsEmergencyActive = emergencyActive;
|
|
|
|
|
|
IsAlarmOutputOn = alarmOutputOn;
|
|
|
|
|
|
StatusText = statusText;
|
|
|
|
|
|
DetailText = detailText;
|
|
|
|
|
|
StatusChanged?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Trace("Dispose() called. Safety monitor disposed.");
|
2026-07-01 20:04:27 +08:00
|
|
|
|
_disposed = true;
|
|
|
|
|
|
_timer?.Dispose();
|
|
|
|
|
|
_gate.Dispose();
|
|
|
|
|
|
}
|
2026-07-12 12:07:41 +08:00
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-01 20:04:27 +08:00
|
|
|
|
}
|
2026-07-03 14:16:15 +08:00
|
|
|
|
|
|
|
|
|
|
public sealed record SafetyResetResult(bool Success, string Message);
|