From e144dc78a71b1bb2f369caf139a751ddc4dac0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chsc=E2=80=9D?= <“huangsucan@kaiyili-lab.com”> Date: Thu, 27 Aug 2026 10:25:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=A0=91=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E6=B5=8B=E8=AF=95=E7=BC=96=E8=BE=91=E5=8C=BA=E5=9F=9F?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Command/Command.csproj | 1 + Common/Common.csproj | 1 + DeviceCommand/DeviceCommand.csproj | 1 + DeviceCommand/Devices/ANEVH80.cs | 15 +- DeviceCommand/Devices/Chroma61800.cs | 78 +++-- DeviceCommand/Devices/DG1000Z.cs | 156 ++++++++-- DeviceCommand/Devices/IOBoard.cs | 37 ++- DeviceCommand/Devices/IOBoardGroup.cs | 62 ++-- DeviceCommand/Devices/IT6720.cs | 27 +- DeviceCommand/Devices/MCc30W.cs | 7 +- DeviceCommand/Devices/PW8001.cs | 294 ++++++++++++++++-- DeviceCommand/Devices/RLT1000.cs | 156 +++++++++- DeviceCommand/Devices/S7200.cs | 272 +++++++++++++--- DeviceCommand/Devices/TektronixMSO.cs | 185 ++++++++--- DeviceCommand/Flexible/FModbusRTU.cs | 78 ++++- DeviceCommand/Flexible/FModbusTCP.cs | 63 +++- DeviceCommand/Flexible/FSerialPort.cs | 40 ++- DeviceCommand/Flexible/FTCP.cs | 51 ++- TSMasterCAN/CAN.cs | 115 +++---- TSMasterCAN/TSMasterCAN.csproj | 1 + .../ViewModels/CommandTreeViewModel.cs | 126 +++++++- .../ViewModels/StepsManagerViewModel.cs | 80 +++-- TestingModule/Views/CommandTree.xaml | 3 +- UIShare/UIViewModel/InstructionNodeVM.cs | 9 +- 24 files changed, 1520 insertions(+), 338 deletions(-) diff --git a/Command/Command.csproj b/Command/Command.csproj index 0eabc2b..5b176ff 100644 --- a/Command/Command.csproj +++ b/Command/Command.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true diff --git a/Common/Common.csproj b/Common/Common.csproj index 366108e..94a29c3 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true diff --git a/DeviceCommand/DeviceCommand.csproj b/DeviceCommand/DeviceCommand.csproj index ca7efa5..998d41b 100644 --- a/DeviceCommand/DeviceCommand.csproj +++ b/DeviceCommand/DeviceCommand.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true diff --git a/DeviceCommand/Devices/ANEVH80.cs b/DeviceCommand/Devices/ANEVH80.cs index 3f50df4..8252766 100644 --- a/DeviceCommand/Devices/ANEVH80.cs +++ b/DeviceCommand/Devices/ANEVH80.cs @@ -11,20 +11,29 @@ using System.Threading.Tasks; namespace DeviceCommand.Devices { /// - /// 负载 一对三 + /// ANEVH80 电子负载(一对三控制),基于 TCP 通信,使用 SCPI 指令集。 + /// 用于被测产品的放电/带载测试,可模拟真实负载工况。 /// [ACPCommand] public class ANEVH80 : Tcp { - // Chroma SCPI 默认使用 \n 作为结束符 + // SCPI 指令默认使用 \n 作为结束符 private const string ScpiDelimiter = "\n"; + /// + /// 构造函数,初始化 TCP 连接配置。 + /// + /// TCP 连接配置(IP、端口等) public ANEVH80(TcpConfig config) : base(config) { } + /// - /// 占位符,可能的操作先占位 + /// 占位符方法,预留后续负载控制操作(如设置负载模式、设置电流等)。 + /// 当前实现为查询频率测量值。 /// + /// 异步取消令牌 + /// 查询到的频率值,解析失败返回 0.0 public virtual async Task 占位符(CancellationToken ct = default) { string resp = await WriteReadAsync($"MEAS:FREQ?{ScpiDelimiter}", ScpiDelimiter, ct); diff --git a/DeviceCommand/Devices/Chroma61800.cs b/DeviceCommand/Devices/Chroma61800.cs index e613a9c..8679b07 100644 --- a/DeviceCommand/Devices/Chroma61800.cs +++ b/DeviceCommand/Devices/Chroma61800.cs @@ -3,6 +3,7 @@ using DeviceCommand.Base; using Model.Models; using System; using System.Globalization; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -21,10 +22,27 @@ namespace DeviceCommand.Devices { } + /// + /// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。 + /// 支持科学计数法(如 2.48E-03),结果保留两位小数。 + /// + /// 设备返回的原始字符串 + /// 提取到的数值(保留两位小数),解析失败时返回 0 + private static double ExtractDouble(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return 0; + + // 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头 + var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?"); + return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val) + ? Math.Round(val, 2) + : 0; + } + #region 1. 基础系统与公共指令 /// - /// 清除错误队列 (*CLS)[cite: 1] + /// 清除错误队列 (*CLS) /// public virtual async Task 清除错误队列Async(CancellationToken ct = default) { @@ -32,7 +50,7 @@ namespace DeviceCommand.Devices } /// - /// 重设仪器为初始状态 (*RST)[cite: 1] + /// 重设仪器为初始状态 (*RST) /// public virtual async Task 复位Async(CancellationToken ct = default) { @@ -40,7 +58,7 @@ namespace DeviceCommand.Devices } /// - /// 查询仪器识别码 (*IDN?)[cite: 1] + /// 查询仪器识别码 (*IDN?) /// public virtual async Task 查询设备信息Async(CancellationToken ct = default) { @@ -49,10 +67,10 @@ namespace DeviceCommand.Devices #endregion - #region 2. 基础输出与耦合控制 (OUTPut 子系统)[cite: 1] + #region 2. 基础输出与耦合控制 (OUTPut 子系统) /// - /// 设置输出开关状态 (OUTP ON / OFF)[cite: 1] + /// 设置输出开关状态 (OUTP ON / OFF) /// /// true: 开启输出, false: 关闭输出 public virtual async Task 设置输出开关Async(bool isOn, CancellationToken ct = default) @@ -62,7 +80,7 @@ namespace DeviceCommand.Devices } /// - /// 设置输出耦合模式 (OUTP:COUP AC | DC | ACDC)[cite: 1] + /// 设置输出耦合模式 (OUTP:COUP AC | DC | ACDC) /// public virtual async Task 设置耦合模式Async(string couplingMode, CancellationToken ct = default) { @@ -71,7 +89,7 @@ namespace DeviceCommand.Devices } /// - /// 设置输出继电器状态 (OUTP:REL ON | OFF)[cite: 1] + /// 设置输出继电器状态 (OUTP:REL ON | OFF) /// public virtual async Task 设置输出继电器Async(bool isOn, CancellationToken ct = default) { @@ -81,10 +99,10 @@ namespace DeviceCommand.Devices #endregion - #region 3. 电源核心参数设置 (SOURCE 子系统)[cite: 1] + #region 3. 电源核心参数设置 (SOURCE 子系统) /// - /// 设置交流电压 (VOLT:AC)[cite: 1] + /// 设置交流电压 (VOLT:AC) /// /// 电压值 (0 ~ 300V 或高压选配范围) public virtual async Task 设置交流电压Async(double voltage, CancellationToken ct = default) @@ -94,7 +112,7 @@ namespace DeviceCommand.Devices } /// - /// 设置直流电压 (VOLT:DC)[cite: 1] + /// 设置直流电压 (VOLT:DC) /// public virtual async Task 设置直流电压Async(double voltage, CancellationToken ct = default) { @@ -103,7 +121,7 @@ namespace DeviceCommand.Devices } /// - /// 设置输出频率 (FREQ)[cite: 1] + /// 设置输出频率 (FREQ) /// /// 频率值 (Hz) public virtual async Task 设置频率Async(double frequency, CancellationToken ct = default) @@ -113,7 +131,7 @@ namespace DeviceCommand.Devices } /// - /// 设置波形选择 (FUNC:SHAP:A SINE / SQUA / CSIN 等)[cite: 1] + /// 设置波形选择 (FUNC:SHAP:A SINE / SQUA / CSIN 等) /// public virtual async Task 设置波形Async(string shape, CancellationToken ct = default) { @@ -122,10 +140,10 @@ namespace DeviceCommand.Devices #endregion - #region 4. 相位与多相模式控制 (INST & PHASE 子系统)[cite: 1] + #region 4. 相位与多相模式控制 (INST & PHASE 子系统) /// - /// 设置单/三相工作模式 (INST:PHAS THREE / SINGLE)[cite: 1] + /// 设置单/三相工作模式 (INST:PHAS THREE / SINGLE) /// /// true: 三相模式, false: 单相模式 public virtual async Task 设置单三相模式Async(bool isThreePhase, CancellationToken ct = default) @@ -135,7 +153,7 @@ namespace DeviceCommand.Devices } /// - /// 设置三相模式下的相互关系 (PHASE:THREE INDEPEND / SAMEFREQ / BALANCE)[cite: 1] + /// 设置三相模式下的相互关系 (PHASE:THREE INDEPEND / SAMEFREQ / BALANCE) /// public virtual async Task 设置三相关系Async(string relation, CancellationToken ct = default) { @@ -144,7 +162,7 @@ namespace DeviceCommand.Devices } /// - /// 设置输出开启相位角 (PHASE:ON)[cite: 1] + /// 设置输出开启相位角 (PHASE:ON) /// public virtual async Task 设置开机角度Async(double degree, CancellationToken ct = default) { @@ -153,7 +171,7 @@ namespace DeviceCommand.Devices } /// - /// 设置输出关闭相位角 (PHASE:OFF)[cite: 1] + /// 设置输出关闭相位角 (PHASE:OFF) /// public virtual async Task 设置关机角度Async(double degree, CancellationToken ct = default) { @@ -163,42 +181,50 @@ namespace DeviceCommand.Devices #endregion - #region 5. 参数测量查询 (MEASure 子系统)[cite: 1] + #region 5. 参数测量查询 (MEASure 子系统) /// - /// 查询交流电压均方根值 (MEAS:VOLT:AC?)[cite: 1] + /// 查询交流电压均方根值 (MEAS:VOLT:AC?) /// + /// 取消令牌 + /// 交流电压均方根值 (单位: V,保留两位小数) public virtual async Task 读取交流电压Async(CancellationToken ct = default) { string resp = await WriteReadAsync($"MEAS:VOLT:AC?{ScpiDelimiter}", ScpiDelimiter, ct); - return double.TryParse(resp, NumberStyles.Any, CultureInfo.InvariantCulture, out double val) ? val : 0.0; + return ExtractDouble(resp); } /// - /// 查询交流电流均方根值 (MEAS:CURR:AC?)[cite: 1] + /// 查询交流电流均方根值 (MEAS:CURR:AC?) /// + /// 取消令牌 + /// 交流电流均方根值 (单位: A,保留两位小数) public virtual async Task 读取交流电流Async(CancellationToken ct = default) { string resp = await WriteReadAsync($"MEAS:CURR:AC?{ScpiDelimiter}", ScpiDelimiter, ct); - return double.TryParse(resp, NumberStyles.Any, CultureInfo.InvariantCulture, out double val) ? val : 0.0; + return ExtractDouble(resp); } /// - /// 查询输出频率 (MEAS:FREQ?)[cite: 1] + /// 查询输出频率 (MEAS:FREQ?) /// + /// 取消令牌 + /// 输出频率值 (单位: Hz,保留两位小数) public virtual async Task 读取频率Async(CancellationToken ct = default) { string resp = await WriteReadAsync($"MEAS:FREQ?{ScpiDelimiter}", ScpiDelimiter, ct); - return double.TryParse(resp, NumberStyles.Any, CultureInfo.InvariantCulture, out double val) ? val : 0.0; + return ExtractDouble(resp); } /// - /// 查询真实输出功率 (MEAS:POW:AC?)[cite: 1] + /// 查询真实输出功率 (MEAS:POW:AC?) /// + /// 取消令牌 + /// 真实输出功率值 (单位: W,保留两位小数) public virtual async Task 读取真实功率Async(CancellationToken ct = default) { string resp = await WriteReadAsync($"MEAS:POW:AC?{ScpiDelimiter}", ScpiDelimiter, ct); - return double.TryParse(resp, NumberStyles.Any, CultureInfo.InvariantCulture, out double val) ? val : 0.0; + return ExtractDouble(resp); } #endregion diff --git a/DeviceCommand/Devices/DG1000Z.cs b/DeviceCommand/Devices/DG1000Z.cs index e88aea8..7caeb78 100644 --- a/DeviceCommand/Devices/DG1000Z.cs +++ b/DeviceCommand/Devices/DG1000Z.cs @@ -3,6 +3,7 @@ using DeviceCommand.Base; using Model.Models; using System; using System.Globalization; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -45,33 +46,74 @@ namespace DeviceCommand.Devices #endregion + /// + /// 信号发生器 (DG1000Z 系列) 一拖三,支持波形输出与频率计测量 + /// [ACPCommand] public class DG1000Z : Tcp { // 使用换行符 \n (ASCII 0x0A) 作为 SCPI 结束符 private const string ScpiDelimiter = "\n"; + /// + /// 构造函数:传入 一次性初始化信号发生器通信参数 + /// public DG1000Z(TcpConfig config) : base(config) { } + /// + /// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。 + /// 支持科学计数法(如 2.48E-03),结果保留两位小数。 + /// + /// 设备返回的原始字符串 + /// 提取到的数值(保留两位小数),解析失败时返回 0 + private static double ExtractDouble(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return 0; + + // 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头 + var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?"); + return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val) + ? Math.Round(val, 2) + : 0; + } + #region 1. IEEE 488.2 公共命令 + /// + /// 清除标准事件状态寄存器和错误队列 (*CLS) + /// + /// 取消令牌 public virtual async Task 清除状态寄存器(CancellationToken ct = default) { await SendAsync($"*CLS{ScpiDelimiter}", ct); } + /// + /// 查询设备识别信息 (*IDN? 指令) + /// + /// 取消令牌 + /// 设备标识字符串 (制造商, 型号, 序列号, 固件版本) public virtual async Task 查询设备标识(CancellationToken ct = default) { return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); } + /// + /// 重设仪器为初始状态 (*RST) + /// + /// 取消令牌 public virtual async Task 重置设备(CancellationToken ct = default) { await SendAsync($"*RST{ScpiDelimiter}", ct); } + /// + /// 查询仪器支持的 SCPI 版本 (SYSTem:VERSion? 指令) + /// + /// 取消令牌 + /// SCPI 版本号字符串 public virtual async Task 查询系统SCPI版本(CancellationToken ct = default) { return await WriteReadAsync($":SYSTem:VERSion?{ScpiDelimiter}", ScpiDelimiter, ct); @@ -82,16 +124,23 @@ namespace DeviceCommand.Devices #region 2. 通道输出控制 (CH1 / CH2) /// - /// 打开或关闭指定通道的输出 + /// 打开或关闭指定通道的输出 (OUTPut 指令) /// /// 通道 1 或 2,默认 1 /// true为开启,false为关闭 + /// 取消令牌 public virtual async Task 设置通道输出状态(int 通道号 = 1, bool 开启 = true, CancellationToken ct = default) { string 参数 = 开启 ? "ON" : "OFF"; await SendAsync($":OUTPut{通道号}:STATe {参数}{ScpiDelimiter}", ct); } + /// + /// 查询指定通道的输出开关状态 (OUTPut:STATe? 指令) + /// + /// 通道 1 或 2,默认 1 + /// 取消令牌 + /// 通道输出状态字符串 (ON / OFF) public virtual async Task 查询通道输出状态(int 通道号 = 1, CancellationToken ct = default) { return await WriteReadAsync($":OUTPut{通道号}:STATe?{ScpiDelimiter}", ScpiDelimiter, ct); @@ -104,51 +153,84 @@ namespace DeviceCommand.Devices /// /// 设置指定通道的波形类型 (如正弦波、方波等) /// + /// 通道 1 或 2,默认 1 + /// 波形类型枚举,默认正弦波 + /// 取消令牌 public virtual async Task 设置波形类型(int 通道号 = 1, SignalWaveform 波形 = SignalWaveform.SINusoid, CancellationToken ct = default) { await SendAsync($":SOURce{通道号}:FUNCtion:SHAPe {波形}{ScpiDelimiter}", ct); } /// - /// 设置指定通道的输出频率 (单位: Hz) + /// 设置指定通道的输出频率 (SOURce:FREQuency 指令) /// + /// 通道 1 或 2,默认 1 + /// 输出频率值 (单位: Hz),默认 1000 + /// 取消令牌 public virtual async Task 设置频率(int 通道号 = 1, double 频率Hz = 1000, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:FREQuency {1:F2}{2}", 通道号, 频率Hz, ScpiDelimiter); await SendAsync(cmd, ct); } - public virtual async Task 查询频率(int 通道号 = 1, CancellationToken ct = default) + /// + /// 查询指定通道的当前输出频率 (SOURce:FREQuency? 指令) + /// + /// 通道 1 或 2,默认 1 + /// 取消令牌 + /// 输出频率值 (单位: Hz,保留两位小数) + public virtual async Task 查询频率(int 通道号 = 1, CancellationToken ct = default) { - return await WriteReadAsync($":SOURce{通道号}:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct); + string resp = await WriteReadAsync($":SOURce{通道号}:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); } /// - /// 设置指定通道的输出幅度 (单位: Vpp,峰峰值) + /// 设置指定通道的输出幅度 (SOURce:VOLTage:AMPLitude 指令) /// + /// 通道 1 或 2,默认 1 + /// 输出幅度 (单位: Vpp 峰峰值),默认 5 + /// 取消令牌 public virtual async Task 设置幅度(int 通道号 = 1, double 幅度Vpp = 5, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:VOLTage:AMPLitude {1:F2}{2}", 通道号, 幅度Vpp, ScpiDelimiter); await SendAsync(cmd, ct); } - public virtual async Task 查询幅度(int 通道号 = 1, CancellationToken ct = default) + /// + /// 查询指定通道的当前输出幅度 (SOURce:VOLTage:AMPLitude? 指令) + /// + /// 通道 1 或 2,默认 1 + /// 取消令牌 + /// 输出幅度 (单位: Vpp,保留两位小数) + public virtual async Task 查询幅度(int 通道号 = 1, CancellationToken ct = default) { - return await WriteReadAsync($":SOURce{通道号}:VOLTage:AMPLitude?{ScpiDelimiter}", ScpiDelimiter, ct); + string resp = await WriteReadAsync($":SOURce{通道号}:VOLTage:AMPLitude?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); } /// - /// 设置指定通道的直流偏移电压 (单位: Vdc) + /// 设置指定通道的直流偏移电压 (SOURce:VOLTage:OFFSet 指令) /// + /// 通道 1 或 2,默认 1 + /// 直流偏移电压 (单位: Vdc),默认 0 + /// 取消令牌 public virtual async Task 设置偏移电压(int 通道号 = 1, double 偏移Vdc = 0, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:VOLTage:OFFSet {1:F2}{2}", 通道号, 偏移Vdc, ScpiDelimiter); await SendAsync(cmd, ct); } - public virtual async Task 查询偏移电压(int 通道号 = 1, CancellationToken ct = default) + /// + /// 查询指定通道的当前直流偏移电压 (SOURce:VOLTage:OFFSet? 指令) + /// + /// 通道 1 或 2,默认 1 + /// 取消令牌 + /// 直流偏移电压 (单位: Vdc,保留两位小数) + public virtual async Task 查询偏移电压(int 通道号 = 1, CancellationToken ct = default) { - return await WriteReadAsync($":SOURce{通道号}:VOLTage:OFFSet?{ScpiDelimiter}", ScpiDelimiter, ct); + string resp = await WriteReadAsync($":SOURce{通道号}:VOLTage:OFFSet?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); } #endregion @@ -156,8 +238,11 @@ namespace DeviceCommand.Devices #region 4. 方波 / 脉冲波占空比设置 (重点用于CP信号模拟) /// - /// 设置方波信号的占空比 (0.01% ~ 99.99%)[cite: 1] + /// 设置方波信号的占空比 (0.01% ~ 99.99%) /// + /// 通道 1 或 2,默认 1 + /// 方波占空比百分比,默认 50.0 + /// 取消令牌 public virtual async Task 设置方波占空比(int 通道号 = 1, double 占空比 = 50.0, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:FUNCtion:SQUare:DCYCle {1:F2}{2}", 通道号, 占空比, ScpiDelimiter); @@ -165,9 +250,12 @@ namespace DeviceCommand.Devices } /// - /// 设置脉冲波信号的占空比 (0.01% ~ 99.99%)[cite: 1] + /// 设置脉冲波信号的占空比 (0.01% ~ 99.99%)。 /// 注意:使用此命令前必须先设置为 PULSe 波形类型 /// + /// 通道 1 或 2,默认 1 + /// 脉冲波占空比百分比,默认 50.0 + /// 取消令牌 public virtual async Task 设置脉冲波占空比(int 通道号 = 1, double 占空比 = 50.0, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:FUNCtion:PULSe:DCYCle {1:F2}{2}", 通道号, 占空比, ScpiDelimiter); @@ -179,47 +267,68 @@ namespace DeviceCommand.Devices #region 5. 频率计测量功能 (高频数据轮询核心) /// - /// 打开或关闭内置频率计功能 + /// 打开或关闭内置频率计功能 (COUNter:STATe 指令) /// + /// true: 开启频率计, false: 关闭频率计 + /// 取消令牌 public virtual async Task 设置频率计开关(bool 开启 = true, CancellationToken ct = default) { string 参数 = 开启 ? "ON" : "OFF"; await SendAsync($":COUNter:STATe {参数}{ScpiDelimiter}", ct); } + /// + /// 查询内置频率计的开关状态 (COUNter:STATe? 指令) + /// + /// 取消令牌 + /// 频率计开关状态字符串 (ON / OFF) public virtual async Task 查询频率计开关状态(CancellationToken ct = default) { return await WriteReadAsync($":COUNter:STATe?{ScpiDelimiter}", ScpiDelimiter, ct); } /// - /// 设置频率计输入信号的耦合方式 + /// 设置频率计输入信号的耦合方式 (COUNter:COUPling 指令) /// + /// 耦合方式枚举 (AC 交流 / DC 直流),默认 AC + /// 取消令牌 public virtual async Task 设置频率计耦合方式(CounterCoupling 耦合 = CounterCoupling.AC, CancellationToken ct = default) { await SendAsync($":COUNter:COUPling {耦合}{ScpiDelimiter}", ct); } /// - /// 设置频率计的触发电平 (单位: V) + /// 设置频率计的触发电平 (COUNter:LEVel 指令) /// + /// 触发电平值 (单位: V),默认 0.1 + /// 取消令牌 public virtual async Task 设置频率计触发电平(double 电平V = 0.1, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, ":COUNter:LEVel {0:F2}{1}", 电平V, ScpiDelimiter); await SendAsync(cmd, ct); } + /// + /// 查询频率计测量值 (COUNter:MEASure? 指令)。 + /// 响应格式为 "频率, 周期, 占空比, 正脉宽, 负脉宽",此方法提取第一项频率值 + /// + /// 取消令牌 + /// 频率计测得的频率值 (单位: Hz,保留两位小数) [Monitorable("信号发生器频率计测量值")] - public virtual async Task 查询频率计测量值(CancellationToken ct = default) + public virtual async Task 查询频率计测量值(CancellationToken ct = default) { - // 返回格式: 频率, 周期, 占空比, 正脉宽, 负脉宽 - return await WriteReadAsync($":COUNter:MEASure?{ScpiDelimiter}", ScpiDelimiter, ct); + string resp = await WriteReadAsync($":COUNter:MEASure?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); } #endregion #region 6. 系统维护与远程控制扩展命令 + /// + /// 切换远程控制模式。信号发生器通过 LAN/USB 通信即自动进入远程模式 + /// + /// 取消令牌 public virtual async Task 切换远程控制模式(CancellationToken ct = default) { // 信号发生器通常通过 LAN/USB 和 VISA 通信即自动进入远程控制, @@ -227,12 +336,21 @@ namespace DeviceCommand.Devices await Task.CompletedTask; } + /// + /// 切换本地控制模式。通常通过前面板按键退出远程 + /// + /// 取消令牌 public virtual async Task 切换本地控制模式(CancellationToken ct = default) { // 通常通过前面板按键退出远程。不过可提供 SCPI 关闭屏幕保护等。 await Task.CompletedTask; } + /// + /// 查询系统错误信息 (SYSTem:ERRor? 指令) + /// + /// 取消令牌 + /// 错误代码和描述信息 public virtual async Task 查询错误信息(CancellationToken ct = default) { return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); @@ -240,4 +358,4 @@ namespace DeviceCommand.Devices #endregion } -} \ No newline at end of file +} diff --git a/DeviceCommand/Devices/IOBoard.cs b/DeviceCommand/Devices/IOBoard.cs index 06ffe84..1a730c3 100644 --- a/DeviceCommand/Devices/IOBoard.cs +++ b/DeviceCommand/Devices/IOBoard.cs @@ -12,22 +12,39 @@ using System.Threading.Tasks; namespace DeviceCommand.Devices { /// - /// IO板卡 一对一由于板卡映射不完全一样采用IOGroup + /// IO 继电器板卡(一对一控制),基于 Modbus TCP 协议通过线圈(Coil)读写控制继电器输出。 + /// 由于多块板卡的点位映射不完全一致,实际业务中使用 统一调度三块板卡。 /// //[ACPCommand] public class IOBoard : ModbusTcp { - + /// + /// 构造函数,初始化 Modbus TCP 连接配置。 + /// + /// TCP 连接配置(IP、端口等) public IOBoard(TcpConfig config) : base(config) { } - + /// + /// 写入单个输出开关(线圈)状态。 + /// + /// Modbus 从站站号 + /// 线圈地址(0 起始) + /// 开关状态(true: 吸合/开启, false: 断开/关闭) public async Task 写输出开关(byte 站号, ushort 开始地址, bool data) { await Modbus.WriteSingleCoilAsync(站号, 开始地址, data); } + + /// + /// 批量写入输出开关状态。先读出全部 16 个线圈当前状态, + /// 在内存中修改指定偏移段的值后再整体写回,避免影响其他点位。 + /// + /// Modbus 从站站号 + /// 起始线圈地址(0 起始) + /// 要写入的开关状态数组,按顺序从开始地址写入 public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas) { var 批量写入 = await Modbus.ReadCoilsAsync(1, 0, 16); @@ -40,10 +57,24 @@ namespace DeviceCommand.Devices } await Modbus.WriteMultipleCoilsAsync(站号, 0, 批量写入); } + + /// + /// 读取单个输出开关(线圈)状态。 + /// + /// Modbus 从站站号 + /// 线圈地址(0 起始) public async Task 读输出开关(byte 站号, ushort 开始地址) { await Modbus.ReadCoilsAsync(站号, 开始地址, 1); } + + /// + /// 批量读取输出开关(线圈)状态。 + /// + /// Modbus 从站站号 + /// 起始线圈地址(0 起始) + /// 要读取的线圈数量 + /// 线圈状态数组,true 表示吸合/开启 public async Task 批量读输出开关(byte 站号, ushort 开始地址,ushort 数量) { return await Modbus.ReadCoilsAsync(站号, 开始地址, 数量); diff --git a/DeviceCommand/Devices/IOBoardGroup.cs b/DeviceCommand/Devices/IOBoardGroup.cs index c62cbbf..3cffa4d 100644 --- a/DeviceCommand/Devices/IOBoardGroup.cs +++ b/DeviceCommand/Devices/IOBoardGroup.cs @@ -48,6 +48,11 @@ namespace DeviceCommand.Devices } } + /// + /// IO 板卡组(三工位统一调度),封装 3 块 IO 继电器板卡的访问。 + /// 根据 ACP 硬件接线图的点位映射字典,按台架序号路由到对应板卡的线圈地址, + /// 并内置充电/放电互锁保护,防止同时吸合造成短路。 + /// [ACPCommand] public class IOBoardGroup { @@ -121,6 +126,13 @@ namespace DeviceCommand.Devices }} }; + /// + /// 构造函数,注入三个工位的 IO 板卡实例。 + /// + /// 1# 工位 IO 板卡实例 + /// 2# 工位 IO 板卡实例 + /// 3# 工位 IO 板卡实例 + /// 任一块板卡实例为空时抛出 // 修改构造函数以支持 3 个板块 public IOBoardGroup(IOBoard board1, IOBoard board2, IOBoard board3) { @@ -168,49 +180,25 @@ namespace DeviceCommand.Devices await 目标板卡.写输出开关(_slaveAddress, io信息.Address, 开关状态); } - // ================================================================================= - // 以下为新增的原生命令透传接口,使用台架序号自动路由至对应 IOBoard - // ================================================================================= - /// - /// 针对指定台架直接写入单个输出开关 + /// 向指定板卡批量写入输出开关状态,直接透传到对应板卡的批量写线圈操作。 /// - public async Task 写输出开关(int 台架序号, byte 站号, ushort 开始地址, bool data) + /// 板卡序号 (1 - 3) + /// Modbus 从站站号 + /// 线圈起始地址 (0-15) + /// 开关状态数组 (true: 开启吸合, false: 关闭断开),例如 [true, true, false] + /// 异步取消令牌 + public async Task 批量写输出开关(int 板卡序号, byte 站号, ushort 开始地址, bool[] 数据, CancellationToken 取消令牌 = default) { - IOBoard targetBoard = GetBoardInstance(台架序号); - await targetBoard.写输出开关(站号, 开始地址, data); + await GetBoardInstance(板卡序号).批量写输出开关(站号, 开始地址, 数据); } - /// - /// 针对指定台架直接批量写入输出开关 - /// - public async Task 批量写输出开关(int 台架序号, byte 站号, ushort 开始地址, bool[] datas) - { - IOBoard targetBoard = GetBoardInstance(台架序号); - await targetBoard.批量写输出开关(站号, 开始地址, datas); - } - - /// - /// 针对指定台架直接读取单个输出开关状态 - /// - public async Task 读输出开关(int 台架序号, byte 站号, ushort 开始地址) - { - IOBoard targetBoard = GetBoardInstance(台架序号); - await targetBoard.读输出开关(站号, 开始地址); - } - - /// - /// 针对指定台架直接批量读取输出开关状态 - /// - public async Task 批量读输出开关(int 台架序号, byte 站号, ushort 开始地址, ushort 数量) - { - IOBoard targetBoard = GetBoardInstance(台架序号); - return await targetBoard.批量读输出开关(站号, 开始地址, 数量); - } - - /// - /// 内部路由辅助方法:根据板卡/台架索引安全获取当前连接的实例对象 + /// 内部路由辅助方法:根据板卡序号安全获取对应的 IO 板卡实例。 /// + /// 板卡序号 (1 - 3) + /// 对应的 IO 板卡实例 + /// 板卡序号不在 1-3 范围内时抛出 + /// 板卡实例未初始化时抛出 private IOBoard GetBoardInstance(int moduleIndex) { IOBoard board = moduleIndex switch diff --git a/DeviceCommand/Devices/IT6720.cs b/DeviceCommand/Devices/IT6720.cs index cfa996c..53043ba 100644 --- a/DeviceCommand/Devices/IT6720.cs +++ b/DeviceCommand/Devices/IT6720.cs @@ -133,6 +133,10 @@ namespace DeviceCommand.Devices throw new Exception("IT6720 切换控制模式时通讯校验失败。"); } + /// + /// 切换为本地(面板)控制模式,与远程控制互斥 + /// + /// 取消令牌 public virtual async Task 切换本地控制模式(CancellationToken ct = default) { await 切换远程控制模式(false, ct); @@ -150,6 +154,11 @@ namespace DeviceCommand.Devices throw new Exception("IT6720 设置输出时通讯校验失败。"); } + /// + /// 查询电源输出开关状态(由实时测量值的状态字节判断) + /// + /// 取消令牌 + /// 输出开关状态字符串 (ON / OFF) public virtual async Task 查询输出开关状态(CancellationToken ct = default) { var (_, _, status) = await 查询实时测量值(ct); @@ -230,18 +239,28 @@ namespace DeviceCommand.Devices return (actualCurrent, actualVoltage, statusByte); } + /// + /// 查询电源的实时实际输出电压 (MEASure 相关寄存器) + /// + /// 取消令牌 + /// 实时电压值 (单位: V,保留两位小数) [Monitorable("IT6720 实际电压")] - public virtual async Task 查询实际电压(CancellationToken ct = default) + public virtual async Task 查询实际电压(CancellationToken ct = default) { var (_, voltage, _) = await 查询实时测量值(ct); - return voltage.ToString("F2", CultureInfo.InvariantCulture); + return Math.Round(voltage, 2); } + /// + /// 查询电源的实时实际输出电流 (MEASure 相关寄存器) + /// + /// 取消令牌 + /// 实时电流值 (单位: A,保留两位小数) [Monitorable("IT6720 实际电流")] - public virtual async Task 查询实际电流(CancellationToken ct = default) + public virtual async Task 查询实际电流(CancellationToken ct = default) { var (current, _, _) = await 查询实时测量值(ct); - return current.ToString("F3", CultureInfo.InvariantCulture); + return Math.Round(current, 2); } /// diff --git a/DeviceCommand/Devices/MCc30W.cs b/DeviceCommand/Devices/MCc30W.cs index 4d5b718..d764dd7 100644 --- a/DeviceCommand/Devices/MCc30W.cs +++ b/DeviceCommand/Devices/MCc30W.cs @@ -9,10 +9,15 @@ using System.Threading.Tasks; namespace DeviceCommand.Devices { /// - /// 水冷机 一拖三 + /// MCc30W 水冷机(一拖三控制),基于 Modbus TCP 协议通信。 + /// 用于被测产品的冷却温控,预留后续读写温度设定、启停控制等寄存器操作。 /// public class MCc30W:ModbusTcp { + /// + /// 构造函数,初始化 Modbus TCP 连接配置。 + /// + /// TCP 连接配置(IP、端口等) public MCc30W(TcpConfig config) : base(config) { } diff --git a/DeviceCommand/Devices/PW8001.cs b/DeviceCommand/Devices/PW8001.cs index 483620b..b783e91 100644 --- a/DeviceCommand/Devices/PW8001.cs +++ b/DeviceCommand/Devices/PW8001.cs @@ -2,13 +2,16 @@ using Common.Attributes; using DeviceCommand.Base; using Model.Models; +using System; +using System.Globalization; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace DeviceCommand.Device { /// - /// 功率分析仪(型号:HIOKI PW8001) 一拖三 + /// 功率分析仪(型号:HIOKI PW8001) 一拖三,支持多通道电压/电流/功率及谐波测量 /// [ACPCommand] public class PW8001 : Tcp @@ -25,23 +28,58 @@ namespace DeviceCommand.Device /// public string? SessionKey { get; set; } + /// + /// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。 + /// 支持科学计数法(如 2.48E-03),结果保留两位小数。 + /// + /// 设备返回的原始字符串 + /// 提取到的数值(保留两位小数),解析失败时返回 0 + private static double ExtractDouble(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return 0; + + // 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头 + var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?"); + return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val) + ? Math.Round(val, 2) + : 0; + } + #region 1. IEEE 488.2 基础标准命令 + /// + /// 查询仪器识别信息 (*IDN? 指令) + /// + /// 取消令牌 + /// 设备标识字符串 (制造商, 型号, 序列号, 固件版本) public virtual async Task 查询机器信息(CancellationToken ct = default) { return await WriteReadAsync("*IDN?\r\n", "\n", ct); } + /// + /// 查询仪器已安装的硬件选件 (*OPT? 指令) + /// + /// 取消令牌 + /// 硬件选件列表字符串 public virtual async Task 查询硬件选项(CancellationToken ct = default) { return await WriteReadAsync("*OPT?\r\n", "\n", ct); } + /// + /// 复位仪器到默认状态 (*RST 指令) + /// + /// 取消令牌 public virtual async Task 复位仪器(CancellationToken ct = default) { await SendAsync("*RST\r\n", ct); } + /// + /// 清除状态寄存器和错误队列 (*CLS 指令) + /// + /// 取消令牌 public virtual async Task 清除状态(CancellationToken ct = default) { await SendAsync("*CLS\r\n", ct); @@ -51,66 +89,129 @@ namespace DeviceCommand.Device #region 2. 仪器系统与测试模式设置设置 (System & Mode) + /// + /// 设置测试模式为 WIDE 宽带测量模式 (:MODE WIDE 指令) + /// + /// 取消令牌 public virtual async Task 设置测试模式_WIDE(CancellationToken ct = default) { await SendAsync(":MODE WIDE\r\n", ct); } + /// + /// 设置测试模式为 IEC 标准测量模式 (:MODE IEC 指令) + /// + /// 取消令牌 public virtual async Task 设置测试模式_IEC(CancellationToken ct = default) { await SendAsync(":MODE IEC\r\n", ct); } + /// + /// 查询当前测试模式 (:MODE? 指令) + /// + /// 取消令牌 + /// 测试模式字符串 (WIDE / IEC) public virtual async Task 查询测试模式(CancellationToken ct = default) { return await WriteReadAsync(":MODE?\r\n", "\n", ct); } + /// + /// 设置同步源 (:SYNC:SOURce 指令) + /// + /// 同步源标识 + /// 取消令牌 public virtual async Task 设置同步源(string 源, CancellationToken ct = default) { await SendAsync($":SYNC:SOURce {源}\r\n", ct); } + /// + /// 查询当前同步源 (:SYNC:SOURce? 指令) + /// + /// 取消令牌 + /// 同步源标识字符串 public virtual async Task 查询同步源(CancellationToken ct = default) { return await WriteReadAsync(":SYNC:SOURce?\r\n", "\n", ct); } + /// + /// 设置零点调整状态 (:ZERO 指令) + /// + /// 零点调整状态 (ON / OFF) + /// 取消令牌 public virtual async Task 设置零点调整(string 状态, CancellationToken ct = default) { await SendAsync($":ZERO {状态}\r\n", ct); } + /// + /// 查询零点调整状态 (:ZERO? 指令) + /// + /// 取消令牌 + /// 零点调整状态字符串 (ON / OFF) public virtual async Task 查询零点调整(CancellationToken ct = default) { return await WriteReadAsync(":ZERO?\r\n", "\n", ct); } + /// + /// 设置零点抑制状态 (:ZSP 指令) + /// + /// 零点抑制状态 (ON / OFF) + /// 取消令牌 public virtual async Task 设置零点抑制(string 状态, CancellationToken ct = default) { await SendAsync($":ZSP {状态}\r\n", ct); } + /// + /// 查询零点抑制状态 (:ZSP? 指令) + /// + /// 取消令牌 + /// 零点抑制状态字符串 (ON / OFF) public virtual async Task 查询零点抑制(CancellationToken ct = default) { return await WriteReadAsync(":ZSP?\r\n", "\n", ct); } + /// + /// 设置响应消息头开关 (:HEADer 指令) + /// + /// 消息头开关状态 (ON / OFF) + /// 取消令牌 public virtual async Task 设置响应消息头(string 状态, CancellationToken ct = default) { await SendAsync($":HEADer {状态}\r\n", ct); } + /// + /// 查询响应消息头开关状态 (:HEADer? 指令) + /// + /// 取消令牌 + /// 消息头开关状态字符串 (ON / OFF) public virtual async Task 查询响应消息头(CancellationToken ct = default) { return await WriteReadAsync(":HEADer?\r\n", "\n", ct); } + /// + /// 设置按键锁定状态 (:KLOCk 指令) + /// + /// 按键锁定状态 (ON / OFF) + /// 取消令牌 public virtual async Task 设置按键锁定(string 状态, CancellationToken ct = default) { await SendAsync($":KLOCk {状态}\r\n", ct); } + /// + /// 查询按键锁定状态 (:KLOCk? 指令) + /// + /// 取消令牌 + /// 按键锁定状态字符串 (ON / OFF) public virtual async Task 查询按键锁定(CancellationToken ct = default) { return await WriteReadAsync(":KLOCk?\r\n", "\n", ct); @@ -120,21 +221,43 @@ namespace DeviceCommand.Device #region 3. 积分控制 (Integration) + /// + /// 设置积分控制模式 (:INTEG:CONTROL 指令) + /// + /// 积分控制模式 + /// 取消令牌 public virtual async Task 设置积分控制(string 模式, CancellationToken ct = default) { await SendAsync($":INTEG:CONTROL {模式}\r\n", ct); } + /// + /// 查询积分控制模式 (:INTEG:CONTROL? 指令) + /// + /// 取消令牌 + /// 积分控制模式字符串 public virtual async Task 查询积分控制(CancellationToken ct = default) { return await WriteReadAsync(":INTEG:CONTROL?\r\n", "\n", ct); } + /// + /// 设置指定通道的积分模式 (:INTEG:MODE 指令) + /// + /// 通道编号 + /// 积分模式 + /// 取消令牌 public virtual async Task 设置积分模式(string 通道号, string 模式, CancellationToken ct = default) { await SendAsync($":INTEG:MODE{通道号} {模式}\r\n", ct); } + /// + /// 查询指定通道的积分模式 (:INTEG:MODE? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 积分模式字符串 public virtual async Task 查询积分模式(string 通道号, CancellationToken ct = default) { return await WriteReadAsync($":INTEG:MODE{通道号}?\r\n", "\n", ct); @@ -146,85 +269,188 @@ namespace DeviceCommand.Device // ---------------- 基础测量(不含变比) ---------------- - public virtual async Task 查询电压_不含变比(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电压测量值(不含变比) (:MEAS:U? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电压测量值 (单位: V,保留两位小数) + public virtual async Task 查询电压_不含变比(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:U{通道号}?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:U{通道号}?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询电流_不含变比(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电流测量值(不含变比) (:MEAS:I? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电流测量值 (单位: A,保留两位小数) + public virtual async Task 查询电流_不含变比(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:I{通道号}?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:I{通道号}?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询功率_不含变比(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的功率测量值(不含变比) (:MEAS:P? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 功率测量值 (单位: W,保留两位小数) + public virtual async Task 查询功率_不含变比(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:P{通道号}?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:P{通道号}?\r\n", "\n", ct); + return ExtractDouble(resp); } // ---------------- 基础测量(含变比 CT) ---------------- - public virtual async Task 查询电压_含变比(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电压测量值(含变比) (:MEAS:U:CT? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电压测量值 (单位: V,保留两位小数) + public virtual async Task 查询电压_含变比(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:U{通道号}:CT?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:U{通道号}:CT?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询电流_含变比(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电流测量值(含变比) (:MEAS:I:CT? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电流测量值 (单位: A,保留两位小数) + public virtual async Task 查询电流_含变比(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:I{通道号}:CT?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:I{通道号}:CT?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询功率_含变比(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的功率测量值(含变比) (:MEAS:P:CT? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 功率测量值 (单位: W,保留两位小数) + public virtual async Task 查询功率_含变比(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:P{通道号}:CT?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:P{通道号}:CT?\r\n", "\n", ct); + return ExtractDouble(resp); } // ---------------- 其他高阶参数测量 ---------------- - public virtual async Task 查询有功功率积分(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的有功功率积分值 (:MEAS:WP? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 有功功率积分值 (单位: Wh,保留两位小数) + public virtual async Task 查询有功功率积分(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:WP{通道号}?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:WP{通道号}?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询电压THD(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电压总谐波失真 (THD) (:MEAS:UTHD? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电压 THD 值 (单位: %,保留两位小数) + public virtual async Task 查询电压THD(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:UTHD{通道号}?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:UTHD{通道号}?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询电流THD(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电流总谐波失真 (THD) (:MEAS:ITHD? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电流 THD 值 (单位: %,保留两位小数) + public virtual async Task 查询电流THD(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:ITHD{通道号}?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:ITHD{通道号}?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询线电压U12(CancellationToken ct = default) + /// + /// 查询线电压 U12 (:MEAS:U12? 指令) + /// + /// 取消令牌 + /// 线电压 U12 值 (单位: V,保留两位小数) + public virtual async Task 查询线电压U12(CancellationToken ct = default) { - return await WriteReadAsync(":MEAS:U12?\r\n", "\n", ct); + string resp = await WriteReadAsync(":MEAS:U12?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询总功率P123(CancellationToken ct = default) + /// + /// 查询三相总功率 P123 (:MEAS:P123? 指令) + /// + /// 取消令牌 + /// 三相总功率值 (单位: W,保留两位小数) + public virtual async Task 查询总功率P123(CancellationToken ct = default) { - return await WriteReadAsync(":MEAS:P123?\r\n", "\n", ct); + string resp = await WriteReadAsync(":MEAS:P123?\r\n", "\n", ct); + return ExtractDouble(resp); } // ---------------- 整流平均值 ---------------- - public virtual async Task 查询电压整流平均值(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电压整流平均值(不含变比) (:MEAS:UMN? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电压整流平均值 (单位: V,保留两位小数) + public virtual async Task 查询电压整流平均值(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:UMN{通道号}?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:UMN{通道号}?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询电流整流平均值(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电流整流平均值(不含变比) (:MEAS:IMN? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电流整流平均值 (单位: A,保留两位小数) + public virtual async Task 查询电流整流平均值(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:IMN{通道号}?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:IMN{通道号}?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询电压整流平均值_含变比(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电压整流平均值(含变比) (:MEAS:UMN:CT? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电压整流平均值 (单位: V,保留两位小数) + public virtual async Task 查询电压整流平均值_含变比(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:UMN{通道号}:CT?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:UMN{通道号}:CT?\r\n", "\n", ct); + return ExtractDouble(resp); } - public virtual async Task 查询电流整流平均值_含变比(int 通道号, CancellationToken ct = default) + /// + /// 查询指定通道的电流整流平均值(含变比) (:MEAS:IMN:CT? 指令) + /// + /// 通道编号 + /// 取消令牌 + /// 电流整流平均值 (单位: A,保留两位小数) + public virtual async Task 查询电流整流平均值_含变比(int 通道号, CancellationToken ct = default) { - return await WriteReadAsync($":MEAS:IMN{通道号}:CT?\r\n", "\n", ct); + string resp = await WriteReadAsync($":MEAS:IMN{通道号}:CT?\r\n", "\n", ct); + return ExtractDouble(resp); } #endregion @@ -232,8 +458,10 @@ namespace DeviceCommand.Device #region 5. 自定义透传指令 /// - /// 发送自定义命令 + /// 发送自定义命令(透传任意 SCPI 指令) /// + /// 自定义命令字符串 + /// 取消令牌 public virtual async Task 发送自定义命令(string 命令, CancellationToken ct = default) { await SendAsync($"{命令}\r\n", ct); @@ -241,4 +469,4 @@ namespace DeviceCommand.Device #endregion } -} \ No newline at end of file +} diff --git a/DeviceCommand/Devices/RLT1000.cs b/DeviceCommand/Devices/RLT1000.cs index 0950a93..613f62a 100644 --- a/DeviceCommand/Devices/RLT1000.cs +++ b/DeviceCommand/Devices/RLT1000.cs @@ -8,30 +8,45 @@ using System.Threading.Tasks; namespace DeviceCommand.Devices { /// - /// 环境箱(型号:RLT1000) 一拖三 + /// 环境箱(型号:RLT1000) 一拖三,支持温度/湿度/水温/水流量的监测与定值控制 (Modbus TCP 协议) /// [ACPCommand] public class RLT1000 : ModbusTcp { + /// + /// 构造函数:传入 初始化环境箱 Modbus TCP 通信参数 + /// public RLT1000(TcpConfig config) : base(config) { } + + // Modbus 从站号 private const byte SlaveId = 1; #region 读取 + /// + /// 读取环境箱运行状态监控字 (保持寄存器 5) + /// + /// 取消令牌 + /// 状态监控字 (ushort 原始值) public async Task 读取状态监控(CancellationToken ct = default) { ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 5, 1, ct); return re[0]; } - public async Task 读取当前温度(CancellationToken ct = default) + /// + /// 读取环境箱当前温度值 (保持寄存器 10-11) + /// + /// 取消令牌 + /// 当前温度值 (单位: ℃,保留两位小数) + public async Task 读取当前温度(CancellationToken ct = default) { try { ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 10, 2, ct); - return ConvertToFloat(re); + return Math.Round(ConvertToFloat(re), 2); } catch (Exception) { @@ -39,18 +54,28 @@ namespace DeviceCommand.Devices } } - public async Task 读取当前湿度(CancellationToken ct = default) + /// + /// 读取环境箱当前湿度值 (保持寄存器 16-17) + /// + /// 取消令牌 + /// 当前湿度值 (单位: %RH,保留两位小数) + public async Task 读取当前湿度(CancellationToken ct = default) { ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 16, 2, ct); - return ConvertToFloat(re); + return Math.Round(ConvertToFloat(re), 2); } - public async Task 读取当前水温(CancellationToken ct = default) + /// + /// 读取环境箱当前水温值 (保持寄存器 22-23) + /// + /// 取消令牌 + /// 当前水温值 (单位: ℃,保留两位小数) + public async Task 读取当前水温(CancellationToken ct = default) { try { ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 22, 2, ct); - return ConvertToFloat(re); + return Math.Round(ConvertToFloat(re), 2); } catch (Exception) { @@ -58,119 +83,212 @@ namespace DeviceCommand.Devices } } - public async Task 读取当前1号水流量值(CancellationToken ct = default) + /// + /// 读取当前 1 号水流量值 (保持寄存器 28-29) + /// + /// 取消令牌 + /// 1 号水流量值 (保留两位小数) + public async Task 读取当前1号水流量值(CancellationToken ct = default) { ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 28, 2, ct); - return ConvertToFloat(re); + return Math.Round(ConvertToFloat(re), 2); } - public async Task 读取当前2号水流量值(CancellationToken ct = default) + /// + /// 读取当前 2 号水流量值 (保持寄存器 34-35) + /// + /// 取消令牌 + /// 2 号水流量值 (保留两位小数) + public async Task 读取当前2号水流量值(CancellationToken ct = default) { ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 34, 2, ct); - return ConvertToFloat(re); + return Math.Round(ConvertToFloat(re), 2); } - public async Task 读取当前3号水流量值(CancellationToken ct = default) + /// + /// 读取当前 3 号水流量值 (保持寄存器 40-41) + /// + /// 取消令牌 + /// 3 号水流量值 (保留两位小数) + public async Task 读取当前3号水流量值(CancellationToken ct = default) { ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 40, 2, ct); - return ConvertToFloat(re); + return Math.Round(ConvertToFloat(re), 2); } #endregion #region 读写(控制命令) + /// + /// 切换环境箱为本地控制模式 (寄存器 147 写 0) + /// + /// 取消令牌 public async Task 切换为本地模式(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 147, 0, ct); } + /// + /// 切换环境箱为远程控制模式 (寄存器 147 写 1) + /// + /// 取消令牌 public async Task 切换为远程模式(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 147, 1, ct); } + /// + /// 远程模式下关闭环境箱 (寄存器 148 写 0) + /// + /// 取消令牌 public async Task 远程模式关机(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 148, 0, ct); } + /// + /// 远程模式下开启环境箱 (寄存器 148 写 1) + /// + /// 取消令牌 public async Task 远程模式开机(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 148, 1, ct); } + /// + /// 远程模式复位结束 (寄存器 149 写 0) + /// + /// 取消令牌 public async Task 远程模式复位结束(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 149, 0, ct); } + /// + /// 远程模式复位 (寄存器 149 写 1) + /// + /// 取消令牌 public async Task 远程模式复位(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 149, 1, ct); } + /// + /// 切换为程序运行模式 (寄存器 153 写 0) + /// + /// 取消令牌 public async Task 切换为程序模式(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 153, 0, ct); } + /// + /// 切换为定值运行模式 (寄存器 153 写 1) + /// + /// 取消令牌 public async Task 切换为定值模式(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 153, 1, ct); } + /// + /// 定值模式下设置目标温度 (寄存器 154-155) + /// + /// 目标温度值 (单位: ℃) + /// 取消令牌 public async Task 定值温度设定(float 温度, CancellationToken ct = default) { var tmp = ConvertFromFloat(温度); await WriteMultipleRegistersAsync(SlaveId, 154, tmp, ct); } + /// + /// 定值模式下设置目标湿度 (寄存器 158-159) + /// + /// 目标湿度值 (单位: %RH) + /// 取消令牌 public async Task 定值湿度设定(float 湿度, CancellationToken ct = default) { var tmp = ConvertFromFloat(湿度); await WriteMultipleRegistersAsync(SlaveId, 158, tmp, ct); } + /// + /// 定值模式下设置目标水温 (寄存器 162-163) + /// + /// 目标水温值 (单位: ℃) + /// 取消令牌 public async Task 定值水温设定(float 水温, CancellationToken ct = default) { var tmp = ConvertFromFloat(水温); await WriteMultipleRegistersAsync(SlaveId, 162, tmp, ct); } + /// + /// 定值模式下设置 1 号水流量目标值 (寄存器 164-165) + /// + /// 1 号水流量目标值 + /// 取消令牌 public async Task 定值1号水流量设定(float 水流量, CancellationToken ct = default) { var tmp = ConvertFromFloat(水流量); await WriteMultipleRegistersAsync(SlaveId, 164, tmp, ct); } + /// + /// 定值模式下设置 2 号水流量目标值 (寄存器 166-167) + /// + /// 2 号水流量目标值 + /// 取消令牌 public async Task 定值2号水流量设定(float 水流量, CancellationToken ct = default) { var tmp = ConvertFromFloat(水流量); await WriteMultipleRegistersAsync(SlaveId, 166, tmp, ct); } + /// + /// 定值模式下设置 3 号水流量目标值 (寄存器 168-169) + /// + /// 3 号水流量目标值 + /// 取消令牌 public async Task 定值3号水流量设定(float 水流量, CancellationToken ct = default) { var tmp = ConvertFromFloat(水流量); await WriteMultipleRegistersAsync(SlaveId, 168, tmp, ct); } + /// + /// 设置水温模式不使用 (寄存器 1181 写 0) + /// + /// 取消令牌 public async Task 水温模式不使用(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 1181, 0, ct); } + /// + /// 设置水温模式使用 (寄存器 1181 写 1) + /// + /// 取消令牌 public async Task 水温模式使用(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 1181, 1, ct); } + /// + /// 设置环境箱温度模式不使用 (寄存器 1182 写 0) + /// + /// 取消令牌 public async Task 环境箱温度模式不使用(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 1182, 0, ct); } + /// + /// 设置环境箱温度模式使用 (寄存器 1182 写 1) + /// + /// 取消令牌 public async Task 环境箱温度模式使用(CancellationToken ct = default) { await WriteSingleRegisterAsync(SlaveId, 1182, 1, ct); @@ -180,6 +298,11 @@ namespace DeviceCommand.Devices #region 数据转换助手方法 + /// + /// 将两个 16 位寄存器(大端序)转换为 IEEE754 单精度浮点数 + /// + /// 两个 ushort 寄存器值 + /// 转换后的浮点数值 private float ConvertToFloat(ushort[] values) { if (values == null || values.Length < 2) return 0f; @@ -192,6 +315,11 @@ namespace DeviceCommand.Devices return BitConverter.ToSingle(bytes, 0); } + /// + /// 将单精度浮点数转换为两个 16 位寄存器(大端序) + /// + /// 待转换的浮点数值 + /// 两个 ushort 寄存器值 private ushort[] ConvertFromFloat(float value) { byte[] bytes = BitConverter.GetBytes(value); @@ -203,4 +331,4 @@ namespace DeviceCommand.Devices #endregion } -} \ No newline at end of file +} diff --git a/DeviceCommand/Devices/S7200.cs b/DeviceCommand/Devices/S7200.cs index 357b217..f0a2397 100644 --- a/DeviceCommand/Devices/S7200.cs +++ b/DeviceCommand/Devices/S7200.cs @@ -1,65 +1,119 @@ using Common.Attributes; using DeviceCommand.Base; +using Model.Models; using System; +using System.Globalization; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -// 根据您的项目实际路径保留枚举的引用 -// using 通讯驱动Base.枚举; namespace DeviceCommand.Devices { + /// + /// S7200 负载工作模式枚举 (恒流/恒压/恒功率/恒阻等) + /// public enum S7200负载模式_枚举 { + /// 恒流模式 CC, + /// 恒压模式 CV, + /// 恒功率模式 CP, + /// 恒阻模式 CR, + /// 恒压+恒流组合模式 CVCC, + /// 恒阻+恒流组合模式 CRCC, + /// 恒压+恒阻组合模式 CVCR, + /// 自动模式 AUTO } + + /// + /// S7200 直流源工作模式枚举 (电压源/电流源/电池模拟/光伏模拟等) + /// public enum S7200直流源模式_枚举 { + /// 电压源模式 VOLTage, + /// 电流源模式 CURRent, + /// 电池测试模式 BATTest, + /// 电池模拟模式 BATSim, + /// 光伏模拟模式 SOLar, + /// EN50530 标准光伏曲线模式 EN50530, + /// 列表输出模式 LIST, + /// 用户自定义波形模式 UDW, + /// 方波输出模式 SQUare, + /// 三角波输出模式 TRIangle, + /// 正弦波输出模式 SINusoid, + /// DIN40839 12V 启动曲线 DIN40839_12V, + /// DIN40839 24V 启动曲线 DIN40839_24V, + /// ISO16750 短时跌落 12V IS016750_SHORTDROP_12V, + /// ISO16750 短时跌落 24V IS016750_SHORTDROP_24V, + /// ISO16750 复位测试 IS016750_RESETTEST, + /// ISO16750 启动曲线 12V 第 1 组 IS016750_STARTINGPROFILE_12V_1, + /// ISO16750 启动曲线 12V 第 2 组 IS016750_STARTINGPROFILE_12V_2, + /// ISO16750 启动曲线 12V 第 3 组 IS016750_STARTINGPROFILE_12V_3, + /// ISO16750 启动曲线 12V 第 4 组 IS016750_STARTINGPROFILE_12V_4, + /// ISO16750 启动曲线 24V 第 1 组 IS016750_STARTINGPROFILE_24V_1, + /// ISO16750 启动曲线 24V 第 2 组 IS016750_STARTINGPROFILE_24V_2, + /// ISO16750 启动曲线 24V 第 3 组 IS016750_STARTINGPROFILE_24V_3, + /// ISO16750 抛负载测试 A 12V IS016750_LOADDUMP_TESTA_12V, + /// ISO16750 抛负载测试 A 24V IS016750_LOADDUMP_TESTA_24V, + /// ISO16750 抛负载测试 B 12V IS016750_LOADDUMP_TESTB_12V, + /// ISO16750 抛负载测试 B 24V IS016750_LOADDUMP_TESTB_24V, + /// LV123 高压曲线 1 LV123_HV_1, + /// LV123 高压曲线 2A LV123_HV_2A, + /// LV123 高压曲线 2B LV123_HV_2B, + /// LV123 高压曲线 3 LV123_HV_3 } + + /// + /// S7200 工作模式枚举 (电源模式 / 负载模式) + /// public enum S7200工作模式_枚举 { + /// 作为电源输出 电源模式, + /// 作为负载吸收 负载模式, } + /// - /// 高压直流源载一体机 (适配 IT6000C SCPI指令集) + /// 高压直流源载一体机 (适配 IT6000C SCPI 指令集),支持电源输出与负载吸收双向工作 /// [ACPCommand] public class S7200 : Tcp @@ -67,115 +121,239 @@ namespace DeviceCommand.Devices // SCPI 指令结束符 private const string ScpiDelimiter = "\n"; - #region 设置 - - public async Task 设置为远程模式(CancellationToken ct = default) + /// + /// 构造函数:传入 一次性初始化一体机通信参数 + /// + public S7200(TcpConfig config) : base(config) { - await SendAsync($"SYSTem:REMote{ScpiDelimiter}", ct); //[cite: 1] } + /// + /// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。 + /// 支持科学计数法(如 2.48E-03),结果保留两位小数。 + /// + /// 设备返回的原始字符串 + /// 提取到的数值(保留两位小数),解析失败时返回 0 + private static double ExtractDouble(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return 0; + + // 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头 + var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?"); + return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val) + ? Math.Round(val, 2) + : 0; + } + + #region 设置 + + /// + /// 设置仪器为远程控制模式 (SYSTem:REMote) + /// + /// 取消令牌 + public async Task 设置为远程模式(CancellationToken ct = default) + { + await SendAsync($"SYSTem:REMote{ScpiDelimiter}", ct); + } + + /// + /// 设置电源工作模式 (电源/负载)。IT6000C 支持双向无缝切换,无需显式下发源载切换指令 + /// + /// 工作模式枚举 (电源模式 / 负载模式) + /// 取消令牌 public async Task 设置电源工作模式(S7200工作模式_枚举 开关, CancellationToken ct = default) { // IT6000C 为双向无缝切换,底层硬件无需显式发送源载切换指令 - // 保留此方法仅为兼容您原有上位机的工步调用流程,直接返回即可 + // 保留此方法仅为兼容原有上位机的工步调用流程 await Task.CompletedTask; } + /// + /// 设置直流源工作模式 (FUNCtion 指令,如 VOLTage / CURRent / SOLar 等) + /// + /// 直流源工作模式枚举 + /// 取消令牌 public async Task 设置直流源工作模式(S7200直流源模式_枚举 模式, CancellationToken ct = default) { - // 原指令: MODE {模式} -> 现 IT6000C 指令: FUNCtion {模式} (如 VOLTage / CURRent)[cite: 1] - // 若枚举 ToString() 后不匹配,建议在此处加 Switch 转换 await SendAsync($"FUNCtion {模式}{ScpiDelimiter}", ct); } + /// + /// 设置负载工作模式。IT6000C 负载控制通过负向边界实现 + /// + /// 负载工作模式枚举 + /// 取消令牌 public async Task 设置负载工作模式(S7200负载模式_枚举 模式, CancellationToken ct = default) { - // IT6000C 负载控制通过负向边界实现。若是想开启纯电阻模式,可调用 SINK:RESistance:STATe ON[cite: 1] + // 若想开启纯电阻模式,可调用 SINK:RESistance:STATe ON await Task.CompletedTask; } + /// + /// 开启或关闭通道输出 (OUTPut 指令) + /// + /// true: 开启输出, false: 关闭输出 + /// 取消令牌 public async Task 设置通道开关(bool 开关, CancellationToken ct = default) { - await SendAsync($"OUTPut {(开关 ? 1 : 0)}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"OUTPut {(开关 ? 1 : 0)}{ScpiDelimiter}", ct); } + /// + /// 设置恒流 (CC) 模式的电流值 (CURRent 指令) + /// + /// 电流设定值 (单位: A) + /// 取消令牌 public async Task 设置CC模式电流(double 电流, CancellationToken ct = default) { - await SendAsync($"CURRent {电流}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"CURRent {电流}{ScpiDelimiter}", ct); } - public async Task 设置CC电压H(double 电流, CancellationToken ct = default) // 沿用原参数名“电流”,实际是设定电压上限 + /// + /// 设置恒流 (CC) 模式的电压上限 (VOLTage:LIMit:POSitive 指令) + /// + /// 电压上限设定值 (单位: V),沿用原参数名 + /// 取消令牌 + public async Task 设置CC电压H(double 电流, CancellationToken ct = default) { - await SendAsync($"VOLTage:LIMit:POSitive {电流}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"VOLTage:LIMit:POSitive {电流}{ScpiDelimiter}", ct); } - public async Task 设置CC电压L(double 电流, CancellationToken ct = default) // 沿用原参数名“电流”,实际是设定电压下限 + /// + /// 设置恒流 (CC) 模式的电压下限 (VOLTage:LIMit:NEGative 指令) + /// + /// 电压下限设定值 (单位: V),沿用原参数名 + /// 取消令牌 + public async Task 设置CC电压L(double 电流, CancellationToken ct = default) { - await SendAsync($"VOLTage:LIMit:NEGative {电流}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"VOLTage:LIMit:NEGative {电流}{ScpiDelimiter}", ct); } + /// + /// 设置恒压 (CV) 模式的正向电流限制 (CURRent:LIMit:POSitive 指令) + /// + /// 正向电流限制值 (单位: A) + /// 取消令牌 public async Task 设置CV正向电流(double 电流, CancellationToken ct = default) { - await SendAsync($"CURRent:LIMit:POSitive {电流}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"CURRent:LIMit:POSitive {电流}{ScpiDelimiter}", ct); } + /// + /// 设置恒压 (CV) 模式的反向电流限制 (CURRent:LIMit:NEGative 指令) + /// + /// 反向电流限制值 (单位: A),负载模式需传负值 + /// 取消令牌 public async Task 设置CV反向电流(double 电流, CancellationToken ct = default) { - // 提醒: 此处传入的参数如果是正数,建议在外部或此处转换为负值,因为载模式限制需要负数[cite: 1] + // 载模式限制需要负数,若传入正数建议在外部转换为负值 await SendAsync($"CURRent:LIMit:NEGative {电流}{ScpiDelimiter}", ct); } + /// + /// 设置正向功率阈值 (POWer:LIMit:POSitive 指令) + /// + /// 正向功率阈值 (单位: W) + /// 取消令牌 public async Task 设置正向功率阈值(double 功率, CancellationToken ct = default) { - await SendAsync($"POWer:LIMit:POSitive {功率}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"POWer:LIMit:POSitive {功率}{ScpiDelimiter}", ct); } + /// + /// 设置反向功率阈值 (POWer:LIMit:NEGative 指令) + /// + /// 反向功率阈值 (单位: W),必须为负数 + /// 取消令牌 public async Task 设置反向功率阈值(double 功率, CancellationToken ct = default) { - // 提醒: 此处传入的参数必须为负数[cite: 1] await SendAsync($"POWer:LIMit:NEGative {功率}{ScpiDelimiter}", ct); } + /// + /// 设置输出电压值 (VOLTage 指令) + /// + /// 电压设定值 (单位: V) + /// 取消令牌 public async Task 设置电压(double 电压, CancellationToken ct = default) { - await SendAsync($"VOLTage {电压}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"VOLTage {电压}{ScpiDelimiter}", ct); } + /// + /// 清除保护状态 (OUTPut:PROTection:CLEar 指令) + /// + /// 取消令牌 public async Task 清除保护状态(CancellationToken ct = default) { - await SendAsync($"OUTPut:PROTection:CLEar{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"OUTPut:PROTection:CLEar{ScpiDelimiter}", ct); } + /// + /// 开启或关闭过流保护 (OCP) 功能 (CURRent:PROTection:STATe 指令) + /// + /// true: 开启过流保护, false: 关闭过流保护 + /// 取消令牌 public async Task 设置OCP开关(bool v, CancellationToken ct = default) { - await SendAsync($"CURRent:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"CURRent:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); } + /// + /// 开启或关闭过压保护 (OVP) 功能 (VOLTage:PROTection:STATe 指令) + /// + /// true: 开启过压保护, false: 关闭过压保护 + /// 取消令牌 public async Task 设置OVP开关(bool v, CancellationToken ct = default) { - await SendAsync($"VOLTage:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"VOLTage:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); } + /// + /// 设置过压保护 (OVP) 的触发电压值 (VOLTage:PROTection 指令) + /// + /// 过压保护触发电压值 (单位: V) + /// 取消令牌 public async Task 设置电压保护OVP电压(double oVP电压, CancellationToken ct = default) { - await SendAsync($"VOLTage:PROTection {oVP电压}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"VOLTage:PROTection {oVP电压}{ScpiDelimiter}", ct); } + /// + /// 设置过流保护 (OCP) 的触发电流值 (CURRent:PROTection 指令) + /// + /// 过流保护触发电流值 (单位: A) + /// 取消令牌 public async Task 设置电流保护OCP电流(double oCP电流, CancellationToken ct = default) { - await SendAsync($"CURRent:PROTection {oCP电流}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"CURRent:PROTection {oCP电流}{ScpiDelimiter}", ct); } + /// + /// 开启或关闭过功率保护 (OPP) 功能 (POWer:PROTection:STATe 指令) + /// + /// true: 开启过功率保护, false: 关闭过功率保护 + /// 取消令牌 public async Task 设置功率保护开关(bool v, CancellationToken ct = default) { - await SendAsync($"POWer:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"POWer:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); } + /// + /// 设置过功率保护 (OPP) 的触发功率值 (POWer:PROTection 指令) + /// + /// 过功率保护触发功率值 (单位: W) + /// 取消令牌 public async Task 设置功率保护功率(double oPP功率, CancellationToken ct = default) { - await SendAsync($"POWer:PROTection {oPP功率}{ScpiDelimiter}", ct); //[cite: 1] + await SendAsync($"POWer:PROTection {oPP功率}{ScpiDelimiter}", ct); } + /// + /// 发送自定义 SCPI 指令 + /// + /// 自定义指令字符串 + /// 取消令牌 public async Task 发送自定义命令(string 指令, CancellationToken ct = default) { await SendAsync($"{指令}{ScpiDelimiter}", ct); @@ -185,29 +363,49 @@ namespace DeviceCommand.Devices #region 查询 + /// + /// 查询设备识别信息 (*IDN? 指令) + /// + /// 取消令牌 + /// 设备标识字符串 (制造商, 型号, 序列号, 固件版本) public async Task 查询设备信息(CancellationToken ct = default) { - return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1] + return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); } + /// + /// 查询实时输出电压 (MEASure:VOLTage? 指令) + /// + /// 取消令牌 + /// 实时电压值 (单位: V,保留两位小数) public async Task 查询实时电压(CancellationToken ct = default) { - var str = await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1] - return double.TryParse(str, out double result) ? result : 0; + var str = await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(str); } + /// + /// 查询实时输出电流 (MEASure:CURRent? 指令) + /// + /// 取消令牌 + /// 实时电流值 (单位: A,保留两位小数) public async Task 查询实时电流(CancellationToken ct = default) { - var str = await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1] - return double.TryParse(str, out double result) ? result : 0; + var str = await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(str); } + /// + /// 查询实时功率 (MEASure:POWer? 指令) + /// + /// 取消令牌 + /// 实时功率值 (单位: W,保留两位小数) public async Task 查询功率(CancellationToken ct = default) { - var str = await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1] - return double.TryParse(str, out double result) ? result : 0; + var str = await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(str); } #endregion } -} \ No newline at end of file +} diff --git a/DeviceCommand/Devices/TektronixMSO.cs b/DeviceCommand/Devices/TektronixMSO.cs index 3de0115..0913bd5 100644 --- a/DeviceCommand/Devices/TektronixMSO.cs +++ b/DeviceCommand/Devices/TektronixMSO.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.IO; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -37,22 +38,48 @@ namespace DeviceCommand.Devices /// public enum TekMeasurementType { + /// 幅度 AMPLitude, + /// 频率 FREQuency, + /// 平均值 MEAN, + /// 峰峰值 PK2PK, + /// 最大值 MAXimum, + /// 最小值 MINimum } #endregion + /// + /// Tektronix 示波器 (MSO 4/5/6 系列),支持采集控制、通道设置、自动测量与截图导出 + /// [ACPCommand] public class TektronixMSO : Tcp { // SCPI 指令结束符 (消息终止符需使用 LF) private const string ScpiDelimiter = "\n"; + /// + /// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。 + /// 支持科学计数法(如 2.48E-03),结果保留两位小数。 + /// + /// 设备返回的原始字符串 + /// 提取到的数值(保留两位小数),解析失败时返回 0 + private static double ExtractDouble(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return 0; + + // 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头 + var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?"); + return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val) + ? Math.Round(val, 2) + : 0; + } + #region 兼容旧 MSO44B 枚举(供 ACP 测试项反序列化) public enum AcquireMode { Sample = 0, PeakDetect = 1, HiRes = 2, Average = 3, Envelope = 4 } @@ -81,21 +108,32 @@ namespace DeviceCommand.Devices #region 1. 公共命令与状态查询 + /// + /// 查询仪器识别码 (*IDN? 指令) + /// + /// 取消令牌 + /// 设备标识字符串 (制造商, 型号, 序列号, 固件版本) public virtual async Task 查询设备标识(CancellationToken ct = default) { - // 返回仪器的标识代码[cite: 2] return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); } + /// + /// 查询仪器是否处于忙碌状态,常用于同步操作 (BUSY? 指令) + /// + /// 取消令牌 + /// 忙碌状态字符串 (0: 空闲, 1: 忙碌) public virtual async Task 查询忙碌状态(CancellationToken ct = default) { - // 查询仪器是否处于忙碌状态,常用于同步操作[cite: 2] return await WriteReadAsync($"BUSY?{ScpiDelimiter}", ScpiDelimiter, ct); } + /// + /// 指示仪器执行信号路径校准 (SPC) (*CAL? 指令) + /// + /// 取消令牌 public virtual async Task 执行自校准(CancellationToken ct = default) { - // 指示仪器执行信号路径校准 (SPC)[cite: 2] await SendAsync($"*CAL?{ScpiDelimiter}", ct); } @@ -104,24 +142,33 @@ namespace DeviceCommand.Devices #region 2. 采集系统控制 (Acquisition) /// - /// 启动或停止采集 + /// 启动或停止采集 (ACQuire:STATE 指令) /// + /// true: 启动采集 (RUN), false: 停止采集 (STOP) + /// 取消令牌 public virtual async Task 设置采集状态(bool 运行, CancellationToken ct = default) { - // 启动、停止或返回采集状态[cite: 2] string 参数 = 运行 ? "RUN" : "STOP"; await SendAsync($"ACQuire:STATE {参数}{ScpiDelimiter}", ct); } + /// + /// 查询当前采集状态 (ACQuire:STATE? 指令) + /// + /// 取消令牌 + /// 采集状态字符串 (RUN / STOP) public virtual async Task 查询采集状态(CancellationToken ct = default) { - // 启动、停止或返回采集状态[cite: 2] return await WriteReadAsync($"ACQuire:STATE?{ScpiDelimiter}", ScpiDelimiter, ct); } + /// + /// 设置采集模式 (ACQuire:MODe 指令) + /// + /// 采集模式枚举 (采样/峰值检测/高分辨率/平均/包络) + /// 取消令牌 public virtual async Task 设置采集模式(TekAcquisitionMode 模式, CancellationToken ct = default) { - // 设置或查询采集模式[cite: 2] await SendAsync($"ACQuire:MODe {模式}{ScpiDelimiter}", ct); } @@ -129,23 +176,36 @@ namespace DeviceCommand.Devices #region 3. 水平系统控制 (Horizontal) + /// + /// 设置水平刻度 (Scale),即每格代表的时间 (HORizontal:SCAle 指令) + /// + /// 水平刻度值 (单位: s/Div) + /// 取消令牌 public virtual async Task 设置水平刻度(double 秒每格, CancellationToken ct = default) { - // 设置或查询水平刻度 (Scale)[cite: 2] string cmd = string.Format(CultureInfo.InvariantCulture, "HORizontal:SCAle {0:E}{1}", 秒每格, ScpiDelimiter); await SendAsync(cmd, ct); } + /// + /// 设置水平记录长度 (HORizontal:RECOrdlength 指令) + /// + /// 记录长度(采样点数) + /// 取消令牌 public virtual async Task 设置记录长度(long 长度, CancellationToken ct = default) { - // 设置或查询水平记录长度[cite: 2] await SendAsync($"HORizontal:RECOrdlength {长度}{ScpiDelimiter}", ct); } - public virtual async Task 查询采样率(CancellationToken ct = default) + /// + /// 查询水平采样率 (HORizontal:SAMPLERate? 指令) + /// + /// 取消令牌 + /// 水平采样率 (单位: Sa/s,保留两位小数) + public virtual async Task 查询采样率(CancellationToken ct = default) { - // 查询水平采样率[cite: 2] - return await WriteReadAsync($"HORizontal:SAMPLERate?{ScpiDelimiter}", ScpiDelimiter, ct); + string resp = await WriteReadAsync($"HORizontal:SAMPLERate?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); } #endregion @@ -153,18 +213,23 @@ namespace DeviceCommand.Devices #region 4. 通道与显示控制 (Vertical) /// - /// 全局启用或关闭某通道的显示 + /// 全局启用或关闭某通道的显示 (DISplay:GLObal:CH:STATE 指令) /// + /// 通道编号 (1 - 4) + /// true: 开启通道显示, false: 关闭通道显示 + /// 取消令牌 public virtual async Task 设置通道显示开关(int 通道号, bool 开启, CancellationToken ct = default) { - // 4/5/6 Series MSO 标准通道显示指令[cite: 2] string 参数 = 开启 ? "ON" : "OFF"; await SendAsync($"DISplay:GLObal:CH{通道号}:STATE {参数}{ScpiDelimiter}", ct); } /// - /// 设置指定通道的垂直缩放比例 (Volts/Div) + /// 设置指定通道的垂直缩放比例 (CH:SCAle 指令) /// + /// 通道编号 (1 - 4) + /// 垂直刻度值 (单位: V/Div) + /// 取消令牌 public virtual async Task 设置通道垂直刻度(int 通道号, double 伏特每格, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, "CH{0}:SCAle {1:E}{2}", 通道号, 伏特每格, ScpiDelimiter); @@ -172,8 +237,11 @@ namespace DeviceCommand.Devices } /// - /// 设置指定通道的垂直位置 (Divisions) + /// 设置指定通道的垂直位置 (CH:POSition 指令) /// + /// 通道编号 (1 - 4) + /// 垂直位置偏移 (单位: Div) + /// 取消令牌 public virtual async Task 设置通道垂直位置(int 通道号, double 格数, CancellationToken ct = default) { string cmd = string.Format(CultureInfo.InvariantCulture, "CH{0}:POSition {1:F2}{2}", 通道号, 格数, ScpiDelimiter); @@ -185,43 +253,73 @@ namespace DeviceCommand.Devices #region 5. 自动测量 (Measurement) /// - /// 动态添加并开启一个测量项 + /// 动态添加并开启一个测量项 (MEASUrement:ADDNew 指令)。 + /// 4/5/6 Series MSO 必须先动态创建测量项 /// + /// 测量项编号 + /// 取消令牌 public virtual async Task 开启测量项(int 测量项编号, CancellationToken ct = default) { - // 4/5/6 Series MSO 必须先动态创建测量项[cite: 2] await SendAsync($"MEASUrement:ADDNew \"MEAS{测量项编号}\"{ScpiDelimiter}", ct); } + /// + /// 设置测量项的测量类型 (MEASUrement:TYPe 指令) + /// + /// 测量项编号 + /// 测量类型枚举 (幅度/频率/平均值/峰峰值等) + /// 取消令牌 public virtual async Task 设置测量类型(int 测量项编号, TekMeasurementType 类型, CancellationToken ct = default) { - // 设置或查询测量类型[cite: 2] await SendAsync($"MEASUrement:MEAS{测量项编号}:TYPe {类型}{ScpiDelimiter}", ct); } + /// + /// 设置测量项的测量源 (MEASUrement:SOUrce1 指令) + /// + /// 测量项编号 + /// 测量源名称 (如 "CH1", "CH2") + /// 取消令牌 public virtual async Task 设置测量源(int 测量项编号, string 源名称, CancellationToken ct = default) { - // 4/5/6 Series 中设置测量源[cite: 2] await SendAsync($"MEASUrement:MEAS{测量项编号}:SOUrce1 {源名称}{ScpiDelimiter}", ct); } + /// + /// 查询测量项的当前采集平均值 (MEASUrement:RESUlts:CURRentacq:MEAN? 指令) + /// + /// 测量项编号 + /// 取消令牌 + /// 测量项当前采集的平均值 (保留两位小数) [Monitorable("测量项的当前采集平均值")] - public virtual async Task 查询测量项当前值(int 测量项编号, CancellationToken ct = default) + public virtual async Task 查询测量项当前值(int 测量项编号, CancellationToken ct = default) { - // 4/5/6 Series 获取当前采集值的标准方式,查询平均值[cite: 2] - return await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:CURRentacq:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct); + string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:CURRentacq:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); } - public virtual async Task 查询测量项全局平均值(int 测量项编号, CancellationToken ct = default) + /// + /// 查询测量项的全局历史累积平均值 (MEASUrement:RESUlts:ALLAcqs:MEAN? 指令) + /// + /// 测量项编号 + /// 取消令牌 + /// 测量项全局平均值 (保留两位小数) + public virtual async Task 查询测量项全局平均值(int 测量项编号, CancellationToken ct = default) { - // 获取所有采集(AllAcqs)历史累积的平均值[cite: 2] - return await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct); + string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); } - public virtual async Task 查询测量项全局最大值(int 测量项编号, CancellationToken ct = default) + /// + /// 查询测量项的全局历史累积最大值 (MEASUrement:RESUlts:ALLAcqs:MAXimum? 指令) + /// + /// 测量项编号 + /// 取消令牌 + /// 测量项全局最大值 (保留两位小数) + public virtual async Task 查询测量项全局最大值(int 测量项编号, CancellationToken ct = default) { - // 获取所有采集(AllAcqs)历史累积的最大值[cite: 2] - return await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct); + string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct); + return ExtractDouble(resp); } #endregion @@ -237,13 +335,13 @@ namespace DeviceCommand.Devices { string 临时文件 = "E:/temp1.png"; - // 1. 直接保存截图到示波器U盘 (自动识别 .png 扩展名)[cite: 2] + // 1. 直接保存截图到示波器U盘 (自动识别 .png 扩展名) await SendAsync($"SAVe:IMAGe \"{临时文件}\"{ScpiDelimiter}", ct); - // 2. 发送 *OPC? 并等待返回 1,确保图像文件已经完全写入磁盘[cite: 2] + // 2. 发送 *OPC? 并等待返回 1,确保图像文件已经完全写入磁盘 await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct); - // 3. 将文件内容读取到当前通信接口[cite: 2] + // 3. 将文件内容读取到当前通信接口 await SendAsync($"FILESystem:READFile \"{临时文件}\"{ScpiDelimiter}", ct); // 4. 提取底层的二进制块 (Block Data) @@ -252,7 +350,7 @@ namespace DeviceCommand.Devices // 5. 将提取的二进制字节流写入上位机本地磁盘 await File.WriteAllBytesAsync(上位机文件路径, imageBytes, ct); - // 6. 删除示波器上的临时文件,保持设备存储干净[cite: 2] + // 6. 删除示波器上的临时文件,保持设备存储干净 await SendAsync($"FILESystem:DELEte \"{临时文件}\"{ScpiDelimiter}", ct); } @@ -331,41 +429,44 @@ namespace DeviceCommand.Devices #region 7. 状态与错误队列 (Status and Error) /// - /// 清除所有的状态寄存器和错误/事件队列 + /// 清除所有的状态寄存器和错误/事件队列 (*CLS 指令) /// + /// 取消令牌 public virtual async Task 清除错误队列Async(CancellationToken ct = default) { - // 发送 *CLS 指令清空状态[cite: 2] await SendAsync($"*CLS{ScpiDelimiter}", ct); } /// - /// 查询错误队列中当前的事件/错误数量 + /// 查询错误队列中当前的事件/错误数量 (EVQty? 指令) /// + /// 取消令牌 + /// 事件队列中的事件数量字符串 public virtual async Task 查询错误数量Async(CancellationToken ct = default) { - // 返回事件队列中的事件数量 return await WriteReadAsync($"EVQty?{ScpiDelimiter}", ScpiDelimiter, ct); } /// - /// 从错误队列中读取最早的一个事件/错误(包含代码和详细消息),并将其从队列中弹出 + /// 从错误队列中读取最早的一个事件/错误(包含代码和详细消息),并将其从队列中弹出 (EVMsg? 指令) /// + /// 取消令牌 + /// 事件代码和消息字符串 public virtual async Task 读取单条错误消息Async(CancellationToken ct = default) { - // 返回事件队列中的事件代码和消息 return await WriteReadAsync($"EVMsg?{ScpiDelimiter}", ScpiDelimiter, ct); } /// - /// 读取并清空队列中的所有错误和事件消息 + /// 读取并清空队列中的所有错误和事件消息 (ALLEv? 指令) /// + /// 取消令牌 + /// 所有事件及其消息字符串 public virtual async Task 读取所有错误消息Async(CancellationToken ct = default) { - // 返回所有事件及其消息 return await WriteReadAsync($"ALLEv?{ScpiDelimiter}", ScpiDelimiter, ct); } #endregion } -} \ No newline at end of file +} diff --git a/DeviceCommand/Flexible/FModbusRTU.cs b/DeviceCommand/Flexible/FModbusRTU.cs index b5078c2..c478d6d 100644 --- a/DeviceCommand/Flexible/FModbusRTU.cs +++ b/DeviceCommand/Flexible/FModbusRTU.cs @@ -8,11 +8,20 @@ using System.Threading.Tasks; namespace DeviceCommand.Flexible { + /// + /// 灵活型 Modbus RTU 串口通信类(免实例化,每次调用临时创建串口连接)。 + /// 支持保持寄存器与线圈的读写操作,内部使用通信锁保证同一时刻只有一个事务在执行, + /// 适用于偶发性、无需保持长连接的 Modbus RTU 设备读写场景。 + /// [ACPCommand] public static class FModbusRTU { + // 通信锁:保证同一时刻只有一个 Modbus 事务在执行 private static readonly SemaphoreSlim _commLock = new(1, 1); + /// + /// 创建串口实例并配置超时参数。 + /// private static SerialPort CreatePort( string portName, int baudRate, @@ -29,6 +38,9 @@ namespace DeviceCommand.Flexible }; } + /// + /// 基于指定串口创建 Modbus RTU 主站。 + /// private static IModbusMaster CreateMaster(SerialPort port) { return new ModbusFactory().CreateRtuMaster(port); @@ -36,7 +48,22 @@ namespace DeviceCommand.Flexible #region Holding Register - public static async Task ReadHoldingRegistersAsync( + /// + /// 读取保持寄存器(功能码 03),返回指定数量的寄存器值数组。 + /// + /// 串口名称,如 "COM1" + /// 波特率,如 9600、115200 + /// Modbus 从站站号 (1-247) + /// 起始寄存器地址 + /// 读取的寄存器数量 + /// 数据位,默认 8 + /// 停止位,默认 1 位 + /// 校验位,默认无校验 + /// 读取超时时间(毫秒),默认 3000 + /// 写入超时时间(毫秒),默认 3000 + /// 异步取消令牌 + /// 读取到的寄存器值数组 + public static async Task 读保持寄存器( string portName, int baudRate, byte slaveAddress, @@ -73,7 +100,21 @@ namespace DeviceCommand.Flexible } } - public static async Task WriteSingleRegisterAsync( + /// + /// 写入单个保持寄存器(功能码 06)。 + /// + /// 串口名称,如 "COM1" + /// 波特率,如 9600、115200 + /// Modbus 从站站号 (1-247) + /// 要写入的寄存器地址 + /// 要写入的寄存器值 (0-65535) + /// 数据位,默认 8 + /// 停止位,默认 1 位 + /// 校验位,默认无校验 + /// 读取超时时间(毫秒),默认 3000 + /// 写入超时时间(毫秒),默认 3000 + /// 异步取消令牌 + public static async Task 写单个寄存器( string portName, int baudRate, byte slaveAddress, @@ -114,7 +155,22 @@ namespace DeviceCommand.Flexible #region Coil - public static async Task ReadCoilsAsync( + /// + /// 读取线圈状态(功能码 01),返回指定数量的线圈开关状态数组。 + /// + /// 串口名称,如 "COM1" + /// 波特率,如 9600、115200 + /// Modbus 从站站号 (1-247) + /// 起始线圈地址 + /// 读取的线圈数量 + /// 数据位,默认 8 + /// 停止位,默认 1 位 + /// 校验位,默认无校验 + /// 读取超时时间(毫秒),默认 3000 + /// 写入超时时间(毫秒),默认 3000 + /// 异步取消令牌 + /// 线圈状态数组,true 表示吸合/导通 + public static async Task 读线圈( string portName, int baudRate, byte slaveAddress, @@ -151,7 +207,21 @@ namespace DeviceCommand.Flexible } } - public static async Task WriteSingleCoilAsync( + /// + /// 写入单个线圈(功能码 05),控制单个开关量输出。 + /// + /// 串口名称,如 "COM1" + /// 波特率,如 9600、115200 + /// Modbus 从站站号 (1-247) + /// 要写入的线圈地址 + /// 线圈状态(true: 吸合/导通, false: 断开) + /// 数据位,默认 8 + /// 停止位,默认 1 位 + /// 校验位,默认无校验 + /// 读取超时时间(毫秒),默认 3000 + /// 写入超时时间(毫秒),默认 3000 + /// 异步取消令牌 + public static async Task 写单个线圈( string portName, int baudRate, byte slaveAddress, diff --git a/DeviceCommand/Flexible/FModbusTCP.cs b/DeviceCommand/Flexible/FModbusTCP.cs index d39e48d..113a550 100644 --- a/DeviceCommand/Flexible/FModbusTCP.cs +++ b/DeviceCommand/Flexible/FModbusTCP.cs @@ -8,11 +8,20 @@ using System.Threading.Tasks; namespace DeviceCommand.Flexible { + /// + /// 灵活型 Modbus TCP 通信类(免实例化,每次调用临时建立 TCP 连接)。 + /// 支持保持寄存器与线圈的读写操作,内部使用通信锁保证同一时刻只有一个事务在执行, + /// 适用于偶发性、无需保持长连接的 Modbus TCP 设备读写场景。 + /// [ACPCommand] public static class FModbusTCP { + // 通信锁:保证同一时刻只有一个 Modbus 事务在执行 private static readonly SemaphoreSlim _commLock = new(1, 1); + /// + /// 建立 TCP 连接并创建 Modbus TCP 主站。 + /// private static async Task ConnectAsync(string ipAddress, int port, int sendTimeout, int receiveTimeout, CancellationToken ct) { var tcpClient = new TcpClient(); @@ -26,7 +35,19 @@ namespace DeviceCommand.Flexible #region Holding Registers - public static async Task ReadHoldingRegistersAsync( + /// + /// 读取保持寄存器(功能码 03),返回指定数量的寄存器值数组。 + /// + /// 设备 IP 地址 + /// Modbus TCP 端口号,默认常用 502 + /// Modbus 从站站号(单元标识符) + /// 起始寄存器地址 + /// 读取的寄存器数量 + /// 发送/连接超时时间(毫秒),默认 3000 + /// 接收超时时间(毫秒),默认 3000 + /// 异步取消令牌 + /// 读取到的寄存器值数组 + public static async Task 读保持寄存器( string ipAddress, int port, byte slaveAddress, @@ -51,7 +72,18 @@ namespace DeviceCommand.Flexible } } - public static async Task WriteSingleRegisterAsync( + /// + /// 写入单个保持寄存器(功能码 06)。 + /// + /// 设备 IP 地址 + /// Modbus TCP 端口号,默认常用 502 + /// Modbus 从站站号(单元标识符) + /// 要写入的寄存器地址 + /// 要写入的寄存器值 (0-65535) + /// 发送/连接超时时间(毫秒),默认 3000 + /// 接收超时时间(毫秒),默认 3000 + /// 异步取消令牌 + public static async Task 写单个寄存器( string ipAddress, int port, byte slaveAddress, @@ -79,7 +111,19 @@ namespace DeviceCommand.Flexible #region Coils - public static async Task ReadCoilsAsync( + /// + /// 读取线圈状态(功能码 01),返回指定数量的线圈开关状态数组。 + /// + /// 设备 IP 地址 + /// Modbus TCP 端口号,默认常用 502 + /// Modbus 从站站号(单元标识符) + /// 起始线圈地址 + /// 读取的线圈数量 + /// 发送/连接超时时间(毫秒),默认 3000 + /// 接收超时时间(毫秒),默认 3000 + /// 异步取消令牌 + /// 线圈状态数组,true 表示吸合/导通 + public static async Task 读线圈( string ipAddress, int port, byte slaveAddress, @@ -104,7 +148,18 @@ namespace DeviceCommand.Flexible } } - public static async Task WriteSingleCoilAsync( + /// + /// 写入单个线圈(功能码 05),控制单个开关量输出。 + /// + /// 设备 IP 地址 + /// Modbus TCP 端口号,默认常用 502 + /// Modbus 从站站号(单元标识符) + /// 要写入的线圈地址 + /// 线圈状态(true: 吸合/导通, false: 断开) + /// 发送/连接超时时间(毫秒),默认 3000 + /// 接收超时时间(毫秒),默认 3000 + /// 异步取消令牌 + public static async Task 写单个线圈( string ipAddress, int port, byte slaveAddress, diff --git a/DeviceCommand/Flexible/FSerialPort.cs b/DeviceCommand/Flexible/FSerialPort.cs index 1a195ef..5d958b5 100644 --- a/DeviceCommand/Flexible/FSerialPort.cs +++ b/DeviceCommand/Flexible/FSerialPort.cs @@ -7,11 +7,21 @@ using System.Threading.Tasks; namespace DeviceCommand.Flexible { + /// + /// 灵活型串口通信类(免实例化,每次调用临时创建串口连接)。 + /// 支持只发送指令、发送并读取应答两种最常用操作, + /// 内部使用通信锁保证同一时刻只有一个串口事务在执行, + /// 适用于偶发性、无需保持长连接的串口设备通信场景(如示波器、电源的 SCPI 指令)。 + /// [ACPCommand] public static class FSerialPort { + // 通信锁:保证同一时刻只有一个串口事务在执行 private static readonly SemaphoreSlim _commLock = new(1, 1); + /// + /// 创建串口实例并配置 UTF8 编码与超时参数。 + /// private static SerialPort CreatePort(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, int sendTimeout,int receiveTimeout) { return new SerialPort(portName, baudRate, parity, dataBits, stopBits) @@ -24,7 +34,19 @@ namespace DeviceCommand.Flexible #region 最常用:只发送字符串 - public static async Task SendAsync(string portName,int baudRate,Parity parity,int dataBits,StopBits stopBits,int sendTimeout,int receiveTimeout,string command,CancellationToken ct = default) + /// + /// 向串口发送一条字符串指令(只发送,不等待应答)。 + /// + /// 串口名称,如 "COM1" + /// 波特率,如 9600、115200 + /// 校验位 + /// 数据位,常用 8 + /// 停止位 + /// 发送超时时间(毫秒) + /// 接收超时时间(毫秒) + /// 要发送的指令字符串 + /// 异步取消令牌 + public static async Task 发送指令(string portName,int baudRate,Parity parity,int dataBits,StopBits stopBits,int sendTimeout,int receiveTimeout,string command,CancellationToken ct = default) { await _commLock.WaitAsync(ct); try @@ -52,7 +74,21 @@ namespace DeviceCommand.Flexible #region 最常用:发送字符串并读取字符串 - public static async Task SendReadAsync(string portName,int baudRate, Parity parity, int dataBits, StopBits stopBits,int sendTimeout, int receiveTimeout,string command,string delimiter = "\n", CancellationToken ct = default) + /// + /// 向串口发送一条字符串指令并读取应答,读取到结束符为止(应答不含结束符)。 + /// + /// 串口名称,如 "COM1" + /// 波特率,如 9600、115200 + /// 校验位 + /// 数据位,常用 8 + /// 停止位 + /// 发送超时时间(毫秒) + /// 接收超时时间(毫秒),超时未收到结束符抛出超时异常 + /// 要发送的指令字符串 + /// 应答结束符,默认换行符 "\n" + /// 异步取消令牌 + /// 去除结束符并去除首尾空白后的应答字符串 + public static async Task 发送并读取应答(string portName,int baudRate, Parity parity, int dataBits, StopBits stopBits,int sendTimeout, int receiveTimeout,string command,string delimiter = "\n", CancellationToken ct = default) { await _commLock.WaitAsync(ct); try diff --git a/DeviceCommand/Flexible/FTCP.cs b/DeviceCommand/Flexible/FTCP.cs index db3f7e5..491a905 100644 --- a/DeviceCommand/Flexible/FTCP.cs +++ b/DeviceCommand/Flexible/FTCP.cs @@ -4,14 +4,29 @@ using System.Text; namespace DeviceCommand.Flexible { + /// + /// 灵活型 TCP 通信类(免实例化,每次调用临时建立 TCP 连接)。 + /// 支持字节/文本发送、定长字节读取、按结束符读取文本行四种操作, + /// 内部使用通信锁保证同一时刻只有一个 TCP 事务在执行, + /// 适用于偶发性、无需保持长连接的 TCP 设备通信场景。 + /// [ACPCommand] public static class FTCP { + // 通信锁:保证同一时刻只有一个 TCP 事务在执行 private static readonly SemaphoreSlim _commLock = new(1, 1); #region Send - public static async Task SendAsync(string ipAddress,int port,int sendTimeout, byte[] buffer, CancellationToken ct = default) + /// + /// 向指定 TCP 端点发送一段字节数据(发送完成后立即断开)。 + /// + /// 设备 IP 地址 + /// TCP 端口号 + /// 发送超时时间(毫秒) + /// 要发送的字节数组 + /// 异步取消令牌 + public static async Task 发送字节数据(string ipAddress,int port,int sendTimeout, byte[] buffer, CancellationToken ct = default) { await _commLock.WaitAsync(ct); try @@ -30,16 +45,33 @@ namespace DeviceCommand.Flexible } } - public static Task SendAsync(string ipAddress, int port,int sendTimeout, string text,CancellationToken ct = default) + /// + /// 向指定 TCP 端点发送一段文本(以 UTF8 编码为字节后发送,发送完成后立即断开)。 + /// + /// 设备 IP 地址 + /// TCP 端口号 + /// 发送超时时间(毫秒) + /// 要发送的文本字符串 + /// 异步取消令牌 + public static Task 发送文本数据(string ipAddress, int port,int sendTimeout, string text,CancellationToken ct = default) { - return SendAsync( ipAddress, port, sendTimeout, Encoding.UTF8.GetBytes(text),ct); + return 发送字节数据( ipAddress, port, sendTimeout, Encoding.UTF8.GetBytes(text),ct); } #endregion #region Read - public static async Task ReadAsync(string ipAddress,int port,int receiveTimeout,int length,CancellationToken ct = default) + /// + /// 连接指定 TCP 端点并读取指定长度的字节数据(连接关闭或读满为止)。 + /// + /// 设备 IP 地址 + /// TCP 端口号 + /// 接收超时时间(毫秒) + /// 要读取的字节长度 + /// 异步取消令牌 + /// 实际读取到的字节数组(对端提前关闭时可能短于请求长度) + public static async Task 读取字节数据(string ipAddress,int port,int receiveTimeout,int length,CancellationToken ct = default) { await _commLock.WaitAsync(ct); try @@ -72,7 +104,16 @@ namespace DeviceCommand.Flexible } } - public static async Task ReadLineAsync( string ipAddress, int port, int receiveTimeout, string delimiter = "\n",CancellationToken ct = default) + /// + /// 连接指定 TCP 端点并读取一行文本,读取到结束符为止(返回内容不含结束符)。 + /// + /// 设备 IP 地址 + /// TCP 端口号 + /// 接收超时时间(毫秒),超时未收到结束符抛出超时异常 + /// 文本行结束符,默认换行符 "\n" + /// 异步取消令牌 + /// 去除结束符并去除首尾空白后的文本行 + public static async Task 读取文本行( string ipAddress, int port, int receiveTimeout, string delimiter = "\n",CancellationToken ct = default) { await _commLock.WaitAsync(ct); try diff --git a/TSMasterCAN/CAN.cs b/TSMasterCAN/CAN.cs index e2b2d3b..6964fc5 100644 --- a/TSMasterCAN/CAN.cs +++ b/TSMasterCAN/CAN.cs @@ -10,6 +10,10 @@ using System.Text.Json; using TSMaster; namespace TSMasterCAN { + /// + /// TSMaster CAN/CANFD 总线控制类,基于 TSMaster API 实现总线连接管理、 + /// DBC 信号读写、报文发送(单次/循环)、日志记录等功能,暴露给命令树供测试步骤调用。 + /// [ACPCommand] public class CAN { @@ -70,9 +74,10 @@ namespace TSMasterCAN } /// - /// 连接 + /// 连接 TSMaster CAN 总线:建立总线连接并启动 RBS 仿真系统, + /// 成功后置位连接标志并触发 连接事件。 /// - /// + /// 返回代码,0 表示成功,非 0 表示失败(可用 查询错误说明) public static int Connect() { @@ -99,9 +104,9 @@ namespace TSMasterCAN } /// - /// 断开 + /// 断开 TSMaster CAN 总线连接,成功后清除连接标志并触发 断开事件。 /// - /// + /// 返回代码,0 表示成功,非 0 表示失败(可用 查询错误说明) public static int DisConnect() { var re = TsMasterApi.tsapp_disconnect(); @@ -161,19 +166,19 @@ namespace TSMasterCAN } /// - /// 开始记录日志 + /// 开始记录总线日志,将总线上的报文数据记录到指定文件。 /// - /// - /// + /// 日志文件保存路径(自动转换为绝对路径) + /// 返回代码,0 表示成功,非 0 表示失败 public static int StartLogging(string filePath) { return TsMasterApi.tsapp_start_logging(Path.GetFullPath(filePath)); } /// - /// 结束记录日志 + /// 结束总线日志记录。 /// - /// + /// 返回代码,0 表示成功,非 0 表示失败 public static int StopLogging() { return TsMasterApi.tsapp_stop_logging(); @@ -191,12 +196,12 @@ namespace TSMasterCAN } /// - /// 获取信号值 + /// 获取 DBC 中定义信号的当前物理值(从总线接收报文中解析)。 /// - /// - /// - /// - /// + /// 通道号(0 起始) + /// DBC 中的报文名称 + /// DBC 中的信号名称 + /// 信号物理值,获取失败时返回 public static double GetSignalValue(byte channel, string AMsgName, string ASgnName) { double value = double.NaN; @@ -237,16 +242,16 @@ namespace TSMasterCAN } /// - /// 设置信号值 + /// 设置 DBC 中定义信号的值(按物理值编码进报文数据),可选择立即发送或周期发送。 /// - /// - /// - /// - /// - /// - /// - /// - /// + /// 通道号(0 起始) + /// DBC 中的报文名称 + /// DBC 中的信号名称 + /// 要设置的信号物理值 + /// 是否发送:false 仅更新数据不发送,true 则发送 + /// 发送周期(毫秒):0 = 只发送一次;>0 = 按该周期循环发送 + /// 返回代码,0 表示成功,非 0 表示失败 + /// 信号编码写入报文失败时抛出 public static int SetSignalValue(byte channel, string AMsgName, string ASgnName, double AValue, bool isSend = false, float sendPeriod = 0) { var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName); @@ -271,12 +276,12 @@ namespace TSMasterCAN /// - /// 设置信号值 + /// 按报文名称发送 DBC 中定义的报文(使用该报文在 DBC 数据库中的当前数据)。 /// - /// - /// - /// - /// + /// 通道号 + /// DBC 中的报文名称 + /// 发送周期(毫秒):0 = 只发送一次;>0 = 按该周期循环发送 + /// 返回代码,0 表示成功,非 0 表示失败 public static int SetSignalValue(APP_CHANNEL channel, string AMsgName, float sendPeriod = 0) { var find = DBCParse.MsgDatabase[(byte)channel].First(s => s.msg_name == AMsgName); @@ -302,11 +307,11 @@ namespace TSMasterCAN } /// - /// 设置报文 + /// 按帧 ID 更新 DBC 数据库中指定报文的数据区(仅更新内存中的数据,不发送)。 /// - /// 通道 - /// DBC数据库ID - /// 报文数组 + /// 通道号 + /// 报文帧 ID(DBC 中定义的 FIdentifier) + /// 要写入的报文数据数组,超出报文数据长度的部分会被截断 public static void SetMsg(APP_CHANNEL channel, int ID, byte[] bytes) { var find = DBCParse.MsgDatabase[(byte)channel].FirstOrDefault(s => s.ACANFD.FIdentifier == ID); @@ -317,20 +322,20 @@ namespace TSMasterCAN } /// - /// 发送自定义报文 + /// 构造并发送自定义 CAN/CANFD 报文,可选择单次发送或周期发送,并可同步更新 DBC 数据库中同 ID 报文的数据。 /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// 通道号 + /// 报文帧 ID + /// 是否为发送帧 + /// 是否为扩展帧(29 位 ID) + /// 是否为远程帧 + /// 数据长度代码(数据字节数,最大 64) + /// 报文数据数组,超过 64 字节的部分会被截断 + /// 是否为 CANFD 帧,默认 true + /// 是否切换数据段波特率(仅 CANFD 有效),默认 false + /// 是否同步更新 DBC 数据库中同帧 ID 报文的数据,默认 false + /// 发送周期(毫秒):0 = 只发送一次;>0 = 按该周期循环发送 + /// 返回代码,0 表示成功,非 0 表示失败 public static int SendMsg(APP_CHANNEL AIdxChn, int AID, bool AIsTx, bool AIsExt, bool AIsRemote, byte ADLC, byte[] ADataArray, bool AIsFD = true, bool AIsBRS = false, bool isUpdateDBCDatabase = false, float sendPeriod = 0) @@ -375,12 +380,12 @@ namespace TSMasterCAN } /// - /// 添加循环发送报文 + /// 将 DBC 中定义的报文加入循环发送列表,按指定周期持续发送。 /// - /// - /// - /// - /// + /// 通道号(0 起始) + /// DBC 中的报文名称 + /// 发送周期(毫秒) + /// 返回代码,0 表示成功,非 0 表示失败 public static int AddCyclicMsg(byte channel, string AMsgName, float sendPeriod) { var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName); @@ -389,19 +394,19 @@ namespace TSMasterCAN } /// - /// 清除循环发送的报文 + /// 清除所有循环发送的报文,停止全部周期发送任务。 /// - /// + /// 返回代码,0 表示成功,非 0 表示失败 public static int DeleteCyclicMsgs() { return TsMasterApi.tsapp_delete_cyclic_msgs(); } /// - /// 获取错误提示 + /// 根据错误代码获取对应的错误文字描述,用于诊断 TSMaster API 返回码。 /// - /// 错误代码 - /// + /// TSMaster API 返回的错误代码 + /// 错误描述文本,未知错误代码时返回提示信息 public static string GetErrorDescription(int errorCode) { IntPtr ADesc = IntPtr.Zero; diff --git a/TSMasterCAN/TSMasterCAN.csproj b/TSMasterCAN/TSMasterCAN.csproj index 0a6e281..023256a 100644 --- a/TSMasterCAN/TSMasterCAN.csproj +++ b/TSMasterCAN/TSMasterCAN.csproj @@ -3,6 +3,7 @@ net8.0 enable + true false enable diff --git a/TestingModule/ViewModels/CommandTreeViewModel.cs b/TestingModule/ViewModels/CommandTreeViewModel.cs index 2fe5c42..39ac9a7 100644 --- a/TestingModule/ViewModels/CommandTreeViewModel.cs +++ b/TestingModule/ViewModels/CommandTreeViewModel.cs @@ -192,21 +192,26 @@ namespace TestingModule.ViewModels var assembly = Assembly.LoadFrom(dllPath); Assemblies.Add(assembly); - // 加载对应的XML注释文件 (项目没有用到) - //string xmlPath = Path.ChangeExtension(dllPath, ".xml"); - //if (File.Exists(xmlPath)) - //{ - // try - // { - // XmlDocument xmlDoc = new XmlDocument(); - // xmlDoc.Load(xmlPath); - // _xmlDocumentCache[assembly.FullName!] = xmlDoc; - // } - // catch (Exception xmlEx) - // { - // LoggerHelper.WarnWithNotify($"加载XML注释失败: {Path.GetFileName(xmlPath)} - {xmlEx.Message}"); - // } - //} + //加载对应的XML注释文件 + string xmlPath = Path.ChangeExtension(dllPath, ".xml"); + if (File.Exists(xmlPath)) + { + try + { + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.Load(xmlPath); + XmlDocumentCache[assembly.FullName!] = xmlDoc; + LoggerHelper.Info($"[XML注释] 成功加载: {Path.GetFileName(xmlPath)} -> 程序集 [{assembly.FullName}]"); + } + catch (Exception xmlEx) + { + LoggerHelper.Warn($"加载XML注释失败: {Path.GetFileName(xmlPath)} - {xmlEx.Message}"); + } + } + else + { + LoggerHelper.Warn($"[XML注释] XML文件不存在: {xmlPath}"); + } } catch (Exception ex) { @@ -284,6 +289,7 @@ namespace TestingModule.ViewModels { Name = subProgram.Name, Tag = subProgram, + Tooltip = subProgram.FilePath }); } @@ -368,6 +374,7 @@ namespace TestingModule.ViewModels { Name = $"{method.Name}({paramText})", Tag = method, + Tooltip = GetMethodDocumentation(method), }; typeNode.Children.Add(methodNode); @@ -433,6 +440,95 @@ namespace TestingModule.ViewModels } } } + // 添加获取注释的方法 + private string? GetMethodDocumentation(MethodInfo method) + { + if (method.DeclaringType == null) return null; + + try + { + string assemblyName = method.DeclaringType.Assembly.FullName!; + if (!_xmlDocumentCache.TryGetValue(assemblyName, out XmlDocument? xmlDoc)) + { + LoggerHelper.Warn($"[XML注释] 缓存未命中: 程序集 [{assemblyName}],缓存包含 { _xmlDocumentCache.Count} 个条目: [{string.Join(", ", _xmlDocumentCache.Keys)}]"); + return null; + } + + // 生成XML文档中的成员ID + string memberName = $"M:{method.DeclaringType.FullName}.{method.Name}"; + var parameters = method.GetParameters(); + if (parameters.Length > 0) + { + memberName += "(" + string.Join(",", parameters.Select(p => p.ParameterType.FullName)) + ")"; + } + + // 查找注释节点 + XmlNode? memberNode = xmlDoc.SelectSingleNode($"//member[@name='{memberName}']"); + if (memberNode == null) + { + LoggerHelper.Warn($"[XML注释] 节点未找到: {memberName}"); + return null; + } + + // 获取摘要(summary) + var summaryNode = memberNode.SelectSingleNode("summary"); + string documentation = ""; + + if (summaryNode != null) + { + documentation += CleanXmlContent(summaryNode.InnerXml); + } + + // 获取参数注释(param) + var paramNodes = memberNode.SelectNodes("param"); + if (paramNodes != null && paramNodes.Count > 0) + { + documentation += "\n\n参数:"; + foreach (XmlNode paramNode in paramNodes) + { + string? paramName = paramNode.Attributes?["name"]?.Value; + if (!string.IsNullOrEmpty(paramName)) + { + documentation += $"\n • {paramName}: {CleanXmlContent(paramNode.InnerXml)}"; + } + } + } + + // 获取返回值注释(returns) + var returnsNode = memberNode.SelectSingleNode("returns"); + if (returnsNode != null) + { + documentation += $"\n\n返回值: {CleanXmlContent(returnsNode.InnerXml)}"; + } + + return string.IsNullOrWhiteSpace(documentation) + ? null + : System.Net.WebUtility.HtmlDecode(documentation.Trim()); + } + catch (Exception ex) + { + LoggerHelper.Warn($"获取注释失败: {method.Name} - {ex.Message}"); + return null; + } + } + + // 辅助方法:清理XML内容 + private string CleanXmlContent(string xmlContent) + { + return xmlContent + .Replace("", "") + .Replace("", "\n") + .Replace("", "") + .Replace("", "") + .Replace("", "") // 处理代码标签 + .Replace("", "") + .Replace("", "") + .Replace("", "") + .Trim(); + } + #endregion #region 指令添加 diff --git a/TestingModule/ViewModels/StepsManagerViewModel.cs b/TestingModule/ViewModels/StepsManagerViewModel.cs index d2853c4..5c76d41 100644 --- a/TestingModule/ViewModels/StepsManagerViewModel.cs +++ b/TestingModule/ViewModels/StepsManagerViewModel.cs @@ -178,10 +178,15 @@ namespace TestingModule.ViewModels private void SelectionChanged(object parameter) { - var selectedList = parameter as IList; - if (selectedList != null) + if (parameter is IList list && list.Count > 0) { - SelectedItems = selectedList.Cast().ToList(); + SelectedItems = list.Cast().ToList(); + } + else if (parameter is IEnumerable enumerable && parameter is not string) + { + var items = enumerable.Cast().ToList(); + if (items.Count > 0) + SelectedItems = items; } } @@ -200,13 +205,19 @@ namespace TestingModule.ViewModels } private void CopyStep() { - if (_globalInfo.IsAdmin && SelectedItems.Any()) + if (!_globalInfo.IsAdmin) return; + + // 优先用多选列表,回退到单选 + var source = (SelectedItems != null && SelectedItems.Any()) + ? SelectedItems + : (SelectedStep != null ? new List { SelectedStep } : null); + + if (source == null || !source.Any()) return; + + tmpCopyList.Clear(); + foreach (var item in source) { - tmpCopyList.Clear(); - foreach (var item in SelectedItems) - { - tmpCopyList.Add(item); - } + tmpCopyList.Add(item); } } private void PasteStep() @@ -243,31 +254,29 @@ namespace TestingModule.ViewModels } private void DeleteStep() { - // 确保有选中的项 - if (_globalInfo.IsAdmin && SelectedItems != null && SelectedItems.Any()) + if (!_globalInfo.IsAdmin) return; + + // 优先用多选列表,回退到单选 + var source = (SelectedItems != null && SelectedItems.Any()) + ? SelectedItems.ToList() + : (SelectedStep != null ? new List { SelectedStep } : null); + + if (source == null || !source.Any()) return; + + foreach (var item in source) { - // 创建一个副本进行循环,防止在 Remove 过程中集合变化导致的问题 - var toDelete = SelectedItems.ToList(); + _eventAggregator.GetEvent().Publish(item.ID); - foreach (var item in toDelete) - { - _eventAggregator.GetEvent().Publish(item.ID); - - if (_ScopedContext.SelectedStepList == "主程序") - { - Program.StepCollection.Remove(item); - } - else if (_ScopedContext.SelectedStepList == "错误程序") - { - Program.ErrorStepCollection.Remove(item); - } - } - - // 3. 清空 ViewModel 的选中状态,避免悬挂引用 - SelectedStep = null; - SelectedItems.Clear(); - _ScopedContext.SelectedStep = null; + if (_ScopedContext.SelectedStepList == "主程序") + Program.StepCollection.Remove(item); + else if (_ScopedContext.SelectedStepList == "错误程序") + Program.ErrorStepCollection.Remove(item); } + + // 清空 ViewModel 的选中状态,避免悬挂引用 + SelectedStep = null; + SelectedItems?.Clear(); + _ScopedContext.SelectedStep = null; } #endregion @@ -313,6 +322,7 @@ namespace TestingModule.ViewModels /// private void OnSubProgramNavigate(SubProgramNavigatePayload payload) { + if (_ScopedContext == null || payload == null) return; var nav = payload.IsErrorProgram ? _ScopedContext.ErrorNav : _ScopedContext.MainNav; if (payload.Action == NavigateAction.Enter && payload.SubProgram != null) { @@ -381,6 +391,12 @@ namespace TestingModule.ViewModels UnsubscribeStepCollections(); SubscribeStepCollections(); + // 通知 UI 刷新 DisplaySteps(导航状态虽然引用同一 Program,但集合实例已变) + RaisePropertyChanged(nameof(MainDisplaySteps)); + RaisePropertyChanged(nameof(ErrorDisplaySteps)); + RaisePropertyChanged(nameof(MainCurrentProgram)); + RaisePropertyChanged(nameof(ErrorCurrentProgram)); + // 集合被整体替换后,对新集合重新编号 Application.Current?.Dispatcher.BeginInvoke(new Action(() => { @@ -433,7 +449,7 @@ namespace TestingModule.ViewModels Program.PropertyChanged -= Program_PropertyChanged; } - // 3. 【核心修复】必须显式退订 Prism 全局事件 + // 3. 显式退订全局 Prism 事件 _eventAggregator?.GetEvent()?.Unsubscribe(null); _eventAggregator?.GetEvent()?.Unsubscribe(null); diff --git a/TestingModule/Views/CommandTree.xaml b/TestingModule/Views/CommandTree.xaml index 781144b..6d5fe1d 100644 --- a/TestingModule/Views/CommandTree.xaml +++ b/TestingModule/Views/CommandTree.xaml @@ -53,7 +53,8 @@ - + + diff --git a/UIShare/UIViewModel/InstructionNodeVM.cs b/UIShare/UIViewModel/InstructionNodeVM.cs index 8d9617f..53e21dd 100644 --- a/UIShare/UIViewModel/InstructionNodeVM.cs +++ b/UIShare/UIViewModel/InstructionNodeVM.cs @@ -12,7 +12,7 @@ namespace UIShare.UIViewModel public class InstructionNodeVM : BindableBase { private string _name = string.Empty; - + public string Name { get => _name; @@ -32,5 +32,12 @@ namespace UIShare.UIViewModel get => _tag; set => SetProperty(ref _tag, value); } + + private string? _tooltip; + public string? Tooltip + { + get => _tooltip; + set => SetProperty(ref _tooltip, value); + } } } \ No newline at end of file