Qt 中基于 Modbus 协议的通用客户端学习笔记
一、概述
本客户端利用 Qt 的 QModbusTcpClient
实现与 Modbus 服务器的通信,具备连接、读写寄存器、心跳检测、自动重连等功能,旨在提供一个可靠且易用的 Modbus 客户端框架,方便在不同项目中集成使用。
二、核心功能实现
(一)初始化
- 在构造函数中:
- 首先初始化基类
QObject
,确保对象层次结构正确构建。 - 实例化
m_modbusClient
,将自身作为其父对象,以便管理内存。 - 创建
m_heartbeatTimer
和m_reconnectTimer
,并设置心跳间隔(如 2 秒)和自动重连间隔(如 2 秒)。 - 连接相关信号与槽:
- 当
m_heartbeatTimer
超时,触发checkHeartbeat
槽函数,用于检测心跳。 - 当
m_reconnectTimer
超时,触发attemptReconnect
槽函数,尝试重新连接服务器。 - 当
m_modbusClient
的状态改变时,连接到handleStateChanged
槽函数,以便及时更新设备连接状态。
- 当
- 首先初始化基类
(二)连接管理
- 获取连接状态:
getStatus
函数通过查询m_modbusClient
的当前状态(QModbusDevice::State
),将其转换为自定义的DeviceStatus
(如Connected
、Connecting
、Disconnected
)枚举类型,并返回。这有助于上层代码了解设备的实时连接情况。
- 连接设备:
connectDevice
函数首先从m_properties
中获取存储的 IP 地址和端口信息,分别设置到m_modbusClient
的连接参数中(使用QModbusDevice::NetworkAddressParameter
和QModbusDevice::NetworkPortParameter
),然后调用m_modbusClient
的connectDevice
方法尝试建立连接。根据连接结果,通过setStatus
函数更新设备状态。
- 断开连接:
disconnectDevice
函数调用m_modbusClient
的disconnectDevice
方法断开与服务器的连接,同时停止心跳定时器和自动重连定时器,并更新设备状态为Disconnected
。
(三)读写寄存器
- 写寄存器:
writeRegisters
函数首先检查设备是否已连接(通过m_modbusClient
的状态判断),若未连接则打印错误信息并返回false
。接着,构建QModbusDataUnit
对象,设置要写入的寄存器类型、起始地址以及数据值。然后,通过m_modbusClient
发送写请求,并连接到回复信号,在回复完成后根据错误码判断写入操作是否成功。
- 读寄存器:
readRegisters
函数同样先检查设备连接状态,若未连接则打印错误信息并返回。构建QModbusDataUnit
对象指定要读取的寄存器类型、起始地址和数量,然后通过m_modbusClient
发送读请求,并连接到onAllRegistersReadReady
槽函数,等待读取结果。
(四)心跳检测与自动重连
- 心跳检测:
checkHeartbeat
函数在心跳定时器超时时触发。它首先检查设备是否仍处于连接状态,若不是则直接处理连接断开情况(调用handleStateChanged
函数将状态设为Unconnected
)。若连接正常,则向服务器发送一个简单的读寄存器请求(通常读取保持寄存器的特定位置,如 0 地址,长度为 1),通过回复的错误码判断连接是否依然存活,若有错误则同样处理连接断开。
- 自动重连:
attemptReconnect
函数在自动重连定时器超时时执行。它检查设备是否未连接,若是,则尝试重新连接(调用connectDevice
函数),并记录重连次数。若重连次数达到预设上限(如MAX_RECONNECT_ATTEMPTS
),则停止自动重连定时器,防止无意义的重复尝试。
三、信号与槽
- 当
m_modbusClient
的状态改变时,handleStateChanged
槽函数被触发,根据不同的状态(UnconnectedState
、ConnectedState
、ConnectingState
)更新设备的自定义连接状态,并触发相应操作,如启动或停止定时器。 - 在读寄存器操作完成后,
onAllRegistersReadReady
槽函数被调用,它从QModbusReply
中获取读取结果数据单元QModbusDataUnit
,并通过信号allRegistersReadCompleted
将结果发射出去,供外部代码进一步处理。
欢迎大家提供优化建议。
#ifndef MODBUSDEVICE_H
#define MODBUSDEVICE_H
#include <QObject>
#include <QModbusTcpClient>
#include <QTimer>
#include <QMap>
#include <QVariant>
// 定义设备状态的枚举类型,用于表示 Modbus 设备当前的连接状态
enum class DeviceStatus {
Disconnected, // 设备处于断开连接状态
Connecting, // 设备正在尝试连接
Connected // 设备已成功连接
};
// ModbusDevice 类继承自 QObject,用于管理 Modbus TCP 客户端的连接和通信
class ModbusDevice : public QObject
{
Q_OBJECT
public:
// 构造函数,接受一个包含设备属性的 QMap 和可选的父对象指针
// properties: 包含设备属性的 QMap,如设备 ID、IP 地址、端口等
// parent: 父对象指针,默认为 nullptr
explicit ModbusDevice(const QMap<QString, QVariant>& properties, QObject *parent = nullptr);
// 析构函数,负责释放动态分配的资源
~ModbusDevice();
// 获取设备当前的连接状态
DeviceStatus getStatus();
// 获取设备的重连尝试次数
int deviceReconnectAttempts() const;
// 设置设备的属性值
// key: 属性的键,如 "deviceID", "ipAddress" 等
// value: 属性的值
void setProperty(const QString& key, const QVariant& value);
// 获取设备的属性值
// key: 属性的键
// 返回值: 属性的值,如果键不存在则返回默认值
QVariant getProperty(const QString& key) const;
// 向 Modbus 设备写入寄存器数据
// registerType: 寄存器类型,如 QModbusDataUnit::HoldingRegisters
// registerAddress: 寄存器的起始地址
// values: 要写入的寄存器值的向量
// 返回值: 写入操作是否成功
bool writeRegisters(QModbusDataUnit::RegisterType registerType, quint16 registerAddress, const QVector<quint16>& values);
// 从 Modbus 设备读取寄存器数据
// registerType: 寄存器类型
// registerAddress: 寄存器的起始地址
// count: 要读取的寄存器数量
void readRegisters(QModbusDataUnit::RegisterType registerType, quint16 registerAddress, quint16 count);
// 尝试连接到 Modbus 设备
// 返回值: 连接是否成功
bool connectDevice();
// 断开与 Modbus 设备的连接
void disconnectDevice();
// 获取设备的 ID
int deviceId() const;
// 获取设备的 IP 地址
QString deviceAddress() const;
// 获取设备的名称
QString deviceName() const;
// 获取设备的端口号
quint16 devicePort() const;
signals:
// 当设备连接状态发生变化时发出的信号,携带设备 ID
// deviceId: 设备的 ID
void signal_DeviceConnectState(int deviceId);
// 当所有寄存器读取完成时发出的信号,携带读取到的数据单元
// unit: 包含读取结果的 QModbusDataUnit
void allRegistersReadCompleted(const QModbusDataUnit& unit);
private slots:
// 心跳检测槽函数,定期检查设备的连接状态
void checkHeartbeat();
// 尝试重新连接设备的槽函数,在连接断开时调用
void attemptReconnect();
// 处理 Modbus 客户端状态变化的槽函数
// state: 新的 Modbus 设备状态
void handleStateChanged(QModbusDevice::State state);
// 处理寄存器读取完成的槽函数
void onAllRegistersReadReady();
// 设置设备的连接状态,并执行相应的操作
// newStatus: 新的设备连接状态
void setStatus(DeviceStatus newStatus);
private:
// 存储设备属性的 QMap,如设备 ID、IP 地址、端口等
QMap<QString, QVariant> m_properties;
// Modbus TCP 客户端指针,用于与 Modbus 设备进行通信
QModbusTcpClient* m_modbusClient;
// 心跳检测定时器,定期触发心跳检测
QTimer* m_heartbeatTimer;
// 重连定时器,在连接断开时定期尝试重新连接
QTimer* m_reconnectTimer;
// 设备当前的连接状态
DeviceStatus m_status;
// 记录设备的重连尝试次数
int m_reconnectAttempts = 0;
// 最大重连尝试次数,超过该次数将停止重连
static const int MAX_RECONNECT_ATTEMPTS = 5;
};
#endif // MODBUSDEVICE_H
#include "modbusdevice.h"
#include <QDebug>
// 构造函数实现
ModbusDevice::ModbusDevice(const QMap<QString, QVariant>& properties, QObject *parent)
: QObject(parent),
m_properties(properties),
m_modbusClient(new QModbusTcpClient(this)),
m_status(DeviceStatus::Disconnected)
{
// 创建心跳检测定时器,并设置定时器的父对象为当前对象
m_heartbeatTimer = new QTimer(this);
// 设置心跳检测定时器的间隔为 2000 毫秒(即 2 秒)
m_heartbeatTimer->setInterval(2000);
// 连接心跳检测定时器的超时信号到 checkHeartbeat 槽函数
connect(m_heartbeatTimer, &QTimer::timeout, this, &ModbusDevice::checkHeartbeat);
// 创建重连定时器,并设置定时器的父对象为当前对象
m_reconnectTimer = new QTimer(this);
// 设置重连定时器的间隔为 2000 毫秒(即 2 秒)
m_reconnectTimer->setInterval(2000);
// 连接重连定时器的超时信号到 attemptReconnect 槽函数
connect(m_reconnectTimer, &QTimer::timeout, this, &ModbusDevice::attemptReconnect);
// 连接 Modbus 客户端的状态变化信号到 handleStateChanged 槽函数
connect(m_modbusClient, &QModbusTcpClient::stateChanged, this, &ModbusDevice::handleStateChanged);
}
// 析构函数实现
ModbusDevice::~ModbusDevice()
{
// 释放 Modbus 客户端对象的内存
delete m_modbusClient;
}
// 获取设备状态的函数实现
DeviceStatus ModbusDevice::getStatus()
{
// 获取 Modbus 客户端的当前状态
QModbusDevice::State state = m_modbusClient->state();
if (state == QModbusDevice::ConnectedState) {
// 如果 Modbus 客户端已连接,设置设备状态为 Connected
setStatus(DeviceStatus::Connected);
} else if (state == QModbusDevice::ConnectingState) {
// 如果 Modbus 客户端正在连接,设置设备状态为 Connecting
setStatus(DeviceStatus::Connecting);
} else if (state == QModbusDevice::UnconnectedState) {
// 如果 Modbus 客户端未连接,设置设备状态为 Disconnected
setStatus(DeviceStatus::Disconnected);
}
// 返回设备的当前状态
return m_status;
}
// 获取设备重连尝试次数的函数实现
int ModbusDevice::deviceReconnectAttempts() const
{
// 返回设备的重连尝试次数
return m_reconnectAttempts;
}
// 设置设备属性的函数实现
void ModbusDevice::setProperty(const QString& key, const QVariant& value)
{
// 将属性键值对添加到 m_properties 中
m_properties[key] = value;
}
// 获取设备属性的函数实现
QVariant ModbusDevice::getProperty(const QString& key) const
{
// 从 m_properties 中获取指定键的属性值
return m_properties.value(key);
}
// 写入寄存器的函数实现
bool ModbusDevice::writeRegisters(QModbusDataUnit::RegisterType registerType, quint16 registerAddress, const QVector<quint16>& values)
{
if (m_modbusClient->state() != QModbusDevice::ConnectedState) {
// 如果 Modbus 客户端未连接,输出错误信息并返回 false
qDebug() << "Device is not connected. Cannot write registers.";
return false;
}
// 创建一个 Modbus 数据单元,用于写入寄存器
QModbusDataUnit writeUnit(registerType, registerAddress, values.size());
for (int i = 0; i < values.size(); ++i) {
// 将值写入 Modbus 数据单元
writeUnit.setValue(i, values[i]);
}
// 发送写入请求,并获取响应对象
QModbusReply* reply = m_modbusClient->sendWriteRequest(writeUnit, m_properties["deviceID"].toInt());
if (reply) {
// 连接响应对象的完成信号到一个 lambda 函数
connect(reply, &QModbusReply::finished, [reply]() {
if (reply->error() != QModbusDevice::NoError) {
// 如果写入过程中出现错误,输出错误信息
qDebug() << "Modbus write registers error:" << reply->errorString();
} else {
// 如果写入成功,输出成功信息
qDebug() << "Modbus write registers success:";
}
// 释放响应对象的内存
reply->deleteLater();
});
// 返回写入操作成功
return true;
} else {
// 如果发送写入请求失败,输出错误信息并返回 false
qDebug() << "Failed to send write registers request";
return false;
}
}
// 读取寄存器的函数实现
void ModbusDevice::readRegisters(QModbusDataUnit::RegisterType registerType, quint16 registerAddress, quint16 count)
{
if (m_modbusClient->state() != QModbusDevice::ConnectedState) {
// 如果 Modbus 客户端未连接,输出错误信息并返回
qDebug() << "Device is not connected. Cannot read registers.";
return;
}
// 创建一个 Modbus 数据单元,用于读取寄存器
QModbusDataUnit readUnit(registerType, registerAddress, count);
// 发送读取请求,并获取响应对象
QModbusReply* reply = m_modbusClient->sendReadRequest(readUnit, m_properties["deviceID"].toInt());
if (reply) {
// 连接响应对象的完成信号到 onAllRegistersReadReady 槽函数
connect(reply, &QModbusReply::finished, this, &ModbusDevice::onAllRegistersReadReady);
} else {
// 如果发送读取请求失败,输出错误信息
qDebug() << "Failed to send read registers request";
}
}
// 设置设备状态的函数实现
void ModbusDevice::setStatus(DeviceStatus newStatus)
{
if (m_status != newStatus) {
// 如果新状态与当前状态不同
m_status = newStatus;
// 发出设备连接状态变化的信号,携带设备 ID
emit signal_DeviceConnectState(m_properties["deviceID"].toInt());
if (m_status == DeviceStatus::Connected) {
// 如果设备已连接,启动心跳检测定时器
m_heartbeatTimer->start();
// 重置重连尝试次数
m_reconnectAttempts = 0;
} else if (m_status == DeviceStatus::Disconnected) {
// 如果设备断开连接,停止心跳检测定时器
m_heartbeatTimer->stop();
if (m_reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
// 如果重连尝试次数未达到最大次数,启动重连定时器
m_reconnectTimer->start();
}
}
}
}
// 处理 Modbus 客户端状态变化的槽函数实现
void ModbusDevice::handleStateChanged(QModbusDevice::State state)
{
switch (state) {
case QModbusDevice::UnconnectedState:
// 如果 Modbus 客户端未连接,设置设备状态为 Disconnected
setStatus(DeviceStatus::Disconnected);
break;
case QModbusDevice::ConnectedState:
// 如果 Modbus 客户端已连接,设置设备状态为 Connected
setStatus(DeviceStatus::Connected);
break;
case QModbusDevice::ConnectingState:
// 如果 Modbus 客户端正在连接,设置设备状态为 Connecting
setStatus(DeviceStatus::Connecting);
break;
default:
break;
}
}
// 心跳检测槽函数实现
void ModbusDevice::checkHeartbeat()
{
if (m_modbusClient->state() != QModbusDevice::ConnectedState) {
// 如果 Modbus 客户端未连接,处理设备断开连接的情况
handleStateChanged(QModbusDevice::UnconnectedState);
return;
}
// 创建一个 Modbus 数据单元,用于读取心跳寄存器
QModbusDataUnit readUnit(QModbusDataUnit::HoldingRegisters, 0, 1);
// 发送读取请求,并获取响应对象
QModbusReply* reply = m_modbusClient->sendReadRequest(readUnit, m_properties["deviceID"].toInt());
if (reply) {
// 连接响应对象的完成信号到一个 lambda 函数
connect(reply, &QModbusReply::finished, this, [this, reply]() {
if (reply->error() != QModbusDevice::NoError) {
// 如果读取过程中出现错误,处理设备断开连接的情况
handleStateChanged(QModbusDevice::UnconnectedState);
}
// 释放响应对象的内存
reply->deleteLater();
});
} else {
// 如果发送读取请求失败,处理设备断开连接的情况
handleStateChanged(QModbusDevice::UnconnectedState);
}
}
// 尝试重新连接设备的槽函数实现
void ModbusDevice::attemptReconnect()
{
if (m_modbusClient->state() != QModbusDevice::ConnectedState) {
// 如果 Modbus 客户端未连接,输出重连尝试信息
qDebug() << "尝试重连设备 ID:" << m_properties["deviceID"].toInt();
if (m_reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
// 如果重连尝试次数未达到最大次数,尝试重新连接设备
connectDevice();
// 增加重连尝试次数
m_reconnectAttempts++;
} else {
// 如果重连尝试次数达到最大次数,输出重连失败信息并停止重连定时器
qDebug() << "设备 ID:" << m_properties["deviceID"].toInt() << " 重连次数达到上限,停止重连";
m_reconnectTimer->stop();
}
}
}
// 处理寄存器读取完成的槽函数实现
void ModbusDevice::onAllRegistersReadReady()
{
// 获取发送信号的对象,并将其转换为 QModbusReply 指针
QModbusReply* reply = qobject_cast<QModbusReply*>(sender());
if (!reply) {
// 如果转换失败,输出错误信息并返回
qDebug() << "Invalid reply object";
return;
}
if (reply->error() != QModbusDevice::NoError) {
// 如果读取过程中出现错误,输出错误信息
qDebug() << "Modbus read all registers error:" << reply->errorString();
} else {
// 如果读取成功,获取读取结果并发出所有寄存器读取完成的信号
QModbusDataUnit unit = reply->result();
emit allRegistersReadCompleted(unit);
}
// 释放响应对象的内存
reply->deleteLater();
}
// 连接设备的函数实现
bool ModbusDevice::connectDevice()
{
// 从设备属性中获取 IP 地址
QString ipAddress = m_properties["ipAddress"].toString();
// 从设备属性中获取端口号
quint16 port = m_properties["port"].toUInt();
// 设置 Modbus 客户端的网络地址参数
m_modbusClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, ipAddress);
// 设置 Modbus 客户端的网络端口参数
m_modbusClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, port);
// 尝试连接 Modbus 设备,并获取连接结果
bool result = m_modbusClient->connectDevice();
if (m_modbusClient->state() == QModbusDevice::ConnectedState) {
// 如果 Modbus 客户端已连接,设置设备状态为 Connected
setStatus(DeviceStatus::Connected);
} else {
// 如果 Modbus 客户端未连接,设置设备状态为 Disconnected
setStatus(DeviceStatus::Disconnected);
}
// 返回连接结果
return result;
}
// 断开设备连接的函数实现
void ModbusDevice::disconnectDevice()
{
// 断开 Modbus 客户端与设备的连接
m_modbusClient->disconnectDevice();
// 设置设备状态为 Disconnected
setStatus(DeviceStatus::Disconnected);
// 停止心跳检测定时器
m_heartbeatTimer->stop();
// 停止重连定时器
m_reconnectTimer->stop();
}
// 获取设备 ID 的函数实现
int ModbusDevice::deviceId() const
{
// 从设备属性中获取设备 ID
return m_properties["deviceID"].toInt();
}
// 获取设备 IP 地址的函数实现
QString ModbusDevice::deviceAddress() const
{
// 从设备属性中获取设备 IP 地址
return m_properties["ipAddress"].toString();
}
// 获取设备名称的函数实现
QString ModbusDevice::deviceName() const
{
// 从设备属性中获取设备名称
return m_properties["deviceName"].toString();
}
// 获取设备端口号的函数实现
quint16 ModbusDevice::devicePort() const
{
// 从设备属性中获取设备端口号
return m_properties["port"].toInt();
}
#include "modbusdevice.h"
#include <QCoreApplication>
#include <QDebug>
int main(int argc, char *argv[])
{
// 创建一个 QCoreApplication 对象,用于管理应用程序的生命周期
QCoreApplication a(argc, argv);
// 创建一个 QMap 对象,用于存储 Modbus 设备的属性
QMap<QString, QVariant> properties;
// 设置设备的 ID 为 1
properties["deviceID"] = 1;
// 设置设备的 IP 地址为本地回环地址
properties["ipAddress"] = "127.0.0.1";
// 设置设备的端口号为 502
properties["port"] = 502;
// 设置设备的名称为 "TestDevice"
properties["deviceName"] = "TestDevice";
// 创建一个 ModbusDevice 对象,传入设备属性
ModbusDevice device(properties);
// 尝试连接 Modbus 设备
if (device.connectDevice()) {
// 如果连接成功,输出连接成功信息
qDebug() << "设备连接成功";
// 创建一个 QVector 对象,存储要写入寄存器的值
QVector<quint16> values = {1, 2, 3};
// 调用 writeRegisters 函数,向设备的保持寄存器写入数据
device.writeRegisters(QModbusDataUnit::HoldingRegisters, 0, values);
// 调用 readRegisters 函数,从设备的保持寄存器读取数据
device.readRegisters(QModbusDataUnit::HoldingRegisters, 0, 3);
// 连接 allRegistersReadCompleted 信号到一个 lambda 函数
// 当寄存器读取完成时,输出读取到的值
QObject::connect(&device, &ModbusDevice::allRegistersReadCompleted, [](const QModbusDataUnit& unit) {
qDebug() << "读取寄存器完成,值为:" << unit.values();
});
} else {
// 如果连接失败,输出连接失败信息
qDebug() << "设备连接失败";
}
// 进入应用程序的事件循环,等待事件发生
return a.exec();
}