diff --git a/DeviceCommand/Devices/SDS2000X_HD.cs b/DeviceCommand/Devices/SDS2000X_HD.cs index 78223d6..2746466 100644 --- a/DeviceCommand/Devices/SDS2000X_HD.cs +++ b/DeviceCommand/Devices/SDS2000X_HD.cs @@ -3,7 +3,7 @@ using DeviceCommand.Base; using Model.Models; using System; using System.Globalization; -using System.Text; +using System.IO; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -17,21 +17,22 @@ namespace DeviceCommand.Devices private const string ScpiDelimiter = "\n"; /// - /// 构造函数:传入 一次性初始化示波器通信参数。 + /// 构造函数:一次性初始化示波器通信参数。 /// + /// TCP 通信配置对象,包含 IP 地址和端口号等信息 public SDS2000X_HD(TcpConfig config) : base(config) { } /// /// 从示波器返回的 SCPI 响应字符串中提取数值部分并转换为 double。 - /// 响应格式示例: "C1:PAVA PKPK,2.48E-03V" → 按逗号分割后提取 2.48E-03 + /// 响应格式示例: "PKPK,2.48E-03V" → 按逗号分割后提取 2.48E-03 /// /// 示波器返回的原始字符串 - /// 提取到的数值,解析失败时返回 0 + /// 提取到的数值,解析失败或字符串为空时返回 0 private static double ExtractDouble(string raw) { - if (string.IsNullOrWhiteSpace(raw)) return 0; + if (string.IsNullOrWhiteSpace(raw)) return 0; string dataPart = raw; // 按逗号分割,取最后一段(数值通常在逗号后面) @@ -49,46 +50,50 @@ namespace DeviceCommand.Devices #region 1. IEEE 488.2 公共命令 /// - /// 清除标准事件状态寄存器和错误队列 + /// 清除标准事件状态寄存器和错误队列 (*CLS)。 /// - /// 取消令牌 + /// 异步取消令牌 + /// 表示异步操作的 Task public virtual async Task 清除状态(CancellationToken ct = default) { await SendAsync($"*CLS{ScpiDelimiter}", ct); } + /// - /// 查询错误信息 + /// 查询错误信息。 /// - /// 取消令牌 - /// 错误代码和描述信息 + /// 异步取消令牌 + /// 示波器返回的错误代码和描述信息的字符串 public virtual async Task 查询错误信息(CancellationToken ct = default) { - return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1] + return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); } + /// - /// 读取示波器识别字符串(制造商、型号、序列号、固件版本) + /// 读取示波器识别字符串(制造商、型号、序列号、固件版本)。 /// - /// 取消令牌 - /// 设备标识字符串(制造商, 型号, 序列号, 固件版本) + /// 异步取消令牌 + /// 设备标识字符串(如 "Siglent Technologies,SDS2004X HD,...") public virtual async Task 查询设备标识(CancellationToken ct = default) { return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); } /// - /// 复位命令。使示波器恢复到默认的出厂设置 + /// 复位命令 (*RST)。使示波器恢复到默认的出厂设置。 /// - /// 取消令牌 + /// 异步取消令牌 + /// 表示异步操作的 Task public virtual async Task 重置设备(CancellationToken ct = default) { await SendAsync($"*RST{ScpiDelimiter}", ct); } /// - /// 查询先前操作是否完成。 + /// 查询先前操作是否完成 (*OPC?)。 /// - /// 取消令牌 - /// true: 操作已完成, false: 操作仍在执行中 + /// 异步取消令牌 + /// 布尔值:true 表示所有挂起的操作已完成,false 表示操作仍在执行中 public virtual async Task 检查操作完成_OPC(CancellationToken ct = default) { string res = await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct); @@ -97,147 +102,129 @@ namespace DeviceCommand.Devices #endregion - #region 2. 运行与捕获控制 (Run / Stop / Single) + #region 2. 运行与捕获控制 (Run / Stop / Trigger Mode) /// - /// 控制示波器开始捕获波形 + /// 控制示波器开始连续捕获波形。 /// - /// 取消令牌 + /// 异步取消令牌 + /// 表示异步操作的 Task public virtual async Task 启动捕获_RUN(CancellationToken ct = default) { await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct); } /// - /// 停止捕获波形 + /// 停止捕获波形,画面将定格在当前捕捉的帧。 /// - /// 取消令牌 + /// 异步取消令牌 + /// 表示异步操作的 Task public virtual async Task 停止捕获_STOP(CancellationToken ct = default) { - await SendAsync($"TRIGger:STOP{ScpiDelimiter}", ct); + await SendAsync($":TRIGger:STOP{ScpiDelimiter}", ct); } - /// - /// 强制示波器进入单次触发捕获模式 - /// - /// 取消令牌 - public virtual async Task 单次触发_SINGLE(CancellationToken ct = default) - { - await SendAsync($":TRIGger:MODE SINGle{ScpiDelimiter}", ct); - } - - /// - /// 触发一次波形采样 - /// - /// 取消令牌 - public virtual async Task 强制触发(CancellationToken ct = default) - { - await SendAsync($"::TRIGger:MODE FTRIG{ScpiDelimiter}", ct); - } - /// - /// 示波器触发控制模式(符合 SDS2000X-HD 规范)。 - /// public enum TriggerMode { - /// - /// 自动触发模式 (即使无触发信号也周期性刷屏) - /// + /// 自动触发模式 (即使无触发信号也周期性刷屏) AUTO, - - /// - /// 普通触发模式 (仅当满足触发条件时才刷新) - /// - NORM, - - /// - /// 单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP) - /// - SINGLE + /// 普通触发模式 (仅当满足触发条件时才刷新) + NORMal, + /// 单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP) + SINGle } + /// - /// 设置触发模式 (AUTO 自动, NORM 普通, SINGLE 单次) + /// 设置触发模式。 /// - /// 触发模式枚举 - /// 取消令牌 + /// 触发模式枚举 (AUTO 自动, NORMal 普通, SINGle 单次) + /// 异步取消令牌 + /// 表示异步操作的 Task public virtual async Task 设置触发模式(TriggerMode mode, CancellationToken ct = default) { - // 例如发送:TRMD AUTO\n、TRMD NORM\n 或 TRMD SINGLE\n - string cmd = $"TRMD {mode}{ScpiDelimiter}"; + string cmd = $":TRIGger:MODE {mode}{ScpiDelimiter}"; await SendAsync(cmd, ct); } + #endregion - #region 3. Channel 垂直控制子系统 (C1 ~ C4) + #region 3. Channel 垂直控制子系统 (电压/电流纹波测试依赖此项) /// - /// 开启或关闭指定的模拟通道 (符合手册 57 页规范) + /// 开启或关闭指定的模拟通道。 /// /// 通道号 (1 - 4) - /// true: 开启通道, false: 关闭通道 - /// 取消令牌 + /// 布尔值:true 表示开启通道, false 表示关闭通道 + /// 异步取消令牌 + /// 表示异步操作的 Task public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default) { string state = enable ? "ON" : "OFF"; - await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct); + await SendAsync($":CHANnel{channel}:SWITch {state}{ScpiDelimiter}", ct); } /// - /// 设置指定通道的垂直电压档位 (Volts/Div) + /// 设置指定通道的物理单位,用于区分测电压还是测电流,影响测量结果读数。 /// /// 通道号 (1 - 4) - /// 电压档位 (Volts/Div) - /// 取消令牌 - public virtual async Task 设置通道电压档位(int channel, double volts, CancellationToken ct = default) + /// 字符串:"V" 代表电压,"A" 代表电流 + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置通道单位(int channel, string unit, CancellationToken ct = default) { - string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:VDIV {1:F4}{2}", channel, volts, ScpiDelimiter); + await SendAsync($":CHANnel{channel}:UNIT {unit.ToUpper()}{ScpiDelimiter}", ct); + } + + /// + /// 设置指定通道的垂直档位 (Volts/Div 或 Amps/Div)。 + /// + /// 通道号 (1 - 4) + /// 档位数值 (例如 0.05 表示 50mV/div 或 50mA/div) + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置通道档位(int channel, double value, CancellationToken ct = default) + { + string cmd = string.Format(CultureInfo.InvariantCulture, ":CHANnel{0}:SCALe {1:E4}{2}", channel, value, ScpiDelimiter); await SendAsync(cmd, ct); } + public enum ChannelCoupling { DC, AC, GND } + /// - /// 查询指定通道当前的电压档位 + /// 设置通道耦合方式 (测纹波时必须设置为 AC 交流耦合)。 /// /// 通道号 (1 - 4) - /// 取消令牌 - /// 当前电压档位值 (Volts/Div) - public virtual async Task 查询通道电压档位(int channel, CancellationToken ct = default) + /// 耦合方式枚举 (DC, AC, GND) + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置通道耦合(int channel, ChannelCoupling coupling, CancellationToken ct = default) { - string res = await WriteReadAsync($"C{channel}:VDIV?{ScpiDelimiter}", ScpiDelimiter, ct); - return ExtractDouble(res); + await SendAsync($":CHANnel{channel}:COUPling {coupling}{ScpiDelimiter}", ct); } /// - /// 设置指定通道的垂直偏移量 (Offset) + /// 设置通道带宽限制 (测纹波时通常设置为 20M 带宽限制以滤除高频噪声)。 /// /// 通道号 (1 - 4) - /// 垂直偏移量 (单位: V) - /// 取消令牌 + /// 带宽限制参数的字符串 (可选: "20M", "200M", "FULL") + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置通道带宽限制(int channel, string limit, CancellationToken ct = default) + { + await SendAsync($":CHANnel{channel}:BWLimit {limit.ToUpper()}{ScpiDelimiter}", ct); + } + + /// + /// 设置通道的垂直偏移量,用于调整波形在屏幕上的垂直位置。 + /// 泄放测试中需要设置合适的偏移使衰减波形完整显示在屏幕内。 + /// + /// 通道号 (1 - 4) + /// 垂直偏移值 (单位:V 或 A,正值向上移动,负值向下移动) + /// 异步取消令牌 + /// 表示异步操作的 Task public virtual async Task 设置通道垂直偏移(int channel, double offset, CancellationToken ct = default) { - string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:OFST {1:F4}{2}", channel, offset, ScpiDelimiter); - await SendAsync(cmd, ct); - } - /// - /// 示波器通道输入阻抗类型。 - /// - public enum ImpedanceType - { - /// 50Ω 阻抗 - FIFty, - - /// 1MΩ 阻抗 - ONEM - } - - /// - /// 设置指定通道的输入阻抗类型 - /// - /// 通道号 (1 - 4) - /// 阻抗类型枚举 (FIFty: 50Ω, ONEM: 1MΩ) - /// 取消令牌 - public virtual async Task 设置通道阻抗(int channel, ImpedanceType impedance, CancellationToken ct = default) - { - // 例如发送:C1:IMPedance FIFty\n 或 C1:IMPedance ONEM\n - string cmd = $"CHANnel{channel}:IMPedance {impedance}{ScpiDelimiter}"; + string cmd = string.Format(CultureInfo.InvariantCulture, ":CHANnel{0}:OFFSet {1:F3}{2}", channel, offset, ScpiDelimiter); await SendAsync(cmd, ct); } @@ -246,168 +233,199 @@ namespace DeviceCommand.Devices #region 4. Timebase 水平时基子系统 /// - /// 设置示波器的水平时基档位 (Time/Div) + /// 设置示波器的水平时基档位 (Time/Div)。 /// - /// 水平时基档位 (Time/Div) - /// 取消令牌 + /// 水平时基数值 (单位:秒/格,如 1.00E-03 表示 1ms/div) + /// 异步取消令牌 + /// 表示异步操作的 Task public virtual async Task 设置水平时基(double scale, CancellationToken ct = default) { - string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter); - await SendAsync(cmd, ct); - } - - /// - /// 设置示波器的触发水平延迟位置 (Horizontal Delay) - /// - /// 水平延迟位置 (单位: s) - /// 取消令牌 - public virtual async Task 设置水平延迟(double delay, CancellationToken ct = default) - { - string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter); + string cmd = string.Format(CultureInfo.InvariantCulture, ":TIMebase:SCALe {0:E6}{1}", scale, ScpiDelimiter); await SendAsync(cmd, ct); } #endregion - #region 5. Trigger 触发子系统 + #region 5. Trigger 触发设置子系统 /// - /// 设置边沿触发的电平值 (Trigger Level) + /// 设置边沿触发的触发源。 /// - /// 触发电平值 (单位: V) - /// 取消令牌 - public virtual async Task 设置触发电平(double level, CancellationToken ct = default) + /// 触发源标识符的字符串 (例如: "C1", "C2", "EX", "LINE") + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置边沿触发源(string source, CancellationToken ct = default) { - - string cmd = string.Format(CultureInfo.InvariantCulture, "TRIGger:EDGE:LEVel {0:F3}{1}", level, ScpiDelimiter); - await SendAsync(cmd, ct); + await SendAsync($":TRIGger:EDGE:SOURce {source.ToUpper()}{ScpiDelimiter}", ct); } /// - /// 设置边沿触发的触发源 (例如: C1, C2, C3, C4, EX, LINE) + /// 设置边沿触发的电平值。 /// - /// 触发源通道标识 (例如: "C1", "C2", "EX", "LINE") - /// 取消令牌 - public virtual async Task 设置触发源(string source, CancellationToken ct = default) + /// 触发电平数值 (根据通道单位,通常为 V 或 A) + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置边沿触发电平(double level, CancellationToken ct = default) { - await SendAsync($"TRSE EDGE,SR,{source.ToUpper()}{ScpiDelimiter}", ct); + string cmd = string.Format(CultureInfo.InvariantCulture, ":TRIGger:EDGE:LEVel {0:F3}{1}", level, ScpiDelimiter); + await SendAsync(cmd, ct); + } + + public enum TriggerSlope { RISing, FALLing, ALTernate } + + /// + /// 设置边沿触发的斜率类型 (测试 AC 泄放时间需使用 FALLing 下降沿)。 + /// + /// 斜率枚举类型 (RISing 上升沿, FALLing 下降沿, ALTernate 任意沿) + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置边沿触发斜率(TriggerSlope slope, CancellationToken ct = default) + { + await SendAsync($":TRIGger:EDGE:SLOPe {slope}{ScpiDelimiter}", ct); } #endregion - #region 6. Measure 自动测量参数回读 + #region 6. Cursor 光标子系统 (用于计算启动时间、泄放时间的 ΔT) /// - /// 查询指定通道自动测量项的当前实时测量数值 + /// 开启手动光标并设置为 X 轴(时间轴)测量模式。 /// - /// 通道号 (1 - 4) - /// 测量参数名称 (如 "PKPK", "FREQ", "RMS" 等) - /// 取消令牌 - /// 测量参数的实时数值 (double) - public virtual async Task 查询通道测量项参数(int channel, string paramName, CancellationToken ct = default) + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 开启手动X光标(CancellationToken ct = default) { - string query = string.Format(CultureInfo.InvariantCulture, "C{0}:PAVA? {1}{2}", channel, paramName.ToUpper(), ScpiDelimiter); - string res = await WriteReadAsync(query, ScpiDelimiter, ct); + await SendAsync($":CURSor:MODE MANual,X{ScpiDelimiter}", ct); + } + + /// + /// 设置 X1 光标的绝对位置。 + /// + /// 光标在时间轴上的位置 (单位:秒) + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置光标X1位置(double position, CancellationToken ct = default) + { + string cmd = string.Format(CultureInfo.InvariantCulture, ":CURSor:X1 {0:E6}{1}", position, ScpiDelimiter); + await SendAsync(cmd, ct); + } + + /// + /// 设置 X2 光标的绝对位置。 + /// + /// 光标在时间轴上的位置 (单位:秒) + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置光标X2位置(double position, CancellationToken ct = default) + { + string cmd = string.Format(CultureInfo.InvariantCulture, ":CURSor:X2 {0:E6}{1}", position, ScpiDelimiter); + await SendAsync(cmd, ct); + } + + /// + /// 查询光标 X2 和 X1 当前位置的时间差 (ΔT = X2 - X1)。 + /// + /// 异步取消令牌 + /// 返回 X 轴两光标之间的时间差数值 (double 类型,单位:秒) + public virtual async Task 查询光标X时间差(CancellationToken ct = default) + { + string res = await WriteReadAsync($":CURSor:XDELta?{ScpiDelimiter}", ScpiDelimiter, ct); return ExtractDouble(res); } + #endregion + + #region 7. Measure 基本测量子系统 (测试纹波PKPK、冲击电流MAX) + /// - /// 查询指定通道的电压峰峰值 (Vpp) + /// 设置基本测量系统的全局参考信源。 /// - /// 通道号 (1 - 4) - /// 取消令牌 - /// 电压峰峰值 (Vpp) - public virtual async Task 查询实际电压峰峰值(int channel, CancellationToken ct = default) + /// 作为测量对象的通道号 (1 - 4) + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置基本测量源(int channel, CancellationToken ct = default) { - return await 查询通道测量项参数(channel, "PKPK", ct); + await SendAsync($":MEASure:SIMPle:SOURce C{channel}{ScpiDelimiter}", ct); } /// - /// 查询指定通道的频率值 (Frequency) + /// 开启或关闭某项基本测量参数 (在屏幕下方显示或隐藏该测量项)。 /// - /// 通道号 (1 - 4) - /// 取消令牌 - /// 频率值 (单位: Hz) - public virtual async Task 查询实际频率(int channel, CancellationToken ct = default) + /// 要设置的测量参数名称 (如: "PKPK", "MAX", "MIN") + /// 布尔值:true 表示开启显示, false 表示关闭 + /// 异步取消令牌 + /// 表示异步操作的 Task + public virtual async Task 设置基本测量项(string parameter, bool enable, CancellationToken ct = default) { - return await 查询通道测量项参数(channel, "FREQ", ct); + string state = enable ? "ON" : "OFF"; + await SendAsync($":MEASure:SIMPle:ITEM {parameter.ToUpper()},{state}{ScpiDelimiter}", ct); } /// - /// 查询指定通道的真均方根电压值 (Vrms) + /// 查询基本测量中指定测量项的当前实时数值。 /// - /// 通道号 (1 - 4) - /// 取消令牌 - /// 真均方根电压值 (Vrms) - public virtual async Task 查询实际电压均方根(int channel, CancellationToken ct = default) + /// 要查询的测量参数名称 (如: "PKPK" 获取峰峰值, "MAX" 获取最大值) + /// 异步取消令牌 + /// 返回该测量参数提取到的实时数值 (double 类型) + public virtual async Task 查询基本测量值(string type, CancellationToken ct = default) { - return await 查询通道测量项参数(channel, "RMS", ct); + string res = await WriteReadAsync($":MEASure:SIMPle:VALue? {type.ToUpper()}{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(res); } #endregion - #region 7. 屏幕截图导出子系统 + #region 8. 屏幕截图导出子系统 + /// /// 【一键获取并保存截图】 - /// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到指定路径。 + /// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到上位机本地作测试记录。 /// - /// 本地绝对保存路径 (例如: @"D:\Oscilloscope\Screen_01.png") - /// 取消令牌 - /// 返回是否获取并保存成功 + /// 目标保存文件的绝对路径 (例如: @"D:\Oscilloscope\RippleTest.png") + /// 异步取消令牌 + /// 布尔值:true 表示成功获取并保存截图,false 表示获取或保存失败 public virtual async Task 获取屏幕图像并保存(string saveFilePath, CancellationToken ct = default) { try { - string FileDirectory = Path.GetDirectoryName(saveFilePath) ?? ""; // 获取目录 - string fileNameWithoutExt = Path.GetFileNameWithoutExtension(saveFilePath); // 获取纯文件名 - string extension = Path.GetExtension(saveFilePath); // 获取后缀名 (如 .png) - // 生成时间戳,格式为 年月日_时分秒 (例如 20260817_123045) + string FileDirectory = Path.GetDirectoryName(saveFilePath) ?? ""; + string fileNameWithoutExt = Path.GetFileNameWithoutExtension(saveFilePath); + string extension = Path.GetExtension(saveFilePath); + // 自动追加当前时间戳避免同名文件覆盖 string timeStamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - // 组合成新的路径 saveFilePath = Path.Combine(FileDirectory, $"{fileNameWithoutExt}_{timeStamp}{extension}"); - // 1. 调用我们在基类 Tcp 中全新实现的 ReadRawAsync - // 它会在内部下发 ":PRINt? PNG\n",自动接收完整的网络字节流 + + // 下发截图指令,接收纯字节流 byte[] pureImageBytes = await ReadAllBytesAsync($":PRINt? PNG{ScpiDelimiter}", ct); - // 2. 校验返回数据 - if (pureImageBytes == null || pureImageBytes.Length < 8) - { - System.Diagnostics.Debug.WriteLine("错误:接收到的图片数据为空或长度不足。"); - return false; - } + if (pureImageBytes == null || pureImageBytes.Length < 8) return false; - // 3. 核心校验:验证是否为标准的 PNG 文件格式 (PNG 头通常为 89 50 4E 47) + // 验证 PNG 格式头,以确认确实是一张合法图像 if (pureImageBytes[0] != 0x89 || pureImageBytes[1] != 'P' || pureImageBytes[2] != 'N' || pureImageBytes[3] != 'G') - { - System.Diagnostics.Debug.WriteLine("错误:接收到的二进制数据不符合标准 PNG 格式头!"); return false; - } - // 4. 如果传入的目标文件夹路径不存在,自动帮其创建 string directory = Path.GetDirectoryName(saveFilePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } - // 5. 使用文件流直接将 PNG 字节数据落地写入本地硬盘 + // 将接收到的二进制数组写入文件 using (FileStream fs = new FileStream(saveFilePath, FileMode.Create, FileAccess.Write)) { await fs.WriteAsync(pureImageBytes, 0, pureImageBytes.Length, ct); - await fs.FlushAsync(ct); // 强制刷新,确保数据完整落地 + await fs.FlushAsync(ct); } - System.Diagnostics.Debug.WriteLine($"成功:截图已保存至路径: {saveFilePath}"); return true; } - catch (Exception ex) + catch (Exception) { - System.Diagnostics.Debug.WriteLine($"崩溃:保存截图时发生未知异常: {ex.Message}"); return false; } } #endregion } -} +} \ No newline at end of file diff --git a/DeviceEditModule/ViewModels/SDS2000X_HDViewModel.cs b/DeviceEditModule/ViewModels/SDS2000X_HDViewModel.cs index 611f36a..3f391c5 100644 --- a/DeviceEditModule/ViewModels/SDS2000X_HDViewModel.cs +++ b/DeviceEditModule/ViewModels/SDS2000X_HDViewModel.cs @@ -2,7 +2,7 @@ using DeviceCommand.Devices; using Prism.Commands; using Prism.Ioc; using System; -using System.Globalization; +using System.IO; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; @@ -60,30 +60,45 @@ namespace DeviceEditModule.ViewModels set => SetProperty(ref _channel, value); } + private string _channelUnit = "V"; + /// 通道物理单位(V 电压 / A 电流)。 + public string ChannelUnit + { + get => _channelUnit; + set => SetProperty(ref _channelUnit, value); + } + + public string[] ChannelUnitOptions { get; } = { "V", "A" }; + private double _voltsPerDiv = 1.0; - /// 垂直电压档位(V/div)。 + /// 垂直档位(V/div 或 A/div)。 public double VoltsPerDiv { get => _voltsPerDiv; set => SetProperty(ref _voltsPerDiv, value); } - private double _offset = 0.0; - /// 垂直偏移(V)。 - public double Offset + private ChannelCoupling _selectedCoupling = ChannelCoupling.DC; + /// 通道耦合方式(DC/AC/GND,测纹波需 AC)。 + public ChannelCoupling SelectedCoupling { - get => _offset; - set => SetProperty(ref _offset, value); + get => _selectedCoupling; + set => SetProperty(ref _selectedCoupling, value); } - private bool _is50Ohm; - /// 是否使用 50Ω 输入阻抗,false 为 1MΩ。 - public bool Is50Ohm + public ChannelCoupling[] CouplingOptions { get; } = + (ChannelCoupling[])Enum.GetValues(typeof(ChannelCoupling)); + + private string _bandwidthLimit = "20M"; + /// 通道带宽限制(20M/200M/FULL,测纹波常用 20M)。 + public string BandwidthLimit { - get => _is50Ohm; - set => SetProperty(ref _is50Ohm, value); + get => _bandwidthLimit; + set => SetProperty(ref _bandwidthLimit, value); } + public string[] BandwidthLimitOptions { get; } = { "20M", "200M", "FULL" }; + private double _timeBase = 0.001; /// 水平时基档位(s/div)。 public double TimeBase @@ -92,20 +107,66 @@ namespace DeviceEditModule.ViewModels set => SetProperty(ref _timeBase, value); } + private TriggerMode _selectedTriggerMode = TriggerMode.AUTO; + /// 触发模式(AUTO/NORMal/SINGle)。 + public TriggerMode SelectedTriggerMode + { + get => _selectedTriggerMode; + set => SetProperty(ref _selectedTriggerMode, value); + } + + public TriggerMode[] TriggerModeOptions { get; } = + (TriggerMode[])Enum.GetValues(typeof(TriggerMode)); + + private string _triggerSource = "C1"; + /// 边沿触发源(C1~C4/EX/LINE)。 + public string TriggerSource + { + get => _triggerSource; + set => SetProperty(ref _triggerSource, value); + } + private double _triggerLevel = 0.0; - /// 触发电平(V)。 + /// 边沿触发电平(V)。 public double TriggerLevel { get => _triggerLevel; set => SetProperty(ref _triggerLevel, value); } - private string _triggerSource = "C1"; - /// 触发源(C1/C2/C3/C4/EX/LINE)。 - public string TriggerSource + private TriggerSlope _selectedTriggerSlope = TriggerSlope.RISing; + /// 边沿触发斜率(RISing/FALLing/ALTernate,测泄放时间用 FALLing)。 + public TriggerSlope SelectedTriggerSlope { - get => _triggerSource; - set => SetProperty(ref _triggerSource, value); + get => _selectedTriggerSlope; + set => SetProperty(ref _selectedTriggerSlope, value); + } + + public TriggerSlope[] TriggerSlopeOptions { get; } = + (TriggerSlope[])Enum.GetValues(typeof(TriggerSlope)); + + private double _cursorX1 = 0.0; + /// X1 光标位置(s)。 + public double CursorX1 + { + get => _cursorX1; + set => SetProperty(ref _cursorX1, value); + } + + private double _cursorX2 = 0.001; + /// X2 光标位置(s)。 + public double CursorX2 + { + get => _cursorX2; + set => SetProperty(ref _cursorX2, value); + } + + private string _saveFilePath = Path.Combine(AppContext.BaseDirectory, "Screenshots", "SDS2000X_HD.png"); + /// 屏幕截图保存路径(保存时自动追加时间戳)。 + public string SaveFilePath + { + get => _saveFilePath; + set => SetProperty(ref _saveFilePath, value); } #endregion @@ -133,6 +194,14 @@ namespace DeviceEditModule.ViewModels set => SetProperty(ref _measuredRms, value); } + private double _cursorDeltaT; + /// X1/X2 光标时间差 ΔT(s)。 + public double CursorDeltaT + { + get => _cursorDeltaT; + set => SetProperty(ref _cursorDeltaT, value); + } + private string _responseLog = string.Empty; /// 命令响应日志(最新消息在顶部)。 public string ResponseLog @@ -145,91 +214,123 @@ namespace DeviceEditModule.ViewModels #region 命令 + // IEEE 488.2 公共命令 public ICommand QueryIdentityCommand { get; } public ICommand ResetDeviceCommand { get; } + public ICommand ClearStatusCommand { get; } + public ICommand QueryErrorCommand { get; } + public ICommand QueryOpcCommand { get; } + + // 运行与捕获控制 public ICommand RunCommand { get; } public ICommand StopCommand { get; } - public ICommand SingleCommand { get; } - public ICommand ForceTriggerCommand { get; } + public ICommand SetTriggerModeCommand { get; } + + // 通道垂直控制 public ICommand SetChannelOnCommand { get; } public ICommand SetChannelOffCommand { get; } + public ICommand SetChannelUnitCommand { get; } public ICommand SetVoltsDivCommand { get; } - public ICommand SetOffsetCommand { get; } - - // 🛠️ 补全:设置阻抗的 Command - public ICommand SetImpedance50Command { get; } - public ICommand SetImpedance1MCommand { get; } + public ICommand SetChannelCouplingCommand { get; } + public ICommand SetBandwidthLimitCommand { get; } + // 水平与触发控制 public ICommand SetTimeBaseCommand { get; } - public ICommand SetTriggerLevelCommand { get; } public ICommand SetTriggerSourceCommand { get; } + public ICommand SetTriggerLevelCommand { get; } + public ICommand SetTriggerSlopeCommand { get; } + + // 光标测量 + public ICommand EnableManualXCursorCommand { get; } + public ICommand SetCursorX1Command { get; } + public ICommand SetCursorX2Command { get; } + public ICommand QueryCursorDeltaTCommand { get; } + + // 基本测量 public ICommand QueryMeasurementsCommand { get; } public ICommand QueryVppCommand { get; } public ICommand QueryFrequencyCommand { get; } public ICommand QueryRmsCommand { get; } + // 屏幕截图 + public ICommand SaveScreenshotCommand { get; } + #endregion public SDS2000X_HDViewModel(IContainerProvider containerProvider) : base(containerProvider) { _deviceManager = containerProvider.Resolve(); + // IEEE 488.2 公共命令 QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.查询设备标识(Ct())))); - ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.重置设备(Ct()); AppendLog("设备已重置"); })); - RunCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.启动捕获_RUN(Ct()); AppendLog("已开始捕获"); })); - StopCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.停止捕获_STOP(Ct()); AppendLog("已停止捕获"); })); - SingleCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.单次触发_SINGLE(Ct()); AppendLog("已触发单次捕获"); })); - ForceTriggerCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.强制触发(Ct()); AppendLog("已强制触发"); })); + ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.重置设备(Ct()); AppendLog("设备已重置 (*RST)"); })); + ClearStatusCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.清除状态(Ct()); AppendLog("状态寄存器与错误队列已清除 (*CLS)"); })); + QueryErrorCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("SYST:ERR? → " + await _device!.查询错误信息(Ct())))); + QueryOpcCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("操作完成 (*OPC?): " + (await _device!.检查操作完成_OPC(Ct()) ? "是" : "否")))); + + // 运行与捕获控制 + RunCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.启动捕获_RUN(Ct()); AppendLog("已开始连续捕获"); })); + StopCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.停止捕获_STOP(Ct()); AppendLog("已停止捕获,画面定格"); })); + SetTriggerModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置触发模式(SelectedTriggerMode, Ct()); AppendLog($"触发模式已设为 {SelectedTriggerMode}"); })); + + // 通道垂直控制 SetChannelOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道开关(Channel, true, Ct()); AppendLog($"通道 {Channel} 已开启"); })); SetChannelOffCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道开关(Channel, false, Ct()); AppendLog($"通道 {Channel} 已关闭"); })); - SetVoltsDivCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道电压档位(Channel, VoltsPerDiv, Ct()); AppendLog($"C{Channel} 电压档位已设为 {VoltsPerDiv} V/div"); })); - SetOffsetCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道垂直偏移(Channel, Offset, Ct()); AppendLog($"C{Channel} 垂直偏移已设为 {Offset} V"); })); - - // 🛠️ 绑定:设置 50Ω 和 1MΩ 阻抗控制 - SetImpedance50Command = new DelegateCommand(async () => await Exec(async () => - { - await _device!.设置通道阻抗(Channel, ImpedanceType.FIFty, Ct()); - Is50Ohm = true; - AppendLog($"C{Channel} 输入阻抗已设为 50Ω"); - })); - - SetImpedance1MCommand = new DelegateCommand(async () => await Exec(async () => - { - await _device!.设置通道阻抗(Channel, ImpedanceType.ONEM, Ct()); - Is50Ohm = false; - AppendLog($"C{Channel} 输入阻抗已设为 1MΩ"); - })); + SetChannelUnitCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道单位(Channel, ChannelUnit, Ct()); AppendLog($"C{Channel} 单位已设为 {ChannelUnit}"); })); + SetVoltsDivCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道档位(Channel, VoltsPerDiv, Ct()); AppendLog($"C{Channel} 垂直档位已设为 {VoltsPerDiv} {ChannelUnit}/div"); })); + SetChannelCouplingCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道耦合(Channel, SelectedCoupling, Ct()); AppendLog($"C{Channel} 耦合方式已设为 {SelectedCoupling}"); })); + SetBandwidthLimitCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道带宽限制(Channel, BandwidthLimit, Ct()); AppendLog($"C{Channel} 带宽限制已设为 {BandwidthLimit}"); })); + // 水平与触发控制 SetTimeBaseCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置水平时基(TimeBase, Ct()); AppendLog($"水平时基已设为 {TimeBase} s/div"); })); - SetTriggerLevelCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置触发电平(TriggerLevel, Ct()); AppendLog($"触发电平已设为 {TriggerLevel} V"); })); - SetTriggerSourceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置触发源(TriggerSource, Ct()); AppendLog($"触发源已设为 {TriggerSource}"); })); + SetTriggerSourceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置边沿触发源(TriggerSource, Ct()); AppendLog($"边沿触发源已设为 {TriggerSource}"); })); + SetTriggerLevelCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置边沿触发电平(TriggerLevel, Ct()); AppendLog($"边沿触发电平已设为 {TriggerLevel} V"); })); + SetTriggerSlopeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置边沿触发斜率(SelectedTriggerSlope, Ct()); AppendLog($"边沿触发斜率已设为 {SelectedTriggerSlope}"); })); - QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () => + // 光标测量 + EnableManualXCursorCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.开启手动X光标(Ct()); AppendLog("已开启手动 X 轴光标"); })); + SetCursorX1Command = new DelegateCommand(async () => await Exec(async () => { await _device!.设置光标X1位置(CursorX1, Ct()); AppendLog($"X1 光标已设为 {CursorX1} s"); })); + SetCursorX2Command = new DelegateCommand(async () => await Exec(async () => { await _device!.设置光标X2位置(CursorX2, Ct()); AppendLog($"X2 光标已设为 {CursorX2} s"); })); + QueryCursorDeltaTCommand = new DelegateCommand(async () => await Exec(async () => { - // 设备层已自动提取纯数值并返回 double - MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct()); - MeasuredFrequency = await _device!.查询实际频率(Channel, Ct()); - MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct()); - - AppendLog($"C{Channel} 测量结果 → Vpp:{MeasuredVpp:0.######}V Freq:{MeasuredFrequency:0.######}Hz RMS:{MeasuredRms:0.######}V"); + CursorDeltaT = await _device!.查询光标X时间差(Ct()); + AppendLog($"光标时间差 ΔT = {CursorDeltaT:0.######E+0} s"); })); + // 基本测量(查询前先将测量源切换到当前通道) QueryVppCommand = new DelegateCommand(async () => await Exec(async () => { - MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct()); - AppendLog($"C{Channel} Vpp: {MeasuredVpp:0.######} V"); + MeasuredVpp = await QuerySimpleValueAsync("PKPK"); + AppendLog($"C{Channel} Vpp: {MeasuredVpp:0.######} {ChannelUnit}"); })); QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () => { - MeasuredFrequency = await _device!.查询实际频率(Channel, Ct()); + MeasuredFrequency = await QuerySimpleValueAsync("FREQ"); AppendLog($"C{Channel} Freq: {MeasuredFrequency:0.######} Hz"); })); QueryRmsCommand = new DelegateCommand(async () => await Exec(async () => { - MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct()); - AppendLog($"C{Channel} RMS: {MeasuredRms:0.######} V"); + MeasuredRms = await QuerySimpleValueAsync("RMS"); + AppendLog($"C{Channel} RMS: {MeasuredRms:0.######} {ChannelUnit}"); + })); + + QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () => + { + await _device!.设置基本测量源(Channel, Ct()); + MeasuredVpp = await _device.查询基本测量值("PKPK", Ct()); + MeasuredFrequency = await _device.查询基本测量值("FREQ", Ct()); + MeasuredRms = await _device.查询基本测量值("RMS", Ct()); + + AppendLog($"C{Channel} 测量结果 → Vpp:{MeasuredVpp:0.######}{ChannelUnit} Freq:{MeasuredFrequency:0.######}Hz RMS:{MeasuredRms:0.######}{ChannelUnit}"); + })); + + // 屏幕截图 + SaveScreenshotCommand = new DelegateCommand(async () => await Exec(async () => + { + bool ok = await _device!.获取屏幕图像并保存(SaveFilePath, Ct()); + AppendLog(ok ? $"截图已保存:{SaveFilePath}" : "截图获取或保存失败,请检查连接与保存路径。"); })); Initialize(); @@ -283,6 +384,17 @@ namespace DeviceEditModule.ViewModels private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token; + /// + /// 将基本测量源切换到当前通道后,查询指定测量项的实时数值。 + /// + /// 测量参数名称(如: "PKPK"、"FREQ"、"RMS") + /// 测量项提取到的实时数值 + private async Task QuerySimpleValueAsync(string type) + { + await _device!.设置基本测量源(Channel, Ct()); + return await _device.查询基本测量值(type, Ct()); + } + private async Task Exec(Func action) { if (_device == null) @@ -327,4 +439,4 @@ namespace DeviceEditModule.ViewModels _cts?.Dispose(); } } -} \ No newline at end of file +} diff --git a/DeviceEditModule/Views/SDS2000X_HDView.xaml b/DeviceEditModule/Views/SDS2000X_HDView.xaml index 3478fff..634ce10 100644 --- a/DeviceEditModule/Views/SDS2000X_HDView.xaml +++ b/DeviceEditModule/Views/SDS2000X_HDView.xaml @@ -84,8 +84,8 @@ - - + @@ -103,11 +103,45 @@