From 1fb1c7ed8c806fdf8fb2f9f76e1c5f9cfeffbbb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 9 Dec 2021 10:28:29 +0100 Subject: [PATCH] Add first version of modbus tools --- modbus/modbusdatautils.cpp | 231 ++++++++++++ modbus/modbusdatautils.h | 101 +++++ modbus/tools/README.md | 129 +++++++ modbus/tools/generate-connection.py | 557 ++++++++++++++++++++++++++++ 4 files changed, 1018 insertions(+) create mode 100644 modbus/modbusdatautils.cpp create mode 100644 modbus/modbusdatautils.h create mode 100644 modbus/tools/README.md create mode 100644 modbus/tools/generate-connection.py diff --git a/modbus/modbusdatautils.cpp b/modbus/modbusdatautils.cpp new file mode 100644 index 0000000..ba1caea --- /dev/null +++ b/modbus/modbusdatautils.cpp @@ -0,0 +1,231 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "modbusdatautils.h" + +#include + +ModbusDataUtils::ModbusDataUtils() +{ + +} + +quint16 ModbusDataUtils::convertToUInt16(const QVector ®isters) +{ + Q_ASSERT_X(registers.count() == 1, "ModbusDataUtils", "invalid raw data size for converting value to quint16"); + return registers.at(0); +} + +qint16 ModbusDataUtils::convertToInt16(const QVector ®isters) +{ + Q_ASSERT_X(registers.count() == 1, "ModbusDataUtils", "invalid raw data size for converting value to qint16"); + return static_cast(registers.at(0)); +} + +quint32 ModbusDataUtils::convertToUInt32(const QVector ®isters) +{ + Q_ASSERT_X(registers.count() == 2, "ModbusDataUtils", "invalid raw data size for converting value to quint32"); + QByteArray data; + QDataStream inputStream(&data, QIODevice::WriteOnly); + inputStream << registers.at(1); + inputStream << registers.at(0); + + QDataStream outputStream(&data, QIODevice::ReadOnly); + quint32 result = 0; + outputStream >> result; + return result; +} + +qint32 ModbusDataUtils::convertToInt32(const QVector ®isters) +{ + Q_ASSERT_X(registers.count() == 2, "ModbusDataUtils", "invalid raw data size for converting value to quint32"); + QByteArray data; + QDataStream inputStream(&data, QIODevice::WriteOnly); + inputStream << registers.at(1); + inputStream << registers.at(0); + + QDataStream outputStream(&data, QIODevice::ReadOnly); + qint32 result = 0; + outputStream >> result; + return result; +} + +quint64 ModbusDataUtils::convertToUInt64(const QVector ®isters) +{ + Q_ASSERT_X(registers.count() == 4, "ModbusDataUtils", "invalid raw data size for converting value to quint64"); + QByteArray data; + QDataStream inputStream(&data, QIODevice::WriteOnly); + inputStream << registers.at(3); + inputStream << registers.at(2); + inputStream << registers.at(1); + inputStream << registers.at(0); + + QDataStream outputStream(&data, QIODevice::ReadOnly); + quint64 result = 0; + outputStream >> result; + return result; +} + +qint64 ModbusDataUtils::convertToInt64(const QVector ®isters) +{ + Q_ASSERT_X(registers.count() == 4, "ModbusDataUtils", "invalid raw data size for converting value to qint64"); + QByteArray data; + QDataStream inputStream(&data, QIODevice::WriteOnly); + inputStream << registers.at(3); + inputStream << registers.at(2); + inputStream << registers.at(1); + inputStream << registers.at(0); + + QDataStream outputStream(&data, QIODevice::ReadOnly); + qint64 result = 0; + outputStream >> result; + return result; +} + +QString ModbusDataUtils::convertToString(const QVector ®isters) +{ + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + for (int i = 0; i < registers.count(); i++) { + stream << registers.at(i); + } + + return QString::fromUtf8(bytes).trimmed(); +} + +float ModbusDataUtils::convertToFloat32(const QVector ®isters) +{ + Q_ASSERT_X(registers.count() == 2, "ModbusDataUtils", "invalid raw data size for converting value to float32"); + quint32 rawValue = ModbusDataUtils::convertToUInt32(registers); + float value = 0; + memcpy(&value, &rawValue, sizeof(quint32)); + return value; +} + +double ModbusDataUtils::convertToFloat64(const QVector ®isters) +{ + + Q_ASSERT_X(registers.count() == 4, "ModbusDataUtils", "invalid raw data size for converting value to float64"); + quint64 rawValue = ModbusDataUtils::convertToUInt64(registers); + double value = 0; + memcpy(&value, &rawValue, sizeof(quint64)); + return value; +} + +QVector ModbusDataUtils::convertFromUInt16(quint16 value) +{ + return QVector() << value; +} + +QVector ModbusDataUtils::convertFromInt16(qint16 value) +{ + return ModbusDataUtils::convertFromUInt16(static_cast(value)); +} + +QVector ModbusDataUtils::convertFromUInt32(quint32 value) +{ + QByteArray data; + QDataStream inputStream(&data, QIODevice::WriteOnly); + inputStream << value; + + QDataStream outputStream(&data, QIODevice::ReadOnly); + QVector values; + for (int i = 0; i < 2; i++) { + quint16 registerValue = 0; + outputStream >> registerValue; + values.prepend(registerValue); + } + return values; +} + +QVector ModbusDataUtils::convertFromInt32(qint32 value) +{ + return ModbusDataUtils::convertFromUInt32(static_cast(value)); +} + +QVector ModbusDataUtils::convertFromUInt64(quint64 value) +{ + QByteArray data; + QDataStream inputStream(&data, QIODevice::WriteOnly); + inputStream << value; + + QDataStream outputStream(&data, QIODevice::ReadOnly); + QVector values; + for (int i = 0; i < 4; i++) { + quint16 registerValue = 0; + outputStream >> registerValue; + values.prepend(registerValue); + } + return values; +} + +QVector ModbusDataUtils::convertFromInt64(qint64 value) +{ + QByteArray data; + QDataStream inputStream(&data, QIODevice::WriteOnly); + inputStream << value; + + QDataStream outputStream(&data, QIODevice::ReadOnly); + QVector values; + for (int i = 0; i < 4; i++) { + quint16 registerValue = 0; + outputStream >> registerValue; + values.prepend(registerValue); + } + return values; +} + +QVector ModbusDataUtils::convertFromString(const QString &value, quint16 stringLength) +{ + Q_ASSERT_X(value.length() <= stringLength, "ModbusDataUtils", "cannot convert a string which is bigger than the desired register vector."); + QByteArray data = value.toLatin1() + QByteArray('\0', stringLength - value.count()); + QDataStream stream(&data, QIODevice::ReadOnly); + QVector values; + for (int i = 0; i < stringLength; i++) { + quint16 registerValue = 0; + stream >> registerValue; + values.append(registerValue); + } + return values; +} + +QVector ModbusDataUtils::convertFromFloat32(float value) +{ + quint32 rawValue = 0; + memcpy(&rawValue, &value, sizeof(float)); + return ModbusDataUtils::convertFromUInt32(rawValue); +} + +QVector ModbusDataUtils::convertFromFloat64(double value) +{ + quint64 rawValue = 0; + memcpy(&rawValue, &value, sizeof(double)); + return ModbusDataUtils::convertFromUInt64(rawValue); +} diff --git a/modbus/modbusdatautils.h b/modbus/modbusdatautils.h new file mode 100644 index 0000000..f442dab --- /dev/null +++ b/modbus/modbusdatautils.h @@ -0,0 +1,101 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MODBUSDATAUTILS_H +#define MODBUSDATAUTILS_H + +#include +#include + +class ModbusDataUtils +{ + Q_GADGET +public: + enum Access { + AccessReadOnly, + AccessWriteOnly, + AccessReadWrite + }; + Q_ENUM(Access) + + enum DataType { + UInt8, + UInt16, + Uint32, + Uint64, + Int8, + Int16, + Int32, + Int64, + Float, + Float64, + String, + Bool + }; + Q_ENUM(DataType) + + typedef struct ModbusRegister { + quint16 address; + quint16 size; + DataType dataType; + Access access; + QString description; + QString unit; + QVector rawData; + } ModbusRegister; + + typedef QVector ModbusRegisters; + + explicit ModbusDataUtils(); + + // Convert to + static quint16 convertToUInt16(const QVector ®isters); + static qint16 convertToInt16(const QVector ®isters); + static quint32 convertToUInt32(const QVector ®isters); + static qint32 convertToInt32(const QVector ®isters); + static quint64 convertToUInt64(const QVector ®isters); + static qint64 convertToInt64(const QVector ®isters); + static QString convertToString(const QVector ®isters); + static float convertToFloat32(const QVector ®isters); + static double convertToFloat64(const QVector ®isters); + + // Convert from + static QVector convertFromUInt16(quint16 value); + static QVector convertFromInt16(qint16 value); + static QVector convertFromUInt32(quint32 value); + static QVector convertFromInt32(qint32 value); + static QVector convertFromUInt64(quint64 value); + static QVector convertFromInt64(qint64 value); + static QVector convertFromString(const QString &value, quint16 stringLength); + static QVector convertFromFloat32(float value); + static QVector convertFromFloat64(double value); +}; + +#endif // MODBUSDATAUTILS_H diff --git a/modbus/tools/README.md b/modbus/tools/README.md new file mode 100644 index 0000000..b8d4127 --- /dev/null +++ b/modbus/tools/README.md @@ -0,0 +1,129 @@ +# Generate a modbus read class + +In order to make the plugin development for modbus TCP devices much easier and faster, a small tool has been developed to generate a modbus TCP master based class providing get and set methods for the registers and property changed signals. + +The workflow looks like this: + +* Write the `registers.json` file containing all register information you are interested to. +* Run the script and provide the class name, output directory and the path to the JSON file +* Include the generated class in your plugin, connect the `Changed()` signal and update the thing state within the plugin. + + +The class will provide 2 main methods for fetching information from the modbus device: + +* `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. + + +# JSON format + +The basic structure of the modbus register JSON looks like following example: + +``` +{ + "enums": [ + { + "name": "NameOfEnum", + "values": [ + { + "key": "EnumValue1", + "value": 0 + }, + { + "key": "EnumValue2", + "value": 1 + }, + .... + ] + } + ], + "registers": [ + { + "id": "registerPropertyName", + "address": 4, + "size": 1, + "type": "uint16", + "readSchedule": "init", + "description": "Description of the register", + "unit": "V", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "registerWithEnumValues", + "address": 5, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "enum": "NameOfEnum", + "defaultValue": "NameOfEnumEnumValue1", + "description": "Description of the enum register like states", + "access": "RO" + }, + ... + ] +} + +``` + +## 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 ` = `. + +If a register represets an enum, you simply add the property `"enum": "NameOfEnum"` in the register map and the property will be defined using the resulting enum type. All convertion between enum and resulting modbus register value will be done automatically. + +## Registers + +Earch register will be defined as a property in the resulting class modbus TCP class providing easy access to the register data. + +* `id`: Mandatory. The id defines the name of the property used in the resulting class. +* `address`: Mandatory. The modbus address of the register. +* `size`: Mandatory. The amount of registers to read for the property. +* `type`: Mandatory. The data type of this property. Available data types are: + * `uint16` : will be converted to `quint16` + * `int16` : will be converted to `qint16` + * `uint32` : will be converted to `quint32` + * `int32` : will be converted to `qint32` + * `uint64` : will be converted to `quint64` + * `int64` : will be converted to `qint64` + * `float`: will be converted to `float` + * `float64`: will be converted to `double` + * `string` : will be converted to `QString` +* `readSchedule`: Optional. Defines when the register needs to be fetched. If no read schedule has been defined, the class will provide only the update methods, but will not read the value during `initialize()` or `update()` calls. Possible values are: + * `init`: The register will be fetched during initialization. Once all `init `registers have been fetched, the `initializationFinished()` signal will be emitted. + * `update`: The register will be feched each time the `update()` method will be called. +* `enum`: Optional: If the given data type represents an enum value, this propery can be set to the name of the used enum from the `enum` definition. The class will take care internally about the data convertion from and to the enum values. +* `description`: Mandatory. A clear description of the register. +* `unit`: Optional. Represents the unit of this register value. +* `registerType`: Optional. Represents the type of the register and how to read/write it. Default is `holdingRegister`. Possible values are: + * `holdingRegister` + * `inputRegister` + * `coils` + * `discreteInputs` +* `access`: Mandatory. Describes the access to this register. Possible valies are: + * `RO`: Read only access. Only the get method and the changed singal will be defined. + * `RW`: Read and write access. Also a set mehtod will be defined. + * `WO`: Write only. Only the set method will be defined. +* `scaleFactor`: Optional. The name of the scale factor register to convert this value to float. `floatValue = intValue * 10^scaleFactor value`. The scale factor value is normally a `int16` value, i.e. -10 or 10 +* `staticScaleFactor`: Optional. Use this static scale factor to convert this register value to float. `floatValue = registerValue * 10^staticScaleFactor`. The scale factor value is normally a `int16` value, i.e. -10 or 10 +* `defaultValue`: Optional. The value for initializing the property. + +# Example + +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` + +You the result will be a header and a source file called: + +* `mymodbusconnection.h` +* `mymodbusconnection.cpp` + +You can include this class in your project and provide one connection per thing. + + + + diff --git a/modbus/tools/generate-connection.py b/modbus/tools/generate-connection.py new file mode 100644 index 0000000..f6be585 --- /dev/null +++ b/modbus/tools/generate-connection.py @@ -0,0 +1,557 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2021 nymea GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# To lazy to type all those register plugins, let's make live much easier and generate code from a json register definition + +import os +import re +import sys +import json +import shutil +import argparse +import datetime + +def convertToAlphaNumeric(text): + finalText = '' + for character in text: + if character.isalnum(): + finalText += character + else: + finalText += ' ' + return finalText + + +def splitCamelCase(text): + return re.sub('([A-Z][a-z]+)', r' \1', re.sub('([A-Z]+)', r' \1', text)).split() + + +def convertToCamelCase(text, capitalize = False): + s = convertToAlphaNumeric(text) + s = s.replace("-", " ").replace("_", " ") + words = s.split() + #print('--> words', words) + finalWords = [] + + for i in range(len(words)): + camelCaseSplit = splitCamelCase(words[i]) + if len(camelCaseSplit) == 0: + finalWords.append(words[i]) + else: + #print('--> camel split words', camelCaseSplit) + for j in range(len(camelCaseSplit)): + finalWords.append(camelCaseSplit[j]) + + if len(finalWords) == 0: + return text + + finalText = '' + if capitalize: + 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) + return finalText + + +def loadJsonFile(filePath): + print('--> Loading JSON file', filePath) + jsonFile = open(filePath, 'r') + return json.load(jsonFile) + + +def writeLine(fileDescriptor, line = ''): + fileDescriptor.write(line + '\n') + + +def writeLicenseHeader(fileDescriptor): + writeLine(fileDescriptor, '/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *') + writeLine(fileDescriptor, '*') + writeLine(fileDescriptor, '* Copyright 2013 - %s, nymea GmbH' % datetime.datetime.now().year) + writeLine(fileDescriptor, '* Contact: contact@nymea.io') + writeLine(fileDescriptor, '*') + writeLine(fileDescriptor, '* This fileDescriptor is part of nymea.') + writeLine(fileDescriptor, '* This project including source code and documentation is protected by') + writeLine(fileDescriptor, '* copyright law, and remains the property of nymea GmbH. All rights, including') + writeLine(fileDescriptor, '* reproduction, publication, editing and translation, are reserved. The use of') + writeLine(fileDescriptor, '* this project is subject to the terms of a license agreement to be concluded') + writeLine(fileDescriptor, '* with nymea GmbH in accordance with the terms of use of nymea GmbH, available') + writeLine(fileDescriptor, '* under https://nymea.io/license') + writeLine(fileDescriptor, '*') + writeLine(fileDescriptor, '* GNU Lesser General Public License Usage') + writeLine(fileDescriptor, '* Alternatively, this project may be redistributed and/or modified under the') + writeLine(fileDescriptor, '* terms of the GNU Lesser General Public License as published by the Free') + writeLine(fileDescriptor, '* Software Foundation; version 3. This project is distributed in the hope that') + writeLine(fileDescriptor, '* it will be useful, but WITHOUT ANY WARRANTY; without even the implied') + writeLine(fileDescriptor, '* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU') + writeLine(fileDescriptor, '* Lesser General Public License for more details.') + writeLine(fileDescriptor, '*') + writeLine(fileDescriptor, '* You should have received a copy of the GNU Lesser General Public License') + writeLine(fileDescriptor, '* along with this project. If not, see .') + writeLine(fileDescriptor, '*') + writeLine(fileDescriptor, '* For any further details and any questions please contact us under') + writeLine(fileDescriptor, '* contact@nymea.io or see our FAQ/Licensing Information on') + writeLine(fileDescriptor, '* https://nymea.io/license/faq') + writeLine(fileDescriptor, '*') + writeLine(fileDescriptor, '* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */') + writeLine(fileDescriptor) + + +def writeEnumDefinition(fileDescriptor, enumDefinition): + print('Writing enum', enumDefinition) + enumName = enumDefinition['name'] + enumValues = enumDefinition['values'] + writeLine(fileDescriptor, ' enum %s {' % enumName) + for i in range(len(enumValues)): + enumData = enumValues[i] + line = (' %s%s = %s' % (enumName, enumData['key'], enumData['value'])) + if i < (len(enumValues) - 1): + line += ',' + + writeLine(fileDescriptor, line) + + writeLine(fileDescriptor, ' };') + writeLine(fileDescriptor, ' Q_ENUM(%s)' % enumName) + writeLine(fileDescriptor) + + +def getCppDataType(registerDefinition): + if 'enum' in registerDefinition: + return registerDefinition['enum'] + + if 'scaleFactor' in registerDefinition: + return 'float' + + if registerDefinition['type'] == 'uint16': + return 'quint16' + + if registerDefinition['type'] == 'int16': + return 'qint16' + + if registerDefinition['type'] == 'uint32': + return 'quint32' + + if registerDefinition['type'] == 'int32': + return 'qint32' + + if registerDefinition['type'] == 'uint64': + return 'quint64' + + if registerDefinition['type'] == 'int64': + return 'qint64' + + if registerDefinition['type'] == 'float': + return 'float' + + if registerDefinition['type'] == 'float64': + return 'double' + + if registerDefinition['type'] == 'string': + return 'QString' + + +def getValueConversionMethod(registerDefinition): + if 'enum' in registerDefinition: + enumName = registerDefinition['enum'] + if registerDefinition['type'] == 'uint16': + return ('static_cast<%s>(ModbusDataUtils::convertToUInt16(unit.values()))' % (enumName)) + elif registerDefinition['type'] == 'int16': + return ('static_cast<%s>(ModbusDataUtils::convertToInt16(unit.values()))' % (enumName)) + elif registerDefinition['type'] == 'uint32': + return ('static_cast<%s>(ModbusDataUtils::convertToUInt32(unit.values()))' % (enumName)) + elif registerDefinition['type'] == 'int32': + return ('static_cast<%s>(ModbusDataUtils::convertToInt32(unit.values()))' % (enumName)) + + if 'scaleFactor' in registerDefinition: + scaleFactorProperty = 'm_%s' % registerDefinition['scaleFactor'] + if registerDefinition['type'] == 'uint16': + return ('ModbusDataUtils::convertToUInt16(unit.values()) * 1.0 * pow(10, %s)' % (scaleFactorProperty)) + elif registerDefinition['type'] == 'int16': + return ('ModbusDataUtils::convertToInt16(unit.values()) * 1.0 * pow(10, %s)' % (scaleFactorProperty)) + elif registerDefinition['type'] == 'uint32': + return ('ModbusDataUtils::convertToUInt32(unit.values()) * 1.0 * pow(10, %s)' % (scaleFactorProperty)) + elif registerDefinition['type'] == 'int32': + return ('ModbusDataUtils::convertToInt32(unit.values()) * 1.0 * pow(10, %s)' % (scaleFactorProperty)) + + + elif registerDefinition['type'] == 'uint16': + return ('ModbusDataUtils::convertToUInt16(unit.values())') + elif registerDefinition['type'] == 'int16': + return ('ModbusDataUtils::convertToInt16(unit.values())') + elif registerDefinition['type'] == 'uint32': + return ('ModbusDataUtils::convertToUInt32(unit.values())') + elif registerDefinition['type'] == 'int32': + return ('ModbusDataUtils::convertToInt32(unit.values())') + elif registerDefinition['type'] == 'uint64': + return ('ModbusDataUtils::convertToUInt64(unit.values())') + elif registerDefinition['type'] == 'int64': + return ('ModbusDataUtils::convertToInt64(unit.values())') + elif registerDefinition['type'] == 'float': + return ('ModbusDataUtils::convertToFloat32(unit.values())') + elif registerDefinition['type'] == 'float64': + return ('ModbusDataUtils::convertToFloat64(unit.values())') + elif registerDefinition['type'] == 'string': + return ('ModbusDataUtils::convertToString(unit.values())') + + +def writePropertyGetMethodDeclarations(fileDescriptor, registerDefinitions): + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + if 'unit' in registerDefinition and registerDefinition['unit'] != '': + 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, ' %s %s() const;' % (propertyTyp, propertyName)) + writeLine(fileDescriptor) + + +def writePropertyGetMethodImplementations(fileDescriptor, className, registerDefinitions): + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + if 'enum' in registerDefinition: + writeLine(fileDescriptor, '%s::%s %s::%s() const' % (className, propertyTyp, className, propertyName)) + else: + writeLine(fileDescriptor, '%s %s::%s() const' % (propertyTyp, className, propertyName)) + + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' return m_%s;' % propertyName) + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +def writePropertyUpdateMethodDeclarations(fileDescriptor, registerDefinitions): + for registerDefinition in registerDefinitions: + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': + continue + + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + writeLine(fileDescriptor, ' void update%s();' % (propertyName[0].upper() + propertyName[1:])) + + +def writePropertyUpdateMethodImplementations(fileDescriptor, className, registerDefinitions): + for registerDefinition in registerDefinitions: + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': + continue + + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + writeLine(fileDescriptor, 'void %s::update%s()' % (className, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' // Update registers from %s' % registerDefinition['description']) + writeLine(fileDescriptor, ' QModbusReply *reply = read%s();' % (propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' if (reply) {') + writeLine(fileDescriptor, ' if (!reply->isFinished()) {') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') + writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::NoError) {') + writeLine(fileDescriptor, ' const QModbusDataUnit unit = reply->result();') + writeLine(fileDescriptor, ' %s received%s = %s;' % (propertyTyp, propertyName[0].upper() + propertyName[1:], getValueConversionMethod(registerDefinition))) + writeLine(fileDescriptor, ' if (m_%s != received%s) {' % (propertyName, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' m_%s = received%s;' % (propertyName, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' emit %sChanged(m_%s);' % (propertyName, propertyName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating \\"%s\\" registers from" << hostAddress().toString() << error << reply->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' emit reply->finished(); // To make sure it will be deleted') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' delete reply; // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << hostAddress().toString() << errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +def writeInternalPropertyReadMethodDeclarations(fileDescriptor, registerDefinitions): + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + writeLine(fileDescriptor, ' QModbusReply *read%s();' % (propertyName[0].upper() + propertyName[1:])) + + +def writeInternalPropertyReadMethodImplementations(fileDescriptor, className, registerDefinitions): + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + writeLine(fileDescriptor, 'QModbusReply *%s::read%s()' % (className, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) + writeLine(fileDescriptor, ' return sendReadRequest(request, m_slaveId);') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +def writePropertyChangedSignals(fileDescriptor, registerDefinitions): + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + if propertyTyp == 'QString': + writeLine(fileDescriptor, ' void %sChanged(const %s &%s);' % (propertyName, propertyTyp, propertyName)) + else: + writeLine(fileDescriptor, ' void %sChanged(%s %s);' % (propertyName, propertyTyp, propertyName)) + + +def writePrivatePropertyMembers(fileDescriptor, registerDefinitions): + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + if 'defaultValue' in registerDefinition: + writeLine(fileDescriptor, ' %s m_%s = %s;' % (propertyTyp, propertyName, registerDefinition['defaultValue'])) + else: + writeLine(fileDescriptor, ' %s m_%s;' % (propertyTyp, propertyName)) + + +def writeInitializeMethod(fileDescriptor, className, registerDefinitions): + writeLine(fileDescriptor, 'void %s::initialize()' % (className)) + writeLine(fileDescriptor, '{') + + writeLine(fileDescriptor, ' QModbusReply *reply = nullptr;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (!m_pendingInitReplies.isEmpty()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Tried to initialize but there are still some init replies pending.";' % className) + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': + writeLine(fileDescriptor, ' // Read %s' % registerDefinition['description']) + writeLine(fileDescriptor, ' reply = read%s();' % (propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' if (reply) {') + writeLine(fileDescriptor, ' if (!reply->isFinished()) {') + writeLine(fileDescriptor, ' m_pendingInitReplies.append(reply);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') + writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::NoError) {') + writeLine(fileDescriptor, ' const QModbusDataUnit unit = reply->result();') + writeLine(fileDescriptor, ' %s received%s = %s;' % (propertyTyp, propertyName[0].upper() + propertyName[1:], getValueConversionMethod(registerDefinition))) + writeLine(fileDescriptor, ' if (m_%s != received%s) {' % (propertyName, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' m_%s = received%s;' % (propertyName, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' emit %sChanged(m_%s);' % (propertyName, propertyName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_pendingInitReplies.removeAll(reply);') + writeLine(fileDescriptor, ' verifyInitFinished();') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << hostAddress().toString() << error << reply->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' emit reply->finished(); // To make sure it will be deleted') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' delete reply; // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << hostAddress().toString() << errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + + writeLine(fileDescriptor, ' ') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +def writeUpdateMethod(fileDescriptor, className, registerDefinitions): + writeLine(fileDescriptor, 'void %s::update()' % (className)) + writeLine(fileDescriptor, '{') + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'update': + writeLine(fileDescriptor, ' update%s();' % (propertyName[0].upper() + propertyName[1:])) + + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +############################################################################################ +# Main +############################################################################################ + +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.') +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' + +headerFilePath = os.path.join(outputDirectory, headerFileName) +sourceFilePath = os.path.join(outputDirectory, sourceFileName) + +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) + +############################################################################# +# Write header file +############################################################################# + +headerFile = open(headerFilePath, 'w') + +writeLicenseHeader(headerFile) +writeLine(headerFile, '#ifndef %s_H' % className.upper()) +writeLine(headerFile, '#define %s_H' % className.upper()) +writeLine(headerFile) +writeLine(headerFile, '#include ') +writeLine(headerFile) +writeLine(headerFile, '#include "../modbus/modbusdatautils.h"') +writeLine(headerFile, '#include "../modbus/modbustcpmaster.h"') + +writeLine(headerFile) + +# Begin of class +writeLine(headerFile, 'class %s : public ModbusTCPMaster' % className) +writeLine(headerFile, '{') +writeLine(headerFile, ' Q_OBJECT') + +# Public members +writeLine(headerFile, 'public:') + +# Enum declarations +for enumDefinition in registerJson['enums']: + writeEnumDefinition(headerFile, enumDefinition) + +# Constructor +writeLine(headerFile, ' explicit %s(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent = nullptr);' % className) +writeLine(headerFile, ' ~%s() = default;' % className) +writeLine(headerFile) + +# Write registers get method declarations +writePropertyGetMethodDeclarations(headerFile, registerJson['registers']) + +# Write init and update method declarations +writeLine(headerFile, ' virtual void initialize();') +writeLine(headerFile, ' virtual void update();') +writeLine(headerFile) + +writePropertyUpdateMethodDeclarations(headerFile, registerJson['registers']) +writeLine(headerFile) + +# Write registers value changed signals +writeLine(headerFile, 'signals:') +writeLine(headerFile, ' void initializationFinished();') +writeLine(headerFile) +writePropertyChangedSignals(headerFile, registerJson['registers']) +writeLine(headerFile) + +# Private members +writeLine(headerFile, 'private:') +writeLine(headerFile, ' quint16 m_slaveId = 1;') +writeLine(headerFile, ' QVector m_pendingInitReplies;') +writeLine(headerFile) +writePrivatePropertyMembers(headerFile, registerJson['registers']) +writeLine(headerFile) +writeLine(headerFile, ' void verifyInitFinished();') +writeLine(headerFile) +writeInternalPropertyReadMethodDeclarations(headerFile, registerJson['registers']) +writeLine(headerFile) + +# End of class +writeLine(headerFile) +writeLine(headerFile, '};') +writeLine(headerFile) +writeLine(headerFile, 'QDebug operator<<(QDebug debug, %s *%s);' % (className, className[0].lower() + className[1:])) +writeLine(headerFile) +writeLine(headerFile, '#endif // %s_H' % className.upper()) + +headerFile.close() + + + +############################################################################# +# Write source file +############################################################################# + +sourceFile = open(sourceFilePath, 'w') +writeLicenseHeader(sourceFile) +writeLine(sourceFile) +writeLine(sourceFile, '#include "%s"' % headerFileName) +writeLine(sourceFile, '#include "loggingcategories.h"') +writeLine(sourceFile) +writeLine(sourceFile, 'NYMEA_LOGGING_CATEGORY(dc%s, "%s")' % (className, className)) +writeLine(sourceFile) + +# Constructor +writeLine(sourceFile, '%s::%s(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent) :' % (className, className)) +writeLine(sourceFile, ' ModbusTCPMaster(hostAddress, port, parent),') +writeLine(sourceFile, ' m_slaveId(slaveId)') +writeLine(sourceFile, '{') +writeLine(sourceFile, ' ') +writeLine(sourceFile, '}') +writeLine(sourceFile) + +# Property get methods +writePropertyGetMethodImplementations(sourceFile, className, registerJson['registers']) + +# Write init and update method implementation +writeInitializeMethod(sourceFile, className, registerJson['registers']) +writeUpdateMethod(sourceFile, className, registerJson['registers']) + +# Write update methods +writePropertyUpdateMethodImplementations(sourceFile, className, registerJson['registers']) + +# Write property read method implementations +writeInternalPropertyReadMethodImplementations(sourceFile, className, registerJson['registers']) + +writeLine(sourceFile, 'void %s::verifyInitFinished()' % (className)) +writeLine(sourceFile, '{') +writeLine(sourceFile, ' if (m_pendingInitReplies.isEmpty()) {') +writeLine(sourceFile, ' qCDebug(dc%s()) << "Initialization finished of %s" << hostAddress().toString();' % (className, className)) +writeLine(sourceFile, ' emit initializationFinished();') +writeLine(sourceFile, ' }') +writeLine(sourceFile, '}') +writeLine(sourceFile) + +# Write the debug print +debugObjectParamName = className[0].lower() + className[1:] +writeLine(sourceFile, 'QDebug operator<<(QDebug debug, %s *%s)' % (className, debugObjectParamName)) +writeLine(sourceFile, '{') +writeLine(sourceFile, ' debug.nospace().noquote() << "%s(" << %s->hostAddress().toString() << ":" << %s->port() << ")" << "\\n";' % (className, debugObjectParamName, debugObjectParamName)) +for registerDefinition in registerJson['registers']: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + line = ('" - %s:" << %s->%s()' % (registerDefinition['description'], debugObjectParamName, propertyName)) + if 'unit' in registerDefinition and registerDefinition['unit'] != '': + line += (' << " [%s]"' % registerDefinition['unit']) + writeLine(sourceFile, ' debug.nospace().noquote() << %s << "\\n";' % (line)) + +writeLine(sourceFile, ' return debug.quote().space();') +writeLine(sourceFile, '}') +writeLine(sourceFile) + +sourceFile.close() \ No newline at end of file