上一文章主要了解下Modbus协议和事务处理流程,本章则直接贴放代码实现qt 上位机与温控器通讯。项目主要实现定时读取温度、设置温度、开始加热和停止加热四个功能。
采用的是 Modbus Rtu 通信
1 Qt modbus 模块依赖
QT += serialbus serialport
源代码
.h文件
cpp
#ifndef MODBUSDEVICE_H
#define MODBUSDEVICE_H
#include <QObject>
#include <QModbusDevice>
#include <QSerialPort>
#include <QModbusPdu>
#include <QModbusRtuSerialMaster>
#include <QSerialPortInfo>
#include <QTimer>
#define ADDR_READ_PV "0100"
#define ADDR_READ_SV "0300"
#define ADDR_SET_SV "0300"
#define ADDR_RUN "0190"
class ModbusDevice : public QObject
{
Q_OBJECT
public:
explicit ModbusDevice(QObject *parent = nullptr);
~ModbusDevice();
void setAddresss(const QVector<quint8> addrs);
Q_INVOKABLE void setCom(const QString &com);
Q_INVOKABLE QString getCom();
void open(const QString &portname);
void closeAndDelete();
Q_INVOKABLE void close();
Q_INVOKABLE void connect(const QString &com);
Q_INVOKABLE void disconnect();
Q_INVOKABLE bool state();
void sendReadPV();
void sendSetSV(const float v);
void sendRun();
void setStopSync(const quint8 addr);
void sendStop();
void sendSetSV(const quint8 addr,const float v);
void sendReadPV(const quint8 addr);
void sendReadSV(const quint8 addr);
void sendRun(const quint8 addr);
void sendStop(const quint8 addr);
void stopTimer();
signals:
void sigError(const QString &msg);
void sigDisconnected();
void sigConnected();
void sigModbusSendError(const QString &msg);
void sigInfo(const QString &msg);
void sigGetPV(const int index,const float v);
void sigGetSV(const int index,const float v);
private slots:
void onReplyTimeout();
private:
void creatDevice();
void reconnect();
void creatTimer();
void startTimer();
void handleSerialPortDisconnected();
void readValue(const quint8 address,const QString addr);
void setRun(const quint8 address,const bool isRun);
void startReplyTimer();
void stopReplyTimer();
QSerialPort::BaudRate baudRate=QSerialPort::Baud9600;
QSerialPort::DataBits dataBits=QSerialPort::Data8;
QSerialPort::Parity parity=QSerialPort::EvenParity;
QSerialPort::StopBits stopBits=QSerialPort::TwoStop;
QTimer m_pvTimer;
QTimer m_replyTimer;
QString m_strCom;
QModbusClient *m_device = nullptr;
QVector<quint8> m_deviceAddrs;
int m_errCount = 0;
int m_timeoutContinousCount = 0;
};
#endif // MODBUSDEVICE_H
cpp文件
cpp
#include "modbusdevice.h"
#include <QDebug>
#include <QModbusReply>
#include <QCoreApplication>
#include <QThread>
ModbusDevice::ModbusDevice(QObject *parent) : QObject(parent)
{
m_deviceAddrs << 1 <<2;
creatTimer();
creatDevice();
}
ModbusDevice::~ModbusDevice()
{
stopTimer();
closeAndDelete();
}
void ModbusDevice::setAddresss(const QVector<quint8> addrs)
{
m_deviceAddrs = addrs;
}
void ModbusDevice::open(const QString &portname)
{
if (!m_device)
{
sigError(QString("串口连接失败,请排查线路"));
return;
}
m_device->setConnectionParameter(QModbusDevice::SerialPortNameParameter,portname);
m_device->connectDevice();
}
void ModbusDevice::closeAndDelete()
{
if (!m_device)
return;
m_device->disconnectDevice();
m_device->disconnect();
delete m_device;
m_device = nullptr;
}
void ModbusDevice::close()
{
if (!m_device)
return;
stopTimer();
m_device->disconnectDevice();
}
void ModbusDevice::connect(const QString &com)
{
setCom(com);
open(com);
}
void ModbusDevice::disconnect()
{
if (!m_device)
return ;
close();
}
bool ModbusDevice::state()
{
if (!m_device)
return false;
return m_device->state() == QModbusDevice::ConnectedState;
}
void ModbusDevice::sendReadPV()
{
for(auto v:m_deviceAddrs)
{
sendReadPV(v);
}
}
void ModbusDevice::sendSetSV(const float v)
{
for(auto addr:m_deviceAddrs)
{
sendSetSV(addr,v);
}
}
void ModbusDevice::sendRun()
{
for(auto addr:m_deviceAddrs)
{
sendRun(addr);
}
}
void ModbusDevice::setStopSync(const quint8 addr)
{
if (!m_device)
return ;
if(m_device->state() != QModbusDevice::ConnectedState)
return;
QModbusReply *reply = nullptr;
const QByteArray pduData = QByteArray::fromHex("01900000");
quint16 fc = 0x06;
reply = m_device->sendRawRequest(QModbusRequest(QModbusRequest::FunctionCode(fc), pduData),addr);
while (!reply->isFinished()) {
QCoreApplication::processEvents();
if (QCoreApplication::instance()->closingDown()) {
break;
}
QThread::msleep(10);
}
if (reply->error() == QModbusDevice::NoError) {
qDebug() << "Command sent successfully.";
} else {
qWarning() << "Failed to send command:" << reply->errorString();
}
reply->deleteLater();
}
void ModbusDevice::sendStop()
{
for(auto addr:m_deviceAddrs)
{
sendStop(addr);
}
}
void ModbusDevice::sendSetSV(const quint8 addr,const float v)
{
if (!m_device)
return ;
if(m_device->state() != QModbusDevice::ConnectedState)
return;
QModbusReply *reply = nullptr;
short tmp = (v*10);
QString msg = QString("0300%1").arg(tmp, 4, 16, QLatin1Char('0')).toUpper();
const QByteArray pduData = QByteArray::fromHex(msg.toLatin1());
quint16 fc = 0x06;
reply = m_device->sendRawRequest(QModbusRequest(QModbusRequest::FunctionCode(fc), pduData),addr);
startReplyTimer();
if (!reply->isFinished()) {
QObject::connect(reply, &QModbusReply::finished, this, [=]() {
stopReplyTimer();
if (reply->error() == QModbusDevice::NoError) {
sigInfo(QString("设置指令发送成功"));
qDebug() << "setD1Value reveive:" << reply->rawResult();
} else if (reply->error() == QModbusDevice::ProtocolError) {
QString msg = QString("setValue response error: %1 Mobus exception: 0x%2").arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16);
qDebug() << msg;
emit sigModbusSendError(msg);
} else {
QString msg = (tr("setValue response error: %1 (code: 0x%2)").arg(reply->errorString()).arg(reply->error(), -1, 16));
qDebug() << msg;
emit sigModbusSendError(msg);
}
reply->deleteLater();
});
} else {
stopReplyTimer();
// broadcast replies return immediately
qDebug() << "Receive: Synchronous response pdu: " << reply->rawResult() << Qt::endl;
reply->deleteLater();
}
}
void ModbusDevice::sendReadPV(const quint8 addr)
{
readValue(addr,ADDR_READ_PV);
}
void ModbusDevice::sendReadSV(const quint8 addr)
{
readValue(addr,ADDR_READ_SV);
}
void ModbusDevice::sendRun(const quint8 addr)
{
setRun(addr,true);
}
void ModbusDevice::sendStop(const quint8 addr)
{
setRun(addr,false);
}
void ModbusDevice::setCom(const QString &com)
{
if(com != m_strCom)
{
m_strCom = com;
}
}
QString ModbusDevice::getCom()
{
return m_strCom;
}
void ModbusDevice::reconnect()
{
if (!m_device)
return ;
if (m_device->state() != QModbusDevice::ConnectedState) {
qInfo() << "Attempting to reconnect...";
m_device->connectDevice();
}
}
void ModbusDevice::creatDevice()
{
m_device = new QModbusRtuSerialMaster;
//m_device->setConnectionParameter(QModbusDevice::SerialPortNameParameter,portname);
m_device->setConnectionParameter(QModbusDevice::SerialParityParameter, parity);
m_device->setConnectionParameter(QModbusDevice::SerialDataBitsParameter,dataBits);
m_device->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,stopBits);
m_device->setConnectionParameter(QModbusDevice::SerialBaudRateParameter,baudRate);
m_device->setTimeout(1000);
m_device->setNumberOfRetries(1);
QObject::connect(m_device, &QModbusDevice::errorOccurred, this, [=](QModbusDevice::Error) {
QString msg = m_device->errorString();
qDebug().noquote() << QStringLiteral("Error: %1").arg(msg);
emit sigError(msg);
}, Qt::QueuedConnection);
QObject::connect(m_device, &QModbusDevice::stateChanged, [=](QModbusDevice::State state) {
switch (state) {
case QModbusDevice::UnconnectedState:
{
qDebug().noquote() << QStringLiteral("State: Entered unconnected state.");
stopTimer();
emit sigDisconnected();
}
break;
case QModbusDevice::ConnectingState:
qDebug().noquote() << QStringLiteral("State: Entered connecting state.");
break;
case QModbusDevice::ConnectedState:
{
qDebug().noquote() << QStringLiteral("State: Entered connected state.");
sendReadPV();
startTimer();
emit sigConnected();
}
break;
case QModbusDevice::ClosingState:
qDebug().noquote() << QStringLiteral("State: Entered closing state.");
break;
}
});
}
void ModbusDevice::handleSerialPortDisconnected()
{
qWarning() << "Serial port disconnected.";
close();
}
void ModbusDevice::creatTimer()
{
m_pvTimer.setInterval(4000);
QObject::connect(&m_pvTimer,&QTimer::timeout,this,[=]{
sendReadPV();
});
m_replyTimer.setInterval(3000);
m_replyTimer.setSingleShot(true);
QObject::connect(&m_replyTimer,SIGNAL(timeout()),this,SLOT(onReplyTimeout()));
}
void ModbusDevice::startTimer()
{
m_pvTimer.start();
}
void ModbusDevice::stopTimer()
{
m_pvTimer.stop();
}
void ModbusDevice::onReplyTimeout()
{
m_timeoutContinousCount++;
qDebug() << __func__;
sigModbusSendError(QString("发包响应超时,请排查线路"));
if(m_timeoutContinousCount>3)
{
sigError(QString("通信超时,请排查线路"));
m_timeoutContinousCount=0;
close();
}
}
void ModbusDevice::readValue(const quint8 address,const QString addr)
{
if (!m_device)
return ;
if(m_device->state() != QModbusDevice::ConnectedState)
return;
QModbusReply *reply = nullptr;
QString msg = addr+"0001";
const QByteArray pduData = QByteArray::fromHex(msg.toLatin1());
//qDebug() << "Send: Sending PDU with predefined function code.";
quint16 fc = 0x03;
reply = m_device->sendRawRequest(QModbusRequest(QModbusRequest::FunctionCode(fc), pduData),address);
startReplyTimer();
if (!reply->isFinished()) {
QObject::connect(reply, &QModbusReply::finished, this, [=]()
{
stopReplyTimer();
if (reply->error() == QModbusDevice::NoError) {
const QByteArray rawData = reply->rawResult().data();
//ex:0x030200dc
QByteArray lastTwoBytes = rawData.mid(rawData.size() - 2, 2);
// 使用 QDataStream 进行字节序转换
QDataStream stream(lastTwoBytes);
quint16 shortValue;
stream >> shortValue;
if(addr == ADDR_READ_PV)
{
emit sigGetPV(address,(float)shortValue/10);
}
else if(addr == ADDR_READ_SV){
emit sigGetSV(address,(float)shortValue/10);
}
} else if (reply->error() == QModbusDevice::ProtocolError) {
QString msg = QString("readValue response error: %1 Mobus exception: 0x%2").arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16);
qDebug() << msg;
emit sigModbusSendError(msg);
} else {
QString msg = (tr("readValue response error: %1 (code: 0x%2)").arg(reply->errorString()).arg(reply->error(), -1, 16));
qDebug() << msg;
m_errCount ++;
if(m_errCount > 3)
{
emit sigError(QString("温控%1 连接已断开").arg(address));
m_errCount = 0;
reconnect();
}
emit sigModbusSendError(msg);
}
reply->deleteLater();
});
} else {
stopReplyTimer();
// broadcast replies return immediately
qDebug() << "Receive: Synchronous response pdu: " << reply->rawResult() << Qt::endl;
reply->deleteLater();
}
}
void ModbusDevice::setRun(const quint8 address,const bool isRun)
{
if (!m_device)
return ;
if(m_device->state() != QModbusDevice::ConnectedState)
return;
QModbusReply *reply = nullptr;
const QByteArray pduData = isRun? QByteArray::fromHex("01900001"):QByteArray::fromHex("01900000");
quint16 fc = 0x06;
reply = m_device->sendRawRequest(QModbusRequest(QModbusRequest::FunctionCode(fc), pduData),address);
startReplyTimer();
if (!reply->isFinished()) {
QObject::connect(reply, &QModbusReply::finished, this, [=]() {
stopReplyTimer();
if (reply->error() == QModbusDevice::NoError) {
if(isRun)
sigInfo(QString("开始指令发送成功"));
else {
sigInfo(QString("停止指令发送成功"));
}
qDebug() << "setRun reveive:" << reply->rawResult();
} else if (reply->error() == QModbusDevice::ProtocolError) {
QString msg = QString("setRun response error: %1 Mobus exception: 0x%2").arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16);
qDebug() << msg;
emit sigModbusSendError(msg);
} else {
QString msg = (tr("setRun response error: %1 (code: 0x%2)").arg(reply->errorString()).arg(reply->error(), -1, 16));
qDebug() << msg;
emit sigModbusSendError(msg);
}
reply->deleteLater();
});
} else {
// broadcast replies return immediately
stopReplyTimer();
qDebug() << "setRun Receive: Synchronous response pdu: " << reply->rawResult() << Qt::endl;
reply->deleteLater();
}
}
void ModbusDevice::startReplyTimer()
{
m_replyTimer.start();
}
void ModbusDevice::stopReplyTimer()
{
m_replyTimer.stop();
}
异常处理
1. 通信断开异常检测
测试过程中发现,拔掉串口线,QModbusClient 并未检测到任何异常,这时候发包也不会有任何异常提示,所以在发包之后自己启动一个timer,来通过超时判断是否串口连接出现问题。有更好的方法可以分享下。
cpp
void ModbusDevice::sendSetSV(const quint8 addr,const float v)
{
if (!m_device)
return ;
if(m_device->state() != QModbusDevice::ConnectedState)
return;
QModbusReply *reply = nullptr;
short tmp = (v*10);
QString msg = QString("0300%1").arg(tmp, 4, 16, QLatin1Char('0')).toUpper();
const QByteArray pduData = QByteArray::fromHex(msg.toLatin1());
quint16 fc = 0x06;
reply = m_device->sendRawRequest(QModbusRequest(QModbusRequest::FunctionCode(fc), pduData),addr);
startReplyTimer();
if (!reply->isFinished()) {
QObject::connect(reply, &QModbusReply::finished, this, [=]() {
stopReplyTimer();
if (reply->error() == QModbusDevice::NoError) {
sigInfo(QString("设置指令发送成功"));
qDebug() << "setD1Value reveive:" << reply->rawResult();
} else if (reply->error() == QModbusDevice::ProtocolError) {
QString msg = QString("setValue response error: %1 Mobus exception: 0x%2").arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16);
qDebug() << msg;
emit sigModbusSendError(msg);
} else {
QString msg = (tr("setValue response error: %1 (code: 0x%2)").arg(reply->errorString()).arg(reply->error(), -1, 16));
qDebug() << msg;
emit sigModbusSendError(msg);
}
reply->deleteLater();
});
} else {
stopReplyTimer();
// broadcast replies return immediately
qDebug() << "Receive: Synchronous response pdu: " << reply->rawResult() << Qt::endl;
reply->deleteLater();
}
}
其中 startReplyTimer() 和 stopReplyTimer() 来实现链路检测。
2. 程序关闭时,需要发送一条指令,等发送和响应结束后才能关闭App
因为是加热设备,为了安全起见,程序关闭时,会主动停止加热。此时就需要实现一个同步发送的指令。等待返回后才能关闭App
cpp
void ModbusDevice::setStopSync(const quint8 addr)
{
if (!m_device)
return ;
if(m_device->state() != QModbusDevice::ConnectedState)
return;
QModbusReply *reply = nullptr;
const QByteArray pduData = QByteArray::fromHex("01900000");
quint16 fc = 0x06;
reply = m_device->sendRawRequest(QModbusRequest(QModbusRequest::FunctionCode(fc), pduData),addr);
while (!reply->isFinished()) {
QCoreApplication::processEvents();
if (QCoreApplication::instance()->closingDown()) {
break;
}
QThread::msleep(10);
}
if (reply->error() == QModbusDevice::NoError) {
qDebug() << "Command sent successfully.";
} else {
qWarning() << "Failed to send command:" << reply->errorString();
}
reply->deleteLater();
}