完善保护功能

This commit is contained in:
zhaotielin 2026-07-02 21:38:25 +08:00
parent 1a8c113fbc
commit 61819a3139
9 changed files with 240 additions and 91 deletions

View File

@ -97,6 +97,12 @@ public partial class ProtectRow : ObservableObject
[ObservableProperty] private double _tripTimeMs; [ObservableProperty] private double _tripTimeMs;
[ObservableProperty] private TestStatus _status = TestStatus.Untested; [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 public static string BuildMeasuredValue(string protectType, double? inputVoltage, double? inputCurrent) => protectType switch
{ {
"跳闸" when inputCurrent.HasValue => $"{inputCurrent.Value:F3} A", "跳闸" when inputCurrent.HasValue => $"{inputCurrent.Value:F3} A",
@ -105,3 +111,63 @@ public partial class ProtectRow : ObservableObject
_ => "----" _ => "----"
}; };
} }
/// <summary>保护功能固定测试项与结果初始化逻辑。</summary>
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);
}
}
}

View File

@ -98,7 +98,7 @@ internal sealed class LegacyReportGenerator
if (sel.Protect && (p.ProtectResults.Count > 0 || HasManualNote(p.TestFunction?.Protect))) 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)); if (p.ProtectResults.Count > 0) col.Item().Element(e => ProtectTable(e, p));
ManualNote(col, p.TestFunction?.Protect); ManualNote(col, p.TestFunction?.Protect);
} }

View File

@ -2,47 +2,30 @@ using SSPCTester.Logic.Models;
namespace SSPCTester.Logic.Testing; namespace SSPCTester.Logic.Testing;
/// <summary> /// <summary>保护功能测试:刷新固定保护测试项的实测值与测量状态。</summary>
/// 保护功能测试:当前阶段仅生成框架占位结果,不真正触发故障。
/// </summary>
public sealed class ProtectTest : ITestModule public sealed class ProtectTest : ITestModule
{ {
public string Name => "保护功能"; public const string ModuleName = "保护功能";
public string Name => ModuleName;
private static readonly (string Type, string Trigger)[] _items = new[]
{
("跳闸", "1.5×额定电流"),
("短路", "输出短路"),
("过温", "≥85℃"),
("过压", "≥32V"),
("欠压", "≤22V"),
("限流", "≥1.2A"),
};
public async Task RunAsync(TestContext ctx, IProgress<TestProgress> progress, CancellationToken ct) public async Task RunAsync(TestContext ctx, IProgress<TestProgress> progress, CancellationToken ct)
{ {
ctx.Project.ProtectResults.Clear(); ProtectTestPlan.EnsureRows(ctx.Project);
var latestPower = ctx.Project.PowerResults.LastOrDefault();
for (int i = 0; i < _items.Length; i++) for (int i = 0; i < ctx.Project.ProtectResults.Count; i++)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var row = new ProtectRow var row = ctx.Project.ProtectResults[i];
{ bool preserveManualJudgement = row.Status is TestStatus.Pass or TestStatus.Fail;
Index = i + 1, if (!preserveManualJudgement)
ProtectType = _items[i].Type, row.Status = TestStatus.Running;
Trigger = _items[i].Trigger, ProtectTestPlan.RefreshMeasuredValues(ctx.Project, overwriteExisting: false);
MeasuredValue = ProtectRow.BuildMeasuredValue(_items[i].Type, latestPower?.Vin, latestPower?.Iin), progress.Report(new TestProgress { ModuleName = Name, Step = i, TotalSteps = ctx.Project.ProtectResults.Count, Message = $"保护项测量:{row.ProtectType}" });
JudgeMode = "人工判别",
Status = TestStatus.Running
};
ctx.Project.ProtectResults.Add(row);
progress.Report(new TestProgress { ModuleName = Name, Step = i, TotalSteps = _items.Length, Message = $"框架占位:{row.ProtectType}" });
await Task.Delay(150, ct).ConfigureAwait(false); await Task.Delay(150, ct).ConfigureAwait(false);
// 框架占位:固定演示数据 if (!preserveManualJudgement && row.Status == TestStatus.Running)
row.TripTimeMs = 2.0 + i * 0.3; row.Status = TestStatus.Measured;
row.Status = TestStatus.Pass; progress.Report(new TestProgress { ModuleName = Name, Step = i + 1, TotalSteps = ctx.Project.ProtectResults.Count, StepStatus = row.Status });
progress.Report(new TestProgress { ModuleName = Name, Step = i + 1, TotalSteps = _items.Length, StepStatus = row.Status });
} }
} }
} }

View File

@ -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<TestProgress>();
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<TestProgress> handler) : IProgress<TestProgress>
{
public void Report(TestProgress value) => handler(value);
}
}

View File

