diff --git a/DeviceCommand/Devices/Chroma61800.cs b/DeviceCommand/Devices/Chroma61800.cs index aa93a10..4523b73 100644 --- a/DeviceCommand/Devices/Chroma61800.cs +++ b/DeviceCommand/Devices/Chroma61800.cs @@ -2,6 +2,7 @@ using DeviceCommand.Base; using Model.Models; using System; +using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; using System.Threading; @@ -39,6 +40,26 @@ namespace DeviceCommand.Devices : 0; } + /// + /// 从设备返回的 SCPI 响应字符串中提取全部数值并按顺序转换为 double 数组。 + /// 用于解析形如 "1.2,3.4,5.6" 的多值响应(如谐波振幅数组 MEAS:HARM:ARR?)。 + /// 兼容逗号/分号/空格分隔及科学计数法。 + /// + /// 设备返回的原始字符串 + /// 按出现顺序排列的数值数组,无有效数值时返回空数组 + private static double[] ExtractDoubleArray(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return Array.Empty(); + + var values = new List(); + foreach (Match m in Regex.Matches(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?")) + { + if (double.TryParse(m.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double v)) + values.Add(v); + } + return values.ToArray(); + } + #region 1. 基础系统与公共指令 /// @@ -65,6 +86,17 @@ namespace DeviceCommand.Devices return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); } + /// + /// 命令流延时 (*D) + /// 在 SCPI 命令流中插入一段延时(单位: ms),常用于 LIST 参数下发完成后、切换 OUTP:MODE 之前等待仪器就绪 + /// + /// 延时时长 (ms) + public virtual async Task 延时Async(double milliseconds, CancellationToken ct = default) + { + string valStr = milliseconds.ToString("0.###", CultureInfo.InvariantCulture); + await SendAsync($"*D {valStr}{ScpiDelimiter}", ct); + } + #endregion #region 2. 基础输出与耦合控制 (OUTPut 子系统) @@ -161,6 +193,37 @@ namespace DeviceCommand.Devices await SendAsync($"PHASE:THREE {relation}{ScpiDelimiter}", ct); } + /// + /// 设置相位耦合模式 (INST:COUP) + /// ALL: 后续命令发送至所有相位; NONE: 仅发送至 NSEL 选定的相位 + /// + /// 耦合模式: ALL | NONE + public virtual async Task 设置相位耦合Async(string coupling, CancellationToken ct = default) + { + await SendAsync($"INST:COUP {coupling}{ScpiDelimiter}", ct); + } + + /// + /// 按编号选择输出相位 (INST:NSEL) + /// 仅在 INST:COUP NONE 时生效,后续 SOURCE 命令将作用于选定相位 + /// + /// 相位编号: 1(A相) | 2(B相) | 3(C相) + public virtual async Task 选择相位Async(int phase, CancellationToken ct = default) + { + await SendAsync($"INST:NSEL {phase}{ScpiDelimiter}", ct); + } + + /// + /// 设置参数编辑方式 (INST:EDIT) + /// EACH: 逐相独立编辑,配合 分别为 A/B/C 三相下发各自的 LIST/参数; + /// ALL: 三相统一编辑,一次下发同时作用于三相 + /// + /// 编辑方式: EACH | ALL + public virtual async Task 设置编辑方式Async(string mode, CancellationToken ct = default) + { + await SendAsync($"INST:EDIT {mode}{ScpiDelimiter}", ct); + } + /// /// 设置输出开启相位角 (PHASE:ON) /// @@ -387,6 +450,110 @@ namespace DeviceCommand.Devices await SendAsync($"TRIG {state}{ScpiDelimiter}", ct); } + /// + /// 查询列表触发执行状态 (TRIG:STATE?) + /// 返回 ON 表示 LIST 正在执行,OFF 表示已执行完毕; + /// 重新下发 LIST 参数前应先轮询本命令等待其变为 OFF,避免执行中被改写 + /// + /// 取消令牌 + /// true: 正在执行(ON); false: 已结束(OFF) + public virtual async Task 查询触发状态Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"TRIG:STATE?{ScpiDelimiter}", ScpiDelimiter, ct); + return resp != null && resp.Trim().StartsWith("ON", StringComparison.OrdinalIgnoreCase); + } + + #endregion + + #region 7. 谐波量测 (Harmonic Sense 子系统) + + /// + /// 设置谐波分析的测量来源 (CONFigure:HARMonic:SOURce) + /// + /// VOLT: 输出电压; CURR: 输出电流 + public virtual async Task 设置谐波量测源Async(string source, CancellationToken ct = default) + { + // 有效参数: VOLT, CURR + await SendAsync($"CONF:HARM:SOUR {source}{ScpiDelimiter}", ct); + } + + /// + /// 设置谐波量测结果显示方式 (CONFigure:HARMonic:TIMES) + /// SINGLE: 保留单次测量结果,约需 3 秒; CONTINUE: 持续更新,约需 10 秒稳定 + /// + /// 显示方式: SINGLE | CONTINUE + public virtual async Task 设置谐波量测方式Async(string times, CancellationToken ct = default) + { + // 有效参数: SINGLE, CONTINUE + await SendAsync($"CONF:HARM:TIM {times}{ScpiDelimiter}", ct); + } + + /// + /// 设置每个谐波阶的数据格式 (CONFigure:HARMonic:PARameter) + /// + /// VALUE: 绝对值; PERCENT: 基频百分率 + public virtual async Task 设置谐波数据格式Async(string parameter, CancellationToken ct = default) + { + // 有效参数: VALUE, PERCENT + await SendAsync($"CONF:HARM:PAR {parameter}{ScpiDelimiter}", ct); + } + + /// + /// 设置谐波分析的原始波形基频 (CONFigure:HARMonic:FREQuency) + /// + /// 基频: 50 或 60 (Hz) + public virtual async Task 设置谐波基频Async(int frequency, CancellationToken ct = default) + { + await SendAsync($"CONF:HARM:FREQ {frequency}{ScpiDelimiter}", ct); + } + + /// + /// 开启/关闭谐波量测 (SENSe:HARMonic) + /// 每次新的 Fetch/Measure 前必须先 ON,约需 3 秒得到结果; + /// 若要量测其他数据,须先设回 OFF。分相量测配合 INST:COUP NONE + INST:NSEL 逐相进行 + /// + /// true: 开启谐波量测(ON); false: 关闭(OFF) + public virtual async Task 设置谐波量测开关Async(bool isOn, CancellationToken ct = default) + { + string state = isOn ? "ON" : "OFF"; + await SendAsync($"SENS:HARM {state}{ScpiDelimiter}", ct); + } + + /// + /// 查询总谐波失真 (MEASure:HARMonic:THD?) + /// + /// 取消令牌 + /// 总谐波失真 THD (单位: %,保留两位小数) + public virtual async Task 查询谐波总失真Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"MEAS:HARM:THD?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); + } + + /// + /// 查询输出电压/电流的基频幅值 (MEASure:HARMonic:FUNDamental?) + /// + /// 取消令牌 + /// 基频幅值 (单位依 CONF:HARM:PAR 为 V/A 绝对值或百分率) + public virtual async Task 查询谐波基频幅值Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"MEAS:HARM:FUND?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); + } + + /// + /// 查询所有谐波阶的振幅数组 (MEASure:HARMonic:ARRay?) + /// 手册 5.7 谐波测量:可量测 2~50 阶谐波,来源可选输出电压(VOLT)或电流(CURR)。 + /// 注意:仅返回各阶振幅,不含各阶相位角,故据此重构的波形形状为"同相叠加"近似 + /// + /// 取消令牌 + /// 各次谐波振幅数组(按阶次升序;单位依 CONF:HARM:PAR) + public virtual async Task 查询谐波振幅数组Async(CancellationToken ct = default) + { + string resp = await WriteReadAsync($"MEAS:HARM:ARR?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDoubleArray(resp); + } + #endregion } diff --git a/DeviceEditModule/ViewModels/Chroma61800ViewModel.cs b/DeviceEditModule/ViewModels/Chroma61800ViewModel.cs index 19cf583..41c8b55 100644 --- a/DeviceEditModule/ViewModels/Chroma61800ViewModel.cs +++ b/DeviceEditModule/ViewModels/Chroma61800ViewModel.cs @@ -2,6 +2,7 @@ using DeviceCommand.Devices; using Prism.Commands; using Prism.Ioc; using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; @@ -30,6 +31,12 @@ namespace DeviceEditModule.ViewModels private double _voltage; public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); } private double _frequency = 50; public double Frequency { get => _frequency; set => SetProperty(ref _frequency, value); } private string _waveform = "SINE"; public string Waveform { get => _waveform; set => SetProperty(ref _waveform, 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 测量结果 @@ -60,18 +67,34 @@ namespace DeviceEditModule.ViewModels Reset = new DelegateCommand(async () => await Exec(async () => { await _dev!.复位Async(Ct()); Log("设备已复位"); })); OutOn = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出开关Async(true, Ct()); Log("输出已开启"); })); OutOff = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出开关Async(false, Ct()); Log("输出已关闭"); })); - SetV = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置交流电压Async(Voltage, Ct()); Log($"电压={Voltage}V"); })); - SetF = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置频率Async(Frequency, Ct()); Log($"频率={Frequency}Hz"); })); - SetWave = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置波形Async(Waveform, Ct()); Log($"波形={Waveform}"); })); + SetV = new DelegateCommand(async () => await Exec(async () => + { + await ApplyPhaseAsync(); + await _dev!.设置交流电压Async(Voltage, Ct()); + Log($"电压={Voltage}V → {PhaseLabel}"); + })); + SetF = new DelegateCommand(async () => await Exec(async () => + { + await ApplyPhaseAsync(); + await _dev!.设置频率Async(Frequency, Ct()); + Log($"频率={Frequency}Hz → {PhaseLabel}"); + })); + SetWave = new DelegateCommand(async () => await Exec(async () => + { + await ApplyPhaseAsync(); + await _dev!.设置波形Async(Waveform, Ct()); + Log($"波形={Waveform} → {PhaseLabel}"); + })); SetThreePhase = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置单三相模式Async(true, Ct()); Log("三相模式"); })); SetSinglePhase = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置单三相模式Async(false, Ct()); Log("单相模式"); })); QMeas = new DelegateCommand(async () => await Exec(async () => { + await ApplyPhaseAsync(); MeasuredVoltage = await _dev!.读取交流电压Async(Ct()); MeasuredCurrent = await _dev!.读取交流电流Async(Ct()); MeasuredFrequency = await _dev!.读取频率Async(Ct()); MeasuredPower = await _dev!.读取真实功率Async(Ct()); - Log($"测量→V:{MeasuredVoltage} I:{MeasuredCurrent} F:{MeasuredFrequency} P:{MeasuredPower}"); + Log($"测量[{PhaseLabel}]→V:{MeasuredVoltage} I:{MeasuredCurrent} F:{MeasuredFrequency} P:{MeasuredPower}"); })); Initialize(); } @@ -106,6 +129,32 @@ namespace DeviceEditModule.ViewModels #region 辅助 + /// 当前选中相位的显示标签 + 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 CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token; private async Task Exec(Func action) diff --git a/DeviceEditModule/Views/Chroma61800View.xaml b/DeviceEditModule/Views/Chroma61800View.xaml index cf657dd..c647b1f 100644 --- a/DeviceEditModule/Views/Chroma61800View.xaml +++ b/DeviceEditModule/Views/Chroma61800View.xaml @@ -94,6 +94,12 @@ + + + + 采样周期(毫秒) private const int SampleIntervalMs = 1000; - /// 合成正弦波的分段数(0~360°,每 2° 一个点,共 181 点) - private const int SinePoints = 180; - - /// 三相相位偏移(°):A=0,B=-120,C=+120 - private static readonly double[] PhaseOffsetDeg = { 0, -120, 120 }; - /// 三相曲线配色:A 红、B 绿、C 蓝(三张图统一,便于对照) private static readonly OxyColor[] PhaseColors = { OxyColor.FromRgb(0xE5, 0x39, 0x35), OxyColor.FromRgb(0x43, 0xA0, 0x47), OxyColor.FromRgb(0x1E, 0x88, 0xE5) }; @@ -45,10 +42,14 @@ namespace PowerAdaptabilityModule.ViewModels private CancellationTokenSource? _cmdCts; private CancellationTokenSource? _sampleCts; - // 三相正弦曲线:按物理量分组(电压 / 电流 / 功率),每组含 A/B/C 三相 + /// 采样计时器,记录从开始采集到当前的经过秒数(X 轴) + private System.Diagnostics.Stopwatch? _sampleStopwatch; + + // 三相电压时间序列曲线(A/B/C 三相) private readonly LineSeries _voltA, _voltB, _voltC; - private readonly LineSeries _currA, _currB, _currC; - private readonly LineSeries _powA, _powB, _powC; + + // 输出频率时间序列曲线(单条,ALL 模式读一个总频率) + private readonly LineSeries _freq; #endregion #region 设备状态属性 @@ -64,26 +65,24 @@ namespace PowerAdaptabilityModule.ViewModels 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 三相图表(按物理量分图:电压 / 电流 / 功率,每图叠加 A/B/C 三相正弦) + #region 图表(三相电压时间序列 + 输出频率时间序列) /// 三相电压波形图表 public PlotModel VoltagePlot { get; } - /// 三相电流波形图表 - public PlotModel CurrentPlot { get; } - - /// 三相功率波形图表 - public PlotModel PowerPlot { get; } + /// 输出频率时间序列图表(受“采集频率”开关控制,默认不采集) + public PlotModel FrequencyPlot { get; } #endregion #region 实时测量值 private double _voltageA; public double VoltageA { get => _voltageA; set => SetProperty(ref _voltageA, value); } - private double _currentA; public double CurrentA { get => _currentA; set => SetProperty(ref _currentA, value); } private double _voltageB; public double VoltageB { get => _voltageB; set => SetProperty(ref _voltageB, value); } - private double _currentB; public double CurrentB { get => _currentB; set => SetProperty(ref _currentB, value); } private double _voltageC; public double VoltageC { get => _voltageC; set => SetProperty(ref _voltageC, value); } - private double _currentC; public double CurrentC { get => _currentC; set => SetProperty(ref _currentC, value); } private double _frequencyNow; /// 当前输出频率 (Hz) @@ -114,63 +113,145 @@ namespace PowerAdaptabilityModule.ViewModels 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 模式) - private string _listCoupling = "NONE"; - /// 列表耦合模态 (ALL / NONE) - public string ListCoupling { get => _listCoupling; set => SetProperty(ref _listCoupling, value); } + #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 = 1; - /// 列表执行次数 (0 ~ 65535) + 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 _listVoltAcStartText = ""; - /// AC 起始电压序列(逗号分隔,如 0,50,100,150) - public string ListVoltAcStartText { get => _listVoltAcStartText; set => SetProperty(ref _listVoltAcStartText, value); } + /// 三相各自的 LIST 序列配置(索引 0=A/Φ1, 1=B/Φ2, 2=C/Φ3),INST:EDIT EACH 逐相下发 + private readonly ListPhaseConfig[] _phaseConfigs = { new(), new(), new() }; - private string _listVoltAcEndText = ""; - /// AC 结束电压序列(逗号分隔) - public string ListVoltAcEndText { get => _listVoltAcEndText; set => SetProperty(ref _listVoltAcEndText, value); } + /// 相位编辑选项(对应 INST:NSEL 1/2/3) + public List ListPhaseOptions { get; } = new() { "A相 (Φ1)", "B相 (Φ2)", "C相 (Φ3)" }; - private string _listFreqStartText = ""; - /// 起始频率序列(逗号分隔,如 50,60,50) - public string ListFreqStartText { get => _listFreqStartText; set => SetProperty(ref _listFreqStartText, value); } + private int _listEditPhaseIndex; + /// 当前正在编辑的相位索引 (0=A, 1=B, 2=C),切换时下方序列文本框自动刷新为该相内容 + public int ListEditPhaseIndex + { + get => _listEditPhaseIndex; + set + { + if (SetProperty(ref _listEditPhaseIndex, value)) + RaiseListSequenceChanged(); + } + } - private string _listFreqEndText = ""; - /// 结束频率序列(逗号分隔) - public string ListFreqEndText { get => _listFreqEndText; set => SetProperty(ref _listFreqEndText, value); } + /// 当前选中相位的配置对象 + private ListPhaseConfig CurCfg => _phaseConfigs[_listEditPhaseIndex]; - private string _listDwellText = ""; - /// 静止时间序列(ms,逗号分隔,如 500,500,500) - public string ListDwellText { get => _listDwellText; set => SetProperty(ref _listDwellText, value); } + // —— 以下文本属性均代理到"当前编辑相位"的配置,切换相位时统一刷新 —— + /// 当前相波形 (FUNC:SHAP:A) + public string ListWaveform { get => CurCfg.Waveform; set { if (CurCfg.Waveform != value) { CurCfg.Waveform = value; RaisePropertyChanged(); } } } + /// 相位角度序列 (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 string _listDegrText = ""; - /// 相位角度序列(°,逗号分隔) - public string ListDegrText { get => _listDegrText; set => SetProperty(ref _listDegrText, value); } - - private string _listShapeText = ""; - /// 波形缓冲顺序(A/B,逗号分隔,如 A,B,A) - public string ListShapeText { get => _listShapeText; set => SetProperty(ref _listShapeText, value); } + /// 切换编辑相位后,通知所有序列文本框刷新为新相位内容 + 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 WaveformOptions { get; } = new[] { "SINE", "SQUA", "CSIN" }; public IReadOnlyList CouplingOptions { get; } = new[] { "AC", "DC", "ACDC" }; public IReadOnlyList RelationOptions { get; } = new[] { "INDEPEND", "SAMEFREQ", "BALANCE" }; - public IReadOnlyList ListCouplingOptions { get; } = new[] { "ALL", "NONE" }; public IReadOnlyList ListTriggerOptions { get; } = new[] { "AUTO", "MANUAL", "EXCITE" }; public IReadOnlyList ListTimeBaseOptions { get; } = new[] { "TIME", "CYCLE" }; #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); } + + 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 = ""; /// 操作与采样日志 @@ -193,9 +274,11 @@ namespace PowerAdaptabilityModule.ViewModels public ICommand SetWaveformCommand { get; } public ICommand SetFixedModeCommand { get; } public ICommand EnterListModeCommand { get; } - public ICommand ApplyListBasicCommand { get; } - public ICommand ApplyListSequencesCommand { get; } - public ICommand ListStartCommand { 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; } @@ -207,13 +290,18 @@ namespace PowerAdaptabilityModule.ViewModels _dm = containerProvider.Resolve(); _globalInfo = containerProvider.Resolve(); - // 初始化三相图表(按物理量分图,每图叠加 A/B/C 三相正弦) - VoltagePlot = BuildSinePlot("三相电压", "V"); - CurrentPlot = BuildSinePlot("三相电流", "A"); - PowerPlot = BuildSinePlot("三相功率", "W"); + // 初始化三相电压时间序列图表(X=时间秒, Y=实测 RMS 电压, 叠加 A/B/C 三相) + VoltagePlot = BuildTimeSeriesPlot("三相电压", "V"); (_voltA, _voltB, _voltC) = AddThreePhaseSeries(VoltagePlot); - (_currA, _currB, _currC) = AddThreePhaseSeries(CurrentPlot); - (_powA, _powB, _powC) = AddThreePhaseSeries(PowerPlot); + + // 输出频率时间序列图表(单条曲线,受“采集频率”开关控制) + FrequencyPlot = BuildTimeSeriesPlot("输出频率", "Hz"); + _freq = NewPhaseSeries("频率", OxyColor.FromRgb(0x8E, 0x24, 0xAA)); + FrequencyPlot.Series.Add(_freq); + + // 初始化测试项文件目录(首次运行迁移内置预设),再从磁盘加载测试项 + EnsurePresetStorage(); + RefreshPresets(); #region 命令初始化 QueryIdnCommand = new DelegateCommand(async () => @@ -247,13 +335,28 @@ namespace PowerAdaptabilityModule.ViewModels await Exec(async () => { await _dev!.设置三相关系Async(ThreePhaseRelation, Ct()); Log($"三相关系={ThreePhaseRelation}"); })); SetVoltageCommand = new DelegateCommand(async () => - await Exec(async () => { await _dev!.设置交流电压Async(Voltage, Ct()); Log($"交流电压={Voltage} V"); })); + await Exec(async () => + { + await ApplyPhaseAsync(); + await _dev!.设置交流电压Async(Voltage, Ct()); + Log($"交流电压={Voltage} V → {PhaseLabel}"); + })); SetFrequencyCommand = new DelegateCommand(async () => - await Exec(async () => { await _dev!.设置频率Async(Frequency, Ct()); Log($"输出频率={Frequency} Hz"); })); + await Exec(async () => + { + await ApplyPhaseAsync(); + await _dev!.设置频率Async(Frequency, Ct()); + Log($"输出频率={Frequency} Hz → {PhaseLabel}"); + })); SetWaveformCommand = new DelegateCommand(async () => - await Exec(async () => { await _dev!.设置波形Async(Waveform, Ct()); Log($"波形={Waveform}"); })); + 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 固定模式"); })); @@ -261,44 +364,77 @@ namespace PowerAdaptabilityModule.ViewModels EnterListModeCommand = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置操作模态Async("LIST", Ct()); Log("已切换为 LIST 模式"); })); - ApplyListBasicCommand = new DelegateCommand(async () => await Exec(async () => + // 载入测试项:把选中测试项的三相序列与基础参数一次性填充到界面 + LoadPresetCommand = new DelegateCommand(() => { - await _dev!.设置列表耦合模态Async(ListCoupling, Ct()); - await _dev!.设置列表触发形态Async(ListTrigger, Ct()); - await _dev!.设置列表执行次数Async(ListCount, Ct()); - await _dev!.设置列表时间基础Async(ListTimeBase, Ct()); - Log($"LIST 基础配置:耦合={ListCoupling} 触发={ListTrigger} 次数={ListCount} 时基={ListTimeBase}"); - })); + 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(); - ApplyListSequencesCommand = new DelegateCommand(async () => await Exec(async () => - { - int applied = 0; - var vAcStart = ParseDoubles(ListVoltAcStartText); - if (vAcStart.Length > 0) { await _dev!.设置列表AC起始电压顺序Async(vAcStart, Ct()); applied++; } - var vAcEnd = ParseDoubles(ListVoltAcEndText); - if (vAcEnd.Length > 0) { await _dev!.设置列表AC结束电压顺序Async(vAcEnd, Ct()); applied++; } - var fStart = ParseDoubles(ListFreqStartText); - if (fStart.Length > 0) { await _dev!.设置列表起始频率顺序Async(fStart, Ct()); applied++; } - var fEnd = ParseDoubles(ListFreqEndText); - if (fEnd.Length > 0) { await _dev!.设置列表结束频率顺序Async(fEnd, Ct()); applied++; } - var dwell = ParseDoubles(ListDwellText); - if (dwell.Length > 0) { await _dev!.设置列表静止时间顺序Async(dwell, Ct()); applied++; } - var degr = ParseDoubles(ListDegrText); - if (degr.Length > 0) { await _dev!.设置列表相位角度顺序Async(degr, Ct()); applied++; } - var shapes = ParseShapes(ListShapeText); - if (shapes.Length > 0) { await _dev!.设置列表波形顺序Async(shapes, Ct()); applied++; } - Log(applied > 0 ? $"已下发 {applied} 组列表序列" : "没有可下发的列表序列(请先填写序列文本框)"); - })); + 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})"); + }); - ListStartCommand = new DelegateCommand(async () => await Exec(async () => + // 保存当前界面的 LIST 配置为测试项文件(写入 D:\acp\源测试\{名称}.json) + SavePresetCommand = new DelegateCommand(SaveCurrentPreset); + + // 删除当前选中的测试项文件(带二次确认) + DeletePresetCommand = new DelegateCommand(DeleteCurrentPreset); + + // 以当前编辑相位为模板复制到三相,B/C 相相位角按 ±120° 自动偏移,生成对称三相序列 + CopyPhaseToThreeCommand = new DelegateCommand(() => { - await _dev!.设置操作模态Async("LIST", Ct()); - await _dev!.触发列表执行Async(true, Ct()); - Log("LIST 已开始执行"); + 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 已停止执行"); })); + await Exec(async () => { await _dev!.触发列表执行Async(false, Ct()); Log("LIST 已停止执行(TRIG OFF)"); })); StartSamplingCommand = new DelegateCommand(StartSampling); StopSamplingCommand = new DelegateCommand(StopSampling); @@ -358,6 +494,7 @@ namespace PowerAdaptabilityModule.ViewModels if (IsSampling) return; IsSampling = true; + _sampleStopwatch = System.Diagnostics.Stopwatch.StartNew(); _sampleCts = new CancellationTokenSource(); var token = _sampleCts.Token; _ = Task.Run(() => SamplingLoopAsync(token), token); @@ -368,6 +505,7 @@ namespace PowerAdaptabilityModule.ViewModels { if (!IsSampling) return; IsSampling = false; + _sampleStopwatch?.Stop(); try { _sampleCts?.Cancel(); } catch { } _sampleCts?.Dispose(); _sampleCts = null; @@ -375,9 +513,8 @@ namespace PowerAdaptabilityModule.ViewModels } /// - /// 采样循环:逐相读取交流电压/电流,合成波形;并刷新频率与功率实时值。 - /// 说明:Chroma61800 类现有测量方法面向"活动相位",三相逐一调用读取; - /// 如设备需要按相切换后读取,可在每相读取前补充相位选择命令。 + /// 采样循环:通过 INST:COUP NONE + INST:NSEL 逐相切换读取三相交流电压, + /// 恢复 ALL 后读取总功率;频率仅在“采集频率”开关开启时读取(一般不采集)。 /// private async Task SamplingLoopAsync(CancellationToken token) { @@ -387,28 +524,36 @@ namespace PowerAdaptabilityModule.ViewModels { if (_dev != null && _dev.IsConnected) { - // A 相 + // 切换为独立相位模式 + await _dev.设置相位耦合Async("NONE", token); + + // A 相 (Φ1) + await _dev.选择相位Async(1, token); double vA = await _dev.读取交流电压Async(token); - double iA = await _dev.读取交流电流Async(token); - // B 相 + // B 相 (Φ2) + await _dev.选择相位Async(2, token); double vB = await _dev.读取交流电压Async(token); - double iB = await _dev.读取交流电流Async(token); - // C 相 + // C 相 (Φ3) + await _dev.选择相位Async(3, token); double vC = await _dev.读取交流电压Async(token); - double iC = await _dev.读取交流电流Async(token); - // 设备级:频率与功率 - double freq = 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(() => { - RegenerateWaveforms(vA, iA, vB, iB, vC, iC, freq, power); + AppendSample(elapsedSec, vA, vB, vC, freq, power); IsConnected = _dev.IsConnected; })); } @@ -426,90 +571,51 @@ namespace PowerAdaptabilityModule.ViewModels } /// - /// 根据实测 RMS 值合成三相正弦波并刷新三张图表(UI 线程调用)。 - /// 横轴为相位角 0~360°,纵轴对称显示正负半周;电压/电流峰值 = √2 × RMS, - /// 三相相位互差 120°(A=0,B=-120,C=+120)。设备仅提供 RMS 标量,故为示意波形。 + /// 追加一个采样点到电压 / 频率时间序列图表(UI 线程调用)。 + /// X 轴为采集开始后的经过秒数;电压为三相 RMS,频率仅在采集开关开启(非 NaN)时追加。 /// - private void RegenerateWaveforms(double vA, double iA, double vB, double iB, - double vC, double iC, double freq, double power) + private void AppendSample(double elapsedSec, double vA, double vB, double vC, double freq, double power) { - // 电压三相 - RegenerateSine(_voltA, vA, PhaseOffsetDeg[0]); - RegenerateSine(_voltB, vB, PhaseOffsetDeg[1]); - RegenerateSine(_voltC, vC, PhaseOffsetDeg[2]); - // 电流三相 - RegenerateSine(_currA, iA, PhaseOffsetDeg[0]); - RegenerateSine(_currB, iB, PhaseOffsetDeg[1]); - RegenerateSine(_currC, iC, PhaseOffsetDeg[2]); - // 功率三相(设备仅提供总有功,平衡三相每相 ≈ 总功率 / 3,按正弦示意呈现正负半周) - double pPhase = power / 3.0; - RegenerateSine(_powA, pPhase, PhaseOffsetDeg[0]); - RegenerateSine(_powB, pPhase, PhaseOffsetDeg[1]); - RegenerateSine(_powC, pPhase, PhaseOffsetDeg[2]); + // 三相电压 + _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; CurrentA = iA; - VoltageB = vB; CurrentB = iB; - VoltageC = vC; CurrentC = iC; - FrequencyNow = freq; PowerNow = power; - - // 纵轴对称范围(取该图三相峰值最大值并留 15% 余量,确保正负半周完整可见) - SetSymmetricAxis(VoltagePlot, vA, vB, vC); - SetSymmetricAxis(CurrentPlot, iA, iB, iC); - SetSymmetricAxis(PowerPlot, pPhase); + VoltageA = vA; + VoltageB = vB; + VoltageC = vC; + if (hasFreq) FrequencyNow = freq; + PowerNow = power; VoltagePlot.InvalidatePlot(true); - CurrentPlot.InvalidatePlot(true); - PowerPlot.InvalidatePlot(true); - } - - /// 按 RMS 幅值与相位偏移,生成 0~360° 一个完整周期的正弦曲线点(峰值 = √2 × RMS)。 - private static void RegenerateSine(LineSeries series, double rms, double phaseDeg) - { - series.Points.Clear(); - double peak = Math.Sqrt(2) * rms; - double phaseRad = phaseDeg * Math.PI / 180.0; - for (int i = 0; i <= SinePoints; i++) - { - double deg = 360.0 * i / SinePoints; - double rad = deg * Math.PI / 180.0 + phaseRad; - series.Points.Add(new DataPoint(deg, peak * Math.Sin(rad))); - } - } - - /// 设置纵轴对称范围(-peak ~ +peak);无输出时回退到 ±1 避免坐标轴退化。 - private static void SetSymmetricAxis(PlotModel plot, params double[] rmsValues) - { - double maxRms = rmsValues.Max(); - double peak = Math.Sqrt(2) * maxRms * 1.15; - if (peak <= 0) peak = 1; - if (plot.Axes.OfType().FirstOrDefault() is { } axis) - { - axis.Minimum = -peak; - axis.Maximum = peak; - } + if (hasFreq) FrequencyPlot.InvalidatePlot(true); } private void ClearCharts() { - foreach (var s in new[] { _voltA, _voltB, _voltC, _currA, _currB, _currC, _powA, _powB, _powC }) + foreach (var s in new[] { _voltA, _voltB, _voltC, _freq }) { s.Points.Clear(); } + _sampleStopwatch?.Restart(); VoltagePlot.InvalidatePlot(true); - CurrentPlot.InvalidatePlot(true); - PowerPlot.InvalidatePlot(true); - Log("已清空波形数据"); + FrequencyPlot.InvalidatePlot(true); + Log("已清空波形数据,计时器已重置"); } #endregion #region 图表构建 /// - /// 构建正弦波图表:横轴为相位角 0~360°,纵轴为对应物理量(对称显示正负半周),含图例。 + /// 构建时间序列图表:横轴为经过时间(秒),纵轴为对应物理量实测 RMS 值,自动缩放。 /// - /// 图表标题(如“三相电压”) + /// 图表标题(如"三相电压") /// 纵轴单位(V / A / W) - private static PlotModel BuildSinePlot(string title, string unit) + private static PlotModel BuildTimeSeriesPlot(string title, string unit) { var pm = new PlotModel { @@ -520,23 +626,23 @@ namespace PowerAdaptabilityModule.ViewModels pm.Axes.Add(new LinearAxis { Position = AxisPosition.Bottom, - Title = "相位角 (°)", + Title = "时间 (s)", Minimum = 0, - Maximum = 360, - MajorStep = 45, - MinorStep = 15, - StringFormat = "0", + Maximum = 10, MajorGridlineStyle = LineStyle.Dot, - MinorGridlineStyle = LineStyle.None + MinorGridlineStyle = LineStyle.None, + StringFormat = "0.0", + IsZoomEnabled = true, + IsPanEnabled = true }); pm.Axes.Add(new LinearAxis { Position = AxisPosition.Left, Title = unit, - Minimum = -1, - Maximum = 1, MajorGridlineStyle = LineStyle.Dot, - MinorGridlineStyle = LineStyle.None + MinorGridlineStyle = LineStyle.None, + IsZoomEnabled = true, + IsPanEnabled = true }); pm.Legends.Add(new Legend { @@ -547,7 +653,7 @@ namespace PowerAdaptabilityModule.ViewModels return pm; } - /// 向图表添加 A/B/C 三相正弦曲线(红/绿/蓝,与三张图配色一致)。 + /// 向图表添加 A/B/C 三相时间序列曲线(红/绿/蓝,带标记点)。 private static (LineSeries A, LineSeries B, LineSeries C) AddThreePhaseSeries(PlotModel plot) { var a = NewPhaseSeries("A 相", PhaseColors[0]); @@ -559,20 +665,205 @@ namespace PowerAdaptabilityModule.ViewModels return (a, b, c); } - /// 创建一条相曲线(无标记点、平滑线条)。 + /// 创建一条相曲线(带小圆标记,便于辨识离散采样点)。 private static LineSeries NewPhaseSeries(string title, OxyColor color) => new LineSeries { Title = title, Color = color, StrokeThickness = 1.8, - MarkerType = MarkerType.None + MarkerType = MarkerType.Circle, + MarkerSize = 3, + MarkerFill = color }; + + #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() + { + 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(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; + } + + /// 把一个测试项写入 D:\acp\源测试\{清洗后名称}.json(缩进格式化,便于人工查看)。 + 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); + } + + /// + /// 保存当前界面的 LIST 配置为测试项文件(名称取自“测试项名称”输入框)。 + /// 快照当前三相序列配置 + 执行次数/触发/时基 + 说明,写入后刷新列表并选中该项。 + /// + 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}"); + } + } + + /// + /// 删除当前选中的测试项文件(带二次确认),删除后刷新列表。 + /// + 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}"); + } + } + + /// 清洗文件名中的非法字符(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 辅助 /// 创建 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) { @@ -625,6 +916,171 @@ namespace PowerAdaptabilityModule.ViewModels .Select(s => s.Trim().ToUpperInvariant()) .ToArray(); } + + /// + /// 下发单个相位的 LIST 序列(调用前须已通过 INST:NSEL 选定目标相、并已 INST:EDIT EACH)。 + /// 依次下发 FUNC:SHAP:A 波形,以及 DEGR / VOLT:AC / VOLT:DC / FREQ / DWEL / SHAP 各序列,留空的序列跳过。 + /// + 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 + } + + /// + /// 轮询 TRIG:STATE? 等待当前 LIST 执行结束(返回 OFF),避免在执行过程中改写参数导致仪器报错。 + /// + /// 最长等待时间(ms),超时后仍继续下发并给出提示 + 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 空闲超时(可能仍在执行),将继续下发,请留意仪器状态"); + } + + /// + /// 将相位角序列整体偏移指定角度(结果对 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 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 }; + } + + /// + /// 构建大纲典型试验预设集合(仅含 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("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() @@ -634,4 +1090,67 @@ namespace PowerAdaptabilityModule.ViewModels _cmdCts?.Dispose(); } } + + /// + /// 单个相位的 LIST 序列配置(对应界面一组序列文本框)。 + /// 三相独立下发(INST:EDIT EACH + INST:NSEL)时,每相持有一份本配置, + /// 各字段均为逗号分隔的原始文本,下发时再解析为数值序列。 + /// + public class ListPhaseConfig + { + /// 该相波形 (FUNC:SHAP:A),如 SINE / SQUA + public string Waveform { get; set; } = "SINE"; + /// 相位角度序列 (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; } + /// 三相序列配置(索引 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") + { + Name = name; + Note = note; + Count = count; + Phases = phases; + Trigger = trigger; + TimeBase = timeBase; + } + } } diff --git a/PowerAdaptabilityModule/Views/PowerAdaptabilityView.xaml b/PowerAdaptabilityModule/Views/PowerAdaptabilityView.xaml index 6400633..18aeed9 100644 --- a/PowerAdaptabilityModule/Views/PowerAdaptabilityView.xaml +++ b/PowerAdaptabilityModule/Views/PowerAdaptabilityView.xaml @@ -137,6 +137,12 @@ + + + + - - + + + + +