/**************************************************************************** ** ** Copyright (C) 2017 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the QtSerialBus module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL3$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see http://www.qt.io/terms-conditions. For further ** information use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPLv3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or later as published by the Free ** Software Foundation and appearing in the file LICENSE.GPL included in ** the packaging of this file. Please review the following information to ** ensure the GNU General Public License version 2.0 requirements will be ** met: http://www.gnu.org/licenses/gpl-2.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qmodbusclient.h" #include "qmodbusclient_p.h" #include "qmodbus_symbols_p.h" #include #include QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS) /*! \class QModbusClient \inmodule QtSerialBus \since 5.8 \brief The QModbusClient class is the interface to send Modbus requests. The QModbusClient API is constructed around one QModbusClient object, which holds the common configuration and settings for the requests it sends. One QModbusClient should be enough for the whole Qt application. Once a QModbusClient object has been created, the application can use it to send requests. The returned object is used to obtain any data returned in response to the corresponding request. QModbusClient has an asynchronous API. When the finished slot is called, the parameter it takes is the QModbusReply object containing the PDU as well as meta-data (Addressing, etc.). Note: QModbusClient queues the requests it receives. The number of requests executed in parallel is dependent on the protocol. For example, the HTTP protocol on desktop platforms issues 6 requests in parallel for one host/port combination. */ /*! Constructs a Modbus client device with the specified \a parent. */ QModbusClient::QModbusClient(QObject *parent) : QModbusDevice(*new QModbusClientPrivate, parent) { } /*! \internal */ QModbusClient::~QModbusClient() { } /*! Sends a request to read the contents of the data pointed by \a read. Returns a new valid \l QModbusReply object if no error occurred, otherwise nullptr. Modbus network may have multiple servers, each server has unique \a serverAddress. */ QModbusReply *QModbusClient::sendReadRequest(const QModbusDataUnit &read, int serverAddress) { Q_D(QModbusClient); return d->sendRequest(d->createReadRequest(read), serverAddress, &read); } /*! Sends a request to modify the contents of the data pointed by \a write. Returns a new valid \l QModbusReply object if no error occurred, otherwise nullptr. Modbus network may have multiple servers, each server has unique \a serverAddress. */ QModbusReply *QModbusClient::sendWriteRequest(const QModbusDataUnit &write, int serverAddress) { Q_D(QModbusClient); return d->sendRequest(d->createWriteRequest(write), serverAddress, &write); } /*! Sends a request to read the contents of the data pointed by \a read and to modify the contents of the data pointed by \a write using Modbus function code \l QModbusPdu::ReadWriteMultipleRegisters. Returns a new valid \l QModbusReply object if no error occurred, otherwise nullptr. Modbus network may have multiple servers, each server has unique \a serverAddress. \note: Sending this kind of request is only valid of both \a read and \a write are of type QModbusDataUnit::HoldingRegisters. */ QModbusReply *QModbusClient::sendReadWriteRequest(const QModbusDataUnit &read, const QModbusDataUnit &write, int serverAddress) { Q_D(QModbusClient); return d->sendRequest(d->createRWRequest(read, write), serverAddress, &read); } /*! Sends a raw Modbus \a request. A raw request can contain anything that fits inside the Modbus PDU data section and has a valid function code. The only check performed before sending is therefore the validity check, see \l QModbusPdu::isValid. If no error occurred the function returns a a new valid \l QModbusReply; nullptr otherwise. Modbus networks may have multiple servers, each server has a unique \a serverAddress. \sa QModbusReply::rawResult() */ QModbusReply *QModbusClient::sendRawRequest(const QModbusRequest &request, int serverAddress) { return d_func()->sendRequest(request, serverAddress, nullptr); } /*! \property QModbusClient::timeout \brief the timeout value used by this client Returns the timeout value used by this QModbusClient instance in ms. A timeout is indicated by a \l TimeoutError. The default value is 1000 ms. \sa setTimeout */ int QModbusClient::timeout() const { Q_D(const QModbusClient); return d->m_responseTimeoutDuration; } /*! \fn void QModbusClient::timeoutChanged(int newTimeout) This signal is emitted when the timeout used by this QModbusClient instance is changed. The new response timeout for the device is passed as \a newTimeout. \sa setTimeout() */ /*! Sets the \a newTimeout for this QModbusClient instance. The minimum timeout is 50 ms. The timeout is used by the client to determine how long it waits for a response from the server. If the response is not received within the required timeout, the \l TimeoutError is set. Already active/running timeouts are not affected by such timeout duration changes. \sa timeout timeoutChanged() */ void QModbusClient::setTimeout(int newTimeout) { if (newTimeout < 50) return; Q_D(QModbusClient); if (d->m_responseTimeoutDuration != newTimeout) { d->m_responseTimeoutDuration = newTimeout; emit timeoutChanged(newTimeout); } } /*! Returns the number of retries a client will perform before a request fails. The default value is set to \c 3. */ int QModbusClient::numberOfRetries() const { Q_D(const QModbusClient); return d->m_numberOfRetries; } /*! Sets the \a number of retries a client will perform before a request fails. The default value is set to \c 3. \note The new value must be greater than or equal to \c 0. Changing this property will only effect new requests, not already scheduled ones. */ void QModbusClient::setNumberOfRetries(int number) { Q_D(QModbusClient); if (number >= 0) d->m_numberOfRetries = number; } /*! \internal */ QModbusClient::QModbusClient(QModbusClientPrivate &dd, QObject *parent) : QModbusDevice(dd, parent) { } /*! Processes a Modbus server \a response and stores the decoded information in \a data. Returns true on success; otherwise false. */ bool QModbusClient::processResponse(const QModbusResponse &response, QModbusDataUnit *data) { return d_func()->processResponse(response, data); } /*! To be implemented by custom Modbus client implementation. The default implementation ignores \a response and \a data. It always returns false to indicate error. */ bool QModbusClient::processPrivateResponse(const QModbusResponse &response, QModbusDataUnit *data) { Q_UNUSED(response) Q_UNUSED(data) return false; } QModbusReply *QModbusClientPrivate::sendRequest(const QModbusRequest &request, int serverAddress, const QModbusDataUnit *const unit) { Q_Q(QModbusClient); if (!isOpen() || q->state() != QModbusDevice::ConnectedState) { qCWarning(QT_MODBUS) << "(Client) Device is not connected"; q->setError(QModbusClient::tr("Device not connected."), QModbusDevice::ConnectionError); return nullptr; } if (!request.isValid()) { qCWarning(QT_MODBUS) << "(Client) Refuse to send invalid request."; q->setError(QModbusClient::tr("Invalid Modbus request."), QModbusDevice::ProtocolError); return nullptr; } if (unit) return enqueueRequest(request, serverAddress, *unit, QModbusReply::Common); return enqueueRequest(request, serverAddress, QModbusDataUnit(), QModbusReply::Raw); } QModbusRequest QModbusClientPrivate::createReadRequest(const QModbusDataUnit &data) const { if (!data.isValid()) return QModbusRequest(); switch (data.registerType()) { case QModbusDataUnit::Coils: return QModbusRequest(QModbusRequest::ReadCoils, quint16(data.startAddress()), quint16(data.valueCount())); case QModbusDataUnit::DiscreteInputs: return QModbusRequest(QModbusRequest::ReadDiscreteInputs, quint16(data.startAddress()), quint16(data.valueCount())); case QModbusDataUnit::InputRegisters: return QModbusRequest(QModbusRequest::ReadInputRegisters, quint16(data.startAddress()), quint16(data.valueCount())); case QModbusDataUnit::HoldingRegisters: return QModbusRequest(QModbusRequest::ReadHoldingRegisters, quint16(data.startAddress()), quint16(data.valueCount())); default: break; } return QModbusRequest(); } QModbusRequest QModbusClientPrivate::createWriteRequest(const QModbusDataUnit &data) const { switch (data.registerType()) { case QModbusDataUnit::Coils: { if (data.valueCount() == 1) { return QModbusRequest(QModbusRequest::WriteSingleCoil, quint16(data.startAddress()), quint16((data.value(0) == 0u) ? Coil::Off : Coil::On)); } quint8 byteCount = data.valueCount() / 8; if ((data.valueCount() % 8) != 0) byteCount += 1; quint8 address = 0; QVector bytes; for (quint8 i = 0; i < byteCount; ++i) { quint8 byte = 0; for (int currentBit = 0; currentBit < 8; ++currentBit) if (data.value(address++)) byte |= (1U << currentBit); bytes.append(byte); } return QModbusRequest(QModbusRequest::WriteMultipleCoils, quint16(data.startAddress()), quint16(data.valueCount()), byteCount, bytes); } break; case QModbusDataUnit::HoldingRegisters: { if (data.valueCount() == 1) { return QModbusRequest(QModbusRequest::WriteSingleRegister, quint16(data.startAddress()), data.value(0)); } const quint8 byteCount = data.valueCount() * 2; return QModbusRequest(QModbusRequest::WriteMultipleRegisters, quint16(data.startAddress()), quint16(data.valueCount()), byteCount, data.values()); } break; case QModbusDataUnit::DiscreteInputs: case QModbusDataUnit::InputRegisters: default: // fall through on purpose break; } return QModbusRequest(); } QModbusRequest QModbusClientPrivate::createRWRequest(const QModbusDataUnit &read, const QModbusDataUnit &write) const { if ((read.registerType() != QModbusDataUnit::HoldingRegisters) && (write.registerType() != QModbusDataUnit::HoldingRegisters)) { return QModbusRequest(); } const quint8 byteCount = write.valueCount() * 2; return QModbusRequest(QModbusRequest::ReadWriteMultipleRegisters, quint16(read.startAddress()), quint16(read.valueCount()), quint16(write.startAddress()), quint16(write.valueCount()), byteCount, write.values()); } void QModbusClientPrivate::processQueueElement(const QModbusResponse &pdu, const QueueElement &element) { element.reply->setRawResult(pdu); if (pdu.isException()) { element.reply->setError(QModbusDevice::ProtocolError, QModbusClient::tr("Modbus Exception Response.")); return; } if (element.reply->type() == QModbusReply::Raw) { element.reply->setFinished(true); return; } QModbusDataUnit unit = element.unit; if (!processResponse(pdu, &unit)) { element.reply->setError(QModbusDevice::UnknownError, QModbusClient::tr("An invalid response has been received.")); return; } element.reply->setResult(unit); element.reply->setFinished(true); } bool QModbusClientPrivate::processResponse(const QModbusResponse &response, QModbusDataUnit *data) { switch (response.functionCode()) { case QModbusRequest::ReadCoils: return processReadCoilsResponse(response, data); case QModbusRequest::ReadDiscreteInputs: return processReadDiscreteInputsResponse(response, data); case QModbusRequest::ReadHoldingRegisters: return processReadHoldingRegistersResponse(response, data); case QModbusRequest::ReadInputRegisters: return processReadInputRegistersResponse(response, data); case QModbusRequest::WriteSingleCoil: return processWriteSingleCoilResponse(response, data); case QModbusRequest::WriteSingleRegister: return processWriteSingleRegisterResponse(response, data); case QModbusRequest::ReadExceptionStatus: case QModbusRequest::Diagnostics: case QModbusRequest::GetCommEventCounter: case QModbusRequest::GetCommEventLog: return false; // Return early, it's not a private response. case QModbusRequest::WriteMultipleCoils: return processWriteMultipleCoilsResponse(response, data); case QModbusRequest::WriteMultipleRegisters: return processWriteMultipleRegistersResponse(response, data); case QModbusRequest::ReportServerId: case QModbusRequest::ReadFileRecord: case QModbusRequest::WriteFileRecord: case QModbusRequest::MaskWriteRegister: return false; // Return early, it's not a private response. case QModbusRequest::ReadWriteMultipleRegisters: return processReadWriteMultipleRegistersResponse(response, data); case QModbusRequest::ReadFifoQueue: case QModbusRequest::EncapsulatedInterfaceTransport: return false; // Return early, it's not a private response. default: break; } return q_func()->processPrivateResponse(response, data); } static bool isValid(const QModbusResponse &response, QModbusResponse::FunctionCode fc) { if (!response.isValid()) return false; if (response.isException()) return false; if (response.functionCode() != fc) return false; return true; } bool QModbusClientPrivate::processReadCoilsResponse(const QModbusResponse &response, QModbusDataUnit *data) { if (!isValid(response, QModbusResponse::ReadCoils)) return false; return collateBits(response, QModbusDataUnit::Coils, data); } bool QModbusClientPrivate::processReadDiscreteInputsResponse(const QModbusResponse &response, QModbusDataUnit *data) { if (!isValid(response, QModbusResponse::ReadDiscreteInputs)) return false; return collateBits(response, QModbusDataUnit::DiscreteInputs, data); } bool QModbusClientPrivate::collateBits(const QModbusPdu &response, QModbusDataUnit::RegisterType type, QModbusDataUnit *data) { if (response.dataSize() < QModbusResponse::minimumDataSize(response)) return false; const QByteArray payload = response.data(); // byte count needs to match available bytes if ((payload.size() - 1) != payload[0]) return false; if (data) { uint value = 0; for (qint32 i = 1; i < payload.size(); ++i) { const quint8 byte = quint8(payload[i]); for (qint32 currentBit = 0; currentBit < 8 && value < data->valueCount(); ++currentBit) data->setValue(value++, byte & (1U << currentBit) ? 1 : 0); } data->setRegisterType(type); } return true; } bool QModbusClientPrivate::processReadHoldingRegistersResponse(const QModbusResponse &response, QModbusDataUnit *data) { if (!isValid(response, QModbusResponse::ReadHoldingRegisters)) return false; return collateBytes(response, QModbusDataUnit::HoldingRegisters, data); } bool QModbusClientPrivate::processReadInputRegistersResponse(const QModbusResponse &response, QModbusDataUnit *data) { if (!isValid(response, QModbusResponse::ReadInputRegisters)) return false; return collateBytes(response, QModbusDataUnit::InputRegisters, data); } bool QModbusClientPrivate::collateBytes(const QModbusPdu &response, QModbusDataUnit::RegisterType type, QModbusDataUnit *data) { if (response.dataSize() < QModbusResponse::minimumDataSize(response)) return false; // byte count needs to match available bytes const quint8 byteCount = quint8(response.data().at(0)); if ((response.dataSize() - 1) != byteCount) return false; // byte count needs to be odd to match full registers if (byteCount % 2 != 0) return false; if (data) { QDataStream stream(response.data().remove(0, 1)); QVector values; const quint8 itemCount = byteCount / 2; for (int i = 0; i < itemCount; i++) { quint16 tmp; stream >> tmp; values.append(tmp); } data->setValues(values); data->setRegisterType(type); } return true; } bool QModbusClientPrivate::processWriteSingleCoilResponse(const QModbusResponse &response, QModbusDataUnit *data) { if (!isValid(response, QModbusResponse::WriteSingleCoil)) return false; return collateSingleValue(response, QModbusDataUnit::Coils, data); } bool QModbusClientPrivate::processWriteSingleRegisterResponse(const QModbusResponse &response, QModbusDataUnit *data) { if (!isValid(response, QModbusResponse::WriteSingleRegister)) return false; return collateSingleValue(response, QModbusDataUnit::HoldingRegisters, data); } bool QModbusClientPrivate::collateSingleValue(const QModbusPdu &response, QModbusDataUnit::RegisterType type, QModbusDataUnit *data) { if (response.dataSize() != QModbusResponse::minimumDataSize(response)) return false; quint16 address, value; response.decodeData(&address, &value); if ((type == QModbusDataUnit::Coils) && (value != Coil::Off) && (value != Coil::On)) return false; if (data) { data->setRegisterType(type); data->setStartAddress(address); data->setValues(QVector{ value }); } return true; } bool QModbusClientPrivate::processWriteMultipleCoilsResponse(const QModbusResponse &response, QModbusDataUnit *data) { if (!isValid(response, QModbusResponse::WriteMultipleCoils)) return false; return collateMultipleValues(response, QModbusDataUnit::Coils, data); } bool QModbusClientPrivate::processWriteMultipleRegistersResponse(const QModbusResponse &response, QModbusDataUnit *data) { if (!isValid(response, QModbusResponse::WriteMultipleRegisters)) return false; return collateMultipleValues(response, QModbusDataUnit::HoldingRegisters, data); } bool QModbusClientPrivate::collateMultipleValues(const QModbusPdu &response, QModbusDataUnit::RegisterType type, QModbusDataUnit *data) { if (response.dataSize() != QModbusResponse::minimumDataSize(response)) return false; quint16 address, count; response.decodeData(&address, &count); // number of registers to write is 1-123 per request if ((type == QModbusDataUnit::HoldingRegisters) && (count < 1 || count > 123)) return false; if (data) { data->setValueCount(count); data->setRegisterType(type); data->setStartAddress(address); } return true; } bool QModbusClientPrivate::processReadWriteMultipleRegistersResponse(const QModbusResponse &resp, QModbusDataUnit *data) { if (!isValid(resp, QModbusResponse::ReadWriteMultipleRegisters)) return false; return collateBytes(resp, QModbusDataUnit::HoldingRegisters, data); } QT_END_NAMESPACE