@ -30,6 +30,7 @@ public sealed class WordReportGeneratorTests
Assert.Contains("ProjectA", text); Assert.Contains("ProjectA", text);
Assert.Contains("基础测试", text); Assert.Contains("基础测试", text);
Assert.Contains("功率损耗", text); Assert.Contains("功率损耗", text);
Assert.Contains("PROTECTION FUNCTION", text);
var fontNames = document.MainDocumentPart.Document.Descendants<RunFonts>() var fontNames = document.MainDocumentPart.Document.Descendants<RunFonts>()
.SelectMany(x => new[] { x.Ascii?.Value, x.HighAnsi?.Value, x.EastAsia?.Value }) .SelectMany(x => new[] { x.Ascii?.Value, x.HighAnsi?.Value, x.EastAsia?.Value })

View File

@ -1,87 +1,73 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using SSPCTester.Devices.Interfaces;
using SSPCTester.Logic.Models; using SSPCTester.Logic.Models;
using SSPCTester.UI.Services; using SSPCTester.UI.Services;
namespace SSPCTester.UI.ViewModels; namespace SSPCTester.UI.ViewModels;
/// <summary>保护功能页 VM(框架占位)。</summary> /// <summary>保护功能页 VM。</summary>
public partial class ProtectViewModel : ObservableObject public partial class ProtectViewModel : ObservableObject
{ {
private readonly IPowerSupply _power;
private readonly UiLogSink _log; private readonly UiLogSink _log;
private TestSessionViewModel? _session;
public ObservableCollection<ProtectRow> Rows { get; } = new(); public ObservableCollection<ProtectRow> Rows { get; } = new();
[ObservableProperty] private Project? _project; [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; public ProtectViewModel(IPowerSupply power, UiLogSink log)
[ObservableProperty] private double _underVoltThreshold = 22; {
[ObservableProperty] private double _overCurrentThreshold = 1.2; _power = power;
[ObservableProperty] private double _overTempThreshold = 85; _log = log;
}
public ProtectViewModel(UiLogSink log) { _log = log; }
public void SetProject(Project p) public void SetProject(Project p)
{ {
Project = p; Project = p;
ProtectTestPlan.EnsureRows(p);
SyncRowsFromProject(); SyncRowsFromProject();
} }
public void AttachSession(TestSessionViewModel session)
{
if (_session != null)
_session.ProtectResultsChanged -= SyncRowsFromProject;
_session = session;
_session.ProtectResultsChanged += SyncRowsFromProject;
}
private void SyncRowsFromProject() private void SyncRowsFromProject()
{ {
if (Project == null) return; if (Project == null) return;
var latestPower = Project.PowerResults.LastOrDefault(); ProtectTestPlan.RefreshMeasuredValues(Project, overwriteExisting: false);
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 = "人工判别";
}
Rows.Clear(); Rows.Clear();
foreach (var row in Project.ProtectResults) Rows.Add(row); foreach (var row in Project.ProtectResults) Rows.Add(row);
} }
[RelayCommand] [RelayCommand]
private void RunDemo() private async Task MeasurePowerAsync(ProtectRow? row)
{ {
if (Project == null) return; if (row == null || !row.CanMeasureFromPower) return;
Project.ProtectResults.Clear();
Rows.Clear(); try
var latestPower = Project.PowerResults.LastOrDefault();
var items = new (string, string)[]
{ {
("跳闸", "1.5x额定电流"), var (volts, amps) = await _power.ReadAsync();
("短路", "输出短路"), row.MeasuredValue = row.ProtectType switch
("过温", $">={OverTempThreshold} ℃"), {
("过压", $">={OverVoltThreshold} V"), "过压" or "欠压" => $"{volts:F3} V",
("欠压", $"<={UnderVoltThreshold} V"), _ => $"{amps:F3} A"
("限流", $">={OverCurrentThreshold} A"),
}; };
for (int i = 0; i < items.Length; i++) if (row.Status is TestStatus.Untested or TestStatus.Running)
{ row.Status = TestStatus.Measured;
var r = new ProtectRow _log.Add(UiLogLevel.Success, $"保护功能 {row.ProtectType} 实测值:{row.MeasuredValue}");
{ }
Index = i + 1, catch (Exception ex)
ProtectType = items[i].Item1, {
Trigger = items[i].Item2, _log.Add(UiLogLevel.Error, $"读取电源失败:{ex.Message}");
MeasuredValue = ProtectRow.BuildMeasuredValue(items[i].Item1, latestPower?.Vin, latestPower?.Iin),
JudgeMode = "人工判别",
TripTimeMs = 2 + i * 0.3,
Status = TestStatus.Pass
};
Project.ProtectResults.Add(r);
Rows.Add(r);
} }
_log.Add(UiLogLevel.Warning, "保护功能模块仅生成框架演示数据。");
} }
} }

