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;
/// 曲线测试页 VM。
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 Rows { get; } = new();
public ObservableCollection 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 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}");
}
}
/// 仅用于"浏览历史项目"时的可视化预览:调用 MockDaq 合成一段波形,不操作 SSPC。
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;
}
}