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 | #include "qmodbusclient.h" |
5 | #include "qmodbusclient_p.h" |
6 | #include "qmodbus_symbols_p.h" |
7 | |
8 | #include <QtCore/qdebug.h> |
9 | #include <QtCore/qloggingcategory.h> |
10 | |
11 | QT_BEGIN_NAMESPACE |
12 | |
13 | Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS) |
14 | |
15 | /*! |
16 | \class QModbusClient |
17 | \inmodule QtSerialBus |
18 | \since 5.8 |
19 | |
20 | \brief The QModbusClient class is the interface to send Modbus requests. |
21 | |
22 | The QModbusClient API is constructed around one QModbusClient object, which holds the common |
23 | configuration and settings for the requests it sends. One QModbusClient should be enough for |
24 | the whole Qt application. |
25 | |
26 | Once a QModbusClient object has been created, the application can use it to send requests. |
27 | The returned object is used to obtain any data returned in response to the corresponding request. |
28 | |
29 | QModbusClient has an asynchronous API. When the finished slot is called, the parameter |
30 | it takes is the QModbusReply object containing the PDU as well as meta-data (Addressing, etc.). |
31 | |
32 | Note: QModbusClient queues the requests it receives. The number of requests executed in |
33 | parallel is dependent on the protocol. For example, the HTTP protocol on desktop platforms |
34 | issues 6 requests in parallel for one host/port combination. |
35 | */ |
36 | |
37 | /*! |
38 | Constructs a Modbus client device with the specified \a parent. |
39 | */ |
40 | QModbusClient::QModbusClient(QObject *parent) |
41 | : QModbusDevice(*new QModbusClientPrivate, parent) |
42 | { |
43 | } |
44 | |
45 | /*! |
46 | \internal |
47 | */ |
48 | QModbusClient::~QModbusClient() |
49 | { |
50 | } |
51 | |
52 | /*! |
53 | Sends a request to read the contents of the data pointed by \a read. |
54 | Returns a new valid \l QModbusReply object if no error occurred, otherwise |
55 | nullptr. Modbus network may have multiple servers, each server has unique |
56 | \a serverAddress. |
57 | */ |
58 | QModbusReply *QModbusClient::sendReadRequest(const QModbusDataUnit &read, int serverAddress) |
59 | { |
60 | Q_D(QModbusClient); |
61 | return d->sendRequest(request: d->createReadRequest(data: read), serverAddress, unit: &read); |
62 | } |
63 | |
64 | /*! |
65 | Sends a request to modify the contents of the data pointed by \a write. |
66 | Returns a new valid \l QModbusReply object if no error occurred, otherwise |
67 | nullptr. Modbus network may have multiple servers, each server has unique |
68 | \a serverAddress. |
69 | */ |
70 | QModbusReply *QModbusClient::sendWriteRequest(const QModbusDataUnit &write, int serverAddress) |
71 | { |
72 | Q_D(QModbusClient); |
73 | return d->sendRequest(request: d->createWriteRequest(data: write), serverAddress, unit: &write); |
74 | } |
75 | |
76 | /*! |
77 | Sends a request to read the contents of the data pointed by \a read and to |
78 | modify the contents of the data pointed by \a write using Modbus function |
79 | code \l QModbusPdu::ReadWriteMultipleRegisters. |
80 | Returns a new valid \l QModbusReply object if no error occurred, otherwise |
81 | nullptr. Modbus network may have multiple servers, each server has unique |
82 | \a serverAddress. |
83 | |
84 | \note: Sending this kind of request is only valid of both \a read and |
85 | \a write are of type QModbusDataUnit::HoldingRegisters. |
86 | */ |
87 | QModbusReply *QModbusClient::sendReadWriteRequest(const QModbusDataUnit &read, |
88 | const QModbusDataUnit &write, int serverAddress) |
89 | { |
90 | Q_D(QModbusClient); |
91 | return d->sendRequest(request: d->createRWRequest(read, write), serverAddress, unit: &read); |
92 | } |
93 | |
94 | /*! |
95 | Sends a raw Modbus \a request. A raw request can contain anything that |
96 | fits inside the Modbus PDU data section and has a valid function code. |
97 | The only check performed before sending is therefore the validity check, |
98 | see \l QModbusPdu::isValid. If no error occurred the function returns a |
99 | new valid \l QModbusReply; nullptr otherwise. Modbus networks may have |
100 | multiple servers, each server has a unique \a serverAddress. |
101 | |
102 | \sa QModbusReply::rawResult() |
103 | */ |
104 | QModbusReply *QModbusClient::sendRawRequest(const QModbusRequest &request, int serverAddress) |
105 | { |
106 | return d_func()->sendRequest(request, serverAddress, unit: nullptr); |
107 | } |
108 | |
109 | /*! |
110 | Returns the timeout value used by this QModbusClient instance in ms. |
111 | A timeout is indicated by a \l TimeoutError. The default value is 1000 ms. |
112 | |
113 | \sa setTimeout timeoutChanged() |
114 | */ |
115 | int QModbusClient::timeout() const |
116 | { |
117 | Q_D(const QModbusClient); |
118 | return d->m_responseTimeoutDuration; |
119 | } |
120 | |
121 | /*! |
122 | \fn void QModbusClient::timeoutChanged(int newTimeout) |
123 | |
124 | This signal is emitted when the timeout used by this QModbusClient instance |
125 | is changed. The new response timeout for the device is passed as \a newTimeout. |
126 | |
127 | \sa setTimeout() |
128 | */ |
129 | |
130 | /*! |
131 | Sets the \a newTimeout for this QModbusClient instance. The minimum timeout |
132 | is 10 ms. |
133 | |
134 | The timeout is used by the client to determine how long it waits for |
135 | a response from the server. If the response is not received within the |
136 | required timeout, the \l TimeoutError is set. |
137 | |
138 | Already active/running timeouts are not affected by such timeout duration |
139 | changes. |
140 | |
141 | \sa timeout timeoutChanged() |
142 | */ |
143 | void QModbusClient::setTimeout(int newTimeout) |
144 | { |
145 | if (newTimeout < 10) |
146 | return; |
147 | |
148 | Q_D(QModbusClient); |
149 | if (d->m_responseTimeoutDuration != newTimeout) { |
150 | d->m_responseTimeoutDuration = newTimeout; |
151 | emit timeoutChanged(newTimeout); |
152 | } |
153 | } |
154 | |
155 | /*! |
156 | Returns the number of retries a client will perform before a |
157 | request fails. The default value is set to \c 3. |
158 | */ |
159 | int QModbusClient::numberOfRetries() const |
160 | { |
161 | Q_D(const QModbusClient); |
162 | return d->m_numberOfRetries; |
163 | } |
164 | |
165 | /*! |
166 | Sets the \a number of retries a client will perform before a |
167 | request fails. The default value is set to \c 3. |
168 | |
169 | \note The new value must be greater than or equal to \c 0. Changing this |
170 | property will only effect new requests, not already scheduled ones. |
171 | */ |
172 | void QModbusClient::setNumberOfRetries(int number) |
173 | { |
174 | Q_D(QModbusClient); |
175 | if (number >= 0) |
176 | d->m_numberOfRetries = number; |
177 | } |
178 | |
179 | /*! |
180 | \internal |
181 | */ |
182 | QModbusClient::QModbusClient(QModbusClientPrivate &dd, QObject *parent) : |
183 | QModbusDevice(dd, parent) |
184 | { |
185 | |
186 | } |
187 | |
188 | /*! |
189 | Processes a Modbus server \a response and stores the decoded information in |
190 | \a data. Returns \c true on success; otherwise \c false. |
191 | |
192 | \note The default implementation does not support all |
193 | \l {QModbusPdu::}{FunctionCode}s. Override this method in a custom Modbus |
194 | client implementations to handle the needed functions. |
195 | */ |
196 | bool QModbusClient::processResponse(const QModbusResponse &response, QModbusDataUnit *data) |
197 | { |
198 | return d_func()->processResponse(response, data); |
199 | } |
200 | |
201 | /*! |
202 | To be implemented by custom Modbus client implementation. The default implementation ignores |
203 | \a response and \a data. It always returns false to indicate error. |
204 | */ |
205 | bool QModbusClient::processPrivateResponse(const QModbusResponse &response, QModbusDataUnit *data) |
206 | { |
207 | Q_UNUSED(response); |
208 | Q_UNUSED(data); |
209 | return false; |
210 | } |
211 | |
212 | QModbusReply *QModbusClientPrivate::sendRequest(const QModbusRequest &request, int serverAddress, |
213 | const QModbusDataUnit *const unit) |
214 | { |
215 | Q_Q(QModbusClient); |
216 | |
217 | if (!isOpen() || q->state() != QModbusDevice::ConnectedState) { |
218 | qCWarning(QT_MODBUS) << "(Client) Device is not connected" ; |
219 | q->setError(errorText: QModbusClient::tr(s: "Device not connected." ), error: QModbusDevice::ConnectionError); |
220 | return nullptr; |
221 | } |
222 | |
223 | if (!request.isValid()) { |
224 | qCWarning(QT_MODBUS) << "(Client) Refuse to send invalid request." ; |
225 | q->setError(errorText: QModbusClient::tr(s: "Invalid Modbus request." ), error: QModbusDevice::ProtocolError); |
226 | return nullptr; |
227 | } |
228 | |
229 | if (unit) |
230 | return enqueueRequest(request, serverAddress, *unit, QModbusReply::Common); |
231 | return enqueueRequest(request, serverAddress, QModbusDataUnit(), QModbusReply::Raw); |
232 | } |
233 | |
234 | QModbusRequest QModbusClientPrivate::createReadRequest(const QModbusDataUnit &data) const |
235 | { |
236 | if (!data.isValid()) |
237 | return QModbusRequest(); |
238 | |
239 | switch (data.registerType()) { |
240 | case QModbusDataUnit::Coils: |
241 | return QModbusRequest(QModbusRequest::ReadCoils, quint16(data.startAddress()), |
242 | quint16(data.valueCount())); |
243 | case QModbusDataUnit::DiscreteInputs: |
244 | return QModbusRequest(QModbusRequest::ReadDiscreteInputs, quint16(data.startAddress()), |
245 | quint16(data.valueCount())); |
246 | case QModbusDataUnit::InputRegisters: |
247 | return QModbusRequest(QModbusRequest::ReadInputRegisters, quint16(data.startAddress()), |
248 | quint16(data.valueCount())); |
249 | case QModbusDataUnit::HoldingRegisters: |
250 | return QModbusRequest(QModbusRequest::ReadHoldingRegisters, quint16(data.startAddress()), |
251 | quint16(data.valueCount())); |
252 | default: |
253 | break; |
254 | } |
255 | |
256 | return QModbusRequest(); |
257 | } |
258 | |
259 | QModbusRequest QModbusClientPrivate::createWriteRequest(const QModbusDataUnit &data) const |
260 | { |
261 | switch (data.registerType()) { |
262 | case QModbusDataUnit::Coils: { |
263 | if (data.valueCount() == 1) { |
264 | return QModbusRequest(QModbusRequest::WriteSingleCoil, quint16(data.startAddress()), |
265 | quint16((data.value(index: 0) == 0u) ? Coil::Off : Coil::On)); |
266 | } |
267 | |
268 | quint8 byteCount = quint8(data.valueCount() / 8); |
269 | if ((data.valueCount() % 8) != 0) |
270 | byteCount += 1; |
271 | |
272 | qsizetype address = 0; |
273 | QList<quint8> bytes; |
274 | for (quint8 i = 0; i < byteCount; ++i) { |
275 | quint8 byte = 0; |
276 | for (int currentBit = 0; currentBit < 8; ++currentBit) |
277 | if (data.value(index: address++)) |
278 | byte |= (1U << currentBit); |
279 | bytes.append(t: byte); |
280 | } |
281 | |
282 | return QModbusRequest(QModbusRequest::WriteMultipleCoils, quint16(data.startAddress()), |
283 | quint16(data.valueCount()), byteCount, bytes); |
284 | } break; |
285 | |
286 | case QModbusDataUnit::HoldingRegisters: { |
287 | if (data.valueCount() == 1) { |
288 | return QModbusRequest(QModbusRequest::WriteSingleRegister, quint16(data.startAddress()), |
289 | data.value(index: 0)); |
290 | } |
291 | |
292 | const quint8 byteCount = quint8(data.valueCount() * 2); |
293 | return QModbusRequest(QModbusRequest::WriteMultipleRegisters, quint16(data.startAddress()), |
294 | quint16(data.valueCount()), byteCount, data.values()); |
295 | } break; |
296 | |
297 | case QModbusDataUnit::DiscreteInputs: |
298 | case QModbusDataUnit::InputRegisters: |
299 | default: // fall through on purpose |
300 | break; |
301 | } |
302 | return QModbusRequest(); |
303 | } |
304 | |
305 | QModbusRequest QModbusClientPrivate::createRWRequest(const QModbusDataUnit &read, |
306 | const QModbusDataUnit &write) const |
307 | { |
308 | if ((read.registerType() != QModbusDataUnit::HoldingRegisters) |
309 | && (write.registerType() != QModbusDataUnit::HoldingRegisters)) { |
310 | return QModbusRequest(); |
311 | } |
312 | |
313 | const quint8 byteCount = quint8(write.valueCount() * 2); |
314 | return QModbusRequest(QModbusRequest::ReadWriteMultipleRegisters, quint16(read.startAddress()), |
315 | quint16(read.valueCount()), quint16(write.startAddress()), |
316 | quint16(write.valueCount()), byteCount, write.values()); |
317 | } |
318 | |
319 | void QModbusClientPrivate::processQueueElement(const QModbusResponse &pdu, |
320 | const QueueElement &element) |
321 | { |
322 | if (element.reply.isNull()) |
323 | return; |
324 | |
325 | element.reply->setRawResult(pdu); |
326 | if (pdu.isException()) { |
327 | element.reply->setError(error: QModbusDevice::ProtocolError, |
328 | errorText: QModbusClient::tr(s: "Modbus Exception Response." )); |
329 | return; |
330 | } |
331 | |
332 | if (element.reply->type() == QModbusReply::Broadcast) { |
333 | element.reply->setFinished(true); |
334 | return; |
335 | } |
336 | |
337 | QModbusDataUnit unit = element.unit; |
338 | if (!q_func()->processResponse(response: pdu, data: &unit)) { |
339 | element.reply->setError(error: QModbusDevice::InvalidResponseError, |
340 | errorText: QModbusClient::tr(s: "An invalid response has been received." )); |
341 | return; |
342 | } |
343 | |
344 | element.reply->setResult(unit); |
345 | element.reply->setFinished(true); |
346 | } |
347 | |
348 | bool QModbusClientPrivate::processResponse(const QModbusResponse &response, QModbusDataUnit *data) |
349 | { |
350 | switch (response.functionCode()) { |
351 | case QModbusRequest::ReadCoils: |
352 | return processReadCoilsResponse(response, data); |
353 | case QModbusRequest::ReadDiscreteInputs: |
354 | return processReadDiscreteInputsResponse(response, data); |
355 | case QModbusRequest::ReadHoldingRegisters: |
356 | return processReadHoldingRegistersResponse(response, data); |
357 | case QModbusRequest::ReadInputRegisters: |
358 | return processReadInputRegistersResponse(response, data); |
359 | case QModbusRequest::WriteSingleCoil: |
360 | return processWriteSingleCoilResponse(response, data); |
361 | case QModbusRequest::WriteSingleRegister: |
362 | return processWriteSingleRegisterResponse(response, data); |
363 | case QModbusRequest::ReadExceptionStatus: |
364 | case QModbusRequest::Diagnostics: |
365 | case QModbusRequest::GetCommEventCounter: |
366 | case QModbusRequest::GetCommEventLog: |
367 | return false; // Return early, it's not a private response. |
368 | case QModbusRequest::WriteMultipleCoils: |
369 | return processWriteMultipleCoilsResponse(response, data); |
370 | case QModbusRequest::WriteMultipleRegisters: |
371 | return processWriteMultipleRegistersResponse(response, data); |
372 | case QModbusRequest::ReportServerId: |
373 | case QModbusRequest::ReadFileRecord: |
374 | case QModbusRequest::WriteFileRecord: |
375 | case QModbusRequest::MaskWriteRegister: |
376 | return false; // Return early, it's not a private response. |
377 | case QModbusRequest::ReadWriteMultipleRegisters: |
378 | return processReadWriteMultipleRegistersResponse(response, data); |
379 | case QModbusRequest::ReadFifoQueue: |
380 | case QModbusRequest::EncapsulatedInterfaceTransport: |
381 | return false; // Return early, it's not a private response. |
382 | default: |
383 | break; |
384 | } |
385 | return q_func()->processPrivateResponse(response, data); |
386 | } |
387 | |
388 | static bool isValid(const QModbusResponse &response, QModbusResponse::FunctionCode fc) |
389 | { |
390 | if (!response.isValid()) |
391 | return false; |
392 | if (response.isException()) |
393 | return false; |
394 | if (response.functionCode() != fc) |
395 | return false; |
396 | return true; |
397 | } |
398 | |
399 | bool QModbusClientPrivate::processReadCoilsResponse(const QModbusResponse &response, |
400 | QModbusDataUnit *data) |
401 | { |
402 | if (!isValid(response, fc: QModbusResponse::ReadCoils)) |
403 | return false; |
404 | return collateBits(pdu: response, type: QModbusDataUnit::Coils, data); |
405 | } |
406 | |
407 | bool QModbusClientPrivate::processReadDiscreteInputsResponse(const QModbusResponse &response, |
408 | QModbusDataUnit *data) |
409 | { |
410 | if (!isValid(response, fc: QModbusResponse::ReadDiscreteInputs)) |
411 | return false; |
412 | return collateBits(pdu: response, type: QModbusDataUnit::DiscreteInputs, data); |
413 | } |
414 | |
415 | bool QModbusClientPrivate::collateBits(const QModbusPdu &response, |
416 | QModbusDataUnit::RegisterType type, QModbusDataUnit *data) |
417 | { |
418 | if (response.dataSize() < QModbusResponse::minimumDataSize(pdu: response)) |
419 | return false; |
420 | |
421 | // byte count needs to match available bytes |
422 | const quint8 byteCount = quint8(response.data().at(i: 0)); |
423 | if ((response.dataSize() - 1) != byteCount) |
424 | return false; |
425 | |
426 | if (data) { |
427 | const int valueCount = byteCount *8; |
428 | const QByteArray payload = response.data(); |
429 | |
430 | qsizetype value = 0; |
431 | QList<quint16> values(valueCount); |
432 | for (qsizetype i = 1; i < payload.size(); ++i) { |
433 | const quint8 byte = quint8(payload[i]); |
434 | for (qint32 currentBit = 0; currentBit < 8 && value < valueCount; ++currentBit) |
435 | values[value++] = (byte & (1U << currentBit) ? 1 : 0); |
436 | } |
437 | data->setValues(values); |
438 | data->setRegisterType(type); |
439 | } |
440 | return true; |
441 | } |
442 | |
443 | bool QModbusClientPrivate::processReadHoldingRegistersResponse(const QModbusResponse &response, |
444 | QModbusDataUnit *data) |
445 | { |
446 | if (!isValid(response, fc: QModbusResponse::ReadHoldingRegisters)) |
447 | return false; |
448 | return collateBytes(pdu: response, type: QModbusDataUnit::HoldingRegisters, data); |
449 | } |
450 | |
451 | bool QModbusClientPrivate::processReadInputRegistersResponse(const QModbusResponse &response, |
452 | QModbusDataUnit *data) |
453 | { |
454 | if (!isValid(response, fc: QModbusResponse::ReadInputRegisters)) |
455 | return false; |
456 | return collateBytes(pdu: response, type: QModbusDataUnit::InputRegisters, data); |
457 | } |
458 | |
459 | bool QModbusClientPrivate::collateBytes(const QModbusPdu &response, |
460 | QModbusDataUnit::RegisterType type, QModbusDataUnit *data) |
461 | { |
462 | if (response.dataSize() < QModbusResponse::minimumDataSize(pdu: response)) |
463 | return false; |
464 | |
465 | // byte count needs to match available bytes |
466 | const quint8 byteCount = quint8(response.data().at(i: 0)); |
467 | if ((response.dataSize() - 1) != byteCount) |
468 | return false; |
469 | |
470 | // byte count needs to be even to match full registers |
471 | if (byteCount % 2 != 0) |
472 | return false; |
473 | |
474 | if (data) { |
475 | QDataStream stream(response.data().remove(index: 0, len: 1)); |
476 | |
477 | QList<quint16> values; |
478 | const quint8 itemCount = byteCount / 2; |
479 | for (int i = 0; i < itemCount; i++) { |
480 | quint16 tmp; |
481 | stream >> tmp; |
482 | values.append(t: tmp); |
483 | } |
484 | data->setValues(values); |
485 | data->setRegisterType(type); |
486 | } |
487 | return true; |
488 | } |
489 | |
490 | bool QModbusClientPrivate::processWriteSingleCoilResponse(const QModbusResponse &response, |
491 | QModbusDataUnit *data) |
492 | { |
493 | if (!isValid(response, fc: QModbusResponse::WriteSingleCoil)) |
494 | return false; |
495 | return collateSingleValue(pdu: response, type: QModbusDataUnit::Coils, data); |
496 | } |
497 | |
498 | bool QModbusClientPrivate::processWriteSingleRegisterResponse(const QModbusResponse &response, |
499 | QModbusDataUnit *data) |
500 | { |
501 | if (!isValid(response, fc: QModbusResponse::WriteSingleRegister)) |
502 | return false; |
503 | return collateSingleValue(pdu: response, type: QModbusDataUnit::HoldingRegisters, data); |
504 | } |
505 | |
506 | bool QModbusClientPrivate::collateSingleValue(const QModbusPdu &response, |
507 | QModbusDataUnit::RegisterType type, QModbusDataUnit *data) |
508 | { |
509 | if (response.dataSize() != QModbusResponse::minimumDataSize(pdu: response)) |
510 | return false; |
511 | |
512 | quint16 address, value; |
513 | response.decodeData(newData: &address, newData: &value); |
514 | if ((type == QModbusDataUnit::Coils) && (value != Coil::Off) && (value != Coil::On)) |
515 | return false; |
516 | |
517 | if (data) { |
518 | data->setRegisterType(type); |
519 | data->setStartAddress(address); |
520 | data->setValues(QList<quint16> { value }); |
521 | } |
522 | return true; |
523 | } |
524 | |
525 | bool QModbusClientPrivate::processWriteMultipleCoilsResponse(const QModbusResponse &response, |
526 | QModbusDataUnit *data) |
527 | { |
528 | if (!isValid(response, fc: QModbusResponse::WriteMultipleCoils)) |
529 | return false; |
530 | return collateMultipleValues(pdu: response, type: QModbusDataUnit::Coils, data); |
531 | } |
532 | |
533 | bool QModbusClientPrivate::processWriteMultipleRegistersResponse(const QModbusResponse &response, |
534 | QModbusDataUnit *data) |
535 | { |
536 | if (!isValid(response, fc: QModbusResponse::WriteMultipleRegisters)) |
537 | return false; |
538 | return collateMultipleValues(pdu: response, type: QModbusDataUnit::HoldingRegisters, data); |
539 | } |
540 | |
541 | bool QModbusClientPrivate::collateMultipleValues(const QModbusPdu &response, |
542 | QModbusDataUnit::RegisterType type, QModbusDataUnit *data) |
543 | { |
544 | if (response.dataSize() != QModbusResponse::minimumDataSize(pdu: response)) |
545 | return false; |
546 | |
547 | quint16 address, count; |
548 | response.decodeData(newData: &address, newData: &count); |
549 | |
550 | // number of registers to write is 1-123 per request |
551 | if ((type == QModbusDataUnit::HoldingRegisters) && (count < 1 || count > 123)) |
552 | return false; |
553 | |
554 | if (data) { |
555 | data->setValueCount(count); |
556 | data->setRegisterType(type); |
557 | data->setStartAddress(address); |
558 | } |
559 | return true; |
560 | } |
561 | |
562 | bool QModbusClientPrivate::processReadWriteMultipleRegistersResponse(const QModbusResponse &resp, |
563 | QModbusDataUnit *data) |
564 | { |
565 | if (!isValid(response: resp, fc: QModbusResponse::ReadWriteMultipleRegisters)) |
566 | return false; |
567 | return collateBytes(response: resp, type: QModbusDataUnit::HoldingRegisters, data); |
568 | } |
569 | |
570 | QT_END_NAMESPACE |
571 | |