2026-06-30 12:10:29 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 14:16:15 +08:00
|
|
|
|
partial void OnSelectedRowChanged(CurveResultRow? value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_syncingViewWindow || value == null) return;
|
|
|
|
|
|
if (SelectedChannel != value.Channel)
|
|
|
|
|
|
SelectedChannel = value.Channel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
RisingViewStartMs = SelectedRow?.RisingViewStartMs;
|
|
|
|
|
|
RisingViewEndMs = SelectedRow?.RisingViewEndMs;
|
|
|
|
|
|
FallingViewStartMs = SelectedRow?.FallingViewStartMs;
|
|
|
|
|
|
FallingViewEndMs = SelectedRow?.FallingViewEndMs;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_syncingViewWindow = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private void ResetRisingCurveView()
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (SelectedRow == null) return;
|
|
|
|
|
|
RisingViewStartMs = null;
|
|
|
|
|
|
RisingViewEndMs = null;
|
|
|
|
|
|
SelectedRow.RisingViewStartMs = null;
|
|
|
|
|
|
SelectedRow.RisingViewEndMs = null;
|
|
|
|
|
|
SaveProjectQuietly();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 13:33:55 +08:00
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void ZoomRisingCurveToMarks()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (SelectedRow == null || RisingCurve == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
double vMax = CurveMetrics.EstimateVMax(RisingCurve.Voltage);
|
|
|
|
|
|
if (!TryBuildMarkWindow(CurveMetrics.AnalyzeRising(RisingCurve, vMax), RisingCurve, out double start, out double end))
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
RisingViewStartMs = start;
|
|
|
|
|
|
RisingViewEndMs = end;
|
|
|
|
|
|
SelectedRow.RisingViewStartMs = start;
|
|
|
|
|
|
SelectedRow.RisingViewEndMs = end;
|
|
|
|
|
|
SaveProjectQuietly();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
[RelayCommand]
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private void ResetFallingCurveView()
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (SelectedRow == null) return;
|
|
|
|
|
|
FallingViewStartMs = null;
|
|
|
|
|
|
FallingViewEndMs = null;
|
|
|
|
|
|
SelectedRow.FallingViewStartMs = null;
|
|
|
|
|
|
SelectedRow.FallingViewEndMs = null;
|
|
|
|
|
|
SaveProjectQuietly();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 13:33:55 +08:00
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void ZoomFallingCurveToMarks()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (SelectedRow == null || FallingCurve == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
double vMax = RisingCurve != null
|
|
|
|
|
|
? CurveMetrics.EstimateVMax(RisingCurve.Voltage)
|
|
|
|
|
|
: CurveMetrics.EstimateVMax(FallingCurve.Voltage);
|
|
|
|
|
|
if (!TryBuildMarkWindow(CurveMetrics.AnalyzeFalling(FallingCurve, vMax), FallingCurve, out double start, out double end))
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
FallingViewStartMs = start;
|
|
|
|
|
|
FallingViewEndMs = end;
|
|
|
|
|
|
SelectedRow.FallingViewStartMs = start;
|
|
|
|
|
|
SelectedRow.FallingViewEndMs = end;
|
|
|
|
|
|
SaveProjectQuietly();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool TryBuildMarkWindow(CurveTransitionMark mark, CurveData curve, out double start, out double end)
|
|
|
|
|
|
{
|
|
|
|
|
|
start = 0;
|
|
|
|
|
|
end = 0;
|
|
|
|
|
|
if (!mark.IsValid || !double.IsFinite(mark.StartTimeMs) || !double.IsFinite(mark.EndTimeMs))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
double span = Math.Abs(mark.EndTimeMs - mark.StartTimeMs);
|
|
|
|
|
|
double padding = Math.Max(span * 0.15, 0.05);
|
|
|
|
|
|
double minTimeMs = -curve.TriggerTimeSec * 1000.0;
|
|
|
|
|
|
double maxTimeMs = (curve.DurationSec - curve.TriggerTimeSec) * 1000.0;
|
|
|
|
|
|
|
|
|
|
|
|
start = Math.Max(minTimeMs, Math.Min(mark.StartTimeMs, mark.EndTimeMs) - padding);
|
|
|
|
|
|
end = Math.Min(maxTimeMs, Math.Max(mark.StartTimeMs, mark.EndTimeMs) + padding);
|
|
|
|
|
|
return double.IsFinite(start) && double.IsFinite(end) && end > start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
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]
|
2026-07-03 14:16:15 +08:00
|
|
|
|
private void OpenPopup(string? curveKind)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
var win = new Views.CurvePopupWindow(this, curveKind) { Owner = System.Windows.Application.Current?.MainWindow };
|
2026-06-30 12:10:29 +08:00
|
|
|
|
win.Show();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private Task CaptureRising() => CaptureSingleCurveAsync(CurveCaptureKind.Rising);
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private Task CaptureFalling() => CaptureSingleCurveAsync(CurveCaptureKind.Falling);
|
|
|
|
|
|
|
|
|
|
|
|
private async Task CaptureSingleCurveAsync(CurveCaptureKind kind)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (Project == null || Busy) return;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
Busy = true;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
int ch = SelectedChannel;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (!ValidateCurveCaptureConfig(out var configMessage))
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Error, $"CH{ch} 曲线采集被阻止:{configMessage}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
await EnsureDevicesConnectedAsync();
|
|
|
|
|
|
var row = Rows.First(r => r.Channel == ch);
|
|
|
|
|
|
row.Status = TestStatus.Running;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
bool rising = kind == CurveCaptureKind.Rising;
|
|
|
|
|
|
if (rising)
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 设置继电器为关断状态,准备采集开启曲线。");
|
|
|
|
|
|
await _sspc.TurnOffAsync(ch);
|
|
|
|
|
|
await BasicTestCriteria.WaitForStableOffAsync(_sspc, ch);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 设置继电器为开启状态,准备采集关断曲线。");
|
|
|
|
|
|
await _sspc.TurnOnAsync(ch);
|
|
|
|
|
|
await Task.Delay(BasicTestCriteria.SwitchSettleDelayMs);
|
|
|
|
|
|
}
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
var request = new DaqActionCaptureRequest(_daqOpts.PreTriggerMs, _daqOpts.PostTriggerMs, rising);
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, rising
|
|
|
|
|
|
? $"CH{ch} 开始预采集,触发后发送开启命令。"
|
|
|
|
|
|
: $"CH{ch} 开始预采集,触发后发送关断命令。");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
var curve = await _daq.CaptureAroundActionAsync(
|
|
|
|
|
|
request,
|
2026-06-30 12:10:29 +08:00
|
|
|
|
async token =>
|
|
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (rising)
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 触发开启动作命令。");
|
|
|
|
|
|
await _sspc.TurnOnAsync(ch, token);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 触发关断动作命令。");
|
|
|
|
|
|
await _sspc.TurnOffAsync(ch, token);
|
|
|
|
|
|
}
|
2026-06-30 12:10:29 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (rising)
|
|
|
|
|
|
{
|
|
|
|
|
|
ApplyRisingCurve(row, curve);
|
|
|
|
|
|
row.RisingWaveformPath = await WaveformStore.SaveAsync(Project, ch, "on", curve);
|
|
|
|
|
|
RisingCurve = curve;
|
|
|
|
|
|
_log.Add(UiLogLevel.Success, $"CH{ch} 开启曲线采集完成。");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
ApplyFallingCurve(row, curve);
|
|
|
|
|
|
row.FallingWaveformPath = await WaveformStore.SaveAsync(Project, ch, "off", curve);
|
|
|
|
|
|
FallingCurve = curve;
|
|
|
|
|
|
_log.Add(UiLogLevel.Success, $"CH{ch} 关断曲线采集完成。");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
UpdateFinalStatusIfComplete(row);
|
|
|
|
|
|
await _store.SaveAsync(Project);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Error,
|
2026-07-01 20:04:27 +08:00
|
|
|
|
$"曲线采集失败:{ex.GetType().Name} - {ex.Message}(CH{SelectedChannel},SSPC连接={_sspc.IsConnected},DAQ连接={_daq.IsConnected})");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
try { await _sspc.TurnOffAsync(SelectedChannel); } catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
finally { Busy = false; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private async Task EnsureDevicesConnectedAsync()
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private void ApplyRisingCurve(CurveResultRow row, CurveData curve)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
double vMax = CurveMetrics.EstimateVMax(curve.Voltage);
|
|
|
|
|
|
VMax = vMax;
|
|
|
|
|
|
row.OnTimeMs = CurveMetrics.OnTimeMs(curve, vMax);
|
|
|
|
|
|
row.RiseTimeMs = CurveMetrics.RiseTimeMs(curve, vMax);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private void ApplyFallingCurve(CurveResultRow row, CurveData curve)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
double vMax = RisingCurve != null
|
|
|
|
|
|
? CurveMetrics.EstimateVMax(RisingCurve.Voltage)
|
|
|
|
|
|
: CurveMetrics.EstimateVMax(curve.Voltage);
|
|
|
|
|
|
VMax = vMax;
|
|
|
|
|
|
row.OffTimeMs = CurveMetrics.OffTimeMs(curve, vMax);
|
|
|
|
|
|
row.FallTimeMs = CurveMetrics.FallTimeMs(curve, vMax);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private void UpdateFinalStatusIfComplete(CurveResultRow row)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (string.IsNullOrWhiteSpace(row.RisingWaveformPath) ||
|
|
|
|
|
|
string.IsNullOrWhiteSpace(row.FallingWaveformPath))
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
row.Status = TestStatus.Running;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var th = Thresholds;
|
2026-07-01 20:04:27 +08:00
|
|
|
|
bool ok = row.OnTimeMs <= th.MaxOnTimeMs && row.RiseTimeMs <= th.MaxRiseTimeMs
|
|
|
|
|
|
&& row.OffTimeMs <= th.MaxOffTimeMs && row.FallTimeMs <= th.MaxFallTimeMs
|
|
|
|
|
|
&& !double.IsNaN(row.OnTimeMs) && !double.IsNaN(row.RiseTimeMs)
|
|
|
|
|
|
&& !double.IsNaN(row.OffTimeMs) && !double.IsNaN(row.FallTimeMs);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
row.Status = ok ? TestStatus.Pass : TestStatus.Fail;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private enum CurveCaptureKind
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
Rising,
|
|
|
|
|
|
Falling
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private async Task CaptureCurrent()
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
if (Project == null) return;
|
|
|
|
|
|
Busy = true;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
int ch = SelectedChannel;
|
|
|
|
|
|
if (!ValidateCurveCaptureConfig(out var configMessage))
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Error, $"CH{ch} 曲线采集被阻止:{configMessage}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 准备采集:先关断并等待稳定。");
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
await _sspc.TurnOffAsync(ch);
|
|
|
|
|
|
await BasicTestCriteria.WaitForStableOffAsync(_sspc, ch);
|
|
|
|
|
|
|
|
|
|
|
|
var request = new DaqActionCaptureRequest(_daqOpts.PreTriggerMs, _daqOpts.PostTriggerMs, true);
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 开始预采集,待触发后发送开启动作。");
|
|
|
|
|
|
var rising = await _daq.CaptureAroundActionAsync(
|
|
|
|
|
|
request,
|
|
|
|
|
|
async token =>
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 触发开启动作命令。");
|
|
|
|
|
|
await _sspc.TurnOnAsync(ch, token);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await Task.Delay(100);
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 开始预采集,待触发后发送关断动作。");
|
|
|
|
|
|
var falling = await _daq.CaptureAroundActionAsync(
|
|
|
|
|
|
request with { IsRising = false },
|
|
|
|
|
|
async token =>
|
|
|
|
|
|
{
|
|
|
|
|
|
_log.Add(UiLogLevel.Info, $"CH{ch} 触发关断动作命令。");
|
|
|
|
|
|
await _sspc.TurnOffAsync(ch, token);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
double vMax = CurveMetrics.EstimateVMax(rising.Voltage);
|
|
|
|
|
|
VMax = vMax;
|
|
|
|
|
|
var row = Rows.First(r => r.Channel == ch);
|
|
|
|
|
|
row.OnTimeMs = CurveMetrics.OnTimeMs(rising, vMax);
|
|
|
|
|
|
row.RiseTimeMs = CurveMetrics.RiseTimeMs(rising, vMax);
|
|
|
|
|
|
row.OffTimeMs = CurveMetrics.OffTimeMs(falling, vMax);
|
|
|
|
|
|
row.FallTimeMs = CurveMetrics.FallTimeMs(falling, vMax);
|
|
|
|
|
|
var th = Thresholds;
|
|
|
|
|
|
bool ok = row.OnTimeMs <= th.MaxOnTimeMs && row.RiseTimeMs <= th.MaxRiseTimeMs
|
|
|
|
|
|
&& row.OffTimeMs <= th.MaxOffTimeMs && row.FallTimeMs <= th.MaxFallTimeMs
|
|
|
|
|
|
&& !double.IsNaN(row.OnTimeMs) && !double.IsNaN(row.OffTimeMs);
|
|
|
|
|
|
row.Status = ok ? TestStatus.Pass : TestStatus.Fail;
|
|
|
|
|
|
row.RisingWaveformPath = await WaveformStore.SaveAsync(Project, ch, "on", rising);
|
|
|
|
|
|
row.FallingWaveformPath = await WaveformStore.SaveAsync(Project, ch, "off", falling);
|
|
|
|
|
|
|
|
|
|
|
|
RisingCurve = rising;
|
|
|
|
|
|
FallingCurve = falling;
|
|
|
|
|
|
await _store.SaveAsync(Project);
|
|
|
|
|
|
_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; }
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|