From 2dfc819bcc930dee19cc2fae3c06c4c1194a3f55 Mon Sep 17 00:00:00 2001 From: hsc Date: Thu, 16 Jul 2026 14:55:14 +0800 Subject: [PATCH] =?UTF-8?q?SDS2000X=E7=A4=BA=E6=B3=A2=E5=99=A8=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DeviceCommand/Base/TCP.cs | 82 ++++++++ DeviceCommand/Devices/SDS2000X_HD.cs | 170 ++++++++++++----- .../ViewModels/SDS2000X_HDViewModel.cs | 175 ++++++++++++++---- DeviceEditModule/Views/IT7800EView.xaml | 2 +- DeviceEditModule/Views/N36200View.xaml | 2 +- DeviceEditModule/Views/N69200View.xaml | 2 +- DeviceEditModule/Views/SDS2000X_HDView.xaml | 15 +- DeviceEditModule/Views/SPAW7000View.xaml | 2 +- ZLGUSBCANFD/USBCANFD.cs | 26 +-- 9 files changed, 362 insertions(+), 114 deletions(-) diff --git a/DeviceCommand/Base/TCP.cs b/DeviceCommand/Base/TCP.cs index c064410..ab5b4fe 100644 --- a/DeviceCommand/Base/TCP.cs +++ b/DeviceCommand/Base/TCP.cs @@ -230,7 +230,89 @@ namespace DeviceCommand.Base _commLock.Release(); } } + #region 扩展:读取所有可用的二进制网络字节 (无 SCPI 块头解析) + /// + /// 无锁核心方法:发送命令并一次性读取所有回传的二进制原始数据包(不进行 # 协议头解析,专用于读取纯文件流如 PNG) + /// + private async Task LoglessReadAllBytesAsync(string queryCommand, CancellationToken ct) + { + if (!IsConnected) throw new InvalidOperationException("TCP未连接。"); + + // 1. 发送查询命令(例如 :PRINt? PNG\n) + await LoglessSendAsync(Encoding.UTF8.GetBytes(queryCommand), ct); + + NetworkStream stream = _tcpClient.GetStream(); + + // 2. 鼎阳示波器的截图数据大概在 100KB - 800KB 左右,使用动态内存流接收整个包 + using (var ms = new MemoryStream()) + { + byte[] buffer = new byte[8192]; // 8KB 缓冲区 + + try + { + // 先给设备短暂的反应时间,等待数据到达网络缓冲区 + int delayCount = 0; + while (!_tcpClient.GetStream().DataAvailable && delayCount < 50) + { + await Task.Delay(10, ct); + delayCount++; + } + + // 循环读取,直到网络流中没有更多数据 + do + { + ct.ThrowIfCancellationRequested(); + + int read = ReceiveTimeout > 0 + ? await stream.ReadAsync(buffer, 0, buffer.Length, ct) + .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct) + .ConfigureAwait(false) + : await stream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false); + + if (read == 0) break; // 远程流关闭 + + ms.Write(buffer, 0, read); + + // 如果流里没有剩余数据了,退出读取(防止 ReadAsync 在没有数据时无限阻塞等待) + if (!stream.DataAvailable) + { + // 极短延时再确认一次,防止分包网络延迟引起的“假结束” + await Task.Delay(30, ct); + if (!stream.DataAvailable) + { + break; + } + } + } while (true); + + return ms.ToArray(); + } + catch (TimeoutException ex) + { + await ResetConnectionAsync(ct); + throw new TimeoutException($"读取二进制大包超时(等待:{ReceiveTimeout} ms),链路已重置。", ex); + } + } + } + + /// + /// 【公开方法】发送命令并读取设备回传的全部原始二进制字节数组(不带协议头解析,直接返回整个字节缓冲区) + /// + public async Task ReadAllBytesAsync(string queryCommand, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + return await LoglessReadAllBytesAsync(queryCommand, ct); + } + finally + { + _commLock.Release(); + } + } + + #endregion public void Dispose() { _tcpClient?.Dispose(); diff --git a/DeviceCommand/Devices/SDS2000X_HD.cs b/DeviceCommand/Devices/SDS2000X_HD.cs index 48d1794..a9fe58b 100644 --- a/DeviceCommand/Devices/SDS2000X_HD.cs +++ b/DeviceCommand/Devices/SDS2000X_HD.cs @@ -3,6 +3,7 @@ using DeviceCommand.Base; using Model.Models; using System; using System.Globalization; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -11,12 +12,11 @@ namespace DeviceCommand.Device [ADPCommand] public class SDS2000X_HD : Tcp { - // 示波器底层 Socket 字符串命令通常以换行符 \n 结束 + // 示波器底层 Socket 字符串命令以换行符 \n 结束 private const string ScpiDelimiter = "\n"; /// /// 构造函数:传入 一次性初始化示波器通信参数。 - /// 鼎阳示波器网口 Socket 默认端口通常为 5025,请在配置中设置。 /// public SDS2000X_HD(TcpConfig config) : base(config) { @@ -49,8 +49,7 @@ namespace DeviceCommand.Device } /// - /// 【补充】查询先前操作是否完成。 - /// 在重置设备(*RST)或切换大物理量程后调用,返回 "1" 代表示波器继电器切换就绪,防止后续指令引发阻塞。 + /// 查询先前操作是否完成。 /// public virtual async Task 检查操作完成_OPC(CancellationToken ct = default) { @@ -63,65 +62,82 @@ namespace DeviceCommand.Device #region 2. 运行与捕获控制 (Run / Stop / Single) /// - /// 控制示波器开始捕获波形 (等同于按下前端面板的 Run 键) + /// 控制示波器开始捕获波形 /// public virtual async Task 启动捕获_RUN(CancellationToken ct = default) { - await SendAsync($"RUN{ScpiDelimiter}", ct); + await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct); } /// - /// 停止捕获波形 (等同于按下前端面板的 Stop 键) + /// 停止捕获波形 /// public virtual async Task 停止捕获_STOP(CancellationToken ct = default) { - await SendAsync($"STOP{ScpiDelimiter}", ct); + await SendAsync($"TRIGger:STOP{ScpiDelimiter}", ct); } /// - /// 强制示波器进入单次触发捕获模式 (常用于捕捉充电瞬间的过冲浪涌波形) + /// 强制示波器进入单次触发捕获模式 /// public virtual async Task 单次触发_SINGLE(CancellationToken ct = default) { - await SendAsync($"SINGle{ScpiDelimiter}", ct); + await SendAsync($":TRIGger:MODE SINGle{ScpiDelimiter}", ct); } /// - /// 触发一次波形采样 (当触发源设为 Manual 时使用) + /// 触发一次波形采样 /// public virtual async Task 强制触发(CancellationToken ct = default) { - await SendAsync($"*TRG{ScpiDelimiter}", ct); + await SendAsync($"::TRIGger:MODE FTRIG{ScpiDelimiter}", ct); } - /// - /// 【补充】设置触发模式 (AUTO, NORM, SINGLE) + /// 示波器触发控制模式(符合 SDS2000X-HD 规范)。 /// - public virtual async Task 设置触发模式(string mode, CancellationToken ct = default) + public enum TriggerMode { - string modeUpper = mode.ToUpper(); - if (modeUpper != "AUTO" && modeUpper != "NORM" && modeUpper != "SINGLE") - throw new ArgumentException("触发模式只能为 AUTO, NORM, 或 SINGLE"); + /// + /// 自动触发模式 (即使无触发信号也周期性刷屏) + /// + AUTO, - await SendAsync($"TRMD {modeUpper}{ScpiDelimiter}", ct); + /// + /// 普通触发模式 (仅当满足触发条件时才刷新) + /// + NORM, + + /// + /// 单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP) + /// + SINGLE + } + /// + /// 设置触发模式 (AUTO 自动, NORM 普通, SINGLE 单次) + /// + /// 触发模式枚举 + /// 取消令牌 + public virtual async Task 设置触发模式(TriggerMode mode, CancellationToken ct = default) + { + // 例如发送:TRMD AUTO\n、TRMD NORM\n 或 TRMD SINGLE\n + string cmd = $"TRMD {mode}{ScpiDelimiter}"; + await SendAsync(cmd, ct); } - #endregion #region 3. Channel 垂直控制子系统 (C1 ~ C4) /// - /// 开启或关闭指定的模拟通道 + /// 开启或关闭指定的模拟通道 (符合手册 57 页规范) /// public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default) { - // 修正:根据手册规范,使用 1/0 比 ON/OFF 在高低版本固件中兼容性更稳定 - string state = enable ? "1" : "0"; + string state = enable ? "ON" : "OFF"; await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct); } /// - /// 设置指定通道的垂直电压档位 (Volts/Div,单位: V,例如 0.05 代表 50mV/div) + /// 设置指定通道的垂直电压档位 (Volts/Div) /// public virtual async Task 设置通道电压档位(int channel, double volts, CancellationToken ct = default) { @@ -130,7 +146,7 @@ namespace DeviceCommand.Device } /// - /// 【补充验证】查询指定通道当前的电压档位 (用于 Setup-Verify 闭环验证逻辑) + /// 查询指定通道当前的电压档位 /// public virtual async Task 查询通道电压档位(int channel, CancellationToken ct = default) { @@ -138,22 +154,29 @@ namespace DeviceCommand.Device } /// - /// 设置指定通道的垂直偏移量 (Offset,单位: V) + /// 设置指定通道的垂直偏移量 (Offset) /// 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); } - /// - /// 设置通道的输入阻抗与耦合模式 + /// 示波器通道输入阻抗类型。 /// - /// 合法参数:A1M (交流1M), D1M (直流1M), D50 (直流50欧) - public virtual async Task 设置通道耦合与阻抗(int channel, string coupling, CancellationToken ct = default) + public enum ImpedanceType { - string coupUpper = coupling.ToUpper(); - await SendAsync($"C{channel}:COUPling {coupUpper}{ScpiDelimiter}", ct); + /// 50Ω 阻抗 + FIFty, + + /// 1MΩ 阻抗 + ONEM + } + 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}"; + await SendAsync(cmd, ct); } #endregion @@ -161,21 +184,19 @@ namespace DeviceCommand.Device #region 4. Timebase 水平时基子系统 /// - /// 设置示波器的水平时基档位 (Time/Div,单位: s,例如 0.001 代表 1ms/div) + /// 设置示波器的水平时基档位 (Time/Div) /// public virtual async Task 设置水平时基(double scale, CancellationToken ct = default) { - // 修正:统一使用更精简且符合手册定义的简写形式 TDIV string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter); await SendAsync(cmd, ct); } /// - /// 设置示波器的触发水平延迟位置 (Horizontal Delay,单位: s) + /// 设置示波器的触发水平延迟位置 (Horizontal Delay) /// public virtual async Task 设置水平延迟(double delay, CancellationToken ct = default) { - // 修正:采用手册标准简写 TRDL 效率更高 string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter); await SendAsync(cmd, ct); } @@ -185,12 +206,12 @@ namespace DeviceCommand.Device #region 5. Trigger 触发子系统 /// - /// 设置边沿触发的电平值 (Trigger Level,单位: V) + /// 设置边沿触发的电平值 (Trigger Level) /// public virtual async Task 设置触发电平(double level, CancellationToken ct = default) { - // 修正:采用标准简写 TRLV - string cmd = string.Format(CultureInfo.InvariantCulture, "TRLV {0:F3}{1}", level, ScpiDelimiter); + + string cmd = string.Format(CultureInfo.InvariantCulture, "TRIGger:EDGE:LEVel {0:F3}{1}", level, ScpiDelimiter); await SendAsync(cmd, ct); } @@ -199,30 +220,24 @@ namespace DeviceCommand.Device /// public virtual async Task 设置触发源(string source, CancellationToken ct = default) { - // 修正:采用标准简写 TRSE 体系配置指令 - await SendAsync($"TRIGger:SOURce {source.ToUpper()}{ScpiDelimiter}", ct); + await SendAsync($"TRSE EDGE,SR,{source.ToUpper()}{ScpiDelimiter}", ct); } #endregion - #region 6. Measure 自动测量参数回读 (高频轮询核心) + #region 6. Measure 自动测量参数回读 /// /// 查询指定通道自动测量项的当前实时测量数值 /// - /// 通道号 (1-4) - /// 参数名称助记符: - /// PKPK(峰峰值), MAX(最大值), MIN(最小值), AMPL(振幅值), - /// FREQ(频率), PER(周期), MEAN(平均值), RMS(均方根) 等 public virtual async Task 查询通道测量项参数(int channel, string paramName, CancellationToken ct = default) { - // 优化:采用更兼容的测量读取指令语法格式 string query = string.Format(CultureInfo.InvariantCulture, "C{0}:PAVA? {1}{2}", channel, paramName.ToUpper(), ScpiDelimiter); return await WriteReadAsync(query, ScpiDelimiter, ct); } /// - /// 轮询便捷接口:查询指定通道的电压峰峰值 (Vpp) + /// 查询指定通道的电压峰峰值 (Vpp) /// public virtual async Task 查询实际电压峰峰值(int channel, CancellationToken ct = default) { @@ -230,7 +245,7 @@ namespace DeviceCommand.Device } /// - /// 轮询便捷接口:查询指定通道的频率值 (Frequency) + /// 查询指定通道的频率值 (Frequency) /// public virtual async Task 查询实际频率(int channel, CancellationToken ct = default) { @@ -238,7 +253,7 @@ namespace DeviceCommand.Device } /// - /// 轮询便捷接口:查询指定通道的真均方根电压值 (Vrms) + /// 查询指定通道的真均方根电压值 (Vrms) /// public virtual async Task 查询实际电压均方根(int channel, CancellationToken ct = default) { @@ -246,5 +261,60 @@ namespace DeviceCommand.Device } #endregion + + #region 7. 屏幕截图导出子系统 + /// 【一键获取并保存截图】 + /// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到指定路径。 + /// + /// 本地绝对保存路径 (例如: @"D:\Oscilloscope\Screen_01.png") + /// 取消令牌 + /// 返回是否获取并保存成功 + public virtual async Task 获取屏幕图像并保存(string saveFilePath, CancellationToken ct = default) + { + try + { + // 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; + } + + // 3. 核心校验:验证是否为标准的 PNG 文件格式 (PNG 头通常为 89 50 4E 47) + 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); // 强制刷新,确保数据完整落地 + } + + System.Diagnostics.Debug.WriteLine($"成功:截图已保存至路径: {saveFilePath}"); + return true; + } + catch (Exception ex) + { + 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 85b8988..61f739c 100644 --- a/DeviceEditModule/ViewModels/SDS2000X_HDViewModel.cs +++ b/DeviceEditModule/ViewModels/SDS2000X_HDViewModel.cs @@ -2,10 +2,14 @@ using DeviceCommand.Device; using Prism.Commands; using Prism.Ioc; using System; +using System.Globalization; +using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using System.Windows.Input; using UIShare.GlobalVariable; using UIShare.ViewModelBase; +using static DeviceCommand.Device.SDS2000X_HD; namespace DeviceEditModule.ViewModels { @@ -142,24 +146,28 @@ namespace DeviceEditModule.ViewModels #region 命令 - public ICommand QueryIdentityCommand { get; } - public ICommand ResetDeviceCommand { get; } - public ICommand RunCommand { get; } - public ICommand StopCommand { get; } - public ICommand SingleCommand { get; } - public ICommand ForceTriggerCommand { get; } - public ICommand SetChannelOnCommand { get; } - public ICommand SetChannelOffCommand { get; } - public ICommand SetVoltsDivCommand { get; } - public ICommand SetOffsetCommand { get; } - - public ICommand SetTimeBaseCommand { get; } - public ICommand SetTriggerLevelCommand { get; } + public ICommand QueryIdentityCommand { get; } + public ICommand ResetDeviceCommand { get; } + public ICommand RunCommand { get; } + public ICommand StopCommand { get; } + public ICommand SingleCommand { get; } + public ICommand ForceTriggerCommand { get; } + public ICommand SetChannelOnCommand { get; } + public ICommand SetChannelOffCommand { get; } + public ICommand SetVoltsDivCommand { get; } + public ICommand SetOffsetCommand { get; } + + // 🛠️ 补全:设置阻抗的 Command + public ICommand SetImpedance50Command { get; } + public ICommand SetImpedance1MCommand { get; } + + public ICommand SetTimeBaseCommand { get; } + public ICommand SetTriggerLevelCommand { get; } public ICommand SetTriggerSourceCommand { get; } - public ICommand QueryMeasurementsCommand{ get; } - public ICommand QueryVppCommand { get; } - public ICommand QueryFrequencyCommand { get; } - public ICommand QueryRmsCommand { get; } + public ICommand QueryMeasurementsCommand { get; } + public ICommand QueryVppCommand { get; } + public ICommand QueryFrequencyCommand { get; } + public ICommand QueryRmsCommand { get; } #endregion @@ -167,32 +175,73 @@ namespace DeviceEditModule.ViewModels { _deviceManager = containerProvider.Resolve(); - 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("已强制触发"); })); - 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"); })); - - 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"); })); + 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("已强制触发"); })); + 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Ω"); + })); + + 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}"); })); QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () => { - MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct()); - MeasuredFrequency = await _device!.查询实际频率(Channel, Ct()); - MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct()); - AppendLog($"C{Channel} 测量 → Vpp:{MeasuredVpp} Freq:{MeasuredFrequency}Hz RMS:{MeasuredRms}V"); + // 1. 保留设备原生吐出的完整原始字符串 + string rawVpp = await _device!.查询实际电压峰峰值(Channel, Ct()); + string rawFreq = await _device!.查询实际频率(Channel, Ct()); + string rawRms = await _device!.查询实际电压均方根(Channel, Ct()); + + // 2. 清洗数据并将科学计数法转为常规小数后赋值给 UI 属性 + MeasuredVpp = ParseMeasurement(rawVpp); + MeasuredFrequency = ParseMeasurement(rawFreq); + MeasuredRms = ParseMeasurement(rawRms); + + // 3. 在日志中同时体现设备原始报文与解析呈现值,极方便联调 + AppendLog($"C{Channel} 测量原始数据 → Vpp:{rawVpp.Trim()} Freq:{rawFreq.Trim()} RMS:{rawRms.Trim()}"); + AppendLog($"C{Channel} 界面呈现数值 → Vpp:{MeasuredVpp}V Freq:{MeasuredFrequency}Hz RMS:{MeasuredRms}V"); })); - QueryVppCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct()); AppendLog($"C{Channel} Vpp: {MeasuredVpp}"); })); - QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredFrequency = await _device!.查询实际频率(Channel, Ct()); AppendLog($"C{Channel} Freq: {MeasuredFrequency}"); })); - QueryRmsCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct()); AppendLog($"C{Channel} RMS: {MeasuredRms}"); })); + QueryVppCommand = new DelegateCommand(async () => await Exec(async () => + { + string raw = await _device!.查询实际电压峰峰值(Channel, Ct()); + MeasuredVpp = ParseMeasurement(raw); + AppendLog($"C{Channel} Vpp: {MeasuredVpp} (Raw: {raw.Trim()})"); + })); + + QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () => + { + string raw = await _device!.查询实际频率(Channel, Ct()); + MeasuredFrequency = ParseMeasurement(raw); + AppendLog($"C{Channel} Freq: {MeasuredFrequency} (Raw: {raw.Trim()})"); + })); + + QueryRmsCommand = new DelegateCommand(async () => await Exec(async () => + { + string raw = await _device!.查询实际电压均方根(Channel, Ct()); + MeasuredRms = ParseMeasurement(raw); + AppendLog($"C{Channel} RMS: {MeasuredRms} (Raw: {raw.Trim()})"); + })); Initialize(); } @@ -224,8 +273,8 @@ namespace DeviceEditModule.ViewModels } } - _device = found; - DeviceName = foundName ?? "SDS2000X_HD (未找到)"; + _device = found; + DeviceName = foundName ?? "SDS2000X_HD (未找到)"; IsConnected = _device?.IsConnected ?? false; AppendLog(found != null @@ -281,6 +330,52 @@ namespace DeviceEditModule.ViewModels : line + "\n" + ResponseLog; } + /// + /// 清洗示波器原始返回的测量字符串(例如 "C1:PAVA RMS,5.57E-03V")并安全转化为无科学计数法的小数形式。 + /// + /// 设备原始应答数据 + /// 可直接绑定到 UI 呈现的字符串数字 + private string ParseMeasurement(string rawResponse) + { + if (string.IsNullOrWhiteSpace(rawResponse)) return "—"; + + try + { + string cleanData = rawResponse.Trim(); + + // 1. 斩断报头,提取逗号后面的具体内容(如 "5.57E-03V" 或 "****") + int commaIndex = cleanData.IndexOf(','); + if (commaIndex == -1) return "—"; + + string valStr = cleanData.Substring(commaIndex + 1); + + var match = Regex.Match(valStr, @"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?"); + if (match.Success) + { + valStr = match.Value; + } + + // 3. 校验并拦截设备未测出时的无效星号 "****" + if (valStr.Contains("*") || string.IsNullOrWhiteSpace(valStr)) + { + return "0"; // 回归为零或 "—",防止触发数据异常 + } + + // 4. 解析科学计数法,并重新以不带科学计数法的小数样式展开 + if (double.TryParse(valStr, NumberStyles.Any, CultureInfo.InvariantCulture, out double result)) + { + // "0.######" 样式会自动消除末尾无用的冗余零,并显示为普通小数(如 0.00557) + return result.ToString("0.######", CultureInfo.InvariantCulture); + } + } + catch + { + // 捕获可能产生的边缘转换故障,保证轮询线程绝不崩溃 + } + + return "—"; + } + #endregion public void Dispose() @@ -289,4 +384,4 @@ namespace DeviceEditModule.ViewModels _cts?.Dispose(); } } -} +} \ No newline at end of file diff --git a/DeviceEditModule/Views/IT7800EView.xaml b/DeviceEditModule/Views/IT7800EView.xaml index 48a9778..6ada2d6 100644 --- a/DeviceEditModule/Views/IT7800EView.xaml +++ b/DeviceEditModule/Views/IT7800EView.xaml @@ -7,7 +7,7 @@ xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" mc:Ignorable="d" - prism:ViewModelLocator.AutoWireViewModel="True" + prism:ViewModelLocator.AutoWireViewModel="False" d:DesignHeight="760" d:DesignWidth="860"> diff --git a/DeviceEditModule/Views/N36200View.xaml b/DeviceEditModule/Views/N36200View.xaml index 9185c2e..90052f0 100644 --- a/DeviceEditModule/Views/N36200View.xaml +++ b/DeviceEditModule/Views/N36200View.xaml @@ -7,7 +7,7 @@ xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" mc:Ignorable="d" - prism:ViewModelLocator.AutoWireViewModel="True" + prism:ViewModelLocator.AutoWireViewModel="False" d:DesignHeight="760" d:DesignWidth="860"> diff --git a/DeviceEditModule/Views/N69200View.xaml b/DeviceEditModule/Views/N69200View.xaml index c4134e7..30a2145 100644 --- a/DeviceEditModule/Views/N69200View.xaml +++ b/DeviceEditModule/Views/N69200View.xaml @@ -7,7 +7,7 @@ xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" mc:Ignorable="d" - prism:ViewModelLocator.AutoWireViewModel="True" + prism:ViewModelLocator.AutoWireViewModel="False" d:DesignHeight="760" d:DesignWidth="860"> diff --git a/DeviceEditModule/Views/SDS2000X_HDView.xaml b/DeviceEditModule/Views/SDS2000X_HDView.xaml index 0201463..3478fff 100644 --- a/DeviceEditModule/Views/SDS2000X_HDView.xaml +++ b/DeviceEditModule/Views/SDS2000X_HDView.xaml @@ -4,10 +4,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:prism="http://prismlibrary.com/" + xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" mc:Ignorable="d" - prism:ViewModelLocator.AutoWireViewModel="True" + prism:ViewModelLocator.AutoWireViewModel="False" d:DesignHeight="760" d:DesignWidth="860"> @@ -92,10 +93,10 @@ - - - - + 1 + 2 + 3 + 4