2026-06-30 12:10:29 +08:00
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
using Pcie8586Probe.Hardware;
|
|
|
|
|
using Pcie8586Probe.Models;
|
|
|
|
|
using SSPCTester.Devices.Config;
|
|
|
|
|
using SSPCTester.Devices.Drivers.Mock;
|
|
|
|
|
using SSPCTester.Devices.Interfaces;
|
|
|
|
|
using RealPcie8586Daq = SSPCTester.Devices.Drivers.Real.Pcie8586Daq;
|
|
|
|
|
|
|
|
|
|
namespace SSPCTester.Tests;
|
|
|
|
|
|
|
|
|
|
public sealed class DaqIntegrationTests
|
|
|
|
|
{
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task MockCaptureAroundAction_InvokesActionAndPlacesSoftwareMarker()
|
|
|
|
|
{
|
|
|
|
|
var options = new DeviceOptions();
|
|
|
|
|
options.Daq.ClockDivider = 1_000;
|
|
|
|
|
var daq = new MockDaq(Options.Create(options), NullLogger<MockDaq>.Instance);
|
|
|
|
|
await daq.ConnectAsync();
|
|
|
|
|
bool invoked = false;
|
|
|
|
|
|
|
|
|
|
var curve = await daq.CaptureAroundActionAsync(
|
|
|
|
|
new DaqActionCaptureRequest(10, 40, true),
|
|
|
|
|
_ =>
|
|
|
|
|
{
|
|
|
|
|
invoked = true;
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Assert.True(invoked);
|
|
|
|
|
Assert.Equal(1_000, curve.TriggerSampleIndex);
|
|
|
|
|
Assert.Equal(0.010, curve.TriggerTimeSec, 6);
|
|
|
|
|
Assert.Contains(curve.EngineeringChannels, x => x.Role == DaqChannelRole.OutputVoltage);
|
|
|
|
|
Assert.True(curve.Voltage[^1] > curve.Voltage[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task MockCaptureAroundAction_ExecutesSspcOnAndOffActions()
|
|
|
|
|
{
|
|
|
|
|
var options = new DeviceOptions();
|
|
|
|
|
var optionWrapper = Options.Create(options);
|
|
|
|
|
var daq = new MockDaq(optionWrapper, NullLogger<MockDaq>.Instance);
|
|
|
|
|
var sspc = new MockSspc(optionWrapper, NullLogger<MockSspc>.Instance);
|
|
|
|
|
await daq.ConnectAsync();
|
|
|
|
|
await sspc.ConnectAsync();
|
|
|
|
|
|
|
|
|
|
await daq.CaptureAroundActionAsync(
|
|
|
|
|
new DaqActionCaptureRequest(10, 40, true),
|
|
|
|
|
token => sspc.TurnOnAsync(3, token));
|
|
|
|
|
Assert.True(await sspc.GetChannelStateAsync(3));
|
|
|
|
|
|
|
|
|
|
await daq.CaptureAroundActionAsync(
|
|
|
|
|
new DaqActionCaptureRequest(10, 40, false),
|
|
|
|
|
token => sspc.TurnOffAsync(3, token));
|
|
|
|
|
Assert.False(await sspc.GetChannelStateAsync(3));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 18:32:20 +08:00
|
|
|
[Fact]
|
|
|
|
|
public async Task MockDaq_AppliesScaleAndOffset()
|
|
|
|
|
{
|
|
|
|
|
var options = new DeviceOptions();
|
|
|
|
|
options.Daq.ClockDivider = 100_000;
|
|
|
|
|
options.Daq.ChannelCount = 2;
|
|
|
|
|
options.Daq.Channels[0].Role = nameof(DaqChannelRole.OutputVoltage);
|
|
|
|
|
options.Daq.Channels[0].Name = "Vout";
|
|
|
|
|
options.Daq.Channels[0].Scale = 2;
|
|
|
|
|
options.Daq.Channels[0].Offset = 1;
|
|
|
|
|
options.Daq.Channels[0].IsCalibrated = true;
|
|
|
|
|
options.Daq.Channels[1].Role = nameof(DaqChannelRole.OutputCurrent);
|
|
|
|
|
options.Daq.Channels[1].Name = "Iout";
|
|
|
|
|
options.Daq.Channels[1].Scale = 3;
|
|
|
|
|
options.Daq.Channels[1].Offset = -1;
|
|
|
|
|
options.Daq.Channels[1].IsCalibrated = true;
|
|
|
|
|
var daq = new MockDaq(Options.Create(options), NullLogger<MockDaq>.Instance)
|
|
|
|
|
{
|
|
|
|
|
NominalVoltage = 10,
|
|
|
|
|
NominalCurrent = 1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var curve = await daq.CaptureAroundActionAsync(
|
|
|
|
|
new DaqActionCaptureRequest(10, 40, true),
|
|
|
|
|
_ => Task.CompletedTask);
|
|
|
|
|
var instant = await daq.ReadInstantAsync(new[] { 0, 1 });
|
|
|
|
|
|
|
|
|
|
Assert.True(curve.Voltage.Max() > 20);
|
|
|
|
|
Assert.True(curve.Current!.Max() > 1.8);
|
|
|
|
|
Assert.Contains(curve.EngineeringChannels, x =>
|
|
|
|
|
x.PhysicalChannel == 0 &&
|
|
|
|
|
x.Role == DaqChannelRole.OutputVoltage &&
|
|
|
|
|
x.Name == "Vout" &&
|
|
|
|
|
x.Scale == 2 &&
|
|
|
|
|
x.Offset == 1);
|
|
|
|
|
Assert.Contains(curve.EngineeringChannels, x =>
|
|
|
|
|
x.PhysicalChannel == 1 &&
|
|
|
|
|
x.Role == DaqChannelRole.OutputCurrent &&
|
|
|
|
|
x.Name == "Iout" &&
|
|
|
|
|
x.Scale == 3 &&
|
|
|
|
|
x.Offset == -1);
|
|
|
|
|
Assert.InRange(instant[0], 20.9, 21.1);
|
|
|
|
|
Assert.InRange(instant[1], 1.9, 2.1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
[Theory]
|
|
|
|
|
[InlineData(1000, 100_000)]
|
|
|
|
|
[InlineData(100, 1_000_000)]
|
|
|
|
|
public void DaqOptions_ComputesActualSampleRate(int divider, double expected)
|
|
|
|
|
{
|
|
|
|
|
var options = new DaqOptions { ClockDivider = divider };
|
|
|
|
|
Assert.Equal(expected, options.ActualSampleRateHz);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
[Fact]
|
2026-06-30 12:10:29 +08:00
|
|
|
public async Task RealCaptureAroundAction_InvokesActionAfterPreSamples()
|
|
|
|
|
{
|
|
|
|
|
var options = CreateRealDaqOptions();
|
|
|
|
|
var digitizer = FakeDigitizer.WithBlocks(
|
|
|
|
|
new SampleBlock(new[] { new[] { 0.1 } }, 100, 0, DateTimeOffset.Now),
|
|
|
|
|
new SampleBlock(new[] { new[] { 1.0 } }, 100, 1, DateTimeOffset.Now));
|
|
|
|
|
var daq = new RealPcie8586Daq(options.Daq, NullLogger<RealPcie8586Daq>.Instance, digitizer);
|
|
|
|
|
bool invoked = false;
|
|
|
|
|
|
|
|
|
|
var curve = await daq.CaptureAroundActionAsync(
|
|
|
|
|
new DaqActionCaptureRequest(10, 10, true),
|
|
|
|
|
_ =>
|
|
|
|
|
{
|
|
|
|
|
invoked = true;
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Assert.True(invoked);
|
|
|
|
|
Assert.True(digitizer.Stopped);
|
|
|
|
|
Assert.Equal(1, curve.TriggerSampleIndex);
|
|
|
|
|
Assert.Equal(new[] { 0.1, 1.0 }, curve.Voltage);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
[Fact]
|
2026-06-30 12:10:29 +08:00
|
|
|
public async Task RealCaptureAroundAction_TimesOutWhenPreSamplesNeverArrive()
|
|
|
|
|
{
|
|
|
|
|
var options = CreateRealDaqOptions();
|
|
|
|
|
var digitizer = FakeDigitizer.WithoutBlocks();
|
|
|
|
|
var daq = new RealPcie8586Daq(options.Daq, NullLogger<RealPcie8586Daq>.Instance, digitizer);
|
|
|
|
|
bool invoked = false;
|
|
|
|
|
|
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
|
|
|
daq.CaptureAroundActionAsync(
|
|
|
|
|
new DaqActionCaptureRequest(10, 10, true),
|
|
|
|
|
_ =>
|
|
|
|
|
{
|
|
|
|
|
invoked = true;
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
Assert.False(invoked);
|
|
|
|
|
Assert.True(digitizer.Stopped);
|
|
|
|
|
Assert.Contains("pre-trigger samples", ex.Message);
|
|
|
|
|
Assert.Contains("samples=0/1", ex.Message);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
[Fact]
|
|
|
|
|
public async Task RealCaptureAroundAction_StopsDigitizerWhenActionFails()
|
|
|
|
|
{
|
|
|
|
|
var options = CreateRealDaqOptions();
|
|
|
|
|
var digitizer = FakeDigitizer.WithBlocks(
|
|
|
|
|
new SampleBlock(new[] { new[] { 0.1 } }, 100, 0, DateTimeOffset.Now),
|
|
|
|
|
new SampleBlock(new[] { new[] { 1.0 } }, 100, 1, DateTimeOffset.Now));
|
|
|
|
|
var daq = new RealPcie8586Daq(options.Daq, NullLogger<RealPcie8586Daq>.Instance, digitizer);
|
|
|
|
|
|
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
|
|
|
daq.CaptureAroundActionAsync(
|
|
|
|
|
new DaqActionCaptureRequest(10, 10, true),
|
|
|
|
|
_ => throw new InvalidOperationException("relay failed")));
|
|
|
|
|
|
|
|
|
|
Assert.True(digitizer.Stopped);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task RealCaptureAroundAction_AppliesEngineeringChannelMetadata()
|
|
|
|
|
{
|
|
|
|
|
var options = CreateRealDaqOptions();
|
|
|
|
|
options.Daq.ChannelCount = 2;
|
|
|
|
|
options.Daq.Channels[0].Scale = 2;
|
|
|
|
|
options.Daq.Channels[0].Offset = 1;
|
|
|
|
|
options.Daq.Channels[1].Role = nameof(DaqChannelRole.OutputCurrent);
|
|
|
|
|
options.Daq.Channels[1].Name = "Iout";
|
|
|
|
|
options.Daq.Channels[1].Scale = 3;
|
|
|
|
|
options.Daq.Channels[1].Offset = -1;
|
|
|
|
|
options.Daq.Channels[1].IsCalibrated = true;
|
|
|
|
|
var digitizer = FakeDigitizer.WithBlocks(
|
|
|
|
|
new SampleBlock(new[] { new[] { 1.0 }, new[] { 4.0 } }, 100, 0, DateTimeOffset.Now),
|
|
|
|
|
new SampleBlock(new[] { new[] { 2.0 }, new[] { 5.0 } }, 100, 1, DateTimeOffset.Now));
|
|
|
|
|
var daq = new RealPcie8586Daq(options.Daq, NullLogger<RealPcie8586Daq>.Instance, digitizer);
|
|
|
|
|
|
|
|
|
|
var curve = await daq.CaptureAroundActionAsync(
|
|
|
|
|
new DaqActionCaptureRequest(10, 10, true),
|
|
|
|
|
_ => Task.CompletedTask);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(1, curve.TriggerSampleIndex);
|
|
|
|
|
Assert.Equal(new[] { 3.0, 5.0 }, curve.Voltage);
|
|
|
|
|
Assert.Equal(new[] { 11.0, 14.0 }, curve.Current);
|
|
|
|
|
Assert.Contains(curve.EngineeringChannels, x =>
|
|
|
|
|
x.PhysicalChannel == 1 &&
|
|
|
|
|
x.Role == DaqChannelRole.OutputCurrent &&
|
|
|
|
|
x.Name == "Iout" &&
|
|
|
|
|
x.Unit == "A" &&
|
|
|
|
|
x.Scale == 3 &&
|
|
|
|
|
x.Offset == -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task RealCaptureAsync_AllowsUnconfiguredChannels()
|
|
|
|
|
{
|
|
|
|
|
var options = new DeviceOptions();
|
|
|
|
|
options.Daq.UseMock = false;
|
|
|
|
|
options.Daq.ClockDivider = 1_000_000;
|
|
|
|
|
options.Daq.ChannelCount = 2;
|
|
|
|
|
options.Daq.Channels[1].Scale = 2;
|
|
|
|
|
options.Daq.Channels[1].Offset = 1;
|
|
|
|
|
var digitizer = FakeDigitizer.WithBlocks(
|
|
|
|
|
new SampleBlock(new[] { new[] { 0.1 }, new[] { 3.0 } }, 100, 0, DateTimeOffset.Now),
|
|
|
|
|
new SampleBlock(new[] { new[] { 0.2 }, new[] { 4.0 } }, 100, 1, DateTimeOffset.Now));
|
|
|
|
|
var daq = new RealPcie8586Daq(options.Daq, NullLogger<RealPcie8586Daq>.Instance, digitizer);
|
|
|
|
|
|
|
|
|
|
var curve = await daq.CaptureAsync(new[] { 1 }, 100, 0.02);
|
|
|
|
|
|
|
|
|
|
Assert.True(digitizer.Stopped);
|
|
|
|
|
Assert.Equal(new[] { 7.0, 9.0 }, curve.Voltage);
|
|
|
|
|
Assert.Contains(curve.EngineeringChannels, x =>
|
|
|
|
|
x.PhysicalChannel == 1 &&
|
|
|
|
|
x.Role == DaqChannelRole.Unconfigured &&
|
|
|
|
|
x.Scale == 2 &&
|
|
|
|
|
x.Offset == 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task RealReadInstant_AllowsConfiguredButUncalibratedRole()
|
|
|
|
|
{
|
|
|
|
|
var options = new DeviceOptions();
|
|
|
|
|
options.Daq.UseMock = false;
|
|
|
|
|
options.Daq.ClockDivider = 100_000;
|
|
|
|
|
options.Daq.ChannelCount = 1;
|
|
|
|
|
options.Daq.Channels[0].Role = nameof(DaqChannelRole.OutputVoltage);
|
|
|
|
|
options.Daq.Channels[0].IsCalibrated = false;
|
|
|
|
|
var digitizer = FakeDigitizer.WithBlocks(
|
|
|
|
|
new SampleBlock(new[] { new[] { 1.0, 2.0 } }, 100, 0, DateTimeOffset.Now),
|
|
|
|
|
new SampleBlock(new[] { new[] { 3.0 } }, 100, 2, DateTimeOffset.Now));
|
|
|
|
|
var daq = new RealPcie8586Daq(options.Daq, NullLogger<RealPcie8586Daq>.Instance, digitizer);
|
|
|
|
|
|
|
|
|
|
var readings = await daq.ReadInstantAsync(new[] { 0 });
|
|
|
|
|
|
|
|
|
|
Assert.True(digitizer.Stopped);
|
|
|
|
|
Assert.Equal(2.0, readings[0]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
private static DeviceOptions CreateRealDaqOptions()
|
|
|
|
|
{
|
|
|
|
|
var options = new DeviceOptions();
|
|
|
|
|
options.Daq.UseMock = false;
|
|
|
|
|
options.Daq.ClockDivider = 1_000_000;
|
|
|
|
|
options.Daq.ChannelCount = 1;
|
|
|
|
|
options.Daq.PreTriggerMs = 10;
|
|
|
|
|
options.Daq.PostTriggerMs = 10;
|
|
|
|
|
options.Daq.Channels[0].Role = nameof(DaqChannelRole.OutputVoltage);
|
|
|
|
|
options.Daq.Channels[0].Name = "Vout";
|
|
|
|
|
options.Daq.Channels[0].Scale = 1;
|
|
|
|
|
options.Daq.Channels[0].IsCalibrated = true;
|
|
|
|
|
return options;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class FakeDigitizer : IDigitizer
|
|
|
|
|
{
|
|
|
|
|
private readonly IReadOnlyList<SampleBlock>? _blocks;
|
|
|
|
|
private bool _open;
|
|
|
|
|
|
|
|
|
|
private FakeDigitizer(IReadOnlyList<SampleBlock>? blocks)
|
|
|
|
|
{
|
|
|
|
|
_blocks = blocks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool IsOpen => _open;
|
|
|
|
|
public bool Stopped { get; private set; }
|
|
|
|
|
|
|
|
|
|
public static FakeDigitizer WithBlocks(params SampleBlock[] blocks) => new(blocks);
|
|
|
|
|
public static FakeDigitizer WithoutBlocks() => new(null);
|
|
|
|
|
|
|
|
|
|
public ValueTask<IReadOnlyList<DeviceInfo>> EnumerateDevicesAsync(CancellationToken cancellationToken) =>
|
|
|
|
|
ValueTask.FromResult<IReadOnlyList<DeviceInfo>>(new[]
|
|
|
|
|
{
|
|
|
|
|
new DeviceInfo(0, 0, "fake", true)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
public ValueTask OpenAsync(DeviceInfo device, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
_open = true;
|
|
|
|
|
return ValueTask.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ValueTask ConfigureAsync(AcquisitionConfig config, CancellationToken cancellationToken) =>
|
|
|
|
|
ValueTask.CompletedTask;
|
|
|
|
|
|
|
|
|
|
public async IAsyncEnumerable<SampleBlock> StartAcquisitionAsync(
|
|
|
|
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (_blocks == null)
|
|
|
|
|
{
|
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
|
|
|
await Task.Delay(25, cancellationToken);
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var block in _blocks)
|
|
|
|
|
{
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
yield return block;
|
|
|
|
|
await Task.Yield();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ValueTask StopAsync()
|
|
|
|
|
{
|
|
|
|
|
Stopped = true;
|
|
|
|
|
return ValueTask.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ValueTask CloseAsync()
|
|
|
|
|
{
|
|
|
|
|
_open = false;
|
|
|
|
|
return ValueTask.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|