516 lines
19 KiB
C#
516 lines
19 KiB
C#
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 ResetRisingCurveView()
|
||
{
|
||
if (SelectedRow == null) return;
|
||
RisingViewStartMs = null;
|
||
RisingViewEndMs = null;
|
||
SelectedRow.RisingViewStartMs = null;
|
||
SelectedRow.RisingViewEndMs = null;
|
||
SaveProjectQuietly();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void ResetFallingCurveView()
|
||
{
|
||
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 Task CaptureRising() => CaptureSingleCurveAsync(CurveCaptureKind.Rising);
|
||
|
||
[RelayCommand]
|
||
private Task CaptureFalling() => CaptureSingleCurveAsync(CurveCaptureKind.Falling);
|
||
|
||
private async Task CaptureSingleCurveAsync(CurveCaptureKind kind)
|
||
{
|
||
if (Project == null || Busy) return;
|
||
Busy = true;
|
||
try
|
||
{
|
||
int ch = SelectedChannel;
|
||
if (!ValidateCurveCaptureConfig(out var configMessage))
|
||
{
|
||
_log.Add(UiLogLevel.Error, $"CH{ch} 曲线采集被阻止:{configMessage}");
|
||
return;
|
||
}
|
||
|
||
await EnsureDevicesConnectedAsync();
|
||
var row = Rows.First(r => r.Channel == ch);
|
||
row.Status = TestStatus.Running;
|
||
|
||
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);
|
||
}
|
||
|
||
var request = new DaqActionCaptureRequest(_daqOpts.PreTriggerMs, _daqOpts.PostTriggerMs, rising);
|
||
_log.Add(UiLogLevel.Info, rising
|
||
? $"CH{ch} 开始预采集,触发后发送开启命令。"
|
||
: $"CH{ch} 开始预采集,触发后发送关断命令。");
|
||
|
||
var curve = await _daq.CaptureAroundActionAsync(
|
||
request,
|
||
async token =>
|
||
{
|
||
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);
|
||
}
|
||
});
|
||
|
||
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);
|
||
}
|
||
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 EnsureDevicesConnectedAsync()
|
||
{
|
||
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 void ApplyRisingCurve(CurveResultRow row, CurveData curve)
|
||
{
|
||
double vMax = CurveMetrics.EstimateVMax(curve.Voltage);
|
||
VMax = vMax;
|
||
row.OnTimeMs = CurveMetrics.OnTimeMs(curve, vMax);
|
||
row.RiseTimeMs = CurveMetrics.RiseTimeMs(curve, vMax);
|
||
}
|
||
|
||
private void ApplyFallingCurve(CurveResultRow row, CurveData curve)
|
||
{
|
||
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);
|
||
}
|
||
|
||
private void UpdateFinalStatusIfComplete(CurveResultRow row)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(row.RisingWaveformPath) ||
|
||
string.IsNullOrWhiteSpace(row.FallingWaveformPath))
|
||
{
|
||
row.Status = TestStatus.Running;
|
||
return;
|
||
}
|
||
|
||
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.RiseTimeMs)
|
||
&& !double.IsNaN(row.OffTimeMs) && !double.IsNaN(row.FallTimeMs);
|
||
row.Status = ok ? TestStatus.Pass : TestStatus.Fail;
|
||
}
|
||
|
||
private enum CurveCaptureKind
|
||
{
|
||
Rising,
|
||
Falling
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task CaptureCurrent()
|
||
{
|
||
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; }
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|