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 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - -