| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017 |
- 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<ModbusRegisterMapping> 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<ModbusRegisterMapping> currentMappings)
- {
- InitializeComponent();
- _configService = new ConfigService();
- Mappings = currentMappings ?? new List<ModbusRegisterMapping>();
-
- // 启用双缓冲以减少闪烁
- 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);
- }
- }
-
- /// <summary>
- /// 启用 DataGridView 双缓冲以减少闪烁
- /// </summary>
- 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 });
- }
-
- /// <summary>
- /// 处理 DataGridView 数据错误(如 ComboBox 值不在列表中)
- /// </summary>
- 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 = "";
- }
- }
- }
-
- /// <summary>
- /// 检查 SCPI 命令是否与设备类型兼容
- /// </summary>
- 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<string>();
-
- 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<ModbusRegisterMapping>(new List<ModbusRegisterMapping>(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<ModbusRegisterMapping>(new List<ModbusRegisterMapping>(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);
- }
- }
- }
-
- /// <summary>
- /// 统计不兼容的 SCPI 命令数量
- /// </summary>
- 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;
- }
-
- /// <summary>
- /// 标记所有不兼容的 SCPI 命令(红色背景)
- /// </summary>
- 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<ModbusRegisterMapping>;
- if (list != null)
- {
- Mappings = new List<ModbusRegisterMapping>(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;
- /// <summary>
- /// 在当前行上方插入新行
- /// </summary>
- private void menuInsertAbove_Click(object sender, EventArgs e)
- {
- InsertRow(0); // 在当前行位置插入
- }
- /// <summary>
- /// 在当前行下方插入新行
- /// </summary>
- private void menuInsertBelow_Click(object sender, EventArgs e)
- {
- InsertRow(1); // 在当前行下方插入
- }
- /// <summary>
- /// 插入新行
- /// </summary>
- /// <param name="offset">0=当前行位置, 1=当前行下方</param>
- private void InsertRow(int offset)
- {
- var list = dgvConfig.DataSource as System.ComponentModel.BindingList<ModbusRegisterMapping>;
- 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];
- }
- }
- /// <summary>
- /// 获取下一个可用地址
- /// </summary>
- private int GetNextAvailableAddress(System.ComponentModel.BindingList<ModbusRegisterMapping> list)
- {
- int maxAddr = 0;
- foreach (var item in list)
- {
- if (item.Address > maxAddr)
- maxAddr = item.Address;
- }
- return maxAddr + 1;
- }
- /// <summary>
- /// 删除当前选中的行
- /// </summary>
- private void menuDeleteRow_Click(object sender, EventArgs e)
- {
- var list = dgvConfig.DataSource as System.ComponentModel.BindingList<ModbusRegisterMapping>;
- if (list == null) return;
- // 取消任何编辑状态,防止冲突
- dgvConfig.EndEdit();
- dgvConfig.CancelEdit();
- // 获取选中的行
- var selectedRows = new List<int>();
- 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);
- }
- }
- /// <summary>
- /// 复制当前行
- /// </summary>
- private void menuCopyRow_Click(object sender, EventArgs e)
- {
- if (dgvConfig.CurrentRow == null || dgvConfig.CurrentRow.IsNewRow)
- return;
- var list = dgvConfig.DataSource as System.ComponentModel.BindingList<ModbusRegisterMapping>;
- 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
- };
- }
- }
- /// <summary>
- /// 粘贴行到当前位置
- /// </summary>
- 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<ModbusRegisterMapping>;
- 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
- }
- }
|