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