Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

538 рядки
16 KiB

  1. /*******************************
  2. * Copyright (C) 2025.
  3. *
  4. * File Name: widget.cpp
  5. * Description: 显示与交互界面配置
  6. * Others:
  7. * Version: 1.0
  8. * Author: lipengpeng
  9. * Date: 2025-7-23
  10. *******************************/
  11. #include "widget.h"
  12. #include "ui_widget.h"
  13. #include <QSerialPortInfo>
  14. #include <QDebug>
  15. #include <QSerialPort>
  16. #include <QByteArray>
  17. #include <QString>
  18. #include <QVector>
  19. #include <QMessageBox>
  20. /**
  21. * @brief Widget 构造函数
  22. * @param parent 父窗口指针
  23. *
  24. * 初始化用户界面、Modbus对象和串口通信对象。
  25. * 配置串口参数并连接信号槽。
  26. * 检查可用串口并填充到下拉框。
  27. */
  28. Widget::Widget(QWidget *parent) :
  29. QWidget(parent),
  30. ui_(new Ui::Widget),
  31. serialComm_(new SerialCommunicator(this)),
  32. modbus_(new MyModbus(this))
  33. {
  34. ui_->setupUi(this);
  35. setFixedSize(700,500); //设置窗口界面大小
  36. // 连接信号槽(串口->界面)
  37. connect(ui_->btn_connect, &QPushButton::clicked,
  38. this, &Widget::btnConnectClicked);
  39. connect(ui_->btn_write, &QPushButton::clicked,
  40. this, &Widget::onBtnWriteClicked);
  41. connect(ui_->btn_read, &QPushButton::clicked,
  42. this, &Widget::onBtnReadClicked);
  43. connect(ui_->btn_clear_read, &QPushButton::clicked,
  44. this, &Widget::onBtnClearreadClicked);
  45. connect(ui_->btn_clear_date, &QPushButton::clicked,
  46. this, &Widget::onBtnCleardateClicked);
  47. connect(ui_->btn_save_date, &QPushButton::clicked,
  48. this, &Widget::onBtnSavedateClicked);
  49. connect(ui_->btn_read_date, &QPushButton::clicked,
  50. this, &Widget::onBtnReaddateClicked);
  51. connect(ui_->btn_refresh, &QPushButton::clicked,
  52. this, &Widget::onBtnRefreshClicked);
  53. connect(ui_->btn_batch_input, &QPushButton::clicked,
  54. this, &Widget::onBtnBatchInputClicked);
  55. connect(ui_->line_input, &QLineEdit::textChanged,
  56. this, &Widget::onTextChanged);
  57. //当串口接收到完整数据后,serialComm_会发出dataReceived信号
  58. connect(serialComm_, &SerialCommunicator::dataReceived,
  59. this, &Widget::onSerialDataReceived);
  60. //当串口状态改变时,serialComm_会发出statusChanged信号
  61. connect(serialComm_, &SerialCommunicator::statusChanged,
  62. this, &Widget::onSerialStatusChanged);
  63. //当发送数据超过超时重发次数后仍未收到数据,serialComm_会发出timeoutOccurred信号
  64. connect(serialComm_, &SerialCommunicator::timeoutOccurred,
  65. this, &Widget::onSerialTimeout);
  66. //当串口连接出现错误时(例如串口突然断开),serialComm_会发出physicalDisconnected信号
  67. connect(serialComm_, &SerialCommunicator::physicalDisconnected,
  68. this, &Widget::sicalDisconnected);
  69. //设置默认波特率、数据位、校验位
  70. ui_->combo_baud_rate->setCurrentIndex(3);
  71. ui_->combo_data_bit->setCurrentIndex(3);
  72. ui_->combo_check->setCurrentIndex(2);
  73. //串口未连接前锁定读取和写入按钮
  74. ui_->btn_read->setEnabled(false);
  75. ui_->btn_write->setEnabled(false);
  76. //检查当前可用串口
  77. QList<QSerialPortInfo> serialList = QSerialPortInfo::availablePorts();
  78. for(const QSerialPortInfo &serialInfo : serialList)
  79. {
  80. ui_->combo_serial_num->addItem(serialInfo.portName());
  81. }
  82. }
  83. /**
  84. * @brief Widget 析构函数
  85. *
  86. * 清理分配的资源:删除UI对象。
  87. */
  88. Widget::~Widget()
  89. {
  90. serialComm_->close();
  91. delete ui_;
  92. }
  93. bool Widget::readLineEdit()
  94. {
  95. bool ok;
  96. //配置从站参数
  97. quint16 stationAddress = ui_->line_station_address->text().toUShort(&ok);
  98. if (!ok)
  99. {
  100. QMessageBox::warning(this, "输入错误", "从站地址应为大于0的整数");
  101. return false;
  102. }
  103. quint16 functionCode;
  104. switch (ui_->combo_function_code->currentIndex())
  105. {
  106. case 0:
  107. functionCode = 0x01; break;
  108. case 1:
  109. functionCode = 0x03; break;
  110. case 2:
  111. functionCode = 0x0F; break;
  112. case 3:
  113. functionCode = 0x10; break;
  114. }
  115. quint16 startAddress = ui_->line_strat_address->text().toUShort(&ok);
  116. if (!ok || startAddress > 19999)
  117. {
  118. QMessageBox::warning(this, "输入错误", "起始地址必须为0-19999之间的整数");
  119. return false;
  120. }
  121. quint16 length = ui_->line_length->text().toUShort(&ok);
  122. if (!ok)
  123. {
  124. QMessageBox::warning(this, "输入错误", "数据长度应为大于0的整数");
  125. return false;
  126. }
  127. //将从站参数传入modbus中
  128. if (!modbus_->setStation(stationAddress,functionCode,startAddress,length))
  129. {
  130. QMessageBox::warning(this, "输入错误", "从站数据配置应大于0");
  131. return false;
  132. }
  133. return true;
  134. }
  135. /**
  136. * @brief 串口连接/断开按钮点击处理
  137. *
  138. * 根据当前按钮状态执行串口连接或断开操作。
  139. * 连接时读取界面配置的串口参数并尝试打开串口。
  140. * 成功连接后启用读写按钮,断开后禁用读写按钮。
  141. */
  142. void Widget::btnConnectClicked()
  143. {
  144. if (ui_->btn_connect->text() == "连接")
  145. {
  146. // 获取界面配置的串口参数2
  147. QString portName = ui_->combo_serial_num->currentText();
  148. QSerialPort::BaudRate baudRate = QSerialPort::BaudRate(ui_->combo_baud_rate->currentText().toInt());
  149. QSerialPort::DataBits dataBits = QSerialPort::DataBits(ui_->combo_data_bit->currentText().toInt());
  150. QSerialPort::Parity parity;
  151. switch (ui_->combo_check->currentIndex())
  152. {
  153. case 0: parity = QSerialPort::NoParity; break;
  154. case 1: parity = QSerialPort::OddParity; break;
  155. case 2: parity = QSerialPort::EvenParity; break;
  156. default: parity = QSerialPort::NoParity;
  157. }
  158. QSerialPort::StopBits stopBits = QSerialPort::StopBits(ui_->comboBox_stopBit->currentText().toInt());
  159. // 配置并打开串口
  160. serialComm_->setPortParams(portName, baudRate, dataBits, parity, stopBits);
  161. if (serialComm_->open())
  162. {
  163. ui_->btn_connect->setText("断开");
  164. ui_->btn_read->setEnabled(true);
  165. ui_->btn_write->setEnabled(true);
  166. }
  167. else
  168. {
  169. QMessageBox::warning(this, "提示", "串口连接失败");
  170. }
  171. }
  172. else
  173. {
  174. // 断开串口
  175. serialComm_->close();
  176. ui_->btn_connect->setText("连接");
  177. ui_->btn_read->setEnabled(false);
  178. ui_->btn_write->setEnabled(false);
  179. }
  180. }
  181. /**
  182. * @brief 写数据按钮点击处理
  183. *
  184. * 根据当前选择的功能码执行写线圈或写寄存器操作。
  185. * 验证输入数据格式,生成Modbus命令并发送。
  186. * 操作期间禁用读写按钮防止重复操作。
  187. */
  188. void Widget::onBtnWriteClicked()
  189. {
  190. // 校验起始地址
  191. switch (ui_->combo_function_code->currentIndex()) //判断当前功能码
  192. {
  193. case 2: //写多个线圈
  194. {
  195. QString sendData = ui_->line_input->text().trimmed(); //读取输入框中的数据
  196. //判断输入框中是否为空
  197. if (sendData.isEmpty())
  198. {
  199. QMessageBox::warning(this, "提示", "请至少输入一个数据");
  200. return;
  201. }
  202. sendData.remove(" "); //移除输入数据中的空格
  203. //判断输入数据是否为二进制格式
  204. for (QChar ch : sendData)
  205. {
  206. if (ch != '0' && ch != '1')
  207. {
  208. QMessageBox::warning(this, "提示", "只允许输入 0 或 1!");
  209. return;
  210. }
  211. }
  212. //拆分数据
  213. QVector<bool> coils;
  214. for (QChar ch : sendData)
  215. {
  216. coils.append(ch == '1');
  217. }
  218. //判断输入数据长度与设置的长度是否一致
  219. if (coils.size() != ui_->line_length->text().toUShort())
  220. {
  221. QMessageBox::warning(this, "提示", "输入数据数与设置的长度不匹配");
  222. return;
  223. }
  224. if (!readLineEdit())
  225. {
  226. return;
  227. }
  228. modbus_->writeCoil(coils); //使用WriteCoil方法生成命令报文
  229. serialComm_->sendData(modbus_->sendCommand()); //发送生成的命令报文
  230. //锁定按钮
  231. ui_->btn_read->setEnabled(false);
  232. ui_->btn_write->setEnabled(false);
  233. break;
  234. }
  235. case 3: //写多个寄存器
  236. {
  237. QString sendData = ui_->line_input->text().trimmed(); //读取数据
  238. //判断输入框中是否为空
  239. if (sendData.isEmpty())
  240. {
  241. QMessageBox::warning(this, "提示", "请输入完整的寄存器数据");
  242. return;
  243. }
  244. //拆分输入的寄存器数据
  245. QStringList sl = sendData.split(',');
  246. QVector<quint16> values;
  247. bool ok;
  248. // 检查输入是否是十进制
  249. bool isDecimal = true;
  250. for (QString s : sl)
  251. {
  252. s.toInt(&ok);
  253. if (!ok)
  254. {
  255. isDecimal = false;
  256. break;
  257. }
  258. }
  259. //若判断输入的数据不是10进制,则按16进制数据处理
  260. for (QString s : sl)
  261. {
  262. quint16 v;
  263. if (isDecimal)
  264. {
  265. // 如果是十进制输入
  266. v = s.toUShort(&ok, 10);
  267. if (!ok)
  268. {
  269. QMessageBox::warning(this, "提示", "请输入正确的十进制值(0-65535)");
  270. return;
  271. }
  272. }
  273. else
  274. {
  275. // 若输入数值不是十进制则按十六进制处理
  276. v = s.toUShort(&ok, 16);
  277. if (!ok)
  278. {
  279. QMessageBox::warning(this, "提示", "请输入正确的10进制或16进制值(0-FFFF),或检查逗号格式");
  280. return;
  281. }
  282. }
  283. values.append(v);
  284. }
  285. //判断输入数据长度与设置的长度是否一致
  286. if (values.size() != ui_->line_length->text().toUShort())
  287. {
  288. QMessageBox::warning(this, "提示", "输入数据数与设置的长度不匹配");
  289. return;
  290. }
  291. if (!readLineEdit())
  292. {
  293. return;
  294. }
  295. modbus_->writeRegister(values); //使用WriteRegister方法生成命令报文
  296. serialComm_->sendData(modbus_->sendCommand()); //发送生成的命令报文
  297. //锁定按钮
  298. ui_->btn_read->setEnabled(false);
  299. ui_->btn_write->setEnabled(false);
  300. break;
  301. }
  302. default:
  303. {
  304. //若当前操作设置错误则进行提示
  305. QMessageBox::warning(this, "提示", "请将“操作”切换为写线圈或写寄存器");
  306. break;
  307. }
  308. }
  309. }
  310. /**
  311. * @brief 读数据按钮点击处理
  312. *
  313. * 根据当前选择的功能码发送读线圈或读寄存器请求。
  314. * 生成Modbus命令并发送,操作期间禁用读写按钮。
  315. */
  316. void Widget::onBtnReadClicked()
  317. {
  318. //若当前操作设置错误则进行提示
  319. if (ui_->combo_function_code->currentIndex() == 2 ||
  320. ui_->combo_function_code->currentIndex() == 3)
  321. {
  322. QMessageBox::warning(this, "提示", "请将“操作”切换为读线圈或读寄存器");
  323. return;
  324. }
  325. if (!readLineEdit())
  326. {
  327. return;
  328. }
  329. modbus_->readCoilAndReg(); //使用ReadCoilAndReg方法生成命令报文
  330. serialComm_->sendData(modbus_->sendCommand()); //发送生成的命令报文
  331. //锁定按钮
  332. ui_->btn_read->setEnabled(false);
  333. ui_->btn_write->setEnabled(false);
  334. }
  335. /**
  336. * @brief 保存通信历史数据
  337. *
  338. * 调用外部函数保存通信历史到文件。
  339. */
  340. void Widget::onBtnSavedateClicked()
  341. {
  342. saveDate(this,ui_->text_information);
  343. }
  344. /**
  345. * @brief 读取通信历史数据
  346. *
  347. * 调用外部函数从文件加载通信历史。
  348. */
  349. void Widget::onBtnReaddateClicked()
  350. {
  351. readDate(this,ui_->text_information);
  352. }
  353. /**
  354. * @brief 清空状态通知文本框
  355. */
  356. void Widget::onBtnCleardateClicked()
  357. {
  358. ui_->text_information->clear();
  359. }
  360. /**
  361. * @brief 清空数据读取文本框
  362. */
  363. void Widget::onBtnClearreadClicked()
  364. {
  365. ui_->text_data->clear();
  366. }
  367. void Widget::sicalDisconnected()
  368. {
  369. ui_->btn_connect->setText("连接");
  370. ui_->btn_read->setEnabled(false);
  371. ui_->btn_write->setEnabled(false);
  372. QMessageBox::warning(this, "警告", "串口已断开连接");
  373. }
  374. /**
  375. * @brief 串口数据接收处理
  376. * @param data 接收到的原始字节数据
  377. *
  378. * 将接收到的数据交给Modbus解析,处理可能的错误,
  379. * 并根据当前功能码解析和显示线圈或寄存器数据。
  380. */
  381. void Widget::onSerialDataReceived(const QByteArray &data)
  382. {
  383. QByteArray revMessage = modbus_->receive(data); // 接收的原始数据交给Modbus解析
  384. // 启用操作按钮
  385. ui_->btn_read->setEnabled(true);
  386. ui_->btn_write->setEnabled(true);
  387. if (revMessage.isEmpty())
  388. {
  389. //若modbus检测响应报文格式错误则丢弃数据,返回值为空
  390. QMessageBox::warning(this, "提示", "返回报文站地址或CRC校验错误,请重试");
  391. return;
  392. }
  393. //显示接收到的响应报文
  394. ui_->text_information->append("接收报文:" + revMessage.toHex().toUpper());
  395. // 检查响应报文中是否带有错误码
  396. quint8 exCode = modbus_->errorCheck();
  397. if (exCode)
  398. {
  399. QString errorMsg;
  400. switch (exCode)
  401. {
  402. case 0x01: errorMsg = "非法功能码"; break;
  403. case 0x02: errorMsg = "非法数据地址"; break;
  404. case 0x03: errorMsg = "非法数据值"; break;
  405. case 0x04: errorMsg = "从站设备故障"; break;
  406. case 0x11: errorMsg = "数据域个数异常"; break;
  407. default: errorMsg = "未知异常"; break;
  408. }
  409. QMessageBox::warning(this, "异常响应",
  410. QString("错误码: 0x%1(%2)").arg(QString::number(exCode, 16).toUpper(), errorMsg));
  411. return;
  412. }
  413. // 解析并显示数据(根据功能码)
  414. switch (ui_->combo_function_code->currentIndex())
  415. {
  416. case 0:
  417. { // 读线圈
  418. QVector<bool> coils = modbus_->analReadCoil(); //使用AnalReadCoil解析线圈数据
  419. ui_->text_data->append("线圈状态:");
  420. for (int i = 0; i < ui_->line_length->text().toInt(); i++)
  421. {
  422. ui_->text_data->append(QString("线圈%1: %2").arg(i+1).arg(coils[i] ? "1" : "0"));
  423. }
  424. break;
  425. }
  426. case 1:
  427. { // 读寄存器
  428. QVector<quint16> regs = modbus_->analReadReg(); //使用AnalReadReg解析寄存器数据
  429. ui_->text_data->append("寄存器值:");
  430. for (int i = 0; i < ui_->line_length->text().toInt(); i++)
  431. {
  432. ui_->text_data->append(QString("寄存器%1: %2").arg(i+1).arg(regs[i]));
  433. }
  434. break;
  435. }
  436. }
  437. }
  438. /**
  439. * @brief 串口状态变化处理
  440. * @param status 状态描述字符串
  441. *
  442. * 在状态通知文本框中显示串口状态信息。
  443. */
  444. void Widget::onSerialStatusChanged(const QString &status)
  445. {
  446. ui_->text_information->append(status); // 显示状态信息(如连接成功、超时重发等)
  447. }
  448. void Widget::onTextChanged()
  449. {
  450. QString data = ui_->line_input->text();
  451. data.remove(" ");
  452. data.remove(",");
  453. ui_->line_number->setText(QString::number(data.size()));
  454. }
  455. /**
  456. * @brief 串口通信超时处理
  457. *
  458. * 显示超时警告并重新启用操作按钮。
  459. */
  460. void Widget::onSerialTimeout()
  461. {
  462. QMessageBox::warning(this, "提示", "等待响应超时,请检查设备");
  463. ui_->btn_read->setEnabled(true);
  464. ui_->btn_write->setEnabled(true);
  465. }
  466. /**
  467. * @brief 检测当前可用串口
  468. *
  469. * 检查可用串口并填充到下拉框。
  470. */
  471. void Widget::onBtnRefreshClicked()
  472. {
  473. ui_->combo_serial_num->clear();
  474. QList<QSerialPortInfo> serialList = QSerialPortInfo::availablePorts();
  475. for(const QSerialPortInfo &serialInfo : serialList)
  476. {
  477. ui_->combo_serial_num->addItem(serialInfo.portName());
  478. }
  479. }
  480. void Widget::onBtnBatchInputClicked()
  481. {
  482. bool ok;
  483. int number = ui_->line_data_num->text().toInt(&ok);
  484. if (!ok || number <= 0 || number > 1000)
  485. {
  486. QMessageBox::warning(this, "输入错误", "请输入1-1000之间的整数");
  487. return;
  488. }
  489. QString inputData = ui_->line_data->text();
  490. for (int i = 0; i < number; i++)
  491. {
  492. ui_->line_input->insert(inputData);
  493. }
  494. }