Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 

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