Introduce libnymea-modbus

Improve tool and prepare autogeneration of connection classes
This commit is contained in:
Simon Stürz 2022-05-04 12:14:40 +02:00
parent 03c77ef3a3
commit 62f78f5e90
20 changed files with 496 additions and 89 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
builddir
doc/html
*.qm
__pycache__

30
debian/control vendored
View File

@ -2,6 +2,7 @@ Source: nymea-plugins-modbus
Section: utils
Priority: options
Maintainer: nymea GmbH <developer@nymea.io>
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

4
debian/libnymea-modbus-dev.install.in vendored Normal file
View File

@ -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

4
debian/libnymea-modbus.install.in vendored Normal file
View File

@ -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

1
debian/rules vendored
View File

@ -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=)

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
#
# 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
}

View File

@ -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 <loggingcategories.h>
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<QModbusReply *>(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;

View File

@ -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 <QUuid>
#include <QTimer>
#include <QObject>
#include <QHostAddress>
#include <QtSerialBus>
#include <QTimer>
#include <QUuid>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcModbusTcpMaster)
class ModbusTCPMaster : public QObject
{
@ -112,6 +115,7 @@ signals:
void receivedDiscreteInput(uint slaveAddress, uint modbusRegister, const QVector<quint16> &values);
void receivedHoldingRegister(uint slaveAddress, uint modbusRegister, const QVector<quint16> &values);
void receivedInputRegister(uint slaveAddress, uint modbusRegister, const QVector<quint16> &values);
};
#endif // MODBUSTCPMASTER_H

View File

@ -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 `<propertyName>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
* `<ClassName><Protocol>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 `<EnumName><EnumValueName> = <value>`.
@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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"
}
]
}
]
}

View File

@ -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 <QObject>')
writeLine(headerFile)
writeLine(headerFile, '#include "../modbus/modbusdatautils.h"')
writeLine(headerFile, '#include "../modbus/modbustcpmaster.h"')
writeLine(headerFile, '#include <modbusdatautils.h>')
writeLine(headerFile, '#include <modbustcpmaster.h>')
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 <loggingcategories.h>')
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 <QObject>')
writeLine(headerFile)
writeLine(headerFile, '#include "../modbus/modbusdatautils.h"')
writeLine(headerFile, '#include <modbusdatautils.h>')
writeLine(headerFile, '#include <hardware/modbus/modbusrtumaster.h>')
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 <loggingcategories.h>')
writeLine(sourceFile, '#include <math.h>')
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='<file>', help='The JSON file containing the register definitions.')
parser.add_argument('-o', '--output-directory', metavar='<directory>', help='The output directory for the resulting class.')
parser.add_argument('-c', '--class-name', metavar='<name>', 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)

12
modbus.pri Normal file
View File

@ -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)

View File

@ -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 \