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

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