using DeviceCommand.Devices; using Microsoft.Win32; using Newtonsoft.Json; 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 System.Windows.Media.Imaging; using UIShare.GlobalVariable; using UIShare.ViewModelBase; namespace PowerAdaptabilityModule.ViewModels { /// /// 电源适应性测试 ViewModel(Chroma61800 交流源一拖三): /// 设备命令面板(三相输出设置 + LIST 模式 + SYNTHESIS 自定义波形输出)。 /// public class PowerAdaptabilityViewModel : NavigateViewModelBase, IDisposable { #region 私有字段 private readonly DeviceManager _dm; private readonly GlobalInfo _globalInfo; private Chroma61800? _dev; private TektronixMSO? _scope; private CancellationTokenSource? _cmdCts; #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); } #endregion #region 示波器(TektronixMSO)屏幕抓图 private string _scopeName = "TektronixMSO"; /// 示波器设备名称(DeviceMap 键名) public string ScopeName { get => _scopeName; set => SetProperty(ref _scopeName, value); } private bool _isScopeConnected; /// 示波器连接状态 public bool IsScopeConnected { get => _isScopeConnected; set => SetProperty(ref _isScopeConnected, value); } private string _scopeImageDir = @"D:\acp\源测试\波形截图"; /// 波形图片存储位置(抓取的屏幕 PNG 保存目录),抓图/浏览选择时自动持久化 public string ScopeImageDir { get => _scopeImageDir; set => SetProperty(ref _scopeImageDir, value); } private BitmapImage? _scopeImage; /// 当前显示的波形截图(由 LIST / SYNTHESIS 按钮按配置文件同名规则加载) public BitmapImage? ScopeImage { get => _scopeImage; set => SetProperty(ref _scopeImage, value); } private bool _hasScopeImage; /// 是否已加载波形截图(控制图片区占位提示的显隐) public bool HasScopeImage { get => _hasScopeImage; set => SetProperty(ref _hasScopeImage, value); } private string _scopeImageHint = ""; /// 波形截图查找/加载结果提示 public string ScopeImageHint { get => _scopeImageHint; set => SetProperty(ref _scopeImageHint, value); } #endregion #region 示波器设置(采集 / 水平 / 通道 / 测量,对应 TektronixMSO 已有 SCPI 方法) /// 采集模式选项 (ACQuire:MODe,显示名与 TekAcquisitionMode 枚举一致) public IReadOnlyList ScopeAcquireModeOptions { get; } = new[] { "SAMple", "PEAKdetect", "HIRes", "AVErage", "ENVelope" }; /// 测量类型选项 (MEASUrement:TYPe,显示名与 TekMeasurementType 枚举一致) public IReadOnlyList ScopeMeasTypeOptions { get; } = new[] { "AMPLitude", "FREQuency", "MEAN", "PK2PK", "MAXimum", "MINimum" }; /// 通道选择列表 (对应通道号 1~4) public IReadOnlyList ScopeChannelOptions { get; } = new[] { "CH1", "CH2", "CH3", "CH4" }; private string _scopeAcquireMode = "SAMple"; /// 采集模式 (ACQuire:MODe) public string ScopeAcquireMode { get => _scopeAcquireMode; set => SetProperty(ref _scopeAcquireMode, value); } private double _scopeTimeScale = 0.001; /// 水平刻度待设值 (HORizontal:SCAle, s/Div) public double ScopeTimeScale { get => _scopeTimeScale; set => SetProperty(ref _scopeTimeScale, value); } private long _scopeRecordLength = 6250000; /// 记录长度待设值 (HORizontal:RECOrdlength, 采样点数) public long ScopeRecordLength { get => _scopeRecordLength; set => SetProperty(ref _scopeRecordLength, value); } private string _sampleRateText = ""; /// 采样率查询结果显示 (HORizontal:SAMPLERate?, Sa/s) public string SampleRateText { get => _sampleRateText; set => SetProperty(ref _sampleRateText, value); } private int _selectedScopeChannelIndex; /// 当前编辑通道索引 (0~3 → CH1~CH4) public int SelectedScopeChannelIndex { get => _selectedScopeChannelIndex; set => SetProperty(ref _selectedScopeChannelIndex, value); } private double _channelScale = 100; /// 通道垂直刻度待设值 (CH:SCAle, V/Div) public double ChannelScale { get => _channelScale; set => SetProperty(ref _channelScale, value); } private double _channelPosition; /// 通道垂直位置待设值 (CH:POSition, Div) public double ChannelPosition { get => _channelPosition; set => SetProperty(ref _channelPosition, value); } private int _measurementNumber = 1; /// 测量项编号 (MEASUrement:MEASn) public int MeasurementNumber { get => _measurementNumber; set => SetProperty(ref _measurementNumber, value); } private string _measurementType = "FREQuency"; /// 测量类型 (MEASUrement:TYPe) public string MeasurementType { get => _measurementType; set => SetProperty(ref _measurementType, value); } private string _measurementSource = "CH1"; /// 测量源 (MEASUrement:SOUrce1, 如 CH1/CH2) public string MeasurementSource { get => _measurementSource; set => SetProperty(ref _measurementSource, value); } private string _measResultText = ""; /// 测量查询结果显示 public string MeasResultText { get => _measResultText; set => SetProperty(ref _measResultText, value); } /// 当前编辑通道号 (1~4) private int ScopeChannelNo => SelectedScopeChannelIndex + 1; #endregion #region 输入参数(三相输出设置) private double _voltage = 220; /// 待设置的交流电压 (V) public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); } private double _frequency = 50; /// 待设置的输出频率 (Hz) public double Frequency { get => _frequency; set => SetProperty(ref _frequency, value); } private string _waveform = "SINE"; /// 波形选择 public string Waveform { get => _waveform; set => SetProperty(ref _waveform, value); } private string _couplingMode = "AC"; /// 输出耦合模式 (AC / DC / ACDC) public string CouplingMode { get => _couplingMode; set => SetProperty(ref _couplingMode, value); } private string _threePhaseRelation = "BALANCE"; /// 三相关系 (INDEPEND / SAMEFREQ / BALANCE) public string ThreePhaseRelation { get => _threePhaseRelation; set => SetProperty(ref _threePhaseRelation, value); } /// 相位选择列表: 0=ALL(三相同时), 1=A相, 2=B相, 3=C相 public List PhaseOptions { get; } = new() { "ALL (三相)", "A相 (Φ1)", "B相 (Φ2)", "C相 (Φ3)" }; private int _selectedPhaseIndex; /// 当前选中的相位索引 (0=ALL, 1=A, 2=B, 3=C) public int SelectedPhaseIndex { get => _selectedPhaseIndex; set => SetProperty(ref _selectedPhaseIndex, value); } #endregion #region 输入参数(LIST 模式 · 三相独立编辑) /// 下发完成到启动之间的延时(ms),对应 *D 命令,等待仪器就绪 private const int StartDelayMs = 200; private string _listTrigger = "AUTO"; /// 列表触发形态 (AUTO / MANUAL / EXCITE) public string ListTrigger { get => _listTrigger; set => SetProperty(ref _listTrigger, value); } private int _listCount; /// 列表执行次数 (0 = 无限循环, 最大 65535) public int ListCount { get => _listCount; set => SetProperty(ref _listCount, value); } private string _listTimeBase = "TIME"; /// 列表时间基础 (TIME / CYCLE) public string ListTimeBase { get => _listTimeBase; set => SetProperty(ref _listTimeBase, value); } private string _listPhaseContinue = "DISABLE"; /// 相位连续功能 (LIST:PHAS:CONT, DISABLE / ENABLE) public string ListPhaseContinue { get => _listPhaseContinue; set => SetProperty(ref _listPhaseContinue, value); } /// 三相各自的 LIST 序列配置(索引 0=A/Φ1, 1=B/Φ2, 2=C/Φ3),INST:EDIT EACH 逐相下发 private readonly ListPhaseConfig[] _phaseConfigs = { new(), new(), new() }; /// 相位编辑选项(对应 INST:NSEL 1/2/3) public List ListPhaseOptions { get; } = new() { "A相 (Φ1)", "B相 (Φ2)", "C相 (Φ3)" }; private int _listEditPhaseIndex; /// 当前正在编辑的相位索引 (0=A, 1=B, 2=C),切换时下方序列文本框自动刷新为该相内容 public int ListEditPhaseIndex { get => _listEditPhaseIndex; set { if (SetProperty(ref _listEditPhaseIndex, value)) RaiseListSequenceChanged(); } } /// 当前选中相位的配置对象 private ListPhaseConfig CurCfg => _phaseConfigs[_listEditPhaseIndex]; // —— 以下文本属性均代理到"当前编辑相位"的配置,切换相位时统一刷新 —— /// 相位角度序列 (LIST:DEGR, °) public string ListDegrText { get => CurCfg.Degr; set { if (CurCfg.Degr != value) { CurCfg.Degr = value; RaisePropertyChanged(); } } } /// AC 起始电压序列 (LIST:VOLT:AC:STAR, V) public string ListVoltAcStartText { get => CurCfg.VoltAcStart; set { if (CurCfg.VoltAcStart != value) { CurCfg.VoltAcStart = value; RaisePropertyChanged(); } } } /// AC 结束电压序列 (LIST:VOLT:AC:END, V) public string ListVoltAcEndText { get => CurCfg.VoltAcEnd; set { if (CurCfg.VoltAcEnd != value) { CurCfg.VoltAcEnd = value; RaisePropertyChanged(); } } } /// DC 起始电压序列 (LIST:VOLT:DC:STAR, V) public string ListVoltDcStartText { get => CurCfg.VoltDcStart; set { if (CurCfg.VoltDcStart != value) { CurCfg.VoltDcStart = value; RaisePropertyChanged(); } } } /// DC 结束电压序列 (LIST:VOLT:DC:END, V) public string ListVoltDcEndText { get => CurCfg.VoltDcEnd; set { if (CurCfg.VoltDcEnd != value) { CurCfg.VoltDcEnd = value; RaisePropertyChanged(); } } } /// 起始频率序列 (LIST:FREQ:STAR, Hz) public string ListFreqStartText { get => CurCfg.FreqStart; set { if (CurCfg.FreqStart != value) { CurCfg.FreqStart = value; RaisePropertyChanged(); } } } /// 结束频率序列 (LIST:FREQ:END, Hz) public string ListFreqEndText { get => CurCfg.FreqEnd; set { if (CurCfg.FreqEnd != value) { CurCfg.FreqEnd = value; RaisePropertyChanged(); } } } /// 静止时间序列 (LIST:DWEL, ms) public string ListDwellText { get => CurCfg.Dwell; set { if (CurCfg.Dwell != value) { CurCfg.Dwell = value; RaisePropertyChanged(); } } } /// 波形顺序 (LIST:SHAP,可直接写形状名或引用 A/B 组) public string ListShapeText { get => CurCfg.ShapeSeq; set { if (CurCfg.ShapeSeq != value) { CurCfg.ShapeSeq = value; RaisePropertyChanged(); } } } /// 切换编辑相位后,通知所有序列文本框刷新为新相位内容 private void RaiseListSequenceChanged() { 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 WaveformOptions { get; } = new[] { "SINE", "SQUA", "TRIAN", "CSIN" }; public IReadOnlyList CouplingOptions { get; } = new[] { "AC", "DC", "ACDC" }; public IReadOnlyList RelationOptions { get; } = new[] { "INDEPEND", "SAMEFREQ", "BALANCE" }; public IReadOnlyList ListTriggerOptions { get; } = new[] { "AUTO", "MANUAL", "EXCITE" }; public IReadOnlyList ListTimeBaseOptions { get; } = new[] { "TIME", "CYCLE" }; public IReadOnlyList ListPhaseContinueOptions { get; } = new[] { "DISABLE", "ENABLE" }; #endregion #region 输入参数(SYNTHESIS 自定义谐波波形输出) /// 谐波阶数据格式选项 (SYNT:COMP):VALUE=绝对值,PERCENT=基频百分率,后缀 1/2/3 为三段波形 public IReadOnlyList SynthComposeOptions { get; } = new[] { "VALUE1", "VALUE2", "VALUE3", "PERCENT1", "PERCENT2", "PERCENT3" }; /// 基频选项 (SYNT:FREQ):仅支持 50 / 60 Hz public IReadOnlyList SynthFrequencyOptions { get; } = new[] { 50, 60 }; private string _synthCompose = "VALUE1"; /// 谐波阶数据格式 (SYNT:COMP) public string SynthCompose { get => _synthCompose; set => SetProperty(ref _synthCompose, value); } private int _synthFrequency = 50; /// 基频 (SYNT:FREQ, 50 | 60 Hz) public int SynthFrequency { get => _synthFrequency; set => SetProperty(ref _synthFrequency, value); } private double _synthFundamental = 220; /// 基波交流电压 (SYNT:FUND, 0.0 ~ 350.0 V) public double SynthFundamental { get => _synthFundamental; set => SetProperty(ref _synthFundamental, value); } private double _synthDc; /// 叠加直流电压 (SYNT:DC, -495.0 ~ 495.0 V) public double SynthDc { get => _synthDc; set => SetProperty(ref _synthDc, value); } // —— SYNTHESIS 三相独立编辑:每相持有各自的起始相位/谐波振幅/谐波相位(索引 0=A/Φ1, 1=B/Φ2, 2=C/Φ3),INST:EDIT EACH 逐相下发 —— private readonly double[] _synthStartPhases = new double[3]; private readonly string[] _synthAmplitudeTexts = new string[3]; private readonly string[] _synthPhaseTexts = new string[3]; private int _synthEditPhaseIndex; /// 当前正在编辑的相位索引 (0=A, 1=B, 2=C),切换时下方起始相位/谐波文本框自动刷新为该相内容 public int SynthEditPhaseIndex { get => _synthEditPhaseIndex; set { if (SetProperty(ref _synthEditPhaseIndex, value)) { RaisePropertyChanged(nameof(SynthStartPhase)); RaisePropertyChanged(nameof(SynthAmplitudeText)); RaisePropertyChanged(nameof(SynthPhaseText)); } } } /// 起始相位角 (SYNT:SPH, 0.0 ~ 359.9 °),作用于当前编辑相位;复制到三相时按 ±120° 偏移 public double SynthStartPhase { get => _synthStartPhases[_synthEditPhaseIndex]; set { _synthStartPhases[_synthEditPhaseIndex] = value; RaisePropertyChanged(); } } /// 各次谐波振幅序列文本 (SYNT:AMPL, 英文逗号分隔, 首值对应 2 次谐波,2~50 次共 49 个值),作用于当前编辑相位 public string SynthAmplitudeText { get => _synthAmplitudeTexts[_synthEditPhaseIndex]; set { _synthAmplitudeTexts[_synthEditPhaseIndex] = value; RaisePropertyChanged(); } } /// 各次谐波相位角序列文本 (SYNT:PHAS, 英文逗号分隔, 0~359.9°),作用于当前编辑相位 public string SynthPhaseText { get => _synthPhaseTexts[_synthEditPhaseIndex]; set { _synthPhaseTexts[_synthEditPhaseIndex] = value; RaisePropertyChanged(); } } #endregion #region SYNTHESIS 波形预设(文件:D:\acp\源测试\合成波形,一键载入/保存自定义谐波波形) /// 合成波形预设文件目录(LIST 测试项目录下的子目录,避免与 LIST 预设混淆) private static readonly string SynthStorageDir = Path.Combine(PresetStorageDir, "合成波形"); /// 合成波形预设文件目录提示(供界面显示) public string SynthStorageHint => $"波形文件目录:{SynthStorageDir}"; // 从磁盘加载的合成波形预设集合 private List _synthPresets = new(); /// 合成波形预设集合 public IReadOnlyList SynthPresets => _synthPresets; private List _synthPresetOptions = new(); /// 合成波形预设名称列表(供 ComboBox 显示) public IReadOnlyList SynthPresetOptions => _synthPresetOptions; private string _synthPresetName = ""; /// 当前波形名称(保存时作为文件名;载入后自动填充) public string SynthPresetName { get => _synthPresetName; set => SetProperty(ref _synthPresetName, value); } private string _synthPresetNote = ""; /// 当前波形说明(随文件保存,载入后自动填充) public string SynthPresetNote { get => _synthPresetNote; set => SetProperty(ref _synthPresetNote, value); } /// 当前已打开的波形文件完整路径(载入或另存后设置);null 表示尚未与磁盘文件绑定 private string? _currentSynthPath; private int _selectedSynthPresetIndex = -1; /// 当前选中的合成波形预设索引(-1 表示未选择) public int SelectedSynthPresetIndex { get => _selectedSynthPresetIndex; set { if (SetProperty(ref _selectedSynthPresetIndex, value)) { // 选中后把名称/说明填入编辑框,便于修改后另存或覆盖保存 if (value >= 0 && value < _synthPresets.Count) { SynthPresetName = _synthPresets[value].Name; SynthPresetNote = _synthPresets[value].Note; } } } } #endregion #region 试验预设(测试项文件:D:\acp\源测试,一键载入大纲典型波形) /// 测试项文件默认存储目录(保存与查询均在此目录) private const string PresetStorageDir = @"D:\acp\源测试"; /// 测试项文件扩展名 private const string PresetFileExt = ".json"; /// 测试项文件目录提示(供界面显示) public string PresetStorageHint => $"测试项文件目录:{PresetStorageDir}"; // 从磁盘加载的测试项集合(源真相为 PresetStorageDir 下的 .json 文件) private List _presets = new(); /// 测试项集合(由 PresetStorageDir 下的文件加载而来) public IReadOnlyList Presets => _presets; // 供 ComboBox 显示的名称列表,随磁盘刷新 private List _presetOptions = new(); /// 测试项名称列表(供 ComboBox 显示) public IReadOnlyList PresetOptions => _presetOptions; private string _presetName = ""; /// 当前测试项名称(保存时作为文件名;载入后自动填充,便于覆盖保存) public string PresetName { get => _presetName; set => SetProperty(ref _presetName, value); } private string _presetNote = ""; /// 当前测试项说明(随文件保存,载入后自动填充) public string PresetNote { get => _presetNote; set => SetProperty(ref _presetNote, value); } /// 当前已打开的测试项文件完整路径(载入或另存后设置);null 表示尚未与磁盘文件绑定 private string? _currentPresetPath; private int _selectedPresetIndex = -1; /// 当前选中的测试项索引(-1 表示未选择) 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 = ""; /// 操作与采样日志 public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); } #endregion #region 命令 public ICommand QueryIdnCommand { get; } public ICommand ResetCommand { get; } public ICommand ExitRemoteCommand { 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 SavePresetAsCommand { get; } public ICommand DeletePresetCommand { get; } public ICommand CopyPhaseToThreeCommand { get; } public ICommand DownloadThreePhaseListCommand { get; } public ICommand ListStopCommand { get; } public ICommand DownloadSynthesisCommand { get; } public ICommand SynthStopCommand { get; } public ICommand ResetSynthHarmonicsCommand { get; } public ICommand CopySynthPhaseToThreeCommand { get; } public ICommand LoadSynthPresetCommand { get; } public ICommand SaveSynthPresetCommand { get; } public ICommand SaveSynthPresetAsCommand { get; } public ICommand DeleteSynthPresetCommand { get; } public ICommand CaptureScopeImageCommand { get; } public ICommand BrowseScopeImageDirCommand { get; } public ICommand ShowListWaveImageCommand { get; } public ICommand ShowSynthWaveImageCommand { get; } public ICommand ScopeAcquireRunCommand { get; } public ICommand ScopeAcquireStopCommand { get; } public ICommand QueryAcquireStateCommand { get; } public ICommand ApplyAcquireModeCommand { get; } public ICommand SetScopeTimeScaleCommand { get; } public ICommand SetRecordLengthCommand { get; } public ICommand QuerySampleRateCommand { get; } public ICommand ChannelDisplayOnCommand { get; } public ICommand ChannelDisplayOffCommand { get; } public ICommand SetChannelScaleCommand { get; } public ICommand SetChannelPositionCommand { get; } public ICommand AddMeasurementCommand { get; } public ICommand QueryMeasCurrentCommand { get; } public ICommand QueryMeasMeanCommand { get; } public ICommand QueryMeasMaxCommand { get; } public ICommand QueryScopeIdnCommand { get; } public ICommand ClearScopeErrorsCommand { get; } public ICommand ReadScopeErrorsCommand { get; } #endregion public PowerAdaptabilityViewModel(IContainerProvider containerProvider) : base(containerProvider) { _dm = containerProvider.Resolve(); _globalInfo = containerProvider.Resolve(); // 初始化测试项文件目录(首次运行迁移内置预设),再从磁盘加载测试项 EnsurePresetStorage(); RefreshPresets(); // 初始化合成波形文件目录(首次运行迁移内置示例),再从磁盘加载波形 EnsureSynthStorage(); RefreshSynthPresets(); // 恢复上次设置的波形图片存储位置 LoadScopeImageDir(); #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("设备已复位"); })); ExitRemoteCommand = new DelegateCommand(async () => await Exec(async () => { await _dev!.退出远程Async(Ct()); Log("已发送退出远程 SYST:LOC,前面板应恢复可用(手册注明僅 RS-232 下有效)"); })); 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; // 旧测试项文件没有相位连续键(PhaseContinue 为 null)时保持界面当前值不动 if (!string.IsNullOrWhiteSpace(p.PhaseContinue)) ListPhaseContinue = p.PhaseContinue; 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; _currentPresetPath = PresetPathOf(p.Name); // 记住当前打开的文件,此后“保存”只覆盖它 Log($"已载入测试项:{p.Name}(次数={(p.Count == 0 ? "无限" : p.Count.ToString())} 触发={p.Trigger} 时基={p.TimeBase} 相位连续={p.PhaseContinue ?? "未指定(保持当前)"})"); }); // 保存:覆盖当前打开的测试项文件(未打开文件时按“另存为”新建) SavePresetCommand = new DelegateCommand(SaveCurrentPreset); // 另存为:以“名称”框内容为新文件名新建文件,不修改当前打开的文件 SavePresetAsCommand = new DelegateCommand(SaveCurrentPresetAs); // 删除当前选中的测试项文件(带二次确认) 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(逐相独立编辑) for (int p = 0; p < 3; p++) { await _dev.选择相位Async(p + 1, Ct()); // INST:NSEL 1|2|3 // COUN/TRIG/BASE 在 EACH 逐相编辑模式下随相位上下文分相:必须在每相 NSEL 之后各发一次, // 否则未选中的相 COUN 保持默认 0(无限循环),表现为“设了次数仍一直执行” await _dev.设置列表执行次数Async(ListCount, Ct()); // LIST:COUN(0=无限循环) await _dev.设置列表触发形态Async(ListTrigger, Ct()); // LIST:TRIG await _dev.设置列表时间基础Async(ListTimeBase, Ct()); // LIST:BASE await _dev.设置列表相位连续Async(ListPhaseContinue, Ct()); // LIST:PHAS:CONT(相位连续,随相位上下文分相) await 下发单相序列Async(_phaseConfigs[p]); // LIST:xxx 序列 } await _dev.设置操作模态Async("LIST", Ct()); // OUTP:MODE LIST // 回读校验:确认 COUN 真的写入(当前相位上下文=相3);读回 0 而设>0 即未生效 int counBack = await _dev.查询列表执行次数Async(Ct()); await _dev.触发列表执行Async(true, Ct()); // TRIG ON Log($"三相独立 LIST 已下发并启动(触发={ListTrigger} 次数={(ListCount == 0 ? "无限" : ListCount.ToString())} 时基={ListTimeBase};回读COUN={counBack})"); })); ListStopCommand = new DelegateCommand(async () => await Exec(async () => { await _dev!.触发列表执行Async(false, Ct()); Log("LIST 已停止执行(TRIG OFF)"); })); // 下发 SYNTHESIS 自定义谐波波形并启动(三相独立):INST:EDIT EACH + NSEL 逐相下发, // 谐波内容三相相同(复制到三相生成),仅起始相位按 ±120° 对称;手册要求进入 SYNTH 前先离开输出 DownloadSynthesisCommand = new DelegateCommand(async () => await Exec(async () => { // 先强制 TRIG OFF 停止连续执行模态并等状态回 OFF,再改写参数(执行中改写会被仪器忽略) await 等待List空闲Async(); await _dev!.清除错误队列Async(Ct()); // *CLS await _dev.设置输出开关Async(false, Ct()); // OUTP OFF(进入 SYNTH 前先离开输出) await _dev.设置操作模态Async("FIXED", Ct()); // OUTP:MODE FIXED(清模态) await _dev.设置单三相模式Async(true, Ct()); // INST:PHAS THREE await _dev.设置编辑方式Async("EACH", Ct()); // INST:EDIT EACH(逐相独立编辑,缺此步无法分相) for (int p = 0; p < 3; p++) { await _dev.选择相位Async(p + 1, Ct()); // INST:NSEL 1|2|3 await _dev.设置合成数据格式Async(SynthCompose, Ct()); // SYNT:COMP(EACH 模式随相位上下文生效,逐相各发一次) await _dev.设置合成基频Async(SynthFrequency, Ct()); // SYNT:FREQ await _dev.设置合成基波电压Async(SynthFundamental, Ct()); // SYNT:FUND await _dev.设置合成直流电压Async(SynthDc, Ct()); // SYNT:DC await _dev.设置合成起始相位Async(_synthStartPhases[p], Ct()); // SYNT:SPH(分相参数,三相 ±120° 对称) var ampl = ParseDoubles(_synthAmplitudeTexts[p]); if (ampl.Length > 0) await _dev.设置合成谐波振幅Async(ampl, Ct()); // SYNT:AMPL var phas = ParseDoubles(_synthPhaseTexts[p]); if (phas.Length > 0) await _dev.设置合成谐波相位Async(phas, Ct()); // SYNT:PHAS } // 回读校验:逐相回读起始相位确认 ±120° 偏移真实写入(读回与界面不符=被拒绝) string compBack = await _dev.查询合成数据格式Async(Ct()); double fundBack = await _dev.查询合成基波电压Async(Ct()); var sphBack = new List(); for (int p = 0; p < 3; p++) { await _dev.选择相位Async(p + 1, Ct()); // INST:NSEL double sph = await _dev.查询合成起始相位Async(Ct()); sphBack.Add($"{ListPhaseOptions[p].Substring(0, 1)}={sph:0.#}°"); } double[] amplBack = await _dev.查询合成谐波振幅Async(Ct()); Log($"回读校验:COMP 设={SynthCompose} 读={compBack};FUND 设={SynthFundamental} 读={fundBack};SPH {string.Join(" ", sphBack)};AMPL 回读{amplBack.Length}阶(仪器实际保留的阶数,当前上下文=C相),前6阶={string.Join(",", amplBack.Take(6))}"); await _dev.设置操作模态Async("SYNTH", Ct()); // OUTP:MODE SYNTH await _dev.触发列表执行Async(true, Ct()); // TRIG ON double vrms = await _dev.读取交流电压Async(Ct()); // MEAS:VOLT:AC? 实测输出 Log($"SYNTHESIS 三相波形已下发并启动(格式={SynthCompose} 基频={SynthFrequency}Hz 基波={SynthFundamental}V 谐波阶数={ParseDoubles(_synthAmplitudeTexts[_synthEditPhaseIndex]).Length})实测Vrms={vrms}"); })); SynthStopCommand = new DelegateCommand(async () => await Exec(async () => { await _dev!.触发列表执行Async(false, Ct()); Log("SYNTHESIS 已停止执行(TRIG OFF)"); })); // 重置谐波:SYNT 数组首值对应 2 次谐波,仪器谐波范围 2~50 阶共 49 个值(填 50 个则末值落到不存在的 51 阶,可能被拒或截断) // (仅改界面文本,需再点「下发并启动 SYNTH」才写入仪器) ResetSynthHarmonicsCommand = new DelegateCommand(() => { string zeros = string.Join(",", Enumerable.Repeat("0", 49)); SynthAmplitudeText = zeros; SynthPhaseText = zeros; Log("SYNTHESIS 谐波已重置:振幅/相位两框均填入 49 个 0(覆盖 2~50 阶);点「下发并启动 SYNTH」写入仪器"); }); // 复制当前相到三相:谐波内容相同,起始相位按 ±120° 偏移生成对称三相(与 LIST 复制同机制,仅改界面数据) CopySynthPhaseToThreeCommand = new DelegateCommand(() => { int src = _synthEditPhaseIndex; for (int p = 0; p < 3; p++) { if (p == src) continue; _synthAmplitudeTexts[p] = _synthAmplitudeTexts[src]; _synthPhaseTexts[p] = _synthPhaseTexts[src]; double d = (_synthStartPhases[src] + (p - src) * 120.0) % 360.0; if (d < 0) d += 360.0; _synthStartPhases[p] = d; } Log($"已将 [{ListPhaseOptions[src]}] 的 SYNTHESIS 参数复制到三相(起始相位自动 ±120° 偏移,谐波内容相同);点「下发并启动 SYNTH」写入仪器"); }); // 合成波形的载入 / 保存 / 另存为 / 删除(与 LIST 测试项同机制,文件存于 D:\acp\源测试\合成波形) LoadSynthPresetCommand = new DelegateCommand(LoadCurrentSynthPreset); SaveSynthPresetCommand = new DelegateCommand(SaveCurrentSynthPreset); SaveSynthPresetAsCommand = new DelegateCommand(SaveCurrentSynthPresetAs); DeleteSynthPresetCommand = new DelegateCommand(DeleteCurrentSynthPreset); // 示波器抓图:抓取当前屏幕保存为 PNG 到存储位置(图片区不自动显示,改由 LIST / SYNTHESIS 按钮按配置文件名加载) CaptureScopeImageCommand = new DelegateCommand(async () => await ExecScope(async () => { if (string.IsNullOrWhiteSpace(ScopeImageDir)) { Log("请先设置波形图片存储位置"); return; } SaveScopeImageDir(); // 记住本次使用的存储位置 Directory.CreateDirectory(ScopeImageDir); string file = Path.Combine(ScopeImageDir, $"屏幕_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png"); await _scope!.保存屏幕截图(file, Ct()); // SAVe:IMAGe → *OPC? → READFile → 写盘 Log($"示波器屏幕图片已抓取:{file}"); })); // 浏览选择波形图片存储位置(选择后立即记住) BrowseScopeImageDirCommand = new DelegateCommand(() => { var dlg = new OpenFolderDialog { Title = "选择波形图片存储位置", InitialDirectory = Directory.Exists(ScopeImageDir) ? ScopeImageDir : PresetStorageDir, }; if (dlg.ShowDialog() != true) return; // 用户取消 ScopeImageDir = dlg.FolderName; SaveScopeImageDir(); Log($"波形图片存储位置已设置:{ScopeImageDir}"); }); // 按当前载入的配置文件查看对应波形截图: // LIST → {当前测试项文件名}.png;SYNTHESIS → {当前波形文件名}.png;找不到则清空图片区 ShowListWaveImageCommand = new DelegateCommand(() => ShowWaveImage(_currentPresetPath, "LIST")); ShowSynthWaveImageCommand = new DelegateCommand(() => ShowWaveImage(_currentSynthPath, "SYNTHESIS")); // —— 采集控制 —— ScopeAcquireRunCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.设置采集状态(true, Ct()); Log("示波器采集已启动 (RUN)"); })); // 停止采集定格画面后抓图,是最稳定的截图方式(截图抓的是当前屏幕画面) ScopeAcquireStopCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.设置采集状态(false, Ct()); Log("示波器采集已停止 (STOP),画面已定格;可点「抓取示波器屏幕图片」截取稳定波形"); })); QueryAcquireStateCommand = new DelegateCommand(async () => await ExecScope(async () => Log("采集状态: " + await _scope!.查询采集状态(Ct())))); ApplyAcquireModeCommand = new DelegateCommand(async () => await ExecScope(async () => { if (!Enum.TryParse(ScopeAcquireMode, out var mode)) { Log($"未知采集模式:{ScopeAcquireMode}"); return; } await _scope!.设置采集模式(mode, Ct()); Log($"采集模式已设置:{mode}"); })); // —— 水平系统 —— SetScopeTimeScaleCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.设置水平刻度(ScopeTimeScale, Ct()); Log($"水平刻度={ScopeTimeScale} s/Div"); })); SetRecordLengthCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.设置记录长度(ScopeRecordLength, Ct()); Log($"记录长度={ScopeRecordLength} 点"); })); QuerySampleRateCommand = new DelegateCommand(async () => await ExecScope(async () => { double sr = await _scope!.查询采样率(Ct()); SampleRateText = $"{sr:0.###} Sa/s"; Log($"采样率={sr} Sa/s"); })); // —— 通道设置 —— ChannelDisplayOnCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.设置通道显示开关(ScopeChannelNo, true, Ct()); Log($"{ScopeChannelOptions[SelectedScopeChannelIndex]} 显示已开启"); })); ChannelDisplayOffCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.设置通道显示开关(ScopeChannelNo, false, Ct()); Log($"{ScopeChannelOptions[SelectedScopeChannelIndex]} 显示已关闭"); })); SetChannelScaleCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.设置通道垂直刻度(ScopeChannelNo, ChannelScale, Ct()); Log($"{ScopeChannelOptions[SelectedScopeChannelIndex]} 垂直刻度={ChannelScale} V/Div"); })); SetChannelPositionCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.设置通道垂直位置(ScopeChannelNo, ChannelPosition, Ct()); Log($"{ScopeChannelOptions[SelectedScopeChannelIndex]} 垂直位置={ChannelPosition} Div"); })); // —— 自动测量:开启测量项 + 类型/源一次完成(4/5/6 系必须先 ADDNew 动态创建) —— AddMeasurementCommand = new DelegateCommand(async () => await ExecScope(async () => { if (!Enum.TryParse(MeasurementType, out var type)) { Log($"未知测量类型:{MeasurementType}"); return; } await _scope!.开启测量项(MeasurementNumber, Ct()); await _scope.设置测量类型(MeasurementNumber, type, Ct()); await _scope.设置测量源(MeasurementNumber, MeasurementSource, Ct()); Log($"测量项 MEAS{MeasurementNumber} 已开启:{type} @ {MeasurementSource}"); })); QueryMeasCurrentCommand = new DelegateCommand(async () => await ExecScope(async () => { double v = await _scope!.查询测量项当前值(MeasurementNumber, Ct()); MeasResultText = $"MEAS{MeasurementNumber} 当前值: {v}"; Log(MeasResultText); })); QueryMeasMeanCommand = new DelegateCommand(async () => await ExecScope(async () => { double v = await _scope!.查询测量项全局平均值(MeasurementNumber, Ct()); MeasResultText = $"MEAS{MeasurementNumber} 全局均值: {v}"; Log(MeasResultText); })); QueryMeasMaxCommand = new DelegateCommand(async () => await ExecScope(async () => { double v = await _scope!.查询测量项全局最大值(MeasurementNumber, Ct()); MeasResultText = $"MEAS{MeasurementNumber} 全局最大: {v}"; Log(MeasResultText); })); // —— 设备辅助 —— QueryScopeIdnCommand = new DelegateCommand(async () => await ExecScope(async () => Log("示波器 IDN: " + await _scope!.查询设备标识(Ct())))); ClearScopeErrorsCommand = new DelegateCommand(async () => await ExecScope(async () => { await _scope!.清除错误队列Async(Ct()); Log("示波器错误队列已清除 (*CLS)"); })); ReadScopeErrorsCommand = new DelegateCommand(async () => await ExecScope(async () => Log("示波器错误队列: " + await _scope!.读取所有错误消息Async(Ct())))); #endregion Initialize(); } #region 初始化 / 导航 /// /// 查找 Chroma61800 设备实例:优先当前作用域 DeviceManager,回退全局 HardwarePool(跨作用域共享实例)。 /// 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 设备,请先在设置界面添加并启用该设备"); // 示波器实例:查找方式与交流源一致(DeviceMap 优先,回退全局 HardwarePool) TektronixMSO? scopeFound = null; string? sfn = null; foreach (var kv in _dm.DeviceMap) { if (kv.Value is TektronixMSO s) { scopeFound = s; sfn = kv.Key; break; } } if (scopeFound == null) { foreach (var kv in _globalInfo.HardwarePool) { if (kv.Value.IsValueCreated && kv.Value.Value is TektronixMSO s) { scopeFound = s; sfn = kv.Key; break; } } } _scope = scopeFound; ScopeName = sfn ?? "TektronixMSO (未找到)"; IsScopeConnected = _scope?.IsConnected ?? false; if (scopeFound != null) Log($"已关联示波器 [{ScopeName}],连接:{(IsScopeConnected ? "已连接" : "未连接")}"); } public override void OnNavigatedTo(NavigationContext navigationContext) { Initialize(); } public override void OnNavigatedFrom(NavigationContext navigationContext) { } #endregion #region 测试项文件读写(D:\acp\源测试) /// /// 确保测试项目录存在;若目录中尚无任何测试项文件,则把内置大纲预设迁移写入为 .json 文件。 /// 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}"); } } /// 从磁盘重新加载测试项列表,刷新下拉框并按名称恢复选中项。 private void RefreshPresets() { // 刷新前记住选中项名称:列表重建后索引会漂移,按名称恢复最可靠 string? selectedName = (_selectedPresetIndex >= 0 && _selectedPresetIndex < _presets.Count) ? _presets[_selectedPresetIndex].Name : null; var list = new List(); 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(json); if (item != null) list.Add(NormalizePresetName(item, file)); } 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)); SelectedPresetIndex = selectedName != null ? _presets.FindIndex(p => p.Name == selectedName) : -1; } /// /// 以磁盘文件名校正测试项名称:文件被外部改名后内容中的 Name 仍是旧名, /// 统一改为文件名(不含扩展名),保证“名称=文件名”不变式,载入/保存/删除都按同一名称寻址。 /// private static ListPreset NormalizePresetName(ListPreset item, string file) { string stem = Path.GetFileNameWithoutExtension(file); return string.Equals(item.Name, stem, StringComparison.Ordinal) ? item : new ListPreset(stem, item.Note, item.Count, item.Phases, item.Trigger, item.TimeBase, item.PhaseContinue); } /// 按名称拼出测试项文件完整路径(写入、另存、删除共用同一规则)。 private static string PresetPathOf(string name) => Path.Combine(PresetStorageDir, SanitizeFileName(name) + PresetFileExt); /// 把一个测试项写入 D:\acp\源测试\{清洗后名称}.json(缩进格式化,便于人工查看)。 private static void WritePresetFile(ListPreset preset) => WritePresetFile(preset, PresetPathOf(preset.Name)); /// 把测试项写入指定文件(覆盖写入)。 private static void WritePresetFile(ListPreset preset, string path) { string json = JsonConvert.SerializeObject(preset, Formatting.Indented); File.WriteAllText(path, json); } /// 用当前界面参数 + 三相配置快照构建测试项对象(内容名称始终与文件名一致)。 private ListPreset SnapshotPreset(string name) => new( name, PresetNote ?? "", ListCount, new[] { _phaseConfigs[0].Clone(), _phaseConfigs[1].Clone(), _phaseConfigs[2].Clone() }, ListTrigger, ListTimeBase, ListPhaseContinue); /// /// 保存:覆盖写入“当前打开的文件”,不改变文件名(名称框内容不参与)。 /// 尚未打开任何文件时,退化为“另存为”,用名称框内容新建文件。 /// private void SaveCurrentPreset() { if (string.IsNullOrEmpty(_currentPresetPath)) { SaveCurrentPresetAs(); return; } // 内容名称以当前文件名为准,保证“名称=文件名”不变式 string name = Path.GetFileNameWithoutExtension(_currentPresetPath); string typed = (PresetName ?? "").Trim(); if (!string.IsNullOrEmpty(typed) && !string.Equals(typed, name, StringComparison.Ordinal)) Log($"名称框内容 [{typed}] 未用于保存:保存只覆盖当前文件,如需换名请用“另存为”"); try { Directory.CreateDirectory(PresetStorageDir); WritePresetFile(SnapshotPreset(name), _currentPresetPath); RefreshPresets(); SelectedPresetIndex = _presets.FindIndex(p => p.Name == name); PresetName = name; // 名称框回显当前文件名 Log($"测试项已保存(覆盖):{_currentPresetPath}"); } catch (Exception ex) { Log($"保存测试项失败:{ex.Message}"); } } /// /// 另存为:弹出文件对话框选择 .json 保存位置,成功后“当前打开文件”切换到新文件。 /// 存入预设目录(D:\acp\源测试)内会自动出现在下拉列表;存到目录外则仅本次会话可用“保存”覆盖, /// 对话框自身的“是否替换”提示已覆盖同名覆盖确认。 /// private void SaveCurrentPresetAs() { var dlg = new SaveFileDialog { Title = "测试项另存为", Filter = "JSON 文件 (*.json)|*.json", DefaultExt = ".json", AddExtension = true, InitialDirectory = PresetStorageDir, FileName = SanitizeFileName((PresetName ?? "").Trim()), }; if (dlg.ShowDialog() != true) return; // 用户取消 try { string path = dlg.FileName; string name = Path.GetFileNameWithoutExtension(path); // 内容名称始终与文件名一致 WritePresetFile(SnapshotPreset(name), path); _currentPresetPath = path; // 另存为成功后,后续“保存”直接修改这个新文件 RefreshPresets(); SelectedPresetIndex = _presets.FindIndex(p => p.Name == name); PresetName = name; Log(Path.GetDirectoryName(path) is string dir && string.Equals(dir, PresetStorageDir, StringComparison.OrdinalIgnoreCase) ? $"测试项已另存为:{path}" : $"测试项已另存为:{path}(在预设目录之外,不会出现在下拉列表,“保存”仍可覆盖此文件)"); } catch (Exception ex) { Log($"另存测试项失败:{ex.Message}"); } } /// /// 删除当前选中的测试项文件(带二次确认),删除后刷新列表。 /// 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); if (string.Equals(path, _currentPresetPath, StringComparison.OrdinalIgnoreCase)) _currentPresetPath = null; // 当前打开的文件已被删除,解除绑定 SelectedPresetIndex = -1; RefreshPresets(); Log($"已删除测试项:{path}"); } catch (Exception ex) { Log($"删除测试项失败:{ex.Message}"); } } /// 清洗文件名中的非法字符(Windows),空白名回退为“未命名”。 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 SYNTHESIS 波形文件读写(D:\acp\源测试\合成波形) /// /// 确保合成波形目录存在(波形 .json 一律由用户自行编写或界面保存,不再自动生成内置示例)。 /// private void EnsureSynthStorage() { try { if (!Directory.Exists(SynthStorageDir)) Directory.CreateDirectory(SynthStorageDir); } catch (Exception ex) { Log($"初始化合成波形目录失败:{ex.Message}"); } } /// 从磁盘重新加载合成波形预设列表,刷新下拉框并按名称恢复选中项。 private void RefreshSynthPresets() { // 刷新前记住选中项名称:列表重建后索引会漂移,按名称恢复最可靠 string? selectedName = (_selectedSynthPresetIndex >= 0 && _selectedSynthPresetIndex < _synthPresets.Count) ? _synthPresets[_selectedSynthPresetIndex].Name : null; var list = new List(); try { if (Directory.Exists(SynthStorageDir)) { foreach (var file in Directory.GetFiles(SynthStorageDir, "*" + PresetFileExt).OrderBy(f => f)) { try { var json = File.ReadAllText(file); var item = JsonConvert.DeserializeObject(json); if (item != null) list.Add(NormalizeSynthPresetName(item, file)); } catch (Exception ex) { Log($"波形文件解析失败 [{Path.GetFileName(file)}]:{ex.Message}"); } } } } catch (Exception ex) { Log($"读取波形目录失败:{ex.Message}"); } _synthPresets = list; _synthPresetOptions = list.Select(p => p.Name).ToList(); RaisePropertyChanged(nameof(SynthPresetOptions)); RaisePropertyChanged(nameof(SynthStorageHint)); SelectedSynthPresetIndex = selectedName != null ? _synthPresets.FindIndex(p => p.Name == selectedName) : -1; } /// 以磁盘文件名校正波形名称(同 的“名称=文件名”不变式)。 private static SynthPreset NormalizeSynthPresetName(SynthPreset item, string file) { string stem = Path.GetFileNameWithoutExtension(file); return string.Equals(item.Name, stem, StringComparison.Ordinal) ? item : new SynthPreset(stem, item.Note, item.Compose, item.Frequency, item.Fundamental, item.Dc, item.StartPhase, item.AmplitudeText, item.PhaseText); } /// 按名称拼出合成波形文件完整路径(写入、另存、删除共用同一规则)。 private static string SynthPathOf(string name) => Path.Combine(SynthStorageDir, SanitizeFileName(name) + PresetFileExt); /// 把一个合成波形预设写入 {SynthStorageDir}\{清洗后名称}.json(缩进格式化)。 private static void WriteSynthPresetFile(SynthPreset preset) => WriteSynthPresetFile(preset, SynthPathOf(preset.Name)); /// 把合成波形预设写入指定文件(覆盖写入)。 private static void WriteSynthPresetFile(SynthPreset preset, string path) { string json = JsonConvert.SerializeObject(preset, Formatting.Indented); File.WriteAllText(path, json); } /// 用当前界面参数快照构建波形预设对象(内容名称始终与文件名一致)。 private SynthPreset SnapshotSynthPreset(string name) => new( name, SynthPresetNote ?? "", SynthCompose, SynthFrequency, SynthFundamental, SynthDc, SynthStartPhase, SynthAmplitudeText, SynthPhaseText); /// 载入选中波形:把文件参数一次性回填到界面各 SYNTHESIS 输入框。 private void LoadCurrentSynthPreset() { if (SelectedSynthPresetIndex < 0 || SelectedSynthPresetIndex >= _synthPresets.Count) { Log("请先在下拉框选择一项波形"); return; } var p = _synthPresets[SelectedSynthPresetIndex]; SynthCompose = p.Compose; SynthFrequency = p.Frequency; SynthFundamental = p.Fundamental; SynthDc = p.Dc; SynthStartPhase = p.StartPhase; SynthAmplitudeText = p.AmplitudeText; SynthPhaseText = p.PhaseText; SynthPresetName = p.Name; SynthPresetNote = p.Note; _currentSynthPath = SynthPathOf(p.Name); // 记住当前打开的文件,此后“保存”只覆盖它 Log($"已载入波形:{p.Name}(格式={p.Compose} 基频={p.Frequency}Hz 基波={p.Fundamental}V)"); } /// /// 保存:覆盖写入“当前打开的文件”,不改变文件名(名称框内容不参与)。 /// 尚未打开任何文件时,退化为“另存为”,用名称框内容新建文件。 /// private void SaveCurrentSynthPreset() { if (string.IsNullOrEmpty(_currentSynthPath)) { SaveCurrentSynthPresetAs(); return; } // 内容名称以当前文件名为准,保证“名称=文件名”不变式 string name = Path.GetFileNameWithoutExtension(_currentSynthPath); string typed = (SynthPresetName ?? "").Trim(); if (!string.IsNullOrEmpty(typed) && !string.Equals(typed, name, StringComparison.Ordinal)) Log($"名称框内容 [{typed}] 未用于保存:保存只覆盖当前文件,如需换名请用“另存为”"); try { Directory.CreateDirectory(SynthStorageDir); WriteSynthPresetFile(SnapshotSynthPreset(name), _currentSynthPath); RefreshSynthPresets(); SelectedSynthPresetIndex = _synthPresets.FindIndex(p => p.Name == name); SynthPresetName = name; // 名称框回显当前文件名 Log($"波形已保存(覆盖):{_currentSynthPath}"); } catch (Exception ex) { Log($"保存波形失败:{ex.Message}"); } } /// /// 另存为:弹出文件对话框选择 .json 保存位置,成功后“当前打开文件”切换到新文件。 /// 存入预设目录(D:\acp\源测试\合成波形)内会自动出现在下拉列表;存到目录外则仅本次会话可用“保存”覆盖, /// 对话框自身的“是否替换”提示已覆盖同名覆盖确认。 /// private void SaveCurrentSynthPresetAs() { var dlg = new SaveFileDialog { Title = "波形另存为", Filter = "JSON 文件 (*.json)|*.json", DefaultExt = ".json", AddExtension = true, InitialDirectory = SynthStorageDir, FileName = SanitizeFileName((SynthPresetName ?? "").Trim()), }; if (dlg.ShowDialog() != true) return; // 用户取消 try { string path = dlg.FileName; string name = Path.GetFileNameWithoutExtension(path); // 内容名称始终与文件名一致 WriteSynthPresetFile(SnapshotSynthPreset(name), path); _currentSynthPath = path; // 另存为成功后,后续“保存”直接修改这个新文件 RefreshSynthPresets(); SelectedSynthPresetIndex = _synthPresets.FindIndex(p => p.Name == name); SynthPresetName = name; Log(Path.GetDirectoryName(path) is string dir && string.Equals(dir, SynthStorageDir, StringComparison.OrdinalIgnoreCase) ? $"波形已另存为:{path}" : $"波形已另存为:{path}(在预设目录之外,不会出现在下拉列表,“保存”仍可覆盖此文件)"); } catch (Exception ex) { Log($"另存波形失败:{ex.Message}"); } } /// 删除当前选中的波形文件(带二次确认),删除后刷新列表。 private void DeleteCurrentSynthPreset() { if (SelectedSynthPresetIndex < 0 || SelectedSynthPresetIndex >= _synthPresets.Count) { Log("请先在下拉框选择要删除的波形"); return; } var p = _synthPresets[SelectedSynthPresetIndex]; string path = Path.Combine(SynthStorageDir, 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); if (string.Equals(path, _currentSynthPath, StringComparison.OrdinalIgnoreCase)) _currentSynthPath = null; // 当前打开的文件已被删除,解除绑定 SelectedSynthPresetIndex = -1; RefreshSynthPresets(); Log($"已删除波形:{path}"); } catch (Exception ex) { Log($"删除波形失败:{ex.Message}"); } } #endregion #region 示波器抓图(TektronixMSO)与存储位置持久化 /// /// 示波器命令执行包装:与 Exec 同一防重入/异常日志机制,仅把设备校验对象换成示波器实例。 /// private async Task ExecScope(Func action) { if (_scope == null) { Log("错误:未找到 TektronixMSO 示波器实例,请先在设置界面添加并启用该设备"); return; } if (IsBusy) return; IsBusy = true; try { await action(); IsScopeConnected = _scope.IsConnected; } catch (OperationCanceledException) { Log("示波器命令超时或已取消"); } catch (Exception ex) { Log($"示波器错误:{ex.Message}"); } finally { IsBusy = false; } } /// /// 把指定的 PNG 加载为 BitmapImage 并刷新界面显示。 /// BitmapCacheOption.OnLoad 读完即释放文件句柄,不占用图片文件;Freeze 后跨线程安全。 /// /// 加载成功返回 true;文件缺失/损坏返回 false(提示由调用方给)。 private bool LoadScopeImage(string file) { try { var bmp = new BitmapImage(); bmp.BeginInit(); bmp.CacheOption = BitmapCacheOption.OnLoad; bmp.UriSource = new Uri(file, UriKind.Absolute); bmp.EndInit(); bmp.Freeze(); ScopeImage = bmp; HasScopeImage = true; return true; } catch (Exception ex) { Log($"加载截图失败:{ex.Message}"); return false; } } /// 清空图片区(没有对应波形截图时不显示任何图片)。 private void ClearScopeImage() { ScopeImage = null; HasScopeImage = false; } /// /// 按“当前模式已载入配置文件的文件名(去扩展名)+ .png”在波形图片存储位置查找截图并显示。 /// LIST 模式取当前打开的测试项文件,SYNTHESIS 模式取当前打开的波形文件; /// 未载入配置或图片不存在时清空图片区,不显示任何图片。 /// private void ShowWaveImage(string? configPath, string mode) { if (string.IsNullOrWhiteSpace(ScopeImageDir)) { ClearScopeImage(); ScopeImageHint = "请先设置波形图片存储位置"; Log("请先设置波形图片存储位置"); return; } if (string.IsNullOrWhiteSpace(configPath)) { ClearScopeImage(); ScopeImageHint = $"请先载入 {mode} 配置文件,再查看对应波形图片"; Log($"请先载入 {mode} 配置文件"); return; } string stem = Path.GetFileNameWithoutExtension(configPath); // 配置文件名去掉 .json 后缀 string file = Path.Combine(ScopeImageDir, stem + ".png"); if (!File.Exists(file)) { ClearScopeImage(); ScopeImageHint = $"未找到波形图片:{stem}.png"; Log($"[{mode}] 未找到波形图片:{file}"); return; } if (LoadScopeImage(file)) { ScopeImageHint = $"{mode} 波形图片:{stem}.png"; Log($"[{mode}] 已显示波形图片:{file}"); } else { ClearScopeImage(); ScopeImageHint = $"波形图片加载失败:{stem}.png"; } } /// 示波器截图目录配置文件路径(%AppData%\ACP\ScopeCaptureDir.json)。 private static string ScopeDirConfigPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ACP", "ScopeCaptureDir.json"); /// 启动时恢复上次设置的波形图片存储位置(配置缺失/损坏时保持默认目录)。 private void LoadScopeImageDir() { try { if (File.Exists(ScopeDirConfigPath)) { var dir = JsonConvert.DeserializeObject(File.ReadAllText(ScopeDirConfigPath)); if (!string.IsNullOrWhiteSpace(dir)) ScopeImageDir = dir; } } catch { /* 配置缺失或损坏时静默使用默认目录 */ } } /// 把当前波形图片存储位置写入配置(抓图或浏览选择后调用)。 private void SaveScopeImageDir() { try { Directory.CreateDirectory(Path.GetDirectoryName(ScopeDirConfigPath)!); File.WriteAllText(ScopeDirConfigPath, JsonConvert.SerializeObject(ScopeImageDir)); } catch (Exception ex) { Log($"保存截图目录设置失败:{ex.Message}"); } } #endregion #region 辅助 /// 创建 10 秒超时的取消令牌(命令用)。 private CancellationToken Ct() => (_cmdCts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token; /// 当前选中相位的显示标签 private string PhaseLabel => SelectedPhaseIndex switch { 1 => "A相", 2 => "B相", 3 => "C相", _ => "ALL" }; /// /// 根据 SelectedPhaseIndex 发送 INST:COUP / INST:NSEL 命令切换相位上下文。 /// 索引 0 → INST:COUP ALL (三相同时);索引 1~3 → INST:COUP NONE + INST:NSEL n /// private async Task ApplyPhaseAsync() { if (SelectedPhaseIndex == 0) { await _dev!.设置相位耦合Async("ALL", Ct()); } else { await _dev!.设置相位耦合Async("NONE", Ct()); await _dev!.选择相位Async(SelectedPhaseIndex, Ct()); } } /// 统一的设备命令执行包装:防重入、异常捕获与日志。 private async Task Exec(Func 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; } } /// 线程安全的日志输出(自动切回 UI 线程,保留最近内容)。 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; } /// 解析逗号/空格/分号分隔的数值序列。 private static double[] ParseDoubles(string text) { if (string.IsNullOrWhiteSpace(text)) return Array.Empty(); 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(); } /// 解析波形顺序序列(形状名或 A/B)。 private static string[] ParseShapes(string text) { if (string.IsNullOrWhiteSpace(text)) return Array.Empty(); return text.Split(new[] { ',', ',', ' ', '\t', ';' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim().ToUpperInvariant()) .ToArray(); } /// /// 下发单个相位的 LIST 序列(调用前须已通过 INST:NSEL 选定目标相、并已 INST:EDIT EACH)。 /// 依次下发 DEGR / VOLT:AC / VOLT:DC / FREQ / DWEL / SHAP 各序列,留空的序列跳过; /// LIST:SHAP 各点可直接写形状名 SINE/SQUA/TRIAN/CSIN/DST01~30/USR01~30,也可引用仪器 A/B 波形组。 /// private async Task 下发单相序列Async(ListPhaseConfig cfg) { 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 } /// /// 轮询 TRIG:STATE? 等待当前 LIST 执行结束(返回 OFF),避免在执行过程中改写参数导致仪器报错。 /// /// 最长等待时间(ms),超时后仍继续下发并给出提示 private async Task 等待List空闲Async(int timeoutMs = 8000) { if (_dev == null) return; // SYNTH / 无限 LIST 属连续执行模态,不会自行回到 OFF;必须先强制 TRIG OFF 停止, // 否则执行中改写参数会被仪器忽略(输出沿用本地已存波形)。 try { await _dev.触发列表执行Async(false, Ct()); } catch { /* 停止失败不阻塞,继续靠轮询兜底 */ } 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("等待空闲超时(可能仍在执行),将继续下发,请留意仪器状态"); } /// /// 将相位角序列整体偏移指定角度(结果对 360 取模,保持 0~359.9),用于生成对称三相的 DEGR。 /// 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); })); } /// /// 构建三相对称的 LIST 相位配置:以给定参数为 A 相,B/C 相复制后 DEGR 自动 +120°/+240°。 /// 适用于三相平衡试验(电压/频率范围、骤变、瞬时跌落等)。DWEL 单位 ms。 /// private static ListPhaseConfig[] Sym(string degr, string vacS, string vacE, string vdcS, string vdcE, string fS, string fE, string dwell, string shape = "") { var a = new ListPhaseConfig { 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 }; } /// /// 构建大纲典型试验预设集合(仅含 LIST 模式可实现的项)。 /// 名称含大纲章节号;对称试验用 生成三相,不对称试验逐相显式定义。 /// private static IReadOnlyList BuildPresets() => new List { // ===== 5.1 交流电压范围(三相平衡渐变,2 循环,DWEL 单位 ms)===== new ListPreset("5.1 上限电压范围 220→265V", "三相平衡:额定 220V 线性升至上限 265V 再回落,各段 90s,2 循环。判定 A。", 2, Sym("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("0,0", "220,85", "85,220", "", "", "50,50", "50,50", "90000,90000")), new ListPreset("5.1 上限保护电压 220→275V", "验证过压保护点与恢复点:220V 升至 275V 再回落,2 循环。判定 C。", 2, Sym("0,0", "220,275", "275,220", "", "", "50,50", "50,50", "90000,90000")), new ListPreset("5.1 下限保护电压 220→80V", "验证欠压保护点与恢复点:220V 降至 80V 再回升,2 循环。判定 C。", 2, Sym("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("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("0,0", "220,220", "220,220", "", "", "50,40", "40,50", "60000,60000")), new ListPreset("5.3 上限保护频率 50→75Hz", "验证过频保护点与恢复点:50Hz 升至 75Hz 再回落,2 循环。判定 C。", 2, Sym("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("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("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("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("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("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("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 { 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 { 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 { 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 量程(-495.0~495.0V),须确认设备为高压选配档,否则命令会被拒绝或截顶。", 2, Sym("0,0,0,0", "", "", "680,860,450,680", "680,860,450,680", "", "", "5000,5000,5000,5000")), }; #endregion public void Dispose() { try { _cmdCts?.Cancel(); } catch { } _cmdCts?.Dispose(); } } /// /// 单个相位的 LIST 序列配置(对应界面一组序列文本框)。 /// 三相独立下发(INST:EDIT EACH + INST:NSEL)时,每相持有一份本配置, /// 各字段均为逗号分隔的原始文本,下发时再解析为数值序列。 /// public class ListPhaseConfig { /// 相位角度序列 (LIST:DEGR, °) public string Degr { get; set; } = ""; /// AC 起始电压序列 (LIST:VOLT:AC:STAR, V) public string VoltAcStart { get; set; } = ""; /// AC 结束电压序列 (LIST:VOLT:AC:END, V) public string VoltAcEnd { get; set; } = ""; /// DC 起始电压序列 (LIST:VOLT:DC:STAR, V) public string VoltDcStart { get; set; } = ""; /// DC 结束电压序列 (LIST:VOLT:DC:END, V) public string VoltDcEnd { get; set; } = ""; /// 起始频率序列 (LIST:FREQ:STAR, Hz) public string FreqStart { get; set; } = ""; /// 结束频率序列 (LIST:FREQ:END, Hz) public string FreqEnd { get; set; } = ""; /// 静止时间序列 (LIST:DWEL, ms) public string Dwell { get; set; } = ""; /// 波形顺序 (LIST:SHAP,可直接写形状名或引用 A/B 组) public string ShapeSeq { get; set; } = ""; /// 浅拷贝一份配置(用于"复制到三相")。 public ListPhaseConfig Clone() => (ListPhaseConfig)MemberwiseClone(); } /// /// 大纲典型试验预设:一次载入即可填充三相 LIST 序列与基础参数(次数/触发/时基/相位连续)。 /// public class ListPreset { /// 预设名称(含大纲章节号,供下拉显示) public string Name { get; } /// 试验说明与判定等级提示 public string Note { get; } /// 列表执行次数 (LIST:COUN,0 = 无限循环) public int Count { get; } /// 触发形态 (LIST:TRIG) public string Trigger { get; } /// 时间基础 (LIST:BASE) public string TimeBase { get; } /// 相位连续功能 (LIST:PHAS:CONT);旧文件无此键时为 null,载入时保持界面当前值 public string? PhaseContinue { get; } /// 三相序列配置(索引 0=A/Φ1, 1=B/Φ2, 2=C/Φ3) public ListPhaseConfig[] Phases { get; } [JsonConstructor] public ListPreset(string name, string note, int count, ListPhaseConfig[] phases, string trigger = "AUTO", string timeBase = "TIME", string? phaseContinue = null) { Name = name; Note = note; Count = count; Phases = phases; Trigger = trigger; TimeBase = timeBase; PhaseContinue = phaseContinue; } } /// /// SYNTHESIS 自定义谐波波形预设:一次载入即可填充合成波形全部参数 /// (数据格式/基频/基波电压/直流/起始相位/谐波振幅与相位序列)。 /// public class SynthPreset { /// 预设名称(保存时作为文件名,供下拉显示) public string Name { get; } /// 波形说明 public string Note { get; } /// 谐波阶数据格式 (SYNT:COMP) public string Compose { get; } /// 基频 (SYNT:FREQ, 50 | 60) public int Frequency { get; } /// 基波交流电压 (SYNT:FUND, V) public double Fundamental { get; } /// 叠加直流电压 (SYNT:DC, V) public double Dc { get; } /// 起始相位角 (SYNT:SPH, °) public double StartPhase { get; } /// 各次谐波振幅序列文本 (SYNT:AMPL, 逗号分隔, 首值=2次谐波) public string AmplitudeText { get; } /// 各次谐波相位角序列文本 (SYNT:PHAS, 逗号分隔, 首值=2次谐波) public string PhaseText { get; } [JsonConstructor] public SynthPreset(string name, string note, string compose, int frequency, double fundamental, double dc, double startPhase, string amplitudeText, string phaseText) { Name = name; Note = note; Compose = compose; Frequency = frequency; Fundamental = fundamental; Dc = dc; StartPhase = startPhase; AmplitudeText = amplitudeText; PhaseText = phaseText; } } }