using System; using System.Collections.Generic; using System.IO; using System.Windows.Forms; using APS7100TestTool.Models; using APS7100TestTool.Services; using MiniExcelLibs; namespace APS7100TestTool.Forms { public partial class ModbusConfigForm : Form { public List Mappings { get; private set; } public string LastLoadedFilePath { get; private set; } = ""; private ConfigService _configService; // Command Templates - 通用命令(两种设备都支持) private readonly string[] _universalCommands = new string[] { // === IEEE 488.2 标准命令 === "*IDN?", // 查询设备识别信息 "*RST", // 重置设备 "*CLS", // 清除状态寄存器 "*TST?", // 设备自检 "*OPC?", // 查询操作完成 "*WAI", // 等待操作完成 "*TRG", // 触发 "*SAV {0}", // 保存设置 "*RCL {0}", // 恢复设置 // === 系统命令 === "SYST:ERR?", // 查询错误队列 "SYST:VERS?", // 查询SCPI版本 // ⚠ SYST:REM/LOC 已移至各设备专用列表 // APS7100 已经是远程模式时再次发送 SYST:REM 会报错! // === 电压设置(通用)=== "SOUR:VOLT {0}", // 设置电压 "SOUR:VOLT?", // 查询电压设定值 }; // APS-7100 专用命令(AC交流电源) // ⚠ 重要:APS7100 使用完整 SCPI 命令树,不支持简写 // 命令前缀 [APS] 用于在下拉列表中区分设备 private readonly string[] _aps7100Commands = new string[] { // === 输出控制 === // ⚠ 必须使用 OUTP:STAT,不支持 OUTP ON/OFF/OUTP? "OUTP:STAT ON", // [APS] 开启输出 "OUTP:STAT OFF", // [APS] 关闭输出 "OUTP:STAT {0}", // [APS] 设置输出 (ON/OFF 或 1/0) "OUTP:STAT?", // [APS] 查询输出状态 "OUTP:PROT:CLE", // [APS] 清除输出保护 // === 面板锁定 === // ⚠ 使用 KLOC,不是 RWLOCK "SYST:KLOC ON", // [APS] 锁定前面板 "SYST:KLOC OFF", // [APS] 解锁前面板 // === 电压量程 === // ⚠ 使用 R155/R310/R600/AUTO,不支持 LOW/HIGH "SOUR:VOLT:RANG R155", // [APS] 量程 0-155V "SOUR:VOLT:RANG R310", // [APS] 量程 0-310V "SOUR:VOLT:RANG R600", // [APS] 量程 0-600V "SOUR:VOLT:RANG AUTO", // [APS] 自动量程 "SOUR:VOLT:RANG R{0}", // [APS] 设置量程 (R155/R310/R600/AUTO) "SOUR:VOLT:RANG?", // [APS] 查询量程 // === 频率设置 === "SOUR:FREQ {0}", // [APS] 设置频率 "SOUR:FREQ 50", // [APS] 设置50Hz "SOUR:FREQ 60", // [APS] 设置60Hz "SOUR:FREQ 400", // [APS] 设置400Hz(航空) "SOUR:FREQ?", // [APS] 查询频率设定值 // === 相位设置 === "SOUR:PHAS {0}", // [APS] 设置相位(度) "SOUR:PHAS?", // [APS] 查询相位 // === 电流限制 === // ⚠ 只有电流限制,没有电流设定!必须使用 CURR:LIM:RMS // ❌ SOUR:CURR 和 SOUR:CURR? 不可用 "SOUR:CURR:LIM:RMS {0}", // [APS] 设置电流限值 (RMS) "SOUR:CURR:LIM:RMS?", // [APS] 查询电流限值 (RMS) // === 测量命令 === // ⚠ 必须走 SCALar 路径!MEAS:VOLT?/MEAS:CURR?/MEAS:POW? 不可用 "MEAS:SCAL:VOLT?", // [APS] 测量电压 "MEAS:SCAL:CURR?", // [APS] 测量电流 "MEAS:SCAL:FREQ?", // [APS] 测量频率 "MEAS:SCAL:POW:AC:REAL?", // [APS] 测量有功功率 P (W) "MEAS:SCAL:POW:AC:APP?", // [APS] 测量视在功率 S (VA) "MEAS:SCAL:POW:AC:PFAC?", // [APS] 测量功率因数 PF // === 触发命令 === "INIT:IMM", // [APS] 立即执行 "INIT:IMM:TRAN", // [APS] 立即执行瞬态 // === 状态查询 === "STAT:OPER?", // [APS] 操作状态 "STAT:QUES?", // [APS] 可疑状态 // === 序列/模拟命令 === // 用于电压跌落、频率扫变、IEC测试(不是波形设置) "DATA:SEQ:CLE", // [APS] 清除序列 "DATA:SEQ:STOR {0}", // [APS] 存储序列 "DATA:SEQ:REC {0}", // [APS] 调用序列 "DATA:SIM:CLE", // [APS] 清除模拟 "DATA:SIM:STOR {0}", // [APS] 存储模拟 "DATA:SIM:REC {0}", // [APS] 调用模拟 // === 远程/本地模式切换 === // ⚠ APS7100 收到任何 SCPI 命令后会自动进入远程模式 // ⚠ 如果已是远程模式,再次发送 SYST:REM 会报错! "SYST:COMM:RLST LOCAL", // [APS] 返回本地模式(面板有效) "SYST:COMM:RLST REMOTE", // [APS] 进入远程模式 }; // PSW-250 专用命令(DC直流电源) // PSW250 支持简化命令格式(与 APS7100 不同) private readonly string[] _psw250Commands = new string[] { // === 输出控制 === // PSW250 支持简写命令 "OUTP ON", // 开启输出 "OUTP OFF", // 关闭输出 "OUTP {0}", // 设置输出 (ON/OFF 或 1/0) "OUTP?", // 查询输出状态 // === 输出控制优先级 === // 注意:CV/CC 是运行结果(取决于负载),不是可切换的模式 // OUTP:MODE 设置的是控制优先级和动态响应策略 "OUTP:MODE CVHS", // 恒压优先(高速响应) "OUTP:MODE CCHS", // 恒流优先(高速响应) "OUTP:MODE CVLS", // 恒压优先(斜率/平滑变化) "OUTP:MODE CCLS", // 恒流优先(斜率/平滑变化) "OUTP:MODE {0}", // 设置控制优先级 "OUTP:MODE?", // 查询控制优先级 (返回0-3) // === 电流设置 === // PSW250 支持直接电流设置 "SOUR:CURR {0}", // 设置电流 "SOUR:CURR?", // 查询电流设定值 // === 测量命令 === // PSW250 支持简写测量命令 "MEAS:VOLT?", // 测量电压 "MEAS:CURR?", // 测量电流 "MEAS:POW?", // 测量功率 // === 过压保护 === "SOUR:VOLT:PROT {0}", // 设置过压保护值 "SOUR:VOLT:PROT?", // 查询过压保护值 "VOLT:PROT:STAT ON", // 启用过压保护 "VOLT:PROT:STAT OFF", // 禁用过压保护 "VOLT:PROT:STAT?", // 查询过压保护状态 // === 过流保护 === "SOUR:CURR:PROT {0}", // 设置过流保护值 "SOUR:CURR:PROT?", // 查询过流保护值 "CURR:PROT:STAT ON", // 启用过流保护 "CURR:PROT:STAT OFF", // 禁用过流保护 "CURR:PROT:STAT?", // 查询过流保护状态 // === 远程控制 === // PSW250 可以重复发送这些命令,不会报错 "SYST:REM", // 进入远程模式 "SYST:COMM:RLST LOCAL", // 返回本地模式(面板有效) "SYST:COMM:RLST REMOTE", // 进入远程模式 }; public ModbusConfigForm(List currentMappings) { InitializeComponent(); _configService = new ConfigService(); Mappings = currentMappings ?? new List(); // 启用双缓冲以减少闪烁 EnableDoubleBuffering(dgvConfig); // 暂停布局以提高初始化性能 this.SuspendLayout(); dgvConfig.SuspendLayout(); try { InitializeGrid(); LoadDataToGrid(); // Listen for cell events dgvConfig.EditingControlShowing += DgvConfig_EditingControlShowing; dgvConfig.CellValueChanged += DgvConfig_CellValueChanged; dgvConfig.CurrentCellDirtyStateChanged += DgvConfig_CurrentCellDirtyStateChanged; // 处理 DataError 事件,防止 ComboBox 值不匹配时弹出错误 dgvConfig.DataError += DgvConfig_DataError; } finally { dgvConfig.ResumeLayout(true); this.ResumeLayout(true); } } /// /// 启用 DataGridView 双缓冲以减少闪烁 /// private void EnableDoubleBuffering(DataGridView dgv) { typeof(DataGridView).InvokeMember("DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, null, dgv, new object[] { true }); } /// /// 处理 DataGridView 数据错误(如 ComboBox 值不在列表中) /// private void DgvConfig_DataError(object sender, DataGridViewDataErrorEventArgs e) { // 忽略 ComboBox 值不匹配的错误,允许用户输入自定义命令 e.ThrowException = false; // 如果是 SCPI 命令列,允许保留原值(自定义命令) if (e.ColumnIndex == 6) // ScpiCommand 列 { e.Cancel = true; } } private void InitializeGrid() { dgvConfig.AutoGenerateColumns = false; dgvConfig.AllowUserToAddRows = true; dgvConfig.ShowCellToolTips = true; // ============ 第一组:寄存器地址 + 类型 ============ // Address - 主寄存器地址 var colAddress = new DataGridViewTextBoxColumn { DataPropertyName = "Address", HeaderText = "寄存器地址", MinimumWidth = 70, AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader, ToolTipText = "Modbus 寄存器地址 (1-65535)\n" + "• Write模式: PLC写入此地址触发命令\n" + "• Read模式: 设备数据写入此地址供PLC读取" }; dgvConfig.Columns.Add(colAddress); // DataType - 寄存器地址的数据类型 var colDataType = new DataGridViewComboBoxColumn { DataPropertyName = "DataType", HeaderText = "类型", MinimumWidth = 60, AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells, ToolTipText = "寄存器地址的数据类型\n" + "• Int16: 有符号整数,1个寄存器\n" + "• UInt16: 无符号整数,1个寄存器\n" + "• Float: 浮点数,2个寄存器\n" + "• Bool: 布尔值,1个寄存器" }; colDataType.Items.AddRange("Int16", "UInt16", "Float", "Bool"); dgvConfig.Columns.Add(colDataType); // ============ 第二组:数据地址 + 类型(触发模式用)============ // DataAddress - 数据源地址 var colDataAddr = new DataGridViewTextBoxColumn { DataPropertyName = "DataAddress", HeaderText = "数据地址", MinimumWidth = 60, AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader, ToolTipText = "数据来源地址(配合触发模式使用,可选)\n" + "• 空白: 使用「寄存器地址」的值作为命令参数\n" + "• 填写地址: 触发时从此地址读取实际数据值\n" + "━━━━━━━━━━━━━━━━━━━\n" + "典型用法示例:\n" + " 寄存器地址=100 (触发开关)\n" + " 数据地址=101 (实际电压值)\n" + " 当100写入1时,从101读值发送命令" }; dgvConfig.Columns.Add(colDataAddr); // DataAddressType - 数据源地址的数据类型 var colDataAddrType = new DataGridViewComboBoxColumn { DataPropertyName = "DataAddressType", HeaderText = "类型", MinimumWidth = 60, AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells, ToolTipText = "数据地址的数据类型(可选)\n" + "• 空白/Float: 浮点数,2个寄存器(默认)\n" + "• Int16: 有符号整数,1个寄存器\n" + "• UInt16: 无符号整数,1个寄存器\n" + "━━━━━━━━━━━━━━━━━━━\n" + "注意: 电压/电流等模拟量通常用 Float" }; colDataAddrType.Items.AddRange("Float", "Int16", "UInt16"); dgvConfig.Columns.Add(colDataAddrType); // ============ 第三组:操作配置 ============ // OperationType var colOpType = new DataGridViewComboBoxColumn { DataPropertyName = "OperationType", HeaderText = "操作", MinimumWidth = 55, AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells, ToolTipText = "操作类型\n" + "• Write: PLC写入触发APP发送命令\n" + "• Read: APP查询设备写入寄存器供PLC读取" }; colOpType.Items.AddRange("Write", "Read"); dgvConfig.Columns.Add(colOpType); // TriggerValue var colTrigger = new DataGridViewTextBoxColumn { DataPropertyName = "TriggerValue", HeaderText = "触发值", MinimumWidth = 50, AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader, ToolTipText = "触发条件值(可选,Write模式用)\n" + "• 空白: 数值同步模式,任何值变化都发送命令\n" + "• 填写数值: 触发模式,仅当值=触发值时才执行\n" + " 例如: 填 1 表示写入1时触发" }; dgvConfig.Columns.Add(colTrigger); // ScpiCommand (ComboBox) var colScpi = new DataGridViewComboBoxColumn { DataPropertyName = "ScpiCommand", HeaderText = "SCPI 命令", MinimumWidth = 120, AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells, ToolTipText = "SCPI 命令模板\n" + "• {0} 会被替换为实际数值\n" + "• 例如: SOUR:VOLT {0} → SOUR:VOLT 220.5\n" + "━━━━━━━━━━━━━━━━━━━\n" + "⚠ 重要:APS7100 和 PSW250 命令格式不同!\n" + "• APS7100 电流: SOUR:CURR:LIM:RMS (不是 SOUR:CURR)\n" + "• APS7100 测量: MEAS:SCAL:VOLT? (不是 MEAS:VOLT?)\n" + "• APS7100 输出: OUTP:STAT ON (不是 OUTP ON)\n" + "━━━━━━━━━━━━━━━━━━━\n" + "💡 点击单元格时会根据设备类型过滤命令列表" }; // 添加所有命令(通用 + APS7100 + PSW250),避免动态填充导致的选择问题 // 注意:用户点击编辑时,会根据 DeviceTarget 列过滤显示 colScpi.Items.AddRange(_universalCommands); colScpi.Items.AddRange(_aps7100Commands); colScpi.Items.AddRange(_psw250Commands); dgvConfig.Columns.Add(colScpi); // ScaleFactor var colScale = new DataGridViewTextBoxColumn { DataPropertyName = "ScaleFactor", HeaderText = "缩放", MinimumWidth = 45, AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader, ToolTipText = "缩放因子\n" + "• Write: 寄存器值 × 缩放 = 命令参数\n" + "• Read: 设备返回值 ÷ 缩放 = 寄存器值" }; dgvConfig.Columns.Add(colScale); // ResponseAddress - 执行确认地址 var colResponse = new DataGridViewTextBoxColumn { DataPropertyName = "ResponseAddress", HeaderText = "确认地址", MinimumWidth = 60, AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader, ToolTipText = "执行确认地址(可选,Write模式用)\n" + "• 空白: 不回复确认\n" + "• 填写地址: 命令执行后写入确认值\n" + "━━━━━━━━━━━━━━━━━━━\n" + "确认值规则:\n" + " 成功: 写入触发值\n" + " 失败: 写入 -1" }; dgvConfig.Columns.Add(colResponse); // ============ 第四组:其他配置 ============ // RegisterType var colRegType = new DataGridViewComboBoxColumn { DataPropertyName = "RegisterType", HeaderText = "寄存器", MinimumWidth = 65, AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells, ToolTipText = "Modbus 寄存器类型\n" + "• Holding: 保持寄存器 (4x区, 可读写)\n" + "• Input: 输入寄存器 (3x区, 只读)" }; colRegType.Items.AddRange("Holding", "Input"); dgvConfig.Columns.Add(colRegType); // DeviceTarget var colDevice = new DataGridViewComboBoxColumn { DataPropertyName = "DeviceTarget", HeaderText = "设备", MinimumWidth = 70, AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells, ToolTipText = "适用设备\n" + "• Universal: 所有设备通用\n" + "• APS7100: 仅 APS7100 生效\n" + "• PSW250: 仅 PSW250 生效" }; colDevice.Items.AddRange("Universal", "APS7100", "PSW250"); dgvConfig.Columns.Add(colDevice); // Description - 使用 Fill 模式填充剩余空间 dgvConfig.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = "Description", HeaderText = "描述", MinimumWidth = 80, AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill // 填充剩余空间 }); } // Handle immediate commit for ComboBox changes (非 SCPI 列) private void DgvConfig_CurrentCellDirtyStateChanged(object sender, EventArgs e) { // SCPI 命令列不立即提交,让用户完成选择 if (dgvConfig.IsCurrentCellDirty && dgvConfig.CurrentCell?.ColumnIndex != 6) { dgvConfig.CommitEdit(DataGridViewDataErrorContexts.Commit); } } private void DgvConfig_CellValueChanged(object sender, DataGridViewCellEventArgs e) { // 当 DeviceTarget 列(索引 10)改变时,检查 SCPI 命令是否兼容 if (e.ColumnIndex == 10 && e.RowIndex >= 0 && e.RowIndex < dgvConfig.Rows.Count) { var row = dgvConfig.Rows[e.RowIndex]; if (row.IsNewRow) return; string newDevice = row.Cells[10].Value?.ToString() ?? "Universal"; string currentScpi = row.Cells[6].Value?.ToString() ?? ""; if (string.IsNullOrEmpty(currentScpi)) return; // 检查当前 SCPI 命令是否与新设备类型兼容 bool isCompatible = IsScpiCommandCompatible(currentScpi, newDevice); if (!isCompatible) { // 高亮显示不兼容的命令 row.Cells[6].Style.BackColor = System.Drawing.Color.LightCoral; row.Cells[6].ToolTipText = $"⚠ 此命令可能不适用于 {newDevice}!\n请点击此单元格选择正确的命令。"; } else { // 清除警告样式 row.Cells[6].Style.BackColor = System.Drawing.Color.Empty; row.Cells[6].ToolTipText = ""; } } } /// /// 检查 SCPI 命令是否与设备类型兼容 /// private bool IsScpiCommandCompatible(string scpiCommand, string deviceTarget) { if (string.IsNullOrEmpty(scpiCommand)) return true; if (deviceTarget == "Universal") return true; // APS7100 不兼容的命令(PSW250 专用) string[] psw250OnlyPatterns = new[] { "OUTP ON", "OUTP OFF", "OUTP?", "OUTP {0}", "MEAS:VOLT?", "MEAS:CURR?", "MEAS:POW?", "SOUR:CURR ", "SOUR:CURR?", // 注意空格,区分 SOUR:CURR:LIM:RMS "OUTP:MODE" }; // PSW250 不兼容的命令(APS7100 专用) string[] aps7100OnlyPatterns = new[] { "OUTP:STAT", "OUTP:PROT", "MEAS:SCAL:", "SOUR:CURR:LIM:RMS", "SOUR:FREQ", "SOUR:PHAS", "SOUR:VOLT:RANG", "SYST:KLOC", "INIT:IMM", "STAT:OPER", "STAT:QUES", "DATA:SEQ", "DATA:SIM" }; if (deviceTarget == "APS7100") { // 检查是否使用了 PSW250 专用命令 foreach (var pattern in psw250OnlyPatterns) { if (scpiCommand.Contains(pattern) && !scpiCommand.Contains("SOUR:CURR:LIM:RMS")) { return false; } } } else if (deviceTarget == "PSW250") { // 检查是否使用了 APS7100 专用命令 foreach (var pattern in aps7100OnlyPatterns) { if (scpiCommand.Contains(pattern)) { return false; } } } return true; } // EditingControlShowing - 根据设备类型设置 SCPI 命令列表(联动选择) private void DgvConfig_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) { if (dgvConfig.CurrentCell?.ColumnIndex != 6) return; // 只处理 ScpiCommand 列 ComboBox cb = e.Control as ComboBox; if (cb == null) return; int rowIndex = dgvConfig.CurrentCell.RowIndex; if (rowIndex < 0 || rowIndex >= dgvConfig.Rows.Count) return; if (dgvConfig.Rows[rowIndex].IsNewRow) return; // 获取当前行的设备类型(DeviceTarget 列索引为 10) string deviceTarget = dgvConfig.Rows[rowIndex].Cells[10].Value?.ToString() ?? "Universal"; // 获取当前单元格已有的值 string currentValue = dgvConfig.CurrentCell.Value?.ToString() ?? ""; // 构建对应设备类型的命令列表 - 严格根据设备类型筛选 var commands = new List(); if (deviceTarget == "APS7100") { // APS7100:只显示通用命令 + APS7100 专用命令 // ❌ 不添加 PSW250 专用命令(如 SOUR:CURR?, MEAS:VOLT? 等) commands.AddRange(_universalCommands); commands.AddRange(_aps7100Commands); } else if (deviceTarget == "PSW250") { // PSW250:只显示通用命令 + PSW250 专用命令 // ❌ 不添加 APS7100 专用命令 commands.AddRange(_universalCommands); commands.AddRange(_psw250Commands); } else // Universal { // Universal:显示所有命令 commands.AddRange(_universalCommands); commands.AddRange(_aps7100Commands); commands.AddRange(_psw250Commands); } // ⚠ 注意:不再自动添加不兼容的旧命令到列表中 // 如果当前值是不兼容的命令,用户需要从下拉列表中选择正确的命令 // 不兼容的命令会通过 CellValueChanged 事件标记为红色 // 设置 ComboBox 的数据源(使用 DataSource 比 Items 更稳定) cb.DataSource = commands; // 如果当前值在列表中,恢复选中 if (!string.IsNullOrEmpty(currentValue) && commands.Contains(currentValue)) { cb.SelectedItem = currentValue; } else if (commands.Count > 0) { // 当前值不在列表中(不兼容),显示列表第一项作为推荐 // 用户可以选择或手动修改 cb.SelectedIndex = -1; // 不自动选择,让用户主动选择 } } private void LoadDataToGrid() { // 暂停绘制以提高性能 dgvConfig.SuspendLayout(); try { dgvConfig.DataSource = null; // 先清空,避免旧数据影响 dgvConfig.DataSource = new System.ComponentModel.BindingList(new List(Mappings)); // 数据加载后,标记不兼容的命令(不弹窗,由调用者决定是否提示) MarkIncompatibleCommands(); } finally { dgvConfig.ResumeLayout(true); } } private void btnImport_Click(object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog { Filter = "Excel Files|*.xlsx;*.xls" }; if (ofd.ShowDialog() == DialogResult.OK) { try { var loaded = _configService.LoadModbusConfig(ofd.FileName); Mappings = loaded; LastLoadedFilePath = ofd.FileName; // 保存文件路径 // 加载数据(LoadDataToGrid 会自动调用 ValidateAllScpiCommands 检查兼容性) // 如果有不兼容的命令,会弹出警告;否则显示成功提示 dgvConfig.SuspendLayout(); try { dgvConfig.DataSource = null; dgvConfig.DataSource = new System.ComponentModel.BindingList(new List(Mappings)); } finally { dgvConfig.ResumeLayout(true); } // 检查兼容性并统计问题数 int incompatibleCount = CountIncompatibleCommands(); if (incompatibleCount > 0) { // 标记不兼容的命令 MarkIncompatibleCommands(); MessageBox.Show( $"成功导入 {loaded.Count} 条规则。\n\n" + $"⚠ 发现 {incompatibleCount} 个不兼容的 SCPI 命令(已用红色标记)。\n\n" + "请检查标红的单元格,并从下拉列表中选择正确的命令。", "导入完成 - 需要检查", MessageBoxButtons.OK, MessageBoxIcon.Warning ); } else { MessageBox.Show($"成功导入 {loaded.Count} 条规则", "提示"); } } catch (Exception ex) { MessageBox.Show($"导入失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } /// /// 统计不兼容的 SCPI 命令数量 /// private int CountIncompatibleCommands() { int count = 0; foreach (DataGridViewRow row in dgvConfig.Rows) { if (row.IsNewRow) continue; string deviceTarget = row.Cells[10].Value?.ToString() ?? "Universal"; string scpiCommand = row.Cells[6].Value?.ToString() ?? ""; if (!string.IsNullOrEmpty(scpiCommand) && !IsScpiCommandCompatible(scpiCommand, deviceTarget)) { count++; } } return count; } /// /// 标记所有不兼容的 SCPI 命令(红色背景) /// private void MarkIncompatibleCommands() { foreach (DataGridViewRow row in dgvConfig.Rows) { if (row.IsNewRow) continue; string deviceTarget = row.Cells[10].Value?.ToString() ?? "Universal"; string scpiCommand = row.Cells[6].Value?.ToString() ?? ""; if (string.IsNullOrEmpty(scpiCommand)) continue; bool isCompatible = IsScpiCommandCompatible(scpiCommand, deviceTarget); if (!isCompatible) { row.Cells[6].Style.BackColor = System.Drawing.Color.LightCoral; row.Cells[6].ToolTipText = $"⚠ 此命令不适用于 {deviceTarget}!请选择正确的命令。"; } else { row.Cells[6].Style.BackColor = System.Drawing.Color.Empty; row.Cells[6].ToolTipText = ""; } } } private void btnExport_Click(object sender, EventArgs e) { // 使用当前记录的路径作为默认目录和文件名 string defaultFileName = "ModbusConfig.xlsx"; string initialDirectory = ""; if (!string.IsNullOrEmpty(LastLoadedFilePath)) { defaultFileName = Path.GetFileName(LastLoadedFilePath); initialDirectory = Path.GetDirectoryName(LastLoadedFilePath) ?? ""; } SaveFileDialog sfd = new SaveFileDialog { Filter = "Excel Files|*.xlsx", FileName = defaultFileName }; if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory)) { sfd.InitialDirectory = initialDirectory; } if (sfd.ShowDialog() == DialogResult.OK) { try { UpdateMappingsFromGrid(); // 如果文件已存在,先删除(MiniExcel 不支持覆盖) if (File.Exists(sfd.FileName)) { File.Delete(sfd.FileName); } MiniExcel.SaveAs(sfd.FileName, Mappings); // 记录导出路径,使保存配置时使用相同路径 LastLoadedFilePath = sfd.FileName; MessageBox.Show($"导出成功\n路径: {sfd.FileName}", "提示"); } catch (Exception ex) { MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void btnSave_Click(object sender, EventArgs e) { try { UpdateMappingsFromGrid(); DialogResult = DialogResult.OK; Close(); } catch (Exception ex) { MessageBox.Show($"保存配置时出错: {ex.Message}", "错误"); } } private void UpdateMappingsFromGrid() { var list = dgvConfig.DataSource as System.ComponentModel.BindingList; if (list != null) { Mappings = new List(list); } foreach (var m in Mappings) { if (m.Address < 1) throw new Exception($"地址 {m.Address} 无效,必须大于0"); if (string.IsNullOrEmpty(m.ScpiCommand)) throw new Exception($"地址 {m.Address} 的 SCPI 命令不能为空"); if (string.IsNullOrEmpty(m.RegisterType)) m.RegisterType = "Holding"; if (string.IsNullOrEmpty(m.OperationType)) m.OperationType = "Write"; if (string.IsNullOrEmpty(m.DataType)) m.DataType = "Int16"; if (string.IsNullOrEmpty(m.DeviceTarget)) m.DeviceTarget = "Universal"; } } private void btnToggleHelp_Click(object sender, EventArgs e) { pnlHelp.Visible = !pnlHelp.Visible; btnToggleHelp.Text = pnlHelp.Visible ? "隐藏说明" : "显示说明"; } #region 右键菜单 - 行操作 // 用于复制粘贴的临时存储 private ModbusRegisterMapping? _copiedRow = null; /// /// 在当前行上方插入新行 /// private void menuInsertAbove_Click(object sender, EventArgs e) { InsertRow(0); // 在当前行位置插入 } /// /// 在当前行下方插入新行 /// private void menuInsertBelow_Click(object sender, EventArgs e) { InsertRow(1); // 在当前行下方插入 } /// /// 插入新行 /// /// 0=当前行位置, 1=当前行下方 private void InsertRow(int offset) { var list = dgvConfig.DataSource as System.ComponentModel.BindingList; if (list == null) return; // 取消任何编辑状态,防止冲突 dgvConfig.EndEdit(); dgvConfig.CancelEdit(); int insertIndex = list.Count; // 默认插入到末尾 if (dgvConfig.CurrentRow != null && !dgvConfig.CurrentRow.IsNewRow) { insertIndex = dgvConfig.CurrentRow.Index + offset; } // 确保索引有效 if (insertIndex < 0) insertIndex = 0; if (insertIndex > list.Count) insertIndex = list.Count; // 创建新行,使用合理的默认值 var newRow = new ModbusRegisterMapping { Address = GetNextAvailableAddress(list), RegisterType = "Holding", OperationType = "Write", DataType = "Float", ScpiCommand = "", DeviceTarget = "Universal", ScaleFactor = 1.0, Description = "" }; list.Insert(insertIndex, newRow); // 选中新插入的行 dgvConfig.ClearSelection(); if (insertIndex < dgvConfig.Rows.Count) { dgvConfig.Rows[insertIndex].Selected = true; dgvConfig.CurrentCell = dgvConfig.Rows[insertIndex].Cells[0]; } } /// /// 获取下一个可用地址 /// private int GetNextAvailableAddress(System.ComponentModel.BindingList list) { int maxAddr = 0; foreach (var item in list) { if (item.Address > maxAddr) maxAddr = item.Address; } return maxAddr + 1; } /// /// 删除当前选中的行 /// private void menuDeleteRow_Click(object sender, EventArgs e) { var list = dgvConfig.DataSource as System.ComponentModel.BindingList; if (list == null) return; // 取消任何编辑状态,防止冲突 dgvConfig.EndEdit(); dgvConfig.CancelEdit(); // 获取选中的行 var selectedRows = new List(); foreach (DataGridViewRow row in dgvConfig.SelectedRows) { if (!row.IsNewRow) selectedRows.Add(row.Index); } // 如果没有选中整行,则删除当前行 if (selectedRows.Count == 0 && dgvConfig.CurrentRow != null && !dgvConfig.CurrentRow.IsNewRow) { selectedRows.Add(dgvConfig.CurrentRow.Index); } if (selectedRows.Count == 0) { MessageBox.Show("请先选择要删除的行", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 确认删除 var result = MessageBox.Show( $"确定要删除选中的 {selectedRows.Count} 行吗?", "确认删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result != DialogResult.Yes) return; // 从后往前删除,避免索引错位 selectedRows.Sort(); selectedRows.Reverse(); foreach (int idx in selectedRows) { if (idx >= 0 && idx < list.Count) list.RemoveAt(idx); } } /// /// 复制当前行 /// private void menuCopyRow_Click(object sender, EventArgs e) { if (dgvConfig.CurrentRow == null || dgvConfig.CurrentRow.IsNewRow) return; var list = dgvConfig.DataSource as System.ComponentModel.BindingList; if (list == null) return; int idx = dgvConfig.CurrentRow.Index; if (idx >= 0 && idx < list.Count) { var source = list[idx]; // 深拷贝 _copiedRow = new ModbusRegisterMapping { Address = source.Address, RegisterType = source.RegisterType, OperationType = source.OperationType, DataType = source.DataType, ScpiCommand = source.ScpiCommand, DeviceTarget = source.DeviceTarget, TriggerValue = source.TriggerValue, DataAddress = source.DataAddress, DataAddressType = source.DataAddressType, ScaleFactor = source.ScaleFactor, Description = source.Description }; } } /// /// 粘贴行到当前位置 /// private void menuPasteRow_Click(object sender, EventArgs e) { if (_copiedRow == null) { MessageBox.Show("没有复制的行可以粘贴", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var list = dgvConfig.DataSource as System.ComponentModel.BindingList; if (list == null) return; // 取消任何编辑状态,防止冲突 dgvConfig.EndEdit(); dgvConfig.CancelEdit(); // 计算插入位置 int insertIndex = list.Count; // 默认插入到末尾 if (dgvConfig.CurrentRow != null && !dgvConfig.CurrentRow.IsNewRow) { insertIndex = dgvConfig.CurrentRow.Index; } // 确保索引有效 if (insertIndex < 0) insertIndex = 0; if (insertIndex > list.Count) insertIndex = list.Count; // 创建副本并分配新地址 var newRow = new ModbusRegisterMapping { Address = GetNextAvailableAddress(list), RegisterType = _copiedRow.RegisterType, OperationType = _copiedRow.OperationType, DataType = _copiedRow.DataType, ScpiCommand = _copiedRow.ScpiCommand, DeviceTarget = _copiedRow.DeviceTarget, TriggerValue = _copiedRow.TriggerValue, DataAddress = _copiedRow.DataAddress, DataAddressType = _copiedRow.DataAddressType, ScaleFactor = _copiedRow.ScaleFactor, Description = _copiedRow.Description + " (副本)" }; list.Insert(insertIndex, newRow); // 选中新插入的行 dgvConfig.ClearSelection(); if (insertIndex < dgvConfig.Rows.Count) { dgvConfig.Rows[insertIndex].Selected = true; dgvConfig.CurrentCell = dgvConfig.Rows[insertIndex].Cells[0]; } } #endregion } }