Files
ADP/DeviceCommand/Devices/SDS2000X_HD.cs
T
2026-09-16 14:36:28 +08:00

431 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Common.Attributes;
using DeviceCommand.Base;
using Model.Models;
using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Devices
{
[ADPCommand]
public class SDS2000X_HD : Tcp
{
// 示波器底层 Socket 字符串命令以换行符 \n 结束
private const string ScpiDelimiter = "\n";
/// <summary>
/// 构造函数:一次性初始化示波器通信参数。
/// </summary>
/// <param name="config">TCP 通信配置对象,包含 IP 地址和端口号等信息</param>
public SDS2000X_HD(TcpConfig config) : base(config)
{
}
/// <summary>
/// 从示波器返回的 SCPI 响应字符串中提取数值部分并转换为 double。
/// 响应格式示例: "PKPK,2.48E-03V" → 按逗号分割后提取 2.48E-03
/// </summary>
/// <param name="raw">示波器返回的原始字符串</param>
/// <returns>提取到的数值,解析失败或字符串为空时返回 0</returns>
private static double ExtractDouble(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return 0;
string dataPart = raw;
// 按逗号分割,取最后一段(数值通常在逗号后面)
int commaIdx = raw.LastIndexOf(',');
if (commaIdx >= 0 && commaIdx < raw.Length - 1)
dataPart = raw.Substring(commaIdx + 1);
// 从数据段中提取数值(含正负号、小数点、科学计数法)
var match = Regex.Match(dataPart, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
? val
: 0;
}
#region 1. IEEE 488.2 公共命令
/// <summary>
/// 清除标准事件状态寄存器和错误队列 (*CLS)。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 清除状态(CancellationToken ct = default)
{
await SendAsync($"*CLS{ScpiDelimiter}", ct);
}
/// <summary>
/// 查询错误信息。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>示波器返回的错误代码和描述信息的字符串</returns>
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
{
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct);
}
/// <summary>
/// 读取示波器识别字符串(制造商、型号、序列号、固件版本)。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>设备标识字符串(如 "Siglent Technologies,SDS2004X HD,..."</returns>
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
{
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
}
/// <summary>
/// 复位命令 (*RST)。使示波器恢复到默认的出厂设置。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 重置设备(CancellationToken ct = default)
{
await SendAsync($"*RST{ScpiDelimiter}", ct);
}
/// <summary>
/// 查询先前操作是否完成 (*OPC?)。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>布尔值:true 表示所有挂起的操作已完成,false 表示操作仍在执行中</returns>
public virtual async Task<bool> 检查操作完成_OPC(CancellationToken ct = default)
{
string res = await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
return res.Trim() == "1";
}
#endregion
#region 2. 运行与捕获控制 (Run / Stop / Trigger Mode)
/// <summary>
/// 控制示波器开始连续捕获波形。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 启动捕获_RUN(CancellationToken ct = default)
{
await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct);
}
/// <summary>
/// 停止捕获波形,画面将定格在当前捕捉的帧。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 停止捕获_STOP(CancellationToken ct = default)
{
await SendAsync($":TRIGger:STOP{ScpiDelimiter}", ct);
}
public enum TriggerMode
{
/// <summary>自动触发模式 (即使无触发信号也周期性刷屏)</summary>
AUTO,
/// <summary>普通触发模式 (仅当满足触发条件时才刷新)</summary>
NORMal,
/// <summary>单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP)</summary>
SINGle
}
/// <summary>
/// 设置触发模式。
/// </summary>
/// <param name="mode">触发模式枚举 (AUTO 自动, NORMal 普通, SINGle 单次)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置触发模式(TriggerMode mode, CancellationToken ct = default)
{
string cmd = $":TRIGger:MODE {mode}{ScpiDelimiter}";
await SendAsync(cmd, ct);
}
#endregion
#region 3. Channel 垂直控制子系统 (电压/电流纹波测试依赖此项)
/// <summary>
/// 开启或关闭指定的模拟通道。
/// </summary>
/// <param name="channel">通道号 (1 - 4)</param>
/// <param name="enable">布尔值:true 表示开启通道, false 表示关闭通道</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default)
{
string state = enable ? "ON" : "OFF";
await SendAsync($":CHANnel{channel}:SWITch {state}{ScpiDelimiter}", ct);
}
/// <summary>
/// 设置指定通道的物理单位,用于区分测电压还是测电流,影响测量结果读数。
/// </summary>
/// <param name="channel">通道号 (1 - 4)</param>
/// <param name="unit">字符串:"V" 代表电压,"A" 代表电流</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置通道单位(int channel, string unit, CancellationToken ct = default)
{
await SendAsync($":CHANnel{channel}:UNIT {unit.ToUpper()}{ScpiDelimiter}", ct);
}
/// <summary>
/// 设置指定通道的垂直档位 (Volts/Div 或 Amps/Div)。
/// </summary>
/// <param name="channel">通道号 (1 - 4)</param>
/// <param name="value">档位数值 (例如 0.05 表示 50mV/div 或 50mA/div)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
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 }
/// <summary>
/// 设置通道耦合方式 (测纹波时必须设置为 AC 交流耦合)。
/// </summary>
/// <param name="channel">通道号 (1 - 4)</param>
/// <param name="coupling">耦合方式枚举 (DC, AC, GND)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置通道耦合(int channel, ChannelCoupling coupling, CancellationToken ct = default)
{
await SendAsync($":CHANnel{channel}:COUPling {coupling}{ScpiDelimiter}", ct);
}
/// <summary>
/// 设置通道带宽限制 (测纹波时通常设置为 20M 带宽限制以滤除高频噪声)。
/// </summary>
/// <param name="channel">通道号 (1 - 4)</param>
/// <param name="limit">带宽限制参数的字符串 (可选: "20M", "200M", "FULL")</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置通道带宽限制(int channel, string limit, CancellationToken ct = default)
{
await SendAsync($":CHANnel{channel}:BWLimit {limit.ToUpper()}{ScpiDelimiter}", ct);
}
/// <summary>
/// 设置通道的垂直偏移量,用于调整波形在屏幕上的垂直位置。
/// 泄放测试中需要设置合适的偏移使衰减波形完整显示在屏幕内。
/// </summary>
/// <param name="channel">通道号 (1 - 4)</param>
/// <param name="offset">垂直偏移值 (单位:V 或 A,正值向上移动,负值向下移动)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置通道垂直偏移(int channel, double offset, CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":CHANnel{0}:OFFSet {1:F3}{2}", channel, offset, ScpiDelimiter);
await SendAsync(cmd, ct);
}
#endregion
#region 4. Timebase 水平时基子系统
/// <summary>
/// 设置示波器的水平时基档位 (Time/Div)。
/// </summary>
/// <param name="scale">水平时基数值 (单位:秒/格,如 1.00E-03 表示 1ms/div)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置水平时基(double scale, CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":TIMebase:SCALe {0:E6}{1}", scale, ScpiDelimiter);
await SendAsync(cmd, ct);
}
#endregion
#region 5. Trigger 触发设置子系统
/// <summary>
/// 设置边沿触发的触发源。
/// </summary>
/// <param name="source">触发源标识符的字符串 (例如: "C1", "C2", "EX", "LINE")</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置边沿触发源(string source, CancellationToken ct = default)
{
await SendAsync($":TRIGger:EDGE:SOURce {source.ToUpper()}{ScpiDelimiter}", ct);
}
/// <summary>
/// 设置边沿触发的电平值。
/// </summary>
/// <param name="level">触发电平数值 (根据通道单位,通常为 V 或 A)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置边沿触发电平(double level, CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":TRIGger:EDGE:LEVel {0:F3}{1}", level, ScpiDelimiter);
await SendAsync(cmd, ct);
}
public enum TriggerSlope { RISing, FALLing, ALTernate }
/// <summary>
/// 设置边沿触发的斜率类型 (测试 AC 泄放时间需使用 FALLing 下降沿)。
/// </summary>
/// <param name="slope">斜率枚举类型 (RISing 上升沿, FALLing 下降沿, ALTernate 任意沿)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置边沿触发斜率(TriggerSlope slope, CancellationToken ct = default)
{
await SendAsync($":TRIGger:EDGE:SLOPe {slope}{ScpiDelimiter}", ct);
}
#endregion
#region 6. Cursor 光标子系统 (用于计算启动时间、泄放时间的 ΔT)
/// <summary>
/// 开启手动光标并设置为 X 轴(时间轴)测量模式。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 开启手动X光标(CancellationToken ct = default)
{
await SendAsync($":CURSor:MODE MANual,X{ScpiDelimiter}", ct);
}
/// <summary>
/// 设置 X1 光标的绝对位置。
/// </summary>
/// <param name="position">光标在时间轴上的位置 (单位:秒)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
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);
}
/// <summary>
/// 设置 X2 光标的绝对位置。
/// </summary>
/// <param name="position">光标在时间轴上的位置 (单位:秒)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
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);
}
/// <summary>
/// 查询光标 X2 和 X1 当前位置的时间差 (ΔT = X2 - X1)。
/// </summary>
/// <param name="ct">异步取消令牌</param>
/// <returns>返回 X 轴两光标之间的时间差数值 (double 类型,单位:秒)</returns>
public virtual async Task<double> 查询光标X时间差(CancellationToken ct = default)
{
string res = await WriteReadAsync($":CURSor:XDELta?{ScpiDelimiter}", ScpiDelimiter, ct);
return ExtractDouble(res);
}
#endregion
#region 7. Measure 基本测量子系统 (测试纹波PKPK、冲击电流MAX)
/// <summary>
/// 设置基本测量系统的全局参考信源。
/// </summary>
/// <param name="channel">作为测量对象的通道号 (1 - 4)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置基本测量源(int channel, CancellationToken ct = default)
{
await SendAsync($":MEASure:SIMPle:SOURce C{channel}{ScpiDelimiter}", ct);
}
/// <summary>
/// 开启或关闭某项基本测量参数 (在屏幕下方显示或隐藏该测量项)。
/// </summary>
/// <param name="parameter">要设置的测量参数名称 (如: "PKPK", "MAX", "MIN")</param>
/// <param name="enable">布尔值:true 表示开启显示, false 表示关闭</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>表示异步操作的 Task</returns>
public virtual async Task 设置基本测量项(string parameter, bool enable, CancellationToken ct = default)
{
string state = enable ? "ON" : "OFF";
await SendAsync($":MEASure:SIMPle:ITEM {parameter.ToUpper()},{state}{ScpiDelimiter}", ct);
}
/// <summary>
/// 查询基本测量中指定测量项的当前实时数值。
/// </summary>
/// <param name="type">要查询的测量参数名称 (如: "PKPK" 获取峰峰值, "MAX" 获取最大值)</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>返回该测量参数提取到的实时数值 (double 类型)</returns>
public virtual async Task<double> 查询基本测量值(string type, CancellationToken ct = default)
{
string res = await WriteReadAsync($":MEASure:SIMPle:VALue? {type.ToUpper()}{ScpiDelimiter}", ScpiDelimiter, ct);
return ExtractDouble(res);
}
#endregion
#region 8. 屏幕截图导出子系统
/// <summary>
/// 【一键获取并保存截图】
/// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到上位机本地作测试记录。
/// </summary>
/// <param name="saveFilePath">目标保存文件的绝对路径 (例如: @"D:\Oscilloscope\RippleTest.png")</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>布尔值:true 表示成功获取并保存截图,false 表示获取或保存失败</returns>
public virtual async Task<bool> 获取屏幕图像并保存(string saveFilePath, CancellationToken ct = default)
{
try
{
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}");
// 下发截图指令,接收纯字节流
byte[] pureImageBytes = await ReadAllBytesAsync($":PRINt? PNG{ScpiDelimiter}", ct);
if (pureImageBytes == null || pureImageBytes.Length < 8) return false;
// 验证 PNG 格式头,以确认确实是一张合法图像
if (pureImageBytes[0] != 0x89 || pureImageBytes[1] != 'P' || pureImageBytes[2] != 'N' || pureImageBytes[3] != 'G')
return false;
string directory = Path.GetDirectoryName(saveFilePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 将接收到的二进制数组写入文件
using (FileStream fs = new FileStream(saveFilePath, FileMode.Create, FileAccess.Write))
{
await fs.WriteAsync(pureImageBytes, 0, pureImageBytes.Length, ct);
await fs.FlushAsync(ct);
}
return true;
}
catch (Exception)
{
return false;
}
}
#endregion
}
}