using Common.Attributes; using DeviceCommand.Base; using Model.Models; using System; using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace DeviceCommand.Devices { /// /// 示波器 MSO 4/5/6 系列 /// #region 枚举定义 /// /// Tektronix MSO 采集模式 /// public enum TekAcquisitionMode { /// 采样模式 SAMple, /// 峰值检测模式 PEAKdetect, /// 高分辨率模式 HIRes, /// 平均模式 AVErage, /// 包络模式 ENVelope } /// /// Tektronix MSO 测量类型 (部分常用) /// public enum TekMeasurementType { /// 幅度 AMPLitude, /// 频率 FREQuency, /// 平均值 MEAN, /// 峰峰值 PK2PK, /// 最大值 MAXimum, /// 最小值 MINimum } #endregion /// /// Tektronix 示波器 (MSO 4/5/6 系列),支持采集控制、通道设置、自动测量与截图导出 /// [ACPCommand] public class TektronixMSO : Tcp { // SCPI 指令结束符 (消息终止符需使用 LF) private const string ScpiDelimiter = "\n"; /// /// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。 /// 支持科学计数法(如 2.48E-03),结果保留两位小数。 /// /// 设备返回的原始字符串 /// 提取到的数值(保留两位小数),解析失败时返回 0 private static double ExtractDouble(string raw) { if (string.IsNullOrWhiteSpace(raw)) return 0; // 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头 var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?"); return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val) ? Math.Round(val, 2) : 0; } #region 兼容旧 MSO44B 枚举(供 ACP 测试项反序列化) public enum AcquireMode { Sample = 0, PeakDetect = 1, HiRes = 2, Average = 3, Envelope = 4 } public enum BandwidthLimit { Full = 0, BW20MHz = 1 } public enum Channel { Channel1 = 1, Channel2 = 2, Channel3 = 3, Channel4 = 4 } public enum CouplingMode { DC = 0, AC = 1, GND = 2 } public enum MeasurementType { Amplitude = 1, Frequency = 2, Mean = 3, PK2PK = 4, Maximum = 5, Minimum = 6, RMS = 7, Overshoot = 8, Undershoot = 9, PositiveOvershoot = 10, NegativeOvershoot = 11, RiseTime = 12, FallTime = 13, DutyCycle = 14, Period = 15, Phase = 16, Delay = 17, BurstWidth = 18, SlewRateRising = 19, SlewRateFalling = 20, High = 21 } public enum StopAfter { Sequence = 0, RunStop = 1 } public enum TriggerMode { Auto = 0, Normal = 1 } public enum TriggerSlope { Rising = 0, Falling = 1 } #endregion public TektronixMSO(TcpConfig config) : base(config) { } #region 1. 公共命令与状态查询 /// /// 查询仪器识别码 (*IDN? 指令) /// /// 取消令牌 /// 设备标识字符串 (制造商, 型号, 序列号, 固件版本) public virtual async Task 查询设备标识(CancellationToken ct = default) { return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); } /// /// 查询仪器是否处于忙碌状态,常用于同步操作 (BUSY? 指令) /// /// 取消令牌 /// 忙碌状态字符串 (0: 空闲, 1: 忙碌) public virtual async Task 查询忙碌状态(CancellationToken ct = default) { return await WriteReadAsync($"BUSY?{ScpiDelimiter}", ScpiDelimiter, ct); } /// /// 指示仪器执行信号路径校准 (SPC) (*CAL? 指令) /// /// 取消令牌 public virtual async Task 执行自校准(CancellationToken ct = default) { await SendAsync($"*CAL?{ScpiDelimiter}", ct); } #endregion #region 2. 采集系统控制 (Acquisition) /// /// 启动或停止采集 (ACQuire:STATE 指令) /// /// true: 启动采集 (RUN), false: 停止采集 (STOP) /// 取消令牌 public virtual async Task 设置采集状态(bool 运行, CancellationToken ct = default) { string 参数 = 运行 ? "RUN" : "STOP"; await SendAsync($"ACQuire:STATE {参数}{ScpiDelimiter}", ct); } /// /// 查询当前采集状态 (ACQuire:STATE? 指令) /// /// 取消令牌 /// 采集状态字符串 (RUN / STOP) public virtual async Task 查询采集状态(CancellationToken ct = default) { return await WriteReadAsync($"ACQuire:STATE?{ScpiDelimiter}", ScpiDelimiter, ct); } /// /// 设置采集模式 (ACQuire:MODe 指令) /// /// 采集模式枚举 (采样/峰值检测/高分辨率/平均/包络) /// 取消令牌 public virtual async Task 设置采集模式(TekAcquisitionMode 模式, CancellationToken ct = default) { await SendAsync($"ACQuire:MODe {模式}{ScpiDelimiter}", ct); } #endregion #region 3. 水平系统控制 (Horizontal) /// /// 设置水平刻度 (Scale),即每格代表的时间 (HORizontal:SCAle 指令) /// /// 水平刻度值 (单位: s/Div) /// 取消令牌 public virtual async Task 设置水平刻度(double 秒每格, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, "HORizontal:SCAle {0:E}{1}", 秒每格, ScpiDelimiter); await SendAsync(cmd, ct); } /// /// 设置水平记录长度 (HORizontal:RECOrdlength 指令) /// /// 记录长度(采样点数) /// 取消令牌 public virtual async Task 设置记录长度(long 长度, CancellationToken ct = default) { await SendAsync($"HORizontal:RECOrdlength {长度}{ScpiDelimiter}", ct); } /// /// 查询水平采样率 (HORizontal:SAMPLERate? 指令) /// /// 取消令牌 /// 水平采样率 (单位: Sa/s,保留两位小数) public virtual async Task 查询采样率(CancellationToken ct = default) { string resp = await WriteReadAsync($"HORizontal:SAMPLERate?{ScpiDelimiter}", ScpiDelimiter, ct); return ExtractDouble(resp); } #endregion #region 4. 通道与显示控制 (Vertical) /// /// 全局启用或关闭某通道的显示 (DISplay:GLObal:CH:STATE 指令) /// /// 通道编号 (1 - 4) /// true: 开启通道显示, false: 关闭通道显示 /// 取消令牌 public virtual async Task 设置通道显示开关(int 通道号, bool 开启, CancellationToken ct = default) { string 参数 = 开启 ? "ON" : "OFF"; await SendAsync($"DISplay:GLObal:CH{通道号}:STATE {参数}{ScpiDelimiter}", ct); } /// /// 设置指定通道的垂直缩放比例 (CH:SCAle 指令) /// /// 通道编号 (1 - 4) /// 垂直刻度值 (单位: V/Div) /// 取消令牌 public virtual async Task 设置通道垂直刻度(int 通道号, double 伏特每格, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, "CH{0}:SCAle {1:E}{2}", 通道号, 伏特每格, ScpiDelimiter); await SendAsync(cmd, ct); } /// /// 设置指定通道的垂直位置 (CH:POSition 指令) /// /// 通道编号 (1 - 4) /// 垂直位置偏移 (单位: Div) /// 取消令牌 public virtual async Task 设置通道垂直位置(int 通道号, double 格数, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, "CH{0}:POSition {1:F2}{2}", 通道号, 格数, ScpiDelimiter); await SendAsync(cmd, ct); } #endregion #region 5. 自动测量 (Measurement) /// /// 动态添加并开启一个测量项 (MEASUrement:ADDNew 指令)。 /// 4/5/6 Series MSO 必须先动态创建测量项 /// /// 测量项编号 /// 取消令牌 public virtual async Task 开启测量项(int 测量项编号, CancellationToken ct = default) { await SendAsync($"MEASUrement:ADDNew \"MEAS{测量项编号}\"{ScpiDelimiter}", ct); } /// /// 设置测量项的测量类型 (MEASUrement:TYPe 指令) /// /// 测量项编号 /// 测量类型枚举 (幅度/频率/平均值/峰峰值等) /// 取消令牌 public virtual async Task 设置测量类型(int 测量项编号, TekMeasurementType 类型, CancellationToken ct = default) { await SendAsync($"MEASUrement:MEAS{测量项编号}:TYPe {类型}{ScpiDelimiter}", ct); } /// /// 设置测量项的测量源 (MEASUrement:SOUrce1 指令) /// /// 测量项编号 /// 测量源名称 (如 "CH1", "CH2") /// 取消令牌 public virtual async Task 设置测量源(int 测量项编号, string 源名称, CancellationToken ct = default) { await SendAsync($"MEASUrement:MEAS{测量项编号}:SOUrce1 {源名称}{ScpiDelimiter}", ct); } /// /// 查询测量项的当前采集平均值 (MEASUrement:RESUlts:CURRentacq:MEAN? 指令) /// /// 测量项编号 /// 取消令牌 /// 测量项当前采集的平均值 (保留两位小数) [Monitorable("测量项的当前采集平均值")] public virtual async Task 查询测量项当前值(int 测量项编号, CancellationToken ct = default) { string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:CURRentacq:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct); return ExtractDouble(resp); } /// /// 查询测量项的全局历史累积平均值 (MEASUrement:RESUlts:ALLAcqs:MEAN? 指令) /// /// 测量项编号 /// 取消令牌 /// 测量项全局平均值 (保留两位小数) public virtual async Task 查询测量项全局平均值(int 测量项编号, CancellationToken ct = default) { string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct); return ExtractDouble(resp); } /// /// 查询测量项的全局历史累积最大值 (MEASUrement:RESUlts:ALLAcqs:MAXimum? 指令) /// /// 测量项编号 /// 取消令牌 /// 测量项全局最大值 (保留两位小数) public virtual async Task 查询测量项全局最大值(int 测量项编号, CancellationToken ct = default) { string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct); return ExtractDouble(resp); } #endregion #region 6. 屏幕截图与文件系统 (Hard Copy & File System) /// /// 捕获示波器屏幕并保存到上位机本地路径 /// /// 保存到电脑本地的文件路径 (例如: @"C:\temp\screen.png") /// 异步取消令牌 public virtual async Task 保存屏幕截图(string 上位机文件路径, CancellationToken ct = default) { string 临时文件 = "E:/temp1.png"; // 1. 直接保存截图到示波器U盘 (自动识别 .png 扩展名) await SendAsync($"SAVe:IMAGe \"{临时文件}\"{ScpiDelimiter}", ct); // 2. 发送 *OPC? 并等待返回 1,确保图像文件已经完全写入磁盘 await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct); // 3. 将文件内容读取到当前通信接口 await SendAsync($"FILESystem:READFile \"{临时文件}\"{ScpiDelimiter}", ct); // 4. 提取底层的二进制块 (Block Data) byte[] imageBytes = await ReadBinaryBlockAsync(ct); // 5. 将提取的二进制字节流写入上位机本地磁盘 await File.WriteAllBytesAsync(上位机文件路径, imageBytes, ct); // 6. 删除示波器上的临时文件,保持设备存储干净 await SendAsync($"FILESystem:DELEte \"{临时文件}\"{ScpiDelimiter}", ct); } /// /// ⚠️ 请在这里将您的网络流对接起来! /// 依据您 Tcp 基类的设计,返回用于读取底层字节流的 Stream。 /// protected virtual Stream GetNetworkStream() { // 【修改提示】: // 如果您的基类暴露了 TcpClient,请取消注释并使用这一行: // return this.TcpClient.GetStream(); // 如果您的基类直接叫 _stream 或 Stream,请返回它: // return this.Stream; // 假如不知道怎么填,请查看 Tcp 类的源码。 throw new NotImplementedException("请在此方法中返回基类的 NetworkStream 或 Socket 流对象。"); } /// /// 辅助方法:读取 IEEE 488.2 标准的二进制块数据 (Block Data) /// protected virtual async Task ReadBinaryBlockAsync(CancellationToken ct = default) { var stream = GetNetworkStream(); byte[] singleByte = new byte[1]; // 1. 逐字节读取,直到读到起始符 '#' while (true) { int read = await stream.ReadAsync(singleByte, 0, 1, ct); if (read == 0) throw new EndOfStreamException("通信中断:未找到二进制块起始符 '#'"); if (singleByte[0] == (byte)'#') break; } // 2. 读取 1 个字符 ,它代表接下来有多少个字符表示数据长度 await stream.ReadAsync(singleByte, 0, 1, ct); char nzDigChar = (char)singleByte[0]; if (!char.IsDigit(nzDigChar)) throw new InvalidDataException($"无效的块头标识: {nzDigChar}"); int lengthBytesCount = int.Parse(nzDigChar.ToString()); // 3. 读取 所指示的长度字符,将其解析为整数 N byte[] lengthBytes = new byte[lengthBytesCount]; int currentRead = 0; while (currentRead < lengthBytesCount) { int read = await stream.ReadAsync(lengthBytes, currentRead, lengthBytesCount - currentRead, ct); if (read == 0) throw new EndOfStreamException("通信中断:读取数据长度失败"); currentRead += read; } int dataLength = int.Parse(Encoding.ASCII.GetString(lengthBytes)); // 4. 从流中精确读取 N 个字节 ...,这就是完整的图像数据 byte[] imageData = new byte[dataLength]; int totalBytesRead = 0; while (totalBytesRead < dataLength) { int read = await stream.ReadAsync(imageData, totalBytesRead, dataLength - totalBytesRead, ct); if (read == 0) throw new EndOfStreamException("通信中断:读取图像数据不完整"); totalBytesRead += read; } // 5. 结尾可能包含 (例如 LF \n),将其读取并丢弃以清空缓冲区 try { await stream.ReadAsync(singleByte, 0, 1, ct); } catch { /* 忽略末尾符读取的可能超时 */ } return imageData; } #endregion #region 7. 状态与错误队列 (Status and Error) /// /// 清除所有的状态寄存器和错误/事件队列 (*CLS 指令) /// /// 取消令牌 public virtual async Task 清除错误队列Async(CancellationToken ct = default) { await SendAsync($"*CLS{ScpiDelimiter}", ct); } /// /// 查询错误队列中当前的事件/错误数量 (EVQty? 指令) /// /// 取消令牌 /// 事件队列中的事件数量字符串 public virtual async Task 查询错误数量Async(CancellationToken ct = default) { return await WriteReadAsync($"EVQty?{ScpiDelimiter}", ScpiDelimiter, ct); } /// /// 从错误队列中读取最早的一个事件/错误(包含代码和详细消息),并将其从队列中弹出 (EVMsg? 指令) /// /// 取消令牌 /// 事件代码和消息字符串 public virtual async Task 读取单条错误消息Async(CancellationToken ct = default) { return await WriteReadAsync($"EVMsg?{ScpiDelimiter}", ScpiDelimiter, ct); } /// /// 读取并清空队列中的所有错误和事件消息 (ALLEv? 指令) /// /// 取消令牌 /// 所有事件及其消息字符串 public virtual async Task 读取所有错误消息Async(CancellationToken ct = default) { return await WriteReadAsync($"ALLEv?{ScpiDelimiter}", ScpiDelimiter, ct); } #endregion } }