// Copyright (C) 2017 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QMODBUSRTUSERIALCLIENT_P_H #define QMODBUSRTUSERIALCLIENT_P_H #include #include #include #include #include #include #include #include #include #include // // W A R N I N G // ------------- // // This file is not part of the Qt API. It exists purely as an // implementation detail. This header file may change from version to // version without notice, or even be removed. // // We mean it. // QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS) Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS_LOW) class Timer : public QObject { Q_OBJECT public: Timer() = default; int start(int msec) { m_timer = QBasicTimer(); m_timer.start(msec, Qt::PreciseTimer, this); return m_timer.timerId(); } void stop() { m_timer.stop(); } bool isActive() const { return m_timer.isActive(); } signals: void timeout(int timerId); private: void timerEvent(QTimerEvent *event) override { const auto id = m_timer.timerId(); if (event->timerId() == id) emit timeout(id); } private: QBasicTimer m_timer; }; class QModbusRtuSerialClientPrivate : public QModbusClientPrivate { Q_DECLARE_PUBLIC(QModbusRtuSerialClient) enum State { Idle, WaitingForReplay, ProcessReply } m_state = Idle; public: void onReadyRead() { m_responseBuffer += m_serialPort->read(m_serialPort->bytesAvailable()); qCDebug(QT_MODBUS_LOW) << "(RTU client) Response buffer:" << m_responseBuffer.toHex(); if (m_responseBuffer.size() < 2) { qCDebug(QT_MODBUS) << "(RTU client) Modbus ADU not complete"; return; } const QModbusSerialAdu tmpAdu(QModbusSerialAdu::Rtu, m_responseBuffer); int pduSizeWithoutFcode = QModbusResponse::calculateDataSize(tmpAdu.pdu()); if (pduSizeWithoutFcode < 0) { // wait for more data qCDebug(QT_MODBUS) << "(RTU client) Cannot calculate PDU size for function code:" << tmpAdu.pdu().functionCode() << ", delaying pending frame"; return; } // server address byte + function code byte + PDU size + 2 bytes CRC int aduSize = 2 + pduSizeWithoutFcode + 2; if (tmpAdu.rawSize() < aduSize) { qCDebug(QT_MODBUS) << "(RTU client) Incomplete ADU received, ignoring"; return; } if (m_queue.isEmpty()) return; auto ¤t = m_queue.first(); // Special case for Diagnostics:ReturnQueryData. The response has no // length indicator and is just a simple echo of what we have send. if (tmpAdu.pdu().functionCode() == QModbusPdu::Diagnostics) { const QModbusResponse response = tmpAdu.pdu(); if (canMatchRequestAndResponse(response, tmpAdu.serverAddress())) { quint16 subCode = 0xffff; response.decodeData(&subCode); if (subCode == Diagnostics::ReturnQueryData) { if (response.data() != current.requestPdu.data()) return; // echo does not match request yet aduSize = 2 + response.dataSize() + 2; if (tmpAdu.rawSize() < aduSize) return; // echo matches, probably checksum missing } } } const QModbusSerialAdu adu(QModbusSerialAdu::Rtu, m_responseBuffer.left(aduSize)); m_responseBuffer.remove(0, aduSize); qCDebug(QT_MODBUS) << "(RTU client) Received ADU:" << adu.rawData().toHex(); if (QT_MODBUS().isDebugEnabled() && !m_responseBuffer.isEmpty()) qCDebug(QT_MODBUS_LOW) << "(RTU client) Pending buffer:" << m_responseBuffer.toHex(); // check CRC if (!adu.matchingChecksum()) { qCWarning(QT_MODBUS) << "(RTU client) Discarding response with wrong CRC, received:" << adu.checksum() << ", calculated CRC:" << QModbusSerialAdu::calculateCRC(adu.data(), adu.size()); m_queue.first().reply->addIntermediateError(QModbusClient::ResponseCrcError); return; } const QModbusResponse response = adu.pdu(); if (!canMatchRequestAndResponse(response, adu.serverAddress())) { qCWarning(QT_MODBUS) << "(RTU client) Cannot match response with open request, " "ignoring"; m_queue.first().reply->addIntermediateError(QModbusClient::ResponseRequestMismatch); return; } m_state = ProcessReply; m_responseTimer.stop(); current.m_timerId = INT_MIN; processQueueElement(response, m_queue.dequeue()); m_state = Idle; scheduleNextRequest(m_interFrameDelayMilliseconds); } void onAboutToClose() { Q_Q(QModbusRtuSerialClient); Q_UNUSED(q); // avoid warning in release mode Q_ASSERT(q->state() == QModbusDevice::ClosingState); m_responseTimer.stop(); } void onResponseTimeout(int timerId) { m_responseTimer.stop(); if (m_state != State::WaitingForReplay || m_queue.isEmpty()) return; const auto ¤t = m_queue.first(); if (current.m_timerId != timerId) return; qCDebug(QT_MODBUS) << "(RTU client) Receive timeout:" << current.requestPdu; if (current.numberOfRetries <= 0) { auto item = m_queue.dequeue(); if (item.reply) { item.reply->setError(QModbusDevice::TimeoutError, QModbusClient::tr("Request timeout.")); } } m_state = Idle; scheduleNextRequest(m_interFrameDelayMilliseconds); } void onBytesWritten(qint64 bytes) { if (m_queue.isEmpty()) return; auto ¤t = m_queue.first(); current.bytesWritten += bytes; if (current.bytesWritten != current.adu.size()) return; qCDebug(QT_MODBUS) << "(RTU client) Send successful:" << current.requestPdu; if (!current.reply.isNull() && current.reply->type() == QModbusReply::Broadcast) { m_state = ProcessReply; processQueueElement({}, m_queue.dequeue()); m_state = Idle; scheduleNextRequest(m_turnaroundDelay); } else { current.m_timerId = m_responseTimer.start(m_responseTimeoutDuration); } } void onError(QSerialPort::SerialPortError error) { if (error == QSerialPort::NoError) return; qCDebug(QT_MODBUS) << "(RTU server) QSerialPort error:" << error << (m_serialPort ? m_serialPort->errorString() : QString()); Q_Q(QModbusRtuSerialClient); switch (error) { case QSerialPort::DeviceNotFoundError: q->setError(QModbusDevice::tr("Referenced serial device does not exist."), QModbusDevice::ConnectionError); break; case QSerialPort::PermissionError: q->setError(QModbusDevice::tr("Cannot open serial device due to permissions."), QModbusDevice::ConnectionError); break; case QSerialPort::OpenError: case QSerialPort::NotOpenError: q->setError(QModbusDevice::tr("Cannot open serial device."), QModbusDevice::ConnectionError); break; case QSerialPort::WriteError: q->setError(QModbusDevice::tr("Write error."), QModbusDevice::WriteError); break; case QSerialPort::ReadError: q->setError(QModbusDevice::tr("Read error."), QModbusDevice::ReadError); break; case QSerialPort::ResourceError: q->setError(QModbusDevice::tr("Resource error."), QModbusDevice::ConnectionError); break; case QSerialPort::UnsupportedOperationError: q->setError(QModbusDevice::tr("Device operation is not supported error."), QModbusDevice::ConfigurationError); break; case QSerialPort::TimeoutError: q->setError(QModbusDevice::tr("Timeout error."), QModbusDevice::TimeoutError); break; case QSerialPort::UnknownError: q->setError(QModbusDevice::tr("Unknown error."), QModbusDevice::UnknownError); break; default: qCDebug(QT_MODBUS) << "(RTU server) Unhandled QSerialPort error" << error; break; } } void setupSerialPort() { Q_Q(QModbusRtuSerialClient); m_serialPort = new QSerialPort(q); QObject::connect(&m_responseTimer, &Timer::timeout, q, [this](int timerId) { onResponseTimeout(timerId); }); QObject::connect(m_serialPort, &QSerialPort::readyRead, q, [this]() { onReadyRead(); }); QObject::connect(m_serialPort, &QSerialPort::aboutToClose, q, [this]() { onAboutToClose(); }); QObject::connect(m_serialPort, &QSerialPort::bytesWritten, q, [this](qint64 bytes) { onBytesWritten(bytes); }); QObject::connect(m_serialPort, &QSerialPort::errorOccurred, q, [this](QSerialPort::SerialPortError error) { onError(error); }); } void setupEnvironment() { if (m_serialPort) { m_serialPort->setPortName(m_comPort); m_serialPort->setParity(m_parity); m_serialPort->setBaudRate(m_baudRate); m_serialPort->setDataBits(m_dataBits); m_serialPort->setStopBits(m_stopBits); } calculateInterFrameDelay(); m_responseBuffer.clear(); m_state = QModbusRtuSerialClientPrivate::Idle; } QModbusReply *enqueueRequest(const QModbusRequest &request, int serverAddress, const QModbusDataUnit &unit, QModbusReply::ReplyType type) override { Q_Q(QModbusRtuSerialClient); auto reply = new QModbusReply(serverAddress == 0 ? QModbusReply::Broadcast : type, serverAddress, q); QueueElement element(reply, request, unit, m_numberOfRetries + 1); element.adu = QModbusSerialAdu::create(QModbusSerialAdu::Rtu, serverAddress, request); m_queue.enqueue(element); scheduleNextRequest(m_interFrameDelayMilliseconds); return reply; } void scheduleNextRequest(int delay) { Q_Q(QModbusRtuSerialClient); if (m_state == Idle && !m_queue.isEmpty()) { m_state = WaitingForReplay; QTimer::singleShot(delay, q, [this]() { processQueue(); }); } } void processQueue() { m_responseBuffer.clear(); m_serialPort->clear(QSerialPort::AllDirections); if (m_queue.isEmpty()) return; auto ¤t = m_queue.first(); if (current.reply.isNull()) { m_queue.dequeue(); m_state = Idle; scheduleNextRequest(m_interFrameDelayMilliseconds); } else { current.bytesWritten = 0; current.numberOfRetries--; m_serialPort->write(current.adu); qCDebug(QT_MODBUS) << "(RTU client) Sent Serial PDU:" << current.requestPdu; qCDebug(QT_MODBUS_LOW).noquote() << "(RTU client) Sent Serial ADU: 0x" + current.adu .toHex(); } } bool canMatchRequestAndResponse(const QModbusResponse &response, int sendingServer) const { if (m_queue.isEmpty()) return false; const auto ¤t = m_queue.first(); if (current.reply.isNull()) return false; // reply deleted if (current.reply->serverAddress() != sendingServer) return false; // server mismatch if (current.requestPdu.functionCode() != response.functionCode()) return false; // request for different function code return true; } bool isOpen() const override { if (m_serialPort) return m_serialPort->isOpen(); return false; } QIODevice *device() const override { return m_serialPort; } Timer m_responseTimer; QByteArray m_responseBuffer; QQueue m_queue; QSerialPort *m_serialPort = nullptr; int m_turnaroundDelay = 100; // Recommended value is between 100 and 200 msec. }; QT_END_NAMESPACE #endif // QMODBUSRTUSERIALCLIENT_P_H