diff --git a/SSPCTester.Logic/Models/Results.cs b/SSPCTester.Logic/Models/Results.cs index fdddf24..46d4443 100644 --- a/SSPCTester.Logic/Models/Results.cs +++ b/SSPCTester.Logic/Models/Results.cs @@ -97,6 +97,12 @@ public partial class ProtectRow : ObservableObject [ObservableProperty] private double _tripTimeMs; [ObservableProperty] private TestStatus _status = TestStatus.Untested; + public bool CanMeasureFromPower => + ProtectType is "跳闸" or "过压" or "欠压" or "限流"; + + partial void OnProtectTypeChanged(string value) => + OnPropertyChanged(nameof(CanMeasureFromPower)); + public static string BuildMeasuredValue(string protectType, double? inputVoltage, double? inputCurrent) => protectType switch { "跳闸" when inputCurrent.HasValue => $"{inputCurrent.Value:F3} A", @@ -105,3 +111,63 @@ public partial class ProtectRow : ObservableObject _ => "----" }; } + +/// 保护功能固定测试项与结果初始化逻辑。 +public static class ProtectTestPlan +{ + public const string ManualJudgeMode = "人工判别"; + + public static readonly IReadOnlyList<(string ProtectType, string Trigger)> Items = new[] + { + ("跳闸", "1.5×额定电流"), + ("短路", "输出短路"), + ("过温", "≥85℃"), + ("过压", "≥32V"), + ("欠压", "≤22V"), + ("限流", "≥1.2A"), + }; + + public static void EnsureRows(Project project) + { + var existing = project.ProtectResults + .GroupBy(x => x.ProtectType) + .ToDictionary(x => x.Key, x => x.First()); + + project.ProtectResults.Clear(); + for (int i = 0; i < Items.Count; i++) + { + var item = Items[i]; + if (!existing.TryGetValue(item.ProtectType, out var row)) + { + row = new ProtectRow + { + ProtectType = item.ProtectType, + Status = TestStatus.Untested + }; + } + + row.Index = i + 1; + row.ProtectType = item.ProtectType; + if (string.IsNullOrWhiteSpace(row.Trigger)) + row.Trigger = item.Trigger; + if (string.IsNullOrWhiteSpace(row.JudgeMode)) + row.JudgeMode = ManualJudgeMode; + + project.ProtectResults.Add(row); + } + + RefreshMeasuredValues(project, overwriteExisting: false); + } + + public static void RefreshMeasuredValues(Project project, bool overwriteExisting = true) + { + var latestPower = project.PowerResults.LastOrDefault(); + foreach (var row in project.ProtectResults) + { + if (!overwriteExisting && !string.IsNullOrWhiteSpace(row.MeasuredValue) && row.MeasuredValue != "----") + continue; + + row.MeasuredValue = ProtectRow.BuildMeasuredValue(row.ProtectType, latestPower?.Vin, latestPower?.Iin); + } + } +} diff --git a/SSPCTester.Logic/Report/ReportGenerator.cs b/SSPCTester.Logic/Report/ReportGenerator.cs index 8817d41..c7ecba3 100644 --- a/SSPCTester.Logic/Report/ReportGenerator.cs +++ b/SSPCTester.Logic/Report/ReportGenerator.cs @@ -98,7 +98,7 @@ internal sealed class LegacyReportGenerator if (sel.Protect && (p.ProtectResults.Count > 0 || HasManualNote(p.TestFunction?.Protect))) { - Section(col, "7. 保护功能(框架占位)"); + Section(col, "7. 保护功能"); if (p.ProtectResults.Count > 0) col.Item().Element(e => ProtectTable(e, p)); ManualNote(col, p.TestFunction?.Protect); } diff --git a/SSPCTester.Logic/Testing/ProtectTest.cs b/SSPCTester.Logic/Testing/ProtectTest.cs index 624c67a..e93b037 100644 --- a/SSPCTester.Logic/Testing/ProtectTest.cs +++ b/SSPCTester.Logic/Testing/ProtectTest.cs @@ -2,47 +2,30 @@ using SSPCTester.Logic.Models; namespace SSPCTester.Logic.Testing; -/// -/// 保护功能测试:当前阶段仅生成框架占位结果,不真正触发故障。 -/// +/// 保护功能测试:刷新固定保护测试项的实测值与测量状态。 public sealed class ProtectTest : ITestModule { - public string Name => "保护功能"; - - private static readonly (string Type, string Trigger)[] _items = new[] - { - ("跳闸", "1.5×额定电流"), - ("短路", "输出短路"), - ("过温", "≥85℃"), - ("过压", "≥32V"), - ("欠压", "≤22V"), - ("限流", "≥1.2A"), - }; + public const string ModuleName = "保护功能"; + public string Name => ModuleName; public async Task RunAsync(TestContext ctx, IProgress progress, CancellationToken ct) { - ctx.Project.ProtectResults.Clear(); - var latestPower = ctx.Project.PowerResults.LastOrDefault(); - for (int i = 0; i < _items.Length; i++) + ProtectTestPlan.EnsureRows(ctx.Project); + + for (int i = 0; i < ctx.Project.ProtectResults.Count; i++) { ct.ThrowIfCancellationRequested(); - var row = new ProtectRow - { - Index = i + 1, - ProtectType = _items[i].Type, - Trigger = _items[i].Trigger, - MeasuredValue = ProtectRow.BuildMeasuredValue(_items[i].Type, latestPower?.Vin, latestPower?.Iin), - JudgeMode = "人工判别", - Status = TestStatus.Running - }; - ctx.Project.ProtectResults.Add(row); - progress.Report(new TestProgress { ModuleName = Name, Step = i, TotalSteps = _items.Length, Message = $"框架占位:{row.ProtectType}" }); + var row = ctx.Project.ProtectResults[i]; + bool preserveManualJudgement = row.Status is TestStatus.Pass or TestStatus.Fail; + if (!preserveManualJudgement) + row.Status = TestStatus.Running; + ProtectTestPlan.RefreshMeasuredValues(ctx.Project, overwriteExisting: false); + progress.Report(new TestProgress { ModuleName = Name, Step = i, TotalSteps = ctx.Project.ProtectResults.Count, Message = $"保护项测量:{row.ProtectType}" }); await Task.Delay(150, ct).ConfigureAwait(false); - // 框架占位:固定演示数据 - row.TripTimeMs = 2.0 + i * 0.3; - row.Status = TestStatus.Pass; - progress.Report(new TestProgress { ModuleName = Name, Step = i + 1, TotalSteps = _items.Length, StepStatus = row.Status }); + if (!preserveManualJudgement && row.Status == TestStatus.Running) + row.Status = TestStatus.Measured; + progress.Report(new TestProgress { ModuleName = Name, Step = i + 1, TotalSteps = ctx.Project.ProtectResults.Count, StepStatus = row.Status }); } } } diff --git a/SSPCTester.Tests/ProtectTestTests.cs b/SSPCTester.Tests/ProtectTestTests.cs new file mode 100644 index 0000000..3bf2810 --- /dev/null +++ b/SSPCTester.Tests/ProtectTestTests.cs @@ -0,0 +1,72 @@ +using SSPCTester.Logic.Models; +using SSPCTester.Logic.Testing; + +namespace SSPCTester.Tests; + +public sealed class ProtectTestTests +{ + [Fact] + public async Task ProtectTest_EnsuresSixProtectionRowsAndRefreshesMeasuredValues() + { + var project = new Project(); + project.PowerResults.Add(new PowerLossRow + { + Vin = 28.193, + Iin = 1.049, + Status = TestStatus.Pass + }); + + var progress = new List(); + var context = new TestContext(project, null!, null!, null!, null!); + + await new ProtectTest().RunAsync(context, new InlineProgress(progress.Add), CancellationToken.None); + + Assert.Equal(6, project.ProtectResults.Count); + Assert.Collection(project.ProtectResults, + row => AssertProtection(row, 1, "跳闸", "1.5×额定电流", "1.049 A"), + row => AssertProtection(row, 2, "短路", "输出短路", "----"), + row => AssertProtection(row, 3, "过温", "≥85℃", "----"), + 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.Contains(progress, x => x.ModuleName == ProtectTest.ModuleName); + } + + [Fact] + public async Task ProtectTest_PreservesEditedTriggerAndMeasuredValue() + { + var project = new Project(); + project.ProtectResults.Add(new ProtectRow + { + ProtectType = "跳闸", + Trigger = "用户触发条件", + MeasuredValue = "1.234 A", + JudgeMode = ProtectTestPlan.ManualJudgeMode, + Status = TestStatus.Pass + }); + + var context = new TestContext(project, null!, null!, null!, null!); + + await new ProtectTest().RunAsync(context, new InlineProgress(_ => { }), CancellationToken.None); + + var trip = project.ProtectResults.Single(x => x.ProtectType == "跳闸"); + Assert.Equal("用户触发条件", trip.Trigger); + Assert.Equal("1.234 A", trip.MeasuredValue); + Assert.Equal(TestStatus.Pass, trip.Status); + } + + private static void AssertProtection(ProtectRow row, int index, string type, string trigger, string measured) + { + Assert.Equal(index, row.Index); + Assert.Equal(type, row.ProtectType); + Assert.Equal(trigger, row.Trigger); + Assert.Equal(measured, row.MeasuredValue); + Assert.Equal(ProtectTestPlan.ManualJudgeMode, row.JudgeMode); + } + + private sealed class InlineProgress(Action handler) : IProgress + { + public void Report(TestProgress value) => handler(value); + } +} diff --git a/SSPCTester.Tests/WordReportGeneratorTests.cs b/SSPCTester.Tests/WordReportGeneratorTests.cs index 49ed0c4..db005b0 100644 --- a/SSPCTester.Tests/WordReportGeneratorTests.cs +++ b/SSPCTester.Tests/WordReportGeneratorTests.cs @@ -30,6 +30,7 @@ public sealed class WordReportGeneratorTests Assert.Contains("ProjectA", text); Assert.Contains("基础测试", text); Assert.Contains("功率损耗", text); + Assert.Contains("PROTECTION FUNCTION", text); var fontNames = document.MainDocumentPart.Document.Descendants() .SelectMany(x => new[] { x.Ascii?.Value, x.HighAnsi?.Value, x.EastAsia?.Value }) diff --git a/SSPCTester.UI/ViewModels/ProtectViewModel.cs b/SSPCTester.UI/ViewModels/ProtectViewModel.cs index 1a308f1..5992192 100644 --- a/SSPCTester.UI/ViewModels/ProtectViewModel.cs +++ b/SSPCTester.UI/ViewModels/ProtectViewModel.cs @@ -1,87 +1,73 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using SSPCTester.Devices.Interfaces; using SSPCTester.Logic.Models; using SSPCTester.UI.Services; namespace SSPCTester.UI.ViewModels; -/// 保护功能页 VM(框架占位)。 +/// 保护功能页 VM。 public partial class ProtectViewModel : ObservableObject { + private readonly IPowerSupply _power; private readonly UiLogSink _log; + private TestSessionViewModel? _session; public ObservableCollection Rows { get; } = new(); [ObservableProperty] private Project? _project; - [ObservableProperty] private bool _enableTrip = true; - [ObservableProperty] private bool _enableShort = true; - [ObservableProperty] private bool _enableOverTemp = true; - [ObservableProperty] private bool _enableOverVolt = true; - [ObservableProperty] private bool _enableUnderVolt = true; - [ObservableProperty] private bool _enableLimit = true; - [ObservableProperty] private double _overVoltThreshold = 32; - [ObservableProperty] private double _underVoltThreshold = 22; - [ObservableProperty] private double _overCurrentThreshold = 1.2; - [ObservableProperty] private double _overTempThreshold = 85; - - public ProtectViewModel(UiLogSink log) { _log = log; } + public ProtectViewModel(IPowerSupply power, UiLogSink log) + { + _power = power; + _log = log; + } public void SetProject(Project p) { Project = p; + ProtectTestPlan.EnsureRows(p); SyncRowsFromProject(); } + public void AttachSession(TestSessionViewModel session) + { + if (_session != null) + _session.ProtectResultsChanged -= SyncRowsFromProject; + + _session = session; + _session.ProtectResultsChanged += SyncRowsFromProject; + } + private void SyncRowsFromProject() { if (Project == null) return; - var latestPower = Project.PowerResults.LastOrDefault(); - for (int i = 0; i < Project.ProtectResults.Count; i++) - { - var row = Project.ProtectResults[i]; - row.Index = i + 1; - row.MeasuredValue = ProtectRow.BuildMeasuredValue(row.ProtectType, latestPower?.Vin, latestPower?.Iin); - if (string.IsNullOrWhiteSpace(row.JudgeMode)) - row.JudgeMode = "人工判别"; - } - + ProtectTestPlan.RefreshMeasuredValues(Project, overwriteExisting: false); Rows.Clear(); foreach (var row in Project.ProtectResults) Rows.Add(row); } [RelayCommand] - private void RunDemo() + private async Task MeasurePowerAsync(ProtectRow? row) { - if (Project == null) return; - Project.ProtectResults.Clear(); - Rows.Clear(); - var latestPower = Project.PowerResults.LastOrDefault(); - var items = new (string, string)[] + if (row == null || !row.CanMeasureFromPower) return; + + try { - ("跳闸", "1.5x额定电流"), - ("短路", "输出短路"), - ("过温", $">={OverTempThreshold} ℃"), - ("过压", $">={OverVoltThreshold} V"), - ("欠压", $"<={UnderVoltThreshold} V"), - ("限流", $">={OverCurrentThreshold} A"), - }; - for (int i = 0; i < items.Length; i++) - { - var r = new ProtectRow + var (volts, amps) = await _power.ReadAsync(); + row.MeasuredValue = row.ProtectType switch { - Index = i + 1, - ProtectType = items[i].Item1, - Trigger = items[i].Item2, - MeasuredValue = ProtectRow.BuildMeasuredValue(items[i].Item1, latestPower?.Vin, latestPower?.Iin), - JudgeMode = "人工判别", - TripTimeMs = 2 + i * 0.3, - Status = TestStatus.Pass + "过压" or "欠压" => $"{volts:F3} V", + _ => $"{amps:F3} A" }; - Project.ProtectResults.Add(r); - Rows.Add(r); + if (row.Status is TestStatus.Untested or TestStatus.Running) + row.Status = TestStatus.Measured; + _log.Add(UiLogLevel.Success, $"保护功能 {row.ProtectType} 实测值:{row.MeasuredValue}"); + } + catch (Exception ex) + { + _log.Add(UiLogLevel.Error, $"读取电源失败:{ex.Message}"); } - _log.Add(UiLogLevel.Warning, "保护功能模块仅生成框架演示数据。"); } } diff --git a/SSPCTester.UI/ViewModels/TestHostViewModel.cs b/SSPCTester.UI/ViewModels/TestHostViewModel.cs index d63c424..bdfad59 100644 --- a/SSPCTester.UI/ViewModels/TestHostViewModel.cs +++ b/SSPCTester.UI/ViewModels/TestHostViewModel.cs @@ -32,5 +32,6 @@ public partial class TestHostViewModel : ObservableObject { Session = s; Curve.AttachSession(s); + Protect.AttachSession(s); } } diff --git a/SSPCTester.UI/ViewModels/TestSessionViewModel.cs b/SSPCTester.UI/ViewModels/TestSessionViewModel.cs index 777235b..7af757b 100644 --- a/SSPCTester.UI/ViewModels/TestSessionViewModel.cs +++ b/SSPCTester.UI/ViewModels/TestSessionViewModel.cs @@ -24,6 +24,7 @@ public sealed class TestSessionViewModel private readonly CancellationTokenSource _cts = new(); public event Action? CurveReady; + public event Action? ProtectResultsChanged; public TestSessionViewModel(IServiceProvider sp, Project project, ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, UiLogSink logSink) @@ -50,6 +51,9 @@ public sealed class TestSessionViewModel IProgress progress = new Progress(p => { + if (p.ModuleName == ProtectTest.ModuleName) + ProtectResultsChanged?.Invoke(); + if (!string.IsNullOrEmpty(p.Message)) { var lvl = p.StepStatus switch diff --git a/SSPCTester.UI/Views/ProtectView.xaml b/SSPCTester.UI/Views/ProtectView.xaml index 067a31f..4307b16 100644 --- a/SSPCTester.UI/Views/ProtectView.xaml +++ b/SSPCTester.UI/Views/ProtectView.xaml @@ -27,15 +27,49 @@ - + - - - - - -