SSPC-Tester/SSPCTester.UI/ViewModels/CurveTestViewModel.cs
2026-06-30 12:10:29 +08:00

457 lines
16 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 CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Options;
using SSPCTester.Devices.Config;
using SSPCTester.Devices.Interfaces;
using SSPCTester.Logic.Calculation;
using SSPCTester.Logic.Models;
using SSPCTester.Logic.Storage;
using SSPCTester.Logic.Testing;
using SSPCTester.UI.Services;
namespace SSPCTester.UI.ViewModels;
/// <summary>曲线测试页 VM。</summary>
public partial class CurveTestViewModel : ObservableObject
{
private readonly ISspc _sspc;
private readonly IDaq _daq;
private readonly DaqOptions _daqOpts;
private readonly UiLogSink _log;
private readonly ProjectStore _store;
private int _loadVersion;
private int _viewWindowSaveVersion;
private bool _syncingViewWindow;
public ObservableCollection<CurveResultRow> Rows { get; } = new();
public ObservableCollection<int> Channels { get; } = new();
[ObservableProperty] private Project? _project;
[ObservableProperty] private int _selectedChannel;
[ObservableProperty] private CurveData? _risingCurve;
[ObservableProperty] private CurveData? _fallingCurve;
[ObservableProperty] private double _vMax = 28.0;
[ObservableProperty] private bool _busy;
[ObservableProperty] private CurveResultRow? _selectedRow;
[ObservableProperty] private double? _risingViewStartMs;
[ObservableProperty] private double? _risingViewEndMs;
[ObservableProperty] private double? _fallingViewStartMs;
[ObservableProperty] private double? _fallingViewEndMs;
public CurveTestViewModel(ISspc sspc, IDaq daq, IOptions<DeviceOptions> opt, UiLogSink log, ProjectStore store)
{
_sspc = sspc; _daq = daq; _daqOpts = opt.Value.Daq; _log = log;
_store = store;
for (int i = 1; i <= _sspc.ChannelCount; i++) Channels.Add(i);
SelectedChannel = 1;
}
public CurveThresholds Thresholds => Project?.CurveThresholds ?? new CurveThresholds();
public void SetProject(Project p)
{
Project = p;
Rows.Clear();
if (p.CurveResults.Count == 0)
{
for (int i = 1; i <= _sspc.ChannelCount; i++)
p.CurveResults.Add(new CurveResultRow { Channel = i });
}
foreach (var r in p.CurveResults) Rows.Add(r);
SyncSelectedRow();
OnPropertyChanged(nameof(Thresholds));
_ = LoadSelectedChannelCurvesAsync();
}
public void AttachSession(TestSessionViewModel s)
{
s.CurveReady += (ch, rising, falling) =>
{
if (ch == SelectedChannel)
{
RisingCurve = rising;
FallingCurve = falling;
}
};
}
partial void OnSelectedChannelChanged(int value)
{
SyncSelectedRow();
_ = LoadSelectedChannelCurvesAsync();
}
partial void OnRisingViewStartMsChanged(double? value)
{
if (_syncingViewWindow || SelectedRow == null) return;
SelectedRow.RisingViewStartMs = value;
SaveProjectQuietly();
}
partial void OnRisingViewEndMsChanged(double? value)
{
if (_syncingViewWindow || SelectedRow == null) return;
SelectedRow.RisingViewEndMs = value;
SaveProjectQuietly();
}
partial void OnFallingViewStartMsChanged(double? value)
{
if (_syncingViewWindow || SelectedRow == null) return;
SelectedRow.FallingViewStartMs = value;
SaveProjectQuietly();
}
partial void OnFallingViewEndMsChanged(double? value)
{
if (_syncingViewWindow || SelectedRow == null) return;
SelectedRow.FallingViewEndMs = value;
SaveProjectQuietly();
}
private void SyncSelectedRow()
{
_syncingViewWindow = true;
try
{
SelectedRow = Rows.FirstOrDefault(x => x.Channel == SelectedChannel)
?? Project?.CurveResults.FirstOrDefault(x => x.Channel == SelectedChannel);
RisingViewStartMs = SelectedRow?.RisingViewStartMs ?? SelectedRow?.ViewStartMs;
RisingViewEndMs = SelectedRow?.RisingViewEndMs ?? SelectedRow?.ViewEndMs;
FallingViewStartMs = SelectedRow?.FallingViewStartMs ?? SelectedRow?.ViewStartMs;
FallingViewEndMs = SelectedRow?.FallingViewEndMs ?? SelectedRow?.ViewEndMs;
}
finally
{
_syncingViewWindow = false;
}
}
[RelayCommand]
private void ResetRisingView()
{
if (SelectedRow == null) return;
RisingViewStartMs = null;
RisingViewEndMs = null;
SelectedRow.RisingViewStartMs = null;
SelectedRow.RisingViewEndMs = null;
SaveProjectQuietly();
}
[RelayCommand]
private void ResetFallingView()
{
if (SelectedRow == null) return;
FallingViewStartMs = null;
FallingViewEndMs = null;
SelectedRow.FallingViewStartMs = null;
SelectedRow.FallingViewEndMs = null;
SaveProjectQuietly();
}
private void SaveProjectQuietly()
{
var project = Project;
if (project == null) return;
int version = Interlocked.Increment(ref _viewWindowSaveVersion);
_ = SaveProjectQuietlyAsync(project, version);
}
private async Task SaveProjectQuietlyAsync(Project project, int version)
{
try
{
await Task.Delay(300);
if (version != _viewWindowSaveVersion) return;
await _store.SaveAsync(project);
}
catch (Exception ex) { _log.Add(UiLogLevel.Warning, $"曲线视图窗口保存失败:{ex.Message}"); }
}
private async Task LoadSelectedChannelCurvesAsync()
{
int version = ++_loadVersion;
var project = Project;
if (project == null)
{
RisingCurve = null;
FallingCurve = null;
return;
}
var row = Rows.FirstOrDefault(x => x.Channel == SelectedChannel)
?? project.CurveResults.FirstOrDefault(x => x.Channel == SelectedChannel);
if (row == null)
{
RisingCurve = null;
FallingCurve = null;
return;
}
try
{
var rising = await WaveformStore.LoadAsync(project, row.RisingWaveformPath);
var falling = await WaveformStore.LoadAsync(project, row.FallingWaveformPath);
if (version != _loadVersion) return;
RisingCurve = rising;
FallingCurve = falling;
if (rising != null)
VMax = CurveMetrics.EstimateVMax(rising.Voltage);
}
catch (Exception ex)
{
if (version != _loadVersion) return;
RisingCurve = null;
FallingCurve = null;
_log.Add(UiLogLevel.Warning, $"CH{SelectedChannel} 历史曲线加载失败:{ex.Message}");
}
}
/// <summary>仅用于"浏览历史项目"时的可视化预览:调用 MockDaq 合成一段波形,不操作 SSPC。</summary>
private async Task PreviewSelectedAsync()
{
if (Busy) return;
try
{
Busy = true;
var request = new DaqActionCaptureRequest(_daqOpts.PreTriggerMs, _daqOpts.PostTriggerMs, true);
var rising = await _daq.CaptureAroundActionAsync(request, _ => Task.CompletedTask);
var falling = await _daq.CaptureAroundActionAsync(request with { IsRising = false }, _ => Task.CompletedTask);
VMax = CurveMetrics.EstimateVMax(rising.Voltage);
RisingCurve = rising;
FallingCurve = falling;
}
catch { /* 预览失败不影响主流程 */ }
finally { Busy = false; }
}
[RelayCommand]
private void OpenPopup()
{
var win = new Views.CurvePopupWindow(this) { Owner = System.Windows.Application.Current?.MainWindow };
win.Show();
}
[RelayCommand]
private async Task CaptureRising()
{
if (Project == null) return;
Busy = true;
try
{
int ch = SelectedChannel;
await EnsureCurveCaptureReadyAsync(ch);
await _sspc.TurnOffAsync(ch);
await BasicTestCriteria.WaitForStableOffAsync(_sspc, ch);
_log.Add(UiLogLevel.Info, $"CH{ch} 开始预采集,待触发后发送开启动作。");
var rising = await _daq.CaptureAroundActionAsync(
new DaqActionCaptureRequest(_daqOpts.PreTriggerMs, _daqOpts.PostTriggerMs, true),
async token =>
{
_log.Add(UiLogLevel.Info, $"CH{ch} 触发开启动作命令。");
await _sspc.TurnOnAsync(ch, token);
});
await ApplyRisingCaptureAsync(Project, ch, rising);
_log.Add(UiLogLevel.Success, $"CH{ch} 开启曲线采集完成。");
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Error,
$"开启曲线采集失败:{ex.GetType().Name} - {ex.Message}CH{SelectedChannel}SSPC连接={_sspc.IsConnected}DAQ连接={_daq.IsConnected}");
try { await _sspc.TurnOffAsync(SelectedChannel); } catch { }
}
finally { Busy = false; }
}
[RelayCommand]
private async Task CaptureFalling()
{
if (Project == null) return;
Busy = true;
try
{
int ch = SelectedChannel;
await EnsureCurveCaptureReadyAsync(ch);
_log.Add(UiLogLevel.Info, $"CH{ch} 准备采集关断曲线:先确保通道已开启并稳定。");
await _sspc.TurnOnAsync(ch);
await WaitForStableOnAsync(ch);
_log.Add(UiLogLevel.Info, $"CH{ch} 开始预采集,待触发后发送关断动作。");
var falling = await _daq.CaptureAroundActionAsync(
new DaqActionCaptureRequest(_daqOpts.PreTriggerMs, _daqOpts.PostTriggerMs, false),
async token =>
{
_log.Add(UiLogLevel.Info, $"CH{ch} 触发关断动作命令。");
await _sspc.TurnOffAsync(ch, token);
});
await ApplyFallingCaptureAsync(Project, ch, falling);
_log.Add(UiLogLevel.Success, $"CH{ch} 关断曲线采集完成。");
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Error,
$"关断曲线采集失败:{ex.GetType().Name} - {ex.Message}CH{SelectedChannel}SSPC连接={_sspc.IsConnected}DAQ连接={_daq.IsConnected}");
try { await _sspc.TurnOffAsync(SelectedChannel); } catch { }
}
finally { Busy = false; }
}
private async Task EnsureCurveCaptureReadyAsync(int ch)
{
if (!ValidateCurveCaptureConfig(out var configMessage))
{
_log.Add(UiLogLevel.Error, $"CH{ch} 曲线采集被阻止:{configMessage}");
throw new InvalidOperationException(configMessage);
}
try
{
if (!_sspc.IsConnected) await _sspc.ConnectAsync();
}
catch (Exception ex)
{
throw new InvalidOperationException($"Modbus继电器端连接失败{ex.Message}。请在设置中检查串口与连接,否则继电器无法动作。", ex);
}
try
{
if (!_daq.IsConnected) await _daq.ConnectAsync();
}
catch (Exception ex)
{
throw new InvalidOperationException($"高速采集卡连接失败:{ex.Message}。请在设置中检查采集卡。", ex);
}
}
private async Task ApplyRisingCaptureAsync(Project project, int ch, CurveData rising)
{
double vMax = CurveMetrics.EstimateVMax(rising.Voltage);
var row = Rows.First(r => r.Channel == ch);
row.OnTimeMs = CurveMetrics.OnTimeMs(rising, vMax);
row.RiseTimeMs = CurveMetrics.RiseTimeMs(rising, vMax);
row.RisingWaveformPath = await WaveformStore.SaveAsync(project, ch, "on", rising);
RisingCurve = rising;
UpdateVMax(rising, FallingCurve);
RefreshCurveRowStatus(row);
await _store.SaveAsync(project);
}
private async Task ApplyFallingCaptureAsync(Project project, int ch, CurveData falling)
{
double vMax = CurveMetrics.EstimateVMax(falling.Voltage);
var row = Rows.First(r => r.Channel == ch);
row.OffTimeMs = CurveMetrics.OffTimeMs(falling, vMax);
row.FallTimeMs = CurveMetrics.FallTimeMs(falling, vMax);
row.FallingWaveformPath = await WaveformStore.SaveAsync(project, ch, "off", falling);
FallingCurve = falling;
UpdateVMax(RisingCurve, falling);
RefreshCurveRowStatus(row);
await _store.SaveAsync(project);
}
private void RefreshCurveRowStatus(CurveResultRow row)
{
bool hasRising = !string.IsNullOrWhiteSpace(row.RisingWaveformPath);
bool hasFalling = !string.IsNullOrWhiteSpace(row.FallingWaveformPath);
if (!hasRising && !hasFalling)
{
row.Status = TestStatus.Untested;
return;
}
if (!(hasRising && hasFalling))
{
row.Status = TestStatus.Measured;
return;
}
var th = Thresholds;
bool ok = double.IsFinite(row.OnTimeMs)
&& double.IsFinite(row.RiseTimeMs)
&& double.IsFinite(row.OffTimeMs)
&& double.IsFinite(row.FallTimeMs)
&& !double.IsNaN(row.OnTimeMs)
&& !double.IsNaN(row.RiseTimeMs)
&& !double.IsNaN(row.OffTimeMs)
&& !double.IsNaN(row.FallTimeMs)
&& row.OnTimeMs <= th.MaxOnTimeMs
&& row.RiseTimeMs <= th.MaxRiseTimeMs
&& row.OffTimeMs <= th.MaxOffTimeMs
&& row.FallTimeMs <= th.MaxFallTimeMs;
row.Status = ok ? TestStatus.Pass : TestStatus.Fail;
}
private void UpdateVMax(params CurveData?[] curves)
{
var candidates = curves.Where(x => x != null)
.Select(x => CurveMetrics.EstimateVMax(x!.Voltage))
.Where(x => double.IsFinite(x) && !double.IsNaN(x))
.ToArray();
if (candidates.Length > 0)
VMax = candidates.Max();
}
private async Task WaitForStableOnAsync(int channel, CancellationToken ct = default)
{
await Task.Delay(BasicTestCriteria.SwitchSettleDelayMs, ct);
var measurement = await _sspc.ReadMeasurementAsync(channel, ct);
if (!BasicTestCriteria.IsOn(measurement.Voltage))
throw new InvalidOperationException($"CH{channel} 未能稳定开启,当前电压 {measurement.Voltage:F3} V。");
}
private bool ValidateCurveCaptureConfig(out string message)
{
if (_daqOpts.UseMock)
{
message = "Mock 模式,曲线采集配置有效。";
return true;
}
if (_daqOpts.ChannelCount is not (1 or 2 or 4 or 8))
{
message = "DAQ 启用通道数只能为 1、2、4 或 8。";
return false;
}
var enabled = _daqOpts.Channels.Where(x => x.PhysicalChannel < _daqOpts.ChannelCount).ToArray();
if (enabled.Length == 0)
{
message = "DAQ 没有可用的启用通道。";
return false;
}
var outputVoltage = enabled.Where(x => string.Equals(x.Role, nameof(DaqChannelRole.OutputVoltage), StringComparison.OrdinalIgnoreCase)).ToArray();
if (outputVoltage.Length != 1)
{
message = $"曲线测试必须配置且只能配置一个 OutputVoltage 角色,当前配置了 {outputVoltage.Length} 个。";
return false;
}
var voltageChannel = outputVoltage[0];
if (!voltageChannel.IsCalibrated)
{
message = $"CH{voltageChannel.PhysicalChannel} OutputVoltage 尚未勾选已标定。";
return false;
}
if (!double.IsFinite(voltageChannel.Scale) || voltageChannel.Scale == 0)
{
message = $"CH{voltageChannel.PhysicalChannel} 输出电压通道 Scale 无效。";
return false;
}
if (!double.IsFinite(voltageChannel.Offset))
{
message = $"CH{voltageChannel.PhysicalChannel} 输出电压通道 Offset 无效。";
return false;
}
message = "曲线采集配置有效。";
return true;
}
}