除控制台外功能修改

This commit is contained in:
zhaotielin 2026-07-10 18:32:20 +08:00
parent 310644500f
commit b8a92220f9
12 changed files with 231 additions and 51 deletions

View File

@ -17,7 +17,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
private double? _nextTriggerSec;
/// <summary>满量程电压V。</summary>
public double NominalVoltage { get; set; } = 28.0;
public double NominalVoltage { get; set; } = 5.0;
/// <summary>满量程电流A。</summary>
public double NominalCurrent { get; set; } = 1.0;
@ -49,6 +49,9 @@ public sealed class MockDaq : MockDeviceBase, IDaq
public async Task<CurveData> CaptureAsync(
int[] channels, int sampleRateHz, double durationSec, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(channels);
if (channels.Length == 0)
throw new ArgumentException("At least one channel must be requested.", nameof(channels));
if (sampleRateHz <= 0) throw new ArgumentOutOfRangeException(nameof(sampleRateHz));
if (durationSec <= 0) throw new ArgumentOutOfRangeException(nameof(durationSec));
@ -57,9 +60,18 @@ public sealed class MockDaq : MockDeviceBase, IDaq
await Task.Delay(simulatedDelayMs, ct).ConfigureAwait(false);
int n = (int)Math.Round(sampleRateHz * durationSec);
var v = new double[n];
var i = new double[n];
var cardVoltage = new double[n];
var cardCurrent = new double[n];
double dt = 1.0 / sampleRateHz;
int channelCount = Math.Max(1, _opts.ChannelCount);
var requestedChannels = channels.Distinct().ToArray();
foreach (int channel in requestedChannels)
{
if (channel < 0 || channel >= channelCount)
throw new ArgumentOutOfRangeException(
nameof(channels),
$"Requested channel {channel} is outside the configured PCIe8586 channel count {channelCount}.");
}
bool rising = NextCaptureIsRising;
double delay = rising ? OnDelaySec : OffDelaySec;
@ -81,7 +93,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
voltage = tRel <= 0 ? NominalVoltage : NominalVoltage * Math.Exp(-tRel / TauSec);
}
voltage += NextGaussian() * NominalVoltage * 0.005;
v[k] = voltage;
cardVoltage[k] = voltage;
double current;
if (rising)
@ -89,16 +101,10 @@ public sealed class MockDaq : MockDeviceBase, IDaq
else
current = tRel <= 0 ? NominalCurrent : NominalCurrent * Math.Exp(-tRel / TauSec);
current += NextGaussian() * NominalCurrent * 0.01;
i[k] = current;
cardCurrent[k] = current;
}
return new CurveData
{
SampleRateHz = sampleRateHz,
TriggerTimeSec = triggerSec,
Voltage = v,
Current = i
};
return BuildCurve(cardVoltage, cardCurrent, sampleRateHz, triggerSec, triggerIndex: 0, requestedChannels[0]);
}
public Task<double[]> ReadInstantAsync(int[] channels, CancellationToken ct = default)
@ -112,7 +118,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
? DaqChannelRole.Unconfigured
: Pcie8586Daq.ParseRole(cfg.Role);
result[k] = role switch
double cardValue = role switch
{
DaqChannelRole.OutputVoltage or DaqChannelRole.InputVoltage =>
NominalVoltage * (1 + (_rng.NextDouble() - 0.5) * 0.004),
@ -120,6 +126,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
NominalCurrent * (1 + (_rng.NextDouble() - 0.5) * 0.02),
_ => 0
};
result[k] = ApplyCalibration(cardValue, cfg);
}
return Task.FromResult(result);
}
@ -137,7 +144,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
NextCaptureIsRising = request.IsRising;
await action(ct).ConfigureAwait(false);
_nextTriggerSec = request.PreTriggerMs / 1_000.0;
var curve = await CaptureAsync(new[] { 0 }, sampleRate, durationSec, ct).ConfigureAwait(false);
var curve = await CaptureAsync(Enumerable.Range(0, Math.Max(1, _opts.ChannelCount)).ToArray(), sampleRate, durationSec, ct).ConfigureAwait(false);
int triggerIndex = (int)Math.Round(sampleRate * request.PreTriggerMs / 1_000.0);
return new CurveData
{
@ -146,14 +153,60 @@ public sealed class MockDaq : MockDeviceBase, IDaq
TriggerTimeSec = triggerIndex / (double)curve.SampleRateHz,
Voltage = curve.Voltage,
Current = curve.Current,
EngineeringChannels = new[]
{
new CurveChannelData(0, DaqChannelRole.OutputVoltage, "模拟输出电压", "V", 1, 0, curve.Voltage),
new CurveChannelData(1, DaqChannelRole.OutputCurrent, "模拟输出电流", "A", 1, 0, curve.Current!)
}
EngineeringChannels = curve.EngineeringChannels
};
}
private CurveData BuildCurve(
double[] cardVoltage,
double[] cardCurrent,
int sampleRateHz,
double triggerSec,
int triggerIndex,
int primaryPhysicalChannel)
{
var engineering = new List<CurveChannelData>();
int channelCount = Math.Max(1, _opts.ChannelCount);
for (int channel = 0; channel < channelCount; channel++)
{
var cfg = _opts.Channels.FirstOrDefault(x => x.PhysicalChannel == channel)
?? new DaqChannelOptions { PhysicalChannel = channel, Name = $"CH{channel}" };
var role = Pcie8586Daq.ParseRole(cfg.Role);
var source = role is DaqChannelRole.OutputCurrent or DaqChannelRole.InputCurrent
? cardCurrent
: cardVoltage;
var values = source.Select(x => ApplyCalibration(x, cfg)).ToArray();
engineering.Add(new CurveChannelData(
channel, role, cfg.Name, UnitFor(role), cfg.Scale, cfg.Offset, values));
}
var outputVoltage = engineering.Where(x => x.Role == DaqChannelRole.OutputVoltage).ToArray();
var primary = outputVoltage.Length == 1
? outputVoltage[0]
: engineering.FirstOrDefault(x => x.PhysicalChannel == primaryPhysicalChannel) ?? engineering.FirstOrDefault();
var current = engineering.FirstOrDefault(x => x.Role == DaqChannelRole.OutputCurrent)?.Samples;
return new CurveData
{
SampleRateHz = sampleRateHz,
TriggerSampleIndex = triggerIndex,
TriggerTimeSec = triggerSec,
Voltage = primary?.Samples ?? Array.Empty<double>(),
Current = current,
EngineeringChannels = engineering
};
}
private static double ApplyCalibration(double value, DaqChannelOptions? cfg) =>
cfg is null ? value : value * cfg.Scale + cfg.Offset;
private static string UnitFor(DaqChannelRole role) => role switch
{
DaqChannelRole.OutputVoltage or DaqChannelRole.InputVoltage => "V",
DaqChannelRole.OutputCurrent or DaqChannelRole.InputCurrent => "A",
_ => ""
};
/// <summary>由 ReadInstantAsync 衍生的"电流读数"接口(基础测试页用)。</summary>
public (double volts, double amps) ReadVoltageCurrent(int channel, bool channelOn)
{

View File

@ -7,8 +7,9 @@ namespace SSPCTester.Logic.Testing;
public static class BasicTestCriteria
{
public const double OnVoltageThreshold = 1.0;
public const double OffVoltageThreshold = 0.5;
public const double OffVoltageThreshold = 0.3;
public const double CurrentPresentThreshold = 0.001;
public const double OffCurrentThreshold = 0.3;
public const int SwitchSettleDelayMs = 1_000;
public const int OffSettleTimeoutMs = 2_000;
public const int OffInitialDelayMs = 100;
@ -27,7 +28,7 @@ public static class BasicTestCriteria
HasVoltageData(voltage) && HasCurrentData(current);
public static bool IsOffState(double voltage, double current) =>
IsOff(voltage) && !HasCurrentData(current);
IsOff(voltage) && Math.Abs(current) < OffCurrentThreshold;
/// <summary>
/// Waits until the channel is stably off: no voltage and no current for consecutive samples.

View File

@ -15,10 +15,10 @@ public sealed class BasicTestCriteriaTests
}
[Theory]
[InlineData(0.49, true)]
[InlineData(-0.49, true)]
[InlineData(0.50, false)]
[InlineData(-0.50, false)]
[InlineData(0.29, true)]
[InlineData(-0.29, true)]
[InlineData(0.30, false)]
[InlineData(-0.30, false)]
public void IsOff_UsesAbsoluteVoltageThreshold(double voltage, bool expected)
{
Assert.Equal(expected, BasicTestCriteria.IsOff(voltage));
@ -35,12 +35,12 @@ public sealed class BasicTestCriteriaTests
[Theory]
[InlineData(0.0, 0.0, true)]
[InlineData(0.49, 0.0, true)]
[InlineData(-0.49, 0.0, true)]
[InlineData(0.0, 0.0005, true)]
[InlineData(0.50, 0.0, false)]
[InlineData(0.29, 0.0, true)]
[InlineData(-0.29, 0.0, true)]
[InlineData(0.0, 0.29, true)]
[InlineData(0.30, 0.0, false)]
[InlineData(0.75, 0.0, false)]
[InlineData(0.0, 0.01, false)]
[InlineData(0.0, 0.30, false)]
[InlineData(28.0, 0.0, false)]
public void IsOffState_RequiresNoVoltageAndNoCurrent(double voltage, double current, bool expected)
{

View File

@ -56,6 +56,51 @@ public sealed class DaqIntegrationTests
Assert.False(await sspc.GetChannelStateAsync(3));
}
[Fact]
public async Task MockDaq_AppliesScaleAndOffset()
{
var options = new DeviceOptions();
options.Daq.ClockDivider = 100_000;
options.Daq.ChannelCount = 2;
options.Daq.Channels[0].Role = nameof(DaqChannelRole.OutputVoltage);
options.Daq.Channels[0].Name = "Vout";
options.Daq.Channels[0].Scale = 2;
options.Daq.Channels[0].Offset = 1;
options.Daq.Channels[0].IsCalibrated = true;
options.Daq.Channels[1].Role = nameof(DaqChannelRole.OutputCurrent);
options.Daq.Channels[1].Name = "Iout";
options.Daq.Channels[1].Scale = 3;
options.Daq.Channels[1].Offset = -1;
options.Daq.Channels[1].IsCalibrated = true;
var daq = new MockDaq(Options.Create(options), NullLogger<MockDaq>.Instance)
{
NominalVoltage = 10,
NominalCurrent = 1
};
var curve = await daq.CaptureAroundActionAsync(
new DaqActionCaptureRequest(10, 40, true),
_ => Task.CompletedTask);
var instant = await daq.ReadInstantAsync(new[] { 0, 1 });
Assert.True(curve.Voltage.Max() > 20);
Assert.True(curve.Current!.Max() > 1.8);
Assert.Contains(curve.EngineeringChannels, x =>
x.PhysicalChannel == 0 &&
x.Role == DaqChannelRole.OutputVoltage &&
x.Name == "Vout" &&
x.Scale == 2 &&
x.Offset == 1);
Assert.Contains(curve.EngineeringChannels, x =>
x.PhysicalChannel == 1 &&
x.Role == DaqChannelRole.OutputCurrent &&
x.Name == "Iout" &&
x.Scale == 3 &&
x.Offset == -1);
Assert.InRange(instant[0], 20.9, 21.1);
Assert.InRange(instant[1], 1.9, 2.1);
}
[Theory]
[InlineData(1000, 100_000)]
[InlineData(100, 1_000_000)]

View File

@ -298,6 +298,36 @@ public partial class CurveTestViewModel : ObservableObject
[RelayCommand]
private Task CaptureFalling() => CaptureSingleCurveAsync(CurveCaptureKind.Falling);
[RelayCommand]
private void Clear()
{
foreach (var row in Rows)
{
row.OnTimeMs = 0;
row.RiseTimeMs = 0;
row.OffTimeMs = 0;
row.FallTimeMs = 0;
row.RisingWaveformPath = null;
row.FallingWaveformPath = null;
row.ViewStartMs = null;
row.ViewEndMs = null;
row.RisingViewStartMs = null;
row.RisingViewEndMs = null;
row.FallingViewStartMs = null;
row.FallingViewEndMs = null;
row.Status = TestStatus.Untested;
}
RisingCurve = null;
FallingCurve = null;
RisingViewStartMs = null;
RisingViewEndMs = null;
FallingViewStartMs = null;
FallingViewEndMs = null;
SaveProjectQuietly();
_log.Add(UiLogLevel.Info, "开启与关断曲线结果已清空。");
}
private async Task CaptureSingleCurveAsync(CurveCaptureKind kind)
{
if (Project == null || Busy) return;

View File

@ -181,6 +181,23 @@ public partial class PowerTestViewModel : ObservableObject
}
}
[RelayCommand]
private void Clear()
{
foreach (var row in Rows)
{
row.Vin = 0;
row.Iin = 0;
row.Vout = 0;
row.Iout = 0;
row.MeasuredPowerOut = null;
row.Status = TestStatus.Untested;
}
Current = Rows.FirstOrDefault();
_log.Add(UiLogLevel.Info, "功率损耗结果已清空。");
}
/// <summary>解析用于输入端电压的高速采集卡通道(取已标定的电压通道,优先 OutputVoltage。</summary>
private static int ResolveDaqVoltageChannel(DaqOptions options)
{

View File

@ -14,7 +14,7 @@
<RowDefinition Height="2*" MinHeight="180" />
</Grid.RowDefinitions>
<TextBlock Text="开启与关断曲线(覆盖指标 1.11.6" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,12" />
<TextBlock Text="开启与关断曲线" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,12" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
@ -144,7 +144,17 @@
<Border Style="{StaticResource Card}" Margin="0,0,12,0">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="各通道时间参数" />
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Style="{StaticResource CardTitle}" Text="各通道时间参数" />
<Button Grid.Column="1"
Content="清空结果"
Command="{Binding ClearCommand}"
IsEnabled="{Binding Busy, Converter={StaticResource InverseBoolConverter}}" />
</Grid>
<DataGrid ItemsSource="{Binding Rows}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}">
<DataGrid.Columns>

View File

@ -13,7 +13,7 @@
<RowDefinition Height="3*" MinHeight="220" />
</Grid.RowDefinitions>
<TextBlock Text="功率损耗(采集输入/输出两端数据)" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,12" />
<TextBlock Text="功率损耗" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,12" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
@ -178,7 +178,17 @@
<Border Style="{StaticResource Card}" Margin="0,0,12,0">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="各通道汇总" />
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Style="{StaticResource CardTitle}" Text="各通道汇总" />
<Button Grid.Column="1"
Content="清空结果"
Command="{Binding ClearCommand}"
IsEnabled="{Binding CanOperate}" />
</Grid>
<DataGrid ItemsSource="{Binding Rows}">
<DataGrid.Columns>
<DataGridTextColumn Header="通道" Width="60">

View File

@ -15,7 +15,6 @@
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="{Binding Project.Name, StringFormat='项目:{0}'}" FontSize="{StaticResource FsH1}" FontWeight="Bold" />
<TextBlock Text="填写被测件信息并选择测试项。" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
</Grid>

View File

@ -15,7 +15,7 @@
<RowDefinition Height="2*" MinHeight="200" />
</Grid.RowDefinitions>
<TextBlock Text="保护功能(覆盖指标 11/14/15/17/19/21/22/23" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,8" />
<TextBlock Text="保护功能" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,8" />

View File

@ -18,7 +18,10 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="测试报告" FontSize="{StaticResource FsH1}" FontWeight="Bold" />
<TextBlock FontSize="{StaticResource FsH1}" FontWeight="Bold">
<Run Text="测试报告:" />
<Run Text="{Binding Project.Name, Mode=OneWay}" />
</TextBlock>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<ComboBox Width="130"
Margin="0,0,8,0"

View File

@ -7,6 +7,7 @@
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=vm:TestHostViewModel}">
<Border Background="{StaticResource BgSurfaceBrush}" BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="0,0,0,1">
<Grid>
<TabControl x:Name="Tabs">
<TabItem Header="基础测试">
<ContentControl Content="{Binding Basic}" />
@ -21,5 +22,16 @@
<ContentControl Content="{Binding Protect}" />
</TabItem>
</TabControl>
<TextBlock Text="{Binding Project.Name, StringFormat='当前项目:{0}'}"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,10,20,0"
Foreground="{StaticResource TextSecondaryBrush}"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxWidth="420"
IsHitTestVisible="False" />
</Grid>
</Border>
</UserControl>