ModbusConfigForm.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Windows.Forms;
  5. using APS7100TestTool.Models;
  6. using APS7100TestTool.Services;
  7. using MiniExcelLibs;
  8. namespace APS7100TestTool.Forms
  9. {
  10. public partial class ModbusConfigForm : Form
  11. {
  12. public List<ModbusRegisterMapping> Mappings { get; private set; }
  13. public string LastLoadedFilePath { get; private set; } = "";
  14. private ConfigService _configService;
  15. // Command Templates - 通用命令(两种设备都支持)
  16. private readonly string[] _universalCommands = new string[] {
  17. // === IEEE 488.2 标准命令 ===
  18. "*IDN?", // 查询设备识别信息
  19. "*RST", // 重置设备
  20. "*CLS", // 清除状态寄存器
  21. "*TST?", // 设备自检
  22. "*OPC?", // 查询操作完成
  23. "*WAI", // 等待操作完成
  24. "*TRG", // 触发
  25. "*SAV {0}", // 保存设置
  26. "*RCL {0}", // 恢复设置
  27. // === 系统命令 ===
  28. "SYST:ERR?", // 查询错误队列
  29. "SYST:VERS?", // 查询SCPI版本
  30. // ⚠ SYST:REM/LOC 已移至各设备专用列表
  31. // APS7100 已经是远程模式时再次发送 SYST:REM 会报错!
  32. // === 电压设置(通用)===
  33. "SOUR:VOLT {0}", // 设置电压
  34. "SOUR:VOLT?", // 查询电压设定值
  35. };
  36. // APS-7100 专用命令(AC交流电源)
  37. // ⚠ 重要:APS7100 使用完整 SCPI 命令树,不支持简写
  38. // 命令前缀 [APS] 用于在下拉列表中区分设备
  39. private readonly string[] _aps7100Commands = new string[] {
  40. // === 输出控制 ===
  41. // ⚠ 必须使用 OUTP:STAT,不支持 OUTP ON/OFF/OUTP?
  42. "OUTP:STAT ON", // [APS] 开启输出
  43. "OUTP:STAT OFF", // [APS] 关闭输出
  44. "OUTP:STAT {0}", // [APS] 设置输出 (ON/OFF 或 1/0)
  45. "OUTP:STAT?", // [APS] 查询输出状态
  46. "OUTP:PROT:CLE", // [APS] 清除输出保护
  47. // === 面板锁定 ===
  48. // ⚠ 使用 KLOC,不是 RWLOCK
  49. "SYST:KLOC ON", // [APS] 锁定前面板
  50. "SYST:KLOC OFF", // [APS] 解锁前面板
  51. // === 电压量程 ===
  52. // ⚠ 使用 R155/R310/R600/AUTO,不支持 LOW/HIGH
  53. "SOUR:VOLT:RANG R155", // [APS] 量程 0-155V
  54. "SOUR:VOLT:RANG R310", // [APS] 量程 0-310V
  55. "SOUR:VOLT:RANG R600", // [APS] 量程 0-600V
  56. "SOUR:VOLT:RANG AUTO", // [APS] 自动量程
  57. "SOUR:VOLT:RANG R{0}", // [APS] 设置量程 (R155/R310/R600/AUTO)
  58. "SOUR:VOLT:RANG?", // [APS] 查询量程
  59. // === 频率设置 ===
  60. "SOUR:FREQ {0}", // [APS] 设置频率
  61. "SOUR:FREQ 50", // [APS] 设置50Hz
  62. "SOUR:FREQ 60", // [APS] 设置60Hz
  63. "SOUR:FREQ 400", // [APS] 设置400Hz(航空)
  64. "SOUR:FREQ?", // [APS] 查询频率设定值
  65. // === 相位设置 ===
  66. "SOUR:PHAS {0}", // [APS] 设置相位(度)
  67. "SOUR:PHAS?", // [APS] 查询相位
  68. // === 电流限制 ===
  69. // ⚠ 只有电流限制,没有电流设定!必须使用 CURR:LIM:RMS
  70. // ❌ SOUR:CURR 和 SOUR:CURR? 不可用
  71. "SOUR:CURR:LIM:RMS {0}", // [APS] 设置电流限值 (RMS)
  72. "SOUR:CURR:LIM:RMS?", // [APS] 查询电流限值 (RMS)
  73. // === 测量命令 ===
  74. // ⚠ 必须走 SCALar 路径!MEAS:VOLT?/MEAS:CURR?/MEAS:POW? 不可用
  75. "MEAS:SCAL:VOLT?", // [APS] 测量电压
  76. "MEAS:SCAL:CURR?", // [APS] 测量电流
  77. "MEAS:SCAL:FREQ?", // [APS] 测量频率
  78. "MEAS:SCAL:POW:AC:REAL?", // [APS] 测量有功功率 P (W)
  79. "MEAS:SCAL:POW:AC:APP?", // [APS] 测量视在功率 S (VA)
  80. "MEAS:SCAL:POW:AC:PFAC?", // [APS] 测量功率因数 PF
  81. // === 触发命令 ===
  82. "INIT:IMM", // [APS] 立即执行
  83. "INIT:IMM:TRAN", // [APS] 立即执行瞬态
  84. // === 状态查询 ===
  85. "STAT:OPER?", // [APS] 操作状态
  86. "STAT:QUES?", // [APS] 可疑状态
  87. // === 序列/模拟命令 ===
  88. // 用于电压跌落、频率扫变、IEC测试(不是波形设置)
  89. "DATA:SEQ:CLE", // [APS] 清除序列
  90. "DATA:SEQ:STOR {0}", // [APS] 存储序列
  91. "DATA:SEQ:REC {0}", // [APS] 调用序列
  92. "DATA:SIM:CLE", // [APS] 清除模拟
  93. "DATA:SIM:STOR {0}", // [APS] 存储模拟
  94. "DATA:SIM:REC {0}", // [APS] 调用模拟
  95. // === 远程/本地模式切换 ===
  96. // ⚠ APS7100 收到任何 SCPI 命令后会自动进入远程模式
  97. // ⚠ 如果已是远程模式,再次发送 SYST:REM 会报错!
  98. "SYST:COMM:RLST LOCAL", // [APS] 返回本地模式(面板有效)
  99. "SYST:COMM:RLST REMOTE", // [APS] 进入远程模式
  100. };
  101. // PSW-250 专用命令(DC直流电源)
  102. // PSW250 支持简化命令格式(与 APS7100 不同)
  103. private readonly string[] _psw250Commands = new string[] {
  104. // === 输出控制 ===
  105. // PSW250 支持简写命令
  106. "OUTP ON", // 开启输出
  107. "OUTP OFF", // 关闭输出
  108. "OUTP {0}", // 设置输出 (ON/OFF 或 1/0)
  109. "OUTP?", // 查询输出状态
  110. // === 输出控制优先级 ===
  111. // 注意:CV/CC 是运行结果(取决于负载),不是可切换的模式
  112. // OUTP:MODE 设置的是控制优先级和动态响应策略
  113. "OUTP:MODE CVHS", // 恒压优先(高速响应)
  114. "OUTP:MODE CCHS", // 恒流优先(高速响应)
  115. "OUTP:MODE CVLS", // 恒压优先(斜率/平滑变化)
  116. "OUTP:MODE CCLS", // 恒流优先(斜率/平滑变化)
  117. "OUTP:MODE {0}", // 设置控制优先级
  118. "OUTP:MODE?", // 查询控制优先级 (返回0-3)
  119. // === 电流设置 ===
  120. // PSW250 支持直接电流设置
  121. "SOUR:CURR {0}", // 设置电流
  122. "SOUR:CURR?", // 查询电流设定值
  123. // === 测量命令 ===
  124. // PSW250 支持简写测量命令
  125. "MEAS:VOLT?", // 测量电压
  126. "MEAS:CURR?", // 测量电流
  127. "MEAS:POW?", // 测量功率
  128. // === 过压保护 ===
  129. "SOUR:VOLT:PROT {0}", // 设置过压保护值
  130. "SOUR:VOLT:PROT?", // 查询过压保护值
  131. "VOLT:PROT:STAT ON", // 启用过压保护
  132. "VOLT:PROT:STAT OFF", // 禁用过压保护
  133. "VOLT:PROT:STAT?", // 查询过压保护状态
  134. // === 过流保护 ===
  135. "SOUR:CURR:PROT {0}", // 设置过流保护值
  136. "SOUR:CURR:PROT?", // 查询过流保护值
  137. "CURR:PROT:STAT ON", // 启用过流保护
  138. "CURR:PROT:STAT OFF", // 禁用过流保护
  139. "CURR:PROT:STAT?", // 查询过流保护状态
  140. // === 远程控制 ===
  141. // PSW250 可以重复发送这些命令,不会报错
  142. "SYST:REM", // 进入远程模式
  143. "SYST:COMM:RLST LOCAL", // 返回本地模式(面板有效)
  144. "SYST:COMM:RLST REMOTE", // 进入远程模式
  145. };
  146. public ModbusConfigForm(List<ModbusRegisterMapping> currentMappings)
  147. {
  148. InitializeComponent();
  149. _configService = new ConfigService();
  150. Mappings = currentMappings ?? new List<ModbusRegisterMapping>();
  151. // 启用双缓冲以减少闪烁
  152. EnableDoubleBuffering(dgvConfig);
  153. // 暂停布局以提高初始化性能
  154. this.SuspendLayout();
  155. dgvConfig.SuspendLayout();
  156. try
  157. {
  158. InitializeGrid();
  159. LoadDataToGrid();
  160. // Listen for cell events
  161. dgvConfig.EditingControlShowing += DgvConfig_EditingControlShowing;
  162. dgvConfig.CellValueChanged += DgvConfig_CellValueChanged;
  163. dgvConfig.CurrentCellDirtyStateChanged += DgvConfig_CurrentCellDirtyStateChanged;
  164. // 处理 DataError 事件,防止 ComboBox 值不匹配时弹出错误
  165. dgvConfig.DataError += DgvConfig_DataError;
  166. }
  167. finally
  168. {
  169. dgvConfig.ResumeLayout(true);
  170. this.ResumeLayout(true);
  171. }
  172. }
  173. /// <summary>
  174. /// 启用 DataGridView 双缓冲以减少闪烁
  175. /// </summary>
  176. private void EnableDoubleBuffering(DataGridView dgv)
  177. {
  178. typeof(DataGridView).InvokeMember("DoubleBuffered",
  179. System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty,
  180. null, dgv, new object[] { true });
  181. }
  182. /// <summary>
  183. /// 处理 DataGridView 数据错误(如 ComboBox 值不在列表中)
  184. /// </summary>
  185. private void DgvConfig_DataError(object sender, DataGridViewDataErrorEventArgs e)
  186. {
  187. // 忽略 ComboBox 值不匹配的错误,允许用户输入自定义命令
  188. e.ThrowException = false;
  189. // 如果是 SCPI 命令列,允许保留原值(自定义命令)
  190. if (e.ColumnIndex == 6) // ScpiCommand 列
  191. {
  192. e.Cancel = true;
  193. }
  194. }
  195. private void InitializeGrid()
  196. {
  197. dgvConfig.AutoGenerateColumns = false;
  198. dgvConfig.AllowUserToAddRows = true;
  199. dgvConfig.ShowCellToolTips = true;
  200. // ============ 第一组:寄存器地址 + 类型 ============
  201. // Address - 主寄存器地址
  202. var colAddress = new DataGridViewTextBoxColumn
  203. {
  204. DataPropertyName = "Address",
  205. HeaderText = "寄存器地址",
  206. MinimumWidth = 70,
  207. AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
  208. ToolTipText = "Modbus 寄存器地址 (1-65535)\n" +
  209. "• Write模式: PLC写入此地址触发命令\n" +
  210. "• Read模式: 设备数据写入此地址供PLC读取"
  211. };
  212. dgvConfig.Columns.Add(colAddress);
  213. // DataType - 寄存器地址的数据类型
  214. var colDataType = new DataGridViewComboBoxColumn
  215. {
  216. DataPropertyName = "DataType",
  217. HeaderText = "类型",
  218. MinimumWidth = 60,
  219. AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells,
  220. ToolTipText = "寄存器地址的数据类型\n" +
  221. "• Int16: 有符号整数,1个寄存器\n" +
  222. "• UInt16: 无符号整数,1个寄存器\n" +
  223. "• Float: 浮点数,2个寄存器\n" +
  224. "• Bool: 布尔值,1个寄存器"
  225. };
  226. colDataType.Items.AddRange("Int16", "UInt16", "Float", "Bool");
  227. dgvConfig.Columns.Add(colDataType);
  228. // ============ 第二组:数据地址 + 类型(触发模式用)============
  229. // DataAddress - 数据源地址
  230. var colDataAddr = new DataGridViewTextBoxColumn
  231. {
  232. DataPropertyName = "DataAddress",
  233. HeaderText = "数据地址",
  234. MinimumWidth = 60,
  235. AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
  236. ToolTipText = "数据来源地址(配合触发模式使用,可选)\n" +
  237. "• 空白: 使用「寄存器地址」的值作为命令参数\n" +
  238. "• 填写地址: 触发时从此地址读取实际数据值\n" +
  239. "━━━━━━━━━━━━━━━━━━━\n" +
  240. "典型用法示例:\n" +
  241. " 寄存器地址=100 (触发开关)\n" +
  242. " 数据地址=101 (实际电压值)\n" +
  243. " 当100写入1时,从101读值发送命令"
  244. };
  245. dgvConfig.Columns.Add(colDataAddr);
  246. // DataAddressType - 数据源地址的数据类型
  247. var colDataAddrType = new DataGridViewComboBoxColumn
  248. {
  249. DataPropertyName = "DataAddressType",
  250. HeaderText = "类型",
  251. MinimumWidth = 60,
  252. AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells,
  253. ToolTipText = "数据地址的数据类型(可选)\n" +
  254. "• 空白/Float: 浮点数,2个寄存器(默认)\n" +
  255. "• Int16: 有符号整数,1个寄存器\n" +
  256. "• UInt16: 无符号整数,1个寄存器\n" +
  257. "━━━━━━━━━━━━━━━━━━━\n" +
  258. "注意: 电压/电流等模拟量通常用 Float"
  259. };
  260. colDataAddrType.Items.AddRange("Float", "Int16", "UInt16");
  261. dgvConfig.Columns.Add(colDataAddrType);
  262. // ============ 第三组:操作配置 ============
  263. // OperationType
  264. var colOpType = new DataGridViewComboBoxColumn
  265. {
  266. DataPropertyName = "OperationType",
  267. HeaderText = "操作",
  268. MinimumWidth = 55,
  269. AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells,
  270. ToolTipText = "操作类型\n" +
  271. "• Write: PLC写入触发APP发送命令\n" +
  272. "• Read: APP查询设备写入寄存器供PLC读取"
  273. };
  274. colOpType.Items.AddRange("Write", "Read");
  275. dgvConfig.Columns.Add(colOpType);
  276. // TriggerValue
  277. var colTrigger = new DataGridViewTextBoxColumn
  278. {
  279. DataPropertyName = "TriggerValue",
  280. HeaderText = "触发值",
  281. MinimumWidth = 50,
  282. AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
  283. ToolTipText = "触发条件值(可选,Write模式用)\n" +
  284. "• 空白: 数值同步模式,任何值变化都发送命令\n" +
  285. "• 填写数值: 触发模式,仅当值=触发值时才执行\n" +
  286. " 例如: 填 1 表示写入1时触发"
  287. };
  288. dgvConfig.Columns.Add(colTrigger);
  289. // ScpiCommand (ComboBox)
  290. var colScpi = new DataGridViewComboBoxColumn
  291. {
  292. DataPropertyName = "ScpiCommand",
  293. HeaderText = "SCPI 命令",
  294. MinimumWidth = 120,
  295. AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells,
  296. ToolTipText = "SCPI 命令模板\n" +
  297. "• {0} 会被替换为实际数值\n" +
  298. "• 例如: SOUR:VOLT {0} → SOUR:VOLT 220.5\n" +
  299. "━━━━━━━━━━━━━━━━━━━\n" +
  300. "⚠ 重要:APS7100 和 PSW250 命令格式不同!\n" +
  301. "• APS7100 电流: SOUR:CURR:LIM:RMS (不是 SOUR:CURR)\n" +
  302. "• APS7100 测量: MEAS:SCAL:VOLT? (不是 MEAS:VOLT?)\n" +
  303. "• APS7100 输出: OUTP:STAT ON (不是 OUTP ON)\n" +
  304. "━━━━━━━━━━━━━━━━━━━\n" +
  305. "💡 点击单元格时会根据设备类型过滤命令列表"
  306. };
  307. // 添加所有命令(通用 + APS7100 + PSW250),避免动态填充导致的选择问题
  308. // 注意:用户点击编辑时,会根据 DeviceTarget 列过滤显示
  309. colScpi.Items.AddRange(_universalCommands);
  310. colScpi.Items.AddRange(_aps7100Commands);
  311. colScpi.Items.AddRange(_psw250Commands);
  312. dgvConfig.Columns.Add(colScpi);
  313. // ScaleFactor
  314. var colScale = new DataGridViewTextBoxColumn
  315. {
  316. DataPropertyName = "ScaleFactor",
  317. HeaderText = "缩放",
  318. MinimumWidth = 45,
  319. AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
  320. ToolTipText = "缩放因子\n" +
  321. "• Write: 寄存器值 × 缩放 = 命令参数\n" +
  322. "• Read: 设备返回值 ÷ 缩放 = 寄存器值"
  323. };
  324. dgvConfig.Columns.Add(colScale);
  325. // ResponseAddress - 执行确认地址
  326. var colResponse = new DataGridViewTextBoxColumn
  327. {
  328. DataPropertyName = "ResponseAddress",
  329. HeaderText = "确认地址",
  330. MinimumWidth = 60,
  331. AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
  332. ToolTipText = "执行确认地址(可选,Write模式用)\n" +
  333. "• 空白: 不回复确认\n" +
  334. "• 填写地址: 命令执行后写入确认值\n" +
  335. "━━━━━━━━━━━━━━━━━━━\n" +
  336. "确认值规则:\n" +
  337. " 成功: 写入触发值\n" +
  338. " 失败: 写入 -1"
  339. };
  340. dgvConfig.Columns.Add(colResponse);
  341. // ============ 第四组:其他配置 ============
  342. // RegisterType
  343. var colRegType = new DataGridViewComboBoxColumn
  344. {
  345. DataPropertyName = "RegisterType",
  346. HeaderText = "寄存器",
  347. MinimumWidth = 65,
  348. AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells,
  349. ToolTipText = "Modbus 寄存器类型\n" +
  350. "• Holding: 保持寄存器 (4x区, 可读写)\n" +
  351. "• Input: 输入寄存器 (3x区, 只读)"
  352. };
  353. colRegType.Items.AddRange("Holding", "Input");
  354. dgvConfig.Columns.Add(colRegType);
  355. // DeviceTarget
  356. var colDevice = new DataGridViewComboBoxColumn
  357. {
  358. DataPropertyName = "DeviceTarget",
  359. HeaderText = "设备",
  360. MinimumWidth = 70,
  361. AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells,
  362. ToolTipText = "适用设备\n" +
  363. "• Universal: 所有设备通用\n" +
  364. "• APS7100: 仅 APS7100 生效\n" +
  365. "• PSW250: 仅 PSW250 生效"
  366. };
  367. colDevice.Items.AddRange("Universal", "APS7100", "PSW250");
  368. dgvConfig.Columns.Add(colDevice);
  369. // Description - 使用 Fill 模式填充剩余空间
  370. dgvConfig.Columns.Add(new DataGridViewTextBoxColumn
  371. {
  372. DataPropertyName = "Description",
  373. HeaderText = "描述",
  374. MinimumWidth = 80,
  375. AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill // 填充剩余空间
  376. });
  377. }
  378. // Handle immediate commit for ComboBox changes (非 SCPI 列)
  379. private void DgvConfig_CurrentCellDirtyStateChanged(object sender, EventArgs e)
  380. {
  381. // SCPI 命令列不立即提交,让用户完成选择
  382. if (dgvConfig.IsCurrentCellDirty && dgvConfig.CurrentCell?.ColumnIndex != 6)
  383. {
  384. dgvConfig.CommitEdit(DataGridViewDataErrorContexts.Commit);
  385. }
  386. }
  387. private void DgvConfig_CellValueChanged(object sender, DataGridViewCellEventArgs e)
  388. {
  389. // 当 DeviceTarget 列(索引 10)改变时,检查 SCPI 命令是否兼容
  390. if (e.ColumnIndex == 10 && e.RowIndex >= 0 && e.RowIndex < dgvConfig.Rows.Count)
  391. {
  392. var row = dgvConfig.Rows[e.RowIndex];
  393. if (row.IsNewRow) return;
  394. string newDevice = row.Cells[10].Value?.ToString() ?? "Universal";
  395. string currentScpi = row.Cells[6].Value?.ToString() ?? "";
  396. if (string.IsNullOrEmpty(currentScpi)) return;
  397. // 检查当前 SCPI 命令是否与新设备类型兼容
  398. bool isCompatible = IsScpiCommandCompatible(currentScpi, newDevice);
  399. if (!isCompatible)
  400. {
  401. // 高亮显示不兼容的命令
  402. row.Cells[6].Style.BackColor = System.Drawing.Color.LightCoral;
  403. row.Cells[6].ToolTipText = $"⚠ 此命令可能不适用于 {newDevice}!\n请点击此单元格选择正确的命令。";
  404. }
  405. else
  406. {
  407. // 清除警告样式
  408. row.Cells[6].Style.BackColor = System.Drawing.Color.Empty;
  409. row.Cells[6].ToolTipText = "";
  410. }
  411. }
  412. }
  413. /// <summary>
  414. /// 检查 SCPI 命令是否与设备类型兼容
  415. /// </summary>
  416. private bool IsScpiCommandCompatible(string scpiCommand, string deviceTarget)
  417. {
  418. if (string.IsNullOrEmpty(scpiCommand)) return true;
  419. if (deviceTarget == "Universal") return true;
  420. // APS7100 不兼容的命令(PSW250 专用)
  421. string[] psw250OnlyPatterns = new[] {
  422. "OUTP ON", "OUTP OFF", "OUTP?", "OUTP {0}",
  423. "MEAS:VOLT?", "MEAS:CURR?", "MEAS:POW?",
  424. "SOUR:CURR ", "SOUR:CURR?", // 注意空格,区分 SOUR:CURR:LIM:RMS
  425. "OUTP:MODE"
  426. };
  427. // PSW250 不兼容的命令(APS7100 专用)
  428. string[] aps7100OnlyPatterns = new[] {
  429. "OUTP:STAT", "OUTP:PROT",
  430. "MEAS:SCAL:",
  431. "SOUR:CURR:LIM:RMS",
  432. "SOUR:FREQ", "SOUR:PHAS",
  433. "SOUR:VOLT:RANG",
  434. "SYST:KLOC",
  435. "INIT:IMM", "STAT:OPER", "STAT:QUES",
  436. "DATA:SEQ", "DATA:SIM"
  437. };
  438. if (deviceTarget == "APS7100")
  439. {
  440. // 检查是否使用了 PSW250 专用命令
  441. foreach (var pattern in psw250OnlyPatterns)
  442. {
  443. if (scpiCommand.Contains(pattern) && !scpiCommand.Contains("SOUR:CURR:LIM:RMS"))
  444. {
  445. return false;
  446. }
  447. }
  448. }
  449. else if (deviceTarget == "PSW250")
  450. {
  451. // 检查是否使用了 APS7100 专用命令
  452. foreach (var pattern in aps7100OnlyPatterns)
  453. {
  454. if (scpiCommand.Contains(pattern))
  455. {
  456. return false;
  457. }
  458. }
  459. }
  460. return true;
  461. }
  462. // EditingControlShowing - 根据设备类型设置 SCPI 命令列表(联动选择)
  463. private void DgvConfig_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
  464. {
  465. if (dgvConfig.CurrentCell?.ColumnIndex != 6) return; // 只处理 ScpiCommand 列
  466. ComboBox cb = e.Control as ComboBox;
  467. if (cb == null) return;
  468. int rowIndex = dgvConfig.CurrentCell.RowIndex;
  469. if (rowIndex < 0 || rowIndex >= dgvConfig.Rows.Count) return;
  470. if (dgvConfig.Rows[rowIndex].IsNewRow) return;
  471. // 获取当前行的设备类型(DeviceTarget 列索引为 10)
  472. string deviceTarget = dgvConfig.Rows[rowIndex].Cells[10].Value?.ToString() ?? "Universal";
  473. // 获取当前单元格已有的值
  474. string currentValue = dgvConfig.CurrentCell.Value?.ToString() ?? "";
  475. // 构建对应设备类型的命令列表 - 严格根据设备类型筛选
  476. var commands = new List<string>();
  477. if (deviceTarget == "APS7100")
  478. {
  479. // APS7100:只显示通用命令 + APS7100 专用命令
  480. // ❌ 不添加 PSW250 专用命令(如 SOUR:CURR?, MEAS:VOLT? 等)
  481. commands.AddRange(_universalCommands);
  482. commands.AddRange(_aps7100Commands);
  483. }
  484. else if (deviceTarget == "PSW250")
  485. {
  486. // PSW250:只显示通用命令 + PSW250 专用命令
  487. // ❌ 不添加 APS7100 专用命令
  488. commands.AddRange(_universalCommands);
  489. commands.AddRange(_psw250Commands);
  490. }
  491. else // Universal
  492. {
  493. // Universal:显示所有命令
  494. commands.AddRange(_universalCommands);
  495. commands.AddRange(_aps7100Commands);
  496. commands.AddRange(_psw250Commands);
  497. }
  498. // ⚠ 注意:不再自动添加不兼容的旧命令到列表中
  499. // 如果当前值是不兼容的命令,用户需要从下拉列表中选择正确的命令
  500. // 不兼容的命令会通过 CellValueChanged 事件标记为红色
  501. // 设置 ComboBox 的数据源(使用 DataSource 比 Items 更稳定)
  502. cb.DataSource = commands;
  503. // 如果当前值在列表中,恢复选中
  504. if (!string.IsNullOrEmpty(currentValue) && commands.Contains(currentValue))
  505. {
  506. cb.SelectedItem = currentValue;
  507. }
  508. else if (commands.Count > 0)
  509. {
  510. // 当前值不在列表中(不兼容),显示列表第一项作为推荐
  511. // 用户可以选择或手动修改
  512. cb.SelectedIndex = -1; // 不自动选择,让用户主动选择
  513. }
  514. }
  515. private void LoadDataToGrid()
  516. {
  517. // 暂停绘制以提高性能
  518. dgvConfig.SuspendLayout();
  519. try
  520. {
  521. dgvConfig.DataSource = null; // 先清空,避免旧数据影响
  522. dgvConfig.DataSource = new System.ComponentModel.BindingList<ModbusRegisterMapping>(new List<ModbusRegisterMapping>(Mappings));
  523. // 数据加载后,标记不兼容的命令(不弹窗,由调用者决定是否提示)
  524. MarkIncompatibleCommands();
  525. }
  526. finally
  527. {
  528. dgvConfig.ResumeLayout(true);
  529. }
  530. }
  531. private void btnImport_Click(object sender, EventArgs e)
  532. {
  533. OpenFileDialog ofd = new OpenFileDialog { Filter = "Excel Files|*.xlsx;*.xls" };
  534. if (ofd.ShowDialog() == DialogResult.OK)
  535. {
  536. try
  537. {
  538. var loaded = _configService.LoadModbusConfig(ofd.FileName);
  539. Mappings = loaded;
  540. LastLoadedFilePath = ofd.FileName; // 保存文件路径
  541. // 加载数据(LoadDataToGrid 会自动调用 ValidateAllScpiCommands 检查兼容性)
  542. // 如果有不兼容的命令,会弹出警告;否则显示成功提示
  543. dgvConfig.SuspendLayout();
  544. try
  545. {
  546. dgvConfig.DataSource = null;
  547. dgvConfig.DataSource = new System.ComponentModel.BindingList<ModbusRegisterMapping>(new List<ModbusRegisterMapping>(Mappings));
  548. }
  549. finally
  550. {
  551. dgvConfig.ResumeLayout(true);
  552. }
  553. // 检查兼容性并统计问题数
  554. int incompatibleCount = CountIncompatibleCommands();
  555. if (incompatibleCount > 0)
  556. {
  557. // 标记不兼容的命令
  558. MarkIncompatibleCommands();
  559. MessageBox.Show(
  560. $"成功导入 {loaded.Count} 条规则。\n\n" +
  561. $"⚠ 发现 {incompatibleCount} 个不兼容的 SCPI 命令(已用红色标记)。\n\n" +
  562. "请检查标红的单元格,并从下拉列表中选择正确的命令。",
  563. "导入完成 - 需要检查",
  564. MessageBoxButtons.OK,
  565. MessageBoxIcon.Warning
  566. );
  567. }
  568. else
  569. {
  570. MessageBox.Show($"成功导入 {loaded.Count} 条规则", "提示");
  571. }
  572. }
  573. catch (Exception ex)
  574. {
  575. MessageBox.Show($"导入失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  576. }
  577. }
  578. }
  579. /// <summary>
  580. /// 统计不兼容的 SCPI 命令数量
  581. /// </summary>
  582. private int CountIncompatibleCommands()
  583. {
  584. int count = 0;
  585. foreach (DataGridViewRow row in dgvConfig.Rows)
  586. {
  587. if (row.IsNewRow) continue;
  588. string deviceTarget = row.Cells[10].Value?.ToString() ?? "Universal";
  589. string scpiCommand = row.Cells[6].Value?.ToString() ?? "";
  590. if (!string.IsNullOrEmpty(scpiCommand) && !IsScpiCommandCompatible(scpiCommand, deviceTarget))
  591. {
  592. count++;
  593. }
  594. }
  595. return count;
  596. }
  597. /// <summary>
  598. /// 标记所有不兼容的 SCPI 命令(红色背景)
  599. /// </summary>
  600. private void MarkIncompatibleCommands()
  601. {
  602. foreach (DataGridViewRow row in dgvConfig.Rows)
  603. {
  604. if (row.IsNewRow) continue;
  605. string deviceTarget = row.Cells[10].Value?.ToString() ?? "Universal";
  606. string scpiCommand = row.Cells[6].Value?.ToString() ?? "";
  607. if (string.IsNullOrEmpty(scpiCommand)) continue;
  608. bool isCompatible = IsScpiCommandCompatible(scpiCommand, deviceTarget);
  609. if (!isCompatible)
  610. {
  611. row.Cells[6].Style.BackColor = System.Drawing.Color.LightCoral;
  612. row.Cells[6].ToolTipText = $"⚠ 此命令不适用于 {deviceTarget}!请选择正确的命令。";
  613. }
  614. else
  615. {
  616. row.Cells[6].Style.BackColor = System.Drawing.Color.Empty;
  617. row.Cells[6].ToolTipText = "";
  618. }
  619. }
  620. }
  621. private void btnExport_Click(object sender, EventArgs e)
  622. {
  623. // 使用当前记录的路径作为默认目录和文件名
  624. string defaultFileName = "ModbusConfig.xlsx";
  625. string initialDirectory = "";
  626. if (!string.IsNullOrEmpty(LastLoadedFilePath))
  627. {
  628. defaultFileName = Path.GetFileName(LastLoadedFilePath);
  629. initialDirectory = Path.GetDirectoryName(LastLoadedFilePath) ?? "";
  630. }
  631. SaveFileDialog sfd = new SaveFileDialog
  632. {
  633. Filter = "Excel Files|*.xlsx",
  634. FileName = defaultFileName
  635. };
  636. if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory))
  637. {
  638. sfd.InitialDirectory = initialDirectory;
  639. }
  640. if (sfd.ShowDialog() == DialogResult.OK)
  641. {
  642. try
  643. {
  644. UpdateMappingsFromGrid();
  645. // 如果文件已存在,先删除(MiniExcel 不支持覆盖)
  646. if (File.Exists(sfd.FileName))
  647. {
  648. File.Delete(sfd.FileName);
  649. }
  650. MiniExcel.SaveAs(sfd.FileName, Mappings);
  651. // 记录导出路径,使保存配置时使用相同路径
  652. LastLoadedFilePath = sfd.FileName;
  653. MessageBox.Show($"导出成功\n路径: {sfd.FileName}", "提示");
  654. }
  655. catch (Exception ex)
  656. {
  657. MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  658. }
  659. }
  660. }
  661. private void btnSave_Click(object sender, EventArgs e)
  662. {
  663. try
  664. {
  665. UpdateMappingsFromGrid();
  666. DialogResult = DialogResult.OK;
  667. Close();
  668. }
  669. catch (Exception ex)
  670. {
  671. MessageBox.Show($"保存配置时出错: {ex.Message}", "错误");
  672. }
  673. }
  674. private void UpdateMappingsFromGrid()
  675. {
  676. var list = dgvConfig.DataSource as System.ComponentModel.BindingList<ModbusRegisterMapping>;
  677. if (list != null)
  678. {
  679. Mappings = new List<ModbusRegisterMapping>(list);
  680. }
  681. foreach (var m in Mappings)
  682. {
  683. if (m.Address < 1) throw new Exception($"地址 {m.Address} 无效,必须大于0");
  684. if (string.IsNullOrEmpty(m.ScpiCommand)) throw new Exception($"地址 {m.Address} 的 SCPI 命令不能为空");
  685. if (string.IsNullOrEmpty(m.RegisterType)) m.RegisterType = "Holding";
  686. if (string.IsNullOrEmpty(m.OperationType)) m.OperationType = "Write";
  687. if (string.IsNullOrEmpty(m.DataType)) m.DataType = "Int16";
  688. if (string.IsNullOrEmpty(m.DeviceTarget)) m.DeviceTarget = "Universal";
  689. }
  690. }
  691. private void btnToggleHelp_Click(object sender, EventArgs e)
  692. {
  693. pnlHelp.Visible = !pnlHelp.Visible;
  694. btnToggleHelp.Text = pnlHelp.Visible ? "隐藏说明" : "显示说明";
  695. }
  696. #region 右键菜单 - 行操作
  697. // 用于复制粘贴的临时存储
  698. private ModbusRegisterMapping? _copiedRow = null;
  699. /// <summary>
  700. /// 在当前行上方插入新行
  701. /// </summary>
  702. private void menuInsertAbove_Click(object sender, EventArgs e)
  703. {
  704. InsertRow(0); // 在当前行位置插入
  705. }
  706. /// <summary>
  707. /// 在当前行下方插入新行
  708. /// </summary>
  709. private void menuInsertBelow_Click(object sender, EventArgs e)
  710. {
  711. InsertRow(1); // 在当前行下方插入
  712. }
  713. /// <summary>
  714. /// 插入新行
  715. /// </summary>
  716. /// <param name="offset">0=当前行位置, 1=当前行下方</param>
  717. private void InsertRow(int offset)
  718. {
  719. var list = dgvConfig.DataSource as System.ComponentModel.BindingList<ModbusRegisterMapping>;
  720. if (list == null) return;
  721. // 取消任何编辑状态,防止冲突
  722. dgvConfig.EndEdit();
  723. dgvConfig.CancelEdit();
  724. int insertIndex = list.Count; // 默认插入到末尾
  725. if (dgvConfig.CurrentRow != null && !dgvConfig.CurrentRow.IsNewRow)
  726. {
  727. insertIndex = dgvConfig.CurrentRow.Index + offset;
  728. }
  729. // 确保索引有效
  730. if (insertIndex < 0) insertIndex = 0;
  731. if (insertIndex > list.Count) insertIndex = list.Count;
  732. // 创建新行,使用合理的默认值
  733. var newRow = new ModbusRegisterMapping
  734. {
  735. Address = GetNextAvailableAddress(list),
  736. RegisterType = "Holding",
  737. OperationType = "Write",
  738. DataType = "Float",
  739. ScpiCommand = "",
  740. DeviceTarget = "Universal",
  741. ScaleFactor = 1.0,
  742. Description = ""
  743. };
  744. list.Insert(insertIndex, newRow);
  745. // 选中新插入的行
  746. dgvConfig.ClearSelection();
  747. if (insertIndex < dgvConfig.Rows.Count)
  748. {
  749. dgvConfig.Rows[insertIndex].Selected = true;
  750. dgvConfig.CurrentCell = dgvConfig.Rows[insertIndex].Cells[0];
  751. }
  752. }
  753. /// <summary>
  754. /// 获取下一个可用地址
  755. /// </summary>
  756. private int GetNextAvailableAddress(System.ComponentModel.BindingList<ModbusRegisterMapping> list)
  757. {
  758. int maxAddr = 0;
  759. foreach (var item in list)
  760. {
  761. if (item.Address > maxAddr)
  762. maxAddr = item.Address;
  763. }
  764. return maxAddr + 1;
  765. }
  766. /// <summary>
  767. /// 删除当前选中的行
  768. /// </summary>
  769. private void menuDeleteRow_Click(object sender, EventArgs e)
  770. {
  771. var list = dgvConfig.DataSource as System.ComponentModel.BindingList<ModbusRegisterMapping>;
  772. if (list == null) return;
  773. // 取消任何编辑状态,防止冲突
  774. dgvConfig.EndEdit();
  775. dgvConfig.CancelEdit();
  776. // 获取选中的行
  777. var selectedRows = new List<int>();
  778. foreach (DataGridViewRow row in dgvConfig.SelectedRows)
  779. {
  780. if (!row.IsNewRow)
  781. selectedRows.Add(row.Index);
  782. }
  783. // 如果没有选中整行,则删除当前行
  784. if (selectedRows.Count == 0 && dgvConfig.CurrentRow != null && !dgvConfig.CurrentRow.IsNewRow)
  785. {
  786. selectedRows.Add(dgvConfig.CurrentRow.Index);
  787. }
  788. if (selectedRows.Count == 0)
  789. {
  790. MessageBox.Show("请先选择要删除的行", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  791. return;
  792. }
  793. // 确认删除
  794. var result = MessageBox.Show(
  795. $"确定要删除选中的 {selectedRows.Count} 行吗?",
  796. "确认删除",
  797. MessageBoxButtons.YesNo,
  798. MessageBoxIcon.Question);
  799. if (result != DialogResult.Yes) return;
  800. // 从后往前删除,避免索引错位
  801. selectedRows.Sort();
  802. selectedRows.Reverse();
  803. foreach (int idx in selectedRows)
  804. {
  805. if (idx >= 0 && idx < list.Count)
  806. list.RemoveAt(idx);
  807. }
  808. }
  809. /// <summary>
  810. /// 复制当前行
  811. /// </summary>
  812. private void menuCopyRow_Click(object sender, EventArgs e)
  813. {
  814. if (dgvConfig.CurrentRow == null || dgvConfig.CurrentRow.IsNewRow)
  815. return;
  816. var list = dgvConfig.DataSource as System.ComponentModel.BindingList<ModbusRegisterMapping>;
  817. if (list == null) return;
  818. int idx = dgvConfig.CurrentRow.Index;
  819. if (idx >= 0 && idx < list.Count)
  820. {
  821. var source = list[idx];
  822. // 深拷贝
  823. _copiedRow = new ModbusRegisterMapping
  824. {
  825. Address = source.Address,
  826. RegisterType = source.RegisterType,
  827. OperationType = source.OperationType,
  828. DataType = source.DataType,
  829. ScpiCommand = source.ScpiCommand,
  830. DeviceTarget = source.DeviceTarget,
  831. TriggerValue = source.TriggerValue,
  832. DataAddress = source.DataAddress,
  833. DataAddressType = source.DataAddressType,
  834. ScaleFactor = source.ScaleFactor,
  835. Description = source.Description
  836. };
  837. }
  838. }
  839. /// <summary>
  840. /// 粘贴行到当前位置
  841. /// </summary>
  842. private void menuPasteRow_Click(object sender, EventArgs e)
  843. {
  844. if (_copiedRow == null)
  845. {
  846. MessageBox.Show("没有复制的行可以粘贴", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  847. return;
  848. }
  849. var list = dgvConfig.DataSource as System.ComponentModel.BindingList<ModbusRegisterMapping>;
  850. if (list == null) return;
  851. // 取消任何编辑状态,防止冲突
  852. dgvConfig.EndEdit();
  853. dgvConfig.CancelEdit();
  854. // 计算插入位置
  855. int insertIndex = list.Count; // 默认插入到末尾
  856. if (dgvConfig.CurrentRow != null && !dgvConfig.CurrentRow.IsNewRow)
  857. {
  858. insertIndex = dgvConfig.CurrentRow.Index;
  859. }
  860. // 确保索引有效
  861. if (insertIndex < 0) insertIndex = 0;
  862. if (insertIndex > list.Count) insertIndex = list.Count;
  863. // 创建副本并分配新地址
  864. var newRow = new ModbusRegisterMapping
  865. {
  866. Address = GetNextAvailableAddress(list),
  867. RegisterType = _copiedRow.RegisterType,
  868. OperationType = _copiedRow.OperationType,
  869. DataType = _copiedRow.DataType,
  870. ScpiCommand = _copiedRow.ScpiCommand,
  871. DeviceTarget = _copiedRow.DeviceTarget,
  872. TriggerValue = _copiedRow.TriggerValue,
  873. DataAddress = _copiedRow.DataAddress,
  874. DataAddressType = _copiedRow.DataAddressType,
  875. ScaleFactor = _copiedRow.ScaleFactor,
  876. Description = _copiedRow.Description + " (副本)"
  877. };
  878. list.Insert(insertIndex, newRow);
  879. // 选中新插入的行
  880. dgvConfig.ClearSelection();
  881. if (insertIndex < dgvConfig.Rows.Count)
  882. {
  883. dgvConfig.Rows[insertIndex].Selected = true;
  884. dgvConfig.CurrentCell = dgvConfig.Rows[insertIndex].Cells[0];
  885. }
  886. }
  887. #endregion
  888. }
  889. }