SSPC-Tester/SSPCTester.UI/ViewModels/BasicTestViewModel.cs
2026-07-03 14:16:15 +08:00

333 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>基础测试页 VM24 通道 + 行内开/关按钮 + 统计条 + 一键测试全部。</summary>
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<int> _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<ChannelResult> Rows { get; } = new();
public IReadOnlyList<int> 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);
row.DisplayVoltage = row.VoltageOn;
row.DisplayCurrent = row.CurrentOn;
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
{
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;
row.CurrentOff = Math.Abs(offResult.Measurement.Current);
row.DisplayVoltage = row.VoltageOff;
row.DisplayCurrent = row.CurrentOff;
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
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 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 _sspc.ReadMeasurementAsync(row.Channel, ct);
row.VoltageOn = measurement.Voltage;
row.CurrentOn = Math.Abs(measurement.Current);
row.DisplayVoltage = row.VoltageOn;
row.DisplayCurrent = row.CurrentOn;
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);
row.VoltageOff = offResult.Measurement.Voltage;
row.CurrentOff = Math.Abs(offResult.Measurement.Current);
row.DisplayVoltage = row.VoltageOff;
row.DisplayCurrent = row.CurrentOff;
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 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 PollMeasurementsAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
int refreshRate = Math.Max(1, SelectedRefreshRateHz);
await Task.Delay(TimeSpan.FromSeconds(1.0 / refreshRate), ct);
IReadOnlyList<ChannelMeasurement> measurements;
try
{
measurements = await _sspc.ReadMeasurementsAsync(ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Error, $"基础测试刷新失败:{ex.Message}");
continue;
}
HashSet<int> 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);
row.DisplayVoltage = row.VoltageOn;
row.DisplayCurrent = row.CurrentOn;
}
}
}
}