SSPC-Tester/SSPCTester.Devices/Drivers/Mock/MockDaq.cs
2026-07-10 18:32:20 +08:00

230 lines
9.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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; } = 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)
{
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<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);
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<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(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<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);
}