diff --git a/debian/control b/debian/control
index b329878..da6d6a4 100644
--- a/debian/control
+++ b/debian/control
@@ -60,6 +60,15 @@ Description: The main libraries and header files for developing with modbus base
This package contains the nymea modbus integration plugin library - development files.
+Package: nymea-modbus-cli
+Architecture: any
+Section: utils
+Depends: ${shlibs:Depends},
+ ${misc:Depends}
+Description: nymea modbus command line tool for testing modbus TCP communication
+ This package contains the nymea modbus command line tool for testing modbus TCP communication.
+
+
Package: nymea-plugin-alphainnotec
Architecture: any
Section: libs
diff --git a/debian/nymea-modbus-cli.install.in b/debian/nymea-modbus-cli.install.in
new file mode 100644
index 0000000..8181cb2
--- /dev/null
+++ b/debian/nymea-modbus-cli.install.in
@@ -0,0 +1 @@
+usr/bin/nymea-modbus-cli
diff --git a/nymea-modbus-cli/main.cpp b/nymea-modbus-cli/main.cpp
new file mode 100644
index 0000000..4e58aa1
--- /dev/null
+++ b/nymea-modbus-cli/main.cpp
@@ -0,0 +1,191 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication application(argc, argv);
+ application.setApplicationName("nymea-modbus-cli");
+ application.setOrganizationName("nymea");
+ application.setApplicationVersion("1.0.0");
+
+ QString description = QString("\nTool for testing and reading Modbus TCP registers.\n\n");
+ description.append(QString("Copyright %1 2016 - 2022 nymea GmbH \n").arg(QChar(0xA9)));
+
+ QCommandLineParser parser;
+ parser.addHelpOption();
+ parser.addVersionOption();
+ parser.setApplicationDescription(description);
+
+ QCommandLineOption addressOption(QStringList() << "a" << "address", QString("The IP address of the modbus TCP server."), "address");
+ parser.addOption(addressOption);
+
+ QCommandLineOption portOption(QStringList() << "p" << "port", QString("The port of the modbus TCP server. Default is 502."), "port");
+ portOption.setDefaultValue("502");
+ parser.addOption(portOption);
+
+ QCommandLineOption modbusServerAddressOption(QStringList() << "m" << "modbus-address", QString("The modbus server address on the bus (slave ID). Default is 1."), "id");
+ modbusServerAddressOption.setDefaultValue("1");
+ parser.addOption(modbusServerAddressOption);
+
+ QCommandLineOption registerTypeOption(QStringList() << "t" << "type", QString("The type of the modbus register. Default is holding."), "input, holding, discrete, coils");
+ registerTypeOption.setDefaultValue("holding");
+ parser.addOption(registerTypeOption);
+
+ QCommandLineOption registerOption(QStringList() << "r" << "register", QString("The number of the modbus register."), "register");
+ parser.addOption(registerOption);
+
+ QCommandLineOption lengthOption(QStringList() << "l" << "length", QString("The number of registers to read. Default is 1."), "length");
+ lengthOption.setDefaultValue("1");
+ parser.addOption(lengthOption);
+
+ QCommandLineOption debugOption(QStringList() << "d" << "debug", QString("Print more information."));
+ parser.addOption(debugOption);
+
+ parser.process(application);
+
+ bool verbose = parser.isSet(debugOption);
+ if (verbose) qDebug() << "Verbose debug print enabled";
+
+ QModbusDataUnit::RegisterType registerType = QModbusDataUnit::RegisterType::Invalid;
+ QString registerTypeString = parser.value(registerTypeOption);
+ if (registerTypeString.toLower() == "input") {
+ registerType = QModbusDataUnit::RegisterType::InputRegisters;
+ } else if (registerTypeString.toLower() == "holding") {
+ registerType = QModbusDataUnit::RegisterType::HoldingRegisters;
+ } else if (registerTypeString.toLower() == "discrete") {
+ registerType = QModbusDataUnit::RegisterType::DiscreteInputs;
+ } else if (registerTypeString.toLower() == "coils") {
+ registerType = QModbusDataUnit::RegisterType::Coils;
+ } else {
+ qCritical() << "Invalid register type:" << parser.value(registerTypeOption) << "Please select on of the valid register types: input, holding, discrete, coils";
+ exit(EXIT_FAILURE);
+ }
+
+ bool valueOk = false;
+ quint16 modbusServerAddress = parser.value(modbusServerAddressOption).toUInt(&valueOk);
+ if (modbusServerAddress < 1 || !valueOk) {
+ qCritical() << "Error: Invalid modbus server address (slave ID):" << parser.value(modbusServerAddressOption);
+ exit(EXIT_FAILURE);
+ }
+
+ quint16 registerAddress = parser.value(registerOption).toUInt(&valueOk);
+ if (!valueOk) {
+ qCritical() << "Error: Invalid register number:" << parser.value(registerOption);
+ exit(EXIT_FAILURE);
+ }
+
+ quint16 length = parser.value(lengthOption).toUInt(&valueOk);
+ if (!valueOk) {
+ qCritical() << "Error: Invalid register length number:" << parser.value(lengthOption);
+ exit(EXIT_FAILURE);
+ }
+
+ if (!parser.isSet(addressOption)) {
+ qWarning() << "Error: please specify the IP address of the modbus TCP server you want connect to. Modbus RTU is not implemented yet so you need to specify it.";
+ exit(EXIT_FAILURE);
+ }
+
+ // TCP connection
+ QHostAddress address = QHostAddress(parser.value(addressOption));
+ if (address.isNull()) {
+ qCritical() << "Error: Invalid address:" << parser.value(addressOption);
+ exit(EXIT_FAILURE);
+ }
+
+ quint16 port = parser.value(portOption).toUInt();
+
+ qInfo() << "Connecting to" << QString("%1:%2").arg(address.toString()).arg(port) << "Modbus server address:" << modbusServerAddress;
+ QModbusTcpClient *client = new QModbusTcpClient(nullptr);
+ client->setConnectionParameter(QModbusDevice::NetworkAddressParameter, address.toString());
+ client->setConnectionParameter(QModbusDevice::NetworkPortParameter, port);
+ client->setTimeout(3000);
+ client->setNumberOfRetries(3);
+
+ QObject::connect(client, &QModbusTcpClient::stateChanged, &application, [=](QModbusDevice::State state){
+ if (verbose) qDebug() << "Connection state changed" << state;
+ if (state == QModbusDevice::ConnectedState) {
+ qDebug() << "Connected successfully to" << QString("%1:%2").arg(address.toString()).arg(port);
+
+ qDebug() << "Start reading from modbus server address" << modbusServerAddress << registerTypeString << "register:" << registerAddress << "Length:" << length;
+ QModbusDataUnit request = QModbusDataUnit(registerType, registerAddress, length);
+ QModbusReply *reply = client->sendReadRequest(request, modbusServerAddress);
+ if (!reply) {
+ qCritical() << "Failed to read register" << client->errorString();
+ exit(EXIT_FAILURE);
+ }
+
+ if (reply->isFinished()) {
+ reply->deleteLater(); // broadcast replies return immediately
+ qCritical() << "Reply finished immediatly. Something might have gone wrong:" << reply->errorString();
+ exit(EXIT_FAILURE);
+ }
+
+ QObject::connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
+ QObject::connect(reply, &QModbusReply::finished, client, [=]() {
+ if (reply->error() != QModbusDevice::NoError) {
+ qCritical() << "Reply finished with error:" << reply->errorString();
+ exit(EXIT_FAILURE);
+ }
+
+ const QModbusDataUnit unit = reply->result();
+ for (uint i = 0; i < unit.valueCount(); i++) {
+ quint16 registerValue = unit.values().at(i);
+ quint16 registerNumber = unit.startAddress() + i;
+ qInfo() << "-->" << registerNumber << ":" << QString("0x%1").arg(registerValue, 4, 16, QLatin1Char('0')) << registerValue;
+ }
+ });
+
+ QObject::connect(reply, &QModbusReply::errorOccurred, client, [=] (QModbusDevice::Error error){
+ qCritical() << "Modbus reply error occurred:" << error << reply->errorString();
+ });
+ }
+ });
+
+ QObject::connect(client, &QModbusTcpClient::errorOccurred, &application, [=](QModbusDevice::Error error){
+ qWarning() << "Modbus error occurred:" << error << client->errorString();
+ exit(EXIT_FAILURE);
+ });
+
+ if (!client->connectDevice()) {
+ qWarning() << "Error: could not connect to" << QString("%1:%2").arg(address.toString()).arg(port);
+ exit(EXIT_FAILURE);
+ }
+
+ return application.exec();
+}
diff --git a/nymea-modbus-cli/nymea-modbus-cli.pro b/nymea-modbus-cli/nymea-modbus-cli.pro
new file mode 100644
index 0000000..05eb40e
--- /dev/null
+++ b/nymea-modbus-cli/nymea-modbus-cli.pro
@@ -0,0 +1,22 @@
+TARGET = nymea-modbus-cli
+
+QT += network serialport serialbus
+QT -= gui
+
+CONFIG += c++11 console
+CONFIG -= app_bundle
+
+QMAKE_CXXFLAGS *= -Werror -std=c++11 -g
+QMAKE_LFLAGS *= -std=c++11
+
+gcc {
+ COMPILER_VERSION = $$system($$QMAKE_CXX " -dumpversion")
+ COMPILER_MAJOR_VERSION = $$str_member($$COMPILER_VERSION)
+ greaterThan(COMPILER_MAJOR_VERSION, 7): QMAKE_CXXFLAGS += -Wno-deprecated-copy
+}
+
+SOURCES += \
+ main.cpp
+
+target.path = $$[QT_INSTALL_PREFIX]/bin
+INSTALLS += target
diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro
index 63ea758..26b3e53 100644
--- a/nymea-plugins-modbus.pro
+++ b/nymea-plugins-modbus.pro
@@ -2,7 +2,7 @@ TEMPLATE = subdirs
# Note: In the loop at the end of this file the plugin
# dependency on the libs will be defined
-SUBDIRS += libnymea-modbus libnymea-sunspec
+SUBDIRS += nymea-modbus-cli libnymea-modbus libnymea-sunspec
PLUGIN_DIRS = \
alphainnotec \