diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8396ff5..9ec82002 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,8 @@ name: Build on: push: - branches: [ master ] + branches: + - master paths: - 'src/**' - 'autogen.sh' @@ -50,4 +51,4 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: build - run: cd contrib/docker && LIMITARCH=${{ github.event.inputs.limitarch }} GIT_REVISION=${{ steps.gittag.outputs.describe }} ./build.sh + run: cd contrib/docker && GIT_BRANCH=${GITHUB_REF##*/} LIMITARCH=${{ github.event.inputs.limitarch }} GIT_REVISION=${{ steps.gittag.outputs.describe }} ./build.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 1001360d..2798e376 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,6 +103,26 @@ if(HAVE_MQTT) endif(mqtt STREQUAL ON) endif(HAVE_MQTT) +option(knx "disable support for KNX handling." ON) +if(knx STREQUAL ON) + message(STATUS "KNX enabled") + set(HAVE_KNX 1) +else(knx STREQUAL ON) + unset(HAVE_KNX) +endif(knx STREQUAL ON) + +if(HAVE_KNX) + find_library(HAVE_KNXD eibclient) + if(HAVE_KNXD) + option(knxd "enable support for KNX handling via knxd." OFF) + if(knxd STREQUAL ON) + message(STATUS "KNX via knxd enabled") + else(knxd STREQUAL ON) + set(HAVE_KNXD 0) + endif(knxd STREQUAL ON) + endif(HAVE_KNXD) +endif(HAVE_KNX) + find_library(HAVE_SSL ssl) find_library(LIB_CRYPTO crypto) if(HAVE_SSL) @@ -159,6 +179,7 @@ endif(BUILD_TESTING) add_subdirectory(src/ebusd) add_subdirectory(src/lib/utils) add_subdirectory(src/lib/ebus) +add_subdirectory(src/lib/knx) add_subdirectory(src/tools) if(EXISTS "${ROOT}/etc/debian_version") @@ -170,3 +191,7 @@ if(HAVE_MQTT) FILE(GLOB MQTT_CFG_FILES "${CMAKE_SOURCE_DIR}/contrib/etc/ebusd/mqtt-*.cfg") install(FILES ${MQTT_CFG_FILES} DESTINATION /etc/ebusd/) endif(HAVE_MQTT) +if(HAVE_KNX) + FILE(GLOB KNX_CFG_FILES "${CMAKE_SOURCE_DIR}/contrib/etc/ebusd/knx*.cfg") + install(FILES ${KNX_CFG_FILES} DESTINATION /etc/ebusd/) +endif(HAVE_KNX) diff --git a/Makefile.am b/Makefile.am index f2488188..23eeccf0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,7 +2,13 @@ ACLOCAL_AMFLAGS = -I m4 SUBDIRS = docs \ src/lib/utils \ - src/lib/ebus \ + src/lib/ebus + +if KNX +SUBDIRS += src/lib/knx +endif + +SUBDIRS += \ src/ebusd \ src/tools @@ -12,7 +18,11 @@ EXTRA_DIST = LICENSE \ VERSION if MQTT -MQTT_INST_ADD = cp -n $(srcdir)/contrib/etc/ebusd/mqtt-*.cfg $(DESTDIR)$(sysconfdir)/ebusd +MQTT_INST_ADD = cp -n $(srcdir)/contrib/etc/ebusd/mqtt-*.cfg $(DESTDIR)$(sysconfdir)/ebusd; +endif + +if KNX +KNX_INST_ADD = cp -n $(srcdir)/contrib/etc/ebusd/knx*.cfg $(DESTDIR)$(sysconfdir)/ebusd; endif install-data-hook: @@ -23,7 +33,8 @@ install-data-hook: cp $(srcdir)/contrib/debian/init.d/ebusd $(DESTDIR)/etc/init.d/; \ cp $(srcdir)/contrib/debian/systemd/ebusd.service $(DESTDIR)/lib/systemd/system/; \ fi; \ - $(MQTT_INST_ADD) + $(MQTT_INST_ADD) \ + $(KNX_INST_ADD) test: $(MAKE) -C src/lib/ebus/test diff --git a/config.h.cmake b/config.h.cmake index a5397fd4..5bfd4250 100755 --- a/config.h.cmake +++ b/config.h.cmake @@ -7,6 +7,12 @@ /* Defined if MQTT handling is enabled. */ #cmakedefine HAVE_MQTT +/* Defined if KNX handling is enabled. */ +#cmakedefine HAVE_KNX + +/* Defined if KNX handling via knxd is enabled. */ +#cmakedefine HAVE_KNXD + /* Defined if SSL is enabled. */ #cmakedefine HAVE_SSL diff --git a/configure.ac b/configure.ac index 917eef59..4775300f 100755 --- a/configure.ac +++ b/configure.ac @@ -63,6 +63,21 @@ if test "x$with_mqtt" != "xno"; then fi AM_CONDITIONAL([MQTT], [test "x$with_mqtt" != "xno"]) +AC_ARG_WITH(knx, AS_HELP_STRING([--without-knx], [disable support for KNX handling]), [], [with_knx=yes]) +if test "x$with_knx" != "xno"; then + AC_DEFINE_UNQUOTED(HAVE_KNX, [1], [Defined if KNX handling is enabled.]) + AC_ARG_WITH(knxd, AS_HELP_STRING([--with-knxd], [enable support for KNX handling via knxd]), [ + AS_IF([test "x$with_knxd" == "xyes"], + [AC_CHECK_LIB([eibclient], [EIBSocketURL], + [AC_DEFINE_UNQUOTED(HAVE_KNXD, [1], [Defined if KNX handling via knxd is enabled.])], + [AC_MSG_RESULT([Could not find EIBSocketURL in libeibclient.]) + with_knxd="no"]) + ]) + ], []) +fi +AM_CONDITIONAL([KNX], [test "x$with_knx" != "xno"]) +AM_CONDITIONAL([KNXD], [test "x$with_knxd" == "xyes"]) + AC_ARG_WITH(ssl, AS_HELP_STRING([--without-ssl], [disable support for SSL]), [], [with_ssl=yes]) if test "x$with_ssl" != "xno"; then AC_CHECK_LIB([ssl], [OPENSSL_init_ssl], @@ -131,6 +146,9 @@ AM_COND_IF([CONTRIB], [AC_CONFIG_FILES([ src/lib/ebus/contrib/Makefile src/lib/ebus/contrib/test/Makefile ])]) +AM_COND_IF([KNX], [AC_CONFIG_FILES([ + src/lib/knx/Makefile + ])]) AC_DEFINE_UNQUOTED(PACKAGE_PIDFILE, LOCALSTATEDIR "/run/" PACKAGE ".pid", [The path and name of the PID file.]) AC_DEFINE_UNQUOTED(PACKAGE_LOGFILE, LOCALSTATEDIR "/log/" PACKAGE ".log", [The path and name of the log file.]) diff --git a/contrib/docker/build.sh b/contrib/docker/build.sh index e52965db..fbed022f 100755 --- a/contrib/docker/build.sh +++ b/contrib/docker/build.sh @@ -27,6 +27,9 @@ if [[ -z "$1" ]]; then target=image outputFmt='-o type=docker,type=registry' tagsuffix=':devel' + if [[ -n "$GIT_BRANCH" ]] && [[ "x$GIT_BRANCH" != "xmaster" ]]; then + tagsuffix="$tagsuffix-$GIT_BRANCH" + fi elif [[ "x$1" = "xrelease" ]]; then namesuffix='.release' target=image diff --git a/contrib/docker/update.sh b/contrib/docker/update.sh index 65a4a0f6..626ae5ea 100755 --- a/contrib/docker/update.sh +++ b/contrib/docker/update.sh @@ -34,7 +34,7 @@ replaceTemplate if [[ -n "$1" ]]; then # build releases update make='GIT_REVISION=\$GIT_REVISION ./make_all.sh' - upload_lines='ARG UPLOAD_URL\nARG UPLOAD_CREDENTIALS\nARG UPLOAD_OS\nRUN if [ -n "\$UPLOAD_URL" ] \&\& [ -n "\$UPLOAD_CREDENTIALS" ]; then for img in ebusd-*.deb; do echo -n "upload \$img: "; curl -fsSk -u "\$UPLOAD_CREDENTIALS" -X POST --data-binary "@\$img" -H "Content-Type: application/octet-stream" "\$UPLOAD_URL/\$img?a=\$EBUSD_ARCH\&o=\$UPLOAD_OS\&v=\$EBUSD_VERSION" || echo "failed"; done; fi' + upload_lines='ARG UPLOAD_URL\nARG UPLOAD_CREDENTIALS\nARG UPLOAD_OS\nRUN if [ -n "\$UPLOAD_URL" ] \&\& [ -n "\$UPLOAD_CREDENTIALS" ]; then for img in ebusd-*.deb; do echo -n "upload \$img: "; curl -fsSk -u "\$UPLOAD_CREDENTIALS" -X POST --data-binary "@\$img" -H "Content-Type: application/octet-stream" "\$UPLOAD_URL/\$img?a=\$EBUSD_ARCH\&o=\$UPLOAD_OS\&v=\$EBUSD_VERSION&b=$GIT_BRANCH" || echo "failed"; done; fi' upload_lines+='\n\n\nFROM scratch as deb\nCOPY --from=build /build/*.deb /' namesuffix='.build' replaceTemplate diff --git a/contrib/etc/ebusd/knx.cfg b/contrib/etc/ebusd/knx.cfg new file mode 100644 index 00000000..b7a5926c --- /dev/null +++ b/contrib/etc/ebusd/knx.cfg @@ -0,0 +1,72 @@ +# Configuration file for ebusd KNX integration with knxd (https://github.com/knxd/knxd). + +# Use this file with ebusd to establish a bridge between KNX and eBUS for a set of messages. +# The commandline options to ebusd should contain e.g.: +# --knxurl=ip:localhost --knxint=/etc/ebusd/knx.cfg + +# Currently only reading from and writing to group addresses as defined here is supported. +# Setting the addresses via ETS is not (yet) possible as well as setting the physical address. +# All entries are set to group address flags as follows: +# - for read and passive write messages: "Read", "Transmit" +# - for active write messages: "Write", "Read" (only answered when value was written before via KNX or eBUS), no "Update" + +# The physical address depends on how knxd is configured and might change each time ebusd and/or knxd is restarted when +# a range of possible client addresses were configured on knxd side (which is recommended when there is more than one +# client using knxd). + +# the own individual address (only relevant when running in KNXnet/IP mode) +# address = 1.1.1 + +# the global value group assignments for running, version, signal, uptime, updatecheck, and scan. +# running: 1 bit, 1=running, DPT 1.002 +global/running = 9/0 +# version: 2 octets, major in MSB, minor in LSB, DPT 217.001 "DPT_Version" +global/version = 9/1 +# signal: 1 bit, 1=signal acquired, DPT 1.002 +global/signal = 9/2 +# uptime: 4 octets int, seconds since start, sent once every hour, DPT 12.100 +global/uptime = 9/3 +# updatecheck: 1 bit, 1=update available, DPT 1.002 +global/updatecheck = 9/4 +# scan: 1 bit, 1=running, DPT 1.002 +global/scan = 9/5 + + +# the message field value group assignments by circuit/message/field name. +# the value coding depends on the field datatype and currently only numeric datatypes are supported. +# the mapping is as follows: +# - BI0:1 - BI7:1, length 1: 1 bit, DPT 1 +# - without divisor: +# - BI0 - BI6, length >1: 1 octet, unsigned, DPT 5.010 +# - UCH: 1 octet, unsigned, DPT 5.010 +# - SCH, D1B: 1 octet, signed, DPT 6.010 +# - UIN, UIR, PIN: 2 octet, unsigned, DPT 7.001 +# - SIN, SIR: 2 octet, signed, DPT 8.001 +# - U3N, U3R, ULG, ULR: 4 octet, unsigned, DPT 12.001 +# - U3N, U3R, SLG, SLR: 4 octet, signed, DPT 13.001 +# - with divisor: +# - BI0 - BI6, length >1: 2 octet, signed float, DPT 9.* +# - UCH, SCH, D1B, UIN, UIR, SIN, SIR: 2 octet, signed float, DPT 9.* +# - U3N, U3R, ULG, ULR, SLG, SLR: 4 octet, signed float, DPT 14.* +# - with or without divisor: +# - D1C, D2B, D2C, FLT, FLR: 2 octet, signed float, DPT 9.* +# - EXP, EXR: 4 octet, signed float, DPT 14.* +# +# note: the float conversion from ebus to KNX may loose precision due to the KNX DPT 9 not being able to carry more than +# two digits after the decimal point and having a mantissa of only 11 bits. +# Consequently, when writing a 2-octet float to ebusd, a consecutive read on the same group address is likely to reveal +# a different value if it was using more than two digits after the decimal point or exceeding the KNX float mantissa +# range, e.g.: +# - an ebus D2B value of 10.004 will read as 10.00 2-octet float on KNX, +# - an ebus UIN with divisor 100 (like heating curve) value of 655.34 will read as 655.04 2-octet float on KNX, +# - writing a KNX 2-octet float value of 12.34 to an ebus UIN with divisor 10 will actually write 12.3 and read as 12.3. +# +# note: writing to ebus via KNX currently is only possible if the ebus message contains a single field respectively at +# most one non-ignored field. this is due to otherwise the value to be set for the other fields would have to be +# determined first which is mostly not possible. Group associations to write messages not fulfilling this requirement +# are silently ignored. +# +# note: the mapping for reads/writes from KNX is done as follows: +# - for KNX read, the precedence on picking the ebus message is: active read, passive read+write. +# - for KNX write, the precedence on picking the ebus message is: active write only. +broadcast/datetime/outsidetemp = 9/10 diff --git a/src/ebusd/CMakeLists.txt b/src/ebusd/CMakeLists.txt index 88de0d1a..325663c2 100644 --- a/src/ebusd/CMakeLists.txt +++ b/src/ebusd/CMakeLists.txt @@ -13,6 +13,12 @@ if(HAVE_MQTT) set(ebusd_LIBS ${ebusd_LIBS} mosquitto) endif(HAVE_MQTT) +if(HAVE_KNX) + set(ebusd_SOURCES ${ebusd_SOURCES} knxhandler.cpp knxhandler.h) + set(ebusd_LIBS ${ebusd_LIBS} knx) + include_directories(../lib/knx) +endif(HAVE_KNX) + if(HAVE_SSL) set(ebusd_LIBS ${ebusd_LIBS} ssl crypto) endif(HAVE_SSL) diff --git a/src/ebusd/Makefile.am b/src/ebusd/Makefile.am index e449dd3a..eb630716 100644 --- a/src/ebusd/Makefile.am +++ b/src/ebusd/Makefile.am @@ -22,6 +22,14 @@ ebusd_LDADD = ../lib/utils/libutils.a \ -lpthread \ @EXTRA_LIBS@ +if KNX +ebusd_SOURCES += knxhandler.cpp knxhandler.h +ebusd_LDADD += ../lib/knx/libknx.a +if KNXD +ebusd_LDADD += -leibclient +endif +endif + if SSL ebusd_LDADD += -lssl -lcrypto endif diff --git a/src/ebusd/datahandler.cpp b/src/ebusd/datahandler.cpp index 216f4662..264c3b80 100755 --- a/src/ebusd/datahandler.cpp +++ b/src/ebusd/datahandler.cpp @@ -24,6 +24,9 @@ #ifdef HAVE_MQTT # include "ebusd/mqtthandler.h" #endif +#ifdef HAVE_KNX +# include "ebusd/knxhandler.h" +#endif namespace ebusd { @@ -36,12 +39,18 @@ static struct argp_child g_argp_children[ #ifdef HAVE_MQTT +1 #endif +#ifdef HAVE_KNX + +1 +#endif ]; const struct argp_child* datahandler_getargs() { size_t count = 0; #ifdef HAVE_MQTT g_argp_children[count++] = *mqtthandler_getargs(); +#endif +#ifdef HAVE_KNX + g_argp_children[count++] = *knxhandler_getargs(); #endif if (count > 0) { g_argp_children[count] = g_last_argp_child; @@ -57,6 +66,11 @@ bool datahandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap if (!mqtthandler_register(userInfo, busHandler, messages, handlers)) { success = false; } +#endif +#ifdef HAVE_KNX + if (!knxhandler_register(userInfo, busHandler, messages, handlers)) { + success = false; + } #endif return success; } diff --git a/src/ebusd/knxhandler.cpp b/src/ebusd/knxhandler.cpp new file mode 100644 index 00000000..bc939327 --- /dev/null +++ b/src/ebusd/knxhandler.cpp @@ -0,0 +1,981 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "ebusd/knxhandler.h" +#ifdef HAVE_PPOLL +# include +#endif +#include +#include +#include +#include "lib/utils/log.h" +#include "lib/ebus/symbol.h" + +#ifndef POLLRDHUP +#define POLLRDHUP 0 +#endif + + +namespace ebusd { + +using std::dec; + +// version is coded as: +// 5 bits magic (to be incremented with incompatible changes, not shown) +// 5 bits major, using major directly +// 6 bits minor, using minor multiplied by 10 to have space for micro versioning in future +#define VERSION_INT ((PACKAGE_VERSION_MAJOR<<6)|(PACKAGE_VERSION_MINOR*10)) + +#define O_URL -2 +#define O_AGR (O_URL-1) +#define O_AGW (O_AGR-1) +#define O_INT (O_AGW-1) +#define O_VAR (O_INT-1) + +/** the definition of the KNX arguments. */ +static const struct argp_option g_knx_argp_options[] = { + {nullptr, 0, nullptr, 0, "KNX options:", 1 }, + {"knxurl", O_URL, "URL", 0, "Connect to KNX daemon on URL (i.e. \"[multicast][@interface]\" for KNXnet/IP" +#ifdef HAVE_KNXD + " or \"ip:host[:port]\" / \"local:/socketpath\" for knxd" +#endif + ") []", 0 }, + {"knxrage", O_AGR, "SEC", 0, "Maximum age in seconds for using the last value of read messages (0=disable) [5]", 0 }, + {"knxwage", O_AGW, "SEC", 0, "Maximum age in seconds for using the last value for reads on write messages (0=disable), [99999999]", 0 }, + {"knxint", O_INT, "FILE", 0, "Read KNX integration settings from FILE [/etc/ebusd/knx.cfg]", 0 }, + {"knxvar", O_VAR, "NAME=VALUE", 0, "Add a variable to the read KNX integration settings", 0 }, + + {nullptr, 0, nullptr, 0, nullptr, 0 }, +}; + +static const char* g_url = nullptr; //!< URL of KNX daemon +static unsigned int g_maxReadAge = 5; //!< max age in seconds for using the last value of read messages +static unsigned int g_maxWriteAge = 99999999; //!< max age in seconds for using the last value for reads on write messages +static const char* g_integrationFile = nullptr; //!< the integration settings file +static vector* g_integrationVars = nullptr; //!< the integration settings variables + +/** + * The KNX argument parsing function. + * @param key the key from @a g_knx_argp_options. + * @param arg the option argument, or nullptr. + * @param state the parsing state. + */ +static error_t knx_parse_opt(int key, char *arg, struct argp_state *state) { + result_t result; + switch (key) { + case O_URL: // --knxurl=localhost + if (arg == nullptr || arg[0] == 0) { + argp_error(state, "invalid knxurl"); + return EINVAL; + } + g_url = arg; + break; + + case O_AGR: // --knxrage=5 + if (arg == nullptr || arg[0] == 0) { + argp_error(state, "invalid knxrage value"); + return EINVAL; + } + g_maxReadAge = parseInt(arg, 10, 0, 99999999, &result); + if (result != RESULT_OK) { + argp_error(state, "invalid knxrage"); + return EINVAL; + } + break; + + case O_AGW: // --knxwage=5 + if (arg == nullptr || arg[0] == 0) { + argp_error(state, "invalid knxwage value"); + return EINVAL; + } + g_maxWriteAge = parseInt(arg, 10, 0, 99999999, &result); + if (result != RESULT_OK) { + argp_error(state, "invalid knxwage"); + return EINVAL; + } + break; + + case O_INT: // --knxint=/etc/ebusd/knx.cfg + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argp_error(state, "invalid knxint file"); + return EINVAL; + } + g_integrationFile = arg; + break; + + case O_VAR: // --knxvar=NAME=VALUE + if (arg == nullptr || arg[0] == 0 || !strchr(arg, '=')) { + argp_error(state, "invalid knxvar"); + return EINVAL; + } + if (!g_integrationVars) { + g_integrationVars = new vector(); + } + g_integrationVars->push_back(string(arg)); + break; + + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +static const struct argp g_knx_argp = { g_knx_argp_options, knx_parse_opt, nullptr, nullptr, nullptr, nullptr, + nullptr }; +static const struct argp_child g_knx_argp_child = {&g_knx_argp, 0, "", 1}; + + +const struct argp_child* knxhandler_getargs() { + return &g_knx_argp_child; +} + +bool knxhandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages, + list* handlers) { + if (g_url) { + handlers->push_back(new KnxHandler(userInfo, busHandler, messages)); + } + return true; +} + +KnxHandler::KnxHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages) + : DataSink(userInfo, "knx"), DataSource(busHandler), WaitThread(), m_messages(messages), + m_start(0), m_lastUpdateCheckResult("."), + m_lastScanStatus(SCAN_STATUS_NONE), m_scanFinishReceived(false), m_lastErrorLogTime(0) { + m_con = KnxConnection::create(g_url); + if (g_integrationFile != nullptr) { + if (!m_replacers.parseFile(g_integrationFile)) { + logOtherError("knx", "unable to open integration file %s", g_integrationFile); + } + } + if (g_integrationVars) { + for (auto& str : *g_integrationVars) { + m_replacers.parseLine(str); + } + delete g_integrationVars; + g_integrationVars = nullptr; + } + if (m_con->isProgrammable()) { + string addrStr = m_replacers.get("address", false); + knx_addr_t address = 0; + if (!addrStr.empty()) { + address = parseAddress(addrStr, false); + if (!address) { + logOtherError("knx", "invalid address: %s", addrStr.c_str()); + } + } + if (address) { + m_con->setAddress(address); + } else { + logOtherNotice("knx", "address not assigned yet, entering programming mode"); + m_con->setProgrammingMode(true); + } + } + // parse all group to message field assignments + vector keys = m_replacers.keys(); + int messageCnt = 0, globalCnt = 0; + for (auto& key : keys) { + auto pos = key.find('/'); + if (pos == string::npos) { + continue; + } + string val = m_replacers.get(key, false); + pos = val.find('/'); + if (pos == string::npos) { + continue; + } + auto dest = parseAddress(val); + if (!dest) { + logOtherError("knx", "invalid assignment %s to %s", key.c_str(), val.c_str()); + continue; + } + if (key.substr(0, 7) != "global/") { + messageCnt++; + m_messageFieldGroupAddress[key] = dest; + continue; + } + key = key.substr(7); + global_t index; + dtlf_t lengthFlag = DTLF_1BIT; // default for <=6 bits + if (key == "version") { + index = GLOBAL_VERSION; + lengthFlag.length = 2; + } else if (key == "running") { + index = GLOBAL_RUNNING; + } else if (key == "uptime") { + index = GLOBAL_UPTIME; + lengthFlag.length = 4; + } else if (key == "signal") { + index = GLOBAL_SIGNAL; + } else if (key == "scan") { + index = GLOBAL_SCAN; + } else if (key == "updatecheck") { + index = GLOBAL_UPDATECHECK; + } else { + logOtherError("knx", "invalid assignment global/%s to %s", key.c_str(), val.c_str()); + continue; + } + m_subscribedGlobals[index] = dest|FLAG_READ; + m_subscribedGroups[dest|FLAG_READ] = { + .messageKey = 0, + .globalIndex = index, + .lengthFlag = lengthFlag, + }; + globalCnt++; + } + logOtherInfo("knx", "parsed %d global and %d message assignments", globalCnt, messageCnt); +} + +KnxHandler::~KnxHandler() { + join(); + if (m_con) { + delete m_con; + m_con = nullptr; + } +} + +void KnxHandler::startHandler() { + WaitThread::start("KNX"); +} + +void KnxHandler::notifyUpdateCheckResult(const string& checkResult) { + if (checkResult != m_lastUpdateCheckResult) { + m_lastUpdateCheckResult = checkResult; + sendGlobalValue(GLOBAL_UPDATECHECK, checkResult.empty() || checkResult=="OK" ? 0 : 1); + } +} + +void KnxHandler::notifyScanStatus(scanStatus_t scanStatus) { + if (scanStatus == SCAN_STATUS_FINISHED) { + m_scanFinishReceived = true; + } + if (scanStatus != m_lastScanStatus) { + m_lastScanStatus = scanStatus; + sendGlobalValue(GLOBAL_SCAN, m_lastScanStatus==SCAN_STATUS_RUNNING ? 1 : 0); + } +} + +result_t getFieldLength(const SingleDataField *field, dtlf_t *length) { + const auto dt = field->getDataType(); + if (field->isIgnored() || !dt->isNumeric() || dt->isAdjustableLength()) { + return RESULT_ERR_INVALID_NUM; + } + size_t bitCnt = dt->getBitCount(); + if (bitCnt == 1) { + *length = DTLF_1BIT; + return RESULT_OK; + } + if (bitCnt < 8) { + *length = DTLF_8BIT; + return RESULT_OK; + } + const auto nt = dynamic_cast(dt); + if (nt->getDivisor()!=1) { + // adjust bit count to 2 octet or 4 octet float DPT + if (bitCnt>=24 && bitCnt<31) { + bitCnt = 32; + } else if (bitCnt<16) { + bitCnt = 16; + } + // TODO uncommon divisor (e.g. >100) may not fit into KNX 2-octet float or truncates precision + } else if (bitCnt>=24 && bitCnt<31) { + // adjust bit count for non-existent 24 bit KNX type + bitCnt = 32; + } + *length = {{ + .hasDivisor = nt->getDivisor()!=1, + .isFloat = dt->hasFlag(EXP), + .isSigned = dt->hasFlag(SIG), + .lastValueSent = false, + .length = static_cast(bitCnt/8), + .lastValue = 0, + }}; + return RESULT_OK; +} + +uint32_t floatToInt16(float val) { + // (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent + if (val == 0) { + return 0; + } + bool negative = val < 0; + if (negative) { + val = -val; + } + val *= 100; + int exp = ilogb(val)-10; + if (exp < -10 || exp > 15) { + return 0x7fff; // invalid value DPT 9 + } + auto shift = exp > 0 ? exp : 0; + auto sig = static_cast(val * exp2(-shift)); + uint32_t value = static_cast(shift << 11) | sig; + if (negative) { + return value | 0x8000; + } + return value; +} + +float int16ToFloat(uint16_t val) { + if (val == 0) { + return 0; + } + if (val == 0x7fff) { + return static_cast(0xffffffff); // NaN + } + bool negative = val&0x8000; + int exp = (val>>11)&0xf; + int sig = val&0x7ff; + return static_cast(sig * exp2(exp) * (negative ? -0.01 : 0.01)); +} + +result_t KnxHandler::sendGroupValue(knx_addr_t dest, apci_t apci, dtlf_t& lengthFlag, unsigned int value, const SingleDataField *field) const { + if (!m_con || !m_con->isConnected() || !m_con->getAddress()) { + return RESULT_EMPTY; + } + uint8_t data[] = {0, 0, 0, 0, 0, 0}; + data[0] = static_cast(apci>>8); + data[1] = static_cast(apci&0xff); + int len = 2; + // convert value to dpt + if (lengthFlag.isFloat || lengthFlag.hasDivisor) { + if (!field) { + return RESULT_ERR_INVALID_NUM; + } + auto nt = dynamic_cast(field->getDataType()); + float fval; + result_t ret = nt->getFloatFromRawValue(value, &fval); + if (ret == RESULT_EMPTY) { + // replacement value: + if (lengthFlag.length==2) { + // shall have 0x7fff for DPT 9 + value = 0x7fff; + } else { + return RESULT_ERR_INVALID_NUM; // not encodable + } + } else if (ret != RESULT_OK) { + return ret; + } else if (lengthFlag.length == 2) { + // convert to (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent + value = floatToInt16(fval); + } else if (lengthFlag.length == 4) { + // convert to IEEE 754 + value = floatToUint(fval); + } else { + return RESULT_ERR_INVALID_NUM; // not encodable + } + } + // else signed values: fine as long as length is identical + if (apci==APCI_GROUPVALUE_WRITE && lengthFlag.lastValueSent && lengthFlag.lastValue==value) { + return RESULT_EMPTY; // no need to send the same group value again + } + lengthFlag.lastValue = value; + lengthFlag.lastValueSent = true; + switch (lengthFlag.length) { + case 0: // short value <= 6 bit + data[1] |= static_cast(value&0x3f); + break; + case 1: // 1 octet + data[2] = static_cast(value&0xff); + break; + case 2: // 2 octets + data[2] = static_cast(value>>8); + data[3] = static_cast(value&0xff); + break; + case 4: // 4 octets + data[2] = static_cast(value>>24); + data[3] = static_cast(value>>16); + data[4] = static_cast(value>>8); + data[5] = static_cast(value&0xff); + break; + default: + return RESULT_ERR_INVALID_NUM; + } + len += lengthFlag.length; + const char* err = m_con->sendGroup(dest, len, data); + if (err) { + logOtherError("knx", "unable to send %s, dest %4.4x, len %d", + apci==APCI_GROUPVALUE_WRITE ? "write" : apci==APCI_GROUPVALUE_READ ? "read" : "response", + dest, len); + return RESULT_ERR_SEND; + } + logOtherDebug("knx", "sent %s, dest %4.4x, len %d", + apci==APCI_GROUPVALUE_WRITE ? "write" : apci==APCI_GROUPVALUE_READ ? "read" : "response", + dest, len); + return RESULT_OK; +} + +void KnxHandler::sendGlobalValue(global_t index, unsigned int value, bool response) { + if (!m_con->isConnected() || !m_con->getAddress()) { + return; + } + const auto vit = m_subscribedGlobals.find(index); + if (vit == m_subscribedGlobals.cend()) { + return; + } + auto git = m_subscribedGroups.find(vit->second); + if (git == m_subscribedGroups.end()) { + return; + } + sendGroupValue(static_cast(vit->second&0xffff), + response ? APCI_GROUPVALUE_RESPONSE : APCI_GROUPVALUE_WRITE, + git->second.lengthFlag, value); +} + +result_t KnxHandler::receiveTelegram(int maxlen, knx_transfer_t* typ, uint8_t *buf, int *recvlen, + knx_addr_t *src, knx_addr_t *dest) { + struct timespec tdiff = { + .tv_sec = 2, + .tv_nsec = 0, + }; + if (!m_con->isConnected()) { + return RESULT_ERR_GENERIC_IO; + } + int fd = m_con->getPollFd(); +#ifdef HAVE_PPOLL + nfds_t nfds = 1; + struct pollfd fds[nfds]; + memset(fds, 0, sizeof(fds)); + fds[0].fd = fd; + fds[0].events = POLLIN | POLLERR | POLLHUP | POLLRDHUP; +#else +#ifdef HAVE_PSELECT + fd_set checkfds, exceptfds; + FD_ZERO(&checkfds); + FD_SET(fd, &checkfds); + FD_ZERO(&exceptfds); + FD_SET(fd, &exceptfds); +#endif +#endif + int ret; +#ifdef HAVE_PPOLL + ret = ppoll(fds, nfds, &tdiff, nullptr); +#else +#ifdef HAVE_PSELECT + fd_set readfds = checkfds; + ret = pselect(fd + 1, &readfds, nullptr, &exceptfds, &tdiff, nullptr); +#endif +#endif + bool newData; +#ifdef HAVE_PPOLL + if (ret < 0 || (ret > 0 && (fds[0].revents & (POLLERR | POLLHUP | POLLRDHUP)))) { + return RESULT_ERR_GENERIC_IO; + } + newData = fds[0].revents & POLLIN; +#else +#ifdef HAVE_PSELECT + if (ret < 0 || FD_ISSET(fd, &exceptfds)) { + return RESULT_ERR_GENERIC_IO; + } + newData = FD_ISSET(fd, &readfds); +#endif +#endif + if (!newData) { + // timeout + return RESULT_ERR_TIMEOUT; + } + *typ = m_con->getPollData(maxlen, buf, recvlen, src, dest); + return *typ == KNX_TRANSFER_NONE ? RESULT_EMPTY : RESULT_OK; +} + +/* +void printResponse(knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data) { + int tctrl = data[0]>>2; + int apci = ((data[0]&0x03)<<8) | data[1]; + if ((apci & APCI_GROUPVALUE_READ_MASK) == 0) { + apci &= ~APCI_GROUPVALUE_READ_MASK; + } + int value = len==2 ? data[1]&0x3f : data[2]; // 6 bits or full octet + if (len>3) { + value = (value<<8) | data[3]; // up to 16 bits + } + if (len>4) { + value = (value<<8) | data[4]; // up to 24 bits + } + if (len>5) { + value = (value<<8) | data[5]; // up to 32 bits + } + logOtherDebug("knx", "recv from %4.4x to %4.4x, %s (0x%3.3x, tctrl 0x%2.2x), len %d", src, dest, + apci==APCI_GROUPVALUE_WRITE ? "write" : apci==APCI_GROUPVALUE_READ ? "read" + : apci==APCI_GROUPVALUE_RESPONSE ? "response" : "other", + apci, tctrl, len); +} +*/ + +void KnxHandler::handleReceivedTelegram(knx_transfer_t typ, knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data) { + if (typ == KNX_TRANSFER_GROUP) { + handleGroupTelegram(src, dest, len, data); + return; + } + if (m_con->isProgrammable() && src && m_con->getAddress()) { + handleNonGroupTelegram(typ, src, dest, len, data); + } +} + +void KnxHandler::sendNonGroupDisconnect(knx_addr_t dest) { + uint8_t buf[] = {0x00}; + if (m_con->sendTyp(KNX_TRANSFER_DISCONNECT, dest, 1, buf)) { + logOtherDebug("knx", "cannot send"); + } + m_lastConnectTime = 0; // state=closed + m_waitForAck = false; +} + +// the connection timeout in millis (6 seconds) +#define CONNECTION_TIMEOUT 6000 + +void KnxHandler::handleNonGroupTelegram(knx_transfer_t typ, knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data) { + if (typ == KNX_TRANSFER_NONE) { + return; + } + logOtherNotice("knx", "skipping non-group PDU %3.3x", typ); +} + +void KnxHandler::handleGroupTelegram(knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data) { + time_t now; + time(&now); + int apci = ((data[0]&0x03)<<8) | data[1]; + int groupReadWriteApci = apci & APCI_GROUPVALUE_READ_WRITE_MASK; + if (groupReadWriteApci == APCI_GROUPVALUE_WRITE || groupReadWriteApci == APCI_GROUPVALUE_READ) { + apci = groupReadWriteApci; + } + bool isWrite = apci==APCI_GROUPVALUE_WRITE; + if (apci!=APCI_GROUPVALUE_READ && !isWrite) { + if (m_con->isProgrammingMode()) { + if (apci == APCI_INDIVIDUALADDRESS_READ && m_lastIndividualAddressResponseTime>8, APCI_INDIVIDUALADDRESS_RESPONSE&0xff}; + logOtherNotice("knx", "answering to A_IndividualAddress_Read"); + if (m_con->sendGroup(0, 2, buf)) { + logOtherDebug("knx", "cannot send"); + } else { + m_lastIndividualAddressResponseTime = now; + } + } else if (apci==APCI_INDIVIDUALADDRESS_WRITE && len==4 && !m_con->getAddress() && (data[2]|data[3])) { + m_con->setAddress((data[2]<<8)|data[3]); + m_lastIndividualAddressResponseTime = 0; + logOtherNotice("knx", "received new address %x", m_con->getAddress()); + } + } + return; // neither A_GroupValue_Read nor A_GroupValue_Write (A_GroupValue_Response not used at all) + } + const auto subKey = static_cast(dest | (isWrite ? FLAG_WRITE : FLAG_READ)); + auto sit = m_subscribedGroups.find(subKey); + if (needsLog(lf_other, ll_debug)) { + logOtherDebug("knx", "received %ssubscribed %s from %4.4x to %4.4x, len %d", + sit == m_subscribedGroups.end() ? "un" : "", + apci==APCI_GROUPVALUE_WRITE ? "write" : apci==APCI_GROUPVALUE_READ ? "read" : "response", + src, dest, len); + } + if (sit == m_subscribedGroups.end()) { + return; // address+direction not subscribed + } + if (sit->second.messageKey == 0) { + // global values, only readable + switch (sit->second.globalIndex) { + case GLOBAL_VERSION: + sendGlobalValue(GLOBAL_VERSION, VERSION_INT, true); + break; + case GLOBAL_RUNNING: + sendGlobalValue(GLOBAL_RUNNING, 1, true); + break; + case GLOBAL_UPTIME: + sendGlobalValue(GLOBAL_UPTIME, static_cast(time(nullptr) - m_start), true); + break; + case GLOBAL_SIGNAL: + sendGlobalValue(GLOBAL_SIGNAL, m_busHandler->hasSignal() ? 1 : 0, true); + break; + case GLOBAL_SCAN: + sendGlobalValue(GLOBAL_SCAN, m_lastScanStatus==SCAN_STATUS_RUNNING ? 1 : 0, true); + break; + case GLOBAL_UPDATECHECK: + sendGlobalValue(GLOBAL_UPDATECHECK, m_lastUpdateCheckResult.empty() || m_lastUpdateCheckResult=="OK" || m_lastUpdateCheckResult=="." ? 0 : 1, true); + break; + default: + return; // ignore + } + return; + } + const vector* messages = m_messages->getByKey(sit->second.messageKey); + if (!messages) { + return; + } + Message *msg = nullptr; + ssize_t fieldIndex = sit->second.fieldIndex; + const SingleDataField* field = nullptr; + for (const auto& message : *messages) { + if (!message->isAvailable() || message->getDstAddress() == SYN) { + continue; + } + if ((message->isWrite() && !message->isPassive()) != isWrite) { + if (isWrite || message->getLastUpdateTime() <= 0) { + continue; + } // else: allow potential "write-read" association to read the last written value + } + field = message->getField(fieldIndex); + if (!field) { + continue; + } + if (isWrite) { + msg = message; + break; // best candidate + } + if (!msg) { + msg = message; + } else if (message->getLastUpdateTime() > 0 + && message->getLastUpdateTime() > msg->getLastUpdateTime()) { + // prefer newer updated, even if it is passive + msg = message; + } else if (!message->isPassive()) { + // prefer active read before passive + msg = message; + } + } + if (!msg) { + logOtherInfo("knx", "unable to answer %s request to %4.4x", isWrite ? "write" : "read", dest); + return; + } + result_t res; + const string circuit = msg->getCircuit(), name = msg->getName(), fieldName = msg->getFieldName(fieldIndex); + if (isWrite) { + unsigned int value = len==2 ? data[1]&0x3f : data[2]; // <=6 bits or full octet + if (len>3) { + value = (value<<8) | data[3]; // up to 16 bits + } + if (len>4) { + value = (value<<8) | data[4]; // up to 24 bits + } + if (len>5) { + value = (value<<8) | data[5]; // up to 32 bits + } + // note: a write from KNX updates the message and thus re-sends the write later on again during update check + logOtherNotice("knx", "received write request from %4.4x to %4.4x for %s/%s/%s, value %d", + src, dest, circuit.c_str(), name.c_str(), fieldName.c_str(), value); + // write new field value to bus if possible + // ugly but least intrusive: format single num field value to string to have it parsed back later on + ostringstream str; + // convert value to dpt + auto lengthFlag = sit->second.lengthFlag; + if (lengthFlag.isFloat || lengthFlag.hasDivisor) { + float fval; + if (lengthFlag.length == 2) { + // convert from (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent + fval = int16ToFloat(static_cast(value)); + } else if (lengthFlag.length == 4) { + // convert from IEEE 754 + bool negative = (value & (1u << 31)) != 0; + fval = uintToFloat(value, negative); + } else { + logOtherNotice("knx", "unable to decode write request from %4.4x to %4.4x for %s/%s/%s, value %d", + src, dest, circuit.c_str(), name.c_str(), fieldName.c_str(), value); + return; // not decodable + } + str << static_cast(fval); + } else { + if (lengthFlag.isSigned) { + // signed values: determine sign + uint32_t bit = 1<<(lengthFlag.length*8-1); + if (value & bit) { + value = -(value&~bit); + } + str << static_cast(value); + } else { + str << static_cast(value); + } + } + res = m_busHandler->readFromBus(msg, str.str()); + if (res == RESULT_OK) { + logOtherDebug("knx", "wrote %s %s", circuit.c_str(), name.c_str()); + } else { + logOtherError("knx", "write %s %s: %s", circuit.c_str(), name.c_str(), getResultCode(res)); + } + return; + } + logOtherNotice("knx", "received read request from %4.4x to %4.4x for %s/%s/%s", + src, dest, circuit.c_str(), name.c_str(), fieldName.c_str()); + if (msg->isWrite() && !msg->isPassive()) { // reading last value of a write message + if (now >= msg->getLastUpdateTime() + g_maxWriteAge) { + logOtherInfo("knx", "unable to answer read request to %4.4x on write message", dest); + return; // impossible to answer + } + } else if (now >= msg->getLastUpdateTime() + g_maxReadAge) { + res = m_busHandler->readFromBus(msg, ""); + if (res != RESULT_OK) { + return; + } + } + unsigned int value = 0; + res = msg->decodeLastDataNumField(nullptr, fieldIndex, &value); + if (res == RESULT_OK) { + logOtherDebug("knx", "read %s %s", circuit.c_str(), name.c_str()); + res = sendGroupValue(dest, APCI_GROUPVALUE_RESPONSE, sit->second.lengthFlag, value, field); + } else { + logOtherError("knx", "read %s %s: %s", circuit.c_str(), name.c_str(), getResultCode(res)); + } +} + +// interval in seconds for sending the uptime value +#define UPTIME_INTERVAL 3600 + +void KnxHandler::run() { + time_t lastTaskRun, now, lastSignal = 0, lastUptime = 0, lastUpdates = 0; + bool signal = false; + result_t result = RESULT_OK; + time(&now); + m_start = lastTaskRun = now; + uint8_t data[256]; + int len = 0; + time_t definitionsSince = 0; + while (isRunning()) { + bool wasConnected = m_con->isConnected(); + bool needsWait = true; + if (!wasConnected) { + const char* err = m_con->open(); + if (!err) { + m_lastErrorLogTime = 0; + logOtherNotice("knx", "connected to %s", m_con->getInfo()); + sendGlobalValue(GLOBAL_VERSION, VERSION_INT); + sendGlobalValue(GLOBAL_RUNNING, 1); + } + if (err) { + m_con->close(); + time(&now); + if (now > m_lastErrorLogTime + 10) { // log at most every 10 seconds + m_lastErrorLogTime = now; + logOtherError("knx", err); + } + } + } + bool reconnected = !wasConnected && m_con->isConnected(); + time(&now); + bool sendSignal = reconnected; + if (now < m_start) { + // clock skew + if (now < lastSignal) { + lastSignal -= lastTaskRun-now; + } + lastTaskRun = now; + } else if (now > lastTaskRun+(m_scanFinishReceived ? 1 : 15)) { + m_scanFinishReceived = false; + if (m_con->isConnected()) { + sendSignal = true; + if (now > lastUptime + UPTIME_INTERVAL) { + lastUptime = now; + sendGlobalValue(GLOBAL_UPTIME, static_cast(now - m_start)); + } + } + if (m_con->isConnected() && definitionsSince == 0) { + definitionsSince = 1; + } + if (m_con->isConnected()) { + deque messages; + m_messages->findAll("", "", m_levels, false, true, true, true, true, true, 0, 0, true, &messages); + int addCnt = 0; + for (const auto& message : messages) { + const auto mit = m_subscribedMessages.find(message->getKey()); + if (mit != m_subscribedMessages.cend()) { + continue; // already subscribed + } + if (message->getDstAddress() == SYN) { + continue; // not usable in absence of destination address + } + bool isWrite = message->isWrite() && !message->isPassive(); // from KNX perspective + if (message->getCreateTime() <= definitionsSince) { // only newer defined + continue; + } + ssize_t fieldCount = static_cast(message->getFieldCount()); + if (isWrite && fieldCount>1) { + // impossible with more than one field + continue; + } + bool added = false; + for (ssize_t index = 0; index < fieldCount; index++) { + const SingleDataField* field = message->getField(index); + if (!field || field->isIgnored()) { + continue; + } + string fieldName = message->getFieldName(index); + if (fieldName.empty() && fieldCount == 1) { + fieldName = "0"; // might occur for unnamed single field sets + } + string key = message->getCircuit()+"/"+message->getName()+"/"+fieldName; + const auto git = m_messageFieldGroupAddress.find(key); + if (git == m_messageFieldGroupAddress.cend()) { + continue; + } + // determine field length in telegram + dtlf_t lengthFlag = {}; + result = getFieldLength(field, &lengthFlag); + if (result != RESULT_OK) { + continue; + } + // store association + knx_addr_t dest = git->second; + auto subKey = static_cast(dest | (isWrite ? FLAG_WRITE : FLAG_READ)); + auto sit = m_subscribedGroups.find(subKey); + if (sit != m_subscribedGroups.cend()) { + if (isWrite) { + logOtherDebug("knx", "ignored already subscribed %s", key.c_str()); + continue; + } + if (sit->second.messageKey == message->getKey()) { + continue; + } // else: overwrite "write-read" with readable message + logOtherDebug("knx", "replacing write-read association %s to %4.4x", key.c_str(), dest); + } + m_subscribedGroups[subKey] = { + .messageKey = message->getKey(), + .fieldIndex = static_cast(index), + .lengthFlag = lengthFlag, + }; + m_subscribedMessages[message->getKey()].push_back(subKey); + logOtherDebug("knx", "added %s association %s to %4.4x", isWrite ? "write" : "read", key.c_str(), dest); + if (isWrite) { + // add "write-read" association to allow reading the last written value of a writable message + // when there is no readable message set directly yet + subKey = static_cast(dest | FLAG_READ); + sit = m_subscribedGroups.find(subKey); + if (sit == m_subscribedGroups.cend()) { + m_subscribedGroups[subKey] = { + .messageKey = message->getKey(), + .fieldIndex = static_cast(index), + .lengthFlag = lengthFlag, + }; + logOtherDebug("knx", "added write-read association %s to %4.4x", key.c_str(), dest); + } + } + added = true; + addCnt++; + } + if (!added) { + continue; + } + if (message->getLastUpdateTime() > message->getCreateTime()) { + // ensure data is published as well + m_updatedMessages[message->getKey()]++; + } else if (message->isWrite()) { + // publish data for read pendant of write message + Message* read = m_messages->find(message->getCircuit(), message->getName(), "", false); + if (read && read->getLastUpdateTime() > 0) { + m_updatedMessages[read->getKey()]++; + } + } + } + if (addCnt>0) { + logOtherInfo("knx", "added %d associations, %d active now", addCnt, m_subscribedGroups.size()); + } + definitionsSince = now; + needsWait = true; + } + time(&lastTaskRun); + } + if (sendSignal) { + if (m_busHandler->hasSignal()) { + lastSignal = now; + if (!signal || reconnected) { + signal = true; + sendGlobalValue(GLOBAL_SIGNAL, 1); + } + } else { + if (signal || reconnected) { + signal = false; + sendGlobalValue(GLOBAL_SIGNAL, 0); + } + } + } + if (m_con->isConnected()) { + if (reconnected) { + // reset the state machine + m_lastConnectTime = 0; + m_waitForAck = false; + } + handleReceivedTelegram(KNX_TRANSFER_NONE, 1, 0, 0, data); // check timeout + knx_addr_t src, dest; + knx_transfer_t typ; + // APDU data starting with octet 6 according to spec, contains 2 bits of application layer + result_t res = RESULT_OK; + do { + res = receiveTelegram(sizeof(data), &typ, data, &len, &src, &dest); + if (res != RESULT_OK) { + if (res == RESULT_ERR_GENERIC_IO) { + m_con->close(); + } + } else { + needsWait = false; + handleReceivedTelegram(typ, src, dest, len, data); + } + } while (res == RESULT_OK); + } + if (!m_updatedMessages.empty()) { + m_messages->lock(); + if (m_con->isConnected()) { + for (auto it = m_updatedMessages.begin(); it != m_updatedMessages.end(); ) { + const vector* messages = m_messages->getByKey(it->first); + if (!messages) { + continue; + } + for (const auto& message : *messages) { + if (message->getLastChangeTime() <= 0) { + continue; + } + const auto mit = m_subscribedMessages.find(message->getKey()); + if (mit == m_subscribedMessages.cend()) { + continue; + } + if (!(message->getDataHandlerState()&2)) { + message->setDataHandlerState(2, true); // first update still needed + } else if (message->getLastChangeTime() <= lastUpdates) { + continue; + } + for (auto destFlags : mit->second) { + auto sit = m_subscribedGroups.find(destFlags); + if (sit == m_subscribedGroups.end()) { + continue; + } + ssize_t index = sit->second.fieldIndex; + const SingleDataField *field = message->getField(index); + if (!field || field->isIgnored()) { + continue; + } + knx_addr_t dest = destFlags&0xffff; + unsigned int value = 0; + result = message->decodeLastDataNumField(nullptr, index, &value); + sendGroupValue(dest, APCI_GROUPVALUE_WRITE, sit->second.lengthFlag, value, field); + } + } + it = m_updatedMessages.erase(it); + } + time(&lastUpdates); + } else { + m_updatedMessages.clear(); + } + m_messages->unlock(); + } + if ((!m_con->isConnected() && !Wait(5)) || (needsWait && !Wait(0, 100)) + ) { + break; + } + } + sendGlobalValue(GLOBAL_RUNNING, 0); + sendGlobalValue(GLOBAL_SIGNAL, 0); + sendGlobalValue(GLOBAL_SCAN, 0); +} + +} // namespace ebusd diff --git a/src/ebusd/knxhandler.h b/src/ebusd/knxhandler.h new file mode 100644 index 00000000..375d2799 --- /dev/null +++ b/src/ebusd/knxhandler.h @@ -0,0 +1,284 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * 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 3 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, see . + */ + +#ifndef EBUSD_KNXHANDLER_H_ +#define EBUSD_KNXHANDLER_H_ + +#include +#include +#include +#include +#include +#include "ebusd/datahandler.h" +#include "ebusd/bushandler.h" +#include "lib/ebus/message.h" +#include "lib/ebus/stringhelper.h" +#include "lib/knx/knx.h" + +namespace ebusd { + +/** @file ebusd/knxhandler.h + * A data handler enabling KNX support. + */ + +using std::map; +using std::string; +using std::vector; + +/** + * Helper function for getting the argp definition for KNX. + * @return a pointer to the argp_child structure. + */ +const struct argp_child* knxhandler_getargs(); + +/** + * Registration function that is called once during initialization. + * @param userInfo the @a UserInfo instance. + * @param busHandler the @a BusHandler instance. + * @param messages the @a MessageMap instance. + * @param handlers the @a list to which new @a DataHandler instances shall be added. + * @return true if registration was successful. + */ +bool knxhandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages, + list* handlers); + + +/** type for KNX APCI values (application control field). */ +enum apci_t { + // within KNX_TRANSFER_GROUP: + APCI_GROUPVALUE_READ = 0x000, //!< A_GroupValue_Read-PDU + APCI_GROUPVALUE_RESPONSE = 0x040, //!< A_GroupValue_Response-PDU (mask APCI_GROUPVALUE_READ_WRITE_MASK) + APCI_GROUPVALUE_WRITE = 0x080, //!< A_GroupValue_Write-PDU (mask APCI_GROUPVALUE_READ_WRITE_MASK) + APCI_INDIVIDUALADDRESS_READ = 0x100, //!< A_IndividualAddress_Read-PDU + APCI_INDIVIDUALADDRESS_RESPONSE = 0x140, //!< A_IndividualAddress_Response-PDU + APCI_INDIVIDUALADDRESS_WRITE = 0x0c0, //!< A_IndividualAddress_Write-PDU + // within KNX_TRANSFER_CONNECTED: + APCI_DEVICEDESCRIPTOR_READ = 0x300, //!< A_DeviceDescriptor_Read-PDU + APCI_DEVICEDESCRIPTOR_RESPONSE = 0x340, //!< A_DeviceDescriptor_Read-PDU (mask should be 0x3c0) + APCI_PROPERTYVALUE_READ = 0x3d5, //!< A_PropertyValue_Read-PDU + APCI_PROPERTYVALUE_RESPONSE = 0x3d6, //!< A_PropertyValue_Response-PDU + APCI_PROPERTYVALUE_WRITE = 0x3d7, //!< A_PropertyValue_Write-PDU + APCI_RESTART = 0x380, //!< A_Restart-PDU +}; + +#define APCI_GROUPVALUE_READ_WRITE_MASK 0x3c0 + +#define FLAG_READ 0x400000 +#define FLAG_WRITE 0x800000 + +/** datatype length flags (byte length on KNX in bits 0-3, extra info in higher bits). */ +typedef union { + struct { + bool hasDivisor: 1; + bool isFloat: 1; + bool isSigned: 1; + bool lastValueSent: 1; + uint8_t length; // 0 for 1-6 bits, number of bytes otherwise + uint32_t lastValue; + }; + uint64_t value; +} dtlf_t; + +#define DTLF_1BIT {.length = 0} +#define DTLF_8BIT {.length = 1} + +/** type for global values not associated with an ebus message. */ +enum global_t { + GLOBAL_VERSION = 1, + GLOBAL_RUNNING = 2, + GLOBAL_UPTIME = 3, + GLOBAL_SIGNAL = 4, + GLOBAL_SCAN = 5, + GLOBAL_UPDATECHECK = 6, +}; + +/** type for several group subscription infos. */ +typedef struct { + uint64_t messageKey; // message key, or 0 for global value + union { + uint8_t fieldIndex; // message field index + global_t globalIndex; // global value index + }; + dtlf_t lengthFlag; // telegram length and flags +} groupInfo_t; + + +/** + * The main class supporting KNX data handling. + */ +class KnxHandler : public DataSink, public DataSource, public WaitThread { + public: + /** + * Constructor. + * @param userInfo the @a UserInfo instance. + * @param busHandler the @a BusHandler instance. + * @param messages the @a MessageMap instance. + */ + KnxHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages); + + public: + /** + * Destructor. + */ + ~KnxHandler() override; + + // @copydoc + void startHandler() override; + + // @copydoc + void notifyUpdateCheckResult(const string& checkResult) override; + + // @copydoc + void notifyScanStatus(scanStatus_t scanStatus) override; + + /** + * Send a group value. + * @param dest the destination group address. + * @param apci the APCI value. + * @param lengthFlag the datatype length flag. + * @param value the value. + * @param field the message field or nullptr for non field related. + * @return the result code. + */ + result_t sendGroupValue(knx_addr_t dest, apci_t apci, dtlf_t& lengthFlag, unsigned int value, const SingleDataField *field = nullptr) const; + + /** + * Send a global value to the registered group address. + * @param index the global value index to send. + * @param value the raw value. + * @param response true to send as response, false to send as write. + */ + void sendGlobalValue(global_t index, unsigned int value, bool response = false); + + /** + * Wait for and receive a KNX group telegram. + * @param maxlen the size of the data buffer. + * @param buf the data buffer. + * @param recvlen pointer to a variable in which to store the actually received length. + * @param src pointer to a variable in which to store the source address. + * @param dest pointer to a variable in which to store the destination group address. + * @return the result code, either RESULT_OK on success, RESULT_ERR_GENERIC_IO on I/O error (e.g. socket closed), + * or RESULT_ERR_TIMEOUT if no data is available. + */ + result_t receiveTelegram(int maxlen, knx_transfer_t* typ, uint8_t *buf, int *recvlen, knx_addr_t *src, knx_addr_t *dest); + + /** + * Handle a received KNX telegram. + * @param typ the transfer data type. + * @param src the source address. + * @param dest the destination group address. + * @param len the data length (including the TPCI/APCI octet 6, i.e. transport control field). + * @param data the data buffer (starting with the TPCI/APCI octet 6, i.e. transport control field). + */ + void handleReceivedTelegram(knx_transfer_t typ, knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data); + + protected: + // @copydoc + void run() override; + + /** + * Handle a received non-group telegram when the device has an individual address and is programmable. + * @param typ the transfer data type. + * @param src the source address (ensured to be non-zero). + * @param dest the destination address (group or individual according to address type encoded in the transfer data type). + * @param len the data length (including the TPCI/APCI octet 6, i.e. transport control field). + * @param data the data buffer (starting with the TPCI/APCI octet 6, i.e. transport control field). + */ + void handleNonGroupTelegram(knx_transfer_t typ, knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data); + + /** + * Send a DISCONNECT to the destination and reset the connected state. + * @param dest the destination individual address to send to. + */ + void sendNonGroupDisconnect(knx_addr_t dest); + + /** + * Handle a received group telegram. + * @param src the source address. + * @param dest the destination group address. + * @param len the data length (including the TPCI/APCI octet 6, i.e. transport control field). + * @param data the data buffer (starting with the TPCI/APCI octet 6, i.e. transport control field). + */ + void handleGroupTelegram(knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data); + + private: + /** the @a MessageMap instance. */ + MessageMap* m_messages; + + /** the @a StringReplacers from the integration file. */ + StringReplacers m_replacers; + + /** the group address for relevant message fields before being subscribed to by "circuit/message/field" name. */ + map m_messageFieldGroupAddress; + + /** + * the group addresses that need to be responded to. + * key is the group address in lower 16 bits, and flags in upper 16 bits with: + * - read direction in bit 6 (<<16), + * - write direction in bit 7 (<<16). + * this way read and write may be mapped to different messages. + * value contains the message key and additional infos. + */ + mapm_subscribedGroups; + + /** the group address and flags (key of m_subscribedGroups) by subscribed message key. */ + map>m_subscribedMessages; + + /** the group address and flags (key of m_subscribedGroups) by subscribed global values. */ + mapm_subscribedGlobals; + + /** the time the run thread was entered. */ + time_t m_start; + + /** the knx connection as long as initialized, or nullptr. */ + KnxConnection* m_con; + + /** the time of the last sent individual address response, or 0. */ + time_t m_lastIndividualAddressResponseTime = 0; + + /** the time of the last connection, or 0 if not connected. */ + long long m_lastConnectTime = 0; + + /** the source address of the last connection, or 0. */ + knx_addr_t m_lastConnectSource = 0; + + /** the SeqNo for reception of the last connection. */ + uint8_t m_lastConnectRecvSeq = 0; + + /** the SeqNo for sending of the last connection. */ + uint8_t m_lastConnectSendSeq = 0; + + /** true when last connection is in state OPEN_WAIT. */ + bool m_waitForAck = false; + + /** the last update check result. */ + string m_lastUpdateCheckResult; + + /** the last scan status. */ + scanStatus_t m_lastScanStatus; + + /** set to true when a scan finish was received. */ + bool m_scanFinishReceived; + + /** the last system time when a communication error was logged. */ + long long m_lastErrorLogTime; +}; + +} // namespace ebusd + +#endif // EBUSD_KNXHANDLER_H_ diff --git a/src/ebusd/mainloop.cpp b/src/ebusd/mainloop.cpp index 61f2d97c..ea43e289 100644 --- a/src/ebusd/mainloop.cpp +++ b/src/ebusd/mainloop.cpp @@ -326,6 +326,10 @@ void MainLoop::run() { if (m_messages->sizeConditions() > 0 && !m_polling) { logError(lf_main, "conditions require a poll interval > 0"); } + // notify data sinks to make them update the messages + for (const auto dataSink : dataSinks) { + dataSink->notifyScanStatus(SCAN_STATUS_FINISHED); + } } if (m_runUpdateCheck && !m_shutdown && now > nextCheckRun) { if (!m_httpClient.connect("upd.ebusd.eu", diff --git a/src/lib/ebus/datatype.cpp b/src/lib/ebus/datatype.cpp index d3108f69..d321bd4d 100755 --- a/src/lib/ebus/datatype.cpp +++ b/src/lib/ebus/datatype.cpp @@ -793,10 +793,72 @@ result_t NumberDataType::readSymbols(size_t offset, size_t length, const SymbolS return readFromRawValue(value, outputFormat, output); } +result_t NumberDataType::getFloatFromRawValue(unsigned int value, float* output) const { + if (!hasFlag(REQ) && value == m_replacement) { + return RESULT_EMPTY; + } + + bool negative; + if (hasFlag(SIG)) { // signed value + negative = (value & (1 << (m_bitCount - 1))) != 0; + if (negative) { // negative signed value + if (value < m_minValue) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + } else if (value > m_maxValue) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + } else if (value < m_minValue || value > m_maxValue) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } else { + negative = false; + } + int signedValue; + if (m_bitCount == 32) { + if (hasFlag(EXP)) { // IEEE 754 binary32 + float val = uintToFloat(value, negative); + if (val != val) { // !isnan(val) + return RESULT_EMPTY; + } + if (val != 0.0) { + if (m_divisor < 0) { + val *= static_cast(-m_divisor); + } else if (m_divisor > 1) { + val /= static_cast(m_divisor); + } + } + *output = static_cast(val); + return RESULT_OK; + } + if (!negative) { + if (m_divisor < 0) { + *output = static_cast(value) * static_cast(-m_divisor); + } else if (m_divisor <= 1) { + *output = static_cast(value); + } else { + *output = static_cast(value) / static_cast(m_divisor); + } + return RESULT_OK; + } + signedValue = static_cast(value); // negative signed value + } else if (negative) { // negative signed value + signedValue = static_cast(value) - (1 << m_bitCount); + } else { + signedValue = static_cast(value); + } + if (m_divisor < 0) { + *output = static_cast(signedValue) * static_cast(-m_divisor); + } else if (m_divisor <= 1) { + *output = static_cast(signedValue); + } else { + *output = static_cast(signedValue) / static_cast(m_divisor); + } + return RESULT_OK; +} + result_t NumberDataType::readFromRawValue(unsigned int value, OutputFormat outputFormat, ostream* output) const { size_t length = (m_bitCount < 8) ? 1 : (m_bitCount/8); - int signedValue; // initialize output *output << setw(0) << std::resetiosflags(output->flags()) << dec << std::skipws << setprecision(6); @@ -824,6 +886,7 @@ result_t NumberDataType::readFromRawValue(unsigned int value, } else { negative = false; } + int signedValue; if (m_bitCount == 32) { if (hasFlag(EXP)) { // IEEE 754 binary32 float val = uintToFloat(value, negative); @@ -932,6 +995,75 @@ result_t NumberDataType::writeRawValue(unsigned int value, size_t offset, size_t return RESULT_OK; } +result_t NumberDataType::getRawValueFromFloat(float val, unsigned int* output) const { + unsigned int value; + if (hasFlag(EXP)) { // IEEE 754 binary32 + double dvalue = val; + if (m_divisor < 0) { + dvalue /= -m_divisor; + } else if (m_divisor > 1) { + dvalue *= m_divisor; + } + value = floatToUint(static_cast(dvalue)); + if (value == 0xffffffff) { + return RESULT_ERR_INVALID_NUM; + } + } else { + if (m_divisor == 1) { + if (hasFlag(SIG)) { + long signedValue = static_cast(val); // TODO static_c? + if (signedValue < 0 && m_bitCount != 32) { + value = (unsigned int)(signedValue + (1 << m_bitCount)); + } else { + value = (unsigned int)signedValue; + } + } else if (val < 0) { + return RESULT_ERR_INVALID_NUM; // invalid value + } else { + value = static_cast(val); + } + } else { + double dvalue = val; + if (m_divisor < 0) { + dvalue = round(dvalue / -m_divisor); + } else { + dvalue = round(dvalue * m_divisor); + } + int length = static_cast(m_bitCount/8); + if (hasFlag(SIG)) { + if (dvalue < -exp2((8 * static_cast(length)) - 1) + || dvalue >= exp2((8 * static_cast(length)) - 1)) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + if (dvalue < 0 && m_bitCount != 32) { + value = static_cast(dvalue + (1 << m_bitCount)); + } else { + value = static_cast(dvalue); + } + } else { + if (dvalue < 0.0 || dvalue >= exp2(8 * static_cast(length))) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + value = (unsigned int)dvalue; + } + } + + if (hasFlag(SIG)) { // signed value + if ((value & (1 << (m_bitCount - 1))) != 0) { // negative signed value + if (value < m_minValue) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + } else if (value > m_maxValue) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + } else if (value < m_minValue || value > m_maxValue) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + } + *output = value; + return RESULT_OK; +} + result_t NumberDataType::writeSymbols(size_t offset, size_t length, istringstream* input, SymbolString* output, size_t* usedLength) const { unsigned int value; diff --git a/src/lib/ebus/datatype.h b/src/lib/ebus/datatype.h index d575001e..388a4126 100755 --- a/src/lib/ebus/datatype.h +++ b/src/lib/ebus/datatype.h @@ -540,6 +540,22 @@ class NumberDataType : public DataType { result_t readSymbols(size_t offset, size_t length, const SymbolString& input, const OutputFormat outputFormat, ostream* output) const override; + /** + * Convert the numeric raw value to its float representation (including optional divisor). + * @param value the numeric raw value. + * @param output the float variable to write the value to. + * @return @a RESULT_OK on success, or an error code. + */ + result_t getFloatFromRawValue(unsigned int value, float* output) const; + + /** + * Convert the float value to the numeric raw value (including optional divisor). + * @param value the float value. + * @param output the variable to write the numeric raw value to. + * @return @a RESULT_OK on success, or an error code. + */ + result_t getRawValueFromFloat(float value, unsigned int* output) const; + /** * Internal method for interpreting a numeric raw value. * @param value the numeric raw value. diff --git a/src/lib/knx/CMakeLists.txt b/src/lib/knx/CMakeLists.txt new file mode 100644 index 00000000..15863be2 --- /dev/null +++ b/src/lib/knx/CMakeLists.txt @@ -0,0 +1,17 @@ +add_definitions(-Wconversion -Wno-unused-parameter) + +set(libknx_a_SOURCES + knx.h knx.cpp +) + +set(libknx_a_LIBS +) + +if(HAVE_KNXD) + set(libknx_a_SOURCES ${libknx_a_SOURCES} knxd.h) + set(libknx_a_LIBS ${libknx_a_LIBS} eibclient) +endif(HAVE_KNXD) + +add_library(knx ${libknx_a_SOURCES}) + +target_link_libraries(knx ${libknx_a_LIBS}) \ No newline at end of file diff --git a/src/lib/knx/Makefile.am b/src/lib/knx/Makefile.am new file mode 100644 index 00000000..4768d5e6 --- /dev/null +++ b/src/lib/knx/Makefile.am @@ -0,0 +1,17 @@ +AM_CXXFLAGS = -I$(top_srcdir)/src \ + -isystem$(top_srcdir) \ + -Wno-conversion + +noinst_LIBRARIES = libknx.a + +libknx_a_SOURCES = \ + knx.h knx.cpp + +if KNXD +libknx_a_SOURCES += knxd.h +else +libknx_a_SOURCES += knxnet.h +endif + +distclean-local: + -rm -f Makefile.in diff --git a/src/lib/knx/knx.cpp b/src/lib/knx/knx.cpp new file mode 100644 index 00000000..8988caed --- /dev/null +++ b/src/lib/knx/knx.cpp @@ -0,0 +1,101 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "lib/knx/knx.h" + +#ifdef HAVE_KNXD +#include "lib/knx/knxd.h" +#endif +#include "lib/knx/knxnet.h" + +#include + +namespace ebusd { + +unsigned int parseInt(const char* str, int base, unsigned int minValue, unsigned int maxValue, + bool* error) { + char* strEnd = nullptr; + + unsigned long ret = strtoul(str, &strEnd, base); + + if (strEnd == nullptr || strEnd == str || *strEnd != 0) { + *error = true; // invalid value + return 0; + } + + if (minValue > ret || ret > maxValue) { + *error = true; // invalid value + return 0; + } + return (unsigned int)ret; +} + +knx_addr_t parseAddress(const string &str, bool isGroup, bool* error) { + auto sep = isGroup ? '/' : '.'; + auto pos = str.find(sep); + if (pos != string::npos) { + auto pos2 = str.find(sep, pos+1); + bool err = false; + unsigned int v = 0; + v = parseInt(str.substr(0, pos).c_str(), 10, 0, isGroup ? 0x1f : 0x0f, &err); + if (!err) { + auto dest = static_cast(v << (isGroup ? 11 : 12)); + if (pos2 == string::npos) { + // 2 level + if (isGroup) { + v = parseInt(str.substr(pos+1).c_str(), 10, 0, 0x7ff, &err); + if (!err) { + dest |= static_cast(v); + return dest; + } + } + } else { + // 3 level + v = parseInt(str.substr(pos+1, pos2-pos-1).c_str(), 10, 0, isGroup ? 0x07 : 0x0f, &err); + if (!err) { + dest |= static_cast(v << 8); + v = parseInt(str.substr(pos2+1).c_str(), 10, 0, 0xff, &err); + if (!err) { + dest |= static_cast(v); + return dest; + } + } + } + } + } + if (error) { + *error = true; + } + return 0; +} + +// copydoc +KnxConnection *KnxConnection::create(const char *url) { +#ifdef HAVE_KNXD + if (strchr(url, ':')) { + return new KnxdConnection(url); + } +#endif + return new KnxNetConnection(url); +} + +} // namespace ebusd diff --git a/src/lib/knx/knx.h b/src/lib/knx/knx.h new file mode 100644 index 00000000..8b44a48b --- /dev/null +++ b/src/lib/knx/knx.h @@ -0,0 +1,186 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * 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 3 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, see . + */ + +#ifndef LIB_KNX_KNX_H_ +#define LIB_KNX_KNX_H_ + +#include +#include + +namespace ebusd { + +/** @file lib/knx/knx.h + * Classes, functions, and constants related to KNX. + */ + +using std::string; + + +/** base KNX address type (group or individual). */ +typedef uint16_t knx_addr_t; + +/** special default address value. */ +#define DEFAULT_ADDRESS 0xffff + +/** the transfer types (lower 8 bits of transport control field with sequence=0, plus bit 8 with address type). */ +enum knx_transfer_t { + // no transfer available + KNX_TRANSFER_NONE = -1, + // data group or broadcast PDU + KNX_TRANSFER_GROUP = 0x100, + // data tag group PDU + KNX_TRANSFER_TAG_GROUP = 0x104, + // data individual PDU + KNX_TRANSFER_INDIVIDUAL = 0x000, + // data connected PDU + KNX_TRANSFER_CONNECTED = 0x040, + // connect PDU + KNX_TRANSFER_CONNECT = 0x080, + // disconnect PDU + KNX_TRANSFER_DISCONNECT = 0x081, + // ACK PDU + KNX_TRANSFER_ACK = 0x0c2, + // NAK PDU + KNX_TRANSFER_NAK = 0x0c3, +}; + + +/** + * Parse a group address in the form "A/B/C" or "A/B" or an individual address in the form "A.B.C". + * @param str the group address string to parse. + * @param error optional variable to set to true in case of an invalid address string. + * @return the parsed address, or 0 on error. + */ +knx_addr_t parseAddress(const string &str, bool isGroup = true, bool* error = nullptr); + +/** + * An abstract KNX connection. + */ +class KnxConnection { + public: + /** + * Construct a new instance. + */ + KnxConnection() {} + + /** + * Destructor. + */ + virtual ~KnxConnection() {} + + /** + * Create a new KnxConnection. + * @param url the URL to connect to in the form "[multicast][@interface]" (for KNXnet/IP) or "ip:host[:port]" / + * "local:/socketpath" for knxd (if compiled in). + * @return the new KnxConnection, or @a nullptr on error. + */ + static KnxConnection* create(const char* url); + + /** + * @return additional infos about this connection for logging. + */ + virtual const char* getInfo() const = 0; + + /** + * Open a connection to the specified URL. + * @return nullptr on success, or an error message. + */ + virtual const char* open() = 0; + + /** + * @return true if connected, false otherwise. + */ + virtual bool isConnected() const = 0; + + /** + * Close the connection. + */ + virtual void close() = 0; + + /** + * @return the file descriptor for polling. + */ + virtual int getPollFd() const = 0; + + /** + * Get the available data (after the file descriptor was checked for availability). + * @param size the size of the data buffer. + * @param data the data buffer to copy the data to. + * @param len pointer to store the actual data length to. + * @param src optional pointer to store the source address (if any, depending on poll data type). + * @param dst optional pointer to store the destination address (if any, depending on poll data type). + * @return the polled transfer type. + */ + virtual knx_transfer_t getPollData(int size, uint8_t* data, int* len, knx_addr_t* src, knx_addr_t* dst) = 0; + + /** + * Send a group APDU. + * @param dst the destination address. + * @param len the APDU length. + * @param data the APDU data buffer. + * @return nullptr on success, or an error message. + */ + virtual const char* sendGroup(knx_addr_t dst, int len, const uint8_t* data) = 0; + + /** + * Send a non-group APDU. + * @param typ the transfer type to send. + * @param dst the destination address. + * @param len the APDU length. + * @param data the APDU data buffer. + * @return nullptr on success, or an error message. + */ + virtual const char* sendTyp(knx_transfer_t typ, knx_addr_t dst, int len, const uint8_t* data) = 0; + + /** + * @return true if connection allows programming via ETS. + */ + virtual bool isProgrammable() const { return false; }; + + /** + * @return the individual address, or 0 if not programmed yet, or any non-zero value if not programmable. + */ + virtual knx_addr_t getAddress() { return DEFAULT_ADDRESS; }; + + /** + * @param address the individual address to set. + */ + virtual void setAddress(knx_addr_t address) { + // default implementation does nothing + } + + /** + * Get the programming mode. + * @return true when in programming mode, false if not. + */ + virtual bool isProgrammingMode() { + return false; + } + + /** + * Set the programming mode. + * @param on true to start programming mode, false to stop it. + */ + virtual void setProgrammingMode(bool on) { + // default implementation does nothing + } +}; + +} // namespace ebusd + +#endif // LIB_KNX_KNX_H_ \ No newline at end of file diff --git a/src/lib/knx/knxd.h b/src/lib/knx/knxd.h new file mode 100644 index 00000000..547d3212 --- /dev/null +++ b/src/lib/knx/knxd.h @@ -0,0 +1,124 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * 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 3 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, see . + */ + +#ifndef LIB_KNX_KNXD_H_ +#define LIB_KNX_KNXD_H_ + +#include +#include "lib/knx/knx.h" + +namespace ebusd { + +/** + * A KnxConnection based on libeibclient using the group communication interface of the connected KNXd. + * Unfortunately, this does not allow acting as a KNX device, i.e. enter programming mode and make individual address + * and group association table writable from ETS. As such, an KNXnet/IP implementation is available as well. + */ +class KnxdConnection : public KnxConnection { + public: + /** + * Construct a new instance. + */ + KnxdConnection(const char *url) + : KnxConnection(), m_url(url), m_con(nullptr) {} + + /** + * Destructor. + */ + virtual ~KnxdConnection() { + close(); + } + + // @copydoc + const char* getInfo() const override { + return "KNXd"; + } + + // @copydoc + const char* open() override { + close(); + m_con = EIBSocketURL(m_url); + if (!m_con) { + return "open error"; + } + if (EIBOpen_GroupSocket(m_con, 0) < 0) { + EIBClose_sync(m_con); + m_con = nullptr; + return "open group error"; + } + return nullptr; + } + + // @copydoc + bool isConnected() const override { + return m_con != nullptr; + } + + void close() override { + if (m_con) { + EIBClose_sync(m_con); + m_con = nullptr; + } + } + + // @copydoc + int getPollFd() const override { + return EIB_Poll_FD(m_con); + } + + // @copydoc + knx_transfer_t getPollData(int size, uint8_t* data, int* len, knx_addr_t* src, knx_addr_t* dst) override { + int ret = EIB_Poll_Complete(m_con); + if (ret == -1) { + // read failed + return KNX_TRANSFER_NONE; + } + ret = EIBGetGroup_Src(m_con, size, data, src, dst); + if (ret < 2) { + return KNX_TRANSFER_NONE; + } + if (len) { + *len = ret; + } + return KNX_TRANSFER_GROUP; + } + + // @copydoc + const char* sendGroup(knx_addr_t dst, int len, const uint8_t* data) override { + if (EIBSendGroup(m_con, dst, len, data) < 0) { + return "send error"; + } + return nullptr; + } + + // @copydoc + const char* sendTyp(knx_transfer_t typ, knx_addr_t dst, int len, const uint8_t* data) override { + return "not available"; + } + + private: + /** the URL to connect to. */ + const char* m_url; + + /** the knx structure if connected, or nullptr. */ + EIBConnection* m_con; +}; + +} // namespace ebusd + +#endif // LIB_KNX_KNXD_H_ \ No newline at end of file diff --git a/src/lib/knx/knxnet.h b/src/lib/knx/knxnet.h new file mode 100644 index 00000000..7bcab6e2 --- /dev/null +++ b/src/lib/knx/knxnet.h @@ -0,0 +1,729 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * 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 3 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, see . + */ + +#ifndef LIB_KNX_KNXNET_H_ +#define LIB_KNX_KNXNET_H_ + +#include +#include +#include +#include +#include +#ifndef __CYGWIN__ +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include "lib/knx/knx.h" + +namespace ebusd { + +using std::string; + +// 16 bit unsigned big endian +typedef union __attribute__ ((packed)) { + uint16_t raw; + struct { + uint8_t high; + uint8_t low; + }; +} uint16be_t; + +// 32 bit unsigned big endian +typedef union __attribute__ ((packed)) { + uint32_t raw; + struct { + uint8_t msb1; + uint8_t msb2; + uint8_t msb3; + uint8_t lsb; + }; +} uint32be_t; + +// KNXnet/IP header +typedef struct __attribute__ ((packed)) { + uint8_t headerLength; // =6 + uint8_t protocolVersion; // =0x10 + uint16be_t serviceTypeIdentifier; + uint16be_t totalLength; // complete length including header +} knxnet_header_t; + +/** service types. */ +typedef enum { + SERVICE_TYPE_SEARCH_REQ = 0x0201, + SERVICE_TYPE_SEARCH_RES = 0x0202, + SERVICE_TYPE_DESC_REQ = 0x0203, + SERVICE_TYPE_DESC_RES = 0x0204, +// SERVICE_TYPE_CONN_REQ = 0x0205, +// SERVICE_TYPE_CONN_RES = 0x0206, +// SERVICE_TYPE_CONNSTATE_REQ = 0x0207, +// SERVICE_TYPE_CONNSTATE_RES = 0x0208, +// SERVICE_TYPE_DISCONN_REQ = 0x0209, +// SERVICE_TYPE_DISCONN_RES = 0x020A, +// SERVICE_TYPE_DEVICE_CFG_REQ = 0x0310, +// SERVICE_TYPE_DEVICE_CFG_ACK = 0x0311, +// SERVICE_TYPE_TUNNEL_REQ = 0x0420, +// SERVICE_TYPE_TUNNEL_ACK = 0x0421, + SERVICE_TYPE_ROUTE_IND = 0x0530, + SERVICE_TYPE_ROUTE_LOST = 0x0531, + SERVICE_TYPE_ROUTE_BUSY = 0x0532, +} knxnet_service_type_t; + +// cEMI frame header (external message interface) +typedef struct __attribute__ ((packed)) { + uint8_t messageCode; + uint8_t additionalInfoLength; // optional immediately following additional bytes, usually =0. fixed to 0 in cEMI management messages +} knxnet_cemi_header_t; + +/* cEMI message codes. */ +typedef enum { + // MESSAGE_CODE_BUSMON_IND = 0x2B, + MESSAGE_CODE_DATA_REQ = 0x11, + MESSAGE_CODE_DATA_CON = 0x2E, + MESSAGE_CODE_DATA_IND = 0x29, +// MESSAGE_CODE_RAW_REQ = 0x10, +// MESSAGE_CODE_RAW_CON = 0x2D, +// MESSAGE_CODE_RAW_IND = 0x2F, + // MESSAGE_CODE_POLLDATA_REQ = 0x13, + // MESSAGE_CODE_POLLDATA_CON = 0x25, + // MESSAGE_CODE_DATACONN_REQ = 0x41, + // MESSAGE_CODE_DATACONN_IND = 0x89, + // MESSAGE_CODE_DATAIND_REQ = 0x4A, + // MESSAGE_CODE_DATAIND_IND = 0x94, + // MESSAGE_CODE_PROPREAD_REQ = 0xFC, + // MESSAGE_CODE_PROPREAD_CON = 0xFB, + // MESSAGE_CODE_PROPWRITE_REQ = 0xF6, + // MESSAGE_CODE_PROPWRITE_CON = 0xF5, + // MESSAGE_CODE_PROPINFO_IND = 0xF7, + // MESSAGE_CODE_FUNCPROPCMD_REQ = 0xF8, + // MESSAGE_CODE_FUNCPROPSTATEREAD_REQ = 0xF9, + // MESSAGE_CODE_FUNCPROP_CON = 0xFA, +// MESSAGE_CODE_RESET_IND = 0xF0, +// MESSAGE_CODE_RESET_REQ = 0xF1, +} knxnet_message_code_t; + +// L_Data services header +typedef struct __attribute__ ((packed)) { + union { + uint8_t raw; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool frameType: 1; // 0=extended, 1=standard + bool reserved: 1; + bool repeat: 1; // 0=repeat, 1=do not repeat + bool systemBroadcast: 1; // 0=system broadcast, 1=broadcast + uint8_t priority: 2; // 0=system, 1=normal, 2=urgent, 3=low + bool acknowledgeRequest: 1; // 1=ack requested + bool confirm: 1; // 0=no error, 1=error +#else + bool confirm: 1; // 0=no error, 1=error + bool acknowledgeRequest: 1; // 1=ack requested + uint8_t priority: 2; // 0=system, 1=normal, 2=urgent, 3=low + bool systemBroadcast: 1; // 0=system broadcast, 1=broadcast + bool repeat: 1; // 0=repeat, 1=do not repeat + bool reserved: 1; + bool frameType: 1; // 0=extended, 1=standard +#endif + }; + } controlField1; + union { + uint8_t raw; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool addressType: 1; // 0=individual, 1=group + uint8_t hopCount: 3; + uint8_t extendedFrameFormat: 4; // 0=standard frame, 0xf=escape +#else + uint8_t extendedFrameFormat: 4; // 0=standard frame, 0xf=escape + uint8_t hopCount: 3; + bool addressType: 1; // 0=individual, 1=group +#endif + }; + } controlField2; + uint16be_t sourceAddress; + uint16be_t destinationAddress; + uint8_t informationLength; // number of NPDU octets (not including the TPCI/APCI octet) +} knxnet_l_data_header_t; + + +typedef union __attribute__ ((packed)) { + uint8_t raw; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool controlFlag: 1; // 0=data, 1=control + bool numbered: 1; // 1=has sequence, 0=no sequence + uint8_t sequence: 4; // optional sequence number + uint8_t apci: 2; // highest 2 bits of APCI +#else + uint8_t apci: 2; // highest 2 bits of APCI + uint8_t sequence: 4; // optional sequence number + bool numbered: 1; // 1=has sequence, 0=no sequence + bool controlFlag: 1; // 0=data, 1=control +#endif + }; +} knxnet_tpci_apci_t; + +typedef struct __attribute__ ((packed)) { + uint8_t length; + uint8_t protocolCode; // 0x01=UDP over IPv4 + uint32be_t ipAddressV4; + uint16be_t port; +} knxnet_hpai_t; + +#define PROTOCOL_CODE_IPV4_UDP 0x01 + +typedef struct __attribute__ ((packed)) { + uint8_t length; + uint8_t descriptionCode; // 0x01=device info + uint8_t medium; // 0x20=IP + uint8_t status; // bit 0=programming mode + uint16be_t individualAddress; + uint16be_t projInstId; + uint8_t serial[6]; + in_addr_t multicastAddress; + uint8_t macAddress[6]; + unsigned char name[30]; +} knxnet_dib_devinfo_t; + +typedef struct __attribute__ ((packed)) { + uint8_t length; + uint8_t descriptionCode; // 0x02=services + struct { + uint8_t familyId; + uint8_t familyVersion; + }; // just one for now +} knxnet_dib_services_t; + +// the default system port +#define SYSTEM_MULTICAST_PORT 3671 + +// the default system multicast address 224.0.23.12 +#define SYSTEM_MULTICAST_IP 0xe000170c + +#define LAST_FRAME_TIMEOUT 2 + +class LastFrame { + friend class LastFrames; + + public: + void set(uint8_t* data, size_t len, size_t lOffset, time_t now) { + if (len>=sizeof(m_data)) { + return; + } + memcpy(m_data, data, len); + m_len = len; + m_lOffset = lOffset; + m_time = now; + } + + bool isValid(time_t now) { + return m_len && m_time>=now-LAST_FRAME_TIMEOUT; + } + + bool isSameAs(uint8_t* data, size_t len, size_t lOffset, time_t now, bool isSend = false) { + if (!m_len || len != m_len || lOffset != m_lOffset) { + return false; + } + if (memcmp(data, m_data, len) == 0) { + m_time = now; + return true; + } + int oldHopCount = (m_data[lOffset+1]&0x70)>>4; + int newHopCount = (data[lOffset+1]&0x70)>>4; + if (newHopCount < 6 // top hop count is always tolerated TODO bad idea? + && memcmp(data, m_data, lOffset+1) == 0 // including first byte of l_data header + && (data[lOffset+1]&~0x70)==(m_data[lOffset+1]&~0x70) // ignore hop count + && (isSend ? newHopCount<=oldHopCount : newHopCountcontrolField2.addressType; + PRINTF("%s msgcode=%2.2x, %d.%d.%d > %d%c%d%c%d, repeat=%s, ack=%s, hopcnt=%d, prio=%s, frame=%s, %sbroad, confirm=%s, tpci/apci=%2.2x", + sent ? "send" : "recv", + c->messageCode, + l->sourceAddress.high>>4, + l->sourceAddress.high&0xf, + l->sourceAddress.low, + isGrp ? l->destinationAddress.high>>3 : l->destinationAddress.high>>4, + isGrp ? '/' : '.', + isGrp ? l->destinationAddress.high&0x1f : l->destinationAddress.high&0xf, + isGrp ? '/' : '.', + l->destinationAddress.low, + l->controlField1.repeat ? "yes" : "no", + l->controlField1.acknowledgeRequest ? "yes" : "no", + l->controlField2.hopCount, + l->controlField1.priority==1 ? "normal" : l->controlField1.priority==2 ? "urgent" : l->controlField1.priority==3 ? "low" : "system", + l->controlField1.frameType ? "std" : "ext", + l->controlField1.systemBroadcast ? "" : "sys ", + l->controlField1.confirm ? "error" : "no err", + d[0] + ); + if (d) { + PRINTF(", data="); + for (int i=0; iinformationLength; i++) { + PRINTF("%2.2x ", d[1+i]); + } + } + PRINTF("\n"); +} +#else +#define PRINTF(...) +#define logTelegram(...) +#endif + + +/** + * A KnxConnection based on IP multicast as alternative to using libeibclient. + * This is still an incomplete KNXnet/IP implementation. + */ +class KnxNetConnection : public KnxConnection { + public: + /** + * Construct a new instance. + */ + KnxNetConnection(const char* url) + : KnxConnection(), m_url(url), m_sock(0), m_programmingMode(false), m_addr(0) {} + + /** + * Destructor. + */ + virtual ~KnxNetConnection() { + close(); + } + + // @copydoc + const char* getInfo() const override { + return "KNXnet/IP multicast"; + } + + // @copydoc + const char* open() override { + close(); + int ret; + struct in_addr mcast = {}; + mcast.s_addr = htonl(SYSTEM_MULTICAST_IP); + m_interface.s_addr = INADDR_ANY; + m_port = SYSTEM_MULTICAST_PORT; + if (m_url && m_url[0]) { // non-empty + string urlStr = m_url; // "[mcast][@intf]" for non-default 224.0.23.12:3671) + if (!urlStr.empty()) { + auto pos = urlStr.find('@'); + if (pos != string::npos) { + string intfStr = urlStr.substr(pos+1); + const char* intfCstr = intfStr.c_str(); + ret = inet_aton(intfCstr, &m_interface); + if (ret == 0) { + return "intf addr"; + } + urlStr = urlStr.substr(0, pos); + } + } + if (!urlStr.empty()) { + const char *mcastStr = urlStr.c_str(); + ret = inet_aton(mcastStr, &mcast); + if (ret == 0) { + return "multicast addr"; + } + } + } + + sockaddr_in address = {}; + address.sin_family = AF_INET; + address.sin_port = htons(m_port); + address.sin_addr.s_addr = INADDR_ANY; + + int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (fd < 0) { + return "create socket"; + } + + // set non-blocking + ret = fcntl(fd, F_SETFL, O_NONBLOCK); + if (ret != 0) { + ::close(fd); + return "non-blocking"; + } + + // set reuse address option + int optint = 1; + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optint, sizeof(optint)); + if (ret != 0) { + ::close(fd); + return "reuse"; + } + + // allow multiple processes using the same port for multicast on the same host + unsigned char optchar = 1; + ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &optchar, sizeof(optchar)); + if (ret != 0) { + ::close(fd); + return "mcast loop"; + } + + if (m_interface.s_addr != INADDR_ANY) { + // set outgoing interface to other than default (determined by routing table) + ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &m_interface, sizeof(m_interface)); + if (ret != 0) { + ::close(fd); + return "mcast intf"; + } + } + + // bind for incoming multicast + ret = bind(fd, (struct sockaddr*) &address, sizeof(address)); + if (ret != 0) { + ::close(fd); + return "bind socket"; + } + + // set the target address for later use by sendto() + m_multicast = address; + m_multicast.sin_addr = mcast; + + // join the multicast inbound + ip_mreq req = {}; + req.imr_multiaddr = mcast; + req.imr_interface = m_interface; + if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req)) < 0) { + ::close(fd); + return "join multicast"; + } + + m_sock = fd; + return nullptr; + } + + // @copydoc + bool isConnected() const override { + return m_sock != 0; + } + + // @copydoc + void close() override { + if (m_sock) { + ::close(m_sock); + m_sock = 0; + } + } + + // @copydoc + int getPollFd() const override { + return m_sock; + } + + // @copydoc + knx_transfer_t getPollData(int size, uint8_t* data, int* recvlen, knx_addr_t* src, knx_addr_t* dst) override { + uint8_t buf[128]; + ssize_t len = recv(m_sock, buf, sizeof(buf), 0); + if (len < sizeof(knxnet_header_t)) { + PRINTF("#skip recv short hdr len=%d\n", len); + return KNX_TRANSFER_NONE; + } + auto h = (knxnet_header_t*)buf; + if (h->headerLength != sizeof(knxnet_header_t) || h->protocolVersion != 0x10) { + PRINTF("#skip recv short/proto len=%d\n", len); + return KNX_TRANSFER_NONE; + } + switch (htons(h->serviceTypeIdentifier.raw)) { + case SERVICE_TYPE_ROUTE_IND: + // expected value + break; +// case SERVICE_TYPE_SEARCH_REQ: +// return KNX_TRANSFER_NONE; +// case SERVICE_TYPE_DESC_REQ: +// return KNX_TRANSFER_NONE; + default: + PRINTF("#skip recv service=%4.4x\n", htons(h->serviceTypeIdentifier.raw)); + return KNX_TRANSFER_NONE; + } + // routing indication + size_t totalLen = htons(h->totalLength.raw); + if (len < totalLen || len < sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)) { + PRINTF("#skip recv short cemi len=%d\n", len); + return KNX_TRANSFER_NONE; + } + auto c = (knxnet_cemi_header_t*)(((uint8_t*)h)+sizeof(knxnet_header_t)); + if (c->messageCode != MESSAGE_CODE_DATA_IND) { + PRINTF("#skip recv msgcode=%2.2x\n", c->messageCode); + return KNX_TRANSFER_NONE; + } + auto lOffset = sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)+c->additionalInfoLength; + ssize_t dataLen = totalLen - (lOffset+sizeof(knxnet_l_data_header_t)); + if (dataLen < 0) { + PRINTF("#skip recv short data len=%d\n", len); + return KNX_TRANSFER_NONE; + } + auto l = (knxnet_l_data_header_t*)(((uint8_t*)h)+lOffset); + auto d = ((uint8_t*)l)+sizeof(knxnet_l_data_header_t); + if (!l->controlField1.frameType || !l->controlField1.systemBroadcast) { + // not a regular standard frame broadcast + PRINTF("#skip recv irregular frame len=%d\n", len); + return KNX_TRANSFER_NONE; + } + if (m_addr && (!l->controlField2.addressType && htons(l->destinationAddress.raw) != m_addr)) { + // ignore packets with individual addr destination other than our own + PRINTF("#skip recv not-own dest len=%d\n", len); + return KNX_TRANSFER_NONE; + } + if (m_addr && !l->controlField2.addressType && htons(l->sourceAddress.raw) == m_addr) { + // ignore own source packets + PRINTF("#skip recv own src len=%d\n", len); + return KNX_TRANSFER_NONE; + } + if (dataLen < 0 || dataLen < l->informationLength) { + PRINTF("#skip recv short payload len=%d\n", len); + return KNX_TRANSFER_NONE; + } + // check repeated frames + time_t now; + time(&now); + // PRINTF("getPoll len=%d, last sent len=%d\n", len, m_lastSentLen); + if (m_lastRecvFrames.isRepetition(buf, totalLen, lOffset, now)) { + // last recv packet repeated + PRINTF("#skip recv last recv len=%d\n", totalLen); + return KNX_TRANSFER_NONE; + } + if (m_lastSentFrames.isRepetition(buf, totalLen, lOffset, now, true)) { + // last sent packet re-received + PRINTF("#skip recv last sent len=%d\n", totalLen); + return KNX_TRANSFER_NONE; + } + logTelegram(false, c, l, d); + m_lastRecvFrames.add(buf, totalLen, lOffset, now); + // all fine + int ret = d[0]; + if (l->controlField2.addressType) { + ret |= 0x100; // address type group + } + if (!(ret&0x80)) { + ret &= ~0x03; // remove two apci bits + } + if (ret&0x40) { + ret &= ~0x3c; // remove sequence number + } + *recvlen = size > dataLen ? dataLen : size; + memcpy(data, d, *recvlen); // including the TPCI/APCI octet 6 + if (src) { + *src = htons(l->sourceAddress.raw); + } + if (dst) { + *dst = htons(l->destinationAddress.raw); + } + return (knx_transfer_t)ret; + } + + // @copydoc + const char* sendGroup(knx_addr_t dst, int len, const uint8_t* data) override { + return send(KNX_TRANSFER_GROUP, dst, len, data); + } + + // @copydoc + const char* sendTyp(knx_transfer_t typ, knx_addr_t dst, int len, const uint8_t* data) override { + return send(typ, dst, len, data); + } + + // @copydoc + bool isProgrammable() const override { return true; }; + + private: + /** + * Send a message. + * @param typ the transfer type to send. + * @param dst the destination address. + * @param len the APDU length. + * @param data the APDU data buffer. + * @return nullptr on success, or an error message. + */ + const char* send(knx_transfer_t typ, knx_addr_t dst, int len, const uint8_t* data) { + uint8_t buf[128]; + auto h = (knxnet_header_t*)buf; + h->headerLength = sizeof(knxnet_header_t); + h->protocolVersion = 0x10; + h->serviceTypeIdentifier.raw = htons(SERVICE_TYPE_ROUTE_IND); + // first byte of data is expected to hold the APCI upper byte: + size_t totalLen = sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)+sizeof(knxnet_l_data_header_t)+len; + h->totalLength.raw = htons(totalLen); + auto c = (knxnet_cemi_header_t*)(buf+sizeof(knxnet_header_t)); + c->messageCode = MESSAGE_CODE_DATA_IND; + c->additionalInfoLength = 0; + auto lOffset = sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)+c->additionalInfoLength; + auto l = (knxnet_l_data_header_t*)(((uint8_t*)h)+lOffset); + l->controlField1.raw = 0xbc; // standard frame, no repeat, broadcast, low prio, no ack, no err + l->controlField2.raw = 0xe0; // group address, hop count 6, standard frame + l->controlField2.addressType = (typ&0x100)!=0; + l->sourceAddress.raw = htons(m_addr); + l->destinationAddress.raw = htons(dst); + if (typ&0x100) { + // ensure at least default individual address + if (!m_addr) { + l->sourceAddress.raw = 0xffff; // for "unregistered device" in S-Mode + } + } + l->informationLength = len-1; // subtracting the TPCI/APCI + uint8_t* d = buf+sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)+sizeof(knxnet_l_data_header_t); + // first byte of data is expected to hold the APCI upper byte, copy remainder: + memcpy(d, data, len); + int tpci = typ&0xff; // TPCI/APCI + if ((typ&0x080)==0) { + tpci |= (d[0]&0x03); // highest 2 bits of APCI + } + if (typ&0x040) { + tpci |= d[0]&((0x0f)<<2); // SeqNo + } + d[0] = tpci; + logTelegram(true, c, l, d); + ssize_t sent = sendto(m_sock, buf, totalLen, MSG_NOSIGNAL, (sockaddr*)&m_multicast, sizeof(m_multicast)); + if (sent < 0) { + return "send error"; + } + time_t now; + time(&now); + m_lastSentFrames.add(buf, totalLen, lOffset, now); + return nullptr; + } + + // copydoc + knx_addr_t getAddress() override { + return m_addr; + } + + // copydoc + void setAddress(knx_addr_t address) override { + m_addr = address; + // flush duplication check buffers + m_lastRecvFrames.reset(); + m_lastSentFrames.reset(); + } + + // copydoc + bool isProgrammingMode() override { + return m_programmingMode; + } + + // copydoc + void setProgrammingMode(bool on) override { + m_programmingMode = on; + } + + private: + /** the URL to connect to. */ + const char* m_url; + + /** the multicast address to join. */ + struct sockaddr_in m_multicast; + + /** the port to listen to. */ + in_port_t m_port; + + /** the optional interface address to bind to. */ + struct in_addr m_interface; + + /** the socket if connected, or 0. */ + int m_sock; + + /** true while in programming mode. */ + bool m_programmingMode; + + /** the own address, or 0 if not yet set. */ + knx_addr_t m_addr; + + /** the last received frames. */ + LastFrames m_lastRecvFrames; + + /** the last sent frames. */ + LastFrames m_lastSentFrames; +}; + +} // namespace ebusd + +#endif // LIB_KNX_KNXNET_H_ diff --git a/src/lib/utils/clock.cpp b/src/lib/utils/clock.cpp index 60bb27f0..1befe810 100755 --- a/src/lib/utils/clock.cpp +++ b/src/lib/utils/clock.cpp @@ -44,4 +44,10 @@ void clockGettime(struct timespec* t) { #endif } +long long clockGetMillis() { + struct timespec t; + clockGettime(&t); + return t.tv_sec*1000LL + t.tv_nsec / 1000000; +} + } // namespace ebusd diff --git a/src/lib/utils/clock.h b/src/lib/utils/clock.h index 6d2f75ed..7f046433 100755 --- a/src/lib/utils/clock.h +++ b/src/lib/utils/clock.h @@ -31,6 +31,11 @@ namespace ebusd { */ void clockGettime(struct timespec* t); +/** + * Get the current system time in milliseconds since the Epoch. + */ +long long clockGetMillis(); + } // namespace ebusd #endif // LIB_UTILS_CLOCK_H_ diff --git a/src/lib/utils/thread.cpp b/src/lib/utils/thread.cpp index ee1f0d56..d34d40d6 100755 --- a/src/lib/utils/thread.cpp +++ b/src/lib/utils/thread.cpp @@ -93,11 +93,18 @@ bool WaitThread::join() { return Thread::join(); } -bool WaitThread::Wait(int seconds) { +bool WaitThread::Wait(int seconds, int millis) { pthread_mutex_lock(&m_mutex); struct timespec t; clockGettime(&t); t.tv_sec += seconds; + long newMillis = t.tv_nsec/1000000 + millis; + if (newMillis >= 1000) { + t.tv_sec += newMillis / 1000; + t.tv_nsec = (newMillis%1000) * 1000000; // rounds down to whole millis + } else { + t.tv_nsec += millis * 1000000; + } pthread_cond_timedwait(&m_cond, &m_mutex, &t); pthread_mutex_unlock(&m_mutex); return isRunning(); @@ -120,11 +127,11 @@ bool NotifiableThread::waitNotified(int millis) { if (!m_notified) { struct timespec t; clockGettime(&t); - t.tv_sec += millis / 1000000000; - t.tv_nsec += (millis % 1000000000) * 1000000; - if (t.tv_nsec > 1000000000) { - t.tv_sec++; - t.tv_nsec -= 1000000000; + t.tv_sec += millis / 1000; + t.tv_nsec += (millis % 1000) * 1000000; + if (t.tv_nsec >= 1000000000) { + t.tv_sec += t.tv_nsec / 1000000000; + t.tv_nsec %= 1000000000; } pthread_cond_timedwait(&m_cond, &m_mutex, &t); } diff --git a/src/lib/utils/thread.h b/src/lib/utils/thread.h index 6cd9f2a7..4690929e 100755 --- a/src/lib/utils/thread.h +++ b/src/lib/utils/thread.h @@ -129,9 +129,10 @@ class WaitThread : public Thread { /** * Wait for the specified amount of time. * @param seconds the number of seconds to wait. + * @param millis the optional number of milliseconds to wait. * @return true if this @a WaitThread is still running and not yet stopped. */ - bool Wait(int seconds); + bool Wait(int seconds, int millis = 0); protected: