2026-06-30 12:10:29 +08:00
|
|
|
|
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>基础测试页 VM:24 通道 + 行内开/关按钮 + 统计条 + 一键测试全部。</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);
|
2026-07-06 17:26:09 +08:00
|
|
|
|
var measurement = await ReadDisplayedMeasurementAsync(row.Channel, CancellationToken.None);
|
|
|
|
|
|
ApplyOnMeasurement(row, measurement);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
|
|
|
|
|
|
row.Status = onOk ? TestStatus.Running : TestStatus.Fail;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
StartPolling(row.Channel);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_log.Add(onOk ? UiLogLevel.Info : UiLogLevel.Error,
|
|
|
|
|
|
$"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A {(onOk ? "开状态合格" : "开状态不合格")}");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-07-06 17:26:09 +08:00
|
|
|
|
if (!_sspc.IsConnected) await _sspc.ConnectAsync();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
await StopPollingAsync(row.Channel);
|
|
|
|
|
|
await _sspc.TurnOffAsync(row.Channel);
|
|
|
|
|
|
await Task.Delay(SwitchSettleDelayMs);
|
2026-07-06 17:26:09 +08:00
|
|
|
|
var offResult = await BasicTestCriteria.WaitForStableOffAsync(
|
|
|
|
|
|
_sspc,
|
|
|
|
|
|
row.Channel,
|
|
|
|
|
|
onSample: measurement => ApplyOffMeasurement(row, measurement),
|
|
|
|
|
|
readSampleAsync: ReadDisplayedMeasurementAsync);
|
|
|
|
|
|
ApplyOffMeasurement(row, offResult.Measurement);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
|
2026-07-06 17:26:09 +08:00
|
|
|
|
bool offOk = offResult.IsStableOff;
|
|
|
|
|
|
row.Status = (onOk && offOk) ? TestStatus.Pass : TestStatus.Fail;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
|
2026-07-06 17:26:09 +08:00
|
|
|
|
string offState = offOk ? "已稳定关断" : "关断超时";
|
2026-06-30 12:10:29 +08:00
|
|
|
|
_log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error,
|
2026-07-03 14:16:15 +08:00
|
|
|
|
$"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V I={row.CurrentOff:F3}A {offState} {verdict}");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
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;
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{row.Channel} 基础测试开始:执行开状态测试。");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
await _sspc.TurnOnAsync(row.Channel, ct);
|
|
|
|
|
|
await Task.Delay(SwitchSettleDelayMs, ct);
|
|
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
var measurement = await ReadDisplayedMeasurementAsync(row.Channel, ct);
|
|
|
|
|
|
ApplyOnMeasurement(row, measurement);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
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 ? "开状态合格" : "开状态不合格")}");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
StartPolling(row.Channel);
|
|
|
|
|
|
|
|
|
|
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
|
|
await StopPollingAsync(row.Channel);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{row.Channel} 执行关状态测试。");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
await _sspc.TurnOffAsync(row.Channel, ct);
|
|
|
|
|
|
await Task.Delay(SwitchSettleDelayMs, ct);
|
|
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
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;
|
2026-07-03 14:16:15 +08:00
|
|
|
|
string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
|
2026-07-06 17:26:09 +08:00
|
|
|
|
string offState = offOk ? "已稳定关断" : "关断超时";
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_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}");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void Clear()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var r in Rows)
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
r.VoltageOn = 0; r.CurrentOn = 0; r.VoltageOff = 0; r.CurrentOff = 0;
|
|
|
|
|
|
r.DisplayVoltage = 0; r.DisplayCurrent = 0;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
private async Task<ChannelMeasurement> 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
ApplyOnMeasurement(row, measurement);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|