From b8a92220f93ef4e8b7b733b03a35c6b2fc1e136c Mon Sep 17 00:00:00 2001 From: zhaotielin Date: Fri, 10 Jul 2026 18:32:20 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=A4=E6=8E=A7=E5=88=B6=E5=8F=B0=E5=A4=96?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SSPCTester.Devices/Drivers/Mock/MockDaq.cs | 91 +++++++++++++++---- SSPCTester.Logic/Testing/BasicTestCriteria.cs | 5 +- SSPCTester.Tests/BasicTestCriteriaTests.cs | 18 ++-- SSPCTester.Tests/DaqIntegrationTests.cs | 45 +++++++++ .../ViewModels/CurveTestViewModel.cs | 30 ++++++ .../ViewModels/PowerTestViewModel.cs | 17 ++++ SSPCTester.UI/Views/CurveTestView.xaml | 14 ++- SSPCTester.UI/Views/PowerTestView.xaml | 14 ++- SSPCTester.UI/Views/ProjectInfoView.xaml | 1 - SSPCTester.UI/Views/ProtectView.xaml | 2 +- SSPCTester.UI/Views/ReportView.xaml | 5 +- SSPCTester.UI/Views/TestHostView.xaml | 40 +++++--- 12 files changed, 231 insertions(+), 51 deletions(-) diff --git a/SSPCTester.Devices/Drivers/Mock/MockDaq.cs b/SSPCTester.Devices/Drivers/Mock/MockDaq.cs index f11a93c..f5e6674 100644 --- a/SSPCTester.Devices/Drivers/Mock/MockDaq.cs +++ b/SSPCTester.Devices/Drivers/Mock/MockDaq.cs @@ -17,7 +17,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq private double? _nextTriggerSec; /// 满量程电压(V)。 - public double NominalVoltage { get; set; } = 28.0; + public double NominalVoltage { get; set; } = 5.0; /// 满量程电流(A)。 public double NominalCurrent { get; set; } = 1.0; @@ -49,6 +49,9 @@ public sealed class MockDaq : MockDeviceBase, IDaq public async Task 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 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(); + 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(), + 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", + _ => "" + }; + /// 由 ReadInstantAsync 衍生的"电流读数"接口(基础测试页用)。 public (double volts, double amps) ReadVoltageCurrent(int channel, bool channelOn) { diff --git a/SSPCTester.Logic/Testing/BasicTestCriteria.cs b/SSPCTester.Logic/Testing/BasicTestCriteria.cs index dc5ed69..e61c31f 100644 --- a/SSPCTester.Logic/Testing/BasicTestCriteria.cs +++ b/SSPCTester.Logic/Testing/BasicTestCriteria.cs @@ -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; /// /// Waits until the channel is stably off: no voltage and no current for consecutive samples. diff --git a/SSPCTester.Tests/BasicTestCriteriaTests.cs b/SSPCTester.Tests/BasicTestCriteriaTests.cs index 1c555c6..8e98eef 100644 --- a/SSPCTester.Tests/BasicTestCriteriaTests.cs +++ b/SSPCTester.Tests/BasicTestCriteriaTests.cs @@ -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) { diff --git a/SSPCTester.Tests/DaqIntegrationTests.cs b/SSPCTester.Tests/DaqIntegrationTests.cs index e5ea4dd..31c06de 100644 --- a/SSPCTester.Tests/DaqIntegrationTests.cs +++ b/SSPCTester.Tests/DaqIntegrationTests.cs @@ -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.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)] diff --git a/SSPCTester.UI/ViewModels/CurveTestViewModel.cs b/SSPCTester.UI/ViewModels/CurveTestViewModel.cs index 0ffa436..b074f70 100644 --- a/SSPCTester.UI/ViewModels/CurveTestViewModel.cs +++ b/SSPCTester.UI/ViewModels/CurveTestViewModel.cs @@ -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; diff --git a/SSPCTester.UI/ViewModels/PowerTestViewModel.cs b/SSPCTester.UI/ViewModels/PowerTestViewModel.cs index 700582d..ec4ed92 100644 --- a/SSPCTester.UI/ViewModels/PowerTestViewModel.cs +++ b/SSPCTester.UI/ViewModels/PowerTestViewModel.cs @@ -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, "功率损耗结果已清空。"); + } + /// 解析用于输入端电压的高速采集卡通道(取已标定的电压通道,优先 OutputVoltage)。 private static int ResolveDaqVoltageChannel(DaqOptions options) { diff --git a/SSPCTester.UI/Views/CurveTestView.xaml b/SSPCTester.UI/Views/CurveTestView.xaml index b076f7a..1a2c23d 100644 --- a/SSPCTester.UI/Views/CurveTestView.xaml +++ b/SSPCTester.UI/Views/CurveTestView.xaml @@ -14,7 +14,7 @@ - + @@ -144,7 +144,17 @@ - + + + + + + +