using System.Collections.ObjectModel; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using SSPCTester.Devices.Interfaces; using SSPCTester.Logic.Models; using SSPCTester.Logic.Testing; using SSPCTester.UI.Services; namespace SSPCTester.UI.ViewModels; /// 基础测试页 VM:24 通道 + 行内开/关按钮 + 统计条 + 一键测试全部。 public partial class BasicTestViewModel : ObservableObject { private const int SwitchSettleDelayMs = BasicTestCriteria.SwitchSettleDelayMs; private readonly ISspc _sspc; private readonly UiLogSink _log; private readonly object _pollLock = new(); private readonly HashSet _pollingChannels = new(); private CancellationTokenSource? _pollCts; private Task? _pollTask; private CancellationTokenSource? _runAllCts; [ObservableProperty] private bool _busy; [ObservableProperty] private bool _isBatchRunning; [ObservableProperty] private Project? _project; [ObservableProperty] private int _selectedRefreshRateHz = 5; public ObservableCollection Rows { get; } = new(); public IReadOnlyList RefreshRatesHz { get; } = new[] { 2, 5 }; public BasicTestViewModel(ISspc sspc, UiLogSink log) { _sspc = sspc; _log = log; } public int Total => Rows.Count; public int PassCount => Rows.Count(r => r.Status == TestStatus.Pass); public int FailCount => Rows.Count(r => r.Status == TestStatus.Fail); public int UntestedCount => Rows.Count(r => r.Status == TestStatus.Untested); public bool CanRunAll => !Busy && !IsBatchRunning; partial void OnBusyChanged(bool value) => OnPropertyChanged(nameof(CanRunAll)); partial void OnIsBatchRunningChanged(bool value) => OnPropertyChanged(nameof(CanRunAll)); public void SetProject(Project p) { StopAllPolling(); Project = p; Rows.Clear(); if (p.BasicResults.Count == 0) { for (int i = 1; i <= _sspc.ChannelCount; i++) p.BasicResults.Add(new ChannelResult { Channel = i }); } foreach (var r in p.BasicResults) { r.PropertyChanged += (_, _) => RaiseStats(); Rows.Add(r); } RaiseStats(); } private void RaiseStats() { Application.Current?.Dispatcher.Invoke(() => { OnPropertyChanged(nameof(PassCount)); OnPropertyChanged(nameof(FailCount)); OnPropertyChanged(nameof(UntestedCount)); OnPropertyChanged(nameof(Total)); }); } [RelayCommand] private async Task TurnOn(ChannelResult? row) { if (row == null) return; Busy = true; try { if (!_sspc.IsConnected) await _sspc.ConnectAsync(); row.Status = TestStatus.Running; await _sspc.TurnOnAsync(row.Channel); await Task.Delay(SwitchSettleDelayMs); var measurement = await _sspc.ReadMeasurementAsync(row.Channel); row.VoltageOn = measurement.Voltage; row.CurrentOn = Math.Abs(measurement.Current); StartPolling(row.Channel); _log.Add(UiLogLevel.Info, $"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A"); } catch (Exception ex) { await StopPollingAsync(row.Channel); row.Status = TestStatus.Fail; _log.Add(UiLogLevel.Error, $"CH{row.Channel} 开启或读取失败:{ex.Message}"); try { await _sspc.TurnOffAsync(row.Channel); } catch { } } finally { Busy = false; } } [RelayCommand] private async Task TurnOff(ChannelResult? row) { if (row == null) return; Busy = true; try { await StopPollingAsync(row.Channel); await _sspc.TurnOffAsync(row.Channel); await Task.Delay(SwitchSettleDelayMs); var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel); row.VoltageOff = offResult.Measurement.Voltage; bool onOk = BasicTestCriteria.IsOn(row.VoltageOn); row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail; string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格"; string offState = offResult.IsStableOff ? "已稳定关断" : "关断超时"; _log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error, $"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V {offState} {verdict}"); } catch (Exception ex) { await StopPollingAsync(row.Channel); row.Status = TestStatus.Fail; _log.Add(UiLogLevel.Error, $"CH{row.Channel} 关断或读取失败:{ex.Message}"); try { await _sspc.TurnOffAsync(row.Channel); } catch { } } finally { Busy = false; } } [RelayCommand] private async Task RunAll() { if (IsBatchRunning) return; _runAllCts?.Dispose(); _runAllCts = new CancellationTokenSource(); var ct = _runAllCts.Token; Busy = true; IsBatchRunning = true; try { foreach (var row in Rows) { ct.ThrowIfCancellationRequested(); await RunChannelAsync(row, ct); } _log.Add(UiLogLevel.Success, "基础测试全部完成。"); } catch (OperationCanceledException) { foreach (var row in Rows) { if (row.Status == TestStatus.Running) row.Status = TestStatus.Untested; } _log.Add(UiLogLevel.Warning, "基础测试批量测试已中断。"); } finally { _runAllCts?.Dispose(); _runAllCts = null; IsBatchRunning = false; Busy = false; } } [RelayCommand] private async Task CancelRunAll() { if (!IsBatchRunning) return; _runAllCts?.Cancel(); foreach (var row in Rows) { if (row.Status != TestStatus.Running) continue; try { await StopPollingAsync(row.Channel); await _sspc.TurnOffAsync(row.Channel); } catch { } } } private async Task RunChannelAsync(ChannelResult row, CancellationToken ct) { if (!_sspc.IsConnected) await _sspc.ConnectAsync(ct); row.Status = TestStatus.Running; await _sspc.TurnOnAsync(row.Channel, ct); await Task.Delay(SwitchSettleDelayMs, ct); var measurement = await _sspc.ReadMeasurementAsync(row.Channel, ct); row.VoltageOn = measurement.Voltage; row.CurrentOn = Math.Abs(measurement.Current); StartPolling(row.Channel); ct.ThrowIfCancellationRequested(); await StopPollingAsync(row.Channel); await _sspc.TurnOffAsync(row.Channel, ct); await Task.Delay(SwitchSettleDelayMs, ct); var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel, ct); row.VoltageOff = offResult.Measurement.Voltage; bool onOk = BasicTestCriteria.IsOn(row.VoltageOn); row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail; } [RelayCommand] private void Clear() { foreach (var r in Rows) { r.VoltageOn = 0; r.CurrentOn = 0; r.VoltageOff = 0; r.Status = TestStatus.Untested; } } private void StartPolling(int channel) { lock (_pollLock) { _pollingChannels.Add(channel); if (_pollTask is { IsCompleted: false }) return; _pollCts = new CancellationTokenSource(); _pollTask = PollMeasurementsAsync(_pollCts.Token); } } private async Task StopPollingAsync(int channel) { Task? taskToWait = null; CancellationTokenSource? ctsToDispose = null; lock (_pollLock) { _pollingChannels.Remove(channel); if (_pollingChannels.Count != 0 || _pollCts == null) return; _pollCts.Cancel(); taskToWait = _pollTask; ctsToDispose = _pollCts; _pollTask = null; _pollCts = null; } if (taskToWait != null) { try { await taskToWait; } catch (OperationCanceledException) { } } ctsToDispose?.Dispose(); } private void StopAllPolling() { lock (_pollLock) { _pollingChannels.Clear(); _pollCts?.Cancel(); _pollCts = null; _pollTask = null; } } private async Task PollMeasurementsAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { int refreshRate = Math.Max(1, SelectedRefreshRateHz); await Task.Delay(TimeSpan.FromSeconds(1.0 / refreshRate), ct); IReadOnlyList measurements; try { measurements = await _sspc.ReadMeasurementsAsync(ct); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; } catch (Exception ex) { _log.Add(UiLogLevel.Error, $"基础测试刷新失败:{ex.Message}"); continue; } HashSet activeChannels; lock (_pollLock) activeChannels = _pollingChannels.ToHashSet(); foreach (var measurement in measurements) { if (!activeChannels.Contains(measurement.PhysicalSlot)) continue; var row = Rows.FirstOrDefault(x => x.Channel == measurement.PhysicalSlot); if (row == null) continue; row.VoltageOn = measurement.Voltage; row.CurrentOn = Math.Abs(measurement.Current); } } } }