121 lines
4.1 KiB
C#
121 lines
4.1 KiB
C#
using System.Runtime.CompilerServices;
|
|
using Pcie8586Probe.Acquisition;
|
|
using Pcie8586Probe.Models;
|
|
|
|
namespace Pcie8586Probe.Hardware;
|
|
|
|
public sealed class SimulatedDigitizer : IDigitizer
|
|
{
|
|
private AcquisitionConfig _config = AcquisitionConfig.Default;
|
|
private bool _isOpen;
|
|
private long _nextSampleIndex;
|
|
|
|
public bool IsOpen => _isOpen;
|
|
|
|
public ValueTask<IReadOnlyList<DeviceInfo>> EnumerateDevicesAsync(CancellationToken cancellationToken)
|
|
{
|
|
IReadOnlyList<DeviceInfo> devices =
|
|
[
|
|
new DeviceInfo(0, 0, "\u6A21\u62DF PCIe8586M \u91C7\u96C6\u5361", true)
|
|
];
|
|
return ValueTask.FromResult(devices);
|
|
}
|
|
|
|
public ValueTask OpenAsync(DeviceInfo device, CancellationToken cancellationToken)
|
|
{
|
|
_isOpen = true;
|
|
_nextSampleIndex = 0;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public ValueTask ConfigureAsync(AcquisitionConfig config, CancellationToken cancellationToken)
|
|
{
|
|
config.Validate();
|
|
_config = config;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public async IAsyncEnumerable<SampleBlock> StartAcquisitionAsync([EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
if (!_isOpen)
|
|
{
|
|
throw new InvalidOperationException("Device is not open.");
|
|
}
|
|
|
|
var sampleRate = _config.SampleRateHz;
|
|
var blockSamples = (int)Math.Clamp(sampleRate / 30.0, 128, 8192);
|
|
var emitted = 0L;
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
var remaining = _config.Mode == AcquisitionMode.Finite
|
|
? _config.FiniteSamplesPerChannel - emitted
|
|
: long.MaxValue;
|
|
|
|
if (remaining <= 0)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
var samplesThisBlock = (int)Math.Min(blockSamples, remaining);
|
|
var raw = GenerateRaw(samplesThisBlock, _config.ChannelCount, sampleRate, _nextSampleIndex, _config.InputRange);
|
|
var channels = CodeConverter.Deinterleave(raw, _config.ChannelCount, _config.InputRange);
|
|
var block = new SampleBlock(channels, sampleRate, _nextSampleIndex, DateTimeOffset.Now);
|
|
|
|
_nextSampleIndex += samplesThisBlock;
|
|
emitted += samplesThisBlock;
|
|
yield return block;
|
|
|
|
var delayMs = Math.Clamp(samplesThisBlock / sampleRate * 1000.0, 1.0, 50.0);
|
|
await Task.Delay(TimeSpan.FromMilliseconds(delayMs), cancellationToken);
|
|
}
|
|
}
|
|
|
|
public ValueTask StopAsync()
|
|
{
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public ValueTask CloseAsync()
|
|
{
|
|
_isOpen = false;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_isOpen = false;
|
|
}
|
|
|
|
private static ushort[] GenerateRaw(int samplesPerChannel, int channelCount, double sampleRate, long startSample, InputRange range)
|
|
{
|
|
var raw = new ushort[samplesPerChannel * channelCount];
|
|
var fullScale = range == InputRange.PlusMinus5V ? 5.0 : 1.0;
|
|
|
|
for (var sample = 0; sample < samplesPerChannel; sample++)
|
|
{
|
|
var absoluteSample = startSample + sample;
|
|
var time = absoluteSample / sampleRate;
|
|
for (var channel = 0; channel < channelCount; channel++)
|
|
{
|
|
var frequency = 250.0 * (channel + 1);
|
|
var signal = Math.Sin(2.0 * Math.PI * frequency * time);
|
|
var harmonic = 0.15 * Math.Sin(2.0 * Math.PI * frequency * 0.31 * time + channel);
|
|
var noise = 0.01 * Math.Sin(2.0 * Math.PI * (97.0 + channel * 13.0) * time);
|
|
var volts = Math.Clamp((signal * 0.65 + harmonic + noise) * fullScale, -fullScale, fullScale);
|
|
raw[sample * channelCount + channel] = VoltsToCode(volts, range);
|
|
}
|
|
}
|
|
|
|
return raw;
|
|
}
|
|
|
|
private static ushort VoltsToCode(double volts, InputRange range)
|
|
{
|
|
var span = range == InputRange.PlusMinus5V ? 10.0 : 2.0;
|
|
var min = range == InputRange.PlusMinus5V ? -5.0 : -1.0;
|
|
var normalized = (volts - min) / span;
|
|
return (ushort)Math.Clamp(Math.Round(normalized * 65_536.0), 0, 65_535);
|
|
}
|
|
}
|