2026-06-30 12:10:29 +08:00
|
|
|
|
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;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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));
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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];
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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}.");
|
|
|
|
|
|
}
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
|
|
|
|
|
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-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 18:32:20 +08:00
|
|
|
|
return BuildCurve(cardVoltage, cardCurrent, sampleRateHz, triggerSec, triggerIndex: 0, requestedChannels[0]);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
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);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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-06-30 12:10:29 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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",
|
|
|
|
|
|
_ => ""
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
/// <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);
|
|
|
|
|
|
}
|