SSPC-Tester/Driver/PCIE8586/Pcie8586Probe/ViewModels/MainViewModel.cs

438 lines
13 KiB
C#
Raw Permalink Normal View History

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Pcie8586Probe.Acquisition;
using Pcie8586Probe.Hardware;
using Pcie8586Probe.Models;
namespace Pcie8586Probe.ViewModels;
public sealed partial class MainViewModel : ObservableObject, IDisposable
{
private readonly IDigitizer _simulatedDigitizer = new SimulatedDigitizer();
private readonly IDigitizer _hardwareDigitizer = new Pcie8586Digitizer();
private readonly Recorder _recorder = new();
private readonly object _frameGate = new();
private readonly SynchronizationContext? _uiContext;
private CancellationTokenSource? _acquisitionCts;
private Task? _acquisitionTask;
private IDigitizer? _activeDigitizer;
private RingHistoryBuffer? _history;
private SampleBlock? _lastCapturedBlock;
private SampleBlock? _latestFrame;
private SampleBlock? _latestRenderFrame;
private SampleBlock? _pendingPlotBlock;
private bool _plotClearRequested;
private InputRange _configuredRange = InputRange.PlusMinus5V;
public MainViewModel()
{
_uiContext = SynchronizationContext.Current;
OutputDirectory = Path.Combine(AppContext.BaseDirectory, "Output");
ChannelOptions = [1, 2, 4, 8];
ClockDividerOptions = [1, 2, 5, 10, 20, 50, 100, 1_000, 10_000, 100_000];
RangeOptions = [new InputRangeOption(InputRange.PlusMinus5V, "\u00B15 V"), new InputRangeOption(InputRange.PlusMinus1V, "\u00B11 V")];
SelectedRangeOption = RangeOptions[0];
_ = RefreshDevicesAsync();
}
public ObservableCollection<DeviceInfo> Devices { get; } = [];
public ObservableCollection<ChannelRow> ChannelRows { get; } = [];
public IReadOnlyList<int> ChannelOptions { get; }
public IReadOnlyList<int> ClockDividerOptions { get; }
public IReadOnlyList<InputRangeOption> RangeOptions { get; }
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(OpenCommand))]
private DeviceInfo? selectedDevice;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartCommand))]
[NotifyCanExecuteChangedFor(nameof(CloseCommand))]
private bool isOpen;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartCommand))]
[NotifyCanExecuteChangedFor(nameof(StopCommand))]
[NotifyCanExecuteChangedFor(nameof(RecordCommand))]
private bool isRunning;
[ObservableProperty]
private bool includeSimulatedDevice;
[ObservableProperty]
private string statusText = "\u5C31\u7EEA";
[ObservableProperty]
private int selectedChannelCount = 1;
[ObservableProperty]
private InputRangeOption selectedRangeOption;
[ObservableProperty]
private int selectedClockDivider = 100;
[ObservableProperty]
private long finiteSamplesPerChannel = 10_000;
[ObservableProperty]
private bool useFiniteMode;
[ObservableProperty]
private string outputDirectory;
[ObservableProperty]
private int clipStartSample;
[ObservableProperty]
private int clipEndSample;
[ObservableProperty]
private double recordingProgress;
[ObservableProperty]
private string lastRecordingPath = string.Empty;
[ObservableProperty]
private string lastOutputFolder = string.Empty;
public double ActualSampleRateHz => AcquisitionConfig.BaseSampleRateHz / SelectedClockDivider;
public InputRange ConfiguredRange => _configuredRange;
partial void OnSelectedClockDividerChanged(int value)
{
OnPropertyChanged(nameof(ActualSampleRateHz));
}
partial void OnSelectedChannelCountChanged(int value)
{
ResetChannelRows(value);
}
partial void OnIncludeSimulatedDeviceChanged(bool value)
{
_ = RefreshDevicesAsync();
}
[RelayCommand]
private async Task RefreshDevicesAsync()
{
Devices.Clear();
SelectedDevice = null;
foreach (var device in await _hardwareDigitizer.EnumerateDevicesAsync(CancellationToken.None))
{
Devices.Add(device);
}
if (IncludeSimulatedDevice)
{
foreach (var device in await _simulatedDigitizer.EnumerateDevicesAsync(CancellationToken.None))
{
Devices.Add(device);
}
}
SelectedDevice = Devices.FirstOrDefault();
StatusText = string.Format("\u5DF2\u627E\u5230 {0} \u4E2A\u8BBE\u5907", Devices.Count);
}
private bool CanOpen() => SelectedDevice is not null && !IsOpen;
[RelayCommand(CanExecute = nameof(CanOpen))]
private async Task OpenAsync()
{
if (SelectedDevice is null)
{
return;
}
_activeDigitizer = SelectedDevice.IsSimulated ? _simulatedDigitizer : _hardwareDigitizer;
await _activeDigitizer.OpenAsync(SelectedDevice, CancellationToken.None);
IsOpen = true;
StatusText = string.Format("\u5DF2\u6253\u5F00 {0}", SelectedDevice.Description);
}
private bool CanClose() => IsOpen && !IsRunning;
[RelayCommand(CanExecute = nameof(CanClose))]
private async Task CloseAsync()
{
if (_activeDigitizer is not null)
{
await _activeDigitizer.CloseAsync();
}
IsOpen = false;
StatusText = "\u5DF2\u5173\u95ED";
}
private bool CanStart() => IsOpen && !IsRunning;
[RelayCommand(CanExecute = nameof(CanStart))]
private async Task StartAsync()
{
if (_activeDigitizer is null)
{
return;
}
var config = CurrentConfig();
await _activeDigitizer.ConfigureAsync(config, CancellationToken.None);
_configuredRange = config.InputRange;
_history = new RingHistoryBuffer(config.SampleRateHz, seconds: 10);
_lastCapturedBlock = null;
_pendingPlotBlock = null;
_acquisitionCts = new CancellationTokenSource();
ChannelRows.Clear();
_latestRenderFrame = null;
IsRunning = true;
StatusText = "\u6B63\u5728\u91C7\u96C6";
_acquisitionTask = Task.Run(async () =>
{
try
{
await foreach (var block in _activeDigitizer.StartAcquisitionAsync(_acquisitionCts.Token))
{
lock (_frameGate)
{
_latestFrame = block;
}
_history.Add(block);
var completed = _recorder.Add(block);
if (completed is not null)
{
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var result = await WaveformWriter.WriteAsync(completed, OutputDirectory, $"record_{stamp}", _configuredRange, _acquisitionCts.Token);
RunOnUi(() =>
{
_lastCapturedBlock = completed;
_pendingPlotBlock = completed;
LastRecordingPath = result.BasePath;
LastOutputFolder = Path.GetDirectoryName(result.BasePath) ?? OutputDirectory;
StatusText = string.Format("\u5F55\u5236\u5B8C\u6210\uFF1A\u6BCF\u901A\u9053 {0} \u70B9", completed.SamplesPerChannel);
});
}
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
RunOnUi(() => StatusText = ex.Message);
}
finally
{
RunOnUi(() => IsRunning = false);
}
});
}
private bool CanStop() => IsRunning;
[RelayCommand(CanExecute = nameof(CanStop))]
private async Task StopAsync()
{
_acquisitionCts?.Cancel();
if (_acquisitionTask is not null)
{
try
{
await _acquisitionTask;
}
catch (OperationCanceledException)
{
}
}
if (_activeDigitizer is not null)
{
await _activeDigitizer.StopAsync();
}
IsRunning = false;
StatusText = "\u5DF2\u505C\u6B62";
}
private bool CanRecord() => IsRunning;
[RelayCommand(CanExecute = nameof(CanRecord))]
private void Record(string durationMs)
{
if (!int.TryParse(durationMs, out var milliseconds))
{
StatusText = "\u5F55\u5236\u65F6\u957F\u65E0\u6548";
return;
}
_recorder.Start(TimeSpan.FromMilliseconds(milliseconds), ActualSampleRateHz);
RecordingProgress = 0;
StatusText = string.Format("\u6B63\u5728\u5F55\u5236 {0} ms", milliseconds);
}
[RelayCommand]
private async Task SaveClipAsync()
{
if (_history is null)
{
StatusText = "\u6682\u65E0\u5386\u53F2\u6570\u636E";
return;
}
var block = _history.Slice(ClipStartSample, ClipEndSample);
if (block is null)
{
StatusText = "\u526A\u8F91\u8303\u56F4\u65E0\u6548";
return;
}
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var result = await WaveformWriter.WriteAsync(block, OutputDirectory, $"clip_{stamp}", _configuredRange, CancellationToken.None);
_lastCapturedBlock = block;
_pendingPlotBlock = block;
LastRecordingPath = result.BasePath;
LastOutputFolder = Path.GetDirectoryName(result.BasePath) ?? OutputDirectory;
StatusText = string.Format("\u526A\u8F91\u5DF2\u4FDD\u5B58\uFF1A\u6BCF\u901A\u9053 {0} \u70B9", block.SamplesPerChannel);
}
[RelayCommand]
private void OpenLastFolder()
{
var folder = Directory.Exists(LastOutputFolder) ? LastOutputFolder : OutputDirectory;
Directory.CreateDirectory(folder);
Process.Start(new ProcessStartInfo
{
FileName = folder,
UseShellExecute = true
});
}
[RelayCommand]
private void Reset()
{
ChannelRows.Clear();
RecordingProgress = 0;
ClipStartSample = 0;
ClipEndSample = 0;
LastRecordingPath = string.Empty;
LastOutputFolder = string.Empty;
_lastCapturedBlock = null;
_pendingPlotBlock = null;
_latestFrame = null;
_latestRenderFrame = null;
_plotClearRequested = true;
StatusText = IsRunning ? "\u6B63\u5728\u91C7\u96C6" : "\u5DF2\u91CD\u7F6E";
}
public SampleBlock? ConsumeLatestFrame()
{
lock (_frameGate)
{
var frame = _latestFrame;
_latestFrame = null;
_latestRenderFrame = frame ?? _latestRenderFrame;
RecordingProgress = _recorder.Progress;
return frame;
}
}
public SampleBlock? ConsumePendingPlotBlock()
{
var block = _pendingPlotBlock;
_pendingPlotBlock = null;
return block;
}
public bool ConsumePlotClearRequest()
{
var requested = _plotClearRequested;
_plotClearRequested = false;
return requested;
}
public void AppendTableRows(SampleBlock block, int maxRows)
{
if (maxRows <= 0 || block.SamplesPerChannel == 0)
{
return;
}
var rowsToAdd = new List<ChannelRow>();
var startSample = Math.Max(0, block.SamplesPerChannel - Math.Max(1, maxRows / Math.Max(1, block.ChannelCount)));
for (var sample = startSample; sample < block.SamplesPerChannel; sample++)
{
for (var channel = 0; channel < block.ChannelCount; channel++)
{
rowsToAdd.Add(new ChannelRow(block.StartSampleIndex + sample, channel, block.Channels[channel][sample]));
}
}
rowsToAdd.Sort(static (left, right) =>
{
var sampleCompare = right.SampleIndex.CompareTo(left.SampleIndex);
return sampleCompare != 0 ? sampleCompare : string.CompareOrdinal(left.Channel, right.Channel);
});
for (var index = rowsToAdd.Count - 1; index >= 0; index--)
{
ChannelRows.Insert(0, rowsToAdd[index]);
}
while (ChannelRows.Count > maxRows)
{
ChannelRows.RemoveAt(ChannelRows.Count - 1);
}
ClipEndSample = Math.Max(ClipEndSample, (int)Math.Min(int.MaxValue, _history?.SamplesPerChannel ?? block.SamplesPerChannel));
}
public void Dispose()
{
_acquisitionCts?.Cancel();
_simulatedDigitizer.Dispose();
_hardwareDigitizer.Dispose();
}
private AcquisitionConfig CurrentConfig()
{
return new AcquisitionConfig(
SelectedChannelCount,
SelectedRangeOption.Value,
SelectedClockDivider,
UseFiniteMode ? AcquisitionMode.Finite : AcquisitionMode.Continuous,
FiniteSamplesPerChannel);
}
private void ResetChannelRows(int count)
{
ChannelRows.Clear();
}
private void RunOnUi(Action action)
{
if (_uiContext is null || SynchronizationContext.Current == _uiContext)
{
action();
return;
}
_uiContext.Post(_ => action(), null);
}
}
public sealed record InputRangeOption(InputRange Value, string DisplayName)
{
public override string ToString() => DisplayName;
}