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