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> EnumerateDevicesAsync(CancellationToken cancellationToken) { IReadOnlyList 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 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); } }