diff options
author | Karsten Heimrich <karsten.heimrich@qt.io> | 2021-02-15 17:05:33 +0100 |
---|---|---|
committer | Karsten Heimrich <karsten.heimrich@qt.io> | 2021-03-01 12:04:57 +0100 |
commit | c88db0ee12ca941d9b37b8e6e5b4f94ac293d866 (patch) | |
tree | e91299d4115ae6362f537d3780e64e8c73c9bcd2 | |
parent | 09927894825d97197ffa656576e6be780371dda0 (diff) |
Add Modbus custom function code example
Task-number: QTBUG-91212
Change-Id: I5df9e33186acb5216335ec1010f6694bcbab9376
Reviewed-by: Alex Blasche <alexander.blasche@qt.io>
19 files changed, 1868 insertions, 1 deletions
diff --git a/examples/serialbus/modbus/CMakeLists.txt b/examples/serialbus/modbus/CMakeLists.txt index 8d63950..2e6b6cb 100644 --- a/examples/serialbus/modbus/CMakeLists.txt +++ b/examples/serialbus/modbus/CMakeLists.txt @@ -1,5 +1,7 @@ add_subdirectory(master) add_subdirectory(slave) +add_subdirectory(custom) + if(QT_FEATURE_modbus_serialport) add_subdirectory(adueditor) endif() diff --git a/examples/serialbus/modbus/custom/CMakeLists.txt b/examples/serialbus/modbus/custom/CMakeLists.txt new file mode 100644 index 0000000..cfe398f --- /dev/null +++ b/examples/serialbus/modbus/custom/CMakeLists.txt @@ -0,0 +1,58 @@ +cmake_minimum_required(VERSION 3.14) +project(customcommand LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/serialbus/modbus/custom") + +find_package(Qt6 COMPONENTS Core) +find_package(Qt6 COMPONENTS Gui) +find_package(Qt6 COMPONENTS SerialBus) +find_package(Qt6 COMPONENTS Widgets) + +qt_add_executable(customcommand + main.cpp + mainwindow.cpp mainwindow.h mainwindow.ui + modbusclient.cpp modbusclient.h + modbusserver.cpp modbusserver.h + registermodel.cpp registermodel.h +) + +set_target_properties(customcommand PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(customcommand PRIVATE + Qt::Core + Qt::Gui + Qt::SerialBus + Qt::Widgets +) + +set(custom_resource_files + "images/connect.png" + "images/disconnect.png" + "images/settings.png" +) + +qt6_add_resources(customcommand "custom" + PREFIX + "/" + FILES + ${custom_resource_files} +) + +install(TARGETS customcommand + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/serialbus/modbus/custom/custom.pro b/examples/serialbus/modbus/custom/custom.pro new file mode 100644 index 0000000..c849c1a --- /dev/null +++ b/examples/serialbus/modbus/custom/custom.pro @@ -0,0 +1,25 @@ +QT += serialbus widgets + +TARGET = customcommand +TEMPLATE = app +CONFIG += c++11 + +SOURCES += main.cpp \ + mainwindow.cpp \ + modbusclient.cpp \ + modbusserver.cpp \ + registermodel.cpp + +HEADERS += \ + mainwindow.h \ + modbusclient.h \ + modbusserver.h \ + registermodel.h + +FORMS += \ + mainwindow.ui + +RESOURCES += images.qrc + +target.path = $$[QT_INSTALL_EXAMPLES]/serialbus/modbus/custom +INSTALLS += target diff --git a/examples/serialbus/modbus/custom/images.qrc b/examples/serialbus/modbus/custom/images.qrc new file mode 100644 index 0000000..d212059 --- /dev/null +++ b/examples/serialbus/modbus/custom/images.qrc @@ -0,0 +1,7 @@ +<RCC> + <qresource prefix="/"> + <file>images/connect.png</file> + <file>images/disconnect.png</file> + <file>images/settings.png</file> + </qresource> +</RCC> diff --git a/examples/serialbus/modbus/custom/images/application-exit.png b/examples/serialbus/modbus/custom/images/application-exit.png Binary files differnew file mode 100644 index 0000000..32be6b3 --- /dev/null +++ b/examples/serialbus/modbus/custom/images/application-exit.png diff --git a/examples/serialbus/modbus/custom/images/connect.png b/examples/serialbus/modbus/custom/images/connect.png Binary files differnew file mode 100644 index 0000000..dd5a51e --- /dev/null +++ b/examples/serialbus/modbus/custom/images/connect.png diff --git a/examples/serialbus/modbus/custom/images/disconnect.png b/examples/serialbus/modbus/custom/images/disconnect.png Binary files differnew file mode 100644 index 0000000..fd58f7a --- /dev/null +++ b/examples/serialbus/modbus/custom/images/disconnect.png diff --git a/examples/serialbus/modbus/custom/images/settings.png b/examples/serialbus/modbus/custom/images/settings.png Binary files differnew file mode 100644 index 0000000..3d1042e --- /dev/null +++ b/examples/serialbus/modbus/custom/images/settings.png diff --git a/examples/serialbus/modbus/custom/main.cpp b/examples/serialbus/modbus/custom/main.cpp new file mode 100644 index 0000000..f42e918 --- /dev/null +++ b/examples/serialbus/modbus/custom/main.cpp @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "mainwindow.h" + +#include <QApplication> +#include <QLoggingCategory> + +int main(int argc, char *argv[]) +{ + // Uncomment the following line to enable logging + // QLoggingCategory::setFilterRules(QStringLiteral("qt.modbus* = true")); + + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/examples/serialbus/modbus/custom/mainwindow.cpp b/examples/serialbus/modbus/custom/mainwindow.cpp new file mode 100644 index 0000000..cd93d78 --- /dev/null +++ b/examples/serialbus/modbus/custom/mainwindow.cpp @@ -0,0 +1,314 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "mainwindow.h" +#include "registermodel.h" +#include "ui_mainwindow.h" + +#include <QtWidgets> +#include <QtSerialBus> + +MainWindow::MainWindow(QWidget* parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + ui->statusbar->setSizeGripEnabled(false); + + setupConnections(); + setupClientContainer(); + setupServerContainer(); +} + +MainWindow::~MainWindow() +{ + m_client.disconnectDevice(); + m_server.disconnectDevice(); + + delete ui; +} + +void MainWindow::setupConnections() +{ + // actions + connect(ui->actionConnect, &QAction::triggered, this, &MainWindow::onConnectButtonClicked); + connect(ui->actionDisconnect, &QAction::triggered, this, &MainWindow::onConnectButtonClicked); + connect(ui->actionExit, &QAction::triggered, this, &QMainWindow::close); + + // buttons + connect(ui->readButton, &QPushButton::clicked, this, &MainWindow::onReadButtonClicked); + connect(ui->writeButton, &QPushButton::clicked, this, &MainWindow::onWriteButtonClicked); + connect(ui->connectButton, &QPushButton::clicked, this, &MainWindow::onConnectButtonClicked); + + // client + connect(&m_client, &QModbusServer::stateChanged, this, &MainWindow::onStateChanged); + connect(&m_client, &QModbusServer::errorOccurred, [this](QModbusDevice::Error) { + statusBar()->showMessage(m_client.errorString(), 5000); + }); + + // server + connect(&m_server, &QModbusServer::dataWritten, this, &MainWindow::updateWidgets); + connect(&m_server, &QModbusServer::errorOccurred, [this](QModbusDevice::Error) { + statusBar()->showMessage(m_server.errorString(), 5000); + }); +} + +void MainWindow::onConnectButtonClicked() +{ + bool intendToConnect = (m_server.state() == QModbusDevice::UnconnectedState + && m_client.state() == QModbusDevice::UnconnectedState); + + if (intendToConnect) { + const QUrl url = QUrl::fromUserInput(ui->addressPort->text()); + m_server.setConnectionParameter(QModbusDevice::NetworkPortParameter, url.port()); + m_server.setConnectionParameter(QModbusDevice::NetworkAddressParameter, url.host()); + m_server.setServerAddress(ui->serverAddress->text().toInt()); + + if (m_server.connectDevice()) { + m_client.setConnectionParameter(QModbusDevice::NetworkPortParameter, url.port()); + m_client.setConnectionParameter(QModbusDevice::NetworkAddressParameter, url.host()); + + if (m_client.connectDevice()) { + ui->actionConnect->setEnabled(false); + ui->actionDisconnect->setEnabled(true); + } else { + statusBar()->showMessage(tr("Client connect failed: ") + m_client.errorString(), 5000); + } + } else { + statusBar()->showMessage(tr("Server connect failed: ") + m_server.errorString(), 5000); + } + } else { + m_client.disconnectDevice(); + m_server.disconnectDevice(); + ui->actionConnect->setEnabled(true); + ui->actionDisconnect->setEnabled(false); + } +} + +void MainWindow::onStateChanged(int state) +{ + ui->serverAddress->setEnabled(state == QModbusDevice::UnconnectedState); + ui->actionConnect->setEnabled(state == QModbusDevice::UnconnectedState); + ui->actionDisconnect->setEnabled(state == QModbusDevice::ConnectedState); + + if (state == QModbusDevice::UnconnectedState) + ui->connectButton->setText(tr("Connect")); + else if (state == QModbusDevice::ConnectedState) + ui->connectButton->setText(tr("Disconnect")); +} + +// -- client + +void MainWindow::onReadReady() +{ + auto reply = qobject_cast<QModbusReply*>(sender()); + if (!reply) + return; + + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + for (int i = 0, total = int(unit.valueCount()); i < total; ++i) { + auto st = unit.startAddress(); + m_model->setData(m_model->index(unit.startAddress() + i, 1), + QString::number(unit.value(i), 16), Qt::EditRole); + } + } else if (reply->error() == QModbusDevice::ProtocolError) { + statusBar()->showMessage(tr("Read response error: %1 (Modbus exception: 0x%2)"). + arg(reply->errorString()). + arg(reply->rawResult().exceptionCode(), -1, 16), 5000); + } else { + statusBar()->showMessage(tr("Read response error: %1 (code: 0x%2)"). + arg(reply->errorString()). + arg(reply->error(), -1, 16), 5000); + } + + reply->deleteLater(); +} + +void MainWindow::onReadButtonClicked() +{ + QModbusRequest readRequest { + QModbusPdu::FunctionCode(ModbusClient::CustomRead), + quint16(ui->startAddress->value()), + qMin(ui->numberOfRegisters->currentText().toUShort(), + quint16(10 - ui->startAddress->value())) // do not go beyond 10 entries + }; + + if (auto* reply = m_client.sendRawRequest(readRequest, ui->serverAddress->value())) { + if (!reply->isFinished()) + connect(reply, &QModbusReply::finished, this, &MainWindow::onReadReady); + } else { + statusBar()->showMessage(tr("Read error: ") + m_client.errorString(), 5000); + } +} + +void MainWindow::onWriteButtonClicked() +{ + QModbusDataUnit unit { + QModbusDataUnit::HoldingRegisters, + ui->startAddress->value(), + qMin(ui->numberOfRegisters->currentText().toUShort(), + quint16(10 - ui->startAddress->value())) // do not go beyond 10 entries + }; + + for (qsizetype i = 0, total = int(unit.valueCount()); i < total; ++i) + unit.setValue(i, m_model->m_registers[i + unit.startAddress()]); + + const quint8 byteCount = unit.valueCount() * 2; + QModbusRequest writeRequest { + QModbusPdu::FunctionCode(ModbusClient::CustomWrite), + quint16(unit.startAddress()), + quint16(unit.valueCount()), byteCount, unit.values() + }; + + if (auto *reply = m_client.sendRawRequest(writeRequest, ui->serverAddress->value())) { + if (!reply->isFinished()) { + connect(reply, &QModbusReply::finished, this, [this, reply]() { + if (reply->error() == QModbusDevice::ProtocolError) { + statusBar()->showMessage(tr("Write response error: %1 (Modbus exception: 0x%2)") + .arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16), + 5000); + } else if (reply->error() != QModbusDevice::NoError) { + statusBar()->showMessage(tr("Write response error: %1 (code: 0x%2)"). + arg(reply->errorString()).arg(reply->error(), -1, 16), 5000); + } + + reply->deleteLater(); + } + ); + } + } else { + statusBar()->showMessage(tr("Write error: ") + m_client.errorString(), 5000); + } +} + +void MainWindow::setupClientContainer() +{ + m_model = new RegisterModel(this); + m_model->setStartAddress(ui->startAddress->value()); + m_model->setNumberOfValues(ui->numberOfRegisters->currentText()); + + ui->registersTable->setModel(m_model); + connect(m_model, &RegisterModel::updateViewport, ui->registersTable->viewport(), + QOverload<>::of(&QWidget::update)); + + auto model = new QStandardItemModel(10, 1, this); + for (int i = 0; i < 10; ++i) + model->setItem(i, new QStandardItem(QStringLiteral("%1").arg(i + 1))); + ui->numberOfRegisters->setModel(model); + ui->numberOfRegisters->setCurrentText("10"); + connect(ui->numberOfRegisters, &QComboBox::currentTextChanged, + m_model, &RegisterModel::setNumberOfValues); + + connect(ui->startAddress, &QSpinBox::valueChanged, m_model, &RegisterModel::setStartAddress); + connect(ui->startAddress, &QSpinBox::valueChanged, this, [this, model](int i) { + int lastPossibleIndex = 0; + const int currentIndex = ui->numberOfRegisters->currentIndex(); + for (int ii = 0; ii < 10; ++ii) { + if (ii < (10 - i)) { + lastPossibleIndex = ii; + model->item(ii)->setEnabled(true); + } else { + model->item(ii)->setEnabled(false); + } + } + if (currentIndex > lastPossibleIndex) + ui->numberOfRegisters->setCurrentIndex(lastPossibleIndex); + } + ); +} + +// -- server + +void MainWindow::setRegister(const QString &value) +{ + const QString objectName = QObject::sender()->objectName(); + if (m_registers.contains(objectName)) { + const quint16 id = quint16(QObject::sender()->property("ID").toUInt()); + + bool ok = false; + ok = m_server.setData(QModbusDataUnit::HoldingRegisters, id, value.toUShort(&ok, 16)); + if (!ok) + statusBar()->showMessage(tr("Could not set register: ") + m_server.errorString(), 5000); + } +} + +void MainWindow::updateWidgets(QModbusDataUnit::RegisterType table, int address, int size) +{ + if (table != QModbusDataUnit::HoldingRegisters) + return; + + QString text; + quint16 value; + for (int i = 0; i < size; ++i) { + m_server.data(table, quint16(address + i), &value); + m_registers.value(QStringLiteral("register_%1").arg(address + i))->setText(text.setNum(value, 16)); + } +} + +void MainWindow::setupServerContainer() +{ + QRegularExpression regexp(QStringLiteral("register_(?<ID>\\d+)")); + const QList<QLineEdit *> qle = ui->server->findChildren<QLineEdit *>(regexp); + for (QLineEdit *lineEdit : qle) { + const quint16 id = regexp.match(lineEdit->objectName()).captured(QStringLiteral("ID")).toInt(); + + lineEdit->setProperty("ID", id); + m_registers.insert(lineEdit->objectName(), lineEdit); + lineEdit->setValidator(new QRegularExpressionValidator( + QRegularExpression(QStringLiteral("[0-9a-f]{0,4}"), QRegularExpression::CaseInsensitiveOption), + this) + ); + + bool ok; + m_server.setData(QModbusDataUnit::HoldingRegisters, id, lineEdit->text().toUShort(&ok, 16)); + connect(lineEdit, &QLineEdit::textChanged, this, &MainWindow::setRegister); + } +} diff --git a/examples/serialbus/modbus/custom/mainwindow.h b/examples/serialbus/modbus/custom/mainwindow.h new file mode 100644 index 0000000..e538da3 --- /dev/null +++ b/examples/serialbus/modbus/custom/mainwindow.h @@ -0,0 +1,105 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "modbusclient.h" +#include "modbusserver.h" + +#include <QHash> +#include <QMainWindow> + +QT_BEGIN_NAMESPACE + +class QLineEdit; + +namespace Ui { +class MainWindow; +} + +QT_END_NAMESPACE + +class RegisterModel; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private Q_SLOTS: + void onConnectButtonClicked(); + void onStateChanged(int state); + + void onReadReady(); + void onReadButtonClicked(); + void onWriteButtonClicked(); + + void setRegister(const QString &value); + void updateWidgets(QModbusDataUnit::RegisterType table, int address, int size); + +private: + void setupConnections(); + void setupClientContainer(); + void setupServerContainer(); + +private: + Ui::MainWindow *ui = nullptr; + RegisterModel *m_model = nullptr; + + ModbusClient m_client; + ModbusServer m_server; + QHash<QString, QLineEdit *> m_registers; +}; + +#endif // MAINWINDOW_H diff --git a/examples/serialbus/modbus/custom/mainwindow.ui b/examples/serialbus/modbus/custom/mainwindow.ui new file mode 100644 index 0000000..d8e6099 --- /dev/null +++ b/examples/serialbus/modbus/custom/mainwindow.ui @@ -0,0 +1,650 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>620</width> + <height>482</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Address & Port:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="addressPort"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>127.0.0.1:502</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>13</width> + <height>17</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="label_28"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Server Address:</string> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QSpinBox" name="serverAddress"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>247</number> + </property> + </widget> + </item> + <item row="0" column="5"> + <widget class="QPushButton" name="connectButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Connect</string> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0" colspan="6"> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Custom Function code (Read):</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label_9"> + <property name="font"> + <font> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>0X41</string> + </property> + </widget> + </item> + <item row="0" column="2" rowspan="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="3" rowspan="2"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>The Modbus specification reserves two ranges of user-defined function codes, i.e. 0x41 to 0x48 and from 0x64 to 0x6e.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Custom Function code (Write):</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="label_10"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>0X42</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QGroupBox" name="server"> + <property name="title"> + <string>Server</string> + </property> + <layout class="QFormLayout" name="formLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="address"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Address</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Registers</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_21"> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="register_0"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_22"> + <property name="text"> + <string>1</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="register_1"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_23"> + <property name="text"> + <string>2</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="register_2"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>3</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="register_3"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>4</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLineEdit" name="register_4"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>5</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QLineEdit" name="register_5"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="7" column="0"> + <widget class="QLabel" name="label_13"> + <property name="text"> + <string>6</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QLineEdit" name="register_6"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="8" column="0"> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>7</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="8" column="1"> + <widget class="QLineEdit" name="register_7"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="9" column="0"> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string>8</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="9" column="1"> + <widget class="QLineEdit" name="register_8"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + <item row="10" column="0"> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string>9</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="10" column="1"> + <widget class="QLineEdit" name="register_9"> + <property name="maxLength"> + <number>32767</number> + </property> + <property name="placeholderText"> + <string>Hexadecimal A-F, a-f, 0-9.</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Client</string> + </property> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Start address:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="startAddress"> + <property name="maximum"> + <number>9</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Number of registers:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="numberOfRegisters"> + <property name="currentIndex"> + <number>9</number> + </property> + <item> + <property name="text"> + <string>1</string> + </property> + </item> + <item> + <property name="text"> + <string>2</string> + </property> + </item> + <item> + <property name="text"> + <string>3</string> + </property> + </item> + <item> + <property name="text"> + <string>4</string> + </property> + </item> + <item> + <property name="text"> + <string>5</string> + </property> + </item> + <item> + <property name="text"> + <string>6</string> + </property> + </item> + <item> + <property name="text"> + <string>7</string> + </property> + </item> + <item> + <property name="text"> + <string>8</string> + </property> + </item> + <item> + <property name="text"> + <string>9</string> + </property> + </item> + <item> + <property name="text"> + <string>10</string> + </property> + </item> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QTreeView" name="registersTable"> + <property name="showDropIndicator" stdset="0"> + <bool>true</bool> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="expandsOnDoubleClick"> + <bool>false</bool> + </property> + <attribute name="headerVisible"> + <bool>true</bool> + </attribute> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>13</width> + <height>17</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="readButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Read</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="writeButton"> + <property name="text"> + <string>Write</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>620</width> + <height>22</height> + </rect> + </property> + <widget class="QMenu" name="menuDevice"> + <property name="title"> + <string>Device</string> + </property> + <addaction name="actionConnect"/> + <addaction name="actionDisconnect"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <addaction name="menuDevice"/> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <action name="actionExit"> + <property name="text"> + <string>Quit</string> + </property> + </action> + <action name="actionConnect"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="images.qrc"> + <normaloff>:/images/connect.png</normaloff>:/images/connect.png</iconset> + </property> + <property name="text"> + <string>Connect</string> + </property> + </action> + <action name="actionDisconnect"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="icon"> + <iconset resource="images.qrc"> + <normaloff>:/images/disconnect.png</normaloff>:/images/disconnect.png</iconset> + </property> + <property name="text"> + <string>Disconnect</string> + </property> + </action> + </widget> + <resources> + <include location="images.qrc"/> + </resources> + <connections/> +</ui> diff --git a/examples/serialbus/modbus/custom/modbusclient.cpp b/examples/serialbus/modbus/custom/modbusclient.cpp new file mode 100644 index 0000000..faba17f --- /dev/null +++ b/examples/serialbus/modbus/custom/modbusclient.cpp @@ -0,0 +1,134 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "modbusclient.h" + +// The read response must contain the start address (quint16), the byte count +// (quint8) and at least one register value (quint16). +static constexpr int ReadHeaderSize {3}; +static constexpr int MinimumReadResponseSize {ReadHeaderSize + 2}; // 2 payload bytes + +// The response must contain the start address (quint16) and the number of +// registers written (quint16). +static constexpr int WriteResponseSize {4}; + +ModbusClient::ModbusClient(QObject *parent) + : QModbusTcpClient(parent) +{ + QModbusResponse::registerDataSizeCalculator(CustomRead, [](const QModbusResponse &response) { + if (!response.isValid()) + return -1; + + if (response.dataSize() < MinimumReadResponseSize) + return -1; + return ReadHeaderSize + quint8(response.data().at(ReadHeaderSize - 1)); + }); + + QModbusResponse::registerDataSizeCalculator(CustomWrite, [](const QModbusResponse &response) { + if (!response.isValid()) + return -1; + return (response.dataSize() != WriteResponseSize ? -1 : WriteResponseSize); + }); +} + +static bool collateBytes(const QModbusPdu &response, QModbusDataUnit *data) +{ + if (response.dataSize() < MinimumReadResponseSize) + return false; + + quint16 address; quint8 byteCount; + response.decodeData(&address, &byteCount); + + if (byteCount % 2 != 0) + return false; + + if (data) { + QDataStream stream(response.data().remove(0, 3)); + + QList<quint16> values; + const quint8 itemCount = byteCount / 2; + for (int i = 0; i < itemCount; i++) { + quint16 tmp; + stream >> tmp; + values.append(tmp); + } + *data = {QModbusDataUnit::HoldingRegisters, address, values}; + } + return true; +} + +static bool collateMultipleValues(const QModbusPdu &response, QModbusDataUnit *data) +{ + if (response.dataSize() != WriteResponseSize) + return false; + + quint16 address, count; + response.decodeData(&address, &count); + + if (count < 1 || count > 10) + return false; + + if (data) + *data = {QModbusDataUnit::HoldingRegisters, address, count}; + return true; +} + +bool ModbusClient::processPrivateResponse(const QModbusResponse &response, QModbusDataUnit *data) +{ + if (!response.isValid()) + return QModbusClient::processPrivateResponse(response, data); + + if (CustomRead == response.functionCode()) + return collateBytes(response, data); + + if (CustomWrite == response.functionCode()) + return collateMultipleValues(response, data); + return QModbusClient::processPrivateResponse(response, data); +} diff --git a/examples/serialbus/modbus/custom/modbusclient.h b/examples/serialbus/modbus/custom/modbusclient.h new file mode 100644 index 0000000..a803839 --- /dev/null +++ b/examples/serialbus/modbus/custom/modbusclient.h @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QModbusTcpClient> + +#ifndef MODBUSCLIENT_H +#define MODBUSCLIENT_H + +class ModbusClient : public QModbusTcpClient +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(ModbusClient) + +public: + ModbusClient(QObject *parent = nullptr); + + static constexpr QModbusPdu::FunctionCode CustomRead {QModbusPdu::FunctionCode(0x41)}; + static constexpr QModbusPdu::FunctionCode CustomWrite {QModbusPdu::FunctionCode(0x42)}; + +private: + bool processPrivateResponse(const QModbusResponse &response, QModbusDataUnit *data) override; +}; + +#endif // MODBUSCLIENT_H diff --git a/examples/serialbus/modbus/custom/modbusserver.cpp b/examples/serialbus/modbus/custom/modbusserver.cpp new file mode 100644 index 0000000..e80a472 --- /dev/null +++ b/examples/serialbus/modbus/custom/modbusserver.cpp @@ -0,0 +1,131 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "modbusserver.h" +#include "modbusclient.h" + +// The request must contain the start address (quint16) and the number of +// registers to read (quint16). +static constexpr int ReadRequestSize {4}; + +// The request must contain the start address (quint16), the number of +// registers to write (quint16), the byte count (quint8) and at least one +// register value (quint16). +static constexpr int WriteHeaderSize {5}; +static constexpr int MinimumWriteRequestSize {WriteHeaderSize + 2}; // 2 payload bytes + +ModbusServer::ModbusServer(QObject *parent) + : QModbusTcpServer(parent) +{ + setMap({{QModbusDataUnit::HoldingRegisters, {QModbusDataUnit::HoldingRegisters, 0, 10}}}); + + QModbusRequest::registerDataSizeCalculator(ModbusClient::CustomRead, [](const QModbusRequest &request) { + if (!request.isValid()) + return -1; + return (request.dataSize() != ReadRequestSize ? -1 : ReadRequestSize); + }); + + QModbusRequest::registerDataSizeCalculator(ModbusClient::CustomWrite, [](const QModbusRequest &request) { + if (!request.isValid()) + return -1; + + if (request.dataSize() < MinimumWriteRequestSize) + return -1; + return WriteHeaderSize + quint8(request.data().at(WriteHeaderSize - 1)); + }); +} + +QModbusResponse ModbusServer::processPrivateRequest(const QModbusPdu &request) +{ + if (!request.isValid()) + return QModbusServer::processPrivateRequest(request); + + if (ModbusClient::CustomRead == request.functionCode()) { + quint16 startAddress, count; + request.decodeData(&startAddress, &count); + + QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, startAddress, count); + if (!data(&unit)) { + return QModbusExceptionResponse(request.functionCode(), + QModbusExceptionResponse::IllegalDataAddress); + } + return QModbusResponse(request.functionCode(), startAddress, quint8(count * 2), unit.values()); + } + + if (ModbusClient::CustomWrite == request.functionCode()) { + quint8 byteCount; + quint16 startAddress, numberOfRegisters; + request.decodeData(&startAddress, &numberOfRegisters, &byteCount); + + if (byteCount % 2 != 0) { + return QModbusExceptionResponse(request.functionCode(), + QModbusExceptionResponse::IllegalDataValue); + } + + const QByteArray pduData = request.data().remove(0, WriteHeaderSize); + QDataStream stream(pduData); + + QList<quint16> values; + for (int i = 0; i < numberOfRegisters; i++) { + quint16 tmp; + stream >> tmp; + values.append(tmp); + } + + if (!writeData({QModbusDataUnit::HoldingRegisters, startAddress, values})) { + return QModbusExceptionResponse(request.functionCode(), + QModbusExceptionResponse::ServerDeviceFailure); + } + + return QModbusResponse(request.functionCode(), startAddress, numberOfRegisters); + } + + return QModbusServer::processPrivateRequest(request); +} diff --git a/examples/serialbus/modbus/custom/modbusserver.h b/examples/serialbus/modbus/custom/modbusserver.h new file mode 100644 index 0000000..cae690e --- /dev/null +++ b/examples/serialbus/modbus/custom/modbusserver.h @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QModbusTcpServer> + +#ifndef MODBUSSERVER_H +#define MODBUSSERVER_H + +class ModbusServer : public QModbusTcpServer +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(ModbusServer) + +public: + ModbusServer(QObject *parent = nullptr); + +private: + QModbusResponse processPrivateRequest(const QModbusPdu &request) override; +}; + +#endif // MODBUSSERVER_H diff --git a/examples/serialbus/modbus/custom/registermodel.cpp b/examples/serialbus/modbus/custom/registermodel.cpp new file mode 100644 index 0000000..7e0f52f --- /dev/null +++ b/examples/serialbus/modbus/custom/registermodel.cpp @@ -0,0 +1,150 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "registermodel.h" + +enum { NumColumn = 0, RegistersColumn = 1, ColumnCount = 2, RowCount = 10 }; + +RegisterModel::RegisterModel(QObject *parent) + : QAbstractTableModel(parent) + , m_registers(RowCount, 0u) +{ +} + +int RegisterModel::rowCount(const QModelIndex &/*parent*/) const +{ + return RowCount; +} + +int RegisterModel::columnCount(const QModelIndex &/*parent*/) const +{ + return ColumnCount; +} + +QVariant RegisterModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= RowCount || index.column() >= ColumnCount) + return QVariant(); + + Q_ASSERT(m_registers.count() == RowCount); + + if (index.column() == NumColumn && role == Qt::DisplayRole) + return QString::number(index.row()); + + if (index.column() == RegistersColumn && role == Qt::DisplayRole) + return QString("0x%1").arg(QString::number(m_registers.at(index.row()), 16)); + + return QVariant(); +} + +QVariant RegisterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Horizontal) { + switch (section) { + case NumColumn: + return tr("Address"); + case RegistersColumn: + return tr("Registers"); + default: + break; + } + } + return QVariant(); +} + +bool RegisterModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() >= RowCount || index.column() >= ColumnCount) + return false; + + Q_ASSERT(m_registers.count() == RowCount); + + if (index.column() == RegistersColumn && role == Qt::EditRole) { + bool result = false; + quint16 newValue = value.toString().toUShort(&result, 16); + if (result) + m_registers[index.row()] = newValue; + + emit dataChanged(index, index); + return result; + } + + return false; +} + +Qt::ItemFlags RegisterModel::flags(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() >= RowCount || index.column() >= ColumnCount) + return QAbstractTableModel::flags(index); + + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + if ((index.row() < m_address) || (index.row() >= (m_address + m_number))) + flags &= ~Qt::ItemIsEnabled; + + if (index.column() == RegistersColumn) + return flags | Qt::ItemIsEditable; + + return flags; +} + +void RegisterModel::setStartAddress(int address) +{ + m_address = address; + emit updateViewport(); +} + +void RegisterModel::setNumberOfValues(const QString &number) +{ + m_number = number.toInt(); + emit updateViewport(); +} diff --git a/examples/serialbus/modbus/custom/registermodel.h b/examples/serialbus/modbus/custom/registermodel.h new file mode 100644 index 0000000..1c06ddd --- /dev/null +++ b/examples/serialbus/modbus/custom/registermodel.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the QtSerialBus module. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef REGISTERMODEL_H +#define REGISTERMODEL_H + +#include <QAbstractItemModel> + +class RegisterModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + RegisterModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + +public slots: + void setStartAddress(int address); + void setNumberOfValues(const QString &number); + +signals: + void updateViewport(); + +public: + int m_number = 0; + int m_address = 0; + QList<quint16> m_registers; +}; + +#endif // REGISTERMODEL_H diff --git a/examples/serialbus/modbus/modbus.pro b/examples/serialbus/modbus/modbus.pro index a15b828..9e94be7 100644 --- a/examples/serialbus/modbus/modbus.pro +++ b/examples/serialbus/modbus/modbus.pro @@ -3,4 +3,5 @@ TEMPLATE = subdirs SUBDIRS += \ master \ slave \ - adueditor + adueditor \ + custom |