diff --git a/Docs/警灯代码.html b/Docs/警灯代码.html
new file mode 100644
index 0000000..e41f1d3
--- /dev/null
+++ b/Docs/警灯代码.html
@@ -0,0 +1,117 @@
+
+
+
+
+
+警示灯图标预览 v2
+
+
+
+警示灯图标预览 v2
+去掉外部红色光晕,改为灯罩本体强闪 + 内部扫光。小尺寸下动态更明显。
+
+
+
+
+
+
未激活:灰色静态
+
+
+
+
+
+
+
激活:本体强闪 + 扫光
+
+
+
+
+
+
+
34px 小图标预览
+
+
+
+
\ No newline at end of file
diff --git a/SSPCTester.Devices/Drivers/Mock/MockSspc.cs b/SSPCTester.Devices/Drivers/Mock/MockSspc.cs
index 57cdd9b..70dd0c6 100644
--- a/SSPCTester.Devices/Drivers/Mock/MockSspc.cs
+++ b/SSPCTester.Devices/Drivers/Mock/MockSspc.cs
@@ -93,9 +93,9 @@ public sealed class MockSspc : MockDeviceBase, ISspc
return Task.FromResult(new ChannelMeasurement
{
PhysicalSlot = physicalSlot,
- Voltage = on ? 28.0 : 0.0,
+ Voltage = 28.0,
Current = on ? 1.0 : 0.0,
- VoltageRaw = on ? (ushort)2800 : (ushort)0,
+ VoltageRaw = 2800,
CurrentRaw = on ? (short)167 : (short)0,
Timestamp = DateTimeOffset.Now
});
diff --git a/SSPCTester.Logic/Models/Results.cs b/SSPCTester.Logic/Models/Results.cs
index 46d4443..7d551e6 100644
--- a/SSPCTester.Logic/Models/Results.cs
+++ b/SSPCTester.Logic/Models/Results.cs
@@ -9,6 +9,9 @@ public partial class ChannelResult : ObservableObject
[ObservableProperty] private double _voltageOn;
[ObservableProperty] private double _currentOn;
[ObservableProperty] private double _voltageOff;
+ [ObservableProperty] private double _currentOff;
+ [ObservableProperty] private double _displayVoltage;
+ [ObservableProperty] private double _displayCurrent;
[ObservableProperty] private TestStatus _status = TestStatus.Untested;
}
@@ -152,6 +155,8 @@ public static class ProtectTestPlan
row.Trigger = item.Trigger;
if (string.IsNullOrWhiteSpace(row.JudgeMode))
row.JudgeMode = ManualJudgeMode;
+ if (row.Status == TestStatus.Measured)
+ row.Status = TestStatus.Untested;
project.ProtectResults.Add(row);
}
diff --git a/SSPCTester.Logic/Models/TestSelection.cs b/SSPCTester.Logic/Models/TestSelection.cs
index f253393..e4ec565 100644
--- a/SSPCTester.Logic/Models/TestSelection.cs
+++ b/SSPCTester.Logic/Models/TestSelection.cs
@@ -25,7 +25,7 @@ public sealed class ReportSelection
public bool Curve { get; set; } = true;
public bool Power { get; set; } = true;
public bool Protect { get; set; } = true;
- public string FileName { get; set; } = "report_{yyyyMMdd_HHmmss}.pdf";
+ public string FileName { get; set; } = "{ProjectName}_{yyyyMMdd_HHmmss}.pdf";
}
/// 四个测试页“测试说明”手动记录内容。
diff --git a/SSPCTester.Logic/Report/ProfessionalReportGenerator.cs b/SSPCTester.Logic/Report/ProfessionalReportGenerator.cs
index 3fe8e25..411f898 100644
--- a/SSPCTester.Logic/Report/ProfessionalReportGenerator.cs
+++ b/SSPCTester.Logic/Report/ProfessionalReportGenerator.cs
@@ -349,12 +349,12 @@ public sealed class ReportGenerator
}
private static void BasicTable(IContainer container, Project p) =>
- DataTable(container, new[] { 0.65f, 1f, 1f, 1.15f, 0.8f },
- new[] { "通道", "导通电压\nV", "导通电流\nA", "关后电压\nV", "判定" },
+ DataTable(container, new[] { 0.65f, 1f, 1f, 1.15f, 1.15f, 0.8f },
+ new[] { "通道", "导通电压\nV", "导通电流\nA", "关后电压\nV", "关后电流\nA", "判定" },
p.BasicResults.Select(r => new ReportRow(r.Status, new[]
{
$"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"),
- Format(r.VoltageOff, "F2"), StatusText(r.Status)
+ Format(r.VoltageOff, "F2"), Format(r.CurrentOff, "F3"), StatusText(r.Status)
})));
private static void CurveTable(IContainer container, Project p) =>
@@ -386,7 +386,7 @@ public sealed class ReportGenerator
ValueOrDash(r.Trigger),
ValueOrDash(r.MeasuredValue),
ValueOrDash(r.JudgeMode),
- StatusText(r.Status)
+ ProtectStatusText(r.Status)
})));
private static void DataTable(IContainer container, IReadOnlyList widths,
@@ -501,6 +501,13 @@ public sealed class ReportGenerator
TestStatus.Running => "测试中", TestStatus.Measured => "已测",
_ => "未测试"
};
+ private static string ProtectStatusText(TestStatus status) => status switch
+ {
+ TestStatus.Pass => "合格",
+ TestStatus.Fail => "不合格",
+ TestStatus.Running => "测试中",
+ _ => "未判定"
+ };
private static string StatusColor(TestStatus status) => status switch
{
TestStatus.Pass => Success, TestStatus.Fail => Danger,
diff --git a/SSPCTester.Logic/Report/ReportFileHelper.cs b/SSPCTester.Logic/Report/ReportFileHelper.cs
index f3ee66b..a0277fe 100644
--- a/SSPCTester.Logic/Report/ReportFileHelper.cs
+++ b/SSPCTester.Logic/Report/ReportFileHelper.cs
@@ -13,7 +13,7 @@ public enum ReportOutputFormat
public static class ReportFileHelper
{
- public const string DefaultTemplate = "report_{yyyyMMdd_HHmmss}.pdf";
+ public const string DefaultTemplate = "{ProjectName}_{yyyyMMdd_HHmmss}.pdf";
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
diff --git a/SSPCTester.Logic/Report/ReportGenerator.cs b/SSPCTester.Logic/Report/ReportGenerator.cs
index c7ecba3..57f878b 100644
--- a/SSPCTester.Logic/Report/ReportGenerator.cs
+++ b/SSPCTester.Logic/Report/ReportGenerator.cs
@@ -189,15 +189,16 @@ internal sealed class LegacyReportGenerator
{
t.ColumnsDefinition(cd =>
{
- cd.ConstantColumn(60); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.ConstantColumn(70);
+ cd.ConstantColumn(60); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.ConstantColumn(70);
});
- HeaderRow(t, "通道", "电压(V)", "电流(A)", "关后电压(V)", "结果");
+ HeaderRow(t, "通道", "开电压(V)", "开电流(A)", "关电压(V)", "关电流(A)", "结果");
foreach (var r in p.BasicResults)
{
t.Cell().Padding(3).Text($"CH{r.Channel}");
t.Cell().Padding(3).Text(r.VoltageOn.ToString("F2"));
t.Cell().Padding(3).Text(r.CurrentOn.ToString("F3"));
t.Cell().Padding(3).Text(r.VoltageOff.ToString("F2"));
+ t.Cell().Padding(3).Text(r.CurrentOff.ToString("F3"));
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
}
});
@@ -271,7 +272,7 @@ internal sealed class LegacyReportGenerator
t.Cell().Padding(3).Text(r.Trigger);
t.Cell().Padding(3).Text(r.MeasuredValue);
t.Cell().Padding(3).Text(r.JudgeMode);
- t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
+ t.Cell().Padding(3).Text(ProtectStatusText(r.Status)).FontColor(StatusColor(r.Status));
}
});
}
@@ -292,6 +293,13 @@ internal sealed class LegacyReportGenerator
TestStatus.Measured => "已测",
_ => "未测试"
};
+ private static string ProtectStatusText(TestStatus s) => s switch
+ {
+ TestStatus.Pass => "合格",
+ TestStatus.Fail => "不合格",
+ TestStatus.Running => "测试中",
+ _ => "未判定"
+ };
private static string StatusColor(TestStatus s) => s switch
{
diff --git a/SSPCTester.Logic/Report/WordReportGenerator.cs b/SSPCTester.Logic/Report/WordReportGenerator.cs
index b098e6a..cad86ff 100644
--- a/SSPCTester.Logic/Report/WordReportGenerator.cs
+++ b/SSPCTester.Logic/Report/WordReportGenerator.cs
@@ -118,11 +118,11 @@ public sealed class WordReportGenerator
Section(body, section++, "基础测试", "BASIC FUNCTION TEST");
if (p.BasicResults.Count > 0)
body.Append(ResultTable(
- new[] { "通道", "导通电压 V", "导通电流 A", "关后电压 V", "判定" },
+ new[] { "通道", "导通电压 V", "导通电流 A", "关后电压 V", "关后电流 A", "判定" },
p.BasicResults.Select(r => new[]
{
$"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"),
- Format(r.VoltageOff, "F2"), StatusText(r.Status)
+ Format(r.VoltageOff, "F2"), Format(r.CurrentOff, "F3"), StatusText(r.Status)
})));
ManualNote(body, p.TestFunction?.Basic);
}
@@ -174,7 +174,7 @@ public sealed class WordReportGenerator
ValueOrDash(r.Trigger),
ValueOrDash(r.MeasuredValue),
ValueOrDash(r.JudgeMode),
- StatusText(r.Status)
+ ProtectStatusText(r.Status)
})));
ManualNote(body, p.TestFunction?.Protect);
}
@@ -417,6 +417,13 @@ public sealed class WordReportGenerator
TestStatus.Measured => "已测",
_ => "未测试"
};
+ private static string ProtectStatusText(TestStatus status) => status switch
+ {
+ TestStatus.Pass => "合格",
+ TestStatus.Fail => "不合格",
+ TestStatus.Running => "测试中",
+ _ => "未判定"
+ };
private static string StatusColor(TestStatus status) => status switch
{
diff --git a/SSPCTester.Logic/Testing/BasicTest.cs b/SSPCTester.Logic/Testing/BasicTest.cs
index 212c23f..6602ee8 100644
--- a/SSPCTester.Logic/Testing/BasicTest.cs
+++ b/SSPCTester.Logic/Testing/BasicTest.cs
@@ -26,6 +26,9 @@ public sealed class BasicTest : ITestModule
var on = await ctx.Sspc.ReadMeasurementAsync(ch, ct).ConfigureAwait(false);
row.VoltageOn = on.Voltage;
row.CurrentOn = Math.Abs(on.Current);
+ row.DisplayVoltage = row.VoltageOn;
+ row.DisplayCurrent = row.CurrentOn;
+ bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
await ctx.Sspc.TurnOffAsync(ch, ct).ConfigureAwait(false);
await Task.Delay(BasicTestCriteria.SwitchSettleDelayMs, ct).ConfigureAwait(false);
@@ -33,7 +36,10 @@ public sealed class BasicTest : ITestModule
.WaitForStableOffAsync(ctx.Sspc, ch, ct)
.ConfigureAwait(false);
row.VoltageOff = offResult.Measurement.Voltage;
- row.Status = BasicTestCriteria.IsOn(row.VoltageOn) && offResult.IsStableOff
+ row.CurrentOff = Math.Abs(offResult.Measurement.Current);
+ row.DisplayVoltage = row.VoltageOff;
+ row.DisplayCurrent = row.CurrentOff;
+ row.Status = onOk && offResult.IsStableOff
? TestStatus.Pass : TestStatus.Fail;
}
catch (Exception ex)
diff --git a/SSPCTester.Logic/Testing/BasicTestCriteria.cs b/SSPCTester.Logic/Testing/BasicTestCriteria.cs
index 0acc6b2..95ebf4f 100644
--- a/SSPCTester.Logic/Testing/BasicTestCriteria.cs
+++ b/SSPCTester.Logic/Testing/BasicTestCriteria.cs
@@ -8,6 +8,7 @@ public static class BasicTestCriteria
{
public const double OnVoltageThreshold = 1.0;
public const double OffVoltageThreshold = 0.5;
+ public const double CurrentPresentThreshold = 0.001;
public const int SwitchSettleDelayMs = 1_000;
public const int OffSettleTimeoutMs = 2_000;
public const int OffInitialDelayMs = 100;
@@ -18,8 +19,18 @@ public static class BasicTestCriteria
public static bool IsOff(double voltage) => Math.Abs(voltage) < OffVoltageThreshold;
+ public static bool HasVoltageData(double voltage) => voltage > OnVoltageThreshold;
+
+ public static bool HasCurrentData(double current) => Math.Abs(current) > CurrentPresentThreshold;
+
+ public static bool IsOnState(double voltage, double current) =>
+ HasVoltageData(voltage) && HasCurrentData(current);
+
+ public static bool IsOffState(double voltage, double current) =>
+ HasVoltageData(voltage) && !HasCurrentData(current);
+
///
- /// 等待关断电压稳定。连续多次低于阈值才确认关断,避免旧帧和瞬时噪声造成误判。
+ /// 等待关状态稳定。连续多次满足“电压有数据、电流无数据”才确认关断,避免旧帧和瞬时噪声造成误判。
///
public static async Task WaitForStableOffAsync(
ISspc sspc,
@@ -35,7 +46,7 @@ public static class BasicTestCriteria
while (stopwatch.ElapsedMilliseconds < OffSettleTimeoutMs)
{
latest = await sspc.ReadMeasurementAsync(channel, ct).ConfigureAwait(false);
- stableSamples = IsOff(latest.Voltage) ? stableSamples + 1 : 0;
+ stableSamples = IsOffState(latest.Voltage, latest.Current) ? stableSamples + 1 : 0;
if (stableSamples >= RequiredStableOffSamples)
return new OffStateResult(true, latest, stopwatch.Elapsed);
diff --git a/SSPCTester.Logic/Testing/PowerTestUdp5080.cs b/SSPCTester.Logic/Testing/PowerTestUdp5080.cs
index 9e9513b..5ce48f6 100644
--- a/SSPCTester.Logic/Testing/PowerTestUdp5080.cs
+++ b/SSPCTester.Logic/Testing/PowerTestUdp5080.cs
@@ -22,6 +22,9 @@ public sealed class PowerTest : ITestModule
{
if (!_power.IsConnected) await _power.ConnectAsync(ct).ConfigureAwait(false);
if (!ctx.Load.IsConnected) await ctx.Load.ConnectAsync(ct).ConfigureAwait(false);
+ await _power.OutputAsync(true, ct).ConfigureAwait(false);
+ await ctx.Load.InputAsync(true, ct).ConfigureAwait(false);
+
int n = ctx.Sspc.ChannelCount;
ctx.Project.PowerResults.Clear();
diff --git a/SSPCTester.Logic/Testing/ProtectTest.cs b/SSPCTester.Logic/Testing/ProtectTest.cs
index e93b037..159f8f7 100644
--- a/SSPCTester.Logic/Testing/ProtectTest.cs
+++ b/SSPCTester.Logic/Testing/ProtectTest.cs
@@ -24,7 +24,7 @@ public sealed class ProtectTest : ITestModule
await Task.Delay(150, ct).ConfigureAwait(false);
if (!preserveManualJudgement && row.Status == TestStatus.Running)
- row.Status = TestStatus.Measured;
+ row.Status = TestStatus.Untested;
progress.Report(new TestProgress { ModuleName = Name, Step = i + 1, TotalSteps = ctx.Project.ProtectResults.Count, StepStatus = row.Status });
}
}
diff --git a/SSPCTester.Tests/BasicTestCriteriaTests.cs b/SSPCTester.Tests/BasicTestCriteriaTests.cs
index b867e4e..feb5707 100644
--- a/SSPCTester.Tests/BasicTestCriteriaTests.cs
+++ b/SSPCTester.Tests/BasicTestCriteriaTests.cs
@@ -22,4 +22,23 @@ public sealed class BasicTestCriteriaTests
{
Assert.Equal(expected, BasicTestCriteria.IsOff(voltage));
}
+
+ [Theory]
+ [InlineData(28.0, 1.0, true)]
+ [InlineData(28.0, 0.0, false)]
+ [InlineData(0.0, 1.0, false)]
+ public void IsOnState_RequiresVoltageAndCurrentData(double voltage, double current, bool expected)
+ {
+ Assert.Equal(expected, BasicTestCriteria.IsOnState(voltage, current));
+ }
+
+ [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)
+ {
+ Assert.Equal(expected, BasicTestCriteria.IsOffState(voltage, current));
+ }
}
diff --git a/SSPCTester.Tests/InterfacePowerTests.cs b/SSPCTester.Tests/InterfacePowerTests.cs
index 7912d3f..1000c6c 100644
--- a/SSPCTester.Tests/InterfacePowerTests.cs
+++ b/SSPCTester.Tests/InterfacePowerTests.cs
@@ -29,7 +29,8 @@ public sealed class InterfacePowerTests
}
}
});
- var test = new PowerTest(new FixedPowerSupply()) { SamplesPerChannel = 1, IntervalMs = 0 };
+ var power = new FixedPowerSupply();
+ var test = new PowerTest(power) { SamplesPerChannel = 1, IntervalMs = 0 };
var context = new TestContext(project, sspc, forbiddenPower, load, daq);
await test.RunAsync(context, new Progress(), CancellationToken.None);
@@ -48,6 +49,8 @@ public sealed class InterfacePowerTests
});
Assert.Equal(0, sspc.TurnOffCount);
Assert.Equal(0, sspc.ReadMeasurementCount);
+ Assert.Equal(1, power.OutputOnCount);
+ Assert.Equal(1, load.InputOnCount);
Assert.Equal(24, load.ReadPowerCount);
Assert.Equal(0, daq.ReadInstantCount);
}
@@ -127,6 +130,7 @@ public sealed class InterfacePowerTests
private sealed class MeasuringLoad : ILoad
{
public int ReadPowerCount { get; private set; }
+ public int InputOnCount { get; private set; }
public bool IsConnected => true;
public string DisplayName => "it8702p";
public event EventHandler? ConnectionChanged;
@@ -134,7 +138,11 @@ public sealed class InterfacePowerTests
public Task DisconnectAsync() => Task.CompletedTask;
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
public Task SetValueAsync(double value, CancellationToken ct = default) => throw new InvalidOperationException();
- public Task InputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
+ public Task InputAsync(bool on, CancellationToken ct = default)
+ {
+ if (on) InputOnCount++;
+ return Task.CompletedTask;
+ }
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((28.0, 1.0));
public Task ReadPowerAsync(CancellationToken ct = default)
{
@@ -146,6 +154,7 @@ public sealed class InterfacePowerTests
private sealed class FixedPowerSupply : IPowerSupply
{
+ public int OutputOnCount { get; private set; }
public bool IsConnected => true;
public string DisplayName => "fixed power";
public event EventHandler? ConnectionChanged;
@@ -153,7 +162,11 @@ public sealed class InterfacePowerTests
public Task DisconnectAsync() => Task.CompletedTask;
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new InvalidOperationException();
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => throw new InvalidOperationException();
- public Task OutputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
+ public Task OutputAsync(bool on, CancellationToken ct = default)
+ {
+ if (on) OutputOnCount++;
+ return Task.CompletedTask;
+ }
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((30.0, 1.0));
public Task QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("fixed");
}
diff --git a/SSPCTester.Tests/ProtectTestTests.cs b/SSPCTester.Tests/ProtectTestTests.cs
index 3bf2810..798f80e 100644
--- a/SSPCTester.Tests/ProtectTestTests.cs
+++ b/SSPCTester.Tests/ProtectTestTests.cs
@@ -29,7 +29,7 @@ public sealed class ProtectTestTests
row => AssertProtection(row, 4, "过压", "≥32V", "28.193 V"),
row => AssertProtection(row, 5, "欠压", "≤22V", "28.193 V"),
row => AssertProtection(row, 6, "限流", "≥1.2A", "1.049 A"));
- Assert.All(project.ProtectResults, row => Assert.Equal(TestStatus.Measured, row.Status));
+ Assert.All(project.ProtectResults, row => Assert.Equal(TestStatus.Untested, row.Status));
Assert.Contains(progress, x => x.ModuleName == ProtectTest.ModuleName);
}
diff --git a/SSPCTester.Tests/ReportFileHelperTests.cs b/SSPCTester.Tests/ReportFileHelperTests.cs
index 8273497..0f36e88 100644
--- a/SSPCTester.Tests/ReportFileHelperTests.cs
+++ b/SSPCTester.Tests/ReportFileHelperTests.cs
@@ -57,11 +57,11 @@ public sealed class ReportFileHelperTests
[Fact]
public void ResolveFileName_UsesFallbackForBlankTemplate()
{
- var project = new Project { Report = { FileName = " " } };
+ var project = new Project { Name = "ProjectA", Report = { FileName = " " } };
string fileName = ReportFileHelper.ResolveFileName(project, new DateTime(2026, 6, 28, 1, 2, 3));
- Assert.Equal("report_20260628_010203.pdf", fileName);
+ Assert.Equal("ProjectA_20260628_010203.pdf", fileName);
}
[Theory]
diff --git a/SSPCTester.Tests/ReportLayoutTests.cs b/SSPCTester.Tests/ReportLayoutTests.cs
index 48831cf..2debcbf 100644
--- a/SSPCTester.Tests/ReportLayoutTests.cs
+++ b/SSPCTester.Tests/ReportLayoutTests.cs
@@ -59,7 +59,8 @@ public sealed class ReportLayoutTests
Channel = channel,
VoltageOn = failed ? 0.12 : 28.01,
CurrentOn = failed ? 0 : 1.002,
- VoltageOff = 0.01,
+ VoltageOff = 28.01,
+ CurrentOff = failed ? 0.05 : 0,
Status = failed ? TestStatus.Fail : TestStatus.Pass
});
project.CurveResults.Add(new CurveResultRow
diff --git a/SSPCTester.Tests/WordReportGeneratorTests.cs b/SSPCTester.Tests/WordReportGeneratorTests.cs
index db005b0..0a44113 100644
--- a/SSPCTester.Tests/WordReportGeneratorTests.cs
+++ b/SSPCTester.Tests/WordReportGeneratorTests.cs
@@ -31,6 +31,7 @@ public sealed class WordReportGeneratorTests
Assert.Contains("基础测试", text);
Assert.Contains("功率损耗", text);
Assert.Contains("PROTECTION FUNCTION", text);
+ Assert.Contains("未判定", text);
var fontNames = document.MainDocumentPart.Document.Descendants()
.SelectMany(x => new[] { x.Ascii?.Value, x.HighAnsi?.Value, x.EastAsia?.Value })
@@ -88,7 +89,8 @@ public sealed class WordReportGeneratorTests
Channel = 1,
VoltageOn = 28.0,
CurrentOn = 1.0,
- VoltageOff = 0.01,
+ VoltageOff = 28.0,
+ CurrentOff = 0.0,
Status = TestStatus.Pass
});
project.CurveResults.Add(new CurveResultRow
@@ -115,7 +117,7 @@ public sealed class WordReportGeneratorTests
ProtectType = "短路",
Trigger = "输出短路",
TripTimeMs = 2.1,
- Status = TestStatus.Pass
+ Status = TestStatus.Untested
});
return project;
}
diff --git a/SSPCTester.UI/Controls/CurvePlotControl.xaml b/SSPCTester.UI/Controls/CurvePlotControl.xaml
index fa61701..1d7f0db 100644
--- a/SSPCTester.UI/Controls/CurvePlotControl.xaml
+++ b/SSPCTester.UI/Controls/CurvePlotControl.xaml
@@ -1,10 +1,16 @@
-
+
+
+
+
+
diff --git a/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs b/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs
index aa75fd9..bac2b0f 100644
--- a/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs
+++ b/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs
@@ -1,16 +1,27 @@
+using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using ScottPlot;
+using ScottPlot.WPF;
using SSPCTester.Devices.Interfaces;
namespace SSPCTester.UI.Controls;
public partial class CurvePlotControl : UserControl
{
+ private readonly bool _isDesignMode;
+ private WpfPlot? _plot;
+
public CurvePlotControl()
{
InitializeComponent();
+ _isDesignMode = DesignerProperties.GetIsInDesignMode(this);
+ if (_isDesignMode)
+ {
+ DesignPlaceholder.Visibility = Visibility.Visible;
+ return;
+ }
Loaded += (_, _) => InitPlot();
}
@@ -42,30 +53,39 @@ public partial class CurvePlotControl : UserControl
private void InitPlot()
{
- Plot.Plot.Title("");
- Plot.Plot.XLabel("t (ms)");
- Plot.Plot.YLabel("V");
- Plot.Plot.Axes.AntiAlias(true);
- Plot.Plot.FigureBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
- Plot.Plot.DataBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
- Plot.Plot.Grid.MajorLineColor = ScottPlot.Color.FromHex("#DFE4EA");
+ _plot ??= CreateRuntimePlot();
+ _plot.Plot.Title("");
+ _plot.Plot.XLabel("t (ms)");
+ _plot.Plot.YLabel("V");
+ _plot.Plot.Axes.AntiAlias(true);
+ _plot.Plot.FigureBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
+ _plot.Plot.DataBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
+ _plot.Plot.Grid.MajorLineColor = ScottPlot.Color.FromHex("#DFE4EA");
Render();
}
+ private WpfPlot CreateRuntimePlot()
+ {
+ var plot = new WpfPlot();
+ RootContainer.Children.Clear();
+ RootContainer.Children.Add(plot);
+ return plot;
+ }
+
private void Render()
{
- if (!IsLoaded || Plot == null) return;
- Plot.Plot.Clear();
+ if (_isDesignMode || !IsLoaded || _plot == null) return;
+ _plot.Plot.Clear();
var c = Curve;
if (c == null || c.Voltage.Length == 0)
{
- Plot.Refresh();
+ _plot.Refresh();
return;
}
var points = BuildViewPoints(c);
if (points.Count == 0)
{
- Plot.Refresh();
+ _plot.Refresh();
return;
}
@@ -78,31 +98,31 @@ public partial class CurvePlotControl : UserControl
ys[i] = points[i].Voltage;
}
var color = IsRising ? "#1A73C4" : "#D23B3B";
- var sig = Plot.Plot.Add.ScatterLine(xs, ys, ScottPlot.Color.FromHex(color));
+ var sig = _plot.Plot.Add.ScatterLine(xs, ys, ScottPlot.Color.FromHex(color));
sig.LineWidth = 2;
sig.MarkerSize = 0;
// 阈值线
double threshold = IsRising ? VMax * 0.9 : VMax * 0.1;
- var hLine = Plot.Plot.Add.HorizontalLine(threshold);
+ var hLine = _plot.Plot.Add.HorizontalLine(threshold);
hLine.Color = ScottPlot.Color.FromHex("#98A1AD");
hLine.LinePattern = ScottPlot.LinePattern.Dashed;
hLine.LineWidth = 1;
- var label = Plot.Plot.Add.Text($"{(IsRising ? 90 : 10)}%", xs[0], threshold);
+ var label = _plot.Plot.Add.Text($"{(IsRising ? 90 : 10)}%", xs[0], threshold);
label.LabelFontColor = ScottPlot.Color.FromHex("#98A1AD");
label.LabelFontSize = 10;
// 触发线 t=0
if (!HasViewWindow(out double start, out double end) || (start <= 0 && end >= 0))
{
- var vLine = Plot.Plot.Add.VerticalLine(0);
+ var vLine = _plot.Plot.Add.VerticalLine(0);
vLine.Color = ScottPlot.Color.FromHex("#98A1AD");
vLine.LinePattern = ScottPlot.LinePattern.Dotted;
vLine.LineWidth = 1;
}
- Plot.Plot.Axes.AutoScale();
- Plot.Refresh();
+ _plot.Plot.Axes.AutoScale();
+ _plot.Refresh();
}
private List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve)
@@ -131,6 +151,9 @@ public partial class CurvePlotControl : UserControl
/// 导出为 PNG 字节。
public byte[] ExportPng(int width = 800, int height = 300)
{
- return Plot.Plot.GetImageBytes(width, height, ImageFormat.Png);
+ if (_plot == null)
+ return Array.Empty();
+
+ return _plot.Plot.GetImageBytes(width, height, ImageFormat.Png);
}
}
diff --git a/SSPCTester.UI/Converters/Converters.cs b/SSPCTester.UI/Converters/Converters.cs
index 83a832d..533aa32 100644
--- a/SSPCTester.UI/Converters/Converters.cs
+++ b/SSPCTester.UI/Converters/Converters.cs
@@ -57,7 +57,7 @@ public sealed class StatusToTextConverter : IValueConverter
TestStatus.Fail => "不合格",
TestStatus.Running => "测试中",
TestStatus.Measured => "已测",
- _ => "未测试"
+ _ => "未判定"
} : "—";
public object ConvertBack(object value, Type targetType, object? parameter, CultureInfo culture) => Binding.DoNothing;
}
@@ -75,6 +75,22 @@ public sealed class BoolToVisConverter : IValueConverter
=> value is Visibility v && v == Visibility.Visible;
}
+/// bool 取反;用于 IsEnabled 这类布尔属性。
+public sealed class InverseBoolConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ bool b = value is bool x && x;
+ return !b;
+ }
+
+ public object ConvertBack(object value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ bool b = value is bool x && x;
+ return !b;
+ }
+}
+
/// null → Collapsed,非 null → Visible(参数 "Invert" 取反)。
public sealed class NullToVisConverter : IValueConverter
{
diff --git a/SSPCTester.UI/Resources/Controls.xaml b/SSPCTester.UI/Resources/Controls.xaml
index cfe44ea..b9e54a8 100644
--- a/SSPCTester.UI/Resources/Controls.xaml
+++ b/SSPCTester.UI/Resources/Controls.xaml
@@ -7,6 +7,7 @@
+
diff --git a/SSPCTester.UI/Services/SafetyMonitorService.cs b/SSPCTester.UI/Services/SafetyMonitorService.cs
index add1e8c..a2355c2 100644
--- a/SSPCTester.UI/Services/SafetyMonitorService.cs
+++ b/SSPCTester.UI/Services/SafetyMonitorService.cs
@@ -16,6 +16,7 @@ public sealed class SafetyMonitorService : IDisposable
private readonly SemaphoreSlim _gate = new(1, 1);
private Timer? _timer;
private bool _handlingEmergency;
+ private bool _softwareEmergencyActive;
private bool _disposed;
public SafetyMonitorService(
@@ -56,18 +57,66 @@ public sealed class SafetyMonitorService : IDisposable
SetStatus(false, false, "设备正常", "正在监控 PCI2312 急停输入");
}
- public async Task ResetAsync(CancellationToken ct = default)
+ 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 TryShutdownPowerAsync().ConfigureAwait(false);
+ await TryShutdownLoadAsync().ConfigureAwait(false);
+ SetStatus(true, true, "软件急停已触发", "报警灯已接通,电源输出和负载输入已发送关闭指令");
+ }
+ 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 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)
{
var options = _options.Pci2312;
+ await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false);
if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false))
{
- SetStatus(true, IsAlarmOutputOn, "急停已触发", "急停输入仍处于接通状态,无法复位报警灯");
- return;
+ const string message = "硬件急停按钮仍处于触发状态,无法执行状态重置。请先旋转或释放设备上的急停按钮,确认急停输入复位后再重试。";
+ SetStatus(true, IsAlarmOutputOn, "急停已触发", message);
+ return new SafetyResetResult(false, message);
}
await SetAlarmOutputAsync(false, ct).ConfigureAwait(false);
- SetStatus(false, false, "设备正常", "急停已复位,正在监控 PCI2312 输入");
+ _softwareEmergencyActive = false;
_handlingEmergency = false;
+ SetStatus(false, false, "设备正常", "急停状态已重置,报警灯已关闭,正在监控 PCI2312 输入");
+ return new SafetyResetResult(true, "急停状态已重置。");
}
private void OnTimer(object? state)
@@ -92,11 +141,13 @@ public sealed class SafetyMonitorService : IDisposable
{
await HandleEmergencyAsync().ConfigureAwait(false);
}
+ else if (_softwareEmergencyActive)
+ {
+ SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "等待状态重置,电源和负载不会自动恢复");
+ }
else if (IsEmergencyActive || IsAlarmOutputOn)
{
- await SetAlarmOutputAsync(false, CancellationToken.None).ConfigureAwait(false);
- _handlingEmergency = false;
- SetStatus(false, false, "设备正常", $"DI{options.EmergencyInputChannel} 已复位,报警灯已关闭");
+ SetStatus(true, IsAlarmOutputOn, "急停已触发", $"DI{options.EmergencyInputChannel} 已复位,请点击状态重置关闭报警灯");
}
else
{
@@ -185,6 +236,16 @@ public sealed class SafetyMonitorService : IDisposable
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;
@@ -201,3 +262,5 @@ public sealed class SafetyMonitorService : IDisposable
_gate.Dispose();
}
}
+
+public sealed record SafetyResetResult(bool Success, string Message);
diff --git a/SSPCTester.UI/ViewModels/BasicTestViewModel.cs b/SSPCTester.UI/ViewModels/BasicTestViewModel.cs
index f74cd6b..dd840aa 100644
--- a/SSPCTester.UI/ViewModels/BasicTestViewModel.cs
+++ b/SSPCTester.UI/ViewModels/BasicTestViewModel.cs
@@ -87,8 +87,13 @@ public partial class BasicTestViewModel : ObservableObject
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;
+ bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
+ row.Status = onOk ? TestStatus.Running : TestStatus.Fail;
StartPolling(row.Channel);
- _log.Add(UiLogLevel.Info, $"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A");
+ _log.Add(onOk ? UiLogLevel.Info : UiLogLevel.Error,
+ $"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A {(onOk ? "开状态合格" : "开状态不合格")}");
}
catch (Exception ex)
{
@@ -112,12 +117,15 @@ public partial class BasicTestViewModel : ObservableObject
await Task.Delay(SwitchSettleDelayMs);
var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel);
row.VoltageOff = offResult.Measurement.Voltage;
- bool onOk = BasicTestCriteria.IsOn(row.VoltageOn);
+ row.CurrentOff = Math.Abs(offResult.Measurement.Current);
+ row.DisplayVoltage = row.VoltageOff;
+ row.DisplayCurrent = row.CurrentOff;
+ bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail;
string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
string offState = offResult.IsStableOff ? "已稳定关断" : "关断超时";
_log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error,
- $"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V {offState} {verdict}");
+ $"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V I={row.CurrentOff:F3}A {offState} {verdict}");
}
catch (Exception ex)
{
@@ -191,24 +199,37 @@ public partial class BasicTestViewModel : ObservableObject
if (!_sspc.IsConnected) await _sspc.ConnectAsync(ct);
row.Status = TestStatus.Running;
+ _log.Add(UiLogLevel.Info, $"CH{row.Channel} 基础测试开始:执行开状态测试。");
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;
+ 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 ? "开状态合格" : "开状态不合格")}");
StartPolling(row.Channel);
ct.ThrowIfCancellationRequested();
await StopPollingAsync(row.Channel);
+ _log.Add(UiLogLevel.Info, $"CH{row.Channel} 执行关状态测试。");
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;
- bool onOk = BasicTestCriteria.IsOn(row.VoltageOn);
+ row.CurrentOff = Math.Abs(offResult.Measurement.Current);
+ row.DisplayVoltage = row.VoltageOff;
+ row.DisplayCurrent = row.CurrentOff;
row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail;
+ string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
+ string offState = offResult.IsStableOff ? "关状态合格" : "关状态不合格";
+ _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}");
}
[RelayCommand]
@@ -216,7 +237,8 @@ public partial class BasicTestViewModel : ObservableObject
{
foreach (var r in Rows)
{
- r.VoltageOn = 0; r.CurrentOn = 0; r.VoltageOff = 0;
+ r.VoltageOn = 0; r.CurrentOn = 0; r.VoltageOff = 0; r.CurrentOff = 0;
+ r.DisplayVoltage = 0; r.DisplayCurrent = 0;
r.Status = TestStatus.Untested;
}
}
@@ -302,6 +324,8 @@ public partial class BasicTestViewModel : ObservableObject
row.VoltageOn = measurement.Voltage;
row.CurrentOn = Math.Abs(measurement.Current);
+ row.DisplayVoltage = row.VoltageOn;
+ row.DisplayCurrent = row.CurrentOn;
}
}
}
diff --git a/SSPCTester.UI/ViewModels/CurveTestViewModel.cs b/SSPCTester.UI/ViewModels/CurveTestViewModel.cs
index 8bead82..b7b7d61 100644
--- a/SSPCTester.UI/ViewModels/CurveTestViewModel.cs
+++ b/SSPCTester.UI/ViewModels/CurveTestViewModel.cs
@@ -82,6 +82,13 @@ public partial class CurveTestViewModel : ObservableObject
_ = LoadSelectedChannelCurvesAsync();
}
+ partial void OnSelectedRowChanged(CurveResultRow? value)
+ {
+ if (_syncingViewWindow || value == null) return;
+ if (SelectedChannel != value.Channel)
+ SelectedChannel = value.Channel;
+ }
+
partial void OnRisingViewStartMsChanged(double? value)
{
if (_syncingViewWindow || SelectedRow == null) return;
@@ -117,10 +124,10 @@ public partial class CurveTestViewModel : ObservableObject
{
SelectedRow = Rows.FirstOrDefault(x => x.Channel == SelectedChannel)
?? Project?.CurveResults.FirstOrDefault(x => x.Channel == SelectedChannel);
- RisingViewStartMs = SelectedRow?.RisingViewStartMs ?? SelectedRow?.ViewStartMs;
- RisingViewEndMs = SelectedRow?.RisingViewEndMs ?? SelectedRow?.ViewEndMs;
- FallingViewStartMs = SelectedRow?.FallingViewStartMs ?? SelectedRow?.ViewStartMs;
- FallingViewEndMs = SelectedRow?.FallingViewEndMs ?? SelectedRow?.ViewEndMs;
+ RisingViewStartMs = SelectedRow?.RisingViewStartMs;
+ RisingViewEndMs = SelectedRow?.RisingViewEndMs;
+ FallingViewStartMs = SelectedRow?.FallingViewStartMs;
+ FallingViewEndMs = SelectedRow?.FallingViewEndMs;
}
finally
{
@@ -228,9 +235,9 @@ public partial class CurveTestViewModel : ObservableObject
}
[RelayCommand]
- private void OpenPopup()
+ private void OpenPopup(string? curveKind)
{
- var win = new Views.CurvePopupWindow(this) { Owner = System.Windows.Application.Current?.MainWindow };
+ var win = new Views.CurvePopupWindow(this, curveKind) { Owner = System.Windows.Application.Current?.MainWindow };
win.Show();
}
diff --git a/SSPCTester.UI/ViewModels/MainViewModel.cs b/SSPCTester.UI/ViewModels/MainViewModel.cs
index f3dc16a..fcbc0e1 100644
--- a/SSPCTester.UI/ViewModels/MainViewModel.cs
+++ b/SSPCTester.UI/ViewModels/MainViewModel.cs
@@ -30,8 +30,6 @@ public partial class MainViewModel : ObservableObject
private readonly UiLogSink _logSink;
private readonly DeviceOptions _deviceOptions;
private readonly DispatcherTimer _deviceMonitorTimer;
- private readonly DispatcherTimer _safetyReadingsTimer;
- private bool _safetyReadingsBusy;
private object? _viewBeforeSettings;
public MainViewModel(
@@ -54,8 +52,6 @@ public partial class MainViewModel : ObservableObject
_deviceMonitorTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_deviceMonitorTimer.Tick += (_, _) => RefreshDeviceStatus();
_deviceMonitorTimer.Start();
- _safetyReadingsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
- _safetyReadingsTimer.Tick += async (_, _) => await RefreshSafetyReadingsAsync().ConfigureAwait(true);
AllProjects = new ObservableCollection();
TreeRoots = new ObservableCollection();
@@ -81,8 +77,8 @@ public partial class MainViewModel : ObservableObject
[ObservableProperty] private object? _currentView;
[ObservableProperty] private bool _devicesConnected;
[ObservableProperty] private bool _devicePanelOpen;
- [ObservableProperty] private bool _safetyPanelOpen;
[ObservableProperty] private bool _deviceOperationBusy;
+ [ObservableProperty] private bool _emergencyOperationBusy;
[ObservableProperty] private string _deviceSummary = "设备状态检查中";
[ObservableProperty] private string _sspcConnectionStatus = "检查中";
[ObservableProperty] private string _powerConnectionStatus = "检查中";
@@ -91,26 +87,13 @@ public partial class MainViewModel : ObservableObject
[ObservableProperty] private string _pci2312ConnectionStatus = "检查中";
[ObservableProperty] private string _safetyStatusText = "设备故障";
[ObservableProperty] private string _safetyDetailText = "设备安全监控未启动";
- [ObservableProperty] private string _safetyPowerCurrentText = "--";
- [ObservableProperty] private string _safetyLoadCurrentText = "--";
[ObservableProperty] private bool _alarmLampActive;
+ [ObservableProperty] private string _emergencyButtonText = "报警急停(状态重置)";
[ObservableProperty] private string _testingMessage = "";
[ObservableProperty] private TestSessionViewModel? _currentSession;
partial void OnStateChanged(AppState value) => RebuildTree();
partial void OnCurrentProjectChanged(Project? value) => RebuildTree();
- partial void OnSafetyPanelOpenChanged(bool value)
- {
- if (value)
- {
- _safetyReadingsTimer.Start();
- _ = RefreshSafetyReadingsAsync();
- }
- else
- {
- _safetyReadingsTimer.Stop();
- }
- }
private async Task ConnectConfiguredDevicesAsync()
{
@@ -180,55 +163,11 @@ public partial class MainViewModel : ObservableObject
{
SafetyStatusText = _safetyMonitor.StatusText;
SafetyDetailText = _safetyMonitor.DetailText;
- AlarmLampActive = _safetyMonitor.IsEmergencyActive || _safetyMonitor.IsAlarmOutputOn;
+ AlarmLampActive = _safetyMonitor.IsAlarmOutputOn;
+ EmergencyButtonText = AlarmLampActive ? "状态重置" : "报警急停";
});
}
- private async Task RefreshSafetyReadingsAsync()
- {
- if (_safetyReadingsBusy) return;
- _safetyReadingsBusy = true;
- try
- {
- SafetyPowerCurrentText = await ReadPowerCurrentTextAsync().ConfigureAwait(true);
- SafetyLoadCurrentText = await ReadLoadCurrentTextAsync().ConfigureAwait(true);
- }
- finally
- {
- _safetyReadingsBusy = false;
- }
- }
-
- private async Task ReadPowerCurrentTextAsync()
- {
- if (!_power.IsConnected) return "未连接";
- try
- {
- var (_, amps) = await _power.ReadAsync().ConfigureAwait(true);
- return $"{amps:F3} A";
- }
- catch (Exception ex)
- {
- _logger.LogDebug(ex, "Read UDP5080 current for safety panel failed.");
- return "读取失败";
- }
- }
-
- private async Task ReadLoadCurrentTextAsync()
- {
- if (!_load.IsConnected) return "未连接";
- try
- {
- var (_, amps) = await _load.ReadAsync().ConfigureAwait(true);
- return $"{amps:F3} A";
- }
- catch (Exception ex)
- {
- _logger.LogDebug(ex, "Read IT8702P current for safety panel failed.");
- return "读取失败";
- }
- }
-
private static string DeviceStatusText(IDeviceConnection device) =>
device.IsConnected ? "已连接" : "未连接";
@@ -240,17 +179,50 @@ public partial class MainViewModel : ObservableObject
}
[RelayCommand]
- private void ToggleSafetyPanel()
+ private async Task ToggleEmergencyAlarm()
{
- RefreshSafetyStatus();
- SafetyPanelOpen = !SafetyPanelOpen;
- }
-
- [RelayCommand]
- private async Task ResetSafetyAlarm()
- {
- await _safetyMonitor.ResetAsync().ConfigureAwait(false);
- RefreshSafetyStatus();
+ if (EmergencyOperationBusy) return;
+ EmergencyOperationBusy = true;
+ try
+ {
+ if (AlarmLampActive)
+ {
+ var result = await _safetyMonitor.ResetAsync().ConfigureAwait(false);
+ RefreshSafetyStatus();
+ if (!result.Success)
+ {
+ Application.Current?.Dispatcher.Invoke(() =>
+ MessageBox.Show(
+ Application.Current?.MainWindow,
+ result.Message,
+ "状态重置受阻",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning));
+ }
+ }
+ else
+ {
+ await _safetyMonitor.TriggerSoftwareEmergencyAsync().ConfigureAwait(false);
+ RefreshSafetyStatus();
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Toggle emergency alarm failed.");
+ _logSink.Add(UiLogLevel.Error, $"报警急停操作失败:{ex.Message}");
+ Application.Current?.Dispatcher.Invoke(() =>
+ MessageBox.Show(
+ Application.Current?.MainWindow,
+ $"报警急停操作失败:{ex.Message}",
+ "报警急停",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error));
+ RefreshSafetyStatus();
+ }
+ finally
+ {
+ EmergencyOperationBusy = false;
+ }
}
[RelayCommand]
@@ -427,8 +399,7 @@ public partial class MainViewModel : ObservableObject
{
CurrentProject = p;
State = AppState.Testing;
- TestingMessage = $"测试进行中:{p.Name}";
-
+ TestingMessage = $"测试页面:{p.Name}";
var session = new TestSessionViewModel(_sp, p, _sspc, _power, _load, _daq, _logSink);
CurrentSession = session;
@@ -439,30 +410,31 @@ public partial class MainViewModel : ObservableObject
try
{
- await session.RunAllAsync();
- _logSink.Add(UiLogLevel.Success, "全部测试完成。");
- }
- catch (OperationCanceledException)
- {
- _logSink.Add(UiLogLevel.Warning, "测试已中止。");
+ await _store.SaveAsync(p);
+ _logSink.Add(UiLogLevel.Info, "已进入测试页面,请在各测试页手动执行当前项目。");
}
catch (Exception ex)
{
- _logger.LogError(ex, "Test failed.");
- _logSink.Add(UiLogLevel.Error, "测试出错:" + ex.Message);
- }
- finally
- {
- await _store.SaveAsync(p);
- State = AppState.Browsing;
- CurrentSession = null;
+ _logger.LogError(ex, "Failed to enter test view.");
+ _logSink.Add(UiLogLevel.Error, "进入测试页面出错:" + ex.Message);
}
}
[RelayCommand]
- private void AbortTest()
+ private async Task AbortTest()
{
CurrentSession?.Cancel();
+ if (CurrentView is TestHostViewModel host && host.Basic.IsBatchRunning)
+ await host.Basic.CancelRunAllCommand.ExecuteAsync(null);
+
+ if (CurrentProject != null)
+ await _store.SaveAsync(CurrentProject);
+
+ CurrentSession = null;
+ TestingMessage = string.Empty;
+ State = AppState.Browsing;
+ if (CurrentProject != null)
+ OpenProjectInfo(CurrentProject);
}
[RelayCommand]
diff --git a/SSPCTester.UI/ViewModels/PowerTestViewModel.cs b/SSPCTester.UI/ViewModels/PowerTestViewModel.cs
index 774f0a4..700582d 100644
--- a/SSPCTester.UI/ViewModels/PowerTestViewModel.cs
+++ b/SSPCTester.UI/ViewModels/PowerTestViewModel.cs
@@ -131,6 +131,9 @@ public partial class PowerTestViewModel : ObservableObject
throw new InvalidOperationException($"IT8702P connection failed: {ex.Message}", ex);
}
+ await _power.OutputAsync(true);
+ await _load.InputAsync(true);
+
double sVin = 0, sIin = 0, sVout = 0, sIout = 0, sPout = 0;
for (int s = 0; s < SamplesPerChannel; s++)
{
@@ -165,7 +168,7 @@ public partial class PowerTestViewModel : ObservableObject
row.MeasuredPowerOut = sPout / SamplesPerChannel;
row.Status = TestStatus.Measured;
Current = row;
- _log.Add(UiLogLevel.Success, $"CH{ch} 功率损耗 {row.PowerLossW:F3} W,效率 {row.EfficiencyPct:F3}%");
+ _log.Add(UiLogLevel.Success, $"CH{ch} 功率损耗 {row.PowerLossW:F3} W,效率 {row.EfficiencyPct:F1}%");
}
catch (Exception ex)
{
diff --git a/SSPCTester.UI/ViewModels/ProtectViewModel.cs b/SSPCTester.UI/ViewModels/ProtectViewModel.cs
index 5992192..4869531 100644
--- a/SSPCTester.UI/ViewModels/ProtectViewModel.cs
+++ b/SSPCTester.UI/ViewModels/ProtectViewModel.cs
@@ -61,8 +61,8 @@ public partial class ProtectViewModel : ObservableObject
"过压" or "欠压" => $"{volts:F3} V",
_ => $"{amps:F3} A"
};
- if (row.Status is TestStatus.Untested or TestStatus.Running)
- row.Status = TestStatus.Measured;
+ if (row.Status == TestStatus.Running)
+ row.Status = TestStatus.Untested;
_log.Add(UiLogLevel.Success, $"保护功能 {row.ProtectType} 实测值:{row.MeasuredValue}");
}
catch (Exception ex)
diff --git a/SSPCTester.UI/ViewModels/ReportViewModel.cs b/SSPCTester.UI/ViewModels/ReportViewModel.cs
index 415d2df..214f566 100644
--- a/SSPCTester.UI/ViewModels/ReportViewModel.cs
+++ b/SSPCTester.UI/ViewModels/ReportViewModel.cs
@@ -60,7 +60,8 @@ public partial class ReportViewModel : ObservableObject
{
Project = p;
if (string.IsNullOrWhiteSpace(p.Report.FileName) ||
- string.Equals(p.Report.FileName, "report.pdf", StringComparison.OrdinalIgnoreCase))
+ string.Equals(p.Report.FileName, "report.pdf", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(p.Report.FileName, "report_{yyyyMMdd_HHmmss}.pdf", StringComparison.OrdinalIgnoreCase))
{
p.Report.FileName = ReportFileHelper.DefaultTemplate;
}
@@ -318,15 +319,15 @@ public partial class ReportViewModel : ObservableObject
: CurveImageRenderer.RenderRisingChannel(
row.Channel,
rising,
- row.RisingViewStartMs ?? row.ViewStartMs,
- row.RisingViewEndMs ?? row.ViewEndMs),
+ row.RisingViewStartMs,
+ row.RisingViewEndMs),
falling == null
? null
: CurveImageRenderer.RenderFallingChannel(
row.Channel,
falling,
- row.FallingViewStartMs ?? row.ViewStartMs,
- row.FallingViewEndMs ?? row.ViewEndMs));
+ row.FallingViewStartMs,
+ row.FallingViewEndMs));
}
catch (Exception ex)
{
diff --git a/SSPCTester.UI/Views/BasicTestView.xaml b/SSPCTester.UI/Views/BasicTestView.xaml
index 80d8cf8..427bd9a 100644
--- a/SSPCTester.UI/Views/BasicTestView.xaml
+++ b/SSPCTester.UI/Views/BasicTestView.xaml
@@ -87,7 +87,7 @@
-
+
@@ -95,7 +95,7 @@
-
+
diff --git a/SSPCTester.UI/Views/CurvePopupWindow.xaml b/SSPCTester.UI/Views/CurvePopupWindow.xaml
index 12bc9d7..c0f12e2 100644
--- a/SSPCTester.UI/Views/CurvePopupWindow.xaml
+++ b/SSPCTester.UI/Views/CurvePopupWindow.xaml
@@ -3,20 +3,20 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctl="clr-namespace:SSPCTester.UI.Controls"
xmlns:vm="clr-namespace:SSPCTester.UI.ViewModels"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="曲线查看器"
Width="1100" Height="720"
MinWidth="600" MinHeight="400"
WindowStartupLocation="CenterOwner"
+ WindowState="Maximized"
Background="{StaticResource BgAppBrush}"
d:DataContext="{d:DesignInstance Type=vm:CurveTestViewModel}"
- xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
-
@@ -31,64 +31,26 @@
-
+
-
-
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/SSPCTester.UI/Views/CurvePopupWindow.xaml.cs b/SSPCTester.UI/Views/CurvePopupWindow.xaml.cs
index c76819e..9807717 100644
--- a/SSPCTester.UI/Views/CurvePopupWindow.xaml.cs
+++ b/SSPCTester.UI/Views/CurvePopupWindow.xaml.cs
@@ -1,14 +1,50 @@
using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using SSPCTester.UI.Controls;
using SSPCTester.UI.ViewModels;
namespace SSPCTester.UI.Views;
public partial class CurvePopupWindow : Window
{
- public CurvePopupWindow(CurveTestViewModel vm)
+ public CurvePopupWindow()
{
InitializeComponent();
+ }
+
+ public CurvePopupWindow(CurveTestViewModel vm, string? curveKind)
+ : this()
+ {
DataContext = vm;
+ ConfigureCurve(curveKind);
+ }
+
+ private void ConfigureCurve(string? curveKind)
+ {
+ bool isFalling = string.Equals(curveKind, "Falling", StringComparison.OrdinalIgnoreCase);
+ string title = isFalling ? "关断曲线(电压下降,10% 阈值)" : "开启曲线(电压上升,90% 阈值)";
+
+ Title = title;
+ TitleRun.Text = $"{title} ";
+ CurveTitleText.Text = title;
+ CurvePlot.IsRising = !isFalling;
+
+ CurvePlot.SetBinding(CurvePlotControl.CurveProperty, new Binding(isFalling ? "FallingCurve" : "RisingCurve"));
+ CurvePlot.SetBinding(CurvePlotControl.ViewStartMsProperty, new Binding(isFalling ? "FallingViewStartMs" : "RisingViewStartMs"));
+ CurvePlot.SetBinding(CurvePlotControl.ViewEndMsProperty, new Binding(isFalling ? "FallingViewEndMs" : "RisingViewEndMs"));
+
+ StartTextBox.SetBinding(TextBox.TextProperty, new Binding(isFalling ? "FallingViewStartMs" : "RisingViewStartMs")
+ {
+ UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
+ TargetNullValue = ""
+ });
+ EndTextBox.SetBinding(TextBox.TextProperty, new Binding(isFalling ? "FallingViewEndMs" : "RisingViewEndMs")
+ {
+ UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
+ TargetNullValue = ""
+ });
+ ResetButton.SetBinding(Button.CommandProperty, new Binding(isFalling ? "ResetFallingCurveViewCommand" : "ResetRisingCurveViewCommand"));
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
diff --git a/SSPCTester.UI/Views/CurveTestView.xaml b/SSPCTester.UI/Views/CurveTestView.xaml
index b180376..21590e6 100644
--- a/SSPCTester.UI/Views/CurveTestView.xaml
+++ b/SSPCTester.UI/Views/CurveTestView.xaml
@@ -54,12 +54,10 @@
+ IsEnabled="{Binding Busy, Converter={StaticResource InverseBoolConverter}}" />
-
+ IsEnabled="{Binding Busy, Converter={StaticResource InverseBoolConverter}}" />
@@ -92,7 +90,8 @@
+ Command="{Binding OpenPopupCommand}"
+ CommandParameter="Rising" />
+ Command="{Binding OpenPopupCommand}"
+ CommandParameter="Falling" />
-
+
diff --git a/SSPCTester.UI/Views/MainWindow.xaml b/SSPCTester.UI/Views/MainWindow.xaml
index 7c14007..6df516f 100644
--- a/SSPCTester.UI/Views/MainWindow.xaml
+++ b/SSPCTester.UI/Views/MainWindow.xaml
@@ -37,58 +37,152 @@
FontSize="{StaticResource FsTitle}" FontWeight="Bold" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/SSPCTester.UI/Views/MainWindow.xaml.cs b/SSPCTester.UI/Views/MainWindow.xaml.cs
index c14dd07..d158c8f 100644
--- a/SSPCTester.UI/Views/MainWindow.xaml.cs
+++ b/SSPCTester.UI/Views/MainWindow.xaml.cs
@@ -1,24 +1,31 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
+using System.ComponentModel;
using SSPCTester.UI.ViewModels;
namespace SSPCTester.UI.Views;
public partial class MainWindow : Window
{
- private readonly MainViewModel _vm;
+ private readonly MainViewModel? _vm;
- public MainWindow(MainViewModel vm)
+ public MainWindow()
{
InitializeComponent();
+ }
+
+ public MainWindow(MainViewModel vm) : this()
+ {
_vm = vm;
DataContext = vm;
- Loaded += (_, _) => _vm.Initialize();
+ if (!DesignerProperties.GetIsInDesignMode(this))
+ Loaded += (_, _) => _vm.Initialize();
}
private void ProjectTree_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
+ if (_vm == null) return;
if (sender is TreeView tv && tv.SelectedItem is ProjectNode node)
{
_vm.HandleNodeActivated(node);
diff --git a/SSPCTester.UI/Views/PowerTestView.xaml b/SSPCTester.UI/Views/PowerTestView.xaml
index 69c1685..1455c05 100644
--- a/SSPCTester.UI/Views/PowerTestView.xaml
+++ b/SSPCTester.UI/Views/PowerTestView.xaml
@@ -30,7 +30,7 @@
-
+
@@ -38,7 +38,7 @@
Command="{Binding TurnOffCurrentChannelCommand}"
IsEnabled="{Binding CanOperate}" />
-
@@ -161,7 +161,7 @@
+ Text="{Binding Current.EfficiencyPct, Converter={StaticResource NumberOrDash}, ConverterParameter=F1}" />
@@ -236,7 +236,7 @@
-
+
diff --git a/SSPCTester.UI/Views/ProtectView.xaml b/SSPCTester.UI/Views/ProtectView.xaml
index 4307b16..fb93a5a 100644
--- a/SSPCTester.UI/Views/ProtectView.xaml
+++ b/SSPCTester.UI/Views/ProtectView.xaml
@@ -90,7 +90,6 @@
MinWidth="90">
-
diff --git a/SSPCTester.UI/Views/SettingsManualView.xaml b/SSPCTester.UI/Views/SettingsManualView.xaml
index 9612492..42fee4c 100644
--- a/SSPCTester.UI/Views/SettingsManualView.xaml
+++ b/SSPCTester.UI/Views/SettingsManualView.xaml
@@ -9,8 +9,8 @@
-
-
+
+
@@ -28,15 +28,15 @@
-
-
-
-
-
+
+
+
+
+
-
+
@@ -57,7 +57,7 @@
-
+
@@ -76,7 +76,7 @@
-
+
@@ -95,7 +95,7 @@
-
+
@@ -116,37 +116,53 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SSPCTester.zip b/SSPCTester.zip
new file mode 100644
index 0000000..fbdab5c
Binary files /dev/null and b/SSPCTester.zip differ
diff --git a/publish.ps1 b/publish.ps1
index af0daac..962755c 100644
--- a/publish.ps1
+++ b/publish.ps1
@@ -34,6 +34,28 @@ param(
)
$ErrorActionPreference = 'Stop'
+
+function Initialize-ConsoleUtf8 {
+ try {
+ if ($IsWindows) {
+ & chcp.com 65001 > $null
+ }
+ } catch {
+ Write-Warning "无法切换到 UTF-8 代码页,将继续使用当前代码页。"
+ }
+
+ $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+ [Console]::InputEncoding = $utf8NoBom
+ [Console]::OutputEncoding = $utf8NoBom
+ Set-Variable -Name OutputEncoding -Value $utf8NoBom -Scope Global
+
+ if ([string]::IsNullOrWhiteSpace($env:DOTNET_CLI_UI_LANGUAGE)) {
+ $env:DOTNET_CLI_UI_LANGUAGE = 'zh-Hans'
+ }
+}
+
+Initialize-ConsoleUtf8
+
$root = $PSScriptRoot
Set-Location $root
diff --git a/run.ps1 b/run.ps1
index 4dace15..8807784 100644
--- a/run.ps1
+++ b/run.ps1
@@ -19,6 +19,28 @@ param(
)
$ErrorActionPreference = 'Stop'
+
+function Initialize-ConsoleUtf8 {
+ try {
+ if ($IsWindows) {
+ & chcp.com 65001 > $null
+ }
+ } catch {
+ Write-Warning "无法切换到 UTF-8 代码页,将继续使用当前代码页。"
+ }
+
+ $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+ [Console]::InputEncoding = $utf8NoBom
+ [Console]::OutputEncoding = $utf8NoBom
+ Set-Variable -Name OutputEncoding -Value $utf8NoBom -Scope Global
+
+ if ([string]::IsNullOrWhiteSpace($env:DOTNET_CLI_UI_LANGUAGE)) {
+ $env:DOTNET_CLI_UI_LANGUAGE = 'zh-Hans'
+ }
+}
+
+Initialize-ConsoleUtf8
+
$root = $PSScriptRoot
Set-Location $root