/******************************* * Copyright (C) 2025. * * File Name: widget.cpp * Description: 显示与交互界面配置 * Others: * Version: 1.0 * Author: lipengpeng * Date: 2025-7-23 *******************************/ #include "widget.h" #include "ui_widget.h" #include #include #include #include #include #include #include /** * @brief Widget 构造函数 * @param parent 父窗口指针 * * 初始化用户界面、Modbus对象和串口通信对象。 * 配置串口参数并连接信号槽。 * 检查可用串口并填充到下拉框。 */ Widget::Widget(QWidget *parent) : QWidget(parent), ui_(new Ui::Widget), serialComm_(new SerialCommunicator(this)), modbus_(new MyModbus(this)) { ui_->setupUi(this); setFixedSize(700,500); //设置窗口界面大小 // 连接信号槽(串口->界面) connect(ui_->btn_connect, &QPushButton::clicked, this, &Widget::btnConnectClicked); connect(ui_->btn_write, &QPushButton::clicked, this, &Widget::onBtnWriteClicked); connect(ui_->btn_read, &QPushButton::clicked, this, &Widget::onBtnReadClicked); connect(ui_->btn_clear_read, &QPushButton::clicked, this, &Widget::onBtnClearreadClicked); connect(ui_->btn_clear_date, &QPushButton::clicked, this, &Widget::onBtnCleardateClicked); connect(ui_->btn_save_date, &QPushButton::clicked, this, &Widget::onBtnSavedateClicked); connect(ui_->btn_read_date, &QPushButton::clicked, this, &Widget::onBtnReaddateClicked); connect(ui_->btn_refresh, &QPushButton::clicked, this, &Widget::onBtnRefreshClicked); connect(ui_->btn_batch_input, &QPushButton::clicked, this, &Widget::onBtnBatchInputClicked); connect(ui_->line_input, &QLineEdit::textChanged, this, &Widget::onTextChanged); connect(ui_->btn_checkmsg, &QPushButton::clicked, this, &Widget::onBtnCheckmsgClicked); //当串口接收到完整数据后,serialComm_会发出dataReceived信号 connect(serialComm_, &SerialCommunicator::dataReceived, this, &Widget::onSerialDataReceived); //当串口状态改变时,serialComm_会发出statusChanged信号 connect(serialComm_, &SerialCommunicator::statusChanged, this, &Widget::onSerialStatusChanged); //当发送数据超过超时重发次数后仍未收到数据,serialComm_会发出timeoutOccurred信号 connect(serialComm_, &SerialCommunicator::timeoutOccurred, this, &Widget::onSerialTimeout); //当串口连接出现错误时(例如串口突然断开),serialComm_会发出physicalDisconnected信号 connect(serialComm_, &SerialCommunicator::physicalDisconnected, this, &Widget::sicalDisconnected); connect(serialComm_, &SerialCommunicator::stationConnect, this, &Widget::staionConneting); //设置默认波特率、数据位、校验位 ui_->combo_baud_rate->setCurrentIndex(3); ui_->combo_data_bit->setCurrentIndex(3); ui_->combo_check->setCurrentIndex(2); //串口未连接前锁定读取和写入按钮 ui_->btn_read->setEnabled(false); ui_->btn_write->setEnabled(false); //检查当前可用串口 QList serialList = QSerialPortInfo::availablePorts(); for(const QSerialPortInfo &serialInfo : serialList) { ui_->combo_serial_num->addItem(serialInfo.portName()); } } /** * @brief Widget 析构函数 * * 清理分配的资源:删除UI对象。 */ Widget::~Widget() { serialComm_->close(); delete ui_; } bool Widget::readLineEdit() { bool ok; //配置从站参数 quint16 stationAddress = ui_->line_station_address->text().toUShort(&ok); if (!ok) { QMessageBox::warning(this, "输入错误", "从站地址应为大于0的整数"); return false; } quint16 functionCode; switch (ui_->combo_function_code->currentIndex()) { case 0: functionCode = 0x01; break; case 1: functionCode = 0x03; break; case 2: functionCode = 0x0F; break; case 3: functionCode = 0x10; break; } quint16 startAddress = ui_->line_strat_address->text().toUShort(&ok); if (!ok || startAddress > 19999) { QMessageBox::warning(this, "输入错误", "起始地址必须为0-19999之间的整数"); return false; } quint16 length = ui_->line_length->text().toUShort(&ok); if (!ok) { QMessageBox::warning(this, "输入错误", "数据长度应为大于0的整数"); return false; } //将从站参数传入modbus中 if (!modbus_->setStation(stationAddress,functionCode,startAddress,length)) { QMessageBox::warning(this, "输入错误", "从站数据配置应大于0"); return false; } return true; } /** * @brief 串口连接/断开按钮点击处理 * * 根据当前按钮状态执行串口连接或断开操作。 * 连接时读取界面配置的串口参数并尝试打开串口。 * 成功连接后启用读写按钮,断开后禁用读写按钮。 */ void Widget::btnConnectClicked() { if (ui_->btn_connect->text() == "连接") { // 获取界面配置的串口参数2 QString portName = ui_->combo_serial_num->currentText(); QSerialPort::BaudRate baudRate = QSerialPort::BaudRate(ui_->combo_baud_rate->currentText().toInt()); QSerialPort::DataBits dataBits = QSerialPort::DataBits(ui_->combo_data_bit->currentText().toInt()); QSerialPort::Parity parity; switch (ui_->combo_check->currentIndex()) { case 0: parity = QSerialPort::NoParity; break; case 1: parity = QSerialPort::OddParity; break; case 2: parity = QSerialPort::EvenParity; break; default: parity = QSerialPort::NoParity; } QSerialPort::StopBits stopBits = QSerialPort::StopBits(ui_->comboBox_stopBit->currentText().toInt()); // 配置并打开串口 serialComm_->setPortParams(portName, baudRate, dataBits, parity, stopBits); if (serialComm_->open()) { ui_->btn_connect->setText("断开"); ui_->btn_read->setEnabled(true); ui_->btn_write->setEnabled(true); } else { QMessageBox::warning(this, "提示", "串口连接失败"); } } else { // 断开串口 serialComm_->close(); ui_->btn_connect->setText("连接"); ui_->btn_read->setEnabled(false); ui_->btn_write->setEnabled(false); } } /** * @brief 写数据按钮点击处理 * * 根据当前选择的功能码执行写线圈或写寄存器操作。 * 验证输入数据格式,生成Modbus命令并发送。 * 操作期间禁用读写按钮防止重复操作。 */ void Widget::onBtnWriteClicked() { // 校验起始地址 switch (ui_->combo_function_code->currentIndex()) //判断当前功能码 { case 2: //写多个线圈 { QString sendData = ui_->line_input->text().trimmed(); //读取输入框中的数据 //判断输入框中是否为空 if (sendData.isEmpty()) { QMessageBox::warning(this, "提示", "请至少输入一个数据"); return; } sendData.remove(" "); //移除输入数据中的空格 //判断输入数据是否为二进制格式 for (QChar ch : sendData) { if (ch != '0' && ch != '1') { QMessageBox::warning(this, "提示", "只允许输入 0 或 1!"); return; } } //拆分数据 QVector coils; for (QChar ch : sendData) { coils.append(ch == '1'); } //判断输入数据长度与设置的长度是否一致 if (coils.size() != ui_->line_length->text().toUShort()) { QMessageBox::warning(this, "提示", "输入数据数与设置的长度不匹配"); return; } if (!readLineEdit()) { return; } modbus_->writeCoil(coils); //使用WriteCoil方法生成命令报文 serialComm_->sendData(modbus_->sendCommand()); //发送生成的命令报文 //锁定按钮 ui_->btn_read->setEnabled(false); ui_->btn_write->setEnabled(false); break; } case 3: //写多个寄存器 { QString sendData = ui_->line_input->text().trimmed(); //读取数据 //判断输入框中是否为空 if (sendData.isEmpty()) { QMessageBox::warning(this, "提示", "请输入完整的寄存器数据"); return; } //拆分输入的寄存器数据 QStringList sl = sendData.split(','); QVector values; bool ok; // 检查输入是否是十进制 bool isDecimal = true; for (QString s : sl) { s.toInt(&ok); if (!ok) { isDecimal = false; break; } } //若判断输入的数据不是10进制,则按16进制数据处理 for (QString s : sl) { quint16 v; if (isDecimal) { // 如果是十进制输入 v = s.toUShort(&ok, 10); if (!ok) { QMessageBox::warning(this, "提示", "请输入正确的十进制值(0-65535)"); return; } } else { // 若输入数值不是十进制则按十六进制处理 v = s.toUShort(&ok, 16); if (!ok) { QMessageBox::warning(this, "提示", "请输入正确的10进制或16进制值(0-FFFF),或检查逗号格式"); return; } } values.append(v); } //判断输入数据长度与设置的长度是否一致 if (values.size() != ui_->line_length->text().toUShort()) { QMessageBox::warning(this, "提示", "输入数据数与设置的长度不匹配"); return; } if (!readLineEdit()) { return; } modbus_->writeRegister(values); //使用WriteRegister方法生成命令报文 serialComm_->sendData(modbus_->sendCommand()); //发送生成的命令报文 //锁定按钮 ui_->btn_read->setEnabled(false); ui_->btn_write->setEnabled(false); break; } default: { //若当前操作设置错误则进行提示 QMessageBox::warning(this, "提示", "请将“操作”切换为写线圈或写寄存器"); break; } } } /** * @brief 读数据按钮点击处理 * * 根据当前选择的功能码发送读线圈或读寄存器请求。 * 生成Modbus命令并发送,操作期间禁用读写按钮。 */ void Widget::onBtnReadClicked() { //若当前操作设置错误则进行提示 if (ui_->combo_function_code->currentIndex() == 2 || ui_->combo_function_code->currentIndex() == 3) { QMessageBox::warning(this, "提示", "请将“操作”切换为读线圈或读寄存器"); return; } if (!readLineEdit()) { return; } modbus_->readCoilAndReg(); //使用ReadCoilAndReg方法生成命令报文 serialComm_->sendData(modbus_->sendCommand()); //发送生成的命令报文 //锁定按钮 ui_->btn_read->setEnabled(false); ui_->btn_write->setEnabled(false); } /** * @brief 保存通信历史数据 * * 调用外部函数保存通信历史到文件。 */ void Widget::onBtnSavedateClicked() { saveDate(this,ui_->text_information); } /** * @brief 读取通信历史数据 * * 调用外部函数从文件加载通信历史。 */ void Widget::onBtnReaddateClicked() { readDate(this,ui_->text_information); } /** * @brief 清空状态通知文本框 */ void Widget::onBtnCleardateClicked() { ui_->text_information->clear(); } /** * @brief 清空数据读取文本框 */ void Widget::onBtnClearreadClicked() { ui_->text_data->clear(); } void Widget::sicalDisconnected() { ui_->btn_connect->setText("连接"); ui_->btn_read->setEnabled(false); ui_->btn_write->setEnabled(false); QMessageBox::warning(this, "警告", "串口已断开连接"); } /** * @brief 串口数据接收处理 * @param data 接收到的原始字节数据 * * 将接收到的数据交给Modbus解析,处理可能的错误, * 并根据当前功能码解析和显示线圈或寄存器数据。 */ void Widget::onSerialDataReceived(const QByteArray &data) { QByteArray revMessage = modbus_->receive(data); // 接收的原始数据交给Modbus解析 // 启用操作按钮 ui_->btn_read->setEnabled(true); ui_->btn_write->setEnabled(true); if (revMessage.isEmpty()) { //若modbus检测响应报文格式错误则丢弃数据,返回值为空 QMessageBox::warning(this, "提示", "返回报文站地址或CRC校验错误,请重试"); return; } //显示接收到的响应报文 ui_->text_information->append("接收报文:" + revMessage.toHex().toUpper()); // 检查响应报文中是否带有错误码 quint8 exCode = modbus_->errorCheck(); if (exCode) { QString errorMsg; switch (exCode) { case 0x01: errorMsg = "非法功能码"; break; case 0x02: errorMsg = "非法数据地址"; break; case 0x03: errorMsg = "非法数据值"; break; case 0x04: errorMsg = "从站设备故障"; break; case 0x11: errorMsg = "数据域个数异常"; break; default: errorMsg = "未知异常"; break; } QMessageBox::warning(this, "异常响应", QString("错误码: 0x%1(%2)").arg(QString::number(exCode, 16).toUpper(), errorMsg)); return; } // 解析并显示数据(根据功能码) switch (ui_->combo_function_code->currentIndex()) { case 0: { // 读线圈 QVector coils = modbus_->analReadCoil(); //使用AnalReadCoil解析线圈数据 ui_->text_data->append("线圈状态:"); for (int i = 0; i < ui_->line_length->text().toInt(); i++) { ui_->text_data->append(QString("线圈%1: %2").arg(i+1).arg(coils[i] ? "1" : "0")); } break; } case 1: { // 读寄存器 QVector regs = modbus_->analReadReg(); //使用AnalReadReg解析寄存器数据 ui_->text_data->append("寄存器值:"); for (int i = 0; i < ui_->line_length->text().toInt(); i++) { ui_->text_data->append(QString("寄存器%1: %2").arg(i+1).arg(regs[i])); } break; } } } /** * @brief 串口状态变化处理 * @param status 状态描述字符串 * * 在状态通知文本框中显示串口状态信息。 */ void Widget::onSerialStatusChanged(const QString &status) { ui_->text_information->append(status); // 显示状态信息(如连接成功、超时重发等) } void Widget::onTextChanged() { QString data = ui_->line_input->text(); data.remove(" "); data.remove(","); ui_->line_number->setText(QString::number(data.size())); } void Widget::staionConneting(bool online) { if(online) { ui_->label_staion->setText("成功连接到从站"); } else { ui_->label_staion->setText("未连接到从站"); } } /** * @brief 串口通信超时处理 * * 显示超时警告并重新启用操作按钮。 */ void Widget::onSerialTimeout() { QMessageBox::warning(this, "提示", "等待响应超时,请检查设备"); ui_->btn_read->setEnabled(true); ui_->btn_write->setEnabled(true); } /** * @brief 检测当前可用串口 * * 检查可用串口并填充到下拉框。 */ void Widget::onBtnRefreshClicked() { ui_->combo_serial_num->clear(); QList serialList = QSerialPortInfo::availablePorts(); for(const QSerialPortInfo &serialInfo : serialList) { ui_->combo_serial_num->addItem(serialInfo.portName()); } } void Widget::onBtnBatchInputClicked() { bool ok; int number = ui_->line_data_num->text().toInt(&ok); if (!ok || number <= 0 || number > 1000) { QMessageBox::warning(this, "输入错误", "请输入1-1000之间的整数"); return; } QString inputData = ui_->line_data->text(); for (int i = 0; i < number; i++) { ui_->line_input->insert(inputData); } } void Widget::onBtnCheckmsgClicked() { bool ok; quint16 stationAddress = ui_->line_station_address->text().toUShort(&ok); if (!ok || 0 == stationAddress) { QMessageBox::warning(this, "输入错误", "从站地址应为大于0的整数"); return; } modbus_->setStation(stationAddress,0x01,0x01,0x01); modbus_->readCoilAndReg(); serialComm_->setCheckMsg(modbus_->sendCommand()); }