SSPC-Tester/SSPCTester.Devices/Drivers/Mock/MockDaq.cs

230 lines
9.6 KiB
C#
Raw Normal View History

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;
/// <summary>
/// 模拟数据采集卡。
/// 生成逼真的电压上升 / 下降波形V(t) = Vmax * (1 - exp(-t/τ)) + 噪声。
/// </summary>
public sealed class MockDaq : MockDeviceBase, IDaq
{
private readonly Random _rng = new(1024);
private readonly DaqOptions _opts;
private double? _nextTriggerSec;
/// <summary>满量程电压V。</summary>
2026-07-10 18:32:20 +08:00
public double NominalVoltage { get; set; } = 5.0;
/// <summary>满量程电流A。</summary>
public double NominalCurrent { get; set; } = 1.0;
/// <summary>开启延时(秒),用于模拟"控制信号 → 开始上升"的延时。</summary>
public double OnDelaySec { get; set; } = 0.0003;
/// <summary>关断延时(秒)。</summary>
public double OffDelaySec { get; set; } = 0.0003;
/// <summary>RC 时间常数(秒)。</summary>
public double TauSec { get; set; } = 0.0005;
/// <summary>下一次 CaptureAsync 的模式true=上升曲线false=下降曲线。</summary>
public bool NextCaptureIsRising { get; set; } = true;
public MockDaq(IOptions<DeviceOptions> options, ILogger<MockDaq> logger)
: base(logger, "PCIe8586 高速采集卡 (Mock)")
{
_opts = options.Value.Daq;
}
public event EventHandler<CurveSample>? SampleReceived;
public Task<IReadOnlyList<DaqDeviceDescriptor>> EnumerateDevicesAsync(CancellationToken ct = default) =>
Task.FromResult<IReadOnlyList<DaqDeviceDescriptor>>(
new[] { new DaqDeviceDescriptor(0, 0, "模拟 PCIe8586", true) });
public async Task<CurveData> CaptureAsync(
int[] channels, int sampleRateHz, double durationSec, CancellationToken ct = default)
{
2026-07-10 18:32:20 +08:00
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);
2026-07-10 18:32:20 +08:00
var cardVoltage = new double[n];
var cardCurrent = new double[n];
double dt = 1.0 / sampleRateHz;
2026-07-10 18:32:20 +08:00
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;
2026-07-10 18:32:20 +08:00
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;
2026-07-10 18:32:20 +08:00
cardCurrent[k] = current;
}
2026-07-10 18:32:20 +08:00
return BuildCurve(cardVoltage, cardCurrent, sampleRateHz, triggerSec, triggerIndex: 0, requestedChannels[0]);
}
public Task<double[]> 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);
2026-07-10 18:32:20 +08:00
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
};
2026-07-10 18:32:20 +08:00
result[k] = ApplyCalibration(cardValue, cfg);
}
return Task.FromResult(result);
}
public async Task<CurveData> CaptureAroundActionAsync(
DaqActionCaptureRequest request,
Func<CancellationToken, Task> 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;
2026-07-10 18:32:20 +08:00
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,
2026-07-10 18:32:20 +08:00
EngineeringChannels = curve.EngineeringChannels
};
}
2026-07-10 18:32:20 +08:00
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)
{
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);
}
/// <summary>Box-Muller 生成高斯噪声。</summary>
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);
}
/// <summary>仅用于编译期消除"事件未使用"警告。</summary>
private void RaiseSample(CurveSample s) => SampleReceived?.Invoke(this, s);
}