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 \