1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#ifndef QMODBUSTCPCLIENT_P_H
6#define QMODBUSTCPCLIENT_P_H
7
8#include <QtCore/qloggingcategory.h>
9#include <QtNetwork/qhostaddress.h>
10#include <QtNetwork/qtcpsocket.h>
11#include "QtSerialBus/qmodbustcpclient.h"
12
13#include "private/qmodbusclient_p.h"
14
15//
16// W A R N I N G
17// -------------
18//
19// This file is not part of the Qt API. It exists purely as an
20// implementation detail. This header file may change from version to
21// version without notice, or even be removed.
22//
23// We mean it.
24//
25
26QT_BEGIN_NAMESPACE
27
28Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS)
29Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS_LOW)
30
31class QModbusTcpClientPrivate : public QModbusClientPrivate
32{
33 Q_DECLARE_PUBLIC(QModbusTcpClient)
34
35public:
36 void setupTcpSocket()
37 {
38 Q_Q(QModbusTcpClient);
39
40 m_socket = new QTcpSocket(q);
41
42 QObject::connect(sender: m_socket, signal: &QAbstractSocket::connected, context: q, slot: [this]() {
43 qCDebug(QT_MODBUS) << "(TCP client) Connected to" << m_socket->peerAddress()
44 << "on port" << m_socket->peerPort();
45 Q_Q(QModbusTcpClient);
46 responseBuffer.clear();
47 q->setState(QModbusDevice::ConnectedState);
48 });
49
50 QObject::connect(sender: m_socket, signal: &QAbstractSocket::disconnected, context: q, slot: [this]() {
51 qCDebug(QT_MODBUS) << "(TCP client) Connection closed.";
52 Q_Q(QModbusTcpClient);
53 q->setState(QModbusDevice::UnconnectedState);
54 cleanupTransactionStore();
55 });
56
57 QObject::connect(sender: m_socket, signal: &QAbstractSocket::errorOccurred, context: q,
58 slot: [this](QAbstractSocket::SocketError /*error*/)
59 {
60 Q_Q(QModbusTcpClient);
61
62 if (m_socket->state() == QAbstractSocket::UnconnectedState) {
63 cleanupTransactionStore();
64 q->setState(QModbusDevice::UnconnectedState);
65 }
66 q->setError(errorText: QModbusClient::tr(s: "TCP socket error (%1).").arg(a: m_socket->errorString()),
67 error: QModbusDevice::ConnectionError);
68 });
69
70 QObject::connect(sender: m_socket, signal: &QIODevice::readyRead, context: q, slot: [this](){
71 responseBuffer += m_socket->read(maxlen: m_socket->bytesAvailable());
72 qCDebug(QT_MODBUS_LOW) << "(TCP client) Response buffer:" << responseBuffer.toHex();
73
74 while (!responseBuffer.isEmpty()) {
75 // can we read enough for Modbus ADU header?
76 if (responseBuffer.size() < mbpaHeaderSize) {
77 qCDebug(QT_MODBUS_LOW) << "(TCP client) MBPA header too short. Waiting for more data.";
78 return;
79 }
80
81 quint8 serverAddress;
82 quint16 transactionId, bytesPdu, protocolId;
83 QDataStream input(responseBuffer);
84 input >> transactionId >> protocolId >> bytesPdu >> serverAddress;
85
86 // stop the timer as soon as we know enough about the transaction
87 const bool knownTransaction = m_transactionStore.contains(key: transactionId);
88 if (knownTransaction && m_transactionStore[transactionId].timer)
89 m_transactionStore[transactionId].timer->stop();
90
91 qCDebug(QT_MODBUS) << "(TCP client) tid:" << Qt::hex << transactionId << "size:"
92 << bytesPdu << "server address:" << serverAddress;
93
94 // The length field is the byte count of the following fields, including the Unit
95 // Identifier and the PDU, so we remove on byte.
96 bytesPdu--;
97
98 int tcpAduSize = mbpaHeaderSize + bytesPdu;
99 if (responseBuffer.size() < tcpAduSize) {
100 qCDebug(QT_MODBUS) << "(TCP client) PDU too short. Waiting for more data";
101 return;
102 }
103
104 QModbusResponse responsePdu;
105 input >> responsePdu;
106 qCDebug(QT_MODBUS) << "(TCP client) Received PDU:" << responsePdu.functionCode()
107 << responsePdu.data().toHex();
108
109 responseBuffer.remove(index: 0, len: tcpAduSize);
110
111 if (!knownTransaction) {
112 qCDebug(QT_MODBUS) << "(TCP client) No pending request for response with "
113 "given transaction ID, ignoring response message.";
114 } else {
115 processQueueElement(pdu: responsePdu, element: m_transactionStore[transactionId]);
116 }
117 }
118 });
119 }
120
121 QModbusReply *enqueueRequest(const QModbusRequest &request, int serverAddress,
122 const QModbusDataUnit &unit,
123 QModbusReply::ReplyType type) override
124 {
125 auto writeToSocket = [this](quint16 tId, const QModbusRequest &request, int address) {
126 QByteArray buffer;
127 QDataStream output(&buffer, QIODevice::WriteOnly);
128 output << tId << quint16(0) << quint16(request.size() + 1) << quint8(address) << request;
129
130 int writtenBytes = m_socket->write(data: buffer);
131 if (writtenBytes == -1 || writtenBytes < buffer.size()) {
132 Q_Q(QModbusTcpClient);
133 qCDebug(QT_MODBUS) << "(TCP client) Cannot write request to socket.";
134 q->setError(errorText: QModbusTcpClient::tr(s: "Could not write request to socket."),
135 error: QModbusDevice::WriteError);
136 return false;
137 }
138 qCDebug(QT_MODBUS_LOW) << "(TCP client) Sent TCP ADU:" << buffer.toHex();
139 qCDebug(QT_MODBUS) << "(TCP client) Sent TCP PDU:" << request << "with tId:" <<Qt:: hex
140 << tId;
141 return true;
142 };
143
144 const int tId = transactionId();
145 if (!writeToSocket(tId, request, serverAddress))
146 return nullptr;
147
148 Q_Q(QModbusTcpClient);
149 auto reply = new QModbusReply(type, serverAddress, q);
150 const auto element = QueueElement{ reply, request, unit, m_numberOfRetries,
151 m_responseTimeoutDuration };
152 m_transactionStore.insert(key: tId, value: element);
153
154 q->connect(sender: reply, signal: &QObject::destroyed, context: q, slot: [this, tId](QObject *) {
155 if (!m_transactionStore.contains(key: tId))
156 return;
157 const QueueElement element = m_transactionStore.take(key: tId);
158 if (element.timer)
159 element.timer->stop();
160 });
161
162 if (element.timer) {
163 q->connect(sender: q, signal: &QModbusClient::timeoutChanged,
164 context: element.timer.data(), slot: QOverload<int>::of(ptr: &QTimer::setInterval));
165 QObject::connect(sender: element.timer.data(), signal: &QTimer::timeout, context: q, slot: [this, writeToSocket, tId]() {
166 if (!m_transactionStore.contains(key: tId))
167 return;
168
169 QueueElement elem = m_transactionStore.take(key: tId);
170 if (elem.reply.isNull())
171 return;
172
173 if (elem.numberOfRetries > 0) {
174 elem.numberOfRetries--;
175 if (!writeToSocket(tId, elem.requestPdu, elem.reply->serverAddress()))
176 return;
177 m_transactionStore.insert(key: tId, value: elem);
178 elem.timer->start();
179 qCDebug(QT_MODBUS) << "(TCP client) Resend request with tId:" << Qt::hex << tId;
180 } else {
181 qCDebug(QT_MODBUS) << "(TCP client) Timeout of request with tId:" <<Qt::hex << tId;
182 elem.reply->setError(error: QModbusDevice::TimeoutError,
183 errorText: QModbusClient::tr(s: "Request timeout."));
184 }
185 });
186 element.timer->start();
187 } else {
188 qCWarning(QT_MODBUS) << "(TCP client) No response timeout timer for request with tId:"
189 << Qt::hex << tId << ". Expected timeout:" << m_responseTimeoutDuration;
190 }
191 incrementTransactionId();
192
193 return reply;
194 }
195
196 // TODO: Review once we have a transport layer in place.
197 bool isOpen() const override
198 {
199 if (m_socket)
200 return m_socket->isOpen();
201 return false;
202 }
203
204 void cleanupTransactionStore()
205 {
206 if (m_transactionStore.isEmpty())
207 return;
208
209 qCDebug(QT_MODBUS) << "(TCP client) Cleanup of pending requests";
210
211 for (const auto &elem : std::as_const(t&: m_transactionStore)) {
212 if (elem.reply.isNull())
213 continue;
214 elem.reply->setError(error: QModbusDevice::ReplyAbortedError,
215 errorText: QModbusClient::tr(s: "Reply aborted due to connection closure."));
216 }
217 m_transactionStore.clear();
218 }
219
220 // This doesn't overflow, it rather "wraps around". Expected.
221 inline void incrementTransactionId() { m_transactionId++; }
222 inline int transactionId() const { return m_transactionId; }
223
224 QIODevice *device() const override { return m_socket; }
225
226 QTcpSocket *m_socket = nullptr;
227 QByteArray responseBuffer;
228 QHash<quint16, QueueElement> m_transactionStore;
229 int mbpaHeaderSize = 7;
230
231private: // Private to avoid using the wrong id inside the timer lambda,
232 quint16 m_transactionId = 0; // capturing 'this' will not copy the id.
233};
234
235QT_END_NAMESPACE
236
237#endif // QMODBUSTCPCLIENT_P_H
238

source code of qtserialbus/src/serialbus/qmodbustcpclient_p.h