1157 lines
56 KiB
C#
1157 lines
56 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|