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.
 
 
 

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