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: 父對象指針,默認為 nullptrexplicit 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();// 獲取設備的 IDint deviceId() const;// 獲取設備的 IP 地址QString deviceAddress() const;// 獲取設備的名稱QString deviceName() const;// 獲取設備的端口號quint16 devicePort() const;signals:// 當設備連接狀態發生變化時發出的信號,攜帶設備 ID// deviceId: 設備的 IDvoid signal_DeviceConnectState(int deviceId);// 當所有寄存器讀取完成時發出的信號,攜帶讀取到的數據單元// unit: 包含讀取結果的 QModbusDataUnitvoid 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 客戶端已連接,設置設備狀態為 ConnectedsetStatus(DeviceStatus::Connected);} else if (state == QModbusDevice::ConnectingState) {// 如果 Modbus 客戶端正在連接,設置設備狀態為 ConnectingsetStatus(DeviceStatus::Connecting);} else if (state == QModbusDevice::UnconnectedState) {// 如果 Modbus 客戶端未連接,設置設備狀態為 DisconnectedsetStatus(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 客戶端未連接,輸出錯誤信息并返回 falseqDebug() << "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 {// 如果發送寫入請求失敗,輸出錯誤信息并返回 falseqDebug() << "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;// 發出設備連接狀態變化的信號,攜帶設備 IDemit 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 客戶端未連接,設置設備狀態為 DisconnectedsetStatus(DeviceStatus::Disconnected);break;case QModbusDevice::ConnectedState:// 如果 Modbus 客戶端已連接,設置設備狀態為 ConnectedsetStatus(DeviceStatus::Connected);break;case QModbusDevice::ConnectingState:// 如果 Modbus 客戶端正在連接,設置設備狀態為 ConnectingsetStatus(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 客戶端已連接,設置設備狀態為 ConnectedsetStatus(DeviceStatus::Connected);} else {// 如果 Modbus 客戶端未連接,設置設備狀態為 DisconnectedsetStatus(DeviceStatus::Disconnected);}// 返回連接結果return result;
}
// 斷開設備連接的函數實現
void ModbusDevice::disconnectDevice()
{// 斷開 Modbus 客戶端與設備的連接m_modbusClient->disconnectDevice();// 設置設備狀態為 DisconnectedsetStatus(DeviceStatus::Disconnected);// 停止心跳檢測定時器m_heartbeatTimer->stop();// 停止重連定時器m_reconnectTimer->stop();
}
// 獲取設備 ID 的函數實現
int ModbusDevice::deviceId() const
{// 從設備屬性中獲取設備 IDreturn 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 為 1properties["deviceID"] = 1;// 設置設備的 IP 地址為本地回環地址properties["ipAddress"] = "127.0.0.1";// 設置設備的端口號為 502properties["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();
}