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 QMODBUSRTUSERIALCLIENT_P_H |
5 | #define QMODBUSRTUSERIALCLIENT_P_H |
6 | |
7 | #include <QtCore/qloggingcategory.h> |
8 | #include <QtCore/qmath.h> |
9 | #include <QtCore/qpointer.h> |
10 | #include <QtCore/qqueue.h> |
11 | #include <QtCore/qtimer.h> |
12 | #include <QtSerialBus/qmodbusrtuserialclient.h> |
13 | #include <QtSerialPort/qserialport.h> |
14 | |
15 | #include <private/qmodbusadu_p.h> |
16 | #include <private/qmodbusclient_p.h> |
17 | #include <private/qmodbus_symbols_p.h> |
18 | |
19 | // |
20 | // W A R N I N G |
21 | // ------------- |
22 | // |
23 | // This file is not part of the Qt API. It exists purely as an |
24 | // implementation detail. This header file may change from version to |
25 | // version without notice, or even be removed. |
26 | // |
27 | // We mean it. |
28 | // |
29 | |
30 | QT_BEGIN_NAMESPACE |
31 | |
32 | Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS) |
33 | Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS_LOW) |
34 | |
35 | class Timer : public QObject |
36 | { |
37 | Q_OBJECT |
38 | |
39 | public: |
40 | Timer() = default; |
41 | int start(int msec) |
42 | { |
43 | m_timer = QBasicTimer(); |
44 | m_timer.start(msec, t: Qt::PreciseTimer, obj: this); |
45 | return m_timer.timerId(); |
46 | } |
47 | void stop() { m_timer.stop(); } |
48 | bool isActive() const { return m_timer.isActive(); } |
49 | |
50 | signals: |
51 | void timeout(int timerId); |
52 | |
53 | private: |
54 | void timerEvent(QTimerEvent *event) override |
55 | { |
56 | const auto id = m_timer.timerId(); |
57 | if (event->timerId() == id) |
58 | emit timeout(timerId: id); |
59 | } |
60 | |
61 | private: |
62 | QBasicTimer m_timer; |
63 | }; |
64 | |
65 | class QModbusRtuSerialClientPrivate : public QModbusClientPrivate |
66 | { |
67 | Q_DECLARE_PUBLIC(QModbusRtuSerialClient) |
68 | enum State |
69 | { |
70 | Idle, |
71 | WaitingForReplay, |
72 | ProcessReply |
73 | } m_state = Idle; |
74 | |
75 | public: |
76 | void onReadyRead() |
77 | { |
78 | m_responseBuffer += m_serialPort->read(maxlen: m_serialPort->bytesAvailable()); |
79 | qCDebug(QT_MODBUS_LOW) << "(RTU client) Response buffer:" << m_responseBuffer.toHex(); |
80 | |
81 | if (m_responseBuffer.size() < 2) { |
82 | qCDebug(QT_MODBUS) << "(RTU client) Modbus ADU not complete" ; |
83 | return; |
84 | } |
85 | |
86 | const QModbusSerialAdu tmpAdu(QModbusSerialAdu::Rtu, m_responseBuffer); |
87 | int pduSizeWithoutFcode = QModbusResponse::calculateDataSize(pdu: tmpAdu.pdu()); |
88 | if (pduSizeWithoutFcode < 0) { |
89 | // wait for more data |
90 | qCDebug(QT_MODBUS) << "(RTU client) Cannot calculate PDU size for function code:" |
91 | << tmpAdu.pdu().functionCode() << ", delaying pending frame" ; |
92 | return; |
93 | } |
94 | |
95 | // server address byte + function code byte + PDU size + 2 bytes CRC |
96 | int aduSize = 2 + pduSizeWithoutFcode + 2; |
97 | if (tmpAdu.rawSize() < aduSize) { |
98 | qCDebug(QT_MODBUS) << "(RTU client) Incomplete ADU received, ignoring" ; |
99 | return; |
100 | } |
101 | |
102 | if (m_queue.isEmpty()) |
103 | return; |
104 | auto ¤t = m_queue.first(); |
105 | |
106 | // Special case for Diagnostics:ReturnQueryData. The response has no |
107 | // length indicator and is just a simple echo of what we have send. |
108 | if (tmpAdu.pdu().functionCode() == QModbusPdu::Diagnostics) { |
109 | const QModbusResponse response = tmpAdu.pdu(); |
110 | if (canMatchRequestAndResponse(response, sendingServer: tmpAdu.serverAddress())) { |
111 | quint16 subCode = 0xffff; |
112 | response.decodeData(newData: &subCode); |
113 | if (subCode == Diagnostics::ReturnQueryData) { |
114 | if (response.data() != current.requestPdu.data()) |
115 | return; // echo does not match request yet |
116 | aduSize = 2 + response.dataSize() + 2; |
117 | if (tmpAdu.rawSize() < aduSize) |
118 | return; // echo matches, probably checksum missing |
119 | } |
120 | } |
121 | } |
122 | |
123 | const QModbusSerialAdu adu(QModbusSerialAdu::Rtu, m_responseBuffer.left(len: aduSize)); |
124 | m_responseBuffer.remove(index: 0, len: aduSize); |
125 | |
126 | qCDebug(QT_MODBUS) << "(RTU client) Received ADU:" << adu.rawData().toHex(); |
127 | if (QT_MODBUS().isDebugEnabled() && !m_responseBuffer.isEmpty()) |
128 | qCDebug(QT_MODBUS_LOW) << "(RTU client) Pending buffer:" << m_responseBuffer.toHex(); |
129 | |
130 | // check CRC |
131 | if (!adu.matchingChecksum()) { |
132 | qCWarning(QT_MODBUS) << "(RTU client) Discarding response with wrong CRC, received:" |
133 | << adu.checksum<quint16>() << ", calculated CRC:" |
134 | << QModbusSerialAdu::calculateCRC(data: adu.data(), len: adu.size()); |
135 | m_queue.first().reply->addIntermediateError(error: QModbusClient::ResponseCrcError); |
136 | return; |
137 | } |
138 | |
139 | const QModbusResponse response = adu.pdu(); |
140 | if (!canMatchRequestAndResponse(response, sendingServer: adu.serverAddress())) { |
141 | qCWarning(QT_MODBUS) << "(RTU client) Cannot match response with open request, " |
142 | "ignoring" ; |
143 | m_queue.first().reply->addIntermediateError(error: QModbusClient::ResponseRequestMismatch); |
144 | return; |
145 | } |
146 | |
147 | m_state = ProcessReply; |
148 | m_responseTimer.stop(); |
149 | current.m_timerId = INT_MIN; |
150 | |
151 | processQueueElement(pdu: response, element: m_queue.dequeue()); |
152 | |
153 | m_state = Idle; |
154 | scheduleNextRequest(delay: m_interFrameDelayMilliseconds); |
155 | } |
156 | |
157 | void onAboutToClose() |
158 | { |
159 | Q_Q(QModbusRtuSerialClient); |
160 | Q_UNUSED(q); // avoid warning in release mode |
161 | Q_ASSERT(q->state() == QModbusDevice::ClosingState); |
162 | |
163 | m_responseTimer.stop(); |
164 | } |
165 | |
166 | void onResponseTimeout(int timerId) |
167 | { |
168 | m_responseTimer.stop(); |
169 | if (m_state != State::WaitingForReplay || m_queue.isEmpty()) |
170 | return; |
171 | const auto ¤t = m_queue.first(); |
172 | |
173 | if (current.m_timerId != timerId) |
174 | return; |
175 | |
176 | qCDebug(QT_MODBUS) << "(RTU client) Receive timeout:" << current.requestPdu; |
177 | |
178 | if (current.numberOfRetries <= 0) { |
179 | auto item = m_queue.dequeue(); |
180 | if (item.reply) { |
181 | item.reply->setError(error: QModbusDevice::TimeoutError, |
182 | errorText: QModbusClient::tr(s: "Request timeout." )); |
183 | } |
184 | } |
185 | |
186 | m_state = Idle; |
187 | scheduleNextRequest(delay: m_interFrameDelayMilliseconds); |
188 | } |
189 | |
190 | void onBytesWritten(qint64 bytes) |
191 | { |
192 | if (m_queue.isEmpty()) |
193 | return; |
194 | auto ¤t = m_queue.first(); |
195 | |
196 | current.bytesWritten += bytes; |
197 | if (current.bytesWritten != current.adu.size()) |
198 | return; |
199 | |
200 | qCDebug(QT_MODBUS) << "(RTU client) Send successful:" << current.requestPdu; |
201 | |
202 | if (!current.reply.isNull() && current.reply->type() == QModbusReply::Broadcast) { |
203 | m_state = ProcessReply; |
204 | processQueueElement(pdu: {}, element: m_queue.dequeue()); |
205 | m_state = Idle; |
206 | scheduleNextRequest(delay: m_turnaroundDelay); |
207 | } else { |
208 | current.m_timerId = m_responseTimer.start(msec: m_responseTimeoutDuration); |
209 | } |
210 | } |
211 | |
212 | void onError(QSerialPort::SerialPortError error) |
213 | { |
214 | if (error == QSerialPort::NoError) |
215 | return; |
216 | |
217 | qCDebug(QT_MODBUS) << "(RTU server) QSerialPort error:" << error |
218 | << (m_serialPort ? m_serialPort->errorString() : QString()); |
219 | |
220 | Q_Q(QModbusRtuSerialClient); |
221 | |
222 | switch (error) { |
223 | case QSerialPort::DeviceNotFoundError: |
224 | q->setError(errorText: QModbusDevice::tr(s: "Referenced serial device does not exist." ), |
225 | error: QModbusDevice::ConnectionError); |
226 | break; |
227 | case QSerialPort::PermissionError: |
228 | q->setError(errorText: QModbusDevice::tr(s: "Cannot open serial device due to permissions." ), |
229 | error: QModbusDevice::ConnectionError); |
230 | break; |
231 | case QSerialPort::OpenError: |
232 | case QSerialPort::NotOpenError: |
233 | q->setError(errorText: QModbusDevice::tr(s: "Cannot open serial device." ), |
234 | error: QModbusDevice::ConnectionError); |
235 | break; |
236 | case QSerialPort::WriteError: |
237 | q->setError(errorText: QModbusDevice::tr(s: "Write error." ), error: QModbusDevice::WriteError); |
238 | break; |
239 | case QSerialPort::ReadError: |
240 | q->setError(errorText: QModbusDevice::tr(s: "Read error." ), error: QModbusDevice::ReadError); |
241 | break; |
242 | case QSerialPort::ResourceError: |
243 | q->setError(errorText: QModbusDevice::tr(s: "Resource error." ), error: QModbusDevice::ConnectionError); |
244 | break; |
245 | case QSerialPort::UnsupportedOperationError: |
246 | q->setError(errorText: QModbusDevice::tr(s: "Device operation is not supported error." ), |
247 | error: QModbusDevice::ConfigurationError); |
248 | break; |
249 | case QSerialPort::TimeoutError: |
250 | q->setError(errorText: QModbusDevice::tr(s: "Timeout error." ), error: QModbusDevice::TimeoutError); |
251 | break; |
252 | case QSerialPort::UnknownError: |
253 | q->setError(errorText: QModbusDevice::tr(s: "Unknown error." ), error: QModbusDevice::UnknownError); |
254 | break; |
255 | default: |
256 | qCDebug(QT_MODBUS) << "(RTU server) Unhandled QSerialPort error" << error; |
257 | break; |
258 | } |
259 | } |
260 | |
261 | void setupSerialPort() |
262 | { |
263 | Q_Q(QModbusRtuSerialClient); |
264 | m_serialPort = new QSerialPort(q); |
265 | |
266 | QObject::connect(sender: &m_responseTimer, signal: &Timer::timeout, context: q, slot: [this](int timerId) { |
267 | onResponseTimeout(timerId); |
268 | }); |
269 | |
270 | QObject::connect(sender: m_serialPort, signal: &QSerialPort::readyRead, context: q, slot: [this]() { |
271 | onReadyRead(); |
272 | }); |
273 | |
274 | QObject::connect(sender: m_serialPort, signal: &QSerialPort::aboutToClose, context: q, slot: [this]() { |
275 | onAboutToClose(); |
276 | }); |
277 | |
278 | QObject::connect(sender: m_serialPort, signal: &QSerialPort::bytesWritten, context: q, slot: [this](qint64 bytes) { |
279 | onBytesWritten(bytes); |
280 | }); |
281 | |
282 | QObject::connect(sender: m_serialPort, signal: &QSerialPort::errorOccurred, |
283 | context: q, slot: [this](QSerialPort::SerialPortError error) { |
284 | onError(error); |
285 | }); |
286 | } |
287 | |
288 | void setupEnvironment() |
289 | { |
290 | if (m_serialPort) { |
291 | m_serialPort->setPortName(m_comPort); |
292 | m_serialPort->setParity(m_parity); |
293 | m_serialPort->setBaudRate(baudRate: m_baudRate); |
294 | m_serialPort->setDataBits(m_dataBits); |
295 | m_serialPort->setStopBits(m_stopBits); |
296 | } |
297 | |
298 | calculateInterFrameDelay(); |
299 | |
300 | m_responseBuffer.clear(); |
301 | m_state = QModbusRtuSerialClientPrivate::Idle; |
302 | } |
303 | |
304 | QModbusReply *enqueueRequest(const QModbusRequest &request, int serverAddress, |
305 | const QModbusDataUnit &unit, QModbusReply::ReplyType type) override |
306 | { |
307 | Q_Q(QModbusRtuSerialClient); |
308 | |
309 | auto reply = new QModbusReply(serverAddress == 0 ? QModbusReply::Broadcast : type, |
310 | serverAddress, q); |
311 | QueueElement element(reply, request, unit, m_numberOfRetries + 1); |
312 | element.adu = QModbusSerialAdu::create(type: QModbusSerialAdu::Rtu, serverAddress, pdu: request); |
313 | m_queue.enqueue(t: element); |
314 | |
315 | scheduleNextRequest(delay: m_interFrameDelayMilliseconds); |
316 | |
317 | return reply; |
318 | } |
319 | |
320 | void scheduleNextRequest(int delay) |
321 | { |
322 | Q_Q(QModbusRtuSerialClient); |
323 | |
324 | if (m_state == Idle && !m_queue.isEmpty()) { |
325 | m_state = WaitingForReplay; |
326 | QTimer::singleShot(interval: delay, receiver: q, slot: [this]() { processQueue(); }); |
327 | } |
328 | } |
329 | |
330 | void processQueue() |
331 | { |
332 | m_responseBuffer.clear(); |
333 | m_serialPort->clear(directions: QSerialPort::AllDirections); |
334 | |
335 | if (m_queue.isEmpty()) |
336 | return; |
337 | auto ¤t = m_queue.first(); |
338 | |
339 | if (current.reply.isNull()) { |
340 | m_queue.dequeue(); |
341 | m_state = Idle; |
342 | scheduleNextRequest(delay: m_interFrameDelayMilliseconds); |
343 | } else { |
344 | current.bytesWritten = 0; |
345 | current.numberOfRetries--; |
346 | m_serialPort->write(data: current.adu); |
347 | |
348 | qCDebug(QT_MODBUS) << "(RTU client) Sent Serial PDU:" << current.requestPdu; |
349 | qCDebug(QT_MODBUS_LOW).noquote() << "(RTU client) Sent Serial ADU: 0x" + current.adu |
350 | .toHex(); |
351 | } |
352 | } |
353 | |
354 | bool canMatchRequestAndResponse(const QModbusResponse &response, int sendingServer) const |
355 | { |
356 | if (m_queue.isEmpty()) |
357 | return false; |
358 | const auto ¤t = m_queue.first(); |
359 | |
360 | if (current.reply.isNull()) |
361 | return false; // reply deleted |
362 | if (current.reply->serverAddress() != sendingServer) |
363 | return false; // server mismatch |
364 | if (current.requestPdu.functionCode() != response.functionCode()) |
365 | return false; // request for different function code |
366 | return true; |
367 | } |
368 | |
369 | bool isOpen() const override |
370 | { |
371 | if (m_serialPort) |
372 | return m_serialPort->isOpen(); |
373 | return false; |
374 | } |
375 | |
376 | QIODevice *device() const override { return m_serialPort; } |
377 | |
378 | Timer m_responseTimer; |
379 | QByteArray m_responseBuffer; |
380 | |
381 | QQueue<QueueElement> m_queue; |
382 | QSerialPort *m_serialPort = nullptr; |
383 | |
384 | int m_turnaroundDelay = 100; // Recommended value is between 100 and 200 msec. |
385 | }; |
386 | |
387 | QT_END_NAMESPACE |
388 | |
389 | #endif // QMODBUSRTUSERIALCLIENT_P_H |
390 | |