Files
ACP/PowerAdaptabilityModule/ViewModels/PowerAdaptabilityViewModel.cs
T
2026-09-24 15:20:29 +08:00

1157 lines
56 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 DeviceCommand.Devices;
using Newtonsoft.Json;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using Prism.Commands;
using Prism.Ioc;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using UIShare.GlobalVariable;
using UIShare.ViewModelBase;
namespace PowerAdaptabilityModule.ViewModels
{
/// <summary>
/// 电源适应性测试 ViewModel(Chroma61800 交流源一拖三):
/// 左侧为设备命令面板(三相输出设置 + LIST 模式),右侧为 A/B/C 三相实时波形图表。
/// </summary>
public class PowerAdaptabilityViewModel : NavigateViewModelBase, IDisposable
{
#region 常量
/// <summary>采样周期(毫秒)</summary>
private const int SampleIntervalMs = 1000;
/// <summary>三相曲线配色:A 红、B 绿、C 蓝(三张图统一,便于对照)</summary>
private static readonly OxyColor[] PhaseColors =
{ OxyColor.FromRgb(0xE5, 0x39, 0x35), OxyColor.FromRgb(0x43, 0xA0, 0x47), OxyColor.FromRgb(0x1E, 0x88, 0xE5) };
#endregion
#region 私有字段
private readonly DeviceManager _dm;
private readonly GlobalInfo _globalInfo;
private Chroma61800? _dev;
private CancellationTokenSource? _cmdCts;
private CancellationTokenSource? _sampleCts;
/// <summary>采样计时器,记录从开始采集到当前的经过秒数(X 轴)</summary>
private System.Diagnostics.Stopwatch? _sampleStopwatch;
// 三相电压时间序列曲线(A/B/C 三相)
private readonly LineSeries _voltA, _voltB, _voltC;
// 输出频率时间序列曲线(单条,ALL 模式读一个总频率)
private readonly LineSeries _freq;
#endregion
#region 设备状态属性
private string _deviceName = "Chroma61800";
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
private bool _isConnected;
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
private bool _isBusy;
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
private bool _isSampling;
/// <summary>是否正在采集波形数据</summary>
public bool IsSampling { get => _isSampling; set => SetProperty(ref _isSampling, value); }
private bool _isFrequencySamplingEnabled;
/// <summary>是否采集频率(默认关闭:频率一般无需持续采集,开启后采样循环才读取并绘制频率曲线)</summary>
public bool IsFrequencySamplingEnabled { get => _isFrequencySamplingEnabled; set => SetProperty(ref _isFrequencySamplingEnabled, value); }
#endregion
#region 图表(三相电压时间序列 + 输出频率时间序列)
/// <summary>三相电压波形图表</summary>
public PlotModel VoltagePlot { get; }
/// <summary>输出频率时间序列图表(受“采集频率”开关控制,默认不采集)</summary>
public PlotModel FrequencyPlot { get; }
#endregion
#region 实时测量值
private double _voltageA; public double VoltageA { get => _voltageA; set => SetProperty(ref _voltageA, value); }
private double _voltageB; public double VoltageB { get => _voltageB; set => SetProperty(ref _voltageB, value); }
private double _voltageC; public double VoltageC { get => _voltageC; set => SetProperty(ref _voltageC, value); }
private double _frequencyNow;
/// <summary>当前输出频率 (Hz)</summary>
public double FrequencyNow { get => _frequencyNow; set => SetProperty(ref _frequencyNow, value); }
private double _powerNow;
/// <summary>当前真实输出功率 (W)</summary>
public double PowerNow { get => _powerNow; set => SetProperty(ref _powerNow, value); }
#endregion
#region 输入参数(三相输出设置)
private double _voltage = 220;
/// <summary>待设置的交流电压 (V)</summary>
public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); }
private double _frequency = 50;
/// <summary>待设置的输出频率 (Hz)</summary>
public double Frequency { get => _frequency; set => SetProperty(ref _frequency, value); }
private string _waveform = "SINE";
/// <summary>波形选择</summary>
public string Waveform { get => _waveform; set => SetProperty(ref _waveform, value); }
private string _couplingMode = "AC";
/// <summary>输出耦合模式 (AC / DC / ACDC)</summary>
public string CouplingMode { get => _couplingMode; set => SetProperty(ref _couplingMode, value); }
private string _threePhaseRelation = "BALANCE";
/// <summary>三相关系 (INDEPEND / SAMEFREQ / BALANCE)</summary>
public string ThreePhaseRelation { get => _threePhaseRelation; set => SetProperty(ref _threePhaseRelation, value); }
/// <summary>相位选择列表: 0=ALL(三相同时), 1=A相, 2=B相, 3=C相</summary>
public List<string> PhaseOptions { get; } = new() { "ALL (三相)", "A相 (Φ1)", "B相 (Φ2)", "C相 (Φ3)" };
private int _selectedPhaseIndex;
/// <summary>当前选中的相位索引 (0=ALL, 1=A, 2=B, 3=C)</summary>
public int SelectedPhaseIndex { get => _selectedPhaseIndex; set => SetProperty(ref _selectedPhaseIndex, value); }
#endregion
#region 输入参数(LIST 模式 · 三相独立编辑)
/// <summary>下发完成到启动之间的延时(ms),对应 *D 命令,等待仪器就绪</summary>
private const int StartDelayMs = 200;
private string _listTrigger = "AUTO";
/// <summary>列表触发形态 (AUTO / MANUAL / EXCITE)</summary>
public string ListTrigger { get => _listTrigger; set => SetProperty(ref _listTrigger, value); }
private int _listCount;
/// <summary>列表执行次数 (0 = 无限循环, 最大 65535)</summary>
public int ListCount { get => _listCount; set => SetProperty(ref _listCount, value); }
private string _listTimeBase = "TIME";
/// <summary>列表时间基础 (TIME / CYCLE)</summary>
public string ListTimeBase { get => _listTimeBase; set => SetProperty(ref _listTimeBase, value); }
/// <summary>三相各自的 LIST 序列配置(索引 0=A/Φ1, 1=B/Φ2, 2=C/Φ3),INST:EDIT EACH 逐相下发</summary>
private readonly ListPhaseConfig[] _phaseConfigs = { new(), new(), new() };
/// <summary>相位编辑选项(对应 INST:NSEL 1/2/3)</summary>
public List<string> ListPhaseOptions { get; } = new() { "A相 (Φ1)", "B相 (Φ2)", "C相 (Φ3)" };
private int _listEditPhaseIndex;
/// <summary>当前正在编辑的相位索引 (0=A, 1=B, 2=C),切换时下方序列文本框自动刷新为该相内容</summary>
public int ListEditPhaseIndex
{
get => _listEditPhaseIndex;
set
{
if (SetProperty(ref _listEditPhaseIndex, value))
RaiseListSequenceChanged();
}
}
/// <summary>当前选中相位的配置对象</summary>
private ListPhaseConfig CurCfg => _phaseConfigs[_listEditPhaseIndex];
// —— 以下文本属性均代理到"当前编辑相位"的配置,切换相位时统一刷新 ——
/// <summary>当前相波形 (FUNC:SHAP:A)</summary>
public string ListWaveform { get => CurCfg.Waveform; set { if (CurCfg.Waveform != value) { CurCfg.Waveform = value; RaisePropertyChanged(); } } }
/// <summary>相位角度序列 (LIST:DEGR, °)</summary>
public string ListDegrText { get => CurCfg.Degr; set { if (CurCfg.Degr != value) { CurCfg.Degr = value; RaisePropertyChanged(); } } }
/// <summary>AC 起始电压序列 (LIST:VOLT:AC:STAR, V)</summary>
public string ListVoltAcStartText { get => CurCfg.VoltAcStart; set { if (CurCfg.VoltAcStart != value) { CurCfg.VoltAcStart = value; RaisePropertyChanged(); } } }
/// <summary>AC 结束电压序列 (LIST:VOLT:AC:END, V)</summary>
public string ListVoltAcEndText { get => CurCfg.VoltAcEnd; set { if (CurCfg.VoltAcEnd != value) { CurCfg.VoltAcEnd = value; RaisePropertyChanged(); } } }
/// <summary>DC 起始电压序列 (LIST:VOLT:DC:STAR, V)</summary>
public string ListVoltDcStartText { get => CurCfg.VoltDcStart; set { if (CurCfg.VoltDcStart != value) { CurCfg.VoltDcStart = value; RaisePropertyChanged(); } } }
/// <summary>DC 结束电压序列 (LIST:VOLT:DC:END, V)</summary>
public string ListVoltDcEndText { get => CurCfg.VoltDcEnd; set { if (CurCfg.VoltDcEnd != value) { CurCfg.VoltDcEnd = value; RaisePropertyChanged(); } } }
/// <summary>起始频率序列 (LIST:FREQ:STAR, Hz)</summary>
public string ListFreqStartText { get => CurCfg.FreqStart; set { if (CurCfg.FreqStart != value) { CurCfg.FreqStart = value; RaisePropertyChanged(); } } }
/// <summary>结束频率序列 (LIST:FREQ:END, Hz)</summary>
public string ListFreqEndText { get => CurCfg.FreqEnd; set { if (CurCfg.FreqEnd != value) { CurCfg.FreqEnd = value; RaisePropertyChanged(); } } }
/// <summary>静止时间序列 (LIST:DWEL, ms)</summary>
public string ListDwellText { get => CurCfg.Dwell; set { if (CurCfg.Dwell != value) { CurCfg.Dwell = value; RaisePropertyChanged(); } } }
/// <summary>波形缓冲顺序 (LIST:SHAP, A/B)</summary>
public string ListShapeText { get => CurCfg.ShapeSeq; set { if (CurCfg.ShapeSeq != value) { CurCfg.ShapeSeq = value; RaisePropertyChanged(); } } }
/// <summary>切换编辑相位后,通知所有序列文本框刷新为新相位内容</summary>
private void RaiseListSequenceChanged()
{
RaisePropertyChanged(nameof(ListWaveform));
RaisePropertyChanged(nameof(ListDegrText));
RaisePropertyChanged(nameof(ListVoltAcStartText));
RaisePropertyChanged(nameof(ListVoltAcEndText));
RaisePropertyChanged(nameof(ListVoltDcStartText));
RaisePropertyChanged(nameof(ListVoltDcEndText));
RaisePropertyChanged(nameof(ListFreqStartText));
RaisePropertyChanged(nameof(ListFreqEndText));
RaisePropertyChanged(nameof(ListDwellText));
RaisePropertyChanged(nameof(ListShapeText));
}
#endregion
#region 下拉选项
public IReadOnlyList<string> WaveformOptions { get; } = new[] { "SINE", "SQUA", "CSIN" };
public IReadOnlyList<string> CouplingOptions { get; } = new[] { "AC", "DC", "ACDC" };
public IReadOnlyList<string> RelationOptions { get; } = new[] { "INDEPEND", "SAMEFREQ", "BALANCE" };
public IReadOnlyList<string> ListTriggerOptions { get; } = new[] { "AUTO", "MANUAL", "EXCITE" };
public IReadOnlyList<string> ListTimeBaseOptions { get; } = new[] { "TIME", "CYCLE" };
#endregion
#region 试验预设(测试项文件:D:\acp\源测试,一键载入大纲典型波形)
/// <summary>测试项文件默认存储目录(保存与查询均在此目录)</summary>
private const string PresetStorageDir = @"D:\acp\源测试";
/// <summary>测试项文件扩展名</summary>
private const string PresetFileExt = ".json";
/// <summary>测试项文件目录提示(供界面显示)</summary>
public string PresetStorageHint => $"测试项文件目录:{PresetStorageDir}";
// 从磁盘加载的测试项集合(源真相为 PresetStorageDir 下的 .json 文件)
private List<ListPreset> _presets = new();
/// <summary>测试项集合(由 PresetStorageDir 下的文件加载而来)</summary>
public IReadOnlyList<ListPreset> Presets => _presets;
// 供 ComboBox 显示的名称列表,随磁盘刷新
private List<string> _presetOptions = new();
/// <summary>测试项名称列表(供 ComboBox 显示)</summary>
public IReadOnlyList<string> PresetOptions => _presetOptions;
private string _presetName = "";
/// <summary>当前测试项名称(保存时作为文件名;载入后自动填充,便于覆盖保存)</summary>
public string PresetName { get => _presetName; set => SetProperty(ref _presetName, value); }
private string _presetNote = "";
/// <summary>当前测试项说明(随文件保存,载入后自动填充)</summary>
public string PresetNote { get => _presetNote; set => SetProperty(ref _presetNote, value); }
private int _selectedPresetIndex = -1;
/// <summary>当前选中的测试项索引(-1 表示未选择)</summary>
public int SelectedPresetIndex
{
get => _selectedPresetIndex;
set
{
if (SetProperty(ref _selectedPresetIndex, value))
{
// 选中后把名称/说明填入编辑框,便于修改后另存或覆盖保存
if (value >= 0 && value < _presets.Count)
{
PresetName = _presets[value].Name;
PresetNote = _presets[value].Note;
}
}
}
}
#endregion
#region 日志
private string _responseLog = "";
/// <summary>操作与采样日志</summary>
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
#endregion
#region 命令
public ICommand QueryIdnCommand { get; }
public ICommand ResetCommand { get; }
public ICommand OutputOnCommand { get; }
public ICommand OutputOffCommand { get; }
public ICommand RelayOnCommand { get; }
public ICommand RelayOffCommand { get; }
public ICommand ApplyCouplingCommand { get; }
public ICommand SetThreePhaseModeCommand { get; }
public ICommand SetSinglePhaseModeCommand { get; }
public ICommand ApplyRelationCommand { get; }
public ICommand SetVoltageCommand { get; }
public ICommand SetFrequencyCommand { get; }
public ICommand SetWaveformCommand { get; }
public ICommand SetFixedModeCommand { get; }
public ICommand EnterListModeCommand { get; }
public ICommand LoadPresetCommand { get; }
public ICommand SavePresetCommand { get; }
public ICommand DeletePresetCommand { get; }
public ICommand CopyPhaseToThreeCommand { get; }
public ICommand DownloadThreePhaseListCommand { get; }
public ICommand ListStopCommand { get; }
public ICommand StartSamplingCommand { get; }
public ICommand StopSamplingCommand { get; }
public ICommand ClearChartsCommand { get; }
#endregion
public PowerAdaptabilityViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_dm = containerProvider.Resolve<DeviceManager>();
_globalInfo = containerProvider.Resolve<GlobalInfo>();
// 初始化三相电压时间序列图表(X=时间秒, Y=实测 RMS 电压, 叠加 A/B/C 三相)
VoltagePlot = BuildTimeSeriesPlot("三相电压", "V");
(_voltA, _voltB, _voltC) = AddThreePhaseSeries(VoltagePlot);
// 输出频率时间序列图表(单条曲线,受“采集频率”开关控制)
FrequencyPlot = BuildTimeSeriesPlot("输出频率", "Hz");
_freq = NewPhaseSeries("频率", OxyColor.FromRgb(0x8E, 0x24, 0xAA));
FrequencyPlot.Series.Add(_freq);
// 初始化测试项文件目录(首次运行迁移内置预设),再从磁盘加载测试项
EnsurePresetStorage();
RefreshPresets();
#region 命令初始化
QueryIdnCommand = new DelegateCommand(async () =>
await Exec(async () => Log("IDN: " + await _dev!.查询设备信息Async(Ct()))));
ResetCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.复位Async(Ct()); Log("设备已复位"); }));
OutputOnCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置输出开关Async(true, Ct()); Log("输出已开启"); }));
OutputOffCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置输出开关Async(false, Ct()); Log("输出已关闭"); }));
RelayOnCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置输出继电器Async(true, Ct()); Log("输出继电器已开启"); }));
RelayOffCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置输出继电器Async(false, Ct()); Log("输出继电器已关闭"); }));
ApplyCouplingCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置耦合模式Async(CouplingMode, Ct()); Log($"耦合模式={CouplingMode}"); }));
SetThreePhaseModeCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置单三相模式Async(true, Ct()); Log("已切换为三相模式"); }));
SetSinglePhaseModeCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置单三相模式Async(false, Ct()); Log("已切换为单相模式"); }));
ApplyRelationCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置三相关系Async(ThreePhaseRelation, Ct()); Log($"三相关系={ThreePhaseRelation}"); }));
SetVoltageCommand = new DelegateCommand(async () =>
await Exec(async () =>
{
await ApplyPhaseAsync();
await _dev!.设置交流电压Async(Voltage, Ct());
Log($"交流电压={Voltage} V → {PhaseLabel}");
}));
SetFrequencyCommand = new DelegateCommand(async () =>
await Exec(async () =>
{
await ApplyPhaseAsync();
await _dev!.设置频率Async(Frequency, Ct());
Log($"输出频率={Frequency} Hz → {PhaseLabel}");
}));
SetWaveformCommand = new DelegateCommand(async () =>
await Exec(async () =>
{
await ApplyPhaseAsync();
await _dev!.设置波形Async(Waveform, Ct());
Log($"波形={Waveform} → {PhaseLabel}");
}));
SetFixedModeCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置操作模态Async("FIXED", Ct()); Log("已切换为 FIXED 固定模式"); }));
EnterListModeCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置操作模态Async("LIST", Ct()); Log("已切换为 LIST 模式"); }));
// 载入测试项:把选中测试项的三相序列与基础参数一次性填充到界面
LoadPresetCommand = new DelegateCommand(() =>
{
if (SelectedPresetIndex < 0 || SelectedPresetIndex >= Presets.Count)
{
Log("请先在下拉框选择一项测试项");
return;
}
var p = Presets[SelectedPresetIndex];
ListCount = p.Count;
ListTrigger = p.Trigger;
ListTimeBase = p.TimeBase;
for (int i = 0; i < 3 && i < p.Phases.Length; i++)
_phaseConfigs[i] = p.Phases[i].Clone();
ListEditPhaseIndex = 0; // 切到 A 相显示(若原本就是 0,setter 不触发刷新,故下方手动刷新)
RaiseListSequenceChanged();
PresetName = p.Name; // 填充名称/说明,便于修改后覆盖保存
PresetNote = p.Note;
Log($"已载入测试项:{p.Name}(次数={(p.Count == 0 ? "无限" : p.Count.ToString())} 触发={p.Trigger} 时基={p.TimeBase})");
});
// 保存当前界面的 LIST 配置为测试项文件(写入 D:\acp\源测试\{名称}.json)
SavePresetCommand = new DelegateCommand(SaveCurrentPreset);
// 删除当前选中的测试项文件(带二次确认)
DeletePresetCommand = new DelegateCommand(DeleteCurrentPreset);
// 以当前编辑相位为模板复制到三相,B/C 相相位角按 ±120° 自动偏移,生成对称三相序列
CopyPhaseToThreeCommand = new DelegateCommand(() =>
{
var src = _phaseConfigs[_listEditPhaseIndex];
for (int p = 0; p < 3; p++)
{
if (p == _listEditPhaseIndex) continue;
var clone = src.Clone();
clone.Degr = OffsetDegr(src.Degr, (p - _listEditPhaseIndex) * 120.0);
_phaseConfigs[p] = clone;
}
RaiseListSequenceChanged();
Log($"已将 [{ListPhaseOptions[_listEditPhaseIndex]}] 序列复制到三相(B/C 相相位角自动 ±120° 偏移)");
});
// 一次性下发三相独立 LIST 并启动,严格遵循仪器要求的命令顺序
DownloadThreePhaseListCommand = new DelegateCommand(async () => await Exec(async () =>
{
// 若上一次 LIST 仍在执行,先等待其结束(TRIG:STATE? = OFF)再改写参数
await 等待List空闲Async();
await _dev!.清除错误队列Async(Ct()); // *CLS
await _dev.设置单三相模式Async(true, Ct()); // INST:PHAS THREE
await _dev.设置编辑方式Async("EACH", Ct()); // INST:EDIT EACH(逐相独立编辑)
await _dev.设置列表执行次数Async(ListCount, Ct()); // LIST:COUN(0=无限循环)
await _dev.设置列表触发形态Async(ListTrigger, Ct()); // LIST:TRIG
await _dev.设置列表时间基础Async(ListTimeBase, Ct()); // LIST:BASE
for (int p = 0; p < 3; p++)
{
await _dev.选择相位Async(p + 1, Ct()); // INST:NSEL 1|2|3
await 下发单相序列Async(_phaseConfigs[p]); // FUNC:SHAP:A + LIST:xxx 序列
}
await _dev.延时Async(StartDelayMs, Ct()); // *D 200(等待仪器就绪)
await _dev.设置操作模态Async("LIST", Ct()); // OUTP:MODE LIST
await _dev.触发列表执行Async(true, Ct()); // TRIG ON
Log($"三相独立 LIST 已下发并启动(触发={ListTrigger} 次数={(ListCount == 0 ? "无限" : ListCount.ToString())} 时基={ListTimeBase})");
}));
ListStopCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.触发列表执行Async(false, Ct()); Log("LIST 已停止执行(TRIG OFF)"); }));
StartSamplingCommand = new DelegateCommand(StartSampling);
StopSamplingCommand = new DelegateCommand(StopSampling);
ClearChartsCommand = new DelegateCommand(ClearCharts);
#endregion
Initialize();
}
#region 初始化 / 导航
/// <summary>
/// 查找 Chroma61800 设备实例:优先当前作用域 DeviceManager,回退全局 HardwarePool(跨作用域共享实例)。
/// </summary>
private void Initialize()
{
Chroma61800? found = null;
string? fn = null;
foreach (var kv in _dm.DeviceMap)
{
if (kv.Value is Chroma61800 it) { found = it; fn = kv.Key; break; }
}
if (found == null)
{
foreach (var kv in _globalInfo.HardwarePool)
{
if (kv.Value.IsValueCreated && kv.Value.Value is Chroma61800 c) { found = c; fn = kv.Key; break; }
}
}
_dev = found;
DeviceName = fn ?? "Chroma61800 (未找到)";
IsConnected = _dev?.IsConnected ?? false;
Log(found != null
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
: "未找到 Chroma61800 设备,请先在设置界面添加并启用该设备");
}
public override void OnNavigatedTo(NavigationContext navigationContext)
{
Initialize();
}
public override void OnNavigatedFrom(NavigationContext navigationContext)
{
// 离开页面时停止采样,避免后台持续占用设备通信
StopSampling();
}
#endregion
#region 数据采集
private void StartSampling()
{
if (_dev == null) { Log("未关联 Chroma61800 设备,无法采集"); return; }
if (IsSampling) return;
IsSampling = true;
_sampleStopwatch = System.Diagnostics.Stopwatch.StartNew();
_sampleCts = new CancellationTokenSource();
var token = _sampleCts.Token;
_ = Task.Run(() => SamplingLoopAsync(token), token);
Log($"开始采集(周期 {SampleIntervalMs} ms)");
}
private void StopSampling()
{
if (!IsSampling) return;
IsSampling = false;
_sampleStopwatch?.Stop();
try { _sampleCts?.Cancel(); } catch { }
_sampleCts?.Dispose();
_sampleCts = null;
Log("停止采集");
}
/// <summary>
/// 采样循环:通过 INST:COUP NONE + INST:NSEL 逐相切换读取三相交流电压,
/// 恢复 ALL 后读取总功率;频率仅在“采集频率”开关开启时读取(一般不采集)。
/// </summary>
private async Task SamplingLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
if (_dev != null && _dev.IsConnected)
{
// 切换为独立相位模式
await _dev.设置相位耦合Async("NONE", token);
// A 相 (Φ1)
await _dev.选择相位Async(1, token);
double vA = await _dev.读取交流电压Async(token);
// B 相 (Φ2)
await _dev.选择相位Async(2, token);
double vB = await _dev.读取交流电压Async(token);
// C 相 (Φ3)
await _dev.选择相位Async(3, token);
double vC = await _dev.读取交流电压Async(token);
// 恢复 ALL 模式后读取总功率;频率按开关决定是否采集
await _dev.设置相位耦合Async("ALL", token);
double power = await _dev.读取真实功率Async(token);
double freq = IsFrequencySamplingEnabled
? await _dev.读取频率Async(token)
: double.NaN;
double elapsedSec = _sampleStopwatch?.Elapsed.TotalSeconds ?? 0;
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher != null)
{
_ = dispatcher.BeginInvoke(new Action(() =>
{
AppendSample(elapsedSec, vA, vB, vC, freq, power);
IsConnected = _dev.IsConnected;
}));
}
}
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
Log($"采样异常:{ex.Message}");
}
try { await Task.Delay(SampleIntervalMs, token); }
catch (OperationCanceledException) { break; }
}
}
/// <summary>
/// 追加一个采样点到电压 / 频率时间序列图表(UI 线程调用)。
/// <para>X 轴为采集开始后的经过秒数;电压为三相 RMS,频率仅在采集开关开启(非 NaN)时追加。</para>
/// </summary>
private void AppendSample(double elapsedSec, double vA, double vB, double vC, double freq, double power)
{
// 三相电压
_voltA.Points.Add(new DataPoint(elapsedSec, vA));
_voltB.Points.Add(new DataPoint(elapsedSec, vB));
_voltC.Points.Add(new DataPoint(elapsedSec, vC));
// 频率(开关关闭时 freq 为 NaN,不追加曲线)
bool hasFreq = !double.IsNaN(freq);
if (hasFreq) _freq.Points.Add(new DataPoint(elapsedSec, freq));
// 实时数值
VoltageA = vA;
VoltageB = vB;
VoltageC = vC;
if (hasFreq) FrequencyNow = freq;
PowerNow = power;
VoltagePlot.InvalidatePlot(true);
if (hasFreq) FrequencyPlot.InvalidatePlot(true);
}
private void ClearCharts()
{
foreach (var s in new[] { _voltA, _voltB, _voltC, _freq })
{
s.Points.Clear();
}
_sampleStopwatch?.Restart();
VoltagePlot.InvalidatePlot(true);
FrequencyPlot.InvalidatePlot(true);
Log("已清空波形数据,计时器已重置");
}
#endregion
#region 图表构建
/// <summary>
/// 构建时间序列图表:横轴为经过时间(秒),纵轴为对应物理量实测 RMS 值,自动缩放。
/// </summary>
/// <param name="title">图表标题(如"三相电压")</param>
/// <param name="unit">纵轴单位(V / A / W)</param>
private static PlotModel BuildTimeSeriesPlot(string title, string unit)
{
var pm = new PlotModel
{
Title = $"{title} ({unit})",
PlotAreaBorderColor = OxyColors.LightGray,
Background = OxyColors.White
};
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Bottom,
Title = "时间 (s)",
Minimum = 0,
Maximum = 10,
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None,
StringFormat = "0.0",
IsZoomEnabled = true,
IsPanEnabled = true
});
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = unit,
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None,
IsZoomEnabled = true,
IsPanEnabled = true
});
pm.Legends.Add(new Legend
{
LegendPosition = LegendPosition.RightTop,
LegendBackground = OxyColor.FromAColor(200, OxyColors.White),
LegendBorder = OxyColors.LightGray
});
return pm;
}
/// <summary>向图表添加 A/B/C 三相时间序列曲线(红/绿/蓝,带标记点)。</summary>
private static (LineSeries A, LineSeries B, LineSeries C) AddThreePhaseSeries(PlotModel plot)
{
var a = NewPhaseSeries("A 相", PhaseColors[0]);
var b = NewPhaseSeries("B 相", PhaseColors[1]);
var c = NewPhaseSeries("C 相", PhaseColors[2]);
plot.Series.Add(a);
plot.Series.Add(b);
plot.Series.Add(c);
return (a, b, c);
}
/// <summary>创建一条相曲线(带小圆标记,便于辨识离散采样点)。</summary>
private static LineSeries NewPhaseSeries(string title, OxyColor color) => new LineSeries
{
Title = title,
Color = color,
StrokeThickness = 1.8,
MarkerType = MarkerType.Circle,
MarkerSize = 3,
MarkerFill = color
};
#endregion
#region 测试项文件读写(D:\acp\源测试)
/// <summary>
/// 确保测试项目录存在;若目录中尚无任何测试项文件,则把内置大纲预设迁移写入为 .json 文件。
/// </summary>
private void EnsurePresetStorage()
{
try
{
if (!Directory.Exists(PresetStorageDir))
Directory.CreateDirectory(PresetStorageDir);
var existing = Directory.GetFiles(PresetStorageDir, "*" + PresetFileExt);
if (existing.Length > 0) return; // 已有测试项文件,无需迁移
// 首次运行:把内置大纲预设逐项迁移为文件
var seeds = BuildPresets();
foreach (var p in seeds)
WritePresetFile(p);
Log($"已将 {seeds.Count} 项内置测试项迁移到 {PresetStorageDir}");
}
catch (Exception ex)
{
Log($"初始化测试项目录失败:{ex.Message}");
}
}
/// <summary>从磁盘重新加载测试项列表,刷新下拉框并保持选中项有效。</summary>
private void RefreshPresets()
{
var list = new List<ListPreset>();
try
{
if (Directory.Exists(PresetStorageDir))
{
foreach (var file in Directory.GetFiles(PresetStorageDir, "*" + PresetFileExt).OrderBy(f => f))
{
try
{
var json = File.ReadAllText(file);
var item = JsonConvert.DeserializeObject<ListPreset>(json);
if (item != null) list.Add(item);
}
catch (Exception ex)
{
Log($"测试项文件解析失败 [{Path.GetFileName(file)}]:{ex.Message}");
}
}
}
}
catch (Exception ex)
{
Log($"读取测试项目录失败:{ex.Message}");
}
_presets = list;
_presetOptions = list.Select(p => p.Name).ToList();
RaisePropertyChanged(nameof(PresetOptions));
RaisePropertyChanged(nameof(PresetStorageHint));
if (_selectedPresetIndex >= _presets.Count)
SelectedPresetIndex = -1;
}
/// <summary>把一个测试项写入 D:\acp\源测试\{清洗后名称}.json(缩进格式化,便于人工查看)。</summary>
private void WritePresetFile(ListPreset preset)
{
string path = Path.Combine(PresetStorageDir, SanitizeFileName(preset.Name) + PresetFileExt);
string json = JsonConvert.SerializeObject(preset, Formatting.Indented);
File.WriteAllText(path, json);
}
/// <summary>
/// 保存当前界面的 LIST 配置为测试项文件(名称取自“测试项名称”输入框)。
/// <para>快照当前三相序列配置 + 执行次数/触发/时基 + 说明,写入后刷新列表并选中该项。</para>
/// </summary>
private void SaveCurrentPreset()
{
string name = (PresetName ?? "").Trim();
if (string.IsNullOrEmpty(name))
{
Log("请先在“测试项名称”填写要保存的名称");
return;
}
try
{
if (!Directory.Exists(PresetStorageDir))
Directory.CreateDirectory(PresetStorageDir);
// 用当前界面参数 + 三相配置快照构建测试项
var phases = new[]
{
_phaseConfigs[0].Clone(),
_phaseConfigs[1].Clone(),
_phaseConfigs[2].Clone(),
};
var preset = new ListPreset(name, PresetNote ?? "", ListCount, phases, ListTrigger, ListTimeBase);
WritePresetFile(preset);
RefreshPresets();
int idx = _presets.FindIndex(p => p.Name == name);
if (idx >= 0) SelectedPresetIndex = idx;
Log($"测试项已保存:{Path.Combine(PresetStorageDir, SanitizeFileName(name) + PresetFileExt)}");
}
catch (Exception ex)
{
Log($"保存测试项失败:{ex.Message}");
}
}
/// <summary>
/// 删除当前选中的测试项文件(带二次确认),删除后刷新列表。
/// </summary>
private void DeleteCurrentPreset()
{
if (SelectedPresetIndex < 0 || SelectedPresetIndex >= _presets.Count)
{
Log("请先在下拉框选择要删除的测试项");
return;
}
var p = _presets[SelectedPresetIndex];
string path = Path.Combine(PresetStorageDir, SanitizeFileName(p.Name) + PresetFileExt);
var confirm = MessageBox.Show(
$"确定删除测试项「{p.Name}」?\n文件:{path}\n此操作不可撤销。",
"删除确认", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (confirm != MessageBoxResult.Yes) return;
try
{
if (File.Exists(path))
File.Delete(path);
SelectedPresetIndex = -1;
RefreshPresets();
Log($"已删除测试项:{path}");
}
catch (Exception ex)
{
Log($"删除测试项失败:{ex.Message}");
}
}
/// <summary>清洗文件名中的非法字符(Windows),空白名回退为“未命名”。</summary>
private static string SanitizeFileName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return "未命名";
foreach (char c in Path.GetInvalidFileNameChars())
name = name.Replace(c, '_');
return name.Trim();
}
#endregion
#region 辅助
/// <summary>创建 10 秒超时的取消令牌(命令用)。</summary>
private CancellationToken Ct() => (_cmdCts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
/// <summary>当前选中相位的显示标签</summary>
private string PhaseLabel => SelectedPhaseIndex switch
{
1 => "A相",
2 => "B相",
3 => "C相",
_ => "ALL"
};
/// <summary>
/// 根据 SelectedPhaseIndex 发送 INST:COUP / INST:NSEL 命令切换相位上下文。
/// <para>索引 0 → INST:COUP ALL (三相同时);索引 1~3 → INST:COUP NONE + INST:NSEL n</para>
/// </summary>
private async Task ApplyPhaseAsync()
{
if (SelectedPhaseIndex == 0)
{
await _dev!.设置相位耦合Async("ALL", Ct());
}
else
{
await _dev!.设置相位耦合Async("NONE", Ct());
await _dev!.选择相位Async(SelectedPhaseIndex, Ct());
}
}
/// <summary>统一的设备命令执行包装:防重入、异常捕获与日志。</summary>
private async Task Exec(Func<Task> action)
{
if (_dev == null) { Log("错误:未找到 Chroma61800 设备实例,请检查设备配置"); return; }
if (IsBusy) return;
IsBusy = true;
try
{
await action();
IsConnected = _dev.IsConnected;
}
catch (OperationCanceledException) { Log("命令超时或已取消"); }
catch (Exception ex) { Log($"错误:{ex.Message}"); }
finally { IsBusy = false; }
}
/// <summary>线程安全的日志输出(自动切回 UI 线程,保留最近内容)。</summary>
private void Log(string message)
{
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
_ = dispatcher.BeginInvoke(new Action(() => Log(message)));
return;
}
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
ResponseLog = ResponseLog.Length > 6000
? line + "\n" + ResponseLog[..4000]
: line + "\n" + ResponseLog;
}
/// <summary>解析逗号/空格/分号分隔的数值序列。</summary>
private static double[] ParseDoubles(string text)
{
if (string.IsNullOrWhiteSpace(text)) return Array.Empty<double>();
return text.Split(new[] { ',', ',', ' ', '\t', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => double.TryParse(s.Trim(), out double v) ? (double?)v : null)
.Where(v => v.HasValue)
.Select(v => v!.Value)
.ToArray();
}
/// <summary>解析波形缓冲顺序(A/B)序列。</summary>
private static string[] ParseShapes(string text)
{
if (string.IsNullOrWhiteSpace(text)) return Array.Empty<string>();
return text.Split(new[] { ',', ',', ' ', '\t', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim().ToUpperInvariant())
.ToArray();
}
/// <summary>
/// 下发单个相位的 LIST 序列(调用前须已通过 INST:NSEL 选定目标相、并已 INST:EDIT EACH)。
/// <para>依次下发 FUNC:SHAP:A 波形,以及 DEGR / VOLT:AC / VOLT:DC / FREQ / DWEL / SHAP 各序列,留空的序列跳过。</para>
/// </summary>
private async Task 下发单相序列Async(ListPhaseConfig cfg)
{
if (!string.IsNullOrWhiteSpace(cfg.Waveform))
await _dev!.设置波形Async(cfg.Waveform.Trim(), Ct()); // FUNC:SHAP:A
var degr = ParseDoubles(cfg.Degr);
if (degr.Length > 0) await _dev!.设置列表相位角度顺序Async(degr, Ct()); // LIST:DEGR
var vAcS = ParseDoubles(cfg.VoltAcStart);
if (vAcS.Length > 0) await _dev!.设置列表AC起始电压顺序Async(vAcS, Ct()); // LIST:VOLT:AC:STAR
var vAcE = ParseDoubles(cfg.VoltAcEnd);
if (vAcE.Length > 0) await _dev!.设置列表AC结束电压顺序Async(vAcE, Ct()); // LIST:VOLT:AC:END
var vDcS = ParseDoubles(cfg.VoltDcStart);
if (vDcS.Length > 0) await _dev!.设置列表DC起始电压顺序Async(vDcS, Ct()); // LIST:VOLT:DC:STAR
var vDcE = ParseDoubles(cfg.VoltDcEnd);
if (vDcE.Length > 0) await _dev!.设置列表DC结束电压顺序Async(vDcE, Ct()); // LIST:VOLT:DC:END
var fS = ParseDoubles(cfg.FreqStart);
if (fS.Length > 0) await _dev!.设置列表起始频率顺序Async(fS, Ct()); // LIST:FREQ:STAR
var fE = ParseDoubles(cfg.FreqEnd);
if (fE.Length > 0) await _dev!.设置列表结束频率顺序Async(fE, Ct()); // LIST:FREQ:END
var dwell = ParseDoubles(cfg.Dwell);
if (dwell.Length > 0) await _dev!.设置列表静止时间顺序Async(dwell, Ct()); // LIST:DWEL
var shapes = ParseShapes(cfg.ShapeSeq);
if (shapes.Length > 0) await _dev!.设置列表波形顺序Async(shapes, Ct()); // LIST:SHAP
}
/// <summary>
/// 轮询 TRIG:STATE? 等待当前 LIST 执行结束(返回 OFF),避免在执行过程中改写参数导致仪器报错。
/// </summary>
/// <param name="timeoutMs">最长等待时间(ms),超时后仍继续下发并给出提示</param>
private async Task 等待List空闲Async(int timeoutMs = 8000)
{
if (_dev == null) return;
var sw = System.Diagnostics.Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < timeoutMs)
{
try
{
if (!await _dev.查询触发状态Async(Ct())) return; // OFF:已空闲
}
catch { return; } // 查询失败不阻塞下发流程
await Task.Delay(200);
}
Log("等待 LIST 空闲超时(可能仍在执行),将继续下发,请留意仪器状态");
}
/// <summary>
/// 将相位角序列整体偏移指定角度(结果对 360 取模,保持 0~359.9),用于生成对称三相的 DEGR。
/// </summary>
private static string OffsetDegr(string degrText, double offset)
{
var vals = ParseDoubles(degrText);
if (vals.Length == 0) return degrText;
return string.Join(",", vals.Select(v =>
{
double d = (v + offset) % 360.0;
if (d < 0) d += 360.0;
return d.ToString("0.#", CultureInfo.InvariantCulture);
}));
}
/// <summary>
/// 构建三相对称的 LIST 相位配置:以给定参数为 A 相,B/C 相复制后 DEGR 自动 +120°/+240°。
/// 适用于三相平衡试验(电压/频率范围、骤变、瞬时跌落等)。DWEL 单位 ms。
/// </summary>
private static ListPhaseConfig[] Sym(string waveform, string degr, string vacS, string vacE,
string vdcS, string vdcE, string fS, string fE, string dwell, string shape = "")
{
var a = new ListPhaseConfig
{
Waveform = waveform,
Degr = degr,
VoltAcStart = vacS,
VoltAcEnd = vacE,
VoltDcStart = vdcS,
VoltDcEnd = vdcE,
FreqStart = fS,
FreqEnd = fE,
Dwell = dwell,
ShapeSeq = shape
};
var b = a.Clone(); b.Degr = OffsetDegr(degr, 120);
var c = a.Clone(); c.Degr = OffsetDegr(degr, 240);
return new[] { a, b, c };
}
/// <summary>
/// 构建大纲典型试验预设集合(仅含 LIST 模式可实现的项)。
/// <para>名称含大纲章节号;对称试验用 <see cref="Sym"/> 生成三相,不对称试验逐相显式定义。</para>
/// </summary>
private static IReadOnlyList<ListPreset> BuildPresets() => new List<ListPreset>
{
// ===== 5.1 交流电压范围(三相平衡渐变,2 循环,DWEL 单位 ms)=====
new ListPreset("5.1 上限电压范围 220→265V",
"三相平衡:额定 220V 线性升至上限 265V 再回落,各段 90s,2 循环。判定 A。",
2, Sym("SINE", "0,0", "220,265", "265,220", "", "", "50,50", "50,50", "90000,90000")),
new ListPreset("5.1 下限电压范围 220→85V",
"三相平衡:220V 降至下限 85V 再回升,各段 90s,2 循环。判定 B。",
2, Sym("SINE", "0,0", "220,85", "85,220", "", "", "50,50", "50,50", "90000,90000")),
new ListPreset("5.1 上限保护电压 220→275V",
"验证过压保护点与恢复点:220V 升至 275V 再回落,2 循环。判定 C。",
2, Sym("SINE", "0,0", "220,275", "275,220", "", "", "50,50", "50,50", "90000,90000")),
new ListPreset("5.1 下限保护电压 220→80V",
"验证欠压保护点与恢复点:220V 降至 80V 再回升,2 循环。判定 C。",
2, Sym("SINE", "0,0", "220,80", "80,220", "", "", "50,50", "50,50", "90000,90000")),
// ===== 5.3 交流频率范围(220V 恒定,频率渐变,2 循环)=====
new ListPreset("5.3 上限频率范围 50→65Hz",
"220V 恒定,频率 50Hz 升至 65Hz 再回落,各段 60s,2 循环。判定 B。",
2, Sym("SINE", "0,0", "220,220", "220,220", "", "", "50,65", "65,50", "60000,60000")),
new ListPreset("5.3 下限频率范围 50→40Hz",
"220V 恒定,频率 50Hz 降至 40Hz 再回升,各段 60s,2 循环。判定 B。",
2, Sym("SINE", "0,0", "220,220", "220,220", "", "", "50,40", "40,50", "60000,60000")),
new ListPreset("5.3 上限保护频率 50→75Hz",
"验证过频保护点与恢复点:50Hz 升至 75Hz 再回落,2 循环。判定 C。",
2, Sym("SINE", "0,0", "220,220", "220,220", "", "", "50,75", "75,50", "60000,60000")),
// ===== 5.4 交流电压骤变(指定相位角阶跃,2 循环)=====
new ListPreset("5.4 上限电压骤变 220→265V@90°",
"稳态 220V(5s)→在 90° 相位瞬间跳到 265V(0.5s)→跳回。改 DEGR 中间值可切 0/180/270°。判定 B。",
2, Sym("SINE", "0,90,0", "220,265,220", "220,265,220", "", "", "50,50,50", "50,50,50", "5000,500,5000")),
new ListPreset("5.4 下限电压骤变 220→90V@90°",
"稳态 220V→在 90° 瞬间跌到 90V(0.5s)→恢复。判定 B。",
2, Sym("SINE", "0,90,0", "220,90,220", "220,90,220", "", "", "50,50,50", "50,50,50", "5000,500,5000")),
// ===== 5.5 交流频率骤变(2 循环)=====
new ListPreset("5.5 上限频率骤变 50→75Hz",
"220V 恒定,频率 50Hz 瞬间跳到 75Hz(1s)再回落。判定 B。",
2, Sym("SINE", "0,90,0", "220,220,220", "220,220,220", "", "", "50,75,50", "50,75,50", "5000,1000,5000")),
new ListPreset("5.5 下限频率骤变 50→40Hz",
"220V 恒定,频率 50Hz 瞬间跌到 40Hz(1s)再回升。判定 B。",
2, Sym("SINE", "0,90,0", "220,220,220", "220,220,220", "", "", "50,40,50", "50,40,50", "5000,1000,5000")),
// ===== 5.12 瞬时电压跌落/冲击(1ms 突变,2 循环)=====
new ListPreset("5.12 0°冲击 310V/1ms",
"稳态 220V→0° 相位冲击到 310V 峰值持续 1ms→恢复。1ms 接近 LIST 最小 dwell,若不精确请改 BASE CYCLE。判定不低于 C。",
2, Sym("SINE", "0,0,0", "220,310,220", "220,310,220", "", "", "50,50,50", "50,50,50", "5000,1,5000")),
new ListPreset("5.12 90°跌落 0V/1ms",
"稳态 220V→90° 相位跌到 0V 持续 1ms→恢复。判定不低于 C。",
2, Sym("SINE", "0,90,0", "220,0,220", "220,0,220", "", "", "50,50,50", "50,50,50", "5000,1,5000")),
// ===== 单相深跌(不对称,A 相深跌 / B·C 相浅跌,无限循环)=====
new ListPreset("5.x 单相深跌 A相11.5V(不对称示例)",
"A 相跌到 11.5V、B/C 相跌到 200.1V,跌落 280ms,前后各稳态 20s;三相 DEGR 基准 0/120/240、中间点各异。无限循环,需手动停止。",
0, new[]
{
new ListPhaseConfig { Waveform = "SINE", Degr = "0,0,0", VoltAcStart = "230,11.5,230", VoltAcEnd = "230,11.5,230", VoltDcStart = "10,10,10", VoltDcEnd = "10,10,10", FreqStart = "50,50,50", FreqEnd = "50,50,50", Dwell = "20000,280,20000" },
new ListPhaseConfig { Waveform = "SINE", Degr = "120,92,120", VoltAcStart = "230,200.1,230", VoltAcEnd = "230,200.1,230", VoltDcStart = "10,10,10", VoltDcEnd = "10,10,10", FreqStart = "50,50,50", FreqEnd = "50,50,50", Dwell = "20000,280,20000" },
new ListPhaseConfig { Waveform = "SINE", Degr = "240,268,240", VoltAcStart = "230,200.1,230", VoltAcEnd = "230,200.1,230", VoltDcStart = "10,10,10", VoltDcEnd = "10,10,10", FreqStart = "50,50,50", FreqEnd = "50,50,50", Dwell = "20000,280,20000" },
}),
// ===== 6.3 直流电压骤变(注意量程与耦合模式)=====
new ListPreset("6.3 直流骤变 680→860→450V【注意量程】",
"直流侧瞬时骤变。⚠ 需先在“耦合模式”设为 DC;且 680~860V 已超 Chroma61800.cs 标注的 DC 量程(-424.2~414.2V),须确认设备为高压选配档,否则命令会被拒绝或截顶。",
2, Sym("SINE", "0,0,0,0", "", "", "680,860,450,680", "680,860,450,680", "", "", "5000,5000,5000,5000")),
};
#endregion
public void Dispose()
{
StopSampling();
try { _cmdCts?.Cancel(); } catch { }
_cmdCts?.Dispose();
}
}
/// <summary>
/// 单个相位的 LIST 序列配置(对应界面一组序列文本框)。
/// <para>三相独立下发(INST:EDIT EACH + INST:NSEL)时,每相持有一份本配置,
/// 各字段均为逗号分隔的原始文本,下发时再解析为数值序列。</para>
/// </summary>
public class ListPhaseConfig
{
/// <summary>该相波形 (FUNC:SHAP:A),如 SINE / SQUA</summary>
public string Waveform { get; set; } = "SINE";
/// <summary>相位角度序列 (LIST:DEGR, °)</summary>
public string Degr { get; set; } = "";
/// <summary>AC 起始电压序列 (LIST:VOLT:AC:STAR, V)</summary>
public string VoltAcStart { get; set; } = "";
/// <summary>AC 结束电压序列 (LIST:VOLT:AC:END, V)</summary>
public string VoltAcEnd { get; set; } = "";
/// <summary>DC 起始电压序列 (LIST:VOLT:DC:STAR, V)</summary>
public string VoltDcStart { get; set; } = "";
/// <summary>DC 结束电压序列 (LIST:VOLT:DC:END, V)</summary>
public string VoltDcEnd { get; set; } = "";
/// <summary>起始频率序列 (LIST:FREQ:STAR, Hz)</summary>
public string FreqStart { get; set; } = "";
/// <summary>结束频率序列 (LIST:FREQ:END, Hz)</summary>
public string FreqEnd { get; set; } = "";
/// <summary>静止时间序列 (LIST:DWEL, ms)</summary>
public string Dwell { get; set; } = "";
/// <summary>波形缓冲顺序 (LIST:SHAP, A/B)</summary>
public string ShapeSeq { get; set; } = "";
/// <summary>浅拷贝一份配置(用于"复制到三相")。</summary>
public ListPhaseConfig Clone() => (ListPhaseConfig)MemberwiseClone();
}
/// <summary>
/// 大纲典型试验预设:一次载入即可填充三相 LIST 序列与基础参数(次数/触发/时基)。
/// </summary>
public class ListPreset
{
/// <summary>预设名称(含大纲章节号,供下拉显示)</summary>
public string Name { get; }
/// <summary>试验说明与判定等级提示</summary>
public string Note { get; }
/// <summary>列表执行次数 (LIST:COUN,0 = 无限循环)</summary>
public int Count { get; }
/// <summary>触发形态 (LIST:TRIG)</summary>
public string Trigger { get; }
/// <summary>时间基础 (LIST:BASE)</summary>
public string TimeBase { get; }
/// <summary>三相序列配置(索引 0=A/Φ1, 1=B/Φ2, 2=C/Φ3)</summary>
public ListPhaseConfig[] Phases { get; }
[JsonConstructor]
public ListPreset(string name, string note, int count, ListPhaseConfig[] phases,
string trigger = "AUTO", string timeBase = "TIME")
{
Name = name;
Note = note;
Count = count;
Phases = phases;
Trigger = trigger;
TimeBase = timeBase;
}
}
}