View File

@ -32,5 +32,6 @@ public partial class TestHostViewModel : ObservableObject
{ {
Session = s; Session = s;
Curve.AttachSession(s); Curve.AttachSession(s);
Protect.AttachSession(s);
} }
} }

View File

@ -24,6 +24,7 @@ public sealed class TestSessionViewModel
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
public event Action<int, CurveData, CurveData>? CurveReady; public event Action<int, CurveData, CurveData>? CurveReady;
public event Action? ProtectResultsChanged;
public TestSessionViewModel(IServiceProvider sp, Project project, public TestSessionViewModel(IServiceProvider sp, Project project,
ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, UiLogSink logSink) ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, UiLogSink logSink)
@ -50,6 +51,9 @@ public sealed class TestSessionViewModel
IProgress<TestProgress> progress = new Progress<TestProgress>(p => IProgress<TestProgress> progress = new Progress<TestProgress>(p =>
{ {
if (p.ModuleName == ProtectTest.ModuleName)
ProtectResultsChanged?.Invoke();
if (!string.IsNullOrEmpty(p.Message)) if (!string.IsNullOrEmpty(p.Message))
{ {
var lvl = p.StepStatus switch var lvl = p.StepStatus switch

View File

@ -27,15 +27,49 @@
<Border Style="{StaticResource Card}" Margin="0,0,12,0"> <Border Style="{StaticResource Card}" Margin="0,0,12,0">
<DockPanel> <DockPanel>
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="测试记录" /> <TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="测试记录" />
<DataGrid ItemsSource="{Binding Rows}"> <DataGrid ItemsSource="{Binding Rows}" IsReadOnly="False">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="序号" Binding="{Binding Index}" Width="60" /> <DataGridTextColumn Header="序号" Binding="{Binding Index}" Width="60" IsReadOnly="True" />
<DataGridTextColumn Header="保护类型" Binding="{Binding ProtectType}" Width="*" /> <DataGridTextColumn Header="保护类型" Binding="{Binding ProtectType}" Width="*" IsReadOnly="True" />
<DataGridTextColumn Header="触发条件" Binding="{Binding Trigger}" Width="2*" /> <DataGridTextColumn Header="触发条件" Binding="{Binding Trigger, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="2*" />
<DataGridTextColumn Header="实测值" Width="*"> <DataGridTemplateColumn Header="实测值" Width="*">
<DataGridTextColumn.Binding><Binding Path="MeasuredValue" /></DataGridTextColumn.Binding> <DataGridTemplateColumn.CellTemplate>
<DataGridTextColumn.ElementStyle><Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" /></DataGridTextColumn.ElementStyle> <DataTemplate>
</DataGridTextColumn> <DockPanel LastChildFill="True">
<Button DockPanel.Dock="Right"
Content="测"
Width="30"
Height="24"
Padding="0"
Margin="6,0,0,0"
Command="{Binding DataContext.MeasurePowerCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Visibility="{Binding CanMeasureFromPower, Converter={StaticResource BoolToVis}}" />
<TextBlock Text="{Binding MeasuredValue}"
Style="{StaticResource MonoText}"
VerticalAlignment="Center" />
</DockPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DockPanel LastChildFill="True">
<Button DockPanel.Dock="Right"
Content="测"
Width="30"
Height="24"
Padding="0"
Margin="6,0,0,0"
Command="{Binding DataContext.MeasurePowerCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Visibility="{Binding CanMeasureFromPower, Converter={StaticResource BoolToVis}}" />
<TextBox Text="{Binding MeasuredValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
FontFamily="{StaticResource FontMono}"
VerticalContentAlignment="Center" />
</DockPanel>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="判别模式" Width="120"> <DataGridTemplateColumn Header="判别模式" Width="120">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
@ -56,6 +90,8 @@
MinWidth="90"> MinWidth="90">
<ComboBoxItem Content="合格" Tag="{x:Static model:TestStatus.Pass}" /> <ComboBoxItem Content="合格" Tag="{x:Static model:TestStatus.Pass}" />
<ComboBoxItem Content="不合格" Tag="{x:Static model:TestStatus.Fail}" /> <ComboBoxItem Content="不合格" Tag="{x:Static model:TestStatus.Fail}" />
<ComboBoxItem Content="已测量" Tag="{x:Static model:TestStatus.Measured}" />
<ComboBoxItem Content="未判定" Tag="{x:Static model:TestStatus.Untested}" />
</ComboBox> </ComboBox>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
@ -72,7 +108,7 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Border Grid.Column="1" Style="{StaticResource Card}" Padding="14"> <Border Grid.Column="0" Style="{StaticResource Card}" Padding="14">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />