177 lines
7.0 KiB
C#
177 lines
7.0 KiB
C#
|
|
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>
|
|||
|
|
public double NominalVoltage { get; set; } = 28.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)
|
|||
|
|
{
|
|||
|
|
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 v = new double[n];
|
|||
|
|
var i = new double[n];
|
|||
|
|
double dt = 1.0 / sampleRateHz;
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
v[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;
|
|||
|
|
i[k] = current;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return new CurveData
|
|||
|
|
{
|
|||
|
|
SampleRateHz = sampleRateHz,
|
|||
|
|
TriggerTimeSec = triggerSec,
|
|||
|
|
Voltage = v,
|
|||
|
|
Current = i
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
|
|||
|
|
result[k] = 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
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
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;
|
|||
|
|
var curve = await CaptureAsync(new[] { 0 }, 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 = new[]
|
|||
|
|
{
|
|||
|
|
new CurveChannelData(0, DaqChannelRole.OutputVoltage, "模拟输出电压", "V", 1, 0, curve.Voltage),
|
|||
|
|
new CurveChannelData(1, DaqChannelRole.OutputCurrent, "模拟输出电流", "A", 1, 0, curve.Current!)
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <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);
|
|||
|
|
}
|