You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

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