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; } }