using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Pcie8586Probe.Hardware;
using Pcie8586Probe.Models;
using SSPCTester.Devices.Config;
using SSPCTester.Devices.Interfaces;
using DriverInputRange = Pcie8586Probe.Models.InputRange;
namespace SSPCTester.Devices.Drivers.Real;
/// PCIe8586 高速采集卡适配器。原生句柄仅由 IDigitizer 持有。
public sealed class Pcie8586Daq : IDaq, IDisposable
{
private readonly DaqOptions _options;
private readonly ILogger _logger;
private readonly IDigitizer _digitizer;
private readonly SemaphoreSlim _operationLock = new(1, 1);
private bool _connected;
public Pcie8586Daq(IOptions options, ILogger logger)
: this(options.Value.Daq, logger, new Pcie8586Digitizer())
{
}
internal Pcie8586Daq(DaqOptions options, ILogger logger, IDigitizer digitizer)
{
_options = options;
_logger = logger;
_digitizer = digitizer;
}
public bool IsConnected => _connected && _digitizer.IsOpen;
public string DisplayName => "PCIe8586 高速采集卡";
public event EventHandler? ConnectionChanged;
public event EventHandler? SampleReceived;
public async Task> EnumerateDevicesAsync(CancellationToken ct = default)
{
try
{
var devices = await _digitizer.EnumerateDevicesAsync(ct);
return devices.Select(x => new DaqDeviceDescriptor(
x.LogicalId, x.PhysicalId, x.Description, x.IsSimulated)).ToArray();
}
catch (DllNotFoundException ex)
{
throw new InvalidOperationException("找不到 ACTS1000_64.dll。请安装厂商驱动并将 x64 运行库放到程序目录。", ex);
}
catch (BadImageFormatException ex)
{
throw new InvalidOperationException("ACTS1000_64.dll 位数不匹配,程序和运行库必须都是 x64。", ex);
}
}
public async Task ConnectAsync(CancellationToken ct = default)
{
await _operationLock.WaitAsync(ct);
try
{
if (IsConnected) return true;
var devices = await EnumerateDevicesAsync(ct);
var selected = devices.SingleOrDefault(x => x.LogicalId == _options.LogicalDeviceId)
?? throw new InvalidOperationException($"未发现逻辑 ID {_options.LogicalDeviceId} 的 PCIe8586。");
await _digitizer.OpenAsync(
new DeviceInfo(selected.LogicalId, selected.PhysicalId, selected.Description, selected.IsSimulated), ct);
_connected = true;
ConnectionChanged?.Invoke(this, true);
return true;
}
finally
{
_operationLock.Release();
}
}
public async Task DisconnectAsync()
{
await _operationLock.WaitAsync();
try
{
await _digitizer.StopAsync();
await _digitizer.CloseAsync();
if (_connected)
{
_connected = false;
ConnectionChanged?.Invoke(this, false);
}
}
finally
{
_operationLock.Release();
}
}
public async Task CaptureAroundActionAsync(
DaqActionCaptureRequest request,
Func action,
CancellationToken ct = default)
{
ValidateOptions(requireOutputVoltage: true);
await _operationLock.WaitAsync(ct);
try
{
if (!IsConnected) await ConnectWithoutLockAsync(ct);
var config = CreateDriverConfig();
await _digitizer.ConfigureAsync(config, ct);
int preSamples = Math.Max(1, (int)Math.Round(config.SampleRateHz * request.PreTriggerMs / 1_000.0));
int postSamples = Math.Max(1, (int)Math.Round(config.SampleRateHz * request.PostTriggerMs / 1_000.0));
int targetSamples = checked(preSamples + postSamples);
var collected = Enumerable.Range(0, config.ChannelCount)
.Select(_ => new List(targetSamples)).ToArray();
bool actionInvoked = false;
int triggerIndex = 0;
var preTriggerTimeout = TimeSpan.FromMilliseconds(Math.Max(3_000, request.PreTriggerMs * 5));
using var acquisitionCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
acquisitionCts.CancelAfter(preTriggerTimeout);
try
{
await foreach (var block in _digitizer.StartAcquisitionAsync(acquisitionCts.Token))
{
if (!actionInvoked)
{
int take = Math.Min(preSamples - collected[0].Count, block.SamplesPerChannel);
for (int channel = 0; channel < config.ChannelCount; channel++)
collected[channel].AddRange(block.Channels[channel].AsSpan(0, take).ToArray());
if (collected[0].Count >= preSamples)
{
triggerIndex = collected[0].Count;
actionInvoked = true;
acquisitionCts.CancelAfter(Timeout.InfiniteTimeSpan);
await action(ct);
}
// 动作发生在本次 DMA 读取完成之后;当前块未使用余量仍属于动作前数据,必须丢弃。
continue;
}
int remaining = targetSamples - collected[0].Count;
int postTake = Math.Min(remaining, block.SamplesPerChannel);
for (int channel = 0; channel < config.ChannelCount; channel++)
collected[channel].AddRange(block.Channels[channel].AsSpan(0, postTake).ToArray());
if (collected[0].Count >= targetSamples) break;
}
}
catch (OperationCanceledException ex) when (!ct.IsCancellationRequested && !actionInvoked)
{
string message =
$"PCIe8586 did not provide enough pre-trigger samples within {preTriggerTimeout.TotalMilliseconds:F0}ms; control action was not executed. " +
$"sampleRate={config.SampleRateHz:F0}Hz, channels={config.ChannelCount}, " +
$"preTrigger={request.PreTriggerMs:F1}ms, postTrigger={request.PostTriggerMs:F1}ms, " +
$"samples={collected[0].Count}/{preSamples}.";
_logger.LogWarning(message);
throw new InvalidOperationException(message, ex);
}
finally
{
await _digitizer.StopAsync();
}
if (!actionInvoked)
throw new InvalidOperationException(
$"PCIe8586 acquisition ended before enough pre-trigger samples were collected; control action was not executed. " +
$"sampleRate={config.SampleRateHz:F0}Hz, channels={config.ChannelCount}, " +
$"preTrigger={request.PreTriggerMs:F1}ms, postTrigger={request.PostTriggerMs:F1}ms, " +
$"samples={collected[0].Count}/{preSamples}.");
if (collected[0].Count < targetSamples)
throw new InvalidOperationException("PCIe8586 采集提前结束,后置波形不完整。");
return BuildCurve(collected.Select(x => x.ToArray()).ToArray(), config.SampleRateHz, triggerIndex);
}
finally
{
_operationLock.Release();
}
}
public async Task CaptureAsync(
int[] channels, int sampleRateHz, double durationSec, CancellationToken ct = default)
{
double preMs = Math.Max(1, durationSec * 1_000 * 0.2);
double postMs = Math.Max(1, durationSec * 1_000 - preMs);
return await CaptureAroundActionAsync(
new DaqActionCaptureRequest(preMs, postMs, true), _ => Task.CompletedTask, ct);
}
public async Task ReadInstantAsync(int[] channels, CancellationToken ct = default)
{
var curve = await CaptureAroundActionAsync(
new DaqActionCaptureRequest(1, 2, true), _ => Task.CompletedTask, ct);
return channels.Select(channel =>
{
var data = curve.EngineeringChannels.SingleOrDefault(x => x.PhysicalChannel == channel);
return data?.Samples.Average() ?? double.NaN;
}).ToArray();
}
public void ValidateOptions(bool requireOutputVoltage)
{
if (_options.ChannelCount is not (1 or 2 or 4 or 8))
throw new InvalidOperationException("PCIe8586 启用通道数只能为 1、2、4 或 8。");
if (_options.ClockDivider < 1)
throw new InvalidOperationException("PCIe8586 时钟分频必须大于 0。");
if (_options.PreTriggerMs <= 0 || _options.PostTriggerMs <= 0)
throw new InvalidOperationException("前置和后置采集窗口必须大于 0。");
var enabled = _options.Channels.Where(x => x.PhysicalChannel < _options.ChannelCount).ToArray();
foreach (var channel in enabled.Where(x => ParseRole(x.Role) != DaqChannelRole.Unconfigured))
{
if (!channel.IsCalibrated)
throw new InvalidOperationException($"PCIe8586 CH{channel.PhysicalChannel}({channel.Name})尚未标定。");
if (!double.IsFinite(channel.Scale) || channel.Scale == 0 || !double.IsFinite(channel.Offset))
throw new InvalidOperationException($"PCIe8586 CH{channel.PhysicalChannel} 标定比例或偏置无效。");
}
if (requireOutputVoltage &&
enabled.Count(x => ParseRole(x.Role) == DaqChannelRole.OutputVoltage && x.IsCalibrated) != 1)
throw new InvalidOperationException("曲线测试必须配置且只能配置一个已标定的“输出电压”通道。");
}
private async Task ConnectWithoutLockAsync(CancellationToken ct)
{
var devices = await EnumerateDevicesAsync(ct);
var selected = devices.SingleOrDefault(x => x.LogicalId == _options.LogicalDeviceId)
?? throw new InvalidOperationException($"未发现逻辑 ID {_options.LogicalDeviceId} 的 PCIe8586。");
await _digitizer.OpenAsync(
new DeviceInfo(selected.LogicalId, selected.PhysicalId, selected.Description, selected.IsSimulated), ct);
_connected = true;
ConnectionChanged?.Invoke(this, true);
}
private AcquisitionConfig CreateDriverConfig() => new(
_options.ChannelCount,
string.Equals(_options.InputRange, "PlusMinus1V", StringComparison.OrdinalIgnoreCase)
? DriverInputRange.PlusMinus1V
: DriverInputRange.PlusMinus5V,
_options.ClockDivider,
AcquisitionMode.Continuous,
10_000);
private CurveData BuildCurve(double[][] cardVolts, double sampleRateHz, int triggerIndex)
{
var engineering = new List();
for (int channel = 0; channel < cardVolts.Length; channel++)
{
var cfg = _options.Channels.FirstOrDefault(x => x.PhysicalChannel == channel)
?? new DaqChannelOptions { PhysicalChannel = channel, Name = $"CH{channel}" };
var role = ParseRole(cfg.Role);
var values = cardVolts[channel].Select(x => x * cfg.Scale + cfg.Offset).ToArray();
engineering.Add(new CurveChannelData(
channel, role, cfg.Name, UnitFor(role), cfg.Scale, cfg.Offset, values));
}
var voltage = engineering.Single(x => x.Role == DaqChannelRole.OutputVoltage).Samples;
var current = engineering.SingleOrDefault(x => x.Role == DaqChannelRole.OutputCurrent)?.Samples;
if (voltage.Length > 0)
{
int sample = Math.Clamp(triggerIndex, 0, voltage.Length - 1);
SampleReceived?.Invoke(this, new CurveSample(
sample / sampleRateHz,
voltage[sample],
current is { Length: > 0 } ? current[Math.Min(sample, current.Length - 1)] : 0));
}
return new CurveData
{
SampleRateHz = (int)Math.Round(sampleRateHz),
TriggerSampleIndex = triggerIndex,
TriggerTimeSec = triggerIndex / sampleRateHz,
Voltage = voltage,
Current = current,
EngineeringChannels = engineering
};
}
internal static DaqChannelRole ParseRole(string? role) =>
Enum.TryParse(role, true, out var value) ? value : DaqChannelRole.Unconfigured;
internal static string UnitFor(DaqChannelRole role) => role switch
{
DaqChannelRole.OutputVoltage or DaqChannelRole.InputVoltage => "V",
DaqChannelRole.OutputCurrent or DaqChannelRole.InputCurrent => "A",
_ => "V"
};
public void Dispose()
{
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
_digitizer.Dispose();
_operationLock.Dispose();
}
}