Merge remote-tracking branch 'origin/feature/knx'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+14
-3
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,981 @@
|
||||
/*
|
||||
* ebusd - daemon for communication with eBUS heating systems.
|
||||
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include "ebusd/knxhandler.h"
|
||||
#ifdef HAVE_PPOLL
|
||||
# include <poll.h>
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <csignal>
|
||||
#include <deque>
|
||||
#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<string>* 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<string>();
|
||||
}
|
||||
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<DataHandler*>* 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<string> 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<const NumberDataType*>(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<uint8_t>(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<uint32_t>(val * exp2(-shift));
|
||||
uint32_t value = static_cast<uint32_t>(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<float>(0xffffffff); // NaN
|
||||
}
|
||||
bool negative = val&0x8000;
|
||||
int exp = (val>>11)&0xf;
|
||||
int sig = val&0x7ff;
|
||||
return static_cast<float>(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<uint8_t>(apci>>8);
|
||||
data[1] = static_cast<uint8_t>(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<const NumberDataType*>(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<uint8_t>(value&0x3f);
|
||||
break;
|
||||
case 1: // 1 octet
|
||||
data[2] = static_cast<uint8_t>(value&0xff);
|
||||
break;
|
||||
case 2: // 2 octets
|
||||
data[2] = static_cast<uint8_t>(value>>8);
|
||||
data[3] = static_cast<uint8_t>(value&0xff);
|
||||
break;
|
||||
case 4: // 4 octets
|
||||
data[2] = static_cast<uint8_t>(value>>24);
|
||||
data[3] = static_cast<uint8_t>(value>>16);
|
||||
data[4] = static_cast<uint8_t>(value>>8);
|
||||
data[5] = static_cast<uint8_t>(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<knx_addr_t>(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<now-3) { // timeout 3 seconds
|
||||
uint8_t buf[] = {APCI_INDIVIDUALADDRESS_RESPONSE>>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<uint32_t>(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<unsigned>(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<Message*>* 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<uint16_t>(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<float>(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<int>(value);
|
||||
} else {
|
||||
str << static_cast<uint32_t>(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<unsigned int>(now - m_start));
|
||||
}
|
||||
}
|
||||
if (m_con->isConnected() && definitionsSince == 0) {
|
||||
definitionsSince = 1;
|
||||
}
|
||||
if (m_con->isConnected()) {
|
||||
deque<Message*> 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<signed>(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<uint32_t>(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<uint8_t>(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<uint32_t>(dest | FLAG_READ);
|
||||
sit = m_subscribedGroups.find(subKey);
|
||||
if (sit == m_subscribedGroups.cend()) {
|
||||
m_subscribedGroups[subKey] = {
|
||||
.messageKey = message->getKey(),
|
||||
.fieldIndex = static_cast<uint8_t>(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<Message*>* 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
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* ebusd - daemon for communication with eBUS heating systems.
|
||||
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef EBUSD_KNXHANDLER_H_
|
||||
#define EBUSD_KNXHANDLER_H_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#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<DataHandler*>* 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<string, knx_addr_t> 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.
|
||||
*/
|
||||
map<uint32_t, groupInfo_t>m_subscribedGroups;
|
||||
|
||||
/** the group address and flags (key of m_subscribedGroups) by subscribed message key. */
|
||||
map<uint64_t, list<uint32_t>>m_subscribedMessages;
|
||||
|
||||
/** the group address and flags (key of m_subscribedGroups) by subscribed global values. */
|
||||
map<global_t, uint32_t>m_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_
|
||||
@@ -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",
|
||||
|
||||
+133
-1
@@ -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<float>(-m_divisor);
|
||||
} else if (m_divisor > 1) {
|
||||
val /= static_cast<float>(m_divisor);
|
||||
}
|
||||
}
|
||||
*output = static_cast<float>(val);
|
||||
return RESULT_OK;
|
||||
}
|
||||
if (!negative) {
|
||||
if (m_divisor < 0) {
|
||||
*output = static_cast<float>(value) * static_cast<float>(-m_divisor);
|
||||
} else if (m_divisor <= 1) {
|
||||
*output = static_cast<float>(value);
|
||||
} else {
|
||||
*output = static_cast<float>(value) / static_cast<float>(m_divisor);
|
||||
}
|
||||
return RESULT_OK;
|
||||
}
|
||||
signedValue = static_cast<int>(value); // negative signed value
|
||||
} else if (negative) { // negative signed value
|
||||
signedValue = static_cast<int>(value) - (1 << m_bitCount);
|
||||
} else {
|
||||
signedValue = static_cast<int>(value);
|
||||
}
|
||||
if (m_divisor < 0) {
|
||||
*output = static_cast<float>(signedValue) * static_cast<float>(-m_divisor);
|
||||
} else if (m_divisor <= 1) {
|
||||
*output = static_cast<float>(signedValue);
|
||||
} else {
|
||||
*output = static_cast<float>(signedValue) / static_cast<float>(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<float>(dvalue));
|
||||
if (value == 0xffffffff) {
|
||||
return RESULT_ERR_INVALID_NUM;
|
||||
}
|
||||
} else {
|
||||
if (m_divisor == 1) {
|
||||
if (hasFlag(SIG)) {
|
||||
long signedValue = static_cast<long>(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<unsigned int>(val);
|
||||
}
|
||||
} else {
|
||||
double dvalue = val;
|
||||
if (m_divisor < 0) {
|
||||
dvalue = round(dvalue / -m_divisor);
|
||||
} else {
|
||||
dvalue = round(dvalue * m_divisor);
|
||||
}
|
||||
int length = static_cast<int>(m_bitCount/8);
|
||||
if (hasFlag(SIG)) {
|
||||
if (dvalue < -exp2((8 * static_cast<double>(length)) - 1)
|
||||
|| dvalue >= exp2((8 * static_cast<double>(length)) - 1)) {
|
||||
return RESULT_ERR_OUT_OF_RANGE; // value out of range
|
||||
}
|
||||
if (dvalue < 0 && m_bitCount != 32) {
|
||||
value = static_cast<unsigned int>(dvalue + (1 << m_bitCount));
|
||||
} else {
|
||||
value = static_cast<unsigned int>(dvalue);
|
||||
}
|
||||
} else {
|
||||
if (dvalue < 0.0 || dvalue >= exp2(8 * static_cast<double>(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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* ebusd - daemon for communication with eBUS heating systems.
|
||||
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include "lib/knx/knx.h"
|
||||
|
||||
#ifdef HAVE_KNXD
|
||||
#include "lib/knx/knxd.h"
|
||||
#endif
|
||||
#include "lib/knx/knxnet.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
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<knx_addr_t>(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<knx_addr_t>(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<knx_addr_t>(v << 8);
|
||||
v = parseInt(str.substr(pos2+1).c_str(), 10, 0, 0xff, &err);
|
||||
if (!err) {
|
||||
dest |= static_cast<knx_addr_t>(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
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* ebusd - daemon for communication with eBUS heating systems.
|
||||
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef LIB_KNX_KNX_H_
|
||||
#define LIB_KNX_KNX_H_
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
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_
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* ebusd - daemon for communication with eBUS heating systems.
|
||||
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef LIB_KNX_KNXD_H_
|
||||
#define LIB_KNX_KNXD_H_
|
||||
|
||||
#include <eibclient.h>
|
||||
#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_
|
||||
@@ -0,0 +1,729 @@
|
||||
/*
|
||||
* ebusd - daemon for communication with eBUS heating systems.
|
||||
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef LIB_KNX_KNXNET_H_
|
||||
#define LIB_KNX_KNXNET_H_
|
||||
|
||||
#include <string>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <net/if.h>
|
||||
#ifndef __CYGWIN__
|
||||
#include <net/if_arp.h>
|
||||
#endif
|
||||
#include <netdb.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <endian.h>
|
||||
#include <cstdio>
|
||||
#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 : newHopCount<oldHopCount) // decremented hop count?
|
||||
&& memcmp(data+lOffset+2, m_data+lOffset+2, len-(lOffset+2)) == 0
|
||||
) {
|
||||
m_time = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
m_time = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
/** the last data. */
|
||||
uint8_t m_data[256];
|
||||
|
||||
/** the length of the last data, or 0 for none. */
|
||||
size_t m_len;
|
||||
|
||||
/** the offset to the L_Data. */
|
||||
size_t m_lOffset;
|
||||
|
||||
/** the time of the last data, or 0 for none. */
|
||||
time_t m_time;
|
||||
};
|
||||
|
||||
|
||||
#define CHECK_REPETITION_COUNT 4
|
||||
|
||||
class LastFrames {
|
||||
public:
|
||||
bool isRepetition(uint8_t* data, size_t len, size_t lOffset, time_t now, bool isSend = false) {
|
||||
for (int i=0; i<CHECK_REPETITION_COUNT; i++) {
|
||||
if (m_lastFrames[i].isValid(now)
|
||||
&& m_lastFrames[i].isSameAs(data, len, lOffset, now, isSend)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void add(uint8_t* data, size_t len, size_t lOffset, time_t now) {
|
||||
int oldestPos = -1;
|
||||
time_t oldestAge = 0;
|
||||
for (int i=0; i<CHECK_REPETITION_COUNT; i++) {
|
||||
if (!m_lastFrames[i].isValid(now)) {
|
||||
m_lastFrames[i].set(data, len, lOffset, now);
|
||||
return;
|
||||
}
|
||||
if (oldestPos<0 || m_lastFrames[i].m_time < oldestAge) {
|
||||
oldestPos = i;
|
||||
oldestAge = m_lastFrames[i].m_time;
|
||||
}
|
||||
}
|
||||
m_lastFrames[oldestPos].set(data, len, lOffset, now);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
for (int i=0; i<CHECK_REPETITION_COUNT; i++) {
|
||||
m_lastFrames[i].reset();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
/** the list of the last telegrams. */
|
||||
LastFrame m_lastFrames[CHECK_REPETITION_COUNT];
|
||||
};
|
||||
|
||||
#ifdef DEBUG
|
||||
#define PRINTF printf
|
||||
|
||||
// helper method to log received/sent telegrams
|
||||
void logTelegram(bool sent, knxnet_cemi_header_t* c, knxnet_l_data_header_t* l, uint8_t* d) {
|
||||
bool isGrp = l->controlField2.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; i<l->informationLength; 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_
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user