diff --git a/.gitignore b/.gitignore index d73d76c..9e237a7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ builddir doc/html *.qm +__pycache__ diff --git a/debian/control b/debian/control index 0ab3eb2..a9eea5a 100644 --- a/debian/control +++ b/debian/control @@ -2,6 +2,7 @@ Source: nymea-plugins-modbus Section: utils Priority: options Maintainer: nymea GmbH +Standards-Version: 3.9.3 Build-depends: debhelper (>= 9.0.0), libnymea-dev (>= 0.17), libnymea-gpio-dev, @@ -10,8 +11,8 @@ Build-depends: debhelper (>= 9.0.0), nymea-dev-tools:native, pkg-config, qtbase5-dev, - libi2c-dev -Standards-Version: 3.9.3 + libi2c-dev, + python3:native Package: libnymea-sunspec1 @@ -22,6 +23,7 @@ Depends: ${shlibs:Depends}, Description: nymea.io sunspec library This package contains the nymea sunspec library. + Package: libnymea-sunspec-dev Section: libdevel Architecture: any @@ -34,6 +36,30 @@ Depends: ${shlibs:Depends}, Description: The main libraries and header files for developing with nymea sunspec. This package contains the nymea sunspec library - development files. + +Package: libnymea-modbus +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends} +Description: nymea modbus integration plugins library + This package contains the nymea modbus library for integration plugins. + + +Package: libnymea-modbus-dev +Section: libdevel +Architecture: any +Multi-Arch: same +Depends: ${shlibs:Depends}, + ${misc:Depends}, + libnymea-modbus (= ${binary:Version}), + pkg-config, + qtbase5-dev, + python3, +Description: The main libraries and header files for developing with modbus based nymea integration plugins. + This package contains the nymea modbus integration plugin library - development files. + + Package: nymea-plugin-alphainnotec Architecture: any Section: libs diff --git a/debian/libnymea-modbus-dev.install.in b/debian/libnymea-modbus-dev.install.in new file mode 100644 index 0000000..d743d89 --- /dev/null +++ b/debian/libnymea-modbus-dev.install.in @@ -0,0 +1,4 @@ +usr/lib/@DEB_HOST_MULTIARCH@/libnymea-modbus.so +usr/include/nymea-modbus/ +usr/lib/@DEB_HOST_MULTIARCH@/pkgconfig/nymea-modbus.pc + diff --git a/debian/libnymea-modbus.install.in b/debian/libnymea-modbus.install.in new file mode 100644 index 0000000..a180f98 --- /dev/null +++ b/debian/libnymea-modbus.install.in @@ -0,0 +1,4 @@ +usr/lib/@DEB_HOST_MULTIARCH@/libnymea-modbus.so.1 +usr/lib/@DEB_HOST_MULTIARCH@/libnymea-modbus.so.1.0 +usr/lib/@DEB_HOST_MULTIARCH@/libnymea-modbus.so.1.0.0 + diff --git a/debian/rules b/debian/rules index 7a8679f..dcdbe2b 100755 --- a/debian/rules +++ b/debian/rules @@ -21,5 +21,6 @@ override_dh_auto_clean: dh_auto_clean find -name *plugininfo.h -exec rm {} \; find -name *.qm -exec rm {} \; + find -name "autogenerated" -type d -exec rm -rvf {} + rm -rf $(PREPROCESS_FILES:.in=) diff --git a/libnymea-modbus/libnymea-modbus.pro b/libnymea-modbus/libnymea-modbus.pro new file mode 100644 index 0000000..61d8b3b --- /dev/null +++ b/libnymea-modbus/libnymea-modbus.pro @@ -0,0 +1,57 @@ +QMAKE_CXXFLAGS += -Werror -std=c++11 -z defs +QMAKE_LFLAGS += -std=c++11 -z defs + +QT += network serialbus + +CONFIG += link_pkgconfig +PKGCONFIG += nymea + +TARGET = nymea-modbus +TEMPLATE = lib + +gcc { + COMPILER_VERSION = $$system($$QMAKE_CXX " -dumpversion") + COMPILER_MAJOR_VERSION = $$str_member($$COMPILER_VERSION) + greaterThan(COMPILER_MAJOR_VERSION, 7): QMAKE_CXXFLAGS += -Wno-deprecated-copy +} + +HEADERS += \ + modbusdatautils.h \ + modbustcpmaster.h + +SOURCES += \ + modbusdatautils.cpp \ + modbustcpmaster.cpp + + +# define install target +target.path = $$[QT_INSTALL_LIBS] +INSTALLS += target + +# install modbustool for external plugins +modbustoolpri.files = modbus-tool.pri +modbustoolpri.path = $$[QT_INSTALL_PREFIX]/include/nymea-modbus/ +modbustool.files = tools/generate-connection.py +modbustool.path = $$[QT_INSTALL_PREFIX]/include/nymea-modbus/tools/ +modbustoolmodules.files = tools/connectiontool/*.py +modbustoolmodules.path = $$[QT_INSTALL_PREFIX]/include/nymea-modbus/tools/connectiontool/ +INSTALLS += modbustoolpri modbustool modbustoolmodules + +# install header file with relative subdirectory +for (header, HEADERS) { + path = $$[QT_INSTALL_PREFIX]/include/nymea-modbus/$${dirname(header)} + eval(headers_$${path}.files += $${header}) + eval(headers_$${path}.path = $${path}) + eval(INSTALLS *= headers_$${path}) +} + +# Create pkgconfig file +CONFIG += create_pc create_prl no_install_prl +QMAKE_PKGCONFIG_NAME = libnymea-modbus +QMAKE_PKGCONFIG_DESCRIPTION = nymea modbus integrations development library +QMAKE_PKGCONFIG_PREFIX = $$[QT_INSTALL_PREFIX] +QMAKE_PKGCONFIG_INCDIR = $$[QT_INSTALL_PREFIX]/include/nymea-modbus/ +QMAKE_PKGCONFIG_LIBDIR = $$target.path +QMAKE_PKGCONFIG_VERSION = 1.0.0 +QMAKE_PKGCONFIG_FILE = nymea-modbus +QMAKE_PKGCONFIG_DESTDIR = pkgconfig diff --git a/libnymea-modbus/modbus-tool.pri b/libnymea-modbus/modbus-tool.pri new file mode 100644 index 0000000..c507b6f --- /dev/null +++ b/libnymea-modbus/modbus-tool.pri @@ -0,0 +1,56 @@ +# Copyright 2013 - 2021, 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 + +# This project include file is meant to be used by nymea modbus integration plugins. +# For external plugins you can generate connection by including this project like following: + +# # Generate modbus connection +# MODBUS_CONNECTIONS += modbus-registers.json +# MODBUS_TOOLS_CONFIG += VERBOSE +# include($$[QT_INSTALL_PREFIX]/include/nymea-modbus/modbus-tool.pri) + +# On each qmake run the classes will be generated in the build directory. + +GENERATE_MODBUS_CONNECTION_BINARY=$${PWD}/tools/generate-connection.py + +for(registerDefinition, MODBUS_CONNECTIONS) { + contains(MODBUS_TOOLS_CONFIG, VERBOSE) { + message("Generating modbus connection class for $${registerDefinition} (verbose)") + system(python3 $${GENERATE_MODBUS_CONNECTION_BINARY} -j $${_PRO_FILE_PWD_}/$${registerDefinition} -o $${OUT_PWD}/autogenerated -v) + } else { + message("Generating class for $${registerDefinition}") + system(python3 $${GENERATE_MODBUS_CONNECTION_BINARY} -j $${_PRO_FILE_PWD_}/$${registerDefinition} -o $${OUT_PWD}/autogenerated) + } +} + +# Add all generated pri files to the project +MODBUS_CONNECTIONS_INCLUDES = $$files($${OUT_PWD}/autogenerated/*.pri) +for(MODBUS_CONNECTION, MODBUS_CONNECTIONS_INCLUDES) { + message("Adding generated connection to project $${MODBUS_CONNECTION}") + include($${MODBUS_CONNECTION}) + INCLUDEPATH += $${OUT_PWD}/autogenerated +} + diff --git a/modbus/modbusdatautils.cpp b/libnymea-modbus/modbusdatautils.cpp similarity index 100% rename from modbus/modbusdatautils.cpp rename to libnymea-modbus/modbusdatautils.cpp diff --git a/modbus/modbusdatautils.h b/libnymea-modbus/modbusdatautils.h similarity index 100% rename from modbus/modbusdatautils.h rename to libnymea-modbus/modbusdatautils.h diff --git a/modbus/modbustcpmaster.cpp b/libnymea-modbus/modbustcpmaster.cpp similarity index 87% rename from modbus/modbustcpmaster.cpp rename to libnymea-modbus/modbustcpmaster.cpp index 880f4c6..8c6a448 100644 --- a/modbus/modbustcpmaster.cpp +++ b/libnymea-modbus/modbustcpmaster.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -30,8 +30,7 @@ #include "modbustcpmaster.h" -#include -NYMEA_LOGGING_CATEGORY(dcModbusTCP, "ModbusTCP") +Q_LOGGING_CATEGORY(dcModbusTcpMaster, "ModbusTcpMaster") ModbusTCPMaster::ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QObject *parent) : QObject(parent), @@ -91,7 +90,7 @@ bool ModbusTCPMaster::connectDevice() { // Only connect if we are in the unconnected state if (m_modbusTcpClient->state() == QModbusDevice::UnconnectedState) { - qCDebug(dcModbusTCP()) << "Connecting modbus TCP client to" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); + qCDebug(dcModbusTcpMaster()) << "Connecting modbus TCP client to" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_port); m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString()); m_modbusTcpClient->setTimeout(m_timeout); @@ -101,7 +100,7 @@ bool ModbusTCPMaster::connectDevice() { // Restart the timer in case of connecting not finished yet or closing m_reconnectTimer->start(); } else { - qCWarning(dcModbusTCP()) << "Connect modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port) << "called, but the socket is currently in the" << m_modbusTcpClient->state(); + qCWarning(dcModbusTcpMaster()) << "Connect modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port) << "called, but the socket is currently in the" << m_modbusTcpClient->state(); } return false; @@ -119,7 +118,7 @@ void ModbusTCPMaster::disconnectDevice() bool ModbusTCPMaster::reconnectDevice() { - qCWarning(dcModbusTCP()) << "Reconnecting modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); + qCWarning(dcModbusTcpMaster()) << "Reconnecting modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); if (!m_modbusTcpClient) return false; @@ -184,12 +183,12 @@ QUuid ModbusTCPMaster::readCoil(uint slaveAddress, uint registerAddress, uint si emit receivedCoil(reply->serverAddress(), modbusAddress, unit.values()); } else { emit readRequestExecuted(requestId, false); - qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); + qCWarning(dcModbusTcpMaster()) << "Read response error:" << reply->error(); } }); connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ - qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; + qCWarning(dcModbusTcpMaster()) << "Modbus reply error:" << error; emit readRequestError(requestId, reply->errorString()); emit reply->finished(); // To make sure it will be deleted }); @@ -200,7 +199,7 @@ QUuid ModbusTCPMaster::readCoil(uint slaveAddress, uint registerAddress, uint si return QUuid(); } } else { - qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); + qCWarning(dcModbusTcpMaster()) << "Read error: " << m_modbusTcpClient->errorString(); return QUuid(); } return requestId; @@ -226,13 +225,13 @@ QUuid ModbusTCPMaster::writeHoldingRegisters(uint slaveAddress, uint registerAdd emit receivedHoldingRegister(reply->serverAddress(), modbusAddress, unit.values()); } else { emit writeRequestExecuted(requestId, false); - qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); + qCWarning(dcModbusTcpMaster()) << "Read response error:" << reply->error(); } reply->deleteLater(); }); connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ - qCWarning(dcModbusTCP()) << "Modbus replay error:" << error; + qCWarning(dcModbusTcpMaster()) << "Modbus replay error:" << error; emit writeRequestError(requestId, reply->errorString()); emit reply->finished(); // To make sure it will be deleted }); @@ -243,7 +242,7 @@ QUuid ModbusTCPMaster::writeHoldingRegisters(uint slaveAddress, uint registerAdd return QUuid(); } } else { - qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); + qCWarning(dcModbusTcpMaster()) << "Read error: " << m_modbusTcpClient->errorString(); return QUuid(); } return requestId; @@ -289,12 +288,12 @@ QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress emit receivedDiscreteInput(reply->serverAddress(), modbusAddress, unit.values()); } else { emit readRequestExecuted(requestId, false); - qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); + qCWarning(dcModbusTcpMaster()) << "Read response error:" << reply->error(); } }); connect(reply, &QModbusReply::errorOccurred, this, [requestId, this] (QModbusDevice::Error error){ - qCWarning(dcModbusTCP()) << "Modbus replay error:" << error; + qCWarning(dcModbusTcpMaster()) << "Modbus replay error:" << error; QModbusReply *reply = qobject_cast(sender()); emit readRequestError(requestId, reply->errorString()); emit reply->finished(); // To make sure it will be deleted @@ -306,7 +305,7 @@ QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress return QUuid(); } } else { - qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); + qCWarning(dcModbusTcpMaster()) << "Read error: " << m_modbusTcpClient->errorString(); return QUuid(); } return requestId; @@ -333,12 +332,12 @@ QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress emit receivedInputRegister(reply->serverAddress(), modbusAddress, unit.values()); } else { emit readRequestExecuted(requestId, false); - qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); + qCWarning(dcModbusTcpMaster()) << "Read response error:" << reply->error(); } }); connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ - qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; + qCWarning(dcModbusTcpMaster()) << "Modbus reply error:" << error; emit readRequestError(requestId, reply->errorString()); emit reply->finished(); // To make sure it will be deleted }); @@ -350,7 +349,7 @@ QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress return QUuid(); } } else { - qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); + qCWarning(dcModbusTcpMaster()) << "Read error: " << m_modbusTcpClient->errorString(); return QUuid(); } return requestId; @@ -378,7 +377,7 @@ QUuid ModbusTCPMaster::readHoldingRegister(uint slaveAddress, uint registerAddre } else { emit writeRequestExecuted(requestId, false); - qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); + qCWarning(dcModbusTcpMaster()) << "Read response error:" << reply->error(); emit readRequestError(requestId, reply->errorString()); } reply->deleteLater(); @@ -386,7 +385,7 @@ QUuid ModbusTCPMaster::readHoldingRegister(uint slaveAddress, uint registerAddre connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ - qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; + qCWarning(dcModbusTcpMaster()) << "Modbus reply error:" << error; emit readRequestError(requestId, reply->errorString()); emit reply->finished(); // To make sure it will be deleted }); @@ -397,7 +396,7 @@ QUuid ModbusTCPMaster::readHoldingRegister(uint slaveAddress, uint registerAddre return QUuid(); } } else { - qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); + qCWarning(dcModbusTcpMaster()) << "Read error: " << m_modbusTcpClient->errorString(); return QUuid(); } return requestId; @@ -431,13 +430,13 @@ QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const } else { emit writeRequestExecuted(requestId, false); - qCWarning(dcModbusTCP()) << "Write response error:" << reply->error(); + qCWarning(dcModbusTcpMaster()) << "Write response error:" << reply->error(); } reply->deleteLater(); }); connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ - qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; + qCWarning(dcModbusTcpMaster()) << "Modbus reply error:" << error; emit writeRequestError(requestId, reply->errorString()); emit reply->finished(); // To make sure it will be deleted }); @@ -448,7 +447,7 @@ QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const return QUuid(); } } else { - qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); + qCWarning(dcModbusTcpMaster()) << "Read error: " << m_modbusTcpClient->errorString(); return QUuid(); } return requestId; @@ -461,12 +460,12 @@ QUuid ModbusTCPMaster::writeHoldingRegister(uint slaveAddress, uint registerAddr void ModbusTCPMaster::onModbusErrorOccurred(QModbusDevice::Error error) { - qCWarning(dcModbusTCP()) << "An error occured" << error; + qCWarning(dcModbusTcpMaster()) << "An error occured" << error; } void ModbusTCPMaster::onModbusStateChanged(QModbusDevice::State state) { - qCDebug(dcModbusTCP()) << "Connection state changed for" << m_hostAddress << state; + qCDebug(dcModbusTcpMaster()) << "Connection state changed for" << m_hostAddress << state; bool connected = (state == QModbusDevice::ConnectedState); if (m_connected != connected) { m_connected = connected; diff --git a/modbus/modbustcpmaster.h b/libnymea-modbus/modbustcpmaster.h similarity index 97% rename from modbus/modbustcpmaster.h rename to libnymea-modbus/modbustcpmaster.h index 7857922..4b456ee 100644 --- a/modbus/modbustcpmaster.h +++ b/libnymea-modbus/modbustcpmaster.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -31,11 +31,14 @@ #ifndef MODBUSTCPMASTER_H #define MODBUSTCPMASTER_H +#include +#include #include #include #include -#include -#include +#include + +Q_DECLARE_LOGGING_CATEGORY(dcModbusTcpMaster) class ModbusTCPMaster : public QObject { @@ -112,6 +115,7 @@ signals: void receivedDiscreteInput(uint slaveAddress, uint modbusRegister, const QVector &values); void receivedHoldingRegister(uint slaveAddress, uint modbusRegister, const QVector &values); void receivedInputRegister(uint slaveAddress, uint modbusRegister, const QVector &values); + }; #endif // MODBUSTCPMASTER_H diff --git a/modbus/tools/README.md b/libnymea-modbus/tools/README.md similarity index 85% rename from modbus/tools/README.md rename to libnymea-modbus/tools/README.md index e1e2e1e..939fd4a 100644 --- a/modbus/tools/README.md +++ b/libnymea-modbus/tools/README.md @@ -14,7 +14,7 @@ The class will provide 2 main methods for fetching information from the modbus d * `initialize()` will read all registers with `"readSchedule": "init"` and emits the signal `initializationFinished()` once all replies returned. * `update()` can be used to update all registers with `"readSchedule": "update"`. The class will then fetch each register and update the specified value internally. If the value has changed, the `Changed()` signal will be emitted. -The reulting class will inhert from the `ModbusTCPMaster` class, providing easy access to all possible modbus operations and inform about the connected state. +The resulting class will inhert from the `ModbusTCPMaster` class, providing easy access to all possible modbus operations and inform about the connected state. # JSON format @@ -23,7 +23,8 @@ The basic structure of the modbus register JSON looks like following example: ``` { - "protocol": "TCP", + "className": "MyConnection", + "protocols": [ "TCP" ], "endianness": "BigEndian", "enums": [ { @@ -65,11 +66,65 @@ The basic structure of the modbus register JSON looks like following example: "access": "RO" }, ... + ], + "blocks": [ + { + "id": "blockName", + "readSchedule": "update", + "registers": [ + { + "id": "registerOne", + "address": 0, + "size": 2, + ... + }, + { + "id": "registerOne", + "address": 0, + "size": 2, + ... + }, + ... + ] + } ] } ``` +## Class name + +If no name class name has been passed to the generator script, the classname defined in the JSON file will be used. + +The naming convention for the classname and the resulting source code files looks like this: + +The class will be defined as + + * `Connection`. + +The source code files will be calld: + + * `classnameprotocolconnection.h` + * `classnameprotocolconnection.cpp` + + + +## Protocol + +Depending on the communication protocol, a different base class will be used for the resulting output class. + +There are 2 protocol types: + +* `RTU`: a communication based on the RS485 serial RTU transport protocol +* `TCP`: a communication based on the TCP transport protocol + +If the modbus device supports both protocols and you want to generate a class for each protocol you can set the protocol to `BOTH` and a class for `RTU` and one for `TCP` will be generated. + + ... + "protocol": "TCP", + ... + + ## Endianness When converting multiple registers to one data type (i.e. 2 registers uint16 values to one uint32), the order of the registers are important to align with the endianness of the data receiving. @@ -79,15 +134,6 @@ There are 2 possibilities: * `BigEndian`: default if not specified: register bytes come in following order `[0, 1, 2, 3]`: `ABCD` * `LittleEndian`: register bytes come in following order `[0, 1, 2, 3]`: `CDAB` -## Protocol - -Depending on the communication protocol, a different base class will be used for the resulting output class. - -There are 2 possibilities: - -* `RTU`: a communication based on the RS485 serial RTU transport protocol -* `TCP`: a communication based on the TCP transport protocol - ## Enums Many modbus devices provide inforation using `Enums`, indicating a special state trough a defined list of values. If a register implements an enum, you can define it in the `enums` section. The `name` property defines the name of the enum, and the script will generate a c++ enum definition from this section. Each enum value will then be generated using ` = `. @@ -179,12 +225,12 @@ Example block: Change into your plugin sub directory. Assuming you wrote the registers.json file you can run now following command to generate your modbus class: -`$ python3 ../modbus/tools/generate-connection.py -j registers.json -o . -c MyModbusConnection` +`$ python3 ../modbus/tools/generate-connection.py -j registers.json -o . -c MyModbus` You the result will be a header and a source file called: -* `mymodbusconnection.h` -* `mymodbusconnection.cpp` +* `mymodbustcpconnection.h` +* `mymodbustcpconnection.cpp` You can include this class in your project and provide one connection per thing. diff --git a/modbus/tools/connectiontool/__init__.py b/libnymea-modbus/tools/connectiontool/__init__.py similarity index 100% rename from modbus/tools/connectiontool/__init__.py rename to libnymea-modbus/tools/connectiontool/__init__.py diff --git a/modbus/tools/connectiontool/modbusrtu.py b/libnymea-modbus/tools/connectiontool/modbusrtu.py similarity index 99% rename from modbus/tools/connectiontool/modbusrtu.py rename to libnymea-modbus/tools/connectiontool/modbusrtu.py index cdddf40..2d490c6 100644 --- a/modbus/tools/connectiontool/modbusrtu.py +++ b/libnymea-modbus/tools/connectiontool/modbusrtu.py @@ -14,6 +14,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +import logging + from .toolcommon import * ############################################################## @@ -63,7 +65,7 @@ def writePropertyGetSetMethodImplementationsRtu(fileDescriptor, className, regis elif registerDefinition['registerType'] == 'coils': writeLine(fileDescriptor, ' return m_modbusRtuMaster->writeCoils(m_slaveId, %s, values);' % (registerDefinition['address'])) else: - print('Error: invalid register type for writing.') + logger.warning('Error: invalid register type for writing.') exit(1) writeLine(fileDescriptor, '}') @@ -222,7 +224,7 @@ def writeInternalBlockReadMethodDeclarationsRtu(fileDescriptor, blockDefinitions writeLine(fileDescriptor, ' - %s [%s] - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['unit'], registerDefinition['address'], registerDefinition['size'])) else: writeLine(fileDescriptor, ' -- %s - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' */ ' ) + writeLine(fileDescriptor, ' */' ) writeLine(fileDescriptor, ' ModbusRtuReply *readBlock%s();' % (blockName[0].upper() + blockName[1:])) writeLine(fileDescriptor) diff --git a/modbus/tools/connectiontool/modbustcp.py b/libnymea-modbus/tools/connectiontool/modbustcp.py similarity index 99% rename from modbus/tools/connectiontool/modbustcp.py rename to libnymea-modbus/tools/connectiontool/modbustcp.py index aef6b00..1fbd057 100644 --- a/modbus/tools/connectiontool/modbustcp.py +++ b/libnymea-modbus/tools/connectiontool/modbustcp.py @@ -63,7 +63,7 @@ def writePropertyGetSetMethodImplementationsTcp(fileDescriptor, className, regis elif registerDefinition['registerType'] == 'coils': writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, %s, values.count());' % (registerDefinition['address'])) else: - print('Error: invalid register type for writing.') + logger.warning('Error: invalid register type for writing.') exit(1) writeLine(fileDescriptor, ' request.setValues(values);') @@ -218,7 +218,7 @@ def writeInternalBlockReadMethodDeclarationsTcp(fileDescriptor, blockDefinitions writeLine(fileDescriptor, ' - %s [%s] - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['unit'], registerDefinition['address'], registerDefinition['size'])) else: writeLine(fileDescriptor, ' - %s - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' */ ' ) + writeLine(fileDescriptor, ' */' ) writeLine(fileDescriptor, ' QModbusReply *readBlock%s();' % (blockName[0].upper() + blockName[1:])) writeLine(fileDescriptor) diff --git a/modbus/tools/connectiontool/toolcommon.py b/libnymea-modbus/tools/connectiontool/toolcommon.py similarity index 94% rename from modbus/tools/connectiontool/toolcommon.py rename to libnymea-modbus/tools/connectiontool/toolcommon.py index caccae2..66cac9c 100644 --- a/modbus/tools/connectiontool/toolcommon.py +++ b/libnymea-modbus/tools/connectiontool/toolcommon.py @@ -19,8 +19,10 @@ import re import sys import json import shutil -import argparse import datetime +import logging + +logger = logging.getLogger('modbus-tools') def convertToAlphaNumeric(text): finalText = '' @@ -40,7 +42,7 @@ def convertToCamelCase(text, capitalize = False): s = convertToAlphaNumeric(text) s = s.replace("-", " ").replace("_", " ") words = s.split() - #print('--> words', words) + logger.debug('--> words', words) finalWords = [] for i in range(len(words)): @@ -48,7 +50,7 @@ def convertToCamelCase(text, capitalize = False): if len(camelCaseSplit) == 0: finalWords.append(words[i]) else: - #print('--> camel split words', camelCaseSplit) + logging.debug('Camel calse split words', camelCaseSplit) for j in range(len(camelCaseSplit)): finalWords.append(camelCaseSplit[j]) @@ -60,12 +62,12 @@ def convertToCamelCase(text, capitalize = False): finalText = finalWords[0].capitalize() + ''.join(i.capitalize() for i in finalWords[1:]) else: finalText = finalWords[0].lower() + ''.join(i.capitalize() for i in finalWords[1:]) - #print('Convert camel case:', text, '-->', finalText) + logging.debug('Convert camel case:', text, '-->', finalText) return finalText def loadJsonFile(filePath): - print('--> Loading JSON file', filePath) + logger.info('Loading JSON file %s', filePath) jsonFile = open(filePath, 'r') return json.load(jsonFile) @@ -117,7 +119,7 @@ def writeLicenseHeader(fileDescriptor): def writeRegistersEnum(fileDescriptor, registerJson): - print('Writing enum for all registers') + logger.debug('Writing enum for all registers') registerEnums = {} @@ -141,9 +143,9 @@ def writeRegistersEnum(fileDescriptor, registerJson): sortedRegistersKeys = sorted(registersKeys) sortedRegisterEnumList = [] - print('Sorted registers') + logger.debug('Sorted registers') for registerAddress in sortedRegistersKeys: - print('--> %s : %s' % (registerAddress, registerEnums[registerAddress])) + logger.debug('--> %s : %s' % (registerAddress, registerEnums[registerAddress])) enumData = {} enumData['key'] = registerEnums[registerAddress] enumData['value'] = registerAddress @@ -165,7 +167,7 @@ def writeRegistersEnum(fileDescriptor, registerJson): def writeEnumDefinition(fileDescriptor, enumDefinition): - print('Writing enum', enumDefinition) + logger.debug('Writing enum %s', enumDefinition) enumName = enumDefinition['name'] enumValues = enumDefinition['values'] writeLine(fileDescriptor, ' enum %s {' % enumName) @@ -379,21 +381,21 @@ def validateBlocks(blockDefinitions): previouseRegisterSize = blockRegisters[i - 1]['size'] previouseRegisterType = blockRegisters[i - 1]['registerType'] if previouseRegisterAddress + previouseRegisterSize != blockRegister['address']: - print('Error: block %s has invalid register order in register %s. There seems to be a gap between the registers.' % (blockName, blockRegister['id'])) + logger.warning('Error: block %s has invalid register order in register %s. There seems to be a gap between the registers.' % (blockName, blockRegister['id'])) exit(1) if blockRegister['access'] != registerAccess: - print('Error: block %s has inconsistent register access in register %s. The block registers dont seem to have the same access rights.' % (blockName, blockRegister['id'])) + logger.warning('Error: block %s has inconsistent register access in register %s. The block registers dont seem to have the same access rights.' % (blockName, blockRegister['id'])) exit(1) if blockRegister['registerType'] != registerType: - print('Error: block %s has inconsistent register type in register %s. The block registers dont seem to be from the same type.' % (blockName, blockRegister['id'])) + logger.warning('Error: block %s has inconsistent register type in register %s. The block registers dont seem to be from the same type.' % (blockName, blockRegister['id'])) exit(1) registerCount += 1 blockSize += blockRegister['size'] - print('Define valid block \"%s\" starting at %s with length %s containing %s properties to read.' % (blockName, blockStartAddress, blockSize, registerCount)) + logger.debug('Define valid block \"%s\" starting at %s with length %s containing %s properties to read.' % (blockName, blockStartAddress, blockSize, registerCount)) def writeBlocksUpdateMethodDeclarations(fileDescriptor, blockDefinitions): @@ -418,7 +420,7 @@ def writeBlocksUpdateMethodDeclarations(fileDescriptor, blockDefinitions): writeLine(fileDescriptor, ' - %s [%s] - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['unit'], registerDefinition['address'], registerDefinition['size'])) else: writeLine(fileDescriptor, ' - %s - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' */ ' ) + writeLine(fileDescriptor, ' */' ) writeLine(fileDescriptor, ' void update%sBlock();' % (blockName[0].upper() + blockName[1:])) writeLine(fileDescriptor) diff --git a/libnymea-modbus/tools/examples/example-registers.json b/libnymea-modbus/tools/examples/example-registers.json new file mode 100644 index 0000000..3bb0b08 --- /dev/null +++ b/libnymea-modbus/tools/examples/example-registers.json @@ -0,0 +1,114 @@ +{ + "className": "Example", + "protocol": "BOTH", + "endianness": "BigEndian", + "enums": [ + { + "name": "NameOfEnum", + "values": [ + { + "key": "EnumValue1", + "value": 0 + }, + { + "key": "EnumValue2", + "value": 1 + } + ] + } + ], + "registers": [ + { + "id": "foo", + "address": 10, + "size": 2, + "type": "float", + "registerType": "inputRegister", + "readSchedule": "update", + "description": "Foo register", + "unit": "ValueUnit", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "bar", + "address": 20, + "size": 2, + "type": "float", + "registerType": "inputRegister", + "readSchedule": "update", + "description": "Bar register", + "unit": "ValueUnit", + "defaultValue": "0", + "access": "RO" + } + ], + "blocks": [ + { + "id": "testBlock", + "readSchedule": "update", + "registers": [ + { + "id": "A", + "address": 0, + "size": 2, + "type": "float", + "registerType": "inputRegister", + "readSchedule": "update", + "description": "A register", + "unit": "X", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "B", + "address": 2, + "size": 2, + "type": "float", + "registerType": "inputRegister", + "readSchedule": "update", + "description": "B register", + "unit": "X", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "C", + "address": 4, + "size": 2, + "type": "float", + "registerType": "inputRegister", + "readSchedule": "update", + "description": "C register", + "unit": "X", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "D", + "address": 6, + "size": 2, + "type": "float", + "registerType": "inputRegister", + "readSchedule": "update", + "description": "D register", + "unit": "X", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "E", + "address": 8, + "size": 2, + "type": "float", + "registerType": "inputRegister", + "readSchedule": "update", + "description": "E register", + "unit": "X", + "defaultValue": "0", + "access": "RO" + } + ] + } + ] +} diff --git a/modbus/tools/generate-connection.py b/libnymea-modbus/tools/generate-connection.py similarity index 82% rename from modbus/tools/generate-connection.py rename to libnymea-modbus/tools/generate-connection.py index a6314fd..0fc7c07 100644 --- a/modbus/tools/generate-connection.py +++ b/libnymea-modbus/tools/generate-connection.py @@ -25,13 +25,14 @@ import json import shutil import argparse import datetime +import logging from connectiontool.toolcommon import * from connectiontool.modbusrtu import * from connectiontool.modbustcp import * def writeTcpHeaderFile(): - print('Writing modbus TCP hader file %s' % headerFilePath) + logger.info('Writing modbus TCP header file %s' % headerFilePath) headerFile = open(headerFilePath, 'w') writeLicenseHeader(headerFile) @@ -40,8 +41,8 @@ def writeTcpHeaderFile(): writeLine(headerFile) writeLine(headerFile, '#include ') writeLine(headerFile) - writeLine(headerFile, '#include "../modbus/modbusdatautils.h"') - writeLine(headerFile, '#include "../modbus/modbustcpmaster.h"') + writeLine(headerFile, '#include ') + writeLine(headerFile, '#include ') writeLine(headerFile) @@ -153,12 +154,12 @@ def writeTcpHeaderFile(): def writeTcpSourceFile(): - print('Writing modbus TCP source file %s' % sourceFilePath) + logger.info('Writing modbus TCP source file %s' % sourceFilePath) sourceFile = open(sourceFilePath, 'w') writeLicenseHeader(sourceFile) writeLine(sourceFile) writeLine(sourceFile, '#include "%s"' % headerFileName) - writeLine(sourceFile, '#include "loggingcategories.h"') + writeLine(sourceFile, '#include ') writeLine(sourceFile) writeLine(sourceFile, 'NYMEA_LOGGING_CATEGORY(dc%s, "%s")' % (className, className)) writeLine(sourceFile) @@ -249,7 +250,7 @@ def writeTcpSourceFile(): ########################################################################################################## def writeRtuHeaderFile(): - print('Writing modbus TCP hader file %s' % headerFilePath) + logger.info('Writing modbus RTU header file %s' % headerFilePath) headerFile = open(headerFilePath, 'w') writeLicenseHeader(headerFile) @@ -258,7 +259,7 @@ def writeRtuHeaderFile(): writeLine(headerFile) writeLine(headerFile, '#include ') writeLine(headerFile) - writeLine(headerFile, '#include "../modbus/modbusdatautils.h"') + writeLine(headerFile, '#include ') writeLine(headerFile, '#include ') writeLine(headerFile) @@ -373,12 +374,12 @@ def writeRtuHeaderFile(): def writeRtuSourceFile(): - print('Writing modbus RTU source file %s' % sourceFilePath) + logger.info('Writing modbus RTU source file %s' % sourceFilePath) sourceFile = open(sourceFilePath, 'w') writeLicenseHeader(sourceFile) writeLine(sourceFile, '#include "%s"' % headerFileName) - writeLine(sourceFile, '#include "loggingcategories.h"') + writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') writeLine(sourceFile) writeLine(sourceFile, 'NYMEA_LOGGING_CATEGORY(dc%s, "%s")' % (className, className)) @@ -466,7 +467,6 @@ def writeRtuSourceFile(): writeLine(sourceFile, '{') writeLine(sourceFile, ' debug.nospace().noquote() << "%s(" << %s->modbusRtuMaster()->modbusUuid().toString() << ", " << %s->modbusRtuMaster()->serialPort() << ", slave ID:" << %s->slaveId() << ")" << "\\n";' % (className, debugObjectParamName, debugObjectParamName, debugObjectParamName)) writeRegistersDebugLine(sourceFile, debugObjectParamName, registerJson['registers']) - if 'blocks' in registerJson: for blockDefinition in registerJson['blocks']: writeRegistersDebugLine(sourceFile, debugObjectParamName, blockDefinition['registers']) @@ -482,35 +482,49 @@ def writeRtuSourceFile(): # Main ############################################################################################ +logger = logging.getLogger('modbus-tools') +logger.setLevel(logging.INFO) +ch = logging.StreamHandler(sys.stdout) +ch.setLevel(logging.INFO) +formatter = logging.Formatter('%(name)s: %(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) + parser = argparse.ArgumentParser(description='Generate modbus tcp connection class from JSON register definitions file.') parser.add_argument('-j', '--json', metavar='', help='The JSON file containing the register definitions.') parser.add_argument('-o', '--output-directory', metavar='', help='The output directory for the resulting class.') -parser.add_argument('-c', '--class-name', metavar='', help='The name of the resulting class.') +parser.add_argument('-v', '--verbose', dest='verboseOutput', action='store_true', help='More verbose output.') args = parser.parse_args() registerJson = loadJsonFile(args.json) scriptPath = os.path.dirname(os.path.realpath(sys.argv[0])) outputDirectory = os.path.realpath(args.output_directory) -className = args.class_name -headerFileName = className.lower() + '.h' -sourceFileName = className.lower() + '.cpp' +if not os.path.exists(outputDirectory): + logger.debug("Output directory does not exist. Creating directory %s", outputDirectory) + os.makedirs(outputDirectory) -headerFilePath = os.path.join(outputDirectory, headerFileName) -sourceFilePath = os.path.join(outputDirectory, sourceFileName) +if args.verboseOutput: + logger.setLevel(logging.DEBUG) + ch.setLevel(logging.DEBUG) -print('Scrip path: %s' % scriptPath) -print('Output directory: %s' % outputDirectory) -print('Class name: %s' % className) -print('Header file: %s' % headerFileName) -print('Source file: %s' % sourceFileName) -print('Header file path: %s' % headerFilePath) -print('Source file path: %s' % sourceFilePath) +logger.debug("Verbose output enabled") + +if not 'className' in registerJson: + logger.warning('Classname missing. Please specify the classname in the json file or pass it to the generatori using -c .') + exit(1) + +classNamePrefix = registerJson['className'] endianness = 'BigEndian' if 'endianness' in registerJson: endianness = registerJson['endianness'] +logger.debug('Scrip path: %s' % scriptPath) +logger.debug('Output directory: %s' % outputDirectory) +logger.debug('Class name prefix: %s' % classNamePrefix) +logger.debug('Endianness: %s' % endianness) + protocol = 'TCP' if 'protocol' in registerJson: protocol = registerJson['protocol'] @@ -518,12 +532,77 @@ if 'protocol' in registerJson: if 'blocks' in registerJson: validateBlocks(registerJson['blocks']) +# Create classes depending on the protocol +writeTcp = True +writeRtu = False + if protocol == 'TCP': + writeTcp = True + writeRtu = False +elif protocol == 'RTU': + writeTcp = False + writeRtu = True +else: + # Any other value generates both classes + writeTcp = True + writeRtu = True + +headerFiles = [] +sourceFiles = [] + +if writeTcp: + className = classNamePrefix + 'ModbusTcpConnection' + headerFileName = className.lower() + '.h' + headerFiles.append(headerFileName) + sourceFileName = className.lower() + '.cpp' + sourceFiles.append(sourceFileName) + + headerFilePath = os.path.join(outputDirectory, headerFileName) + sourceFilePath = os.path.join(outputDirectory, sourceFileName) + logger.debug('=======================================================') + logger.debug('Class name: %s' % className) + logger.debug('Header file: %s' % headerFileName) + logger.debug('Source file: %s' % sourceFileName) + logger.debug('Header file path: %s' % headerFilePath) + logger.debug('Source file path: %s' % sourceFilePath) writeTcpHeaderFile() writeTcpSourceFile() -else: + +if writeRtu: + className = classNamePrefix + 'ModbusRtuConnection' + headerFileName = className.lower() + '.h' + headerFiles.append(headerFileName) + sourceFileName = className.lower() + '.cpp' + sourceFiles.append(sourceFileName) + headerFilePath = os.path.join(outputDirectory, headerFileName) + sourceFilePath = os.path.join(outputDirectory, sourceFileName) + logger.debug('=======================================================') + logger.debug('Class name: %s' % className) + logger.debug('Header file: %s' % headerFileName) + logger.debug('Source file: %s' % sourceFileName) + logger.debug('Header file path: %s' % headerFilePath) + logger.debug('Source file path: %s' % sourceFilePath) writeRtuHeaderFile() writeRtuSourceFile() +# Write pri file +projectIncludeFileName = classNamePrefix.lower() + '.pri' +projectIncludeFilePath = os.path.join(outputDirectory, projectIncludeFileName) - +logger.info('Writing connection project include file %s' % projectIncludeFileName) +projectIncludeFile = open(projectIncludeFilePath, 'w') +writeLine(projectIncludeFile, '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #') +writeLine(projectIncludeFile, '#') +writeLine(projectIncludeFile, '# This file has been autogenerated.') +writeLine(projectIncludeFile, '# Any changes in this file may be overwritten from qmake.') +writeLine(projectIncludeFile, '#') +writeLine(projectIncludeFile, '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #') +writeLine(projectIncludeFile) +writeLine(projectIncludeFile, 'HEADERS = \\') +for generatedHeaderFileName in headerFiles: + writeLine(projectIncludeFile, ' $${PWD}/%s \\' % generatedHeaderFileName) + +writeLine(projectIncludeFile) +writeLine(projectIncludeFile, "SOURCES = \\") +for generatedSourceFileName in sourceFiles: + writeLine(projectIncludeFile, ' $${PWD}/%s \\' % generatedSourceFileName) diff --git a/modbus.pri b/modbus.pri new file mode 100644 index 0000000..840bcb4 --- /dev/null +++ b/modbus.pri @@ -0,0 +1,12 @@ +QT += network serialport serialbus + +top_srcdir=$$PWD +top_builddir=$$shadowed($$PWD) + +INCLUDEPATH += $$top_srcdir/libnymea-modbus +LIBS += -L$$top_builddir/libnymea-modbus/ -lnymea-modbus + +OTHER_FILES += $${MODBUS_CONNECTIONS} + +include(libnymea-modbus/modbus-tool.pri) + diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index 6124d5c..2e60263 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -2,7 +2,7 @@ TEMPLATE = subdirs # Note keep it ordered so the lib will be built first CONFIG += ordered -SUBDIRS += libnymea-sunspec +SUBDIRS += libnymea-modbus libnymea-sunspec PLUGIN_DIRS = \ alphainnotec \