// Copyright (C) 2017 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QMODBUSTCPCLIENT_P_H #define QMODBUSTCPCLIENT_P_H #include #include #include #include "QtSerialBus/qmodbustcpclient.h" #include "private/qmodbusclient_p.h" // // W A R N I N G // ------------- // // This file is not part of the Qt API. It exists purely as an // implementation detail. This header file may change from version to // version without notice, or even be removed. // // We mean it. // QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS) Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS_LOW) class QModbusTcpClientPrivate : public QModbusClientPrivate { Q_DECLARE_PUBLIC(QModbusTcpClient) public: void setupTcpSocket() { Q_Q(QModbusTcpClient); m_socket = new QTcpSocket(q); QObject::connect(m_socket, &QAbstractSocket::connected, q, [this]() { qCDebug(QT_MODBUS) << "(TCP client) Connected to" << m_socket->peerAddress() << "on port" << m_socket->peerPort(); Q_Q(QModbusTcpClient); responseBuffer.clear(); q->setState(QModbusDevice::ConnectedState); }); QObject::connect(m_socket, &QAbstractSocket::disconnected, q, [this]() { qCDebug(QT_MODBUS) << "(TCP client) Connection closed."; Q_Q(QModbusTcpClient); q->setState(QModbusDevice::UnconnectedState); cleanupTransactionStore(); }); QObject::connect(m_socket, &QAbstractSocket::errorOccurred, q, [this](QAbstractSocket::SocketError /*error*/) { Q_Q(QModbusTcpClient); if (m_socket->state() == QAbstractSocket::UnconnectedState) { cleanupTransactionStore(); q->setState(QModbusDevice::UnconnectedState); } q->setError(QModbusClient::tr("TCP socket error (%1).").arg(m_socket->errorString()), QModbusDevice::ConnectionError); }); QObject::connect(m_socket, &QIODevice::readyRead, q, [this](){ responseBuffer += m_socket->read(m_socket->bytesAvailable()); qCDebug(QT_MODBUS_LOW) << "(TCP client) Response buffer:" << responseBuffer.toHex(); while (!responseBuffer.isEmpty()) { // can we read enough for Modbus ADU header? if (responseBuffer.size() < mbpaHeaderSize) { qCDebug(QT_MODBUS_LOW) << "(TCP client) MBPA header too short. Waiting for more data."; return; } quint8 serverAddress; quint16 transactionId, bytesPdu, protocolId; QDataStream input(responseBuffer); input >> transactionId >> protocolId >> bytesPdu >> serverAddress; // stop the timer as soon as we know enough about the transaction const bool knownTransaction = m_transactionStore.contains(transactionId); if (knownTransaction && m_transactionStore[transactionId].timer) m_transactionStore[transactionId].timer->stop(); qCDebug(QT_MODBUS) << "(TCP client) tid:" << Qt::hex << transactionId << "size:" << bytesPdu << "server address:" << serverAddress; // The length field is the byte count of the following fields, including the Unit // Identifier and the PDU, so we remove on byte. bytesPdu--; int tcpAduSize = mbpaHeaderSize + bytesPdu; if (responseBuffer.size() < tcpAduSize) { qCDebug(QT_MODBUS) << "(TCP client) PDU too short. Waiting for more data"; return; } QModbusResponse responsePdu; input >> responsePdu; qCDebug(QT_MODBUS) << "(TCP client) Received PDU:" << responsePdu.functionCode() << responsePdu.data().toHex(); responseBuffer.remove(0, tcpAduSize); if (!knownTransaction) { qCDebug(QT_MODBUS) << "(TCP client) No pending request for response with " "given transaction ID, ignoring response message."; } else { processQueueElement(responsePdu, m_transactionStore[transactionId]); } } }); } QModbusReply *enqueueRequest(const QModbusRequest &request, int serverAddress, const QModbusDataUnit &unit, QModbusReply::ReplyType type) override { auto writeToSocket = [this](quint16 tId, const QModbusRequest &request, int address) { QByteArray buffer; QDataStream output(&buffer, QIODevice::WriteOnly); output << tId << quint16(0) << quint16(request.size() + 1) << quint8(address) << request; int writtenBytes = m_socket->write(buffer); if (writtenBytes == -1 || writtenBytes < buffer.size()) { Q_Q(QModbusTcpClient); qCDebug(QT_MODBUS) << "(TCP client) Cannot write request to socket."; q->setError(QModbusTcpClient::tr("Could not write request to socket."), QModbusDevice::WriteError); return false; } qCDebug(QT_MODBUS_LOW) << "(TCP client) Sent TCP ADU:" << buffer.toHex(); qCDebug(QT_MODBUS) << "(TCP client) Sent TCP PDU:" << request << "with tId:" <connect(reply, &QObject::destroyed, q, [this, tId](QObject *) { if (!m_transactionStore.contains(tId)) return; const QueueElement element = m_transactionStore.take(tId); if (element.timer) element.timer->stop(); }); if (element.timer) { q->connect(q, &QModbusClient::timeoutChanged, element.timer.data(), QOverload::of(&QTimer::setInterval)); QObject::connect(element.timer.data(), &QTimer::timeout, q, [this, writeToSocket, tId]() { if (!m_transactionStore.contains(tId)) return; QueueElement elem = m_transactionStore.take(tId); if (elem.reply.isNull()) return; if (elem.numberOfRetries > 0) { elem.numberOfRetries--; if (!writeToSocket(tId, elem.requestPdu, elem.reply->serverAddress())) return; m_transactionStore.insert(tId, elem); elem.timer->start(); qCDebug(QT_MODBUS) << "(TCP client) Resend request with tId:" << Qt::hex << tId; } else { qCDebug(QT_MODBUS) << "(TCP client) Timeout of request with tId:" <setError(QModbusDevice::TimeoutError, QModbusClient::tr("Request timeout.")); } }); element.timer->start(); } else { qCWarning(QT_MODBUS) << "(TCP client) No response timeout timer for request with tId:" << Qt::hex << tId << ". Expected timeout:" << m_responseTimeoutDuration; } incrementTransactionId(); return reply; } // TODO: Review once we have a transport layer in place. bool isOpen() const override { if (m_socket) return m_socket->isOpen(); return false; } void cleanupTransactionStore() { if (m_transactionStore.isEmpty()) return; qCDebug(QT_MODBUS) << "(TCP client) Cleanup of pending requests"; for (const auto &elem : std::as_const(m_transactionStore)) { if (elem.reply.isNull()) continue; elem.reply->setError(QModbusDevice::ReplyAbortedError, QModbusClient::tr("Reply aborted due to connection closure.")); } m_transactionStore.clear(); } // This doesn't overflow, it rather "wraps around". Expected. inline void incrementTransactionId() { m_transactionId++; } inline int transactionId() const { return m_transactionId; } QIODevice *device() const override { return m_socket; } QTcpSocket *m_socket = nullptr; QByteArray responseBuffer; QHash m_transactionStore; int mbpaHeaderSize = 7; private: // Private to avoid using the wrong id inside the timer lambda, quint16 m_transactionId = 0; // capturing 'this' will not copy the id. }; QT_END_NAMESPACE #endif // QMODBUSTCPCLIENT_P_H