2026-06-30 12:10:29 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>PCIe8586 高速采集卡适配器。原生句柄仅由 IDigitizer 持有。</summary>
|
|
|
|
|
|
public sealed class Pcie8586Daq : IDaq, IDisposable
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly DaqOptions _options;
|
|
|
|
|
|
private readonly ILogger<Pcie8586Daq> _logger;
|
|
|
|
|
|
private readonly IDigitizer _digitizer;
|
|
|
|
|
|
private readonly SemaphoreSlim _operationLock = new(1, 1);
|
|
|
|
|
|
private bool _connected;
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
public Pcie8586Daq(IOptions<DeviceOptions> options, ILogger<Pcie8586Daq> logger, IDigitizer digitizer)
|
|
|
|
|
|
: this(options.Value.Daq, logger, digitizer)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal Pcie8586Daq(DaqOptions options, ILogger<Pcie8586Daq> logger, IDigitizer digitizer)
|
|
|
|
|
|
{
|
|
|
|
|
|
_options = options;
|
|
|
|
|
|
_logger = logger;
|
|
|
|
|
|
_digitizer = digitizer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsConnected => _connected && _digitizer.IsOpen;
|
|
|
|
|
|
public string DisplayName => "PCIe8586 高速采集卡";
|
|
|
|
|
|
public event EventHandler<bool>? ConnectionChanged;
|
|
|
|
|
|
public event EventHandler<CurveSample>? SampleReceived;
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<IReadOnlyList<DaqDeviceDescriptor>> 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<bool> 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<CurveData> CaptureAroundActionAsync(
|
|
|
|
|
|
DaqActionCaptureRequest request,
|
|
|
|
|
|
Func<CancellationToken, Task> action,
|
|
|
|
|
|
CancellationToken ct = default)
|
|
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
ArgumentNullException.ThrowIfNull(action);
|
|
|
|
|
|
if (request.PreTriggerMs <= 0 || request.PostTriggerMs <= 0)
|
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(request), "Pre/post trigger windows must be positive.");
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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<double>(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 采集提前结束,后置波形不完整。");
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
return BuildCurve(
|
|
|
|
|
|
collected.Select(x => x.ToArray()).ToArray(),
|
|
|
|
|
|
config.SampleRateHz,
|
|
|
|
|
|
triggerIndex,
|
|
|
|
|
|
requireOutputVoltage: true);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_operationLock.Release();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<CurveData> CaptureAsync(
|
|
|
|
|
|
int[] channels, int sampleRateHz, double durationSec, CancellationToken ct = default)
|
|
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
ArgumentNullException.ThrowIfNull(channels);
|
|
|
|
|
|
if (channels.Length == 0)
|
|
|
|
|
|
throw new ArgumentException("At least one channel must be requested.", nameof(channels));
|
|
|
|
|
|
if (durationSec <= 0 || !double.IsFinite(durationSec))
|
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(durationSec), "Capture duration must be positive.");
|
|
|
|
|
|
|
|
|
|
|
|
ValidateOptions(requireOutputVoltage: false);
|
|
|
|
|
|
await _operationLock.WaitAsync(ct);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!IsConnected) await ConnectWithoutLockAsync(ct);
|
|
|
|
|
|
var config = CreateDriverConfig();
|
|
|
|
|
|
var requestedChannels = channels.Distinct().ToArray();
|
|
|
|
|
|
foreach (int channel in requestedChannels)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (channel < 0 || channel >= config.ChannelCount)
|
|
|
|
|
|
throw new ArgumentOutOfRangeException(
|
|
|
|
|
|
nameof(channels),
|
|
|
|
|
|
$"Requested channel {channel} is outside the configured PCIe8586 channel count {config.ChannelCount}.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await _digitizer.ConfigureAsync(config, ct);
|
|
|
|
|
|
|
|
|
|
|
|
int targetSamples = Math.Max(1, (int)Math.Round(config.SampleRateHz * durationSec));
|
|
|
|
|
|
var collected = Enumerable.Range(0, config.ChannelCount)
|
|
|
|
|
|
.Select(_ => new List<double>(targetSamples)).ToArray();
|
|
|
|
|
|
var acquisitionTimeout = TimeSpan.FromMilliseconds(Math.Max(3_000, durationSec * 5_000));
|
|
|
|
|
|
using var acquisitionCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
|
|
|
|
acquisitionCts.CancelAfter(acquisitionTimeout);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await foreach (var block in _digitizer.StartAcquisitionAsync(acquisitionCts.Token))
|
|
|
|
|
|
{
|
|
|
|
|
|
int remaining = targetSamples - collected[0].Count;
|
|
|
|
|
|
int take = Math.Min(remaining, block.SamplesPerChannel);
|
|
|
|
|
|
for (int channel = 0; channel < config.ChannelCount; channel++)
|
|
|
|
|
|
collected[channel].AddRange(block.Channels[channel].AsSpan(0, take).ToArray());
|
|
|
|
|
|
if (collected[0].Count >= targetSamples) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException ex) when (!ct.IsCancellationRequested)
|
|
|
|
|
|
{
|
|
|
|
|
|
string message =
|
|
|
|
|
|
$"PCIe8586 did not provide enough samples within {acquisitionTimeout.TotalMilliseconds:F0}ms. " +
|
|
|
|
|
|
$"sampleRate={config.SampleRateHz:F0}Hz, channels={config.ChannelCount}, " +
|
|
|
|
|
|
$"duration={durationSec:F3}s, samples={collected[0].Count}/{targetSamples}.";
|
|
|
|
|
|
_logger.LogWarning(message);
|
|
|
|
|
|
throw new InvalidOperationException(message, ex);
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
await _digitizer.StopAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (collected[0].Count < targetSamples)
|
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
|
$"PCIe8586 acquisition ended before the requested sample window completed; " +
|
|
|
|
|
|
$"samples={collected[0].Count}/{targetSamples}.");
|
|
|
|
|
|
|
|
|
|
|
|
return BuildCurve(
|
|
|
|
|
|
collected.Select(x => x.ToArray()).ToArray(),
|
|
|
|
|
|
config.SampleRateHz,
|
|
|
|
|
|
triggerIndex: 0,
|
|
|
|
|
|
requireOutputVoltage: false,
|
|
|
|
|
|
primaryPhysicalChannel: requestedChannels[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_operationLock.Release();
|
|
|
|
|
|
}
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<double[]> ReadInstantAsync(int[] channels, CancellationToken ct = default)
|
|
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
ArgumentNullException.ThrowIfNull(channels);
|
|
|
|
|
|
if (channels.Length == 0) return Array.Empty<double>();
|
|
|
|
|
|
|
|
|
|
|
|
var curve = await CaptureAsync(channels, _options.SampleRateHz, durationSec: 0.003, ct);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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();
|
2026-07-01 20:04:27 +08:00
|
|
|
|
foreach (var channel in enabled.Where(x => requireOutputVoltage && ParseRole(x.Role) != DaqChannelRole.Unconfigured))
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private CurveData BuildCurve(
|
|
|
|
|
|
double[][] cardVolts,
|
|
|
|
|
|
double sampleRateHz,
|
|
|
|
|
|
int triggerIndex,
|
|
|
|
|
|
bool requireOutputVoltage,
|
|
|
|
|
|
int? primaryPhysicalChannel = null)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
var engineering = new List<CurveChannelData>();
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
var outputVoltage = engineering.Where(x => x.Role == DaqChannelRole.OutputVoltage).ToArray();
|
|
|
|
|
|
if (requireOutputVoltage && outputVoltage.Length != 1)
|
|
|
|
|
|
throw new InvalidOperationException("Curve capture requires exactly one configured OutputVoltage channel.");
|
|
|
|
|
|
|
|
|
|
|
|
var primary = !requireOutputVoltage && primaryPhysicalChannel.HasValue
|
|
|
|
|
|
? engineering.FirstOrDefault(x => x.PhysicalChannel == primaryPhysicalChannel) ?? engineering.FirstOrDefault()
|
|
|
|
|
|
: outputVoltage.Length == 1
|
|
|
|
|
|
? outputVoltage[0]
|
|
|
|
|
|
: engineering.FirstOrDefault();
|
|
|
|
|
|
var voltage = primary?.Samples ?? Array.Empty<double>();
|
|
|
|
|
|
var current = engineering.FirstOrDefault(x => x.Role == DaqChannelRole.OutputCurrent)?.Samples;
|
|
|
|
|
|
if (outputVoltage.Length == 1 && voltage.Length > 0)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
int sample = Math.Clamp(triggerIndex, 0, voltage.Length - 1);
|
|
|
|
|
|
SampleReceived?.Invoke(this, new CurveSample(
|
|
|
|
|
|
sample / sampleRateHz,
|
2026-07-01 20:04:27 +08:00
|
|
|
|
outputVoltage[0].Samples[sample],
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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<DaqChannelRole>(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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|