From 8dd08fcc7a6183be37c0dabf603577b4412f959c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chsc=E2=80=9D?= <“huangsucan@kaiyili-lab.com”> Date: Fri, 9 Oct 2026 09:23:32 +0800 Subject: [PATCH] =?UTF-8?q?chroma=20synthesis=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DeviceCommand/Devices/Chroma61800.cs | 161 ++++- .../ViewModels/PowerAdaptabilityViewModel.cs | 597 ++++++++++-------- .../Views/PowerAdaptabilityView.xaml | 178 +++--- 3 files changed, 572 insertions(+), 364 deletions(-) diff --git a/DeviceCommand/Devices/Chroma61800.cs b/DeviceCommand/Devices/Chroma61800.cs index 4523b73..f3a24fb 100644 --- a/DeviceCommand/Devices/Chroma61800.cs +++ b/DeviceCommand/Devices/Chroma61800.cs @@ -97,6 +97,25 @@ namespace DeviceCommand.Devices await SendAsync($"*D {valStr}{ScpiDelimiter}", ct); } + /// + /// 退出远程状态、回到本地 (SYST:LOC,即 SYSTem:LOCal) + /// 设定后仪器回到 LOCAL 状态,前面板恢复可用。手册注明本命令僅可用於 RS-232 控制之下, + /// 经 Ethernet/USB 发送时仪器可能不响应 + /// + public virtual async Task 退出远程Async(CancellationToken ct = default) + { + await SendAsync($"SYST:LOC{ScpiDelimiter}", ct); + } + + /// + /// 进入远程状态 (SYST:REM,即 SYSTem:REMote) + /// 设定后除 LOCAL/REMOTE 键外前面板其他键皆无效。手册注明本命令僅可用於 RS-232 控制之下 + /// + public virtual async Task 进入远程Async(CancellationToken ct = default) + { + await SendAsync($"SYST:REM{ScpiDelimiter}", ct); + } + #endregion #region 2. 基础输出与耦合控制 (OUTPut 子系统) @@ -509,7 +528,8 @@ namespace DeviceCommand.Devices /// /// 开启/关闭谐波量测 (SENSe:HARMonic) - /// 每次新的 Fetch/Measure 前必须先 ON,约需 3 秒得到结果; + /// 每次新的 Fetch + /// ./Measure 前必须先 ON,约需 3 秒得到结果; /// 若要量测其他数据,须先设回 OFF。分相量测配合 INST:COUP NONE + INST:NSEL 逐相进行 /// /// true: 开启谐波量测(ON); false: 关闭(OFF) @@ -556,5 +576,144 @@ namespace DeviceCommand.Devices #endregion + #region 8. SYNTHESIS 波形合成子系统 (SYNThesis 子系统 · 自定义谐波波形输出) + + /// + /// 设定每个谐波阶的数据格式 (SOUR:SYNT:COMP) + /// VALUE: 绝对值; PERCENT: 基频电压的百分率。后缀 1/2/3 对应三段可编程波形 + /// + /// 数据格式: VALUE1 | VALUE2 | VALUE3 | PERCENT1 | PERCENT2 | PERCENT3 + public virtual async Task 设置合成数据格式Async(string compose, CancellationToken ct = default) + { + await SendAsync($"SOUR:SYNT:COMP {compose}{ScpiDelimiter}", ct); + } + + /// + /// 设定每个谐波阶的振幅 (SOUR:SYNT:AMPL) + /// 最大位阶数为 50;各阶取值上限随阶次递减(2~10 阶 0~90.0,11~20 阶 0~60.0, + /// 21~30 阶 0~30.0,31~40 阶 0~30.0,41~50 阶 0~15.0;PERCENT 格式对应减半) + /// + /// 各次谐波振幅序列(按阶次升序,逗号下发) + public virtual async Task 设置合成谐波振幅Async(double[] amplitudes, CancellationToken ct = default) + { + string valStr = string.Join(",", Array.ConvertAll(amplitudes, v => v.ToString("0.###", CultureInfo.InvariantCulture))); + await SendAsync($"SOUR:SYNT:AMPL {valStr}{ScpiDelimiter}", ct); + } + + /// + /// 设定每个谐波阶的相位角 (SOUR:SYNT:PHAS) + /// + /// 各次谐波相位角序列(单位: °,每个值 0.0 ~ 359.9) + public virtual async Task 设置合成谐波相位Async(double[] phases, CancellationToken ct = default) + { + string valStr = string.Join(",", Array.ConvertAll(phases, v => v.ToString("0.#", CultureInfo.InvariantCulture))); + await SendAsync($"SOUR:SYNT:PHAS {valStr}{ScpiDelimiter}", ct); + } + + /// + /// 设定 SYNTHESIS 模式的基波交流电压 (SOUR:SYNT:FUND) + /// + /// 基波交流电压 (单位: V,0.0 ~ 300.0) + public virtual async Task 设置合成基波电压Async(double voltage, CancellationToken ct = default) + { + string valStr = voltage.ToString("0.###", CultureInfo.InvariantCulture); + await SendAsync($"SOUR:SYNT:FUND {valStr}{ScpiDelimiter}", ct); + } + + /// + /// 设定叠加于 SYNTHESIS 波形的直流电压 (SOUR:SYNT:DC) + /// + /// 直流电压 (单位: V,-424.2 ~ 424.2) + public virtual async Task 设置合成直流电压Async(double voltage, CancellationToken ct = default) + { + string valStr = voltage.ToString("0.###", CultureInfo.InvariantCulture); + await SendAsync($"SOUR:SYNT:DC {valStr}{ScpiDelimiter}", ct); + } + + /// + /// 设定 SYNTHESIS 模式的基频 (SOUR:SYNT:FREQ) + /// + /// 基频: 50 或 60 (Hz) + public virtual async Task 设置合成基频Async(int frequency, CancellationToken ct = default) + { + await SendAsync($"SOUR:SYNT:FREQ {frequency}{ScpiDelimiter}", ct); + } + + /// + /// 设定 SYNTHESIS 模式的起始相位角 (SOUR:SYNT:SPH) + /// + /// 起始相位角 (单位: °,0.0 ~ 359.9) + public virtual async Task 设置合成起始相位Async(double degree, CancellationToken ct = default) + { + string valStr = degree.ToString("0.#", CultureInfo.InvariantCulture); + await SendAsync($"SOUR:SYNT:SPH {valStr}{ScpiDelimiter}", ct); + } + + /// + /// 查询当前合成数据格式 (SOUR:SYNT:COMP?),用于下发后回读校验 + /// + /// VALUE1|2|3 / PERCENT1|2|3 + public virtual async Task 查询合成数据格式Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"SOUR:SYNT:COMP?{ScpiDelimiter}", ScpiDelimiter, ct); + return resp?.Trim() ?? string.Empty; + } + + /// + /// 查询 SYNTHESIS 基波交流电压 (SOUR:SYNT:FUND?),用于下发后回读校验 + /// + public virtual async Task 查询合成基波电压Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"SOUR:SYNT:FUND?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); + } + + /// + /// 查询 SYNTHESIS 基频 (SOUR:SYNT:FREQ?),返回 50 或 60 + /// + public virtual async Task 查询合成基频Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"SOUR:SYNT:FREQ?{ScpiDelimiter}", ScpiDelimiter, ct); + return (int)ExtractDouble(resp); + } + + /// + /// 查询 SYNTHESIS 叠加直流电压 (SOUR:SYNT:DC?) + /// + public virtual async Task 查询合成直流电压Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"SOUR:SYNT:DC?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); + } + + /// + /// 查询 SYNTHESIS 起始相位角 (SOUR:SYNT:SPH?) + /// + public virtual async Task 查询合成起始相位Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"SOUR:SYNT:SPH?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); + } + + /// + /// 查询各谐波阶振幅数组 (SOUR:SYNT:AMPL?),首值对应 2 次谐波 + /// + public virtual async Task 查询合成谐波振幅Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"SOUR:SYNT:AMPL?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDoubleArray(resp); + } + + /// + /// 查询各谐波阶相位角数组 (SOUR:SYNT:PHAS?),首值对应 2 次谐波 + /// + public virtual async Task 查询合成谐波相位Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"SOUR:SYNT:PHAS?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDoubleArray(resp); + } + + #endregion + } } \ No newline at end of file diff --git a/PowerAdaptabilityModule/ViewModels/PowerAdaptabilityViewModel.cs b/PowerAdaptabilityModule/ViewModels/PowerAdaptabilityViewModel.cs index b18de6c..761c20e 100644 --- a/PowerAdaptabilityModule/ViewModels/PowerAdaptabilityViewModel.cs +++ b/PowerAdaptabilityModule/ViewModels/PowerAdaptabilityViewModel.cs @@ -1,9 +1,5 @@ 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; @@ -22,34 +18,15 @@ namespace PowerAdaptabilityModule.ViewModels { /// /// 电源适应性测试 ViewModel(Chroma61800 交流源一拖三): - /// 左侧为设备命令面板(三相输出设置 + LIST 模式),右侧为 A/B/C 三相实时波形图表。 + /// 设备命令面板(三相输出设置 + LIST 模式 + SYNTHESIS 自定义波形输出)。 /// public class PowerAdaptabilityViewModel : NavigateViewModelBase, IDisposable { - #region 常量 - /// 采样周期(毫秒) - private const int SampleIntervalMs = 1000; - - /// 三相曲线配色:A 红、B 绿、C 蓝(三张图统一,便于对照) - 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; - - /// 采样计时器,记录从开始采集到当前的经过秒数(X 轴) - private System.Diagnostics.Stopwatch? _sampleStopwatch; - - // 三相电压时间序列曲线(A/B/C 三相) - private readonly LineSeries _voltA, _voltB, _voltC; - - // 输出频率时间序列曲线(单条,ALL 模式读一个总频率) - private readonly LineSeries _freq; #endregion #region 设备状态属性 @@ -61,36 +38,6 @@ namespace PowerAdaptabilityModule.ViewModels private bool _isBusy; public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); } - - private bool _isSampling; - /// 是否正在采集波形数据 - public bool IsSampling { get => _isSampling; set => SetProperty(ref _isSampling, value); } - - private bool _isFrequencySamplingEnabled; - /// 是否采集频率(默认关闭:频率一般无需持续采集,开启后采样循环才读取并绘制频率曲线) - public bool IsFrequencySamplingEnabled { get => _isFrequencySamplingEnabled; set => SetProperty(ref _isFrequencySamplingEnabled, value); } - #endregion - - #region 图表(三相电压时间序列 + 输出频率时间序列) - /// 三相电压波形图表 - public PlotModel VoltagePlot { get; } - - /// 输出频率时间序列图表(受“采集频率”开关控制,默认不采集) - 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; - /// 当前输出频率 (Hz) - public double FrequencyNow { get => _frequencyNow; set => SetProperty(ref _frequencyNow, value); } - - private double _powerNow; - /// 当前真实输出功率 (W) - public double PowerNow { get => _powerNow; set => SetProperty(ref _powerNow, value); } #endregion #region 输入参数(三相输出设置) @@ -204,6 +151,87 @@ namespace PowerAdaptabilityModule.ViewModels public IReadOnlyList ListTimeBaseOptions { get; } = new[] { "TIME", "CYCLE" }; #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 ~ 300.0 V) + public double SynthFundamental { get => _synthFundamental; set => SetProperty(ref _synthFundamental, value); } + + private double _synthDc; + /// 叠加直流电压 (SYNT:DC, -424.2 ~ 424.2 V) + public double SynthDc { get => _synthDc; set => SetProperty(ref _synthDc, value); } + + private double _synthStartPhase; + /// 起始相位角 (SYNT:SPH, 0.0 ~ 359.9 °) + public double SynthStartPhase { get => _synthStartPhase; set => SetProperty(ref _synthStartPhase, value); } + + private string _synthAmplitudeText = ""; + /// 各次谐波振幅序列文本 (SYNT:AMPL, 英文逗号分隔, 最多 50 阶) + public string SynthAmplitudeText { get => _synthAmplitudeText; set => SetProperty(ref _synthAmplitudeText, value); } + + private string _synthPhaseText = ""; + /// 各次谐波相位角序列文本 (SYNT:PHAS, 英文逗号分隔, 0~359.9°) + public string SynthPhaseText { get => _synthPhaseText; set => SetProperty(ref _synthPhaseText, value); } + #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); } + + 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\源测试"; @@ -261,6 +289,7 @@ namespace PowerAdaptabilityModule.ViewModels #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; } @@ -280,9 +309,11 @@ namespace PowerAdaptabilityModule.ViewModels public ICommand CopyPhaseToThreeCommand { get; } public ICommand DownloadThreePhaseListCommand { get; } public ICommand ListStopCommand { get; } - public ICommand StartSamplingCommand { get; } - public ICommand StopSamplingCommand { get; } - public ICommand ClearChartsCommand { get; } + public ICommand DownloadSynthesisCommand { get; } + public ICommand SynthStopCommand { get; } + public ICommand LoadSynthPresetCommand { get; } + public ICommand SaveSynthPresetCommand { get; } + public ICommand DeleteSynthPresetCommand { get; } #endregion public PowerAdaptabilityViewModel(IContainerProvider containerProvider) : base(containerProvider) @@ -290,19 +321,14 @@ namespace PowerAdaptabilityModule.ViewModels _dm = containerProvider.Resolve(); _globalInfo = containerProvider.Resolve(); - // 初始化三相电压时间序列图表(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(); + // 初始化合成波形文件目录(首次运行迁移内置示例),再从磁盘加载波形 + EnsureSynthStorage(); + RefreshSynthPresets(); + #region 命令初始化 QueryIdnCommand = new DelegateCommand(async () => await Exec(async () => Log("IDN: " + await _dev!.查询设备信息Async(Ct())))); @@ -310,6 +336,9 @@ namespace PowerAdaptabilityModule.ViewModels 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("输出已开启"); })); @@ -436,9 +465,47 @@ namespace PowerAdaptabilityModule.ViewModels 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); + // 下发 SYNTHESIS 自定义谐波波形并启动:手册要求进入 SYNTH 前先离开输出,参数下发后 OUTP:MODE SYNTH → TRIG ON + 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(SynthCompose, Ct()); // SYNT:COMP + await _dev.设置合成基频Async(SynthFrequency, Ct()); // SYNT:FREQ + await _dev.设置合成基波电压Async(SynthFundamental, Ct()); // SYNT:FUND + await _dev.设置合成直流电压Async(SynthDc, Ct()); // SYNT:DC + await _dev.设置合成起始相位Async(SynthStartPhase, Ct()); // SYNT:SPH + + var ampl = ParseDoubles(SynthAmplitudeText); + if (ampl.Length > 0) await _dev.设置合成谐波振幅Async(ampl, Ct()); // SYNT:AMPL + var phas = ParseDoubles(SynthPhaseText); + if (phas.Length > 0) await _dev.设置合成谐波相位Async(phas, Ct()); // SYNT:PHAS + + // 回读校验:用仪器自己的回答确认参数是否真的写入(设定与回读不符=被拒绝或落到别的槽位) + string compBack = await _dev.查询合成数据格式Async(Ct()); + double fundBack = await _dev.查询合成基波电压Async(Ct()); + double[] amplBack = await _dev.查询合成谐波振幅Async(Ct()); + Log($"回读校验:COMP 设={SynthCompose} 读={compBack};FUND 设={SynthFundamental} 读={fundBack};AMPL 读前6阶={string.Join(",", amplBack.Take(6))}"); + + await _dev.延时Async(StartDelayMs, Ct()); // *D 200(等待仪器就绪) + 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 谐波阶数={ampl.Length})实测Vrms={vrms}"); + })); + + SynthStopCommand = new DelegateCommand(async () => + await Exec(async () => { await _dev!.触发列表执行Async(false, Ct()); Log("SYNTHESIS 已停止执行(TRIG OFF)"); })); + + // 合成波形的载入 / 保存 / 删除(与 LIST 测试项同机制,文件存于 D:\acp\源测试\合成波形) + LoadSynthPresetCommand = new DelegateCommand(LoadCurrentSynthPreset); + SaveSynthPresetCommand = new DelegateCommand(SaveCurrentSynthPreset); + DeleteSynthPresetCommand = new DelegateCommand(DeleteCurrentSynthPreset); #endregion Initialize(); @@ -482,202 +549,9 @@ namespace PowerAdaptabilityModule.ViewModels 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("停止采集"); - } - - /// - /// 采样循环:通过 INST:COUP NONE + INST:NSEL 逐相切换读取三相交流电压, - /// 恢复 ALL 后读取总功率;频率仅在“采集频率”开关开启时读取(一般不采集)。 - /// - 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; } - } - } - - /// - /// 追加一个采样点到电压 / 频率时间序列图表(UI 线程调用)。 - /// X 轴为采集开始后的经过秒数;电压为三相 RMS,频率仅在采集开关开启(非 NaN)时追加。 - /// - 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 图表构建 - /// - /// 构建时间序列图表:横轴为经过时间(秒),纵轴为对应物理量实测 RMS 值,自动缩放。 - /// - /// 图表标题(如"三相电压") - /// 纵轴单位(V / A / W) - 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; - } - - /// 向图表添加 A/B/C 三相时间序列曲线(红/绿/蓝,带标记点)。 - 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); - } - - /// 创建一条相曲线(带小圆标记,便于辨识离散采样点)。 - 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\源测试) /// /// 确保测试项目录存在;若目录中尚无任何测试项文件,则把内置大纲预设迁移写入为 .json 文件。 @@ -834,6 +708,156 @@ namespace PowerAdaptabilityModule.ViewModels } #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() + { + 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(item); + } + 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)); + + if (_selectedSynthPresetIndex >= _synthPresets.Count) + SelectedSynthPresetIndex = -1; + } + + /// 把一个合成波形预设写入 {SynthStorageDir}\{清洗后名称}.json(缩进格式化)。 + private void WriteSynthPresetFile(SynthPreset preset) + { + string path = Path.Combine(SynthStorageDir, SanitizeFileName(preset.Name) + PresetFileExt); + string json = JsonConvert.SerializeObject(preset, Formatting.Indented); + File.WriteAllText(path, json); + } + + /// 载入选中波形:把文件参数一次性回填到界面各 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; + Log($"已载入波形:{p.Name}(格式={p.Compose} 基频={p.Frequency}Hz 基波={p.Fundamental}V)"); + } + + /// + /// 保存当前界面的 SYNTHESIS 参数快照为波形文件(名称取自“波形名称”输入框)。 + /// + private void SaveCurrentSynthPreset() + { + string name = (SynthPresetName ?? "").Trim(); + if (string.IsNullOrEmpty(name)) + { + Log("请先在“波形名称”填写要保存的名称"); + return; + } + + try + { + if (!Directory.Exists(SynthStorageDir)) + Directory.CreateDirectory(SynthStorageDir); + + var preset = new SynthPreset(name, SynthPresetNote ?? "", SynthCompose, SynthFrequency, + SynthFundamental, SynthDc, SynthStartPhase, SynthAmplitudeText, SynthPhaseText); + WriteSynthPresetFile(preset); + + RefreshSynthPresets(); + int idx = _synthPresets.FindIndex(p => p.Name == name); + if (idx >= 0) SelectedSynthPresetIndex = idx; + + Log($"波形已保存:{Path.Combine(SynthStorageDir, SanitizeFileName(name) + PresetFileExt)}"); + } + 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); + + SelectedSynthPresetIndex = -1; + RefreshSynthPresets(); + Log($"已删除波形:{path}"); + } + catch (Exception ex) + { + Log($"删除波形失败:{ex.Message}"); + } + } + + #endregion + #region 辅助 /// 创建 10 秒超时的取消令牌(命令用)。 private CancellationToken Ct() => (_cmdCts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token; @@ -958,6 +982,10 @@ namespace PowerAdaptabilityModule.ViewModels 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) { @@ -968,7 +996,7 @@ namespace PowerAdaptabilityModule.ViewModels catch { return; } // 查询失败不阻塞下发流程 await Task.Delay(200); } - Log("等待 LIST 空闲超时(可能仍在执行),将继续下发,请留意仪器状态"); + Log("等待空闲超时(可能仍在执行),将继续下发,请留意仪器状态"); } /// @@ -1085,7 +1113,6 @@ namespace PowerAdaptabilityModule.ViewModels public void Dispose() { - StopSampling(); try { _cmdCts?.Cancel(); } catch { } _cmdCts?.Dispose(); } @@ -1153,4 +1180,46 @@ namespace PowerAdaptabilityModule.ViewModels TimeBase = timeBase; } } + + /// + /// 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; + } + } } diff --git a/PowerAdaptabilityModule/Views/PowerAdaptabilityView.xaml b/PowerAdaptabilityModule/Views/PowerAdaptabilityView.xaml index 18aeed9..25cbdff 100644 --- a/PowerAdaptabilityModule/Views/PowerAdaptabilityView.xaml +++ b/PowerAdaptabilityModule/Views/PowerAdaptabilityView.xaml @@ -8,7 +8,6 @@ xmlns:prism="http://prismlibrary.com/" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" - xmlns:oxy="http://oxyplot.org/wpf" prism:ViewModelLocator.AutoWireViewModel="True" d:DesignHeight="1080" d:DesignWidth="1920"> @@ -19,7 +18,6 @@ - @@ -97,6 +95,9 @@ Style="{StaticResource WarnBtn}" />