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.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.Instance); var sspc = new MockSspc(optionWrapper, NullLogger.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)); } [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.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); } [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); } [Fact] 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.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); } [Fact] public async Task RealCaptureAroundAction_TimesOutWhenPreSamplesNeverArrive() { var options = CreateRealDaqOptions(); var digitizer = FakeDigitizer.WithoutBlocks(); var daq = new RealPcie8586Daq(options.Daq, NullLogger.Instance, digitizer); bool invoked = false; var ex = await Assert.ThrowsAsync(() => 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); } [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.Instance, digitizer); await Assert.ThrowsAsync(() => 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.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.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.Instance, digitizer); var readings = await daq.ReadInstantAsync(new[] { 0 }); Assert.True(digitizer.Stopped); Assert.Equal(2.0, readings[0]); } 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? _blocks; private bool _open; private FakeDigitizer(IReadOnlyList? 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> EnumerateDevicesAsync(CancellationToken cancellationToken) => ValueTask.FromResult>(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 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() { } } }