using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SSPCTester.Devices.Config; using SSPCTester.Devices.Drivers.Real; using SSPCTester.Devices.Interfaces; namespace SSPCTester.Devices.Drivers.Mock; /// /// 模拟数据采集卡。 /// 生成逼真的电压上升 / 下降波形:V(t) = Vmax * (1 - exp(-t/τ)) + 噪声。 /// public sealed class MockDaq : MockDeviceBase, IDaq { private readonly Random _rng = new(1024); private readonly DaqOptions _opts; private double? _nextTriggerSec; /// 满量程电压(V)。 public double NominalVoltage { get; set; } = 5.0; /// 满量程电流(A)。 public double NominalCurrent { get; set; } = 1.0; /// 开启延时(秒),用于模拟"控制信号 → 开始上升"的延时。 public double OnDelaySec { get; set; } = 0.0003; /// 关断延时(秒)。 public double OffDelaySec { get; set; } = 0.0003; /// RC 时间常数(秒)。 public double TauSec { get; set; } = 0.0005; /// 下一次 CaptureAsync 的模式:true=上升曲线,false=下降曲线。 public bool NextCaptureIsRising { get; set; } = true; public MockDaq(IOptions options, ILogger logger) : base(logger, "PCIe8586 高速采集卡 (Mock)") { _opts = options.Value.Daq; } public event EventHandler? SampleReceived; public Task> EnumerateDevicesAsync(CancellationToken ct = default) => Task.FromResult>( new[] { new DaqDeviceDescriptor(0, 0, "模拟 PCIe8586", true) }); 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)); // 模拟采集耗时(不全等比例,避免阻塞太久) int simulatedDelayMs = (int)Math.Min(durationSec * 1000.0 * 0.05, 400); await Task.Delay(simulatedDelayMs, ct).ConfigureAwait(false); int n = (int)Math.Round(sampleRateHz * durationSec); 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; // 触发时刻设在 1/4 处,便于前后查看 double triggerSec = _nextTriggerSec ?? durationSec * 0.25; _nextTriggerSec = null; for (int k = 0; k < n; k++) { double t = k * dt; double tRel = t - triggerSec - delay; // 相对于"开始上升/下降"的时间 double voltage; if (rising) { voltage = tRel <= 0 ? 0 : NominalVoltage * (1 - Math.Exp(-tRel / TauSec)); } else { voltage = tRel <= 0 ? NominalVoltage : NominalVoltage * Math.Exp(-tRel / TauSec); } voltage += NextGaussian() * NominalVoltage * 0.005; cardVoltage[k] = voltage; double current; if (rising) current = tRel <= 0 ? 0 : NominalCurrent * (1 - Math.Exp(-tRel / TauSec)); else current = tRel <= 0 ? NominalCurrent : NominalCurrent * Math.Exp(-tRel / TauSec); current += NextGaussian() * NominalCurrent * 0.01; cardCurrent[k] = current; } return BuildCurve(cardVoltage, cardCurrent, sampleRateHz, triggerSec, triggerIndex: 0, requestedChannels[0]); } public Task ReadInstantAsync(int[] channels, CancellationToken ct = default) { var result = new double[channels.Length]; for (int k = 0; k < channels.Length; k++) { int physical = channels[k]; var cfg = _opts.Channels.FirstOrDefault(x => x.PhysicalChannel == physical); var role = cfg is null ? DaqChannelRole.Unconfigured : Pcie8586Daq.ParseRole(cfg.Role); double cardValue = role switch { DaqChannelRole.OutputVoltage or DaqChannelRole.InputVoltage => NominalVoltage * (1 + (_rng.NextDouble() - 0.5) * 0.004), DaqChannelRole.OutputCurrent or DaqChannelRole.InputCurrent => NominalCurrent * (1 + (_rng.NextDouble() - 0.5) * 0.02), _ => 0 }; result[k] = ApplyCalibration(cardValue, cfg); } return Task.FromResult(result); } public async Task CaptureAroundActionAsync( DaqActionCaptureRequest request, Func action, CancellationToken ct = default) { // Mock 下也遵循“先采前置窗口,再执行动作”的时序,避免与真卡行为不一致。 await Task.Delay((int)Math.Max(1, request.PreTriggerMs * 0.2), ct).ConfigureAwait(false); double durationSec = (request.PreTriggerMs + request.PostTriggerMs) / 1_000.0; int sampleRate = _opts.SampleRateHz; NextCaptureIsRising = request.IsRising; await action(ct).ConfigureAwait(false); _nextTriggerSec = request.PreTriggerMs / 1_000.0; 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 { SampleRateHz = curve.SampleRateHz, TriggerSampleIndex = triggerIndex, TriggerTimeSec = triggerIndex / (double)curve.SampleRateHz, Voltage = curve.Voltage, Current = 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) { if (!channelOn) return (0.0, 0.0); double v = NominalVoltage * (1 + (_rng.NextDouble() - 0.5) * 0.004); double a = NominalCurrent * (1 + (_rng.NextDouble() - 0.5) * 0.02); return (v, a); } /// Box-Muller 生成高斯噪声。 private double NextGaussian() { double u1 = 1.0 - _rng.NextDouble(); double u2 = 1.0 - _rng.NextDouble(); return Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Cos(2.0 * Math.PI * u2); } /// 仅用于编译期消除"事件未使用"警告。 private void RaiseSample(CurveSample s) => SampleReceived?.Invoke(this, s); }