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 ReadDisplayedMeasurementAsync(row.Channel, CancellationToken.None); ApplyOnMeasurement(row, measurement); bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn); row.Status = onOk ? TestStatus.Running : TestStatus.Fail; StartPolling(row.Channel); _log.Add(onOk ? UiLogLevel.Info : UiLogLevel.Error, $"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A {(onOk ? "开状态合格" : "开状态不合格")}"); } 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 { if (!_sspc.IsConnected) await _sspc.ConnectAsync(); await StopPollingAsync(row.Channel); await _sspc.TurnOffAsync(row.Channel); await Task.Delay(SwitchSettleDelayMs); var offResult = await BasicTestCriteria.WaitForStableOffAsync( _sspc, row.Channel, onSample: measurement => ApplyOffMeasurement(row, measurement), readSampleAsync: ReadDisplayedMeasurementAsync); ApplyOffMeasurement(row, offResult.Measurement); bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn); bool offOk = offResult.IsStableOff; row.Status = (onOk && offOk) ? TestStatus.Pass : TestStatus.Fail; string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格"; string offState = offOk ? "已稳定关断" : "关断超时"; _log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error, $"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V I={row.CurrentOff:F3}A {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; _log.Add(UiLogLevel.Info, $"CH{row.Channel} 基础测试开始:执行开状态测试。"); await _sspc.TurnOnAsync(row.Channel, ct); await Task.Delay(SwitchSettleDelayMs, ct); var measurement = await ReadDisplayedMeasurementAsync(row.Channel, ct); ApplyOnMeasurement(row, measurement); bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn); _log.Add(onOk ? UiLogLevel.Info : UiLogLevel.Error, $"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A {(onOk ? "开状态合格" : "开状态不合格")}"); StartPolling(row.Channel); ct.ThrowIfCancellationRequested(); await StopPollingAsync(row.Channel); _log.Add(UiLogLevel.Info, $"CH{row.Channel} 执行关状态测试。"); await _sspc.TurnOffAsync(row.Channel, ct); await Task.Delay(SwitchSettleDelayMs, ct); var offResult = await BasicTestCriteria.WaitForStableOffAsync( _sspc, row.Channel, ct, measurement => ApplyOffMeasurement(row, measurement), ReadDisplayedMeasurementAsync); ApplyOffMeasurement(row, offResult.Measurement); bool offOk = offResult.IsStableOff; row.Status = (onOk && offOk) ? TestStatus.Pass : TestStatus.Fail; string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格"; string offState = offOk ? "已稳定关断" : "关断超时"; _log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error, $"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V I={row.CurrentOff:F3}A {offState} {verdict}"); } [RelayCommand] private void Clear() { foreach (var r in Rows) { r.VoltageOn = 0; r.CurrentOn = 0; r.VoltageOff = 0; r.CurrentOff = 0; r.DisplayVoltage = 0; r.DisplayCurrent = 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 ReadDisplayedMeasurementAsync(int channel, CancellationToken ct) { var measurements = await _sspc.ReadMeasurementsAsync(ct); var measurement = measurements.FirstOrDefault(x => x.PhysicalSlot == channel); if (measurement != null) return measurement; return await _sspc.ReadMeasurementAsync(channel, ct); } private static void ApplyOnMeasurement(ChannelResult row, ChannelMeasurement measurement) { ApplyMeasurement(row, measurement, isOnMeasurement: true); } private static void ApplyOffMeasurement(ChannelResult row, ChannelMeasurement measurement) { ApplyMeasurement(row, measurement, isOnMeasurement: false); } private static void ApplyMeasurement(ChannelResult row, ChannelMeasurement measurement, bool isOnMeasurement) { void Apply() { double voltage = measurement.Voltage; double current = Math.Abs(measurement.Current); if (isOnMeasurement) { row.VoltageOn = voltage; row.CurrentOn = current; } else { row.VoltageOff = voltage; row.CurrentOff = current; } row.DisplayVoltage = voltage; row.DisplayCurrent = current; } var dispatcher = Application.Current?.Dispatcher; if (dispatcher == null || dispatcher.CheckAccess()) Apply(); else dispatcher.Invoke(Apply); } 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; ApplyOnMeasurement(row, measurement); } } } }