Compare commits
4
Commits
b1a160d159
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0050cb341b | ||
|
|
96680fd4c6 | ||
|
|
432ed2a70c | ||
|
|
186eec4e75 |
@@ -0,0 +1,47 @@
|
|||||||
|
using Common.Attributes;
|
||||||
|
using Common.Tools;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Command
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 步骤运行耗时统计命令。
|
||||||
|
/// <para>
|
||||||
|
/// 命令库为纯 .NET 类库,不直接引用宿主的步骤管理器类型;
|
||||||
|
/// 实际的步骤数据查询能力由宿主(StepRunning)在执行测试步骤前通过
|
||||||
|
/// <see cref="Common.Tools.StepRuntimeContext.StepTimeQuery"/> 注入(依赖倒置),
|
||||||
|
/// 多台架并发运行时各作用域的查询器随异步流隔离,互不串扰。
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[ADPCommand]
|
||||||
|
public static class CommandRunningTime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 计算测试步骤运行时间之和:统计步骤管理器主程序中,
|
||||||
|
/// 序号从起始序号到结束序号(含两端)范围内所有步骤的运行耗时总和。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="起始序号">起始步骤序号(从 1 开始)</param>
|
||||||
|
/// <param name="结束序号">结束步骤序号(含本身)</param>
|
||||||
|
/// <returns>范围内步骤运行时间之和(毫秒);序号超界、范围内无步骤或查询异常时输出 0</returns>
|
||||||
|
public static double 获取步骤运行时间之和(int 起始序号, int 结束序号)
|
||||||
|
{
|
||||||
|
// 读取当前异步执行流注入的步骤耗时查询器(由 StepRunning 设置)
|
||||||
|
var query = StepRuntimeContext.StepTimeQuery.Value;
|
||||||
|
if (query == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[步骤耗时] 宿主未注入步骤耗时查询器,无法计算序号 {起始序号}~{结束序号} 的运行时间之和");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return query(起始序号, 结束序号);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[步骤耗时] 计算序号 {起始序号}~{结束序号} 的运行时间之和失败: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Common.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 步骤运行时上下文桥接器(命令库与宿主之间的依赖倒置通道)。
|
||||||
|
/// <para>
|
||||||
|
/// 命令库(Command 项目)不引用宿主类型,无法直接访问步骤管理器的数据;
|
||||||
|
/// 宿主(StepRunning)在执行测试步骤前,将当前作用域的步骤耗时查询委托写入
|
||||||
|
/// <see cref="StepTimeQuery"/>;命令库中的耗时统计命令在执行时读取该委托完成查询。
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// 使用 <see cref="AsyncLocal{T}"/> 承载:委托随异步执行流(ExecutionContext)流动,
|
||||||
|
/// 多台架并发运行时各自持有自己作用域的查询器,天然隔离互不串扰。
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static class StepRuntimeContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 当前异步执行流的步骤耗时查询委托:(起始序号, 结束序号) → 该范围内步骤运行时间之和(毫秒)。
|
||||||
|
/// 由宿主(StepRunning)在每次执行测试步骤方法前设置;未设置时命令侧按未注入处理。
|
||||||
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
|
public static readonly AsyncLocal<Func<int, int, double>?> StepTimeQuery = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using DeviceCommand.Base;
|
|||||||
using Model.Models;
|
using Model.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -17,21 +17,22 @@ namespace DeviceCommand.Devices
|
|||||||
private const string ScpiDelimiter = "\n";
|
private const string ScpiDelimiter = "\n";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造函数:传入 <see cref="TcpConfig"/> 一次性初始化示波器通信参数。
|
/// 构造函数:一次性初始化示波器通信参数。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="config">TCP 通信配置对象,包含 IP 地址和端口号等信息</param>
|
||||||
public SDS2000X_HD(TcpConfig config) : base(config)
|
public SDS2000X_HD(TcpConfig config) : base(config)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从示波器返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
/// 从示波器返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||||
/// 响应格式示例: "C1:PAVA PKPK,2.48E-03V" → 按逗号分割后提取 2.48E-03
|
/// 响应格式示例: "PKPK,2.48E-03V" → 按逗号分割后提取 2.48E-03
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="raw">示波器返回的原始字符串</param>
|
/// <param name="raw">示波器返回的原始字符串</param>
|
||||||
/// <returns>提取到的数值,解析失败时返回 0</returns>
|
/// <returns>提取到的数值,解析失败或字符串为空时返回 0</returns>
|
||||||
private static double ExtractDouble(string raw)
|
private static double ExtractDouble(string raw)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||||
|
|
||||||
string dataPart = raw;
|
string dataPart = raw;
|
||||||
// 按逗号分割,取最后一段(数值通常在逗号后面)
|
// 按逗号分割,取最后一段(数值通常在逗号后面)
|
||||||
@@ -49,46 +50,50 @@ namespace DeviceCommand.Devices
|
|||||||
#region 1. IEEE 488.2 公共命令
|
#region 1. IEEE 488.2 公共命令
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清除标准事件状态寄存器和错误队列
|
/// 清除标准事件状态寄存器和错误队列 (*CLS)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
public virtual async Task 清除状态(CancellationToken ct = default)
|
public virtual async Task 清除状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询错误信息
|
/// 查询错误信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
/// <returns>错误代码和描述信息</returns>
|
/// <returns>示波器返回的错误代码和描述信息的字符串</returns>
|
||||||
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取示波器识别字符串(制造商、型号、序列号、固件版本)
|
/// 读取示波器识别字符串(制造商、型号、序列号、固件版本)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
/// <returns>设备标识字符串(制造商, 型号, 序列号, 固件版本)</returns>
|
/// <returns>设备标识字符串(如 "Siglent Technologies,SDS2004X HD,...")</returns>
|
||||||
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 复位命令。使示波器恢复到默认的出厂设置
|
/// 复位命令 (*RST)。使示波器恢复到默认的出厂设置。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
public virtual async Task 重置设备(CancellationToken ct = default)
|
public virtual async Task 重置设备(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询先前操作是否完成。
|
/// 查询先前操作是否完成 (*OPC?)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
/// <returns>true: 操作已完成, false: 操作仍在执行中</returns>
|
/// <returns>布尔值:true 表示所有挂起的操作已完成,false 表示操作仍在执行中</returns>
|
||||||
public virtual async Task<bool> 检查操作完成_OPC(CancellationToken ct = default)
|
public virtual async Task<bool> 检查操作完成_OPC(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string res = await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
string res = await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -97,147 +102,129 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 2. 运行与捕获控制 (Run / Stop / Single)
|
#region 2. 运行与捕获控制 (Run / Stop / Trigger Mode)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 控制示波器开始捕获波形
|
/// 控制示波器开始连续捕获波形。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
public virtual async Task 启动捕获_RUN(CancellationToken ct = default)
|
public virtual async Task 启动捕获_RUN(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct);
|
await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 停止捕获波形
|
/// 停止捕获波形,画面将定格在当前捕捉的帧。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
public virtual async Task 停止捕获_STOP(CancellationToken ct = default)
|
public virtual async Task 停止捕获_STOP(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"TRIGger:STOP{ScpiDelimiter}", ct);
|
await SendAsync($":TRIGger:STOP{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 强制示波器进入单次触发捕获模式
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ct">取消令牌</param>
|
|
||||||
public virtual async Task 单次触发_SINGLE(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
await SendAsync($":TRIGger:MODE SINGle{ScpiDelimiter}", ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 触发一次波形采样
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ct">取消令牌</param>
|
|
||||||
public virtual async Task 强制触发(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
await SendAsync($"::TRIGger:MODE FTRIG{ScpiDelimiter}", ct);
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// 示波器触发控制模式(符合 SDS2000X-HD 规范)。
|
|
||||||
/// </summary>
|
|
||||||
public enum TriggerMode
|
public enum TriggerMode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>自动触发模式 (即使无触发信号也周期性刷屏)</summary>
|
||||||
/// 自动触发模式 (即使无触发信号也周期性刷屏)
|
|
||||||
/// </summary>
|
|
||||||
AUTO,
|
AUTO,
|
||||||
|
/// <summary>普通触发模式 (仅当满足触发条件时才刷新)</summary>
|
||||||
/// <summary>
|
NORMal,
|
||||||
/// 普通触发模式 (仅当满足触发条件时才刷新)
|
/// <summary>单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP)</summary>
|
||||||
/// </summary>
|
SINGle
|
||||||
NORM,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP)
|
|
||||||
/// </summary>
|
|
||||||
SINGLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置触发模式 (AUTO 自动, NORM 普通, SINGLE 单次)
|
/// 设置触发模式。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mode">触发模式枚举</param>
|
/// <param name="mode">触发模式枚举 (AUTO 自动, NORMal 普通, SINGle 单次)</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
public virtual async Task 设置触发模式(TriggerMode mode, CancellationToken ct = default)
|
public virtual async Task 设置触发模式(TriggerMode mode, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 例如发送:TRMD AUTO\n、TRMD NORM\n 或 TRMD SINGLE\n
|
string cmd = $":TRIGger:MODE {mode}{ScpiDelimiter}";
|
||||||
string cmd = $"TRMD {mode}{ScpiDelimiter}";
|
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 3. Channel 垂直控制子系统 (C1 ~ C4)
|
#region 3. Channel 垂直控制子系统 (电压/电流纹波测试依赖此项)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 开启或关闭指定的模拟通道 (符合手册 57 页规范)
|
/// 开启或关闭指定的模拟通道。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
/// <param name="enable">true: 开启通道, false: 关闭通道</param>
|
/// <param name="enable">布尔值:true 表示开启通道, false 表示关闭通道</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default)
|
public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string state = enable ? "ON" : "OFF";
|
string state = enable ? "ON" : "OFF";
|
||||||
await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct);
|
await SendAsync($":CHANnel{channel}:SWITch {state}{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定通道的垂直电压档位 (Volts/Div)
|
/// 设置指定通道的物理单位,用于区分测电压还是测电流,影响测量结果读数。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
/// <param name="volts">电压档位 (Volts/Div)</param>
|
/// <param name="unit">字符串:"V" 代表电压,"A" 代表电流</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
public virtual async Task 设置通道电压档位(int channel, double volts, CancellationToken ct = default)
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ChannelCoupling { DC, AC, GND }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道当前的电压档位
|
/// 设置通道耦合方式 (测纹波时必须设置为 AC 交流耦合)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="coupling">耦合方式枚举 (DC, AC, GND)</param>
|
||||||
/// <returns>当前电压档位值 (Volts/Div)</returns>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
public virtual async Task<double> 查询通道电压档位(int channel, CancellationToken ct = default)
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
|
public virtual async Task 设置通道耦合(int channel, ChannelCoupling coupling, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string res = await WriteReadAsync($"C{channel}:VDIV?{ScpiDelimiter}", ScpiDelimiter, ct);
|
await SendAsync($":CHANnel{channel}:COUPling {coupling}{ScpiDelimiter}", ct);
|
||||||
return ExtractDouble(res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定通道的垂直偏移量 (Offset)
|
/// 设置通道带宽限制 (测纹波时通常设置为 20M 带宽限制以滤除高频噪声)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
/// <param name="offset">垂直偏移量 (单位: V)</param>
|
/// <param name="limit">带宽限制参数的字符串 (可选: "20M", "200M", "FULL")</param>
|
||||||
/// <param name="ct">取消令牌</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)
|
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);
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":CHANnel{0}:OFFSet {1:F3}{2}", channel, offset, ScpiDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// 示波器通道输入阻抗类型。
|
|
||||||
/// </summary>
|
|
||||||
public enum ImpedanceType
|
|
||||||
{
|
|
||||||
/// <summary>50Ω 阻抗</summary>
|
|
||||||
FIFty,
|
|
||||||
|
|
||||||
/// <summary>1MΩ 阻抗</summary>
|
|
||||||
ONEM
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设置指定通道的输入阻抗类型
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
|
||||||
/// <param name="impedance">阻抗类型枚举 (FIFty: 50Ω, ONEM: 1MΩ)</param>
|
|
||||||
/// <param name="ct">取消令牌</param>
|
|
||||||
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,168 +233,199 @@ namespace DeviceCommand.Devices
|
|||||||
#region 4. Timebase 水平时基子系统
|
#region 4. Timebase 水平时基子系统
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置示波器的水平时基档位 (Time/Div)
|
/// 设置示波器的水平时基档位 (Time/Div)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="scale">水平时基档位 (Time/Div)</param>
|
/// <param name="scale">水平时基数值 (单位:秒/格,如 1.00E-03 表示 1ms/div)</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
public virtual async Task 设置水平时基(double scale, CancellationToken ct = default)
|
public virtual async Task 设置水平时基(double scale, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":TIMebase:SCALe {0:E6}{1}", scale, ScpiDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设置示波器的触发水平延迟位置 (Horizontal Delay)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="delay">水平延迟位置 (单位: s)</param>
|
|
||||||
/// <param name="ct">取消令牌</param>
|
|
||||||
public virtual async Task 设置水平延迟(double delay, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter);
|
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 5. Trigger 触发子系统
|
#region 5. Trigger 触发设置子系统
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置边沿触发的电平值 (Trigger Level)
|
/// 设置边沿触发的触发源。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="level">触发电平值 (单位: V)</param>
|
/// <param name="source">触发源标识符的字符串 (例如: "C1", "C2", "EX", "LINE")</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
public virtual async Task 设置触发电平(double level, CancellationToken ct = default)
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
|
public virtual async Task 设置边沿触发源(string source, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
await SendAsync($":TRIGger:EDGE:SOURce {source.ToUpper()}{ScpiDelimiter}", ct);
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "TRIGger:EDGE:LEVel {0:F3}{1}", level, ScpiDelimiter);
|
|
||||||
await SendAsync(cmd, ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置边沿触发的触发源 (例如: C1, C2, C3, C4, EX, LINE)
|
/// 设置边沿触发的电平值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="source">触发源通道标识 (例如: "C1", "C2", "EX", "LINE")</param>
|
/// <param name="level">触发电平数值 (根据通道单位,通常为 V 或 A)</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
public virtual async Task 设置触发源(string source, CancellationToken ct = default)
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
|
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 }
|
||||||
|
|
||||||
|
/// <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
|
#endregion
|
||||||
|
|
||||||
#region 6. Measure 自动测量参数回读
|
#region 6. Cursor 光标子系统 (用于计算启动时间、泄放时间的 ΔT)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道自动测量项的当前实时测量数值
|
/// 开启手动光标并设置为 X 轴(时间轴)测量模式。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
/// <param name="paramName">测量参数名称 (如 "PKPK", "FREQ", "RMS" 等)</param>
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
/// <param name="ct">取消令牌</param>
|
public virtual async Task 开启手动X光标(CancellationToken ct = default)
|
||||||
/// <returns>测量参数的实时数值 (double)</returns>
|
|
||||||
public virtual async Task<double> 查询通道测量项参数(int channel, string paramName, CancellationToken ct = default)
|
|
||||||
{
|
{
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, "C{0}:PAVA? {1}{2}", channel, paramName.ToUpper(), ScpiDelimiter);
|
await SendAsync($":CURSor:MODE MANual,X{ScpiDelimiter}", ct);
|
||||||
string res = await WriteReadAsync(query, 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);
|
return ExtractDouble(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 7. Measure 基本测量子系统 (测试纹波PKPK、冲击电流MAX)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的电压峰峰值 (Vpp)
|
/// 设置基本测量系统的全局参考信源。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
/// <param name="channel">作为测量对象的通道号 (1 - 4)</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
/// <returns>电压峰峰值 (Vpp)</returns>
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
public virtual async Task<double> 查询实际电压峰峰值(int channel, CancellationToken ct = default)
|
public virtual async Task 设置基本测量源(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await 查询通道测量项参数(channel, "PKPK", ct);
|
await SendAsync($":MEASure:SIMPle:SOURce C{channel}{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的频率值 (Frequency)
|
/// 开启或关闭某项基本测量参数 (在屏幕下方显示或隐藏该测量项)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
/// <param name="parameter">要设置的测量参数名称 (如: "PKPK", "MAX", "MIN")</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="enable">布尔值:true 表示开启显示, false 表示关闭</param>
|
||||||
/// <returns>频率值 (单位: Hz)</returns>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
public virtual async Task<double> 查询实际频率(int channel, CancellationToken ct = default)
|
/// <returns>表示异步操作的 Task</returns>
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的真均方根电压值 (Vrms)
|
/// 查询基本测量中指定测量项的当前实时数值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1 - 4)</param>
|
/// <param name="type">要查询的测量参数名称 (如: "PKPK" 获取峰峰值, "MAX" 获取最大值)</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
/// <returns>真均方根电压值 (Vrms)</returns>
|
/// <returns>返回该测量参数提取到的实时数值 (double 类型)</returns>
|
||||||
public virtual async Task<double> 查询实际电压均方根(int channel, CancellationToken ct = default)
|
public virtual async Task<double> 查询基本测量值(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
|
#endregion
|
||||||
|
|
||||||
#region 7. 屏幕截图导出子系统
|
#region 8. 屏幕截图导出子系统
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 【一键获取并保存截图】
|
/// 【一键获取并保存截图】
|
||||||
/// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到指定路径。
|
/// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到上位机本地作测试记录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="saveFilePath">本地绝对保存路径 (例如: @"D:\Oscilloscope\Screen_01.png")</param>
|
/// <param name="saveFilePath">目标保存文件的绝对路径 (例如: @"D:\Oscilloscope\RippleTest.png")</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
/// <returns>返回是否获取并保存成功</returns>
|
/// <returns>布尔值:true 表示成功获取并保存截图,false 表示获取或保存失败</returns>
|
||||||
public virtual async Task<bool> 获取屏幕图像并保存(string saveFilePath, CancellationToken ct = default)
|
public virtual async Task<bool> 获取屏幕图像并保存(string saveFilePath, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string FileDirectory = Path.GetDirectoryName(saveFilePath) ?? ""; // 获取目录
|
string FileDirectory = Path.GetDirectoryName(saveFilePath) ?? "";
|
||||||
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(saveFilePath); // 获取纯文件名
|
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(saveFilePath);
|
||||||
string extension = Path.GetExtension(saveFilePath); // 获取后缀名 (如 .png)
|
string extension = Path.GetExtension(saveFilePath);
|
||||||
// 生成时间戳,格式为 年月日_时分秒 (例如 20260817_123045)
|
// 自动追加当前时间戳避免同名文件覆盖
|
||||||
string timeStamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
string timeStamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
|
|
||||||
// 组合成新的路径
|
|
||||||
saveFilePath = Path.Combine(FileDirectory, $"{fileNameWithoutExt}_{timeStamp}{extension}");
|
saveFilePath = Path.Combine(FileDirectory, $"{fileNameWithoutExt}_{timeStamp}{extension}");
|
||||||
// 1. 调用我们在基类 Tcp 中全新实现的 ReadRawAsync
|
|
||||||
// 它会在内部下发 ":PRINt? PNG\n",自动接收完整的网络字节流
|
// 下发截图指令,接收纯字节流
|
||||||
byte[] pureImageBytes = await ReadAllBytesAsync($":PRINt? PNG{ScpiDelimiter}", ct);
|
byte[] pureImageBytes = await ReadAllBytesAsync($":PRINt? PNG{ScpiDelimiter}", ct);
|
||||||
|
|
||||||
// 2. 校验返回数据
|
if (pureImageBytes == null || pureImageBytes.Length < 8) return false;
|
||||||
if (pureImageBytes == null || pureImageBytes.Length < 8)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine("错误:接收到的图片数据为空或长度不足。");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 核心校验:验证是否为标准的 PNG 文件格式 (PNG 头通常为 89 50 4E 47)
|
// 验证 PNG 格式头,以确认确实是一张合法图像
|
||||||
if (pureImageBytes[0] != 0x89 || pureImageBytes[1] != 'P' || pureImageBytes[2] != 'N' || pureImageBytes[3] != 'G')
|
if (pureImageBytes[0] != 0x89 || pureImageBytes[1] != 'P' || pureImageBytes[2] != 'N' || pureImageBytes[3] != 'G')
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine("错误:接收到的二进制数据不符合标准 PNG 格式头!");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 如果传入的目标文件夹路径不存在,自动帮其创建
|
|
||||||
string directory = Path.GetDirectoryName(saveFilePath);
|
string directory = Path.GetDirectoryName(saveFilePath);
|
||||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 使用文件流直接将 PNG 字节数据落地写入本地硬盘
|
// 将接收到的二进制数组写入文件
|
||||||
using (FileStream fs = new FileStream(saveFilePath, FileMode.Create, FileAccess.Write))
|
using (FileStream fs = new FileStream(saveFilePath, FileMode.Create, FileAccess.Write))
|
||||||
{
|
{
|
||||||
await fs.WriteAsync(pureImageBytes, 0, pureImageBytes.Length, ct);
|
await fs.WriteAsync(pureImageBytes, 0, pureImageBytes.Length, ct);
|
||||||
await fs.FlushAsync(ct); // 强制刷新,确保数据完整落地
|
await fs.FlushAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.Diagnostics.Debug.WriteLine($"成功:截图已保存至路径: {saveFilePath}");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine($"崩溃:保存截图时发生未知异常: {ex.Message}");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ using DeviceCommand.Devices;
|
|||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
@@ -60,30 +60,45 @@ namespace DeviceEditModule.ViewModels
|
|||||||
set => SetProperty(ref _channel, value);
|
set => SetProperty(ref _channel, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string _channelUnit = "V";
|
||||||
|
/// <summary>通道物理单位(V 电压 / A 电流)。</summary>
|
||||||
|
public string ChannelUnit
|
||||||
|
{
|
||||||
|
get => _channelUnit;
|
||||||
|
set => SetProperty(ref _channelUnit, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] ChannelUnitOptions { get; } = { "V", "A" };
|
||||||
|
|
||||||
private double _voltsPerDiv = 1.0;
|
private double _voltsPerDiv = 1.0;
|
||||||
/// <summary>垂直电压档位(V/div)。</summary>
|
/// <summary>垂直档位(V/div 或 A/div)。</summary>
|
||||||
public double VoltsPerDiv
|
public double VoltsPerDiv
|
||||||
{
|
{
|
||||||
get => _voltsPerDiv;
|
get => _voltsPerDiv;
|
||||||
set => SetProperty(ref _voltsPerDiv, value);
|
set => SetProperty(ref _voltsPerDiv, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double _offset = 0.0;
|
private ChannelCoupling _selectedCoupling = ChannelCoupling.DC;
|
||||||
/// <summary>垂直偏移(V)。</summary>
|
/// <summary>通道耦合方式(DC/AC/GND,测纹波需 AC)。</summary>
|
||||||
public double Offset
|
public ChannelCoupling SelectedCoupling
|
||||||
{
|
{
|
||||||
get => _offset;
|
get => _selectedCoupling;
|
||||||
set => SetProperty(ref _offset, value);
|
set => SetProperty(ref _selectedCoupling, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _is50Ohm;
|
public ChannelCoupling[] CouplingOptions { get; } =
|
||||||
/// <summary>是否使用 50Ω 输入阻抗,false 为 1MΩ。</summary>
|
(ChannelCoupling[])Enum.GetValues(typeof(ChannelCoupling));
|
||||||
public bool Is50Ohm
|
|
||||||
|
private string _bandwidthLimit = "20M";
|
||||||
|
/// <summary>通道带宽限制(20M/200M/FULL,测纹波常用 20M)。</summary>
|
||||||
|
public string BandwidthLimit
|
||||||
{
|
{
|
||||||
get => _is50Ohm;
|
get => _bandwidthLimit;
|
||||||
set => SetProperty(ref _is50Ohm, value);
|
set => SetProperty(ref _bandwidthLimit, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string[] BandwidthLimitOptions { get; } = { "20M", "200M", "FULL" };
|
||||||
|
|
||||||
private double _timeBase = 0.001;
|
private double _timeBase = 0.001;
|
||||||
/// <summary>水平时基档位(s/div)。</summary>
|
/// <summary>水平时基档位(s/div)。</summary>
|
||||||
public double TimeBase
|
public double TimeBase
|
||||||
@@ -92,20 +107,66 @@ namespace DeviceEditModule.ViewModels
|
|||||||
set => SetProperty(ref _timeBase, value);
|
set => SetProperty(ref _timeBase, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TriggerMode _selectedTriggerMode = TriggerMode.AUTO;
|
||||||
|
/// <summary>触发模式(AUTO/NORMal/SINGle)。</summary>
|
||||||
|
public TriggerMode SelectedTriggerMode
|
||||||
|
{
|
||||||
|
get => _selectedTriggerMode;
|
||||||
|
set => SetProperty(ref _selectedTriggerMode, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TriggerMode[] TriggerModeOptions { get; } =
|
||||||
|
(TriggerMode[])Enum.GetValues(typeof(TriggerMode));
|
||||||
|
|
||||||
|
private string _triggerSource = "C1";
|
||||||
|
/// <summary>边沿触发源(C1~C4/EX/LINE)。</summary>
|
||||||
|
public string TriggerSource
|
||||||
|
{
|
||||||
|
get => _triggerSource;
|
||||||
|
set => SetProperty(ref _triggerSource, value);
|
||||||
|
}
|
||||||
|
|
||||||
private double _triggerLevel = 0.0;
|
private double _triggerLevel = 0.0;
|
||||||
/// <summary>触发电平(V)。</summary>
|
/// <summary>边沿触发电平(V)。</summary>
|
||||||
public double TriggerLevel
|
public double TriggerLevel
|
||||||
{
|
{
|
||||||
get => _triggerLevel;
|
get => _triggerLevel;
|
||||||
set => SetProperty(ref _triggerLevel, value);
|
set => SetProperty(ref _triggerLevel, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _triggerSource = "C1";
|
private TriggerSlope _selectedTriggerSlope = TriggerSlope.RISing;
|
||||||
/// <summary>触发源(C1/C2/C3/C4/EX/LINE)。</summary>
|
/// <summary>边沿触发斜率(RISing/FALLing/ALTernate,测泄放时间用 FALLing)。</summary>
|
||||||
public string TriggerSource
|
public TriggerSlope SelectedTriggerSlope
|
||||||
{
|
{
|
||||||
get => _triggerSource;
|
get => _selectedTriggerSlope;
|
||||||
set => SetProperty(ref _triggerSource, value);
|
set => SetProperty(ref _selectedTriggerSlope, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TriggerSlope[] TriggerSlopeOptions { get; } =
|
||||||
|
(TriggerSlope[])Enum.GetValues(typeof(TriggerSlope));
|
||||||
|
|
||||||
|
private double _cursorX1 = 0.0;
|
||||||
|
/// <summary>X1 光标位置(s)。</summary>
|
||||||
|
public double CursorX1
|
||||||
|
{
|
||||||
|
get => _cursorX1;
|
||||||
|
set => SetProperty(ref _cursorX1, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _cursorX2 = 0.001;
|
||||||
|
/// <summary>X2 光标位置(s)。</summary>
|
||||||
|
public double CursorX2
|
||||||
|
{
|
||||||
|
get => _cursorX2;
|
||||||
|
set => SetProperty(ref _cursorX2, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _saveFilePath = Path.Combine(AppContext.BaseDirectory, "Screenshots", "SDS2000X_HD.png");
|
||||||
|
/// <summary>屏幕截图保存路径(保存时自动追加时间戳)。</summary>
|
||||||
|
public string SaveFilePath
|
||||||
|
{
|
||||||
|
get => _saveFilePath;
|
||||||
|
set => SetProperty(ref _saveFilePath, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -133,6 +194,14 @@ namespace DeviceEditModule.ViewModels
|
|||||||
set => SetProperty(ref _measuredRms, value);
|
set => SetProperty(ref _measuredRms, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double _cursorDeltaT;
|
||||||
|
/// <summary>X1/X2 光标时间差 ΔT(s)。</summary>
|
||||||
|
public double CursorDeltaT
|
||||||
|
{
|
||||||
|
get => _cursorDeltaT;
|
||||||
|
set => SetProperty(ref _cursorDeltaT, value);
|
||||||
|
}
|
||||||
|
|
||||||
private string _responseLog = string.Empty;
|
private string _responseLog = string.Empty;
|
||||||
/// <summary>命令响应日志(最新消息在顶部)。</summary>
|
/// <summary>命令响应日志(最新消息在顶部)。</summary>
|
||||||
public string ResponseLog
|
public string ResponseLog
|
||||||
@@ -145,91 +214,123 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
|
|
||||||
|
// IEEE 488.2 公共命令
|
||||||
public ICommand QueryIdentityCommand { get; }
|
public ICommand QueryIdentityCommand { get; }
|
||||||
public ICommand ResetDeviceCommand { get; }
|
public ICommand ResetDeviceCommand { get; }
|
||||||
|
public ICommand ClearStatusCommand { get; }
|
||||||
|
public ICommand QueryErrorCommand { get; }
|
||||||
|
public ICommand QueryOpcCommand { get; }
|
||||||
|
|
||||||
|
// 运行与捕获控制
|
||||||
public ICommand RunCommand { get; }
|
public ICommand RunCommand { get; }
|
||||||
public ICommand StopCommand { get; }
|
public ICommand StopCommand { get; }
|
||||||
public ICommand SingleCommand { get; }
|
public ICommand SetTriggerModeCommand { get; }
|
||||||
public ICommand ForceTriggerCommand { get; }
|
|
||||||
|
// 通道垂直控制
|
||||||
public ICommand SetChannelOnCommand { get; }
|
public ICommand SetChannelOnCommand { get; }
|
||||||
public ICommand SetChannelOffCommand { get; }
|
public ICommand SetChannelOffCommand { get; }
|
||||||
|
public ICommand SetChannelUnitCommand { get; }
|
||||||
public ICommand SetVoltsDivCommand { get; }
|
public ICommand SetVoltsDivCommand { get; }
|
||||||
public ICommand SetOffsetCommand { get; }
|
public ICommand SetChannelCouplingCommand { get; }
|
||||||
|
public ICommand SetBandwidthLimitCommand { get; }
|
||||||
// 🛠️ 补全:设置阻抗的 Command
|
|
||||||
public ICommand SetImpedance50Command { get; }
|
|
||||||
public ICommand SetImpedance1MCommand { get; }
|
|
||||||
|
|
||||||
|
// 水平与触发控制
|
||||||
public ICommand SetTimeBaseCommand { get; }
|
public ICommand SetTimeBaseCommand { get; }
|
||||||
public ICommand SetTriggerLevelCommand { get; }
|
|
||||||
public ICommand SetTriggerSourceCommand { 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 QueryMeasurementsCommand { get; }
|
||||||
public ICommand QueryVppCommand { get; }
|
public ICommand QueryVppCommand { get; }
|
||||||
public ICommand QueryFrequencyCommand { get; }
|
public ICommand QueryFrequencyCommand { get; }
|
||||||
public ICommand QueryRmsCommand { get; }
|
public ICommand QueryRmsCommand { get; }
|
||||||
|
|
||||||
|
// 屏幕截图
|
||||||
|
public ICommand SaveScreenshotCommand { get; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public SDS2000X_HDViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
public SDS2000X_HDViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||||
{
|
{
|
||||||
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
||||||
|
|
||||||
|
// IEEE 488.2 公共命令
|
||||||
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.查询设备标识(Ct()))));
|
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.查询设备标识(Ct()))));
|
||||||
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.重置设备(Ct()); AppendLog("设备已重置"); }));
|
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.重置设备(Ct()); AppendLog("设备已重置 (*RST)"); }));
|
||||||
RunCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.启动捕获_RUN(Ct()); AppendLog("已开始捕获"); }));
|
ClearStatusCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.清除状态(Ct()); AppendLog("状态寄存器与错误队列已清除 (*CLS)"); }));
|
||||||
StopCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.停止捕获_STOP(Ct()); AppendLog("已停止捕获"); }));
|
QueryErrorCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("SYST:ERR? → " + await _device!.查询错误信息(Ct()))));
|
||||||
SingleCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.单次触发_SINGLE(Ct()); AppendLog("已触发单次捕获"); }));
|
QueryOpcCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("操作完成 (*OPC?): " + (await _device!.检查操作完成_OPC(Ct()) ? "是" : "否"))));
|
||||||
ForceTriggerCommand = 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("已停止捕获,画面定格"); }));
|
||||||
|
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} 已开启"); }));
|
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} 已关闭"); }));
|
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"); }));
|
SetChannelUnitCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道单位(Channel, ChannelUnit, Ct()); AppendLog($"C{Channel} 单位已设为 {ChannelUnit}"); }));
|
||||||
SetOffsetCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道垂直偏移(Channel, Offset, Ct()); AppendLog($"C{Channel} 垂直偏移已设为 {Offset} V"); }));
|
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}"); }));
|
||||||
// 🛠️ 绑定:设置 50Ω 和 1MΩ 阻抗控制
|
SetBandwidthLimitCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道带宽限制(Channel, BandwidthLimit, Ct()); AppendLog($"C{Channel} 带宽限制已设为 {BandwidthLimit}"); }));
|
||||||
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"); }));
|
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
|
CursorDeltaT = await _device!.查询光标X时间差(Ct());
|
||||||
MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct());
|
AppendLog($"光标时间差 ΔT = {CursorDeltaT:0.######E+0} s");
|
||||||
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");
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 基本测量(查询前先将测量源切换到当前通道)
|
||||||
QueryVppCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryVppCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct());
|
MeasuredVpp = await QuerySimpleValueAsync("PKPK");
|
||||||
AppendLog($"C{Channel} Vpp: {MeasuredVpp:0.######} V");
|
AppendLog($"C{Channel} Vpp: {MeasuredVpp:0.######} {ChannelUnit}");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
MeasuredFrequency = await _device!.查询实际频率(Channel, Ct());
|
MeasuredFrequency = await QuerySimpleValueAsync("FREQ");
|
||||||
AppendLog($"C{Channel} Freq: {MeasuredFrequency:0.######} Hz");
|
AppendLog($"C{Channel} Freq: {MeasuredFrequency:0.######} Hz");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
QueryRmsCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryRmsCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct());
|
MeasuredRms = await QuerySimpleValueAsync("RMS");
|
||||||
AppendLog($"C{Channel} RMS: {MeasuredRms:0.######} V");
|
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();
|
Initialize();
|
||||||
@@ -283,6 +384,17 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将基本测量源切换到当前通道后,查询指定测量项的实时数值。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">测量参数名称(如: "PKPK"、"FREQ"、"RMS")</param>
|
||||||
|
/// <returns>测量项提取到的实时数值</returns>
|
||||||
|
private async Task<double> QuerySimpleValueAsync(string type)
|
||||||
|
{
|
||||||
|
await _device!.设置基本测量源(Channel, Ct());
|
||||||
|
return await _device.查询基本测量值(type, Ct());
|
||||||
|
}
|
||||||
|
|
||||||
private async Task Exec(Func<Task> action)
|
private async Task Exec(Func<Task> action)
|
||||||
{
|
{
|
||||||
if (_device == null)
|
if (_device == null)
|
||||||
@@ -327,4 +439,4 @@ namespace DeviceEditModule.ViewModels
|
|||||||
_cts?.Dispose();
|
_cts?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,8 +84,8 @@
|
|||||||
<!-- ─── 左列 ─── -->
|
<!-- ─── 左列 ─── -->
|
||||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||||
|
|
||||||
<!-- 通道选择 -->
|
<!-- 通道与垂直设置 -->
|
||||||
<GroupBox Header="通道选择" Margin="0,0,0,8"
|
<GroupBox Header="通道与垂直设置" Margin="0,0,0,8"
|
||||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
<StackPanel Margin="4,4,4,4">
|
<StackPanel Margin="4,4,4,4">
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
@@ -103,11 +103,45 @@
|
|||||||
<Button Content="关闭" Command="{Binding SetChannelOffCommand}"
|
<Button Content="关闭" Command="{Binding SetChannelOffCommand}"
|
||||||
Style="{StaticResource WarnBtn}"/>
|
Style="{StaticResource WarnBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="单位" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<ComboBox Width="70" Height="32" Margin="4,0"
|
||||||
|
ItemsSource="{Binding ChannelUnitOptions}"
|
||||||
|
SelectedItem="{Binding ChannelUnit}"
|
||||||
|
VerticalContentAlignment="Center" FontSize="12"/>
|
||||||
|
<Button Content="设置" Command="{Binding SetChannelUnitCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="档位 (/div)" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<TextBox Style="{StaticResource NumInput}"
|
||||||
|
Text="{Binding VoltsPerDiv}"/>
|
||||||
|
<Button Content="设置" Command="{Binding SetVoltsDivCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="耦合方式" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<ComboBox Width="80" Height="32" Margin="4,0"
|
||||||
|
ItemsSource="{Binding CouplingOptions}"
|
||||||
|
SelectedItem="{Binding SelectedCoupling}"
|
||||||
|
VerticalContentAlignment="Center" FontSize="12"/>
|
||||||
|
<Button Content="设置" Command="{Binding SetChannelCouplingCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="带宽限制" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<ComboBox Width="90" Height="32" Margin="4,0"
|
||||||
|
ItemsSource="{Binding BandwidthLimitOptions}"
|
||||||
|
SelectedItem="{Binding BandwidthLimit}"
|
||||||
|
VerticalContentAlignment="Center" FontSize="12"/>
|
||||||
|
<Button Content="设置" Command="{Binding SetBandwidthLimitCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
<!-- 运行控制 -->
|
<!-- 运行与捕获 -->
|
||||||
<GroupBox Header="运行控制" Margin="0,0,0,8"
|
<GroupBox Header="运行与捕获" Margin="0,0,0,8"
|
||||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
<StackPanel Margin="4,4,4,4">
|
<StackPanel Margin="4,4,4,4">
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
@@ -117,49 +151,20 @@
|
|||||||
Height="32" Padding="16,0" FontSize="12" Margin="4,0"/>
|
Height="32" Padding="16,0" FontSize="12" Margin="4,0"/>
|
||||||
<Button Content="Stop" Command="{Binding StopCommand}"
|
<Button Content="Stop" Command="{Binding StopCommand}"
|
||||||
Style="{StaticResource WarnBtn}"/>
|
Style="{StaticResource WarnBtn}"/>
|
||||||
<Button Content="Single" Command="{Binding SingleCommand}"
|
|
||||||
Style="{StaticResource CmdBtn}"/>
|
|
||||||
<Button Content="Force Trig" Command="{Binding ForceTriggerCommand}"
|
|
||||||
Style="{StaticResource CmdBtn}"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<Button Content="查询 IDN" Command="{Binding QueryIdentityCommand}"
|
<TextBlock Text="触发模式" Style="{StaticResource ParamLabel}"/>
|
||||||
Style="{StaticResource CmdBtn}"/>
|
<ComboBox Width="90" Height="32" Margin="4,0"
|
||||||
<Button Content="重置设备" Command="{Binding ResetDeviceCommand}"
|
ItemsSource="{Binding TriggerModeOptions}"
|
||||||
Style="{StaticResource WarnBtn}"/>
|
SelectedItem="{Binding SelectedTriggerMode}"
|
||||||
</StackPanel>
|
VerticalContentAlignment="Center" FontSize="12"/>
|
||||||
</StackPanel>
|
<Button Content="设置" Command="{Binding SetTriggerModeCommand}"
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 垂直设置 -->
|
|
||||||
<GroupBox Header="垂直设置" Margin="0,0,0,8"
|
|
||||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
|
||||||
<StackPanel Margin="4,4,4,4">
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
|
||||||
<TextBlock Text="V/div (V)" Style="{StaticResource ParamLabel}"/>
|
|
||||||
<TextBox Style="{StaticResource NumInput}"
|
|
||||||
Text="{Binding VoltsPerDiv}"/>
|
|
||||||
<Button Content="设置" Command="{Binding SetVoltsDivCommand}"
|
|
||||||
Style="{StaticResource CmdBtn}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
|
||||||
<TextBlock Text="Offset (V)" Style="{StaticResource ParamLabel}"/>
|
|
||||||
<TextBox Style="{StaticResource NumInput}"
|
|
||||||
Text="{Binding Offset, UpdateSourceTrigger=PropertyChanged}"/>
|
|
||||||
<Button Content="设置" Command="{Binding SetOffsetCommand}"
|
|
||||||
Style="{StaticResource CmdBtn}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
|
||||||
<TextBlock Text="输入阻抗" Style="{StaticResource ParamLabel}"/>
|
|
||||||
<Button Content="50Ω" Command="{Binding SetImpedance50Command}"
|
|
||||||
Style="{StaticResource CmdBtn}"/>
|
|
||||||
<Button Content="1MΩ" Command="{Binding SetImpedance1MCommand}"
|
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
<!-- 水平/触发设置 -->
|
<!-- 水平与触发 -->
|
||||||
<GroupBox Header="水平与触发" Margin="0,0,0,8"
|
<GroupBox Header="水平与触发" Margin="0,0,0,8"
|
||||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
<StackPanel Margin="4,4,4,4">
|
<StackPanel Margin="4,4,4,4">
|
||||||
@@ -170,6 +175,13 @@
|
|||||||
<Button Content="设置" Command="{Binding SetTimeBaseCommand}"
|
<Button Content="设置" Command="{Binding SetTimeBaseCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="触发源" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<TextBox Style="{StaticResource NumInput}" Width="80"
|
||||||
|
Text="{Binding TriggerSource, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<Button Content="设置" Command="{Binding SetTriggerSourceCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="触发电平 (V)" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="触发电平 (V)" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource NumInput}"
|
<TextBox Style="{StaticResource NumInput}"
|
||||||
@@ -178,10 +190,33 @@
|
|||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="触发源" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="触发斜率" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource NumInput}" Width="80"
|
<ComboBox Width="100" Height="32" Margin="4,0"
|
||||||
Text="{Binding TriggerSource, UpdateSourceTrigger=PropertyChanged}"/>
|
ItemsSource="{Binding TriggerSlopeOptions}"
|
||||||
<Button Content="设置" Command="{Binding SetTriggerSourceCommand}"
|
SelectedItem="{Binding SelectedTriggerSlope}"
|
||||||
|
VerticalContentAlignment="Center" FontSize="12"/>
|
||||||
|
<Button Content="设置" Command="{Binding SetTriggerSlopeCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 系统命令 -->
|
||||||
|
<GroupBox Header="系统命令" Margin="0,0,0,8"
|
||||||
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
|
<StackPanel Margin="4,4,4,4">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<Button Content="查询 IDN" Command="{Binding QueryIdentityCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
<Button Content="重置设备" Command="{Binding ResetDeviceCommand}"
|
||||||
|
Style="{StaticResource WarnBtn}"/>
|
||||||
|
<Button Content="清除状态" Command="{Binding ClearStatusCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<Button Content="查询错误" Command="{Binding QueryErrorCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
<Button Content="*OPC?" Command="{Binding QueryOpcCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -191,6 +226,38 @@
|
|||||||
<!-- ─── 右列 ─── -->
|
<!-- ─── 右列 ─── -->
|
||||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||||
|
|
||||||
|
<!-- 光标测量 -->
|
||||||
|
<GroupBox Header="光标测量 (ΔT)" Margin="0,0,0,8"
|
||||||
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
|
<StackPanel Margin="4,4,4,4">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<Button Content="开启手动 X 光标" Command="{Binding EnableManualXCursorCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="X1 位置 (s)" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<TextBox Style="{StaticResource NumInput}"
|
||||||
|
Text="{Binding CursorX1, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<Button Content="设置" Command="{Binding SetCursorX1Command}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="X2 位置 (s)" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<TextBox Style="{StaticResource NumInput}"
|
||||||
|
Text="{Binding CursorX2, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<Button Content="设置" Command="{Binding SetCursorX2Command}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="ΔT (s)" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<TextBox Style="{StaticResource MeasureBox}"
|
||||||
|
Text="{Binding CursorDeltaT, Mode=OneWay}"/>
|
||||||
|
<Button Content="查询" Command="{Binding QueryCursorDeltaTCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
<!-- 自动测量 -->
|
<!-- 自动测量 -->
|
||||||
<GroupBox Header="自动测量" Margin="0,0,0,8"
|
<GroupBox Header="自动测量" Margin="0,0,0,8"
|
||||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
@@ -199,7 +266,6 @@
|
|||||||
<TextBlock Text="Vpp" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="Vpp" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource MeasureBox}"
|
<TextBox Style="{StaticResource MeasureBox}"
|
||||||
Text="{Binding MeasuredVpp, Mode=OneWay}"/>
|
Text="{Binding MeasuredVpp, Mode=OneWay}"/>
|
||||||
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
|
||||||
<Button Content="刷新" Command="{Binding QueryVppCommand}"
|
<Button Content="刷新" Command="{Binding QueryVppCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -207,7 +273,6 @@
|
|||||||
<TextBlock Text="频率" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="频率" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource MeasureBox}"
|
<TextBox Style="{StaticResource MeasureBox}"
|
||||||
Text="{Binding MeasuredFrequency, Mode=OneWay}"/>
|
Text="{Binding MeasuredFrequency, Mode=OneWay}"/>
|
||||||
<TextBlock Text="Hz" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
|
||||||
<Button Content="刷新" Command="{Binding QueryFrequencyCommand}"
|
<Button Content="刷新" Command="{Binding QueryFrequencyCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -215,7 +280,6 @@
|
|||||||
<TextBlock Text="RMS" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="RMS" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource MeasureBox}"
|
<TextBox Style="{StaticResource MeasureBox}"
|
||||||
Text="{Binding MeasuredRms, Mode=OneWay}"/>
|
Text="{Binding MeasuredRms, Mode=OneWay}"/>
|
||||||
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
|
||||||
<Button Content="刷新" Command="{Binding QueryRmsCommand}"
|
<Button Content="刷新" Command="{Binding QueryRmsCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -225,10 +289,26 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 屏幕截图 -->
|
||||||
|
<GroupBox Header="屏幕截图" Margin="0,0,0,8"
|
||||||
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
|
<StackPanel Margin="4,4,4,4">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="保存路径" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<TextBox Style="{StaticResource NumInput}" Width="300"
|
||||||
|
Text="{Binding SaveFilePath, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<Button Content="保存截图 (自动追加时间戳)" Command="{Binding SaveScreenshotCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
<!-- 响应日志 -->
|
<!-- 响应日志 -->
|
||||||
<GroupBox Header="响应日志" Margin="0,0,0,8"
|
<GroupBox Header="响应日志" Margin="0,0,0,8"
|
||||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
<ScrollViewer Height="460" VerticalScrollBarVisibility="Auto">
|
<ScrollViewer Height="240" VerticalScrollBarVisibility="Auto">
|
||||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||||
materialDesign:HintAssist.Hint=""
|
materialDesign:HintAssist.Hint=""
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
|
|||||||
@@ -433,7 +433,9 @@ namespace MonitorModule.ViewModels
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
|
|||||||
@@ -233,7 +233,9 @@ namespace MonitorModule.ViewModels
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
|
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
|
||||||
/// 委托签名统一为 Func<object, CancellationToken, Task<string>>
|
/// 委托签名统一为 Func<object, CancellationToken, Task<double>>
|
||||||
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
|
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
|
||||||
/// - 第二个参数 = CancellationToken(无参方法会忽略它)
|
/// - 第二个参数 = CancellationToken(无参方法会忽略它)
|
||||||
/// - 返回值 = Task<string>
|
/// - 返回值 = Task<double>(Task<string> 方法自动 TryParse 为 double)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<string>> Invoker, object Device)> _registeredMethods = new();
|
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<double>> Invoker, object Device)> _registeredMethods = new();
|
||||||
|
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@@ -76,11 +76,13 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
|
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
|
||||||
/// 统一签名为 Func<object, CancellationToken, Task<string>>:
|
/// 统一签名为 Func<object, CancellationToken, Task<double>>:
|
||||||
|
/// - Task<double> 方法:直接返回 double 值
|
||||||
|
/// - Task<string> 方法:自动 TryParse 为 double
|
||||||
/// - 无参方法:忽略 CancellationToken
|
/// - 无参方法:忽略 CancellationToken
|
||||||
/// - 带 CancellationToken 方法:直接透传
|
/// - 带 CancellationToken 方法:直接透传
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Func<object, CancellationToken, Task<string>> BuildInvoker(MethodInfo method)
|
private static Func<object, CancellationToken, Task<double>> BuildInvoker(MethodInfo method)
|
||||||
{
|
{
|
||||||
var deviceParam = Expression.Parameter(typeof(object), "device");
|
var deviceParam = Expression.Parameter(typeof(object), "device");
|
||||||
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
||||||
@@ -96,8 +98,24 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
var call = Expression.Call(castDevice, method, callArgs);
|
var call = Expression.Call(castDevice, method, callArgs);
|
||||||
|
|
||||||
return Expression.Lambda<Func<object, CancellationToken, Task<string>>>(
|
if (method.ReturnType == typeof(Task<double>))
|
||||||
call, deviceParam, ctParam).Compile();
|
{
|
||||||
|
// Task<double> 方法:直接返回
|
||||||
|
return Expression.Lambda<Func<object, CancellationToken, Task<double>>>(
|
||||||
|
call, deviceParam, ctParam).Compile();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Task<string> 方法:await 后 TryParse 为 double
|
||||||
|
return async (device, ct) =>
|
||||||
|
{
|
||||||
|
// 通过 MethodInfo.Invoke 调用(仅 Task<string> 路径,性能可接受)
|
||||||
|
var task = (Task<string>)method.Invoke(device,
|
||||||
|
parms.Length == 1 ? new object[] { ct } : Array.Empty<object>())!;
|
||||||
|
string raw = await task.ConfigureAwait(false);
|
||||||
|
return double.TryParse(raw, out double v) ? v : double.NaN;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -126,7 +144,9 @@ namespace UIShare.GlobalVariable
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
@@ -184,10 +204,10 @@ namespace UIShare.GlobalVariable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 直接委托调用,零反射开销
|
// 直接委托调用,零反射开销(Task<double> 路径)或自动 TryParse(Task<string> 路径)
|
||||||
string raw = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
|
double value = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!double.TryParse(raw, out double value)) return;
|
if (double.IsNaN(value)) return;
|
||||||
|
|
||||||
// 采样成功,重置失败计数
|
// 采样成功,重置失败计数
|
||||||
_failureCounts.TryRemove(channelKey, out _);
|
_failureCounts.TryRemove(channelKey, out _);
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ namespace UIShare
|
|||||||
ResetAllStepStatus(program.ErrorStepCollection);
|
ResetAllStepStatus(program.ErrorStepCollection);
|
||||||
tmpParameters.Clear();
|
tmpParameters.Clear();
|
||||||
TestRoundID = Guid.NewGuid();
|
TestRoundID = Guid.NewGuid();
|
||||||
|
// 注入步骤耗时查询器:AsyncLocal 随异步执行流流动,命令库(CommandRunningTime)据此查询当前作用域主程序的步骤耗时
|
||||||
|
StepRuntimeContext.StepTimeQuery.Value = QueryStepRunTimeSum;
|
||||||
}
|
}
|
||||||
foreach (var item in program.Parameters)
|
foreach (var item in program.Parameters)
|
||||||
{
|
{
|
||||||
@@ -261,6 +263,8 @@ namespace UIShare
|
|||||||
tmpParameters.Clear();
|
tmpParameters.Clear();
|
||||||
testItemStack.Clear();
|
testItemStack.Clear();
|
||||||
TestRoundID = Guid.NewGuid();
|
TestRoundID = Guid.NewGuid();
|
||||||
|
// 注入步骤耗时查询器:AsyncLocal 随异步执行流流动,命令库(CommandRunningTime)据此查询当前作用域主程序的步骤耗时
|
||||||
|
StepRuntimeContext.StepTimeQuery.Value = QueryStepRunTimeSum;
|
||||||
}
|
}
|
||||||
int initialLoopStackCount = loopStack.Count;
|
int initialLoopStackCount = loopStack.Count;
|
||||||
foreach (var item in program.Parameters)
|
foreach (var item in program.Parameters)
|
||||||
@@ -469,7 +473,11 @@ namespace UIShare
|
|||||||
{
|
{
|
||||||
if(_scopedContext.Program.StepCollection.Count>1)
|
if(_scopedContext.Program.StepCollection.Count>1)
|
||||||
_scopedContext.SelectedStep = null;
|
_scopedContext.SelectedStep = null;
|
||||||
|
// 区间延迟不计入步骤运行时间:延迟期间暂停步骤计时器(子程序内的区间延迟同样排除,它们都归属顶层步骤的计时区间)
|
||||||
|
bool timingPaused = stepStopwatch.IsRunning;
|
||||||
|
if (timingPaused) stepStopwatch.Stop();
|
||||||
await Task.Delay(_systemConfig.PerformanceLevel, cancellationToken);
|
await Task.Delay(_systemConfig.PerformanceLevel, cancellationToken);
|
||||||
|
if (timingPaused) stepStopwatch.Start();
|
||||||
|
|
||||||
|
|
||||||
// 1. 查找类型
|
// 1. 查找类型
|
||||||
@@ -800,6 +808,30 @@ namespace UIShare
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询当前作用域主程序步骤集合中,序号位于 [起始序号, 结束序号](含两端)范围内步骤的运行时间之和(毫秒)。
|
||||||
|
/// <para>由 ExecuteSteps / ExecuteErrorSteps 顶层启动时经 StepRuntimeContext 注入(AsyncLocal 随异步流流动),
|
||||||
|
/// 供命令库 CommandRunningTime.获取步骤运行时间之和 调用(依赖倒置:命令库不引用宿主类型)。</para>
|
||||||
|
/// <para>序号倒置、超界(范围内不存在步骤)时记录错误日志并返回 0;未执行步骤的耗时按 0 计入。</para>
|
||||||
|
/// </summary>
|
||||||
|
private double QueryStepRunTimeSum(int startIndex, int endIndex)
|
||||||
|
{
|
||||||
|
var steps = _scopedContext.Program.StepCollection;
|
||||||
|
if (startIndex > endIndex)
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[步骤耗时] 查询失败:起始序号 {startIndex} 大于结束序号 {endIndex},返回 0");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var matched = steps.Where(s => s.Index >= startIndex && s.Index <= endIndex).ToList();
|
||||||
|
if (matched.Count == 0)
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[步骤耗时] 查询失败:序号 {startIndex}~{endIndex} 范围内不存在测试步骤(主程序共 {steps.Count} 步),返回 0");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 尚未执行或未启用的步骤 RunTime 为 null,按 0 计入
|
||||||
|
return matched.Sum(s => s.RunTime ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateCurrentStepResult(StepVM step, bool paraResult = true, bool stepResult = true, int depth = 0)
|
private void UpdateCurrentStepResult(StepVM step, bool paraResult = true, bool stepResult = true, int depth = 0)
|
||||||
{
|
{
|
||||||
if (stepResult && paraResult)
|
if (stepResult && paraResult)
|
||||||
|
|||||||
Reference in New Issue
Block a user