diff --git a/SSPCTester.Logic/Calculation/CurveMetrics.cs b/SSPCTester.Logic/Calculation/CurveMetrics.cs
index e6983c4..13c9397 100644
--- a/SSPCTester.Logic/Calculation/CurveMetrics.cs
+++ b/SSPCTester.Logic/Calculation/CurveMetrics.cs
@@ -25,10 +25,8 @@ public static class CurveMetrics
///
public static double OnTimeMs(CurveData curve, double vMax, double thresholdRatio = 0.9)
{
- int idx = FirstReachIndex(curve.Voltage, vMax * thresholdRatio, rising: true);
- if (idx < 0) return double.NaN;
- double t = curve.TimeAt(idx) - curve.TriggerTimeSec;
- return t * 1000.0;
+ var mark = AnalyzeRising(curve, vMax, 0.1, thresholdRatio);
+ return mark.IsValid ? mark.EndTimeMs : double.NaN;
}
///
@@ -36,10 +34,8 @@ public static class CurveMetrics
///
public static double OffTimeMs(CurveData curve, double vMax, double thresholdRatio = 0.1)
{
- int idx = FirstReachIndex(curve.Voltage, vMax * thresholdRatio, rising: false);
- if (idx < 0) return double.NaN;
- double t = curve.TimeAt(idx) - curve.TriggerTimeSec;
- return t * 1000.0;
+ var mark = AnalyzeFalling(curve, vMax, 0.9, thresholdRatio);
+ return mark.IsValid ? mark.EndTimeMs : double.NaN;
}
///
@@ -65,15 +61,21 @@ public static class CurveMetrics
{
double startThreshold = 0;
double endThreshold = vMax * highRatio;
+ if (!CanAnalyze(curve, vMax, startThreshold, endThreshold))
+ return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+
int triggerIndex = TriggerIndex(curve);
- int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: true, startIndex: triggerIndex);
- if (endIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+ double[] filtered = Smooth(curve.Voltage);
+ int preliminaryEndIndex = FindStableReachIndex(filtered, endThreshold, rising: true, triggerIndex, filtered.Length - 1);
+ if (preliminaryEndIndex < 0)
+ preliminaryEndIndex = FirstReachIndex(filtered, endThreshold, rising: true, startIndex: triggerIndex);
+ if (preliminaryEndIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
- int startIndex = LastReachIndex(curve.Voltage, startThreshold, rising: false, startIndex: triggerIndex, endIndex: endIndex);
- if (startIndex < 0) startIndex = FirstReachIndex(curve.Voltage, startThreshold, rising: true, startIndex: triggerIndex);
- if (startIndex < 0 || startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+ int startIndex = FindDepartureStartIndex(filtered, curve.SampleRateHz, triggerIndex, preliminaryEndIndex, rising: true, vMax);
+ int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: true, startIndex: startIndex);
+ if (endIndex < 0 || startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
- return BuildTransitionMark(curve, startIndex, endIndex, startThreshold, endThreshold);
+ return BuildTransitionMark(curve, startIndex, endIndex, curve.Voltage[startIndex], endThreshold);
}
/// Falling curve 90% - 10% transition mark.
@@ -81,15 +83,21 @@ public static class CurveMetrics
{
double startThreshold = vMax;
double endThreshold = vMax * lowRatio;
+ if (!CanAnalyze(curve, vMax, startThreshold, endThreshold))
+ return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+
int triggerIndex = TriggerIndex(curve);
- int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: false, startIndex: triggerIndex);
- if (endIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+ double[] filtered = Smooth(curve.Voltage);
+ int preliminaryEndIndex = FindStableReachIndex(filtered, endThreshold, rising: false, triggerIndex, filtered.Length - 1);
+ if (preliminaryEndIndex < 0)
+ preliminaryEndIndex = FirstReachIndex(filtered, endThreshold, rising: false, startIndex: triggerIndex);
+ if (preliminaryEndIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
- int startIndex = LastReachIndex(curve.Voltage, vMax * 0.98, rising: true, startIndex: triggerIndex, endIndex: endIndex);
- if (startIndex < 0) startIndex = triggerIndex;
- if (startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+ int startIndex = FindDepartureStartIndex(filtered, curve.SampleRateHz, triggerIndex, preliminaryEndIndex, rising: false, vMax);
+ int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: false, startIndex: startIndex);
+ if (endIndex < 0 || startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
- return BuildTransitionMark(curve, startIndex, endIndex, startThreshold, endThreshold);
+ return BuildTransitionMark(curve, startIndex, endIndex, curve.Voltage[startIndex], endThreshold);
}
private static int TriggerIndex(CurveData curve)
@@ -116,6 +124,169 @@ public static class CurveMetrics
return -1;
}
+ private static bool CanAnalyze(CurveData curve, double vMax, double startThreshold, double endThreshold) =>
+ curve.Voltage.Length > 1
+ && curve.SampleRateHz > 0
+ && double.IsFinite(vMax)
+ && vMax > 0
+ && double.IsFinite(startThreshold)
+ && double.IsFinite(endThreshold);
+
+ private static double[] Smooth(double[] values)
+ {
+ double[] median = MedianFilter(values, window: 5);
+ return MovingAverage(median, window: 5);
+ }
+
+ private static double[] MedianFilter(double[] values, int window)
+ {
+ if (values.Length == 0 || window <= 1)
+ return (double[])values.Clone();
+
+ int radius = window / 2;
+ var result = new double[values.Length];
+ var scratch = new double[window];
+
+ for (int i = 0; i < values.Length; i++)
+ {
+ int from = Math.Max(0, i - radius);
+ int to = Math.Min(values.Length - 1, i + radius);
+ int count = 0;
+ for (int j = from; j <= to; j++)
+ scratch[count++] = values[j];
+
+ Array.Sort(scratch, 0, count);
+ result[i] = scratch[count / 2];
+ }
+
+ return result;
+ }
+
+ private static double[] MovingAverage(double[] values, int window)
+ {
+ if (values.Length == 0 || window <= 1)
+ return (double[])values.Clone();
+
+ int radius = window / 2;
+ var result = new double[values.Length];
+
+ for (int i = 0; i < values.Length; i++)
+ {
+ int from = Math.Max(0, i - radius);
+ int to = Math.Min(values.Length - 1, i + radius);
+ double sum = 0;
+ int count = 0;
+ for (int j = from; j <= to; j++)
+ {
+ sum += values[j];
+ count++;
+ }
+ result[i] = count > 0 ? sum / count : values[i];
+ }
+
+ return result;
+ }
+
+ private static int FindDepartureStartIndex(double[] values, int sampleRateHz, int triggerIndex, int endIndex, bool rising, double vMax)
+ {
+ if (values.Length == 0) return -1;
+
+ triggerIndex = Math.Clamp(triggerIndex, 0, values.Length - 1);
+ endIndex = Math.Clamp(endIndex, triggerIndex, values.Length - 1);
+
+ double baseline = EstimateBaseline(values, triggerIndex);
+ double noise = EstimateAmplitudeNoise(values, triggerIndex);
+ double deadband = Math.Max(vMax * 0.005, noise * 6.0);
+ int stableCount = StableCount(sampleRateHz);
+ int lastStableBaselineEnd = triggerIndex;
+
+ for (int i = triggerIndex; i <= endIndex - stableCount + 1; i++)
+ {
+ bool isBaseline = true;
+ for (int j = i; j < i + stableCount; j++)
+ {
+ bool nearBaseline = rising
+ ? values[j] <= baseline + deadband
+ : values[j] >= baseline - deadband;
+ if (!nearBaseline)
+ {
+ isBaseline = false;
+ break;
+ }
+ }
+
+ if (isBaseline)
+ lastStableBaselineEnd = i + stableCount - 1;
+ }
+
+ int start = Math.Min(lastStableBaselineEnd + 1, endIndex);
+ return Math.Clamp(start, triggerIndex, values.Length - 1);
+ }
+
+ private static int FindStableReachIndex(double[] values, double target, bool rising, int startIndex, int endIndex)
+ {
+ if (values.Length == 0) return -1;
+
+ const int stableCount = 3;
+ startIndex = Math.Clamp(startIndex, 0, values.Length - 1);
+ endIndex = Math.Clamp(endIndex, 0, values.Length - 1);
+
+ for (int i = startIndex; i <= endIndex; i++)
+ {
+ bool ok = true;
+ int to = Math.Min(endIndex, i + stableCount - 1);
+ for (int j = i; j <= to; j++)
+ {
+ if (!(rising ? values[j] >= target : values[j] <= target))
+ {
+ ok = false;
+ break;
+ }
+ }
+
+ if (ok && to - i + 1 == stableCount)
+ return i;
+ }
+
+ return -1;
+ }
+
+ private static int StableCount(int sampleRateHz)
+ {
+ if (sampleRateHz <= 0) return 3;
+ int count = (int)Math.Round(sampleRateHz * 0.00002);
+ return Math.Clamp(count, 3, 25);
+ }
+
+ private static double EstimateBaseline(double[] values, int triggerIndex)
+ {
+ int end = Math.Clamp(triggerIndex, 0, values.Length - 1);
+ int count = Math.Min(Math.Max(end + 1, 1), 100);
+ int start = Math.Max(0, end - count + 1);
+ var samples = new double[end - start + 1];
+ for (int i = start; i <= end; i++)
+ samples[i - start] = values[i];
+
+ Array.Sort(samples);
+ return samples[samples.Length / 2];
+ }
+
+ private static double EstimateAmplitudeNoise(double[] values, int triggerIndex)
+ {
+ int end = Math.Clamp(triggerIndex, 0, values.Length - 1);
+ int count = Math.Min(Math.Max(end + 1, 1), 100);
+ int start = Math.Max(0, end - count + 1);
+ if (end <= start) return 0;
+
+ double baseline = EstimateBaseline(values, triggerIndex);
+ var deviations = new double[end - start + 1];
+ for (int i = start; i <= end; i++)
+ deviations[i - start] = Math.Abs(values[i] - baseline);
+
+ Array.Sort(deviations);
+ return deviations[deviations.Length / 2];
+ }
+
private static CurveTransitionMark BuildTransitionMark(
CurveData curve,
int startIndex,
diff --git a/SSPCTester.Logic/Testing/BasicTest.cs b/SSPCTester.Logic/Testing/BasicTest.cs
index 6602ee8..f1af5ba 100644
--- a/SSPCTester.Logic/Testing/BasicTest.cs
+++ b/SSPCTester.Logic/Testing/BasicTest.cs
@@ -39,7 +39,8 @@ public sealed class BasicTest : ITestModule
row.CurrentOff = Math.Abs(offResult.Measurement.Current);
row.DisplayVoltage = row.VoltageOff;
row.DisplayCurrent = row.CurrentOff;
- row.Status = onOk && offResult.IsStableOff
+ bool offOk = offResult.IsStableOff;
+ row.Status = onOk && offOk
? TestStatus.Pass : TestStatus.Fail;
}
catch (Exception ex)
diff --git a/SSPCTester.Logic/Testing/BasicTestCriteria.cs b/SSPCTester.Logic/Testing/BasicTestCriteria.cs
index 95ebf4f..dc5ed69 100644
--- a/SSPCTester.Logic/Testing/BasicTestCriteria.cs
+++ b/SSPCTester.Logic/Testing/BasicTestCriteria.cs
@@ -3,7 +3,7 @@ using SSPCTester.Devices.Interfaces;
namespace SSPCTester.Logic.Testing;
-/// 基础通断测试的统一判定规则。
+/// Shared pass/fail rules for the basic on/off test.
public static class BasicTestCriteria
{
public const double OnVoltageThreshold = 1.0;
@@ -27,15 +27,17 @@ public static class BasicTestCriteria
HasVoltageData(voltage) && HasCurrentData(current);
public static bool IsOffState(double voltage, double current) =>
- HasVoltageData(voltage) && !HasCurrentData(current);
+ IsOff(voltage) && !HasCurrentData(current);
///
- /// 等待关状态稳定。连续多次满足“电压有数据、电流无数据”才确认关断,避免旧帧和瞬时噪声造成误判。
+ /// Waits until the channel is stably off: no voltage and no current for consecutive samples.
///
public static async Task WaitForStableOffAsync(
ISspc sspc,
int channel,
- CancellationToken ct = default)
+ CancellationToken ct = default,
+ Action? onSample = null,
+ Func>? readSampleAsync = null)
{
await Task.Delay(OffInitialDelayMs, ct).ConfigureAwait(false);
@@ -45,7 +47,8 @@ public static class BasicTestCriteria
while (stopwatch.ElapsedMilliseconds < OffSettleTimeoutMs)
{
- latest = await sspc.ReadMeasurementAsync(channel, ct).ConfigureAwait(false);
+ latest = await ReadSampleAsync(sspc, channel, ct, readSampleAsync).ConfigureAwait(false);
+ onSample?.Invoke(latest);
stableSamples = IsOffState(latest.Voltage, latest.Current) ? stableSamples + 1 : 0;
if (stableSamples >= RequiredStableOffSamples)
@@ -54,9 +57,17 @@ public static class BasicTestCriteria
await Task.Delay(OffSampleIntervalMs, ct).ConfigureAwait(false);
}
- latest ??= await sspc.ReadMeasurementAsync(channel, ct).ConfigureAwait(false);
+ latest ??= await ReadSampleAsync(sspc, channel, ct, readSampleAsync).ConfigureAwait(false);
+ onSample?.Invoke(latest);
return new OffStateResult(false, latest, stopwatch.Elapsed);
}
+
+ private static Task ReadSampleAsync(
+ ISspc sspc,
+ int channel,
+ CancellationToken ct,
+ Func>? readSampleAsync) =>
+ readSampleAsync?.Invoke(channel, ct) ?? sspc.ReadMeasurementAsync(channel, ct);
}
public sealed record OffStateResult(
diff --git a/SSPCTester.Tests/BasicTestCriteriaTests.cs b/SSPCTester.Tests/BasicTestCriteriaTests.cs
index feb5707..1c555c6 100644
--- a/SSPCTester.Tests/BasicTestCriteriaTests.cs
+++ b/SSPCTester.Tests/BasicTestCriteriaTests.cs
@@ -1,4 +1,5 @@
using SSPCTester.Logic.Testing;
+using SSPCTester.Devices.Interfaces;
namespace SSPCTester.Tests;
@@ -33,12 +34,74 @@ public sealed class BasicTestCriteriaTests
}
[Theory]
- [InlineData(28.0, 0.0, true)]
- [InlineData(28.0, 0.0005, true)]
- [InlineData(28.0, 0.01, false)]
- [InlineData(0.0, 0.0, false)]
- public void IsOffState_RequiresVoltageDataAndNoCurrentData(double voltage, double current, bool expected)
+ [InlineData(0.0, 0.0, true)]
+ [InlineData(0.49, 0.0, true)]
+ [InlineData(-0.49, 0.0, true)]
+ [InlineData(0.0, 0.0005, true)]
+ [InlineData(0.50, 0.0, false)]
+ [InlineData(0.75, 0.0, false)]
+ [InlineData(0.0, 0.01, false)]
+ [InlineData(28.0, 0.0, false)]
+ public void IsOffState_RequiresNoVoltageAndNoCurrent(double voltage, double current, bool expected)
{
Assert.Equal(expected, BasicTestCriteria.IsOffState(voltage, current));
}
+
+ [Fact]
+ public async Task WaitForStableOffAsync_ReportsEachSample()
+ {
+ var readings = new[]
+ {
+ new ChannelMeasurement { PhysicalSlot = 3, Voltage = 0.1, Current = 0.0 },
+ new ChannelMeasurement { PhysicalSlot = 3, Voltage = 0.0, Current = 0.0 }
+ };
+ var sspc = new SequenceSspc(readings);
+ var samples = new List();
+
+ var result = await BasicTestCriteria.WaitForStableOffAsync(sspc, 3, onSample: samples.Add);
+
+ Assert.True(result.IsStableOff);
+ Assert.Equal(2, samples.Count);
+ Assert.Equal(0.1, samples[0].Voltage);
+ Assert.Equal(0.0, samples[0].Current);
+ Assert.Equal(0.0, result.Measurement.Voltage);
+ }
+
+ private sealed class SequenceSspc : ISspc
+ {
+ private readonly Queue _readings;
+
+ public SequenceSspc(IEnumerable readings)
+ {
+ _readings = new Queue(readings);
+ }
+
+ public int ChannelCount => 24;
+ public bool IsConnected => true;
+ public string DisplayName => "Sequence SSPC";
+ public event EventHandler? ConnectionChanged;
+
+ public Task ConnectAsync(CancellationToken ct = default)
+ {
+ ConnectionChanged?.Invoke(this, true);
+ return Task.FromResult(true);
+ }
+
+ public Task DisconnectAsync() => Task.CompletedTask;
+ public Task TurnOnAsync(int physicalSlot, CancellationToken ct = default) => Task.CompletedTask;
+ public Task TurnOffAsync(int physicalSlot, CancellationToken ct = default) => Task.CompletedTask;
+ public Task FlashAsync(int physicalSlot, int milliseconds, CancellationToken ct = default) => Task.CompletedTask;
+ public Task GetChannelStateAsync(int physicalSlot, CancellationToken ct = default) => Task.FromResult(false);
+
+ public Task ReadMeasurementAsync(int physicalSlot, CancellationToken ct = default)
+ {
+ if (_readings.Count > 1)
+ return Task.FromResult(_readings.Dequeue());
+
+ return Task.FromResult(_readings.Peek());
+ }
+
+ public Task> ReadMeasurementsAsync(CancellationToken ct = default) =>
+ Task.FromResult>(_readings.ToArray());
+ }
}
diff --git a/SSPCTester.Tests/CalculationTests.cs b/SSPCTester.Tests/CalculationTests.cs
index 89c4dab..b007802 100644
--- a/SSPCTester.Tests/CalculationTests.cs
+++ b/SSPCTester.Tests/CalculationTests.cs
@@ -56,12 +56,12 @@ public class CurveMetricsTests
var mark = CurveMetrics.AnalyzeRising(c, 28);
int triggerIndex = (int)Math.Round(c.TriggerTimeSec * c.SampleRateHz);
int expectedEnd = CurveMetrics.FirstReachIndex(c.Voltage, 28 * 0.9, rising: true, triggerIndex);
- int expectedStart = triggerIndex;
+ int expectedStart = triggerIndex + 1;
Assert.True(mark.IsValid);
Assert.Equal(expectedStart, mark.StartIndex);
Assert.Equal(expectedEnd, mark.EndIndex);
- Assert.Equal(0, mark.StartThresholdV, 6);
+ Assert.Equal(c.Voltage[expectedStart], mark.StartThresholdV, 6);
Assert.Equal((c.TimeAt(expectedEnd) - c.TimeAt(expectedStart)) * 1000.0, mark.DurationMs, 6);
Assert.Equal(mark.DurationMs, CurveMetrics.RiseTimeMs(c, 28), 6);
}
@@ -81,21 +81,72 @@ public class CurveMetricsTests
var mark = CurveMetrics.AnalyzeFalling(c, 28);
int triggerIndex = (int)Math.Round(c.TriggerTimeSec * c.SampleRateHz);
int expectedEnd = CurveMetrics.FirstReachIndex(c.Voltage, 28 * 0.1, rising: false, triggerIndex);
- int expectedStart = triggerIndex;
- for (int i = triggerIndex; i <= expectedEnd; i++)
- {
- if (c.Voltage[i] >= 28 * 0.98)
- expectedStart = i;
- }
+ int expectedStart = triggerIndex + 1;
Assert.True(mark.IsValid);
Assert.Equal(expectedStart, mark.StartIndex);
Assert.Equal(expectedEnd, mark.EndIndex);
- Assert.Equal(28, mark.StartThresholdV, 6);
+ Assert.Equal(c.Voltage[expectedStart], mark.StartThresholdV, 6);
Assert.Equal((c.TimeAt(expectedEnd) - c.TimeAt(expectedStart)) * 1000.0, mark.DurationMs, 6);
Assert.Equal(mark.DurationMs, CurveMetrics.FallTimeMs(c, 28), 6);
}
+ [Fact]
+ public void AnalyzeRising_StartsAtSustainedRiseAndEndsAtFirstRawNinetyPercent()
+ {
+ const int rate = 1_000_000;
+ const double trigger = 0.0002;
+ int triggerIndex = (int)Math.Round(trigger * rate);
+ var v = Enumerable.Repeat(0.0, 2_000).ToArray();
+
+ v[triggerIndex + 100] = -4.0;
+ v[triggerIndex + 101] = 2.0;
+ v[triggerIndex + 102] = 0.0;
+
+ int riseStart = triggerIndex + 500;
+ for (int i = riseStart; i < v.Length; i++)
+ {
+ double progress = Math.Clamp((i - riseStart) / 60.0, 0.0, 1.0);
+ v[i] = 5.0 * progress;
+ }
+
+ var curve = new CurveData { SampleRateHz = rate, TriggerTimeSec = trigger, Voltage = v };
+ var mark = CurveMetrics.AnalyzeRising(curve, 5.0);
+ int expectedEnd = CurveMetrics.FirstReachIndex(v, 4.5, rising: true, riseStart);
+
+ Assert.True(mark.IsValid);
+ Assert.InRange(mark.StartIndex, riseStart - 3, riseStart + 5);
+ Assert.Equal(expectedEnd, mark.EndIndex);
+ }
+
+ [Fact]
+ public void AnalyzeFalling_StartsAtSlowDropAndEndsAtFirstRawTenPercent()
+ {
+ const int rate = 1_000_000;
+ const double trigger = 0.0002;
+ int triggerIndex = (int)Math.Round(trigger * rate);
+ var v = Enumerable.Repeat(3.0, 2_000).ToArray();
+
+ int fallStart = triggerIndex + 300;
+ for (int i = fallStart; i < triggerIndex + 850; i++)
+ v[i] = 3.0 - 0.55 * (i - fallStart) / 550.0;
+
+ v[triggerIndex + 850] = 5.0;
+ for (int i = triggerIndex + 851; i < v.Length; i++)
+ {
+ double progress = Math.Clamp((i - (triggerIndex + 851)) / 45.0, 0.0, 1.0);
+ v[i] = Math.Max(-0.2, 2.45 * (1.0 - progress));
+ }
+
+ var curve = new CurveData { SampleRateHz = rate, TriggerTimeSec = trigger, Voltage = v };
+ var mark = CurveMetrics.AnalyzeFalling(curve, 3.0);
+ int expectedEnd = CurveMetrics.FirstReachIndex(v, 0.3, rising: false, fallStart);
+
+ Assert.True(mark.IsValid);
+ Assert.InRange(mark.StartIndex, fallStart - 5, fallStart + 30);
+ Assert.Equal(expectedEnd, mark.EndIndex);
+ }
+
[Fact]
public void AnalyzeTransition_ReturnsInvalidWhenThresholdsAreNotReached()
{
diff --git a/SSPCTester.UI/Services/SafetyMonitorService.cs b/SSPCTester.UI/Services/SafetyMonitorService.cs
index a2355c2..e6afb0c 100644
--- a/SSPCTester.UI/Services/SafetyMonitorService.cs
+++ b/SSPCTester.UI/Services/SafetyMonitorService.cs
@@ -10,6 +10,7 @@ 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;
@@ -23,6 +24,7 @@ public sealed class SafetyMonitorService : IDisposable
ISafetyDio safetyDio,
IPowerSupply power,
ILoad load,
+ ISspc sspc,
IOptions options,
UiLogSink log,
ILogger logger)
@@ -30,6 +32,7 @@ public sealed class SafetyMonitorService : IDisposable
_safetyDio = safetyDio;
_power = power;
_load = load;
+ _sspc = sspc;
_options = options.Value;
_log = log;
_logger = logger;
@@ -69,9 +72,10 @@ public sealed class SafetyMonitorService : IDisposable
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, "软件急停已触发", "报警灯已接通,电源输出和负载输入已发送关闭指令");
+ SetStatus(true, true, "软件急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
}
catch (Exception ex)
{
@@ -175,9 +179,10 @@ public sealed class SafetyMonitorService : IDisposable
try
{
await SetAlarmOutputAsync(true, CancellationToken.None).ConfigureAwait(false);
+ await TryShutdownSspcAsync().ConfigureAwait(false);
await TryShutdownPowerAsync().ConfigureAwait(false);
await TryShutdownLoadAsync().ConfigureAwait(false);
- SetStatus(true, true, "急停已触发", "报警灯已接通,电源输出和负载输入已发送关闭指令");
+ SetStatus(true, true, "急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
}
catch (Exception ex)
{
@@ -188,6 +193,33 @@ public sealed class SafetyMonitorService : IDisposable
}
}
+ 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
diff --git a/SSPCTester.UI/ViewModels/BasicTestViewModel.cs b/SSPCTester.UI/ViewModels/BasicTestViewModel.cs
index dd840aa..06252d5 100644
--- a/SSPCTester.UI/ViewModels/BasicTestViewModel.cs
+++ b/SSPCTester.UI/ViewModels/BasicTestViewModel.cs
@@ -84,11 +84,8 @@ public partial class BasicTestViewModel : ObservableObject
row.Status = TestStatus.Running;
await _sspc.TurnOnAsync(row.Channel);
await Task.Delay(SwitchSettleDelayMs);
- var measurement = await _sspc.ReadMeasurementAsync(row.Channel);
- row.VoltageOn = measurement.Voltage;
- row.CurrentOn = Math.Abs(measurement.Current);
- row.DisplayVoltage = row.VoltageOn;
- row.DisplayCurrent = row.CurrentOn;
+ var measurement = await ReadDisplayedMeasurementAsync(row.Channel, CancellationToken.None);
+ ApplyOnMeasurement(row, measurement);
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
row.Status = onOk ? TestStatus.Running : TestStatus.Fail;
StartPolling(row.Channel);
@@ -112,18 +109,21 @@ public partial class BasicTestViewModel : ObservableObject
Busy = true;
try
{
+ if (!_sspc.IsConnected) await _sspc.ConnectAsync();
await StopPollingAsync(row.Channel);
await _sspc.TurnOffAsync(row.Channel);
await Task.Delay(SwitchSettleDelayMs);
- var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel);
- row.VoltageOff = offResult.Measurement.Voltage;
- row.CurrentOff = Math.Abs(offResult.Measurement.Current);
- row.DisplayVoltage = row.VoltageOff;
- row.DisplayCurrent = row.CurrentOff;
+ var offResult = await BasicTestCriteria.WaitForStableOffAsync(
+ _sspc,
+ row.Channel,
+ onSample: measurement => ApplyOffMeasurement(row, measurement),
+ readSampleAsync: ReadDisplayedMeasurementAsync);
+ ApplyOffMeasurement(row, offResult.Measurement);
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
- row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail;
+ bool offOk = offResult.IsStableOff;
+ row.Status = (onOk && offOk) ? TestStatus.Pass : TestStatus.Fail;
string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
- string offState = offResult.IsStableOff ? "已稳定关断" : "关断超时";
+ string offState = offOk ? "已稳定关断" : "关断超时";
_log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error,
$"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V I={row.CurrentOff:F3}A {offState} {verdict}");
}
@@ -203,11 +203,8 @@ public partial class BasicTestViewModel : ObservableObject
await _sspc.TurnOnAsync(row.Channel, ct);
await Task.Delay(SwitchSettleDelayMs, ct);
- var measurement = await _sspc.ReadMeasurementAsync(row.Channel, ct);
- row.VoltageOn = measurement.Voltage;
- row.CurrentOn = Math.Abs(measurement.Current);
- row.DisplayVoltage = row.VoltageOn;
- row.DisplayCurrent = row.CurrentOn;
+ var measurement = await ReadDisplayedMeasurementAsync(row.Channel, ct);
+ ApplyOnMeasurement(row, measurement);
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
_log.Add(onOk ? UiLogLevel.Info : UiLogLevel.Error,
$"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A {(onOk ? "开状态合格" : "开状态不合格")}");
@@ -220,14 +217,17 @@ public partial class BasicTestViewModel : ObservableObject
await _sspc.TurnOffAsync(row.Channel, ct);
await Task.Delay(SwitchSettleDelayMs, ct);
- var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel, ct);
- row.VoltageOff = offResult.Measurement.Voltage;
- row.CurrentOff = Math.Abs(offResult.Measurement.Current);
- row.DisplayVoltage = row.VoltageOff;
- row.DisplayCurrent = row.CurrentOff;
- row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail;
+ var offResult = await BasicTestCriteria.WaitForStableOffAsync(
+ _sspc,
+ row.Channel,
+ ct,
+ measurement => ApplyOffMeasurement(row, measurement),
+ ReadDisplayedMeasurementAsync);
+ ApplyOffMeasurement(row, offResult.Measurement);
+ bool offOk = offResult.IsStableOff;
+ row.Status = (onOk && offOk) ? TestStatus.Pass : TestStatus.Fail;
string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
- string offState = offResult.IsStableOff ? "关状态合格" : "关状态不合格";
+ string offState = offOk ? "已稳定关断" : "关断超时";
_log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error,
$"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V I={row.CurrentOff:F3}A {offState} {verdict}");
}
@@ -290,6 +290,54 @@ public partial class BasicTestViewModel : ObservableObject
}
}
+ private async Task ReadDisplayedMeasurementAsync(int channel, CancellationToken ct)
+ {
+ var measurements = await _sspc.ReadMeasurementsAsync(ct);
+ var measurement = measurements.FirstOrDefault(x => x.PhysicalSlot == channel);
+ if (measurement != null)
+ return measurement;
+
+ return await _sspc.ReadMeasurementAsync(channel, ct);
+ }
+
+ private static void ApplyOnMeasurement(ChannelResult row, ChannelMeasurement measurement)
+ {
+ ApplyMeasurement(row, measurement, isOnMeasurement: true);
+ }
+
+ private static void ApplyOffMeasurement(ChannelResult row, ChannelMeasurement measurement)
+ {
+ ApplyMeasurement(row, measurement, isOnMeasurement: false);
+ }
+
+ private static void ApplyMeasurement(ChannelResult row, ChannelMeasurement measurement, bool isOnMeasurement)
+ {
+ void Apply()
+ {
+ double voltage = measurement.Voltage;
+ double current = Math.Abs(measurement.Current);
+ if (isOnMeasurement)
+ {
+ row.VoltageOn = voltage;
+ row.CurrentOn = current;
+ }
+ else
+ {
+ row.VoltageOff = voltage;
+ row.CurrentOff = current;
+ }
+
+ row.DisplayVoltage = voltage;
+ row.DisplayCurrent = current;
+ }
+
+ var dispatcher = Application.Current?.Dispatcher;
+ if (dispatcher == null || dispatcher.CheckAccess())
+ Apply();
+ else
+ dispatcher.Invoke(Apply);
+ }
+
private async Task PollMeasurementsAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
@@ -322,10 +370,7 @@ public partial class BasicTestViewModel : ObservableObject
var row = Rows.FirstOrDefault(x => x.Channel == measurement.PhysicalSlot);
if (row == null) continue;
- row.VoltageOn = measurement.Voltage;
- row.CurrentOn = Math.Abs(measurement.Current);
- row.DisplayVoltage = row.VoltageOn;
- row.DisplayCurrent = row.CurrentOn;
+ ApplyOnMeasurement(row, measurement);
}
}
}