diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5df83a3bf..01cea8776 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,7 @@ body: id: description attributes: label: Description - description: A clear and concise description of what the bug is. Anything related to actual message definitions in [ebusd-configuration](https://github.com/john30/ebusd-configuration), serial bridge in [ebusd-esp](https://github.com/john30/ebusd-esp), or [eBUS adapter v3/v2](https://github.com/eBUS/adapter) do not belong here! Use the appropriate repository for those. + description: A clear and concise description of what the bug is. Anything related to actual message definitions in [ebusd-configuration](https://github.com/john30/ebusd-configuration), [eBUS Adapter Shield v5](https://github.com/john30/ebusd-esp32), serial bridge in [ebusd-esp](https://github.com/john30/ebusd-esp), or [eBUS adapter v3/v2](https://github.com/eBUS/adapter) do not belong here! Use the appropriate repository for those. placeholder: e.g. during startup, an error message as described below is reported instead of... validations: required: true @@ -35,6 +35,11 @@ body: description: the ebusd version in use options: - current source from git + - '25.1' + - '24.1' + - '23.3' + - '23.2' + - '23.1' - '22.4' - '22.3' - '22.2' @@ -58,6 +63,7 @@ body: label: Operating system description: the operating system in use options: + - Debian 12 (Bookworm) / Ubuntu 22-23 / Raspberry Pi OS 12 (including lite) - Debian 11 (Bullseye) / Ubuntu 20-21 / Raspbian 11 / Raspberry Pi OS 11 (including lite) - Debian 10 (Buster) / Ubuntu 18-19 / Raspbian 10 / Raspberry Pi OS 10 (including lite) - Debian 9 (Stretch) / Ubuntu 16-17 / Raspbian 9 / Raspberry Pi OS 9 (including lite) @@ -93,17 +99,15 @@ body: label: Hardware interface description: the eBUS hardware interface in use options: - - adapter 3.1 USB - - adapter 3.1 WiFi - - adapter 3.1 Ethernet - - adapter 3.1 RPi - - adapter 3.0 USB - - adapter 3.0 WiFi - - adapter 3.0 Ethernet - - adapter 3.0 RPi - - adapter 2 - - Esera USB - - Esera Ethernet + - Adapter Shield v5/C6/Stick via USB + - Adapter Shield v5/C6/Stick via WiFi + - Adapter Shield v5/C6/Stick via Ethernet + - Adapter Shield v5/C6 via Raspberry GPIO + - Adapter v3 USB + - Adapter v3 WiFi + - Adapter v3 Ethernet + - Adapter v3 RPi + - Adapter v2 - other validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index bc0428a23..f1b1b6681 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -8,7 +8,7 @@ body: id: description attributes: label: Description - description: A clear and concise description of the requested feature, additional behaviour, different functionality. Anything related to actual message definitions in ebusd-configuration, serial bridge in ebusd-esp, or adapter v3/v2 do not belong here! Use the appropriate repository for those. - placeholder: e.g. during startup, an error message as described below is reported instead of... + description: A clear and concise description of the requested feature, additional behaviour, different functionality. Anything related to actual message definitions in ebusd-configuration, serial bridge in ebusd-esp, or adapter v5/v3/v2 do not belong here! Use the appropriate repository for those. + placeholder: e.g. add direct integration with xxx... validations: required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..120c6893b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bdf57e26..f14379a7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,23 +29,23 @@ jobs: steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: gh-describe id: gittag - uses: proudust/gh-describe@v1.4.6 + uses: proudust/gh-describe@v2.1.0 - name: set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: set up buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 with: buildkitd-flags: --debug - name: login to docker hub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 50ab4efb2..fef11f918 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -15,10 +15,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: packages - run: sudo apt-get update && sudo apt-get install -y libmosquitto1 libmosquitto-dev libssl1.1 libssl-dev + run: sudo apt-get update && sudo apt-get install -y libmosquitto1 libmosquitto-dev libssl3 libssl-dev - name: build run: cmake -Dcoverage=1 -DBUILD_TESTING=1 . && make @@ -40,9 +40,10 @@ jobs: run: ./test_coverage.sh - name: push result - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: name: codecov-umbrella + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true gcov: true diff --git a/.github/workflows/preparerelease.yml b/.github/workflows/preparerelease.yml index b14f2354f..847dc54bd 100644 --- a/.github/workflows/preparerelease.yml +++ b/.github/workflows/preparerelease.yml @@ -26,9 +26,9 @@ on: type: choice options: - '' + - bookworm - bullseye - buster - - stretch jobs: prepare-release: @@ -36,18 +36,18 @@ jobs: steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: gh-describe id: gittag - uses: proudust/gh-describe@v1.4.6 + uses: proudust/gh-describe@v2.1.0 - name: set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: set up buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 with: buildkitd-flags: --debug - @@ -61,7 +61,7 @@ jobs: LIMITIMG: ${{ github.event.inputs.limitimg }} - name: archive - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: debian packages path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9fce6c21..86d1ddeab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,19 +15,19 @@ jobs: steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: set up buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 with: buildkitd-flags: --debug - name: login to docker hub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index d027f22ed..f9ec2d1e4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ Testing cmake_install.cmake Makefile Makefile.in -/build/* +/build/ +/cmake-build-*/ +build-/ /aclocal.m4 /autom4te.cache/ /config.* @@ -21,23 +23,20 @@ Makefile.in /stamp-h? .deps/ *.o +*.a *.dirstamp *.gc* app.info /src/ebusd/ebusd /src/tools/ebusctl /src/tools/ebusfeed -/src/lib/utils/libutils.a -/src/lib/ebus/libebus.a /src/lib/ebus/contrib/test/test_tem /src/lib/ebus/test/test_symbol /src/lib/ebus/test/test_data /src/lib/ebus/test/test_message /src/lib/ebus/test/test_filereader -/src/lib/ebus/contrib/libebuscontrib.a /src/lib/ebus/contrib/test/test_contrib /docs/Doxyfile /docs/doxyfile.stamp /docs/html -/cmake-build-debug*/ -/.idea/ \ No newline at end of file +/.idea/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 2798e3766..cb532e877 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,11 @@ file(STRINGS "VERSION" VERSION) project(ebusd) +include(GNUInstallDirs) +include(CheckFunctionExists) +include(CheckCXXSourceRuns) +include(CheckIncludeFile) + set(PACKAGE ${CMAKE_PROJECT_NAME}) set(PACKAGE_NAME ${CMAKE_PROJECT_NAME}) set(PACKAGE_TARNAME ${CMAKE_PROJECT_NAME}) @@ -45,13 +50,11 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "" CACHE PATH "..." FORCE) endif() -include(GNUInstallDirs) -include(CheckFunctionExists) -include(CheckCXXSourceRuns) -include(CheckIncludeFile) add_definitions(-fpic -Wall -Wno-unused-function -Wextra) +set(BUILD_SHARED_LIBS OFF) + check_include_file(arpa/inet.h HAVE_ARPA_INET_H) check_include_file(dirent.h HAVE_DIRENT_H) check_include_file(fcntl.h HAVE_FCNTL_H) @@ -61,25 +64,19 @@ check_include_file(pthread.h HAVE_PTHREAD_H) check_include_file(sys/ioctl.h HAVE_SYS_IOCTL_H) check_include_file(sys/select.h HAVE_SYS_SELECT_H) check_include_file(sys/time.h HAVE_SYS_TIME_H) +check_include_file(syslog.h HAVE_SYSLOG_H) check_include_file(time.h HAVE_TIME_H) check_include_file(termios.h HAVE_TERMIOS_H) +check_function_exists(cfsetspeed HAVE_CFSETSPEED) set(CMAKE_REQUIRED_LIBRARIES pthread rt) check_function_exists(pthread_setname_np HAVE_PTHREAD_SETNAME_NP) check_function_exists(pselect HAVE_PSELECT) check_function_exists(ppoll HAVE_PPOLL) +check_function_exists(timegm HAVE_TIMEGM) check_include_file(linux/serial.h HAVE_LINUX_SERIAL -DHAVE_LINUX_SERIAL=1) check_include_file(dev/usb/uftdiio.h HAVE_FREEBSD_UFTDI -DHAVE_FREEBSD_UFTDI=1) -check_function_exists(argp_parse HAVE_ARGP) -if(NOT HAVE_ARGP) - find_library(LIB_ARGP argp) - if (NOT LIB_ARGP) - message(FATAL_ERROR "argp library not available") - endif(NOT LIB_ARGP) - set(CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES} ${LIB_ARGP}") -endif(NOT HAVE_ARGP) - option(coverage "enable code coverage tracking." OFF) if(NOT coverage STREQUAL OFF) add_definitions(-g -O0 --coverage -Wall) @@ -167,7 +164,7 @@ int main() { endif(HAVE_DIRECT_FLOAT_FORMAT_REV) endif(NOT HAVE_DIRECT_FLOAT_FORMAT) -add_definitions(-D_GNU_SOURCE -DHAVE_CONFIG_H -DSYSCONFDIR="${CMAKE_INSTALL_FULL_SYSCONFDIR}" -DLOCALSTATEDIR="${CMAKE_INSTALL_FULL_LOCALSTATEDIR}") +add_definitions(-D_GNU_SOURCE -DHAVE_CONFIG_H) configure_file(config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(${ebusd_SOURCE_DIR}/src) diff --git a/ChangeLog.md b/ChangeLog.md index 00ad538e6..591edd563 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,11 +1,117 @@ -# next (tbd) -## Buf Fixes +# 25.1 (2025-04-13) +## Bug Fixes +* fix for device string symlink with colon +* fix "read" and "write" command response +* fix dump of divisor +* fix max value for S3N, S3N, SLG, and SLR types +* fix socket options for KNXnet/IP integration +* fix constant encoding in json +* fix parsing unexpected mDNS response +* fix longer message key and check +* fix unnecessary poll on scan messages + +## Features +* add "-m" option to "encode" and "decode" commands +* add output for commands executed with "--inject=stop" +* add secondary replacement value for date types +* add value range and step support for numeric types +* add numeric base types without replacement value (e.g. "U1L", "S1L", "U2L", "S2L", "U2B", "S2B", etc.) +* add also include write messages as read-only ones in MQTT definition topic if writes are excluded + + +# 24.1 (2024-10-27) +## Bug Fixes +* fix conditional messages not being sent to message definition in MQTT integration and not being used in KNX group association +* fix CSV dump of config files on command line +* fix DTM type with recent dates +* fix for some updated messages not appearing on KNX or MQTT +* fix for parsing certain condition ranges +* fix for "reload" command not starting the scan again +* fix datetime type mapping in MQTT + +## Features +* add "inject" command +* add config path to verbose "info" command +* add "answer" command +* add option to inject start-up commands +* add verbose raw data option to "read" and "write" commands +* add option to allow write direction in "read" command when inline defining a new message +* add option to discover device via mDNS +* add dedicated log level for device messages +* add option to extend MQTT variables from env/cmdline +* add date+datetime mapping, better device update check, and remove single-field-message field names in Home Assistant MQTT discovery integration + +## Breaking Changes +* change default config path to https://ebus.github.io/ serving files generated from new TypeSpec message definition sources +* change validation of identifiers to no longer accept unusual characters +* change default device connection to be resolved automatically via mDNS + + +# 23.3 (2023-12-26) +## Bug Fixes +* fix MQTT topic string validation +* fix lost scanconfig default behaviour +* fix send empty message instead of logging an error for MQTT on messages without any field +* fix MacOS build +* fix name prefix warnings in Home Assistant MQTT discovery integration +* fix impossible usage of multi-field writes in MQTT integration +* fix initial broadcast scan +* fix potentially unusable SSL context +* fix SYN generator timing +* fix missing check for PB/SB validity +* fix non-SSL build +* fix scan when no signal ++ fix negative float values in KNX integration + +## Features +* add temperatures in Kelvin and ... to Home Assistant MQTT discovery integration +* add options to turn off scanconfig and limit number of retries +* remove dependency on argp +* add time fields to Home Assistant MQTT discovery integration +* add templates endpoint to HTTP JSON +* add reworked eBUS protocol engine that is especially useful for slow network issues +* add knxd support to devel docker image + + +# 23.2 (2023-07-08) +## Bug Fixes +* fix bounds check for variable length datatypes +* add timeout for enhanced protocol info exchange and allow more of them being optional +* fix some warnings in Home Assistant MQTT discovery integration +* fix for high traffic on KNX integration +* add workaround for libssl issues, add debug logging and force it using IPv4 +* fix log level potentially set too late during startup +* fix too low timeout for config web service +* fix MQTT topic construction with prefix containing a separator + +## Features +* add log entry for unstartable TCP ports +* add support for injecting scan messages being used for scan procedure +* add value lists support to MQTT integration and use it for Home Assistant MQTT discovery integration +* add update check for v5 device +* add "scan status" command and add the status to "info" command + + +# 23.1 (2023-01-06) +## Bug Fixes * fix potentially invalid settings picked up from environment variables * fix potentially unnecessary arbitration start for non-enhanced proto * fix smaller issues in KNX integration +* fix numeric replacement+infinite and float min/max values in MQTT JSON payload format +* fix duplicate definition sent for same message when writable messages are included in MQTT integration +* fix UDP based devices no longer working since 22.4 +* fix for older SSL libraries not automatically retrying if necessary +* fix enhanced side data transfer from device to host +* fix for fast participants starting immediately after own SYN at the end of a sent command +* add a single retry when initial config location check fails ## Features -* add support for setting visual ping and IP gateway to ebuspicloader +* add support for setting visual ping, IP gateway, MAC from ID, and variant to ebuspicloader +* add step variable for numeric values to message definition in MQTT integration +* add yes/no values and writable heating curve to Home Assistant MQTT discovery integration +* add device version to update check and switch to Home Assistant update integration for current update check and additionally for device +* add preferred language support to web services and use it instead of default LANG environment with fallback to German +* add option to exit non-zero on non-success response from ebusd to ebusctl # 22.4 (2022-09-18) diff --git a/README.md b/README.md index 79d75fb3f..1a1ea09b4 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ebusd is a daemon for handling communication with eBUS devices connected to a 2-wire bus system ("energy bus" used by numerous heating systems). [![Build](https://github.com/john30/ebusd/actions/workflows/build.yml/badge.svg)](https://github.com/john30/ebusd/actions/workflows/build.yml) +![CodeQL](https://github.com/john30/ebusd/workflows/CodeQL/badge.svg) [![codecov](https://codecov.io/gh/john30/ebusd/branch/master/graph/badge.svg)](https://codecov.io/gh/john30/ebusd) [![Release Downloads](https://img.shields.io/github/downloads/john30/ebusd/total)](https://github.com/john30/ebusd/releases/latest) [![Docker Downloads](https://img.shields.io/docker/pulls/john30/ebusd)](https://hub.docker.com/repository/docker/john30/ebusd) @@ -23,25 +24,29 @@ The main features of the daemon are: * TCP * UDP * enhanced ebusd protocol allowing arbitration to be done directly by the hardware, e.g. for recent - * [ebus adapter 3](https://adapter.ebusd.eu/), or + * [eBUS Adapter Shields C6](https://adapter.ebusd.eu/v5-c6/), [Stick](https://adapter.ebusd.eu/v5-c6/stick.en.html), and [v5](https://adapter.ebusd.eu/v5/), + * [adapter v3.1](https://adapter.ebusd.eu/v31)/[v3.0](https://adapter.ebusd.eu/v3), or * [ebusd-esp firmware](https://github.com/john30/ebusd-esp/) + * auto-discover device connection via mDNS * actively send messages to and receive answers from the eBUS * passively listen to messages sent on the eBUS + * answer to messages received from the eBUS * regularly poll for messages * cache all messages - * scan for bus participants and automatically pick matching message configuration files from config web service at ebusd.eu (or alternatively local files) - * parse messages to human readable values and vice versa via message configuration files - * automatically check for updates of daemon and configuration files - * pick preferred language for translatable message configuration parts + * scan for bus participants and automatically pick matching message definition files from config CDN at [ebus.github.io](https://ebus.github.io/) or from local files + * parse messages to human readable values and vice versa via message definition files + * automatically check for updates of daemon and message definition files + * pick preferred language for translatable message definition parts * grab all messages on the eBUS and provide decoding hints + * send arbitrary messages from hex input or inject those * log messages and problems to a log file * capture messages or sent/received bytes to a log file as text * dump received bytes to binary files for later playback/analysis - * listen for [command line client](3.1.-TCP-client-commands) connections on a dedicated TCP port + * listen for [command line client](https://github.com/john30/ebusd/wiki/3.1.-TCP-client-commands) connections on a dedicated TCP port * provide a rudimentary HTML interface * format messages and data in [JSON on dedicated HTTP port](https://github.com/john30/ebusd/wiki/3.2.-HTTP-client) * publish received data to [MQTT topics](https://github.com/john30/ebusd/wiki/3.3.-MQTT-client) and vice versa (if authorized) - * announce [message definitions and status by MQTT](https://github.com/john30/ebusd/wiki/MQTT-integration) to e.g. integrate with [Home Assistant](https://www.home-assistant.io/) using [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/) + * announce [message definitions and status by MQTT](https://github.com/john30/ebusd/wiki/MQTT-integration) to e.g. integrate with [Home Assistant](https://www.home-assistant.io/) using [MQTT Discovery](https://www.home-assistant.io/integrations/mqtt#mqtt-discovery) * support MQTT publish to [Azure IoT hub](https://docs.microsoft.com/en-us/azure/iot-hub/) (see [MQTT integration](https://github.com/john30/ebusd/wiki/MQTT-integration)) * act as a [KNX device](https://github.com/john30/ebusd/wiki/3.4.-KNX-device) by publishing received data to KNX groups and answer to read/write requests from KNX, i.e. build an eBUS-KNX bridge * [user authentication](https://github.com/john30/ebusd/wiki/3.1.-TCP-client-commands#auth) via [ACL file](https://github.com/john30/ebusd/wiki/2.-Run#daemon-options) for access control to certain messages @@ -61,7 +66,8 @@ Building ebusd from the source requires the following packages and/or features: * g++ with C++11 support (>=4.8.1) * make * kernel with pselect or ppoll support - * glibc with argp support or argp-standalone + * glibc with getopt_long support + * optional: knxd-dev for knxd support (KNXnet/IP support is always included) * libmosquitto-dev for MQTT support * libssl-dev for SSL support @@ -84,18 +90,18 @@ Configuration ------------- The most important part of each ebusd installation is the message configuration. -Starting with version 3.2, **ebusd by default uses the config web service at ebusd.eu to retrieve -the latest configuration files** that are reflected by the configuration repository (follow the "latest" symlink there): +Starting with version 3.2, **ebusd by default uses the config web service to retrieve +the latest configuration files** that are reflected by the configuration repository: > https://github.com/john30/ebusd-configuration Docker image ------------ -A multi-architecture Docker image using the config web service for retrieving the latest message configuration files is available on the hub. +A multi-architecture Docker image using the config web service for retrieving the latest message configuration files is available on the hub. You can use it like this: > docker pull john30/ebusd -> docker run -it --rm --device=/dev/ttyUSB0 -p 8888 john30/ebusd +> docker run -it --rm --device=/dev/ttyUSB0 -p 8888 john30/ebusd -d ens:/dev/ttyUSB0 For more details, see [Docker Readme](https://github.com/john30/ebusd/blob/master/contrib/docker/README.md). diff --git a/VERSION b/VERSION index 1da8ccd28..d41368806 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -22.4 \ No newline at end of file +25.1 \ No newline at end of file diff --git a/config.h.cmake b/config.h.cmake index 5bfd4250c..83e80bd26 100755 --- a/config.h.cmake +++ b/config.h.cmake @@ -31,6 +31,18 @@ /* Defined if pthread_setname_np is available. */ #cmakedefine HAVE_PTHREAD_SETNAME_NP +/* Defined if cfsetspeed() is available. */ +#cmakedefine HAVE_CFSETSPEED + +/* Defined if time.h is available. */ +#cmakedefine HAVE_TIME_H + +/* Defined if timegm() is available. */ +#cmakedefine HAVE_TIMEGM + +/* Defined if syslog.h is available. */ +#cmakedefine HAVE_SYSLOG_H + /* The name of package. */ #cmakedefine PACKAGE "${PACKAGE_NAME}" @@ -38,13 +50,13 @@ #cmakedefine PACKAGE_BUGREPORT "${PACKAGE_BUGREPORT}" /* The path and name of the log file. */ -#define PACKAGE_LOGFILE LOCALSTATEDIR "/log/" PACKAGE ".log" +#cmakedefine PACKAGE_LOGFILE "${PACKAGE_LOGFILE}" /* The full name of this package. */ #cmakedefine PACKAGE_NAME "${PACKAGE_NAME}" /* The path and name of the PID file. */ -#define PACKAGE_PIDFILE LOCALSTATEDIR "/run/" PACKAGE ".pid" +#cmakedefine PACKAGE_PIDFILE "${PACKAGE_PIDFILE}" /* The full name and version of this package. */ #cmakedefine PACKAGE_STRING "${PACKAGE_STRING}" diff --git a/configure.ac b/configure.ac index 4775300f8..54cbb18a8 100755 --- a/configure.ac +++ b/configure.ac @@ -8,7 +8,7 @@ AC_CONFIG_AUX_DIR([build]) AC_CONFIG_MACRO_DIR([m4]) AC_GNU_SOURCE -if test -z $CXXFLAGS; then +if test -z "${CXXFLAGS}"; then CXXFLAGS="-fpic -Wall -Wno-unused-function -Wextra -g -O2" fi AC_PROG_CXX([g++-6 g++-5 g++-4.9 g++-4.8 g++]) @@ -23,9 +23,13 @@ AC_CHECK_HEADERS([arpa/inet.h \ sys/ioctl.h \ sys/select.h \ sys/time.h \ + syslog.h \ time.h \ termios.h]) +AC_CHECK_FUNC([timegm], [AC_DEFINE(HAVE_TIMEGM, [1], [Defined if timegm() is available.])]) +AC_CHECK_FUNC([cfsetspeed], [AC_DEFINE(HAVE_CFSETSPEED, [1], [Defined if cfsetspeed() is available.])]) + AC_CHECK_LIB([pthread], [pthread_setname_np], AC_DEFINE([HAVE_PTHREAD_SETNAME_NP], [1], [Defined if pthread_setname_np is available.]), AC_MSG_RESULT([Could not find pthread_setname_np in pthread.])) @@ -43,21 +47,17 @@ AC_ARG_WITH(contrib, AS_HELP_STRING([--without-contrib], [disable inclusion of c if test "x$with_contrib" != "xno"; then AC_DEFINE_UNQUOTED(HAVE_CONTRIB, [1], [Defined if contributed sources are enabled.]) fi -AC_ARG_WITH(ebusfeed, AS_HELP_STRING([--with-ebusfeed], [enable inclusion of ebusfeed tool]), [with_ebusfeed=yes], []) -AM_CONDITIONAL([WITH_EBUSFEED], [test "x$with_ebusfeed" == "xyes"]) -AC_ARG_WITH(argp-lib, AS_HELP_STRING([--with-argp-lib=PATH], [path to argp libraries]), [LDFLAGS+="-L$with_argp_lib"]) -AC_ARG_WITH(argp-include, AS_HELP_STRING([--with-argp-include=PATH], [path to argp includes]), [CXXFLAGS+="-I$with_argp_include"]) -AC_CHECK_FUNC([argp_parse], [have_argp=yes], AC_CHECK_LIB([argp], [argp_parse], [have_argp=yes; LIBS="-largp $LIBS"], [have_argp=no])) -if test "x$have_argp" = "xyes"; then - AC_CHECK_HEADER([argp.h], AC_DEFINE([HAVE_ARGP_H], [1], [Defined if argp.h is available.]), AC_MSG_ERROR([argp.h not found])) -else - AC_MSG_ERROR([argp library not found, specify argp-standalone location in --with-argp-lib= and --with-argp-include= options.]) -fi +AC_ARG_WITH(docs, AS_HELP_STRING([--with-docs], [generate documentation]), [ + AC_CHECK_PROGS([HAVE_DOXYGEN], [doxygen], []) + if test -z "$HAVE_DOXYGEN"; then + AC_MSG_ERROR([Could not find doxygen.]) + fi +], []) + AC_ARG_WITH(mqtt, AS_HELP_STRING([--without-mqtt], [disable support for MQTT handling]), [], [with_mqtt=yes]) if test "x$with_mqtt" != "xno"; then AC_CHECK_LIB([mosquitto], [mosquitto_lib_init], - [AC_DEFINE_UNQUOTED(HAVE_MQTT, [1], [Defined if MQTT handling is enabled.]) - EXTRA_LIBS+=" -lmosquitto"], + [AC_DEFINE_UNQUOTED(HAVE_MQTT, [1], [Defined if MQTT handling is enabled.])], [AC_MSG_RESULT([Could not find mosquitto_lib_init in libmosquitto.]) with_mqtt="no"]) fi @@ -150,8 +150,8 @@ 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.]) +AC_DEFINE_UNQUOTED(PACKAGE_PIDFILE, "${localstatedir}/run/${PACKAGE_NAME}.pid", [The path and name of the PID file.]) +AC_DEFINE_UNQUOTED(PACKAGE_LOGFILE, "${localstatedir}/log/${PACKAGE_NAME}.log", [The path and name of the log file.]) AC_DEFINE(SCAN_VERSION, "[m4_esyscmd_s([sed -e 's#^\([0-9]*\.[0-9]*\).*#\1#' -e 's#\.\([0-9]\)$#0\1#' -e 's#\.##' VERSION])]", [The version of the package formatted for the scan result.]) AC_DEFINE(PACKAGE_VERSION_MAJOR, [m4_esyscmd_s([sed -e 's#^\([0-9]*\)\..*$#\1#' VERSION])], [The major version of the package.]) AC_DEFINE(PACKAGE_VERSION_MINOR, [m4_esyscmd_s([sed -e 's#^.*\.\([0-9]*\)$#\1#' VERSION])], [The minor version of the package.]) @@ -161,10 +161,6 @@ if test -n "$GIT_REVISION"; then else AC_DEFINE(REVISION, "[m4_esyscmd_s([git describe --always 2>/dev/null || (date +p%Y%m%d)])]", [The revision of the package.]) fi -AC_CHECK_PROGS([HAVE_DOXYGEN], [doxygen], []) -if test -z "$HAVE_DOXYGEN"; then - AC_MSG_WARN([Doxygen not found - continuing without Doxygen support.]) -fi AM_CONDITIONAL([HAVE_DOXYGEN], [test -n "$HAVE_DOXYGEN"]) AM_COND_IF([HAVE_DOXYGEN], [AC_CONFIG_FILES([docs/Doxyfile])]) diff --git a/contrib/alpine/APKBUILD b/contrib/alpine/APKBUILD index 826b5916a..61b31f79e 100644 --- a/contrib/alpine/APKBUILD +++ b/contrib/alpine/APKBUILD @@ -1,19 +1,18 @@ # Contributor: Tim -# Maintainer: Tim +# Maintainer: John pkgname=ebusd -pkgver=22.4 +pkgver=25.1 pkgrel=0 pkgdesc="Daemon for communication with eBUS heating systems" url="https://github.com/john30/ebusd" # Upstream only supports these archs. arch="x86 x86_64 aarch64 armhf armv7" license="GPL-3.0-only" -makedepends="argp-standalone cmake mosquitto-dev openssl-dev" -source="$pkgname-$pkgver.tar.gz::https://github.com/john30/ebusd/archive/refs/tags/v$pkgver.tar.gz" +makedepends="cmake mosquitto-dev openssl-dev samurai" +source="$pkgname-$pkgver.tar.gz::https://github.com/john30/ebusd/archive/refs/tags/$pkgver.tar.gz" build() { - cmake -B build \ - -DCMAKE_INSTALL_PREFIX=/usr \ + cmake -B build -G Ninja \ -DCMAKE_BUILD_TYPE=MinSizeRel \ -DBUILD_TESTING=ON cmake --build build @@ -24,9 +23,9 @@ check() { } package() { - DESTDIR="$pkgdir" cmake --install build + DESTDIR="$pkgdir" cmake --install build --prefix /usr } sha512sums=" -f625a8813eb5f844d1148eb9d683b9b730573e8c2bc1e3e2fec6462b3943340cfa1cfaf4cd50ff48b45aac47841189ad5d699316907b6abb6e7d1c20a0352842 ebusd-22.4.tar.gz +ff69ee0b36b0e2ad9e44d090ba9507f51430ebb66e79948a44f8c719dbaf00e03a47c791e8f9e305b7dcb8e4fe87a7f00fb8f05c8a7c89e9a1b524e257576d7c ebusd-25.1.tar.gz " diff --git a/contrib/alpine/APKBUILD.git b/contrib/alpine/APKBUILD.git new file mode 100644 index 000000000..1e38304bb --- /dev/null +++ b/contrib/alpine/APKBUILD.git @@ -0,0 +1,37 @@ +# Maintainer: John +pkgname=ebusd-git +pkgver=25.1 +pkgrel=0 +pkgdesc="Daemon for communication with eBUS heating systems" +url="https://github.com/john30/ebusd" +# Upstream only supports these archs. +arch="x86 x86_64 aarch64 armhf armv7" +license="GPL-3.0-only" +makedepends="cmake mosquitto-dev openssl-dev samurai" +source="$pkgname-$pkgver.tar.gz::https://codeload.github.com/john30/ebusd/legacy.tar.gz/refs/heads/master" + +unpack() { + mkdir -p "$srcdir" + msg "Unpacking $s..." + tar -C "$srcdir" -zxf "$SRCDEST/$(filename_from_uri $source)" --strip-components=1 || return 1 +} + +build() { + cmake -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DBUILD_TESTING=ON \ + "$srcdir" + cmake --build build +} + +check() { + ctest --output-on-failure --test-dir build +} + +package() { + DESTDIR="$pkgdir" cmake --install build --prefix /usr +} + +sha512sums=" +REPLACED ${pkgname}-${pkgver}.tar.gz +" \ No newline at end of file diff --git a/contrib/alpine/build-git.sh b/contrib/alpine/build-git.sh new file mode 100644 index 000000000..ade6166a6 --- /dev/null +++ b/contrib/alpine/build-git.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# helper script to check the build from git master is fine +docker run -it --rm -v $PWD:/build -w /build/contrib/alpine alpine:edge sh -c 'apk add --upgrade abuild build-base && adduser -D test && addgroup test abuild && su test -c "abuild-keygen -a && mkdir git && cd git && cp ../APKBUILD.git APKBUILD && abuild -r checksum && abuild -r"' \ No newline at end of file diff --git a/contrib/alpine/build.sh b/contrib/alpine/build.sh index 9ddf0e31e..83e374155 100644 --- a/contrib/alpine/build.sh +++ b/contrib/alpine/build.sh @@ -1,3 +1,3 @@ #!/bin/bash -# helper script to check the build is fine -docker run -it --rm -v $PWD:/build -w /build/contrib/alpine alpine sh -c 'apk add --upgrade abuild && adduser -D test && addgroup test abuild && su test -c "abuild-keygen && abuild -r"' \ No newline at end of file +# helper script to check the release build is fine +docker run -it --rm -v $PWD:/build -w /build/contrib/alpine alpine:edge sh -c 'apk add --upgrade abuild build-base && adduser -D test && addgroup test abuild && su test -c "abuild-keygen -a && abuild -r"' \ No newline at end of file diff --git a/contrib/archlinux/PKGBUILD b/contrib/archlinux/PKGBUILD index 5b117b4ec..9909193a5 100644 --- a/contrib/archlinux/PKGBUILD +++ b/contrib/archlinux/PKGBUILD @@ -2,7 +2,7 @@ # Contributor: Milan Knizek # Usage: makepkg pkgname=ebusd -pkgver=22.4 +pkgver=25.1 pkgrel=1 pkgdesc="ebusd, the daemon for communication with eBUS heating systems." arch=('i686' 'x86_64' 'armv6h' 'armv7h' 'aarch64') @@ -16,7 +16,7 @@ provides=('ebusd') install=ebusd.install options=() backup=('etc/conf.d/ebusd') -source=("https://github.com/john30/${pkgname}/archive/v${pkgver}.tar.gz") +source=("https://github.com/john30/${pkgname}/archive/refs/tags/${pkgver}.tar.gz") pkgver() { cat "${srcdir}/${pkgname}-${pkgver}/VERSION"|sed -e 's#-#_#g' @@ -41,4 +41,4 @@ package() { install -m 0644 contrib/etc/ebusd/mqtt-integration.cfg "${pkgdir}/etc/ebusd/mqtt-integration.cfg" } # update md5sums: updpkgsums -md5sums=('a3875319c4e8547b9ee54681ef8e136a') +md5sums=('213a0ab21600798de5642cc7a0518c76') diff --git a/contrib/archlinux/PKGBUILD.git b/contrib/archlinux/PKGBUILD.git index 7885e4f71..533bbc805 100644 --- a/contrib/archlinux/PKGBUILD.git +++ b/contrib/archlinux/PKGBUILD.git @@ -3,7 +3,7 @@ # Usage: makepkg -p PKGBUILD.git pkgname=ebusd-git _gitname=ebusd -pkgver=22.4 +pkgver=25.1 pkgrel=1 pkgdesc="ebusd, the daemon for communication with eBUS heating systems." arch=('i686' 'x86_64' 'armv6h' 'armv7h' 'aarch64') @@ -43,7 +43,7 @@ package() { install -m 0644 contrib/archlinux/conf.d/ebusd "${pkgdir}/etc/conf.d/ebusd" install -d "${pkgdir}/etc/ebusd" install -m 0644 contrib/etc/ebusd/mqtt-hassio.cfg "${pkgdir}/etc/ebusd/mqtt-hassio.cfg" - install -m 0644 contrib/etc/ebusd/mqtt-integration.cfg "${pkgdir}/etc/ebusd/mqtt-integration.cfg + install -m 0644 contrib/etc/ebusd/mqtt-integration.cfg "${pkgdir}/etc/ebusd/mqtt-integration.cfg" } md5sums=('SKIP') diff --git a/contrib/archlinux/build-git.sh b/contrib/archlinux/build-git.sh new file mode 100644 index 000000000..7c9aac705 --- /dev/null +++ b/contrib/archlinux/build-git.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# helper script to check the build from git master is fine +docker run -it --rm -v $PWD:/build -w /build/contrib/archlinux archlinux sh -c 'pacman -Sy && pacman -Sq fakeroot binutils mosquitto autoconf automake make gcc git && useradd test && su test -c "makepkg -p PKGBUILD.git"' \ No newline at end of file diff --git a/contrib/archlinux/build.sh b/contrib/archlinux/build.sh new file mode 100644 index 000000000..50bf85e34 --- /dev/null +++ b/contrib/archlinux/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# helper script to check the release build is fine +docker run -it --rm -v $PWD:/build -w /build/contrib/archlinux archlinux sh -c 'pacman -Sy && pacman -Sq fakeroot binutils mosquitto autoconf automake make gcc && useradd test && su test -c "makepkg"' \ No newline at end of file diff --git a/contrib/config/README.md b/contrib/config/README.md deleted file mode 100644 index 2ea3d4b31..000000000 --- a/contrib/config/README.md +++ /dev/null @@ -1,8 +0,0 @@ - -ebusd.eu config webservice -========================== - -This is the code of the webservice at https://cfg.ebusd.eu/ that allows ebusd to download the needed CSV configuration -files instead of having to install the ebusd-configuration package. - -It is enabled by default in ebusd due to the default value of the `--configpath=https://cfg.ebusd.eu/` commandline option. diff --git a/contrib/config/index.php b/contrib/config/index.php deleted file mode 100644 index 4bb82fd24..000000000 --- a/contrib/config/index.php +++ /dev/null @@ -1,61 +0,0 @@ - diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index a999bf357..3ba4f3d89 100755 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -1,9 +1,9 @@ ARG BASE_IMAGE -FROM $BASE_IMAGE as build +FROM $BASE_IMAGE AS build RUN apt-get update && apt-get install -y \ - libmosquitto-dev libssl-dev libstdc++6 libc6 libgcc1 \ + knxd-dev knxd libmosquitto-dev libssl-dev libstdc++6 libc6 libgcc1 \ curl \ autoconf automake g++ make git \ && rm -rf /var/lib/apt/lists/* @@ -20,15 +20,15 @@ ENV EBUSD_VERSION $EBUSD_VERSION ENV GIT_REVISION $GIT_REVISION ADD . /build -RUN RUNTEST=full GIT_REVISION=$GIT_REVISION ./make_debian.sh +RUN RUNTEST=full GIT_REVISION=$GIT_REVISION ./make_debian.sh --with-knxd -FROM $BASE_IMAGE-slim as image +FROM $BASE_IMAGE-slim AS image RUN apt-get update && apt-get install -y \ - libmosquitto1 libssl1.1 ca-certificates libstdc++6 libc6 libgcc1 \ + knxd-dev knxd libmosquitto1 libssl1.1 ca-certificates libstdc++6 libc6 libgcc1 \ && rm -rf /var/lib/apt/lists/* LABEL maintainer="ebusd@ebusd.eu" diff --git a/contrib/docker/Dockerfile.release b/contrib/docker/Dockerfile.release index f43157320..2ae411728 100644 --- a/contrib/docker/Dockerfile.release +++ b/contrib/docker/Dockerfile.release @@ -1,6 +1,6 @@ ARG BASE_IMAGE -FROM $BASE_IMAGE as build +FROM $BASE_IMAGE AS build RUN apt-get update && apt-get install -y \ libmosquitto-dev libssl-dev libstdc++6 libc6 libgcc1 \ @@ -25,7 +25,7 @@ RUN GIT_REVISION=$GIT_REVISION ./make_debian.sh -FROM $BASE_IMAGE-slim as image +FROM $BASE_IMAGE-slim AS image RUN apt-get update && apt-get install -y \ libmosquitto1 libssl1.1 ca-certificates libstdc++6 libc6 libgcc1 \ @@ -43,7 +43,7 @@ ENV EBUSD_VERSION $EBUSD_VERSION LABEL version="${EBUSD_VERSION}-${EBUSD_ARCH}" -ADD https://github.com/john30/ebusd/releases/download/v${EBUSD_VERSION}/ebusd-${EBUSD_VERSION}_${TARGETARCH}${TARGETVARIANT}-${EBUSD_IMAGE}_mqtt1.deb ebusd.deb +ADD https://github.com/john30/ebusd/releases/download/${EBUSD_VERSION}/ebusd-${EBUSD_VERSION}_${TARGETARCH}${TARGETVARIANT}-${EBUSD_IMAGE}_mqtt1.deb ebusd.deb RUN dpkg -i "--path-exclude=/etc/default/*" "--path-exclude=/etc/init.d/*" "--path-exclude=/lib/systemd/*" ebusd.deb && rm -f ebusd.deb \ && update-ca-certificates \ diff --git a/contrib/docker/Dockerfile.template b/contrib/docker/Dockerfile.template index ae486783f..7d384f64f 100644 --- a/contrib/docker/Dockerfile.template +++ b/contrib/docker/Dockerfile.template @@ -1,9 +1,9 @@ ARG BASE_IMAGE -FROM $BASE_IMAGE as build +FROM $BASE_IMAGE AS build RUN apt-get update && apt-get install -y \ - libmosquitto-dev libssl-dev libstdc++6 libc6 libgcc1 \ + %EBUSD_EXTRAPKGS%libmosquitto-dev libssl-dev libstdc++6 libc6 libgcc1 \ curl \ autoconf automake g++ make git \ && rm -rf /var/lib/apt/lists/* @@ -25,10 +25,10 @@ RUN %EBUSD_MAKE% %EBUSD_UPLOAD_LINES% -FROM $BASE_IMAGE-slim as image +FROM $BASE_IMAGE-slim AS image RUN apt-get update && apt-get install -y \ - libmosquitto1 libssl1.1 ca-certificates libstdc++6 libc6 libgcc1 \ + %EBUSD_EXTRAPKGS%libmosquitto1 libssl1.1 ca-certificates libstdc++6 libc6 libgcc1 \ && rm -rf /var/lib/apt/lists/* LABEL maintainer="ebusd@ebusd.eu" diff --git a/contrib/docker/README.md b/contrib/docker/README.md index 43a80c4da..dfe681744 100644 --- a/contrib/docker/README.md +++ b/contrib/docker/README.md @@ -2,10 +2,10 @@ ebusd Docker image ================== An [ebusd](https://github.com/john30/ebusd/) Docker image is available on the -[Docker Hub](https://hub.docker.com/r/john30/ebusd/) and is able to download the latest released German -[configuration files](https://github.com/john30/ebusd-configuration/) from a dedicated webservice. +[Docker Hub](https://hub.docker.com/r/john30/ebusd/) and is able to download the latest released +[configuration files](https://github.com/john30/ebusd-configuration/) from a [dedicated webservice](https://ebus.github.io/). -It allows you to run ebusd without actually installing (or even building) it on your system. +It allows running ebusd without actually installing (or even building) it on the host. You might even be able to run it on a non-Linux operating system, which is at least known to work well on a Synology Diskstation as well as Windows with Docker Desktop. @@ -21,26 +21,24 @@ The image is able to run on any of the following architectures and the right ima * arm32v7 * arm64v8 -Due to changes of docker hub policies, the development set of images with "devel" tag are currently not automatically -built with every commit to the git repository. Run the following command to use it: +In addition to the default "latest" tag, a development set of images is available with "devel" tag. This is built +automatically from the latest source on the git repository. Run the following command to use it: > docker pull john30/ebusd:devel Running interactively --------------------- - -To run an ebusd container interactively, e.g. on serial device /dev/ttyUSB1, use the following command: -> docker run --rm -it --device=/dev/ttyUSB1:/dev/ttyUSB0 -p 8888 john30/ebusd +To run an ebusd container interactively, e.g. on enhanced serial device /dev/ttyUSB1, use the following command: +> docker run --rm -it --device=/dev/ttyUSB1:/dev/ttyUSB0 -p 8888 john30/ebusd -d ens:/dev/ttyUSB0 This will show the ebusd output directly in the terminal. Running in background --------------------- - -To start an ebusd container and have it run in the background, e.g. on serial device /dev/ttyUSB1, use the following command: -> docker run -d --name=ebusd --device=/dev/ttyUSB1:/dev/ttyUSB0 -p 8888 john30/ebusd +To start an ebusd container and have it run in the background, e.g. on enhanced serial device /dev/ttyUSB1, use the following command: +> docker run -d --name=ebusd --device=/dev/ttyUSB1:/dev/ttyUSB0 -p 8888 john30/ebusd -d ens:/dev/ttyUSB0 The container has the name "ebusd", so you can use that when querying docker about the container. @@ -50,10 +48,14 @@ In order to get the log output from ebusd, use the following command: Using a network device ---------------------- - When using a network device, the "--device" argument to docker can be omitted, but the device information has to be passed on to ebusd: -> docker run --rm -it -p 8888 john30/ebusd --scanconfig -d 192.168.178.123:10000 --latency=20 +> docker run --rm -it -p 8888 john30/ebusd --scanconfig -d ens:192.168.178.123 --latency=20 + +If mDNS device discovery is supposed to be used, then the container needs to run on the host network instead of the default bridge network, +as multicast traffic is usually only routed via the host network, i.e. use "--network=host" as additional argument. +Then the device argument can be omitted (as long as there is only one mDNS discoverable device on the net), e.g.: +> docker run --rm -it -p 8888 --network=host john30/ebusd --scanconfig --latency=20 Note: the required "-f" (foreground) argument is passed as environment variable and does not need to be specified anymore. @@ -65,16 +67,16 @@ Running with MQTT broker ------------------------ To start an ebusd container in the background and have it connect to your MQTT broker, use the following command while replacing "BROKERHOST" with your MQTT broker host name or IP address: -> docker run -d --name=ebusd --device=/dev/ttyUSB0 -p 8888 john30/ebusd --scanconfig -d /dev/ttyUSB0 --mqttport=1883 --mqtthost=BROKERHOST +> docker run -d --name=ebusd --device=/dev/ttyUSB0 -p 8888 john30/ebusd --scanconfig -d ens:/dev/ttyUSB0 --mqttport=1883 --mqtthost=BROKERHOST Use of environment variables ---------------------------- Instead of passing arguments (at the end of docker run) to ebusd, almost all (long) arguments can also be passed as environment variables with the prefix `EBUSD_`, e.g. the following line can be used instead of the last example above: -> docker run -d --name=ebusd --device=/dev/ttyUSB0 -p 8888 -e EBUSD_SCANCONFIG= -e EBUSD_DEVICE=/dev/ttyUSB0 -e EBUSD_MQTTPORT=1883 -e EBUSD_MQTTHOST=BROKERHOST john30/ebusd +> docker run -d --name=ebusd --device=/dev/ttyUSB0 -p 8888 -e EBUSD_SCANCONFIG= -e EBUSD_DEVICE=ens:/dev/ttyUSB0 -e EBUSD_MQTTPORT=1883 -e EBUSD_MQTTHOST=BROKERHOST john30/ebusd -This eases use of e.g. docker-compose files. +This eases use of e.g. "docker-compose.yaml" files like [the example docker-compose file](https://github.com/john30/ebusd/blob/master/contrib/docker/docker-compose.example.yaml) also describing each available environment variable in it. Running newer images on older operating systems diff --git a/contrib/docker/build.sh b/contrib/docker/build.sh index fbed022fb..a77786914 100755 --- a/contrib/docker/build.sh +++ b/contrib/docker/build.sh @@ -39,7 +39,7 @@ elif [[ "x$1" = "xrelease" ]]; then else namesuffix='.build' target=deb - images='bullseye buster stretch' + images='bookworm bullseye buster' if [[ -n "$LIMITIMG" ]]; then images=$(echo " $images " | sed -e "s#.* \($LIMITIMG\) .*#\1#") echo "limiting to image $images" diff --git a/contrib/docker/docker-compose.example.yaml b/contrib/docker/docker-compose.example.yaml index 373b31a1d..5b094c362 100644 --- a/contrib/docker/docker-compose.example.yaml +++ b/contrib/docker/docker-compose.example.yaml @@ -1,5 +1,6 @@ services: ebusd: + # alternatively, use "john30/ebusd:devel" for the latest build of the current source code image: john30/ebusd container_name: ebusd restart: unless-stopped @@ -13,9 +14,16 @@ services: environment: # Device options: - # Use DEV as eBUS device ("enh:DEVICE" or "enh:IP:PORT" for enhanced device, "ens:DEVICE" for enhanced high speed - # serial device, "DEVICE" for serial device, or "[udp:]IP:PORT" for network device) - EBUSD_DEVICE: "/dev/ttyUSB0" + # Use DEV as eBUS device: + # - "mdns:" for auto discovery via mDNS with optional suffix "[ID][@INTF]" for using a specific hardware ID + # and/or IP interface INTF for the discovery (only for eBUS Adapter Shield; on docker, the network device + # needs to support multicast routing e.g. like the host network), or + # - prefix "ens:" for enhanced high speed device, + # - prefix "enh:" for enhanced device, or + # - no prefix for plain device, and + # - suffix "IP[:PORT]" for network device, or + # - suffix "DEVICE" for serial device + EBUSD_DEVICE: "ens:/dev/ttyUSB0" # Skip serial eBUS device test #EBUSD_NODEVICECHECK: "" # Only read from device, never write to it @@ -29,9 +37,15 @@ services: # Read CSV config files from PATH (local folder or HTTPS URL) #EBUSD_CONFIGPATH: "/path/to/local/configs" - # Pick CSV config files matching initial scan (ADDR="none" or empty for no initial scan message, "full" for full - # scan, or a single hex address to scan, default is broadcast ident message). + # Pick CSV config files matching initial scan. + # - empty for broadcast ident message (default when EBUSD_CONFIGPATH is not given), + # - "none" for no initial scan message, + # - "full" for full scan, + # - a single hex address to scan, or + # - "off" for not picking CSV files by scan result (default when EBUSD_CONFIGPATH is given). EBUSD_SCANCONFIG: "" + # Retry scanning devices COUNT times + #EBUSD_SCANRETRIES: 5 # Prefer LANG in multilingual configuration files #EBUSD_CONFIGLANG: "en" # Poll for data every SEC seconds (0=disable) @@ -43,8 +57,8 @@ services: # eBUS options: - # Use ADDR as own bus address - #EBUSD_ADDRESS: ff + # Use hex ADDR as own master bus address + #EBUSD_ADDRESS: "ff" # Actively answer to requests from other masters #EBUSD_ANSWER: "" # Stop bus acquisition after MSEC ms @@ -66,7 +80,7 @@ services: #EBUSD_ACCESSLEVEL: "*" # Read access control list from FILE #EBUSD_ACLFILE: "/path/to/aclfile" - # Enable hex command + # Enable hex/inject/answer commands #EBUSD_ENABLEHEX: "" # Enable define command #EBUSD_ENABLEDEFINE: "" @@ -87,12 +101,12 @@ services: # Write log to FILE (only for daemon, empty string for using syslog) #EBUSD_LOGFILE: "/var/log/ebusd.log" - # Only write log for matching AREA(S) below or equal to LEVEL (alternative to EBUSD_LOGAREAS/EBUSD_LOGLEVEL, may - # be used multiple times) + # Only write log for matching AREA(S) up to LEVEL (alternative to EBUSD_LOGAREAS/EBUSD_LOGLEVEL, may be used + # multiple times) #EBUSD_LOG: "all:notice" - # Only write log for matching AREA(S): main|network|bus|update|other|all + # Only write log for matching AREA(S): main|network|bus|device|update|other|all #EBUSD_LOGAREAS: "all" - # Only write log below or equal to LEVEL: error|notice|info|debug + # Only write log up to LEVEL: error|notice|info|debug #EBUSD_LOGLEVEL: "notice" # Raw logging options: @@ -137,8 +151,8 @@ services: #EBUSD_MQTTQOS: 0 # Read MQTT integration settings from FILE (no default) #EBUSD_MQTTINT: "/etc/ebusd/mqtt-hassio.cfg" - # Add variable(s) to the read MQTT integration settings - #EBUSD_MQTTVAR: "key=value" + # Add variable(s) to the read MQTT integration settings (append to already existing value with "NAME+=VALUE") + #EBUSD_MQTTVAR: "name[+]=value[,...]" # Publish in JSON format instead of strings, optionally in short (value directly below field key) #EBUSD_MQTTJSON: "" # Publish all available attributes @@ -168,7 +182,7 @@ services: #EBUSD_KNXURL: "" # Maximum age in seconds for using the last value of read messages (0=disable) #EBUSD_KNXRAGE: 30 - # Maximum age in seconds for using the last value for reads on write messages (0=disable), + # Maximum age in seconds for using the last value for reads on write messages (0=disable) #EBUSD_KNXWAGE: 7200 # Read KNX integration settings from FILE #EBUSD_KNXINT: "/etc/ebusd/knx.cfg" diff --git a/contrib/docker/update.sh b/contrib/docker/update.sh index 626ae5eab..914055b4f 100755 --- a/contrib/docker/update.sh +++ b/contrib/docker/update.sh @@ -5,6 +5,7 @@ function replaceTemplate () { sed \ -e "s#%EBUSD_MAKE%#${make}#g" \ -e "s#%EBUSD_VERSION_VARIANT%#${version_variant}#g" \ + -e "s#%EBUSD_EXTRAPKGS%#${extrapkgs}#g" \ -e "s#%EBUSD_UPLOAD_LINES%#${upload_lines}#g" \ -e "s#%EBUSD_COPYDEB%#${copydeb}#g" \ -e "s#%EBUSD_DEBSRC%#${debsrc}#g" \ @@ -15,8 +16,8 @@ function replaceTemplate () { # devel update version_variant='-devel' -make='RUNTEST=full GIT_REVISION=\$GIT_REVISION ./make_debian.sh' -upload_lines='' +make='RUNTEST=full GIT_REVISION=\$GIT_REVISION ./make_debian.sh --with-knxd' +extrapkgs='knxd-dev knxd ' copydeb='COPY --from=build /build/ebusd-*_mqtt1.deb ebusd.deb' debsrc='ebusd.deb \&\& rm -f ebusd.deb' copyentry='COPY --from=build /build/contrib/docker/docker-entrypoint.sh /' @@ -26,7 +27,8 @@ replaceTemplate # release update version_variant='' make='GIT_REVISION=\$GIT_REVISION ./make_debian.sh' -copydeb="ADD https://github.com/john30/ebusd/releases/download/v\${EBUSD_VERSION}/ebusd-\${EBUSD_VERSION}_\${TARGETARCH}\${TARGETVARIANT}-\${EBUSD_IMAGE}_mqtt1.deb ebusd.deb" +extrapkgs='' +copydeb="ADD https://github.com/john30/ebusd/releases/download/\${EBUSD_VERSION}/ebusd-\${EBUSD_VERSION}_\${TARGETARCH}\${TARGETVARIANT}-\${EBUSD_IMAGE}_mqtt1.deb ebusd.deb" copyentry='COPY contrib/docker/docker-entrypoint.sh /' namesuffix='.release' replaceTemplate @@ -34,7 +36,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&b=$GIT_BRANCH" || echo "failed"; done; fi' + upload_lines='ARG UPLOAD_URL\nARG UPLOAD_CREDENTIALS\nARG UPLOAD_OS\nRUN if [ -n "\$UPLOAD_URL" ] \&\& [ -n "\$UPLOAD_CREDENTIALS" ]; then for img in ebusd-*.deb; do echo -n "upload \$img: "; curl -fsSk -u "\$UPLOAD_CREDENTIALS" -X POST --data-binary "@\$img" -H "Content-Type: application/octet-stream" "\$UPLOAD_URL/\$img?a=\$EBUSD_ARCH\&o=\$UPLOAD_OS\&v=\$EBUSD_VERSION\&b=$GIT_BRANCH" || echo "failed"; done; fi' upload_lines+='\n\n\nFROM scratch as deb\nCOPY --from=build /build/*.deb /' namesuffix='.build' replaceTemplate diff --git a/contrib/etc/ebusd/broadcast.csv b/contrib/etc/ebusd/broadcast.csv index c14d0a6e7..b4f69a71d 100644 --- a/contrib/etc/ebusd/broadcast.csv +++ b/contrib/etc/ebusd/broadcast.csv @@ -2,10 +2,9 @@ *r,broadcast,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, *b,broadcast,,,,FE,,,,,,,,,,,,,,,,,,,,,,,,,, *w,broadcast,,,,FE,,,,,,,,,,,,,,,,,,,,,,,,,, -b,,datetime,date/time,,,0700,,outsidetemp,,D2B,,,°C,time,,BTI,,,,date,,BDA,,,,,,,,, +b,,datetime,date/time,,,0700,,outsidetemp,,D2B,,°C,,time,,BTI,,,,date,,BDA,,,,,,,,, r;b,,id,identification,,,0704,,manufacturer,,UCH,0x06=Dungs;0x0f=FH Ostfalia;0x10=TEM;0x11=Lamberti;0x14=CEB;0x15=Landis-Staefa;0x16=FERRO;0x17=MONDIAL;0x18=Wikon;0x19=Wolf;0x20=RAWE;0x30=Satronic;0x40=ENCON;0x50=Kromschröder;0x60=Eberle;0x65=EBV;0x75=Grässlin;0x85=ebm-papst;0x95=SIG;0xa5=Theben;0xa7=Thermowatt;0xb5=Vaillant;0xc0=Toby;0xc5=Weishaupt;0xfd=ebusd.eu,,device manufacturer,id,,STR:5,,,device id,software,,PIN,,,software version,hardware,,PIN,,,hardware version -w,,id,identification,,FE,0704,,,,,,,,,,,,,,,,,,,,,,,,, -w,,queryexistence,Inquiry of existence,,FE,07FE,,,,,,,,,,,,,,,,,,,,,,,,, +w,,queryexistence,Inquiry of existence,,,07FE,,,,,,,,,,,,,,,,,,,,,,,,, b,,signoflife,sign of life,,,07FF,,,,,,,,,,,,,,,,,,,,,,,,, b,,error,error message,,,FE01,,error,,STR:10,,,,,,,,,,,,,,,,,,,,, b,,netresetstate,reset network start,,,FF00,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/contrib/etc/ebusd/mqtt-hassio.cfg b/contrib/etc/ebusd/mqtt-hassio.cfg index d3d4d6162..921e08c29 100644 --- a/contrib/etc/ebusd/mqtt-hassio.cfg +++ b/contrib/etc/ebusd/mqtt-hassio.cfg @@ -1,7 +1,7 @@ # Configuration file for ebusd MQTT integration with Home Assistant (https://www.home-assistant.io/). # Use this file with ebusd in MQTT JSON mode to have many seen messages automatically appear on HA. This is achieved by -# using the MQTT Discovery feature of Home Assistant (see https://www.home-assistant.io/docs/mqtt/discovery/). +# using the MQTT Discovery feature of Home Assistant (see https://www.home-assistant.io/integrations/mqtt#mqtt-discovery). # The commandline options to ebusd should contain e.g.: # --mqttport=1883 --mqttjson --mqttint=/etc/ebusd/mqtt-hassio.cfg @@ -52,11 +52,12 @@ # %field the field name and key for JSON objects (equals the field index if no name is defined or the names are not # unique in the message). # %fieldname the field name (and nothing else like in %field, might be empty though!) +# %fieldnamemult the field name unless there is a single field only (similar to %fieldname) # %type the field type (one of "number", "list", "string", "date", "time", or "datetime"). # %basetype the base data type ID (e.g. "UCH"). # %comment the field comment (if any). # %unit the field unit (if any). -# %min and %max the minimum/maximum possible value for number fields. +# %min, %max, and %step the minimum/maximum possible value and step value for number fields. # %topic the field (or message) update topic built from the mqtttopic configuration option and/or the %topic variable # defined here. @@ -90,7 +91,8 @@ # - "|" allows defining alternatives, e.g. "a|b" matches "but" as well as "all". # - "^" matches the beginning of the input, e.g. "^al" matches "al" but not "hal". # - "$" matches the end of the input, e.g. "al$" matches "hal" but not "all". -# - "*" matches a single arbitrary length wildcard part in the middle, e.g. "^a*l$" matches "all" but not "always". +# - "*" matches a single arbitrary length wildcard part in the middle (can only be used once per filter/alternative), +# e.g. "^a*l$" matches "all" but not "always". # include only messages having data sent at least once (only checked for passive or read messages, not for active write) # when set to 1. If set to >1, then all messages passing the other filter criteria (including active read messages) will @@ -105,7 +107,7 @@ filter-seen = 5 #filter-non-circuit = # include only messages having the specified name (partial match, alternatives and wildcard supported). # HA integration: filter to some useful names for monitoring the heating circuit -filter-name = status|temp|yield|count|energy|power|runtime|hours|starts|mode|curve|^load$|^party$|sensor +filter-name = status|temp|humidity|yield|count|energy|power|runtime|hours|starts|mode|curve|^load$|^party$|sensor|timer # exclude messages having the specified name (partial match, alternatives and wildcard supported). #filter-non-name = # include only messages having the specified level (partial match, alternatives and wildcard supported). @@ -146,19 +148,20 @@ circuit_part = { # This is also an implicit field type filter as missing or empty mappings are not published at all. type_map-number = number type_map-list = string -# HA integration: skip string/date/time types completely +type_map-time = time +type_map-date = date +type_map-datetime = datetime + +# HA integration: skip string types completely type_map-string = -type_map-date = -type_map-time = -type_map-datetime = # field type switch designator, see below. -# HA integration: this is used to set several variable values depending on the field type, name, message, and field unit. +# HA integration: this is used to set several variable values depending on the field type, message name+field name, and unit. type_switch-by = %name%field,%unit # field type switch variables names to use in addition to %type_switch in case of multiple keys (separated by comma). -# HA integration: var names for topic/class/state values mapped from field type, name, message, and unit. -type_switch-names = type_topic,type_class,type_state +# HA integration: var names for topic/class/state/sub values mapped from field type, message name+field name, and unit. +type_switch-names = type_topic,type_class,type_state,type_sub # field type switch for each field type and optionally direction (between dash before the field type) available as # %type_switch (as well as the variable names defined in "type_switch-names" if any). @@ -167,24 +170,30 @@ type_switch-names = type_topic,type_class,type_state # when the wildcard string in the right part matched the "type_switch-by" value. The list is traversed from top to # bottom stopping at the first match. If direction specific definitions exist, these are traversed first. If no line # matches at all, the variable(s) are set to the empty string. -# HA integration: the mapping list for (potentially) writable number entities by field type, name, message, and unit. +# HA integration: the mapping list for (potentially) writable number entities field type, message name+field name, and unit. type_switch-w-number = number,temperature, = temp|,°C$ + number,temperature, = temp|,K$ + number,, = integral|,°min$ number,power_factor, = power*%% number,power, = power|,kW$|,W$ number,voltage, = volt|,V$ - number,current, = current,|,A$ + number,current, = current,|currentvalue,|,A$ number,pressure, = bar$ number,gas, = gas*/min$ number,humidity, = humid*%%$ + number,, = curve,|curvevalue, -# HA integration: the mapping list for numeric sensor entities by field type, name, message, and unit. +# HA integration: the mapping list for numeric sensor entities field type, message name+field name, and unit. type_switch-number = + sensor,,total_increasing = poweron|count,|countvalue, sensor,temperature,measurement = temp|,°C$ + sensor,temperature,measurement = temp|,K$ sensor,power_factor,measurement = power*%% sensor,power,measurement = power|,kW$|,W$ sensor,voltage,measurement = volt|,V$ - sensor,current,measurement = current,|,A$ + sensor,current,measurement = current,|currentvalue,|,A$ + sensor,,measurement = integral|,°min$ sensor,energy,total_increasing = energy|,Wh$ sensor,yield,total_increasing = total*,Wh$ sensor,,total_increasing = hours|,h$ @@ -194,24 +203,44 @@ type_switch-number = sensor,humidity,measurement = humid*%%$ sensor,, = -# HA integration: the mapping list for (potentially) writable binary switch entities by field type, name, message, and unit. +# HA integration: the mapping list for (potentially) writable binary switch entities field type, message name+field name, and unit. type_switch-w-list = switch,, = onoff + switch,,,yesno = yesno + select,, = -# HA integration: the mapping list for rather binary sensor entities by field type, name, message, and unit. +# HA integration: the mapping list for rather binary sensor entities field type, message name+field name, and unit. type_switch-list = binary_sensor,,measurement = onoff - sensor,, = + binary_sensor,,measurement,yesno = yesno + sensor,,,list = + +# HA integration: the mapping list for (potentially) writable string entities containing a time value field type, message name+field name, and unit. +type_switch-w-time = + text,,,time = from,|to,|time2,|timer + text,,,time3 = time,|timevalue, + +type_switch-time = + sensor,,,time = from,|to,|time2,|timer + sensor,,,time3 = time,|timevalue, + +# HA integration: the mapping list for (potentially) writable string entities containing a date value field type, message name+field name, and unit. +type_switch-w-date = + text,,,date = + +type_switch-date = + sensor,,, = + +# HA integration: the mapping list for (potentially) writable string entities containing a datetime value field type, message name+field name, and unit. +type_switch-w-datetime = + text,,,datetime = + +type_switch-datetime = + sensor,,, = # HA integration: currently unused mapping lists for non-numeric/non-binary entities. #type_switch-string = # sensor,, = -#type_switch-date = -# sensor,,measurement = -#type_switch-time = -# sensor,,measurement = -#type_switch-datetime = -# sensor, = # HA integration: optional variable with the entity device class for numbers type_class_number ?= , @@ -229,17 +258,27 @@ state_class ?= , unit_of_measurement ?= , "unit_of_measurement":"%unit" +# HA integration: optional variable with the minimum numeric value +min_number ?= , + "min":%min + +# HA integration: optional variable with the maximum numeric value +max_number ?= , + "max":%max + +# HA integration: optional variable with the numeric step value +step_number ?= , + "step":%step + # field type part suffix to use instead of the field type itself, see below. -# HA integration: %type_part variable mapped from the mapped %type_topic -type_part-by = %type_topic +# HA integration: %type_part variable mapped from the mapped %type_topic and optional %type_sub +type_part-by = %type_topic%type_sub # field type part mappings for each field type (or the "type_part-by" variable value) in the suffix (available as # %type_part). # HA integration: %type_part variable for number %type_topic type_part-number = , - "command_topic":"%topic/set", - "min":%min, - "max":%max%unit_of_measurement%state_class%type_class_number + "command_topic":"%topic/set"%min_number%max_number%step_number%unit_of_measurement%state_class%type_class_number # HA integration: %type_part variable for sensor %type_topic type_part-sensor = %unit_of_measurement%state_class%type_class_sensor @@ -250,11 +289,56 @@ type_part-switch = , "payload_on":"on", "payload_off":"off"%state_class +type_part-switchyesno = , + "command_topic":"%topic/set", + "payload_on":"yes", + "payload_off":"no"%state_class + # HA integration: %type_part variable for binary_sensor %type_topic type_part-binary_sensor = , "payload_on":"on", "payload_off":"off"%state_class +type_part-binary_sensoryesno = , + "payload_on":"yes", + "payload_off":"no"%state_class + +# HA integration: %type_part variable for text %type_topic +type_part-texttime = , + "command_topic":"%topic/set", + "pattern": "^[012][0-9]:[0-5][0-9]$"%state_class +type_part-texttime3 = , + "command_topic":"%topic/set", + "pattern": "^[012][0-9]:[0-5][0-9]:[0-5][0-9]$"%state_class + +# HA integration: %type_part variable for date %type_topic +type_part-textdate = , + "command_topic":"%topic/set", + "pattern": "^[0-3][0-9].[01][0-9].20[0-3][0-9]$"%state_class + +# HA integration: %type_part variable for datetime %type_topic +type_part-textdatetime = , + "command_topic":"%topic/set", + "pattern": "^[0-3][0-9].[01][0-9].20[0-3][0-9] [012][0-9]:[0-5][0-9]$"%state_class + +# optional format string for converting a fields value list into %field_values. +# "$value" and "$text" are being replaced by the corresponding part. +field_values-entry = "$text" +# optional separator for concatenating of field value list items. +field_values-separator = , +# optional prefix for surrounding field value list items. +field_values-prefix = [ +# optional suffix for surrounding field value list items. +field_values-suffix = ] + +# HA integration: %type_part variable for select %type_topic and sensor with list of known values +type_part-select = , + "command_topic":"%topic/set", + "options":%field_values +type_part-sensorlist = , + "device_class":"enum", + "options":%field_values + # the field specific part (evaluated after the message specific part). # HA integration: set to the mapped %type_part from above field_payload = %type_part @@ -267,7 +351,7 @@ definition-topic ?= %haprefix/%type_topic/%{TOPIC}_%FIELD/config # HA integration: this is the config topic payload for HA's MQTT discovery. definition-payload = { "unique_id":"%{TOPIC}_%FIELD", - "name":"%prefixn %circuit %name %fieldname", + "name":"%name %fieldnamemult", "device":%circuit_part, "value_template":"{{value_json[\"%field\"].value}}", "state_topic":"%topic"%field_payload @@ -296,11 +380,10 @@ global_prefix = { "unique_id":"%TOPIC", "device":%global_device, "state_topic":"%topic", - "name":"%prefixn %name" + "name":"global %name" # HA integration: boolean suffix for global parts global_boolean_suffix = , - "state_class":"measurement", "payload_on":"true", "payload_off":"false" } @@ -309,8 +392,7 @@ global_boolean_suffix = , # and scan if not otherwise defined explicitly). # HA integration: the config topic for HA's MQTT discovery for the ebusd global parts. def_global-topic = %haprefix/sensor/%TOPIC/config -def_global-payload = %global_prefix, - "state_class":"measurement" +def_global-payload = %global_prefix } #def_global-retain = 0 @@ -320,6 +402,7 @@ def_global_running-payload = %global_prefix, "device_class":"running"%global_boolean_suffix def_global_version-topic = def_global_uptime-payload = %global_prefix, + "device_class":"duration", "state_class":"total_increasing", "unit_of_measurement":"s" } @@ -327,9 +410,26 @@ def_global_signal-topic = %haprefix/binary_sensor/%TOPIC/config def_global_signal-payload = %global_prefix, "device_class":"connectivity"%global_boolean_suffix +def_global_updatecheck-topic = %haprefix/update/%TOPIC/config def_global_updatecheck-payload = %global_prefix, - "state_class":"measurement", - "value_template":"{{value_json|truncate(255)}}" + "value_template":"{%% set my_new = value_json|truncate(255)|regex_replace(find=',.*| available|revision v|version v|OK',replace='') %%}{%% if my_new == '' %%}{%% set my_new = '%version' %%}{%% endif %%}{{ {'installed_version':'%version','latest_version':my_new,'entity_picture':'https://ebusd.eu/logo-32x32.png','release_url':'https://github.com/john30/ebusd/releases/latest'} | tojson }}" + } + + +# optional secondary update check for the enhanced eBUS device (consuming the same topic though!) +def_global_updatecheck_device-topic = %haprefix/update/%{TOPIC}_device/config +def_global_updatecheck_device-payload = { + "unique_id":"%{TOPIC}_device", + "device":{ + "identifiers":"%{PREFIXN}_device", + "manufacturer":"ebusd.eu", + "name":"%prefixn eBUS device", + "via_device":"%PREFIXN", + "suggested_area":"%area" + }, + "state_topic":"%topic", + "name":"%name", + "value_template":"{%% set my_new = value_json|truncate(255)|regex_replace(find='^[^,]*|, device firmware |,.*| available',replace='') %%}{%% set my_ds = 'v31/firmware/' %%}{%% if my_new is search('up to date') %%}{%% set my_ds = my_new|regex_replace(find=' .*',replace='/') %%}{%% set my_new = 'current' %%}{%% set my_cur = 'current' %%}{%% else %%}{%% set my_cur = 'old' %%}{%% if my_new is search(' ') %%}{%% set my_ds = my_new|regex_replace(find=' .*',replace='/') %%}{%% set my_new = my_new|regex_replace(find='.* ',replace='') %%}{%% else %%}{%% if my_new == '' %%}{%% set my_new = 'current' %%}{%% set my_cur = 'current' %%}{%% endif %%}{%% endif %%}{%% endif %%}{{ {'installed_version':my_cur,'latest_version':my_new,'entity_picture':'https://adapter.ebusd.eu/'+my_ds+'favicon.ico','release_url':'https://adapter.ebusd.eu/'+my_ds+'ChangeLog'} | tojson }}" } # the topic and payload to listen to in order to republish all config messages. diff --git a/contrib/etc/ebusd/mqtt-integration.cfg b/contrib/etc/ebusd/mqtt-integration.cfg index 82f8ce492..5b8c4fdb9 100644 --- a/contrib/etc/ebusd/mqtt-integration.cfg +++ b/contrib/etc/ebusd/mqtt-integration.cfg @@ -50,7 +50,7 @@ # %basetype the base data type ID (e.g. "UCH"). # %comment the field comment (if any). # %unit the field unit (if any). -# %min and %max the minimum/maximum possible value for number fields. +# %min, %max, and %step the minimum/maximum possible value and step value for number fields. # %topic the field (or message) update topic built from the mqtttopic configuration option and/or the %topic variable # defined here. @@ -84,7 +84,8 @@ # - "|" allows defining alternatives, e.g. "a|b" matches "but" as well as "all". # - "^" matches the beginning of the input, e.g. "^al" matches "al" but not "hal". # - "$" matches the end of the input, e.g. "al$" matches "hal" but not "all". -# - "*" matches a single arbitrary length wildcard part in the middle, e.g. "^a*l$" matches "all" but not "always". +# - "*" matches a single arbitrary length wildcard part in the middle (can only be used once per filter/alternative), +# e.g. "^a*l$" matches "all" but not "always". # include only messages having data sent at least once (only checked for passive or read messages, not for active write) # when set to 1. If set to >1, then all messages passing the other filter criteria (including active read messages) will @@ -149,6 +150,17 @@ type_map-datetime = string #type_part-number = , +# optional format string for converting a fields value list into %field_values. +# "$value" and "$text" are being replaced by the corresponding part. +#field_values-entry = $text +# optional separator for concatenating of field value list items. +#field_values-separator = , +# optional prefix for surrounding field value list items. +#field_values-prefix = +# optional suffix for surrounding field value list items. +#field_values-suffix = + + # the field specific part (evaluated after the message specific part). #field_payload = %type_part @@ -178,11 +190,14 @@ def_global-payload = { #def_global-retain = 0 # individual global running, version, signal, uptime, updatecheck, and scan config topic, payload, and retain setting. +# a secondary update check for the eBUS device (consuming the same updatecheck topic) can be set up, which will only be +# used if an enhanced eBUS device supporting extra info is present (does not make use of the common global defaults). #def_global_running-... #def_global_version-... #def_global_signal-... #def_global_uptime-... #def_global_updatecheck-... +#def_global_updatecheck_device-... #def_global_scan-... diff --git a/contrib/gentoo/conf.d/ebusd b/contrib/gentoo/conf.d/ebusd index 0d1b72876..2abae6bec 100644 --- a/contrib/gentoo/conf.d/ebusd +++ b/contrib/gentoo/conf.d/ebusd @@ -1,5 +1,5 @@ -# /etc/conf.d/ebusd: -# config file for ebusd service. +# Copyright 1999-2024 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 -# Options to pass to ebusd (run "ebusd -?" for more info): +# Options to pass to ebusd (run "ebusd --help" for more info) EBUSD_OPTS="--scanconfig" diff --git a/contrib/gentoo/init.d/ebusd b/contrib/gentoo/init.d/ebusd index 9735db355..eabbf36d9 100755 --- a/contrib/gentoo/init.d/ebusd +++ b/contrib/gentoo/init.d/ebusd @@ -1,14 +1,17 @@ -#!/sbin/runscript -# Copyright 1999-2013 Gentoo Foundation +#!/sbin/openrc-run +# Copyright 1999-2024 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 -# $Header: $ command="/usr/bin/ebusd" command_args="${EBUSD_OPTS}" -start_stop_daemon_args="--quiet" -description="ebusd, the daemon for communication with eBUS heating systems." +logfile_path="/var/log/ebusd" +logfile="${logfile_path}/ebusd.log" +name="eBUS daemon" +pidfile_path="/run/ebusd" +pidfile="${pidfile_path}/ebusd.pid" -depend() { - need localmount - use logger +start_pre() { + checkpath -d -q "${logfile_path}" + checkpath -d -q "${pidfile_path}" + checkpath -f -q "${logfile}" } diff --git a/contrib/gentoo/systemd/ebusd.service b/contrib/gentoo/systemd/ebusd.service new file mode 100644 index 000000000..b66f44352 --- /dev/null +++ b/contrib/gentoo/systemd/ebusd.service @@ -0,0 +1,13 @@ +[Unit] +Description=eBUS daemon +After=network-online.target + +[Service] +Type=forking +User=ebusd +Group=ebusd +ExecStart=/usr/bin/ebusd ${EBUSD_OPTS} +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/contrib/html/openapi.yaml b/contrib/html/openapi.yaml index c0c52845b..89fdc178b 100644 --- a/contrib/html/openapi.yaml +++ b/contrib/html/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: ebusd-http description: The API that ebusd provides on HTTP port. - version: "22.4" + version: "25.1" servers: - url: http://127.0.0.1:8080/ paths: @@ -145,6 +145,50 @@ paths: 500: description: General error. content: { } + /templates: + get: + summary: Get all known field templates for the root. + responses: + 200: + description: Success. + content: + application/json;charset=utf-8: + schema: + $ref: '#/components/schemas/Templates' + 400: + description: Invalid request parameters. + content: { } + 403: + description: User not authorized. + content: { } + 500: + description: General error. + content: { } + /templates/{path}: + get: + summary: Get all known field templates for the path. + parameters: + - name: path + in: path + required: true + schema: + type: string + responses: + 200: + description: Success. + content: + application/json;charset=utf-8: + schema: + $ref: '#/components/schemas/Templates' + 400: + description: Invalid request parameters. + content: { } + 403: + description: User not authorized. + content: { } + 500: + description: General error. + content: { } /raw: get: summary: Retrieve raw data from grabbed and/or decoded messages. @@ -392,8 +436,12 @@ components: type: integer minimum: 0 condition: - type: string - description: the condition string in case of a conditional message (only with full). + description: the condition(s) in case of a conditional message (only with full). + oneOf: + - $ref: '#/components/schemas/Condition' + - type: array + items: + $ref: '#/components/schemas/Condition' lastup: $ref: '#/components/schemas/Seconds' description: the time in UTC seconds of the last update of the message (0 @@ -407,8 +455,12 @@ components: description: destination master or slave address. example: 8 id: - $ref: '#/components/schemas/Symbols' - description: the message ID composed of PBSB and further master data bytes (only with def). + description: the message ID composed of PBSB and further master data bytes (only with def), or an array thereof in case of a chained message. + oneOf: + - $ref: '#/components/schemas/Symbols' + - type: array + items: + $ref: '#/components/schemas/Symbols' comment: type: string description: the message comment (only with verbose). @@ -465,10 +517,10 @@ components: - type: string - type: number nullable: true - FieldDef: + FieldTemplate: + description: a single field template. required: - name - - slave - type - isbits - length @@ -477,9 +529,6 @@ components: name: type: string description: the field name. - slave: - type: boolean - description: whether the field is part of the slave data. type: type: string description: the field type. @@ -513,6 +562,66 @@ components: comment: type: string description: the field comment. + FieldDef: + description: a single field definition. + allOf: + - $ref: '#/components/schemas/FieldTemplate' + - type: object + required: + - slave + properties: + slave: + type: boolean + description: whether the field is part of the slave data. + Templates: + type: array + description: list of known field templates. + items: + oneOf: + - $ref: '#/components/schemas/FieldTemplate' + - type: object + required: + - name + - sequence + properties: + name: + type: string + description: the template set name. + sequence: + type: array + description: the sequence of fields. + items: + $ref: '#/components/schemas/FieldTemplate' + Condition: + description: a single condition. + required: + - name + - message + properties: + name: + type: string + description: name of the condition. + message: + type: string + description: name of the referenced message. + circuit: + type: string + description: name of the referenced circuit. + zz: + maximum: 255 + minimum: 0 + type: integer + description: the circuit slave address. + field: + type: string + description: the field name in the referenced message. + value: + type: array + description: the value ranges for the condition. + items: + oneOf: + - type: number + - type: string DataType: description: a known field data type. type: object @@ -555,6 +664,15 @@ components: precision: type: number description: the precision (number of fraction digits) when divisor is >1. + min: + type: number + description: the minimum allowed value. + max: + type: number + description: the maximum allowed value. + step: + type: number + description: the smallest step value. required: - type - isbits diff --git a/contrib/munin/ebusd_ b/contrib/munin/ebusd_ index e1e6388d8..b3e76595f 100755 --- a/contrib/munin/ebusd_ +++ b/contrib/munin/ebusd_ @@ -57,9 +57,9 @@ if [ -n "$find2" ]; then fi result="$result\n$result2" fi -sensors=`echo "$result"|sed -e 's# =.*$##' -e 's# #:#'|egrep "$match"` +sensors=`echo "$result"|sed -e 's# =.*$##' -e 's# #:#'|grep -E "$match"` if [ -n "$skip" ]; then - sensors=`echo "$sensors"|egrep -v "$skip"` + sensors=`echo "$sensors"|grep -Ev "$skip"` fi sensors=`echo "$sensors"|sort -u` if [ "$1" = "config" ]; then diff --git a/contrib/scripts/readall.sh b/contrib/scripts/readall.sh index 26d37e433..74da9ee6d 100755 --- a/contrib/scripts/readall.sh +++ b/contrib/scripts/readall.sh @@ -11,7 +11,7 @@ if [ "x$1" = "x-R" ]; then readargs=$1 shift fi -for i in `echo "f -F circuit,name" "$@"|nc -q 1 127.0.0.1 $port|sort -u|egrep ','`; do +for i in `echo "f -F circuit,name" "$@"|nc -q 1 127.0.0.1 $port|sort -u|grep ','`; do circuit=${i%%,*} name=${i##*,} if [ -z "$circuit" ] || [ -z "$name" ] || [ "$circuit,$name" = "scan,id" ]; then diff --git a/contrib/updatecheck/calcversions.sh b/contrib/updatecheck/calcversions.sh index 9893e59b0..3b0a646e1 100755 --- a/contrib/updatecheck/calcversions.sh +++ b/contrib/updatecheck/calcversions.sh @@ -1,8 +1,12 @@ #!/bin/sh version=`head -n 1 ../../VERSION` revision=`git describe --always` -echo "ebusd=${version},${revision}" > versions.txt -echo "ebusd=${version},${revision}" > oldversions.txt -files=`find config/ -type f -or -type l` -../../src/lib/ebus/test/test_filereader $files|sed -e 's#^config/##' -e 's#^\([^ ]*\) #\1=#' -e 's# #,#g'|sort >> versions.txt -#./oldtest_filereader $files|sed -e 's#^config/##' -e 's#^\([^ ]*\) #\1=#' -e 's# #,#g'|sort >> oldversions.txt +echo "ebusd=${version},${revision}" > cdnversions.txt +devver=`curl -s https://adapter.ebusd.eu/v31/firmware/ChangeLog|grep "Version "|head -n 1|sed -e 's#.*]*>##' -e 's#<.*##' -e 's# ##'` +devbl=`curl -s https://adapter.ebusd.eu/v31/firmware/ChangeLog|grep "Bootloader version"|head -n 1|sed -e 's#.*]*>##' -e 's#<.*##' -e 's# ##'` +echo "device=${devver},${devbl}" >> cdnversions.txt +devver=`curl -s https://adapter.ebusd.eu/v5/ChangeLog|grep "Version "|head -n 1|sed -e 's#.*id="\([^"]*\)".*]*>\([^<]*\)<.*#\2,\2,\1#'` +echo "device=${devver}" >> cdnversions.txt +curl -sS https://ebus.github.io/en/versions.json -o veren.json +curl -sS https://ebus.github.io/de/versions.json -o verde.json +node -e 'fs=require("fs");e=JSON.parse(fs.readFileSync("veren.json","utf-8"));d=JSON.parse(fs.readFileSync("verde.json","utf-8"));console.log(Object.entries(d).map(([k,de])=>{en=e[k];return `${k}=${de.hash},${de.size},${de.mtime},${en.hash},${en.size},${en.mtime}`;}).join("\n"))'|sort >> cdnversions.txt diff --git a/contrib/updatecheck/index.php b/contrib/updatecheck/index.php index 9a0d92c3f..d40cfa5f0 100644 --- a/contrib/updatecheck/index.php +++ b/contrib/updatecheck/index.php @@ -5,7 +5,7 @@ $r = @file_get_contents('php://input'); header('Content-Type: text/plain'); $r = @json_decode($r, true); - echo checkUpdate(@$r['v'], @$r['r'], @$r['a'], @$r['l']); + echo checkUpdate(@$r['v'], @$r['r'], @$r['a'], @$r['dv'], @$r['di'], @$r['l'], @$r['lc'], @$r['cp']); exit; } readVersions(); @@ -23,21 +23,29 @@ } ?>

latest ebusd version:

-

config files: +

latest device firmware: v5*=, v3*=

+

config files:"; global $ref; - $str = "$k: ".date('d.m.Y H:i:s', $v[2]); + $str = date('d.m.Y H:i:s', $v[2]); if ($ref) { - echo "
$str\n"; + echo "\n"; } else { - echo "
$str\n"; + echo "\n"; + } + $str = date('d.m.Y H:i:s', $v[5]); + if ($ref) { + echo "\n"; + } else { + echo "\n"; } }; array_walk($versions, $func); ?> -

- +
FileGermanEnglish
$k$str$str$str$str

+ \ No newline at end of file diff --git a/contrib/updatecheck/prepend.inc b/contrib/updatecheck/prepend.inc index 76c4492f8..1e4099471 100644 --- a/contrib/updatecheck/prepend.inc +++ b/contrib/updatecheck/prepend.inc @@ -1,26 +1,34 @@ 1) { - $versions[$val[0]] = explode(',', $val[1]); - } - }; - array_walk($v, $func); + $v = @file_get_contents($prefix.'versions.txt'); + if (!$v) { + return false; } + $v = explode("\n", $v); + $func = function($val, $k) { + global $versions; + $val = explode('=', $val); + if (count($val)>1) { + $key = $val[0]; + $val = explode(',', $val[1]); + if ($key==='device' && count($val)>=2 && $val[0]===$val[1]) { + $key = 'devicesame'; + } + $versions[$key] = $val; + } + }; + array_walk($v, $func); + return true; } -function checkUpdate($ebusdVersion, $ebusdRelease, $architecture, $loadedFiles) { +function checkUpdate($ebusdVersion, $ebusdRelease, $architecture, $deviceVersion, $deviceId, $loadedFiles, $language, $cdnPath) { if (!$ebusdVersion) { return 'invalid request'; } - readVersions($architecture && substr($architecture, 0, 3)!=='arm' && (((float)$ebusdVersion)<3.3 || ($ebusdVersion==='3.3' && ($ebusdRelease==='v3.3' || (strtok($ebusdRelease, '-')==='v3.3') && strtok('-')<18)))); + $old = $architecture && substr($architecture, 0, 3)!=='arm' && (((float)$ebusdVersion)<3.3 || ($ebusdVersion==='3.3' && ($ebusdRelease==='v3.3' || (strtok($ebusdRelease, '-')==='v3.3') && strtok('-')<18))); + readVersions($cdnPath ? 'cdn' : ($old ? 'old' : '')) || ($cdnPath && readVersions()); global $versions; $ret = 'unknown'; if ($ebusdVersion==$versions['ebusd'][0] && $ebusdRelease==$versions['ebusd'][1]) { @@ -30,13 +38,29 @@ function checkUpdate($ebusdVersion, $ebusdRelease, $architecture, $loadedFiles) } else { $ret = 'version '.$versions['ebusd'][0].' available'; } + if ($deviceVersion && @$versions['device']) { + $feat = strtok($deviceVersion, '.'); + $ver = strtok('.'); + if ($ver) { + $key = ($ver===strtok('.')) ? 'devicesame' : 'device'; + $devTypes = array('00'=>'v5', '01'=>'v5-c6', '00n'=>'v5', '01n'=>''); + $devType = $key==='devicesame' && strlen($deviceId)===9*2 && substr($deviceId,-2)==='00' ? $devTypes[substr($deviceId,-4,2).(substr($deviceId,-6,2)==='00'?'n':'')] : ''; + if ($devType) { + $devVer = $versions[$key][0]; + $ret .= ', device firmware '.$devType.' '.$devVer.($ver===$devVer ? ' up to date' : ' available'); + } else if ($ver!==$versions[$key][0]) { + $ret .= ', device firmware '.$versions[$key][0].' available'; + } + } + } $newerAvailable = 0; $configs = ''; + $offset = $language==='en' ? 3 : 0; foreach ($loadedFiles as $k => $val) { $v = $versions[$k]; if ($v) { - $newer = $v[2]>$val['t']; - if ($v[0]!=$val['h'] || $v[1]!=$val['s']) { + $newer = $v[2+$offset]>$val['t']; + if ($v[0+$offset]!=$val['h'] || $v[1+$offset]!=$val['s']) { if ($newer) { $newerAvailable++; } @@ -49,4 +73,4 @@ function checkUpdate($ebusdVersion, $ebusdRelease, $architecture, $loadedFiles) } return $ret.$configs; } -?> +?> \ No newline at end of file diff --git a/docs/enhanced_proto.md b/docs/enhanced_proto.md index 3ad225fa5..2869bbcbf 100644 --- a/docs/enhanced_proto.md +++ b/docs/enhanced_proto.md @@ -1,6 +1,6 @@ ## Transfer speed -In order to compensate potential overhead of transfer encoding, the transfer speed is set to 9600 Baud or 115200 Baud +In order to compensate potential overhead of transfer encoding, the transfer speed is set to 115200 Baud (or 9600 Baud) with 8 bits, no parity, and 1 stop bit. @@ -73,7 +73,7 @@ first second The data byte in `d` contains the error message. * host communication error ` ` - Indicates an error in the host UART. + Indicates an error in the host receiver/transmitter. The data byte in `d` contains the error message. @@ -106,7 +106,7 @@ These are the predefined symbols as used above. ### Feature bits (both directions) * bit 7-2: tbd * // planned: bit 1: full message sending (complete sequence instead of single bytes) - * bit 0: additional infos (version, PIC ID, etc.) + * bit 0: additional infos (version, HW ID, etc.) ### Information IDs (both directions) The first level below is the `info_id` value and the second level describes the response data byte sequence. @@ -114,28 +114,31 @@ The first byte transferred in response is always the number of data bytes to be * 0x00: version * `length`: =8 (2 before 20220220, 5 before 20220831) * `version`: version number - * `features`: feature bits + * `features`: feature bits (see above) * `checksum_H` `checksum_L`: checksum (since 20220220) - * `jumpers`: jumper settings + * `jumpers`: jumper settings (0x01=enhanced, 0x02=high speed, 0x04=Ethernet, 0x08=WIFI, 0x10=v3.1, 0x20=ignore hard jumpers) * `bootloader_version`: bootloader version (since 20220831) * `bootloader_checksum_H` `bootloader_checksum_L`: bootloader checksum - * 0x01: PIC ID + * 0x01: HW ID * `length`: =9 - * 9*`mui`: PIC MUI - * 0x02: PIC config + * 9*`hwid`: hardware identifier + * 0x02: HW config * `length`: =8 - * 8*`config_H` `config_L`: PIC config - * 0x03: PIC temperature - * `length`: =1 - * `temp`: temperature in degrees Celsius - * 0x04: PIC supply voltage + * `config_H` `config_L`: hardware config (chip specific) + * 0x03: HW temperature * `length`: =2 - * `millivolt_H` `millivolt_L`: voltage value in mV + * `temp_H` `temp_L`: hardware temperature in degrees Celsius + * 0x04: HW supply voltage + * `length`: =2 + * `millivolt_H` `millivolt_L`: supply voltage in mV, or 0 if unknown * 0x05: bus voltage * `length`: =2 - * `voltage_max`: maximum bus voltage in 10th volts - * `voltage_min`: minimum bus voltage in 10th volts + * `voltage_max`: maximum eBUS voltage in 10th volts, or 0 if unknown + * `voltage_min`: minimum eBUS voltage in 10th volts, or 0 if unknown * 0x06: reset info (since 20220831) * `length`: =2 * `reset_cause`: reset cause (1=power-on, 2=brown-out, 3=watchdog, 4=clear, 5=reset, 6=stack, 7=memory) * `restart_count`: restart count (within same power cycle) + * 0x07: WIFI status (since 20231226) + * `length`: =2 + * `rssi`: signal strength in dBm (rssi, usually negative), 0 if unknown diff --git a/make_debian.sh b/make_debian.sh index cb8d1f943..bbe00965b 100755 --- a/make_debian.sh +++ b/make_debian.sh @@ -1,6 +1,6 @@ #!/bin/sh # ebusd - daemon for communication with eBUS heating systems. -# Copyright (C) 2014-2022 John Baier +# Copyright (C) 2014-2025 John Baier # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -66,24 +66,24 @@ if [ -n "$reusebuilddir" ] || [ -z "$keepbuilddir" ]; then fi make DESTDIR="$PWD/$RELEASE" install-strip || exit 1 extralibs= -mqtt= -ldd $RELEASE/usr/bin/ebusd | egrep -q libmosquitto.so.0 +ldd $RELEASE/usr/bin/ebusd | grep -q libeibclient.so.0 if [ $? -eq 0 ]; then - extralibs=', libmosquitto0' - PACKAGE="${PACKAGE}_mqtt0" - mqtt=1 + extralibs="$extralibs, knxd-tools" +fi +ldd $RELEASE/usr/bin/ebusd | grep -q libmosquitto.so.1 +if [ $? -eq 0 ]; then + extralibs="$extralibs, libmosquitto1" + PACKAGE="${PACKAGE}_mqtt1" +fi +ldd $RELEASE/usr/bin/ebusd | grep -q libssl.so.3 +if [ $? -eq 0 ]; then + extralibs="$extralibs, libssl3 (>= 3.0.0), ca-certificates" else - ldd $RELEASE/usr/bin/ebusd | egrep -q libmosquitto.so.1 + ldd $RELEASE/usr/bin/ebusd | grep -q libssl.so.1.1 if [ $? -eq 0 ]; then - extralibs=', libmosquitto1' - PACKAGE="${PACKAGE}_mqtt1" - mqtt=1 + extralibs="$extralibs, libssl1.1 (>= 1.1.0), ca-certificates" fi fi -ldd $RELEASE/usr/bin/ebusd | egrep -q libssl.so.1.1 -if [ $? -eq 0 ]; then - extralibs="$extralibs, libssl1.1 (>= 1.1.0), ca-certificates" -fi if [ -n "$RUNTEST" ]; then echo @@ -103,8 +103,8 @@ if [ -n "$RUNTEST" ]; then (cd src/lib/ebus/test && make test_symbol && ./test_symbol) > test.txt || testdie "symbol" fi # note: this can't run on file system base when using qemu for arm 32bit and host is 64bit due to glibc readdir() inode 32 bit values (see https://bugs.launchpad.net/qemu/+bug/1805913), thus using test end point instead: - ("$RELEASE/usr/bin/ebusd" -f -s -c https://cfg.ebusd.eu/test -d /dev/null --log=all:debug --inject=stop 10fe0900040000803e/ > test.txt) || testdie "float conversion" - egrep "received update-read broadcast test QQ=10: 0\.25$" test.txt || testdie "float result" + ("$RELEASE/usr/bin/ebusd" -f -s --configlang=tt -d /dev/null --log=all:debug --inject=stop 10fe0900040000803e/ > test.txt) || testdie "float conversion" + grep -E "received update-read broadcast test QQ=10: 0\.25$" test.txt || testdie "float result" fi echo diff --git a/src/ebusd/CMakeLists.txt b/src/ebusd/CMakeLists.txt index 325663c23..e8e56e47e 100644 --- a/src/ebusd/CMakeLists.txt +++ b/src/ebusd/CMakeLists.txt @@ -3,13 +3,16 @@ add_definitions(-Wconversion -Wno-unused-parameter) set(ebusd_SOURCES bushandler.h bushandler.cpp datahandler.h datahandler.cpp + request.h request.cpp network.h network.cpp mainloop.h mainloop.cpp - main.h main.cpp + scan.h scan.cpp + main.h main.cpp main_args.cpp ) if(HAVE_MQTT) - set(ebusd_SOURCES ${ebusd_SOURCES} mqtthandler.cpp mqtthandler.h) + set(ebusd_SOURCES ${ebusd_SOURCES} mqtthandler.cpp mqtthandler.h mqttclient.cpp mqttclient.h) + set(ebusd_SOURCES ${ebusd_SOURCES} mqttclient_mosquitto.cpp mqttclient_mosquitto.h) set(ebusd_LIBS ${ebusd_LIBS} mosquitto) endif(HAVE_MQTT) @@ -31,7 +34,7 @@ include_directories(../lib/ebus) include_directories(../lib/utils) add_executable(ebusd ${ebusd_SOURCES}) -target_link_libraries(ebusd utils ebus pthread rt ${LIB_ARGP} ${ebusd_LIBS}) +target_link_libraries(ebusd utils ebus pthread rt ${ebusd_LIBS}) -install(TARGETS ebusd EXPORT ebusd DESTINATION usr/bin) +install(TARGETS ebusd EXPORT ebusd DESTINATION bin) diff --git a/src/ebusd/Makefile.am b/src/ebusd/Makefile.am index eb6307168..473c766f6 100644 --- a/src/ebusd/Makefile.am +++ b/src/ebusd/Makefile.am @@ -1,27 +1,30 @@ AM_CXXFLAGS = -I$(top_srcdir)/src \ -isystem$(top_srcdir) \ - -Wconversion -Wno-unused-parameter \ - -DSYSCONFDIR=\"$(sysconfdir)\" \ - -DLOCALSTATEDIR=\"$(localstatedir)\" + -Wconversion -Wno-unused-parameter bin_PROGRAMS = ebusd ebusd_SOURCES = \ bushandler.h bushandler.cpp \ datahandler.h datahandler.cpp \ + request.h request.cpp \ network.h network.cpp \ mainloop.h mainloop.cpp \ - main.h main.cpp + scan.h scan.cpp \ + main.h main.cpp main_args.cpp -if MQTT -ebusd_SOURCES += mqtthandler.cpp mqtthandler.h -endif -ebusd_LDADD = ../lib/utils/libutils.a \ - ../lib/ebus/libebus.a \ +ebusd_LDADD = ../lib/ebus/libebus.a \ + ../lib/utils/libutils.a \ -lpthread \ @EXTRA_LIBS@ +if MQTT +ebusd_SOURCES += mqtthandler.cpp mqtthandler.h mqttclient.cpp mqttclient.h +ebusd_SOURCES += mqttclient_mosquitto.cpp mqttclient_mosquitto.h +ebusd_LDADD += -lmosquitto +endif + if KNX ebusd_SOURCES += knxhandler.cpp knxhandler.h ebusd_LDADD += ../lib/knx/libknx.a diff --git a/src/ebusd/bushandler.cpp b/src/ebusd/bushandler.cpp index 47bb5547e..a24595125 100644 --- a/src/ebusd/bushandler.cpp +++ b/src/ebusd/bushandler.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +22,6 @@ #include "ebusd/bushandler.h" #include -#include "ebusd/main.h" #include "lib/utils/log.h" namespace ebusd { @@ -33,35 +32,6 @@ using std::setfill; using std::setw; using std::endl; -// the string used for answering to a scan request (07h 04h) -#define SCAN_ANSWER ("ebusd.eu;" PACKAGE_NAME ";" SCAN_VERSION ";100") - -/** - * Return the string corresponding to the @a BusState. - * @param state the @a BusState. - * @return the string corresponding to the @a BusState. - */ -const char* getStateCode(BusState state) { - switch (state) { - case bs_noSignal: return "no signal"; - case bs_skip: return "skip"; - case bs_ready: return "ready"; - case bs_sendCmd: return "send command"; - case bs_recvCmdCrc: return "receive command CRC"; - case bs_recvCmdAck: return "receive command ACK"; - case bs_recvRes: return "receive response"; - case bs_recvResCrc: return "receive response CRC"; - case bs_sendResAck: return "send response ACK"; - case bs_recvCmd: return "receive command"; - case bs_recvResAck: return "receive response ACK"; - case bs_sendCmdCrc: return "send command CRC"; - case bs_sendCmdAck: return "send command ACK"; - case bs_sendRes: return "send response"; - case bs_sendResCrc: return "send response CRC"; - case bs_sendSyn: return "send SYN"; - default: return "unknown"; - } -} result_t PollRequest::prepare(symbol_t ownMasterAddress) { istringstream input; @@ -131,7 +101,7 @@ bool ScanRequest::notify(result_t result, const SlaveSymbolString& slave) { } if (result == RESULT_OK) { ostringstream output; - result = m_message->decodeLastData(true, nullptr, -1, OF_NONE, &output); // decode data + result = m_message->decodeLastData(pt_any, true, nullptr, -1, OF_NONE, &output); // decode data string str = output.str(); m_busHandler->setScanResult(dstAddress, m_notifyIndex+m_index, str); } @@ -157,7 +127,7 @@ bool ScanRequest::notify(result_t result, const SlaveSymbolString& slave) { } } m_result = result; - if (m_slaves.empty()) { + if (m_slaves.empty() || result == RESULT_ERR_NO_SIGNAL) { if (m_deleteOnFinish) { logNotice(lf_bus, "scan finished"); } @@ -182,17 +152,6 @@ bool ScanRequest::notify(result_t result, const SlaveSymbolString& slave) { } -bool ActiveBusRequest::notify(result_t result, const SlaveSymbolString& slave) { - if (result == RESULT_OK) { - string str = slave.getStr(); - logDebug(lf_bus, "read res: %s", str.c_str()); - } - m_result = result; - *m_slave = slave; - return false; -} - - void GrabbedMessage::setLastData(const MasterSymbolString& master, const SlaveSymbolString& slave) { time(&m_lastTime); m_lastMaster = master; @@ -232,7 +191,7 @@ bool decodeType(const DataType* type, const SymbolString& input, size_t length, first = false; *output << endl << " "; ostringstream::pos_type cnt = output->tellp(); - type->dump(OF_NONE, length, false, output); + type->dump(OF_NONE, length, ad_none, output); cnt = output->tellp() - cnt; while (cnt < 5) { *output << " "; @@ -356,37 +315,14 @@ bool GrabbedMessage::dump(bool unknown, MessageMap* messages, bool first, Output void BusHandler::clear() { - memset(m_seenAddresses, 0, sizeof(m_seenAddresses)); - m_masterCount = 1; + m_protocol->clear(); m_scanResults.clear(); -} - -result_t BusHandler::sendAndWait(const MasterSymbolString& master, SlaveSymbolString* slave) { - result_t result = RESULT_ERR_NO_SIGNAL; - slave->clear(); - ActiveBusRequest request(master, slave); - logInfo(lf_bus, "send message: %s", master.getStr().c_str()); - - for (int sendRetries = m_failedSendRetries + 1; sendRetries > 0; sendRetries--) { - m_nextRequests.push(&request); - bool success = m_finishedRequests.remove(&request, true); - result = success ? request.m_result : RESULT_ERR_TIMEOUT; - if (result == RESULT_OK) { - break; - } - if (!success || result == RESULT_ERR_NO_SIGNAL || result == RESULT_ERR_SEND || result == RESULT_ERR_DEVICE) { - logError(lf_bus, "send to %2.2x: %s, give up", master[1], getResultCode(result)); - break; - } - logError(lf_bus, "send to %2.2x: %s%s", master[1], getResultCode(result), sendRetries > 1 ? ", retry" : ""); - request.m_busLostRetries = 0; - } - return result; + memset(m_seenAddresses, 0, sizeof(m_seenAddresses)); } result_t BusHandler::readFromBus(Message* message, const string& inputStr, symbol_t dstAddress, symbol_t srcAddress) { - symbol_t masterAddress = srcAddress == SYN ? m_ownMasterAddress : srcAddress; + symbol_t masterAddress = srcAddress == SYN ? m_protocol->getOwnMasterAddress() : srcAddress; result_t ret = RESULT_EMPTY; MasterSymbolString master; SlaveSymbolString slave; @@ -398,7 +334,7 @@ result_t BusHandler::readFromBus(Message* message, const string& inputStr, symbo break; } // send message - ret = sendAndWait(master, &slave); + ret = m_protocol->sendAndWait(master, &slave); if (ret != RESULT_OK) { logError(lf_bus, "send message part %d: %s", index, getResultCode(ret)); break; @@ -412,804 +348,63 @@ result_t BusHandler::readFromBus(Message* message, const string& inputStr, symbo return ret; } -void BusHandler::run() { - unsigned int symCount = 0; - time_t now, lastTime; - time(&lastTime); - lastTime += 2; - logNotice(lf_bus, "bus started with own address %2.2x/%2.2x%s", m_ownMasterAddress, m_ownSlaveAddress, - m_answer?" in answer mode":""); - - do { - if (m_device->isValid() && !m_reconnect) { - result_t result = handleSymbol(); - time(&now); - if (result != RESULT_ERR_TIMEOUT && now >= lastTime) { - symCount++; - } - if (now > lastTime) { - m_symPerSec = symCount / (unsigned int)(now-lastTime); - if (m_symPerSec > m_maxSymPerSec) { - m_maxSymPerSec = m_symPerSec; - if (m_maxSymPerSec > 100) { - logNotice(lf_bus, "max. symbols per second: %d", m_maxSymPerSec); - } - } - lastTime = now; - symCount = 0; - } - } else { - if (!m_device->isValid()) { - logNotice(lf_bus, "device invalid"); - } - if (!Wait(5)) { - break; - } - m_reconnect = false; - result_t result = m_device->open(); - if (result == RESULT_OK) { - logNotice(lf_bus, "re-opened %s", m_device->getName()); - } else { - logError(lf_bus, "unable to open %s: %s", m_device->getName(), getResultCode(result)); - setState(bs_noSignal, result); - } - symCount = 0; - m_symbolLatencyMin = m_symbolLatencyMax = m_arbitrationDelayMin = m_arbitrationDelayMax = -1; - time(&lastTime); - lastTime += 2; - } - } while (isRunning()); -} - -#ifndef FALLTHROUGH -#if defined(__GNUC__) && __GNUC__ >= 7 -#define FALLTHROUGH [[fallthrough]]; -#else -#define FALLTHROUGH -#endif -#endif - -result_t BusHandler::handleSymbol() { - unsigned int timeout = SYN_TIMEOUT; - symbol_t sendSymbol = ESC; - bool sending = false; - - // check if another symbol has to be sent and determine timeout for receive - switch (m_state) { - case bs_noSignal: - timeout = m_generateSynInterval > 0 ? m_generateSynInterval : SIGNAL_TIMEOUT; - break; - - case bs_skip: - timeout = SYN_TIMEOUT; - FALLTHROUGH - case bs_ready: - if (m_currentRequest != nullptr) { - setState(bs_ready, RESULT_ERR_TIMEOUT); // just to be sure an old BusRequest is cleaned up - } - if (!m_device->isArbitrating() && m_currentRequest == nullptr && m_remainLockCount == 0) { - BusRequest* startRequest = m_nextRequests.peek(); - if (startRequest == nullptr && m_pollInterval > 0) { // check for poll/scan - time_t now; - time(&now); - if (m_lastPoll == 0 || difftime(now, m_lastPoll) > m_pollInterval) { - Message* message = m_messages->getNextPoll(); - if (message != nullptr) { - m_lastPoll = now; - if (difftime(now, message->getLastUpdateTime()) > m_pollInterval) { - // only poll this message if it was not updated already by other means within the interval - auto request = new PollRequest(message); - result_t ret = request->prepare(m_ownMasterAddress); - if (ret != RESULT_OK) { - logError(lf_bus, "prepare poll message: %s", getResultCode(ret)); - delete request; - } else { - startRequest = request; - m_nextRequests.push(request); - } - } - } - } - } - if (startRequest != nullptr) { // initiate arbitration - logDebug(lf_bus, "start request %2.2x", startRequest->m_master[0]); - result_t ret = m_device->startArbitration(startRequest->m_master[0]); - if (ret == RESULT_OK) { - logDebug(lf_bus, "arbitration start with %2.2x", startRequest->m_master[0]); - } else { - logError(lf_bus, "arbitration start: %s", getResultCode(ret)); - m_nextRequests.remove(startRequest); - m_currentRequest = startRequest; - setState(bs_ready, ret); // force the failed request to be notified - } - } - } - break; - - case bs_recvCmd: - case bs_recvCmdCrc: - timeout = m_slaveRecvTimeout; - break; - - case bs_recvCmdAck: - timeout = m_slaveRecvTimeout; - break; - - case bs_recvRes: - case bs_recvResCrc: - if (m_response.size() > 0 || m_slaveRecvTimeout > SYN_TIMEOUT) { - timeout = m_slaveRecvTimeout; - } else { - timeout = SYN_TIMEOUT; - } - break; - - case bs_recvResAck: - timeout = m_slaveRecvTimeout; - break; - - case bs_sendCmd: - if (m_currentRequest != nullptr) { - sendSymbol = m_currentRequest->m_master[m_nextSendPos]; // unescaped command - sending = true; - } - break; - - case bs_sendCmdCrc: - if (m_currentRequest != nullptr) { - sendSymbol = m_crc; - sending = true; - } - break; - - case bs_sendResAck: - if (m_currentRequest != nullptr) { - sendSymbol = m_crcValid ? ACK : NAK; - sending = true; - } - break; - - case bs_sendCmdAck: - if (m_answer) { - sendSymbol = m_crcValid ? ACK : NAK; - sending = true; - } - break; - - case bs_sendRes: - if (m_answer) { - sendSymbol = m_response[m_nextSendPos]; // unescaped response - sending = true; - } - break; - - case bs_sendResCrc: - if (m_answer) { - sendSymbol = m_crc; - sending = true; - } - break; - - case bs_sendSyn: - sendSymbol = SYN; - sending = true; - break; - } - - // send symbol if necessary - result_t result; - struct timespec sentTime, recvTime; - if (sending) { - if (m_state != bs_sendSyn && (sendSymbol == ESC || sendSymbol == SYN)) { - if (m_escape) { - sendSymbol = (symbol_t)(sendSymbol == ESC ? 0x00 : 0x01); - } else { - m_escape = sendSymbol; - sendSymbol = ESC; - } - } - result = m_device->send(sendSymbol); - clockGettime(&sentTime); - if (result == RESULT_OK) { - if (m_state == bs_ready) { - timeout = m_busAcquireTimeout; - } else { - timeout = SEND_TIMEOUT; - } - } else { - sending = false; - timeout = SYN_TIMEOUT; - setState(bs_skip, result); - } - } else { - clockGettime(&sentTime); // for measuring arbitration delay in enhanced protocol - } - - // receive next symbol (optionally check reception of sent symbol) - symbol_t recvSymbol; - ArbitrationState arbitrationState = as_none; - result = m_device->recv(timeout, &recvSymbol, &arbitrationState); - if (sending) { - clockGettime(&recvTime); - } - bool sentAutoSyn = false; - if (!sending && result == RESULT_ERR_TIMEOUT && m_generateSynInterval > 0 - && timeout >= m_generateSynInterval && (m_state == bs_noSignal || m_state == bs_skip)) { - // check if acting as AUTO-SYN generator is required - result = m_device->send(SYN); - if (result != RESULT_OK) { - return setState(bs_skip, result); - } - clockGettime(&sentTime); - recvSymbol = ESC; - result = m_device->recv(SEND_TIMEOUT, &recvSymbol, &arbitrationState); - clockGettime(&recvTime); - if (result != RESULT_OK) { - logError(lf_bus, "unable to receive sent AUTO-SYN symbol: %s", getResultCode(result)); - return setState(bs_noSignal, result); - } - if (recvSymbol != SYN) { - logError(lf_bus, "received %2.2x instead of AUTO-SYN symbol", recvSymbol); - return setState(bs_noSignal, result); - } - measureLatency(&sentTime, &recvTime); - if (m_generateSynInterval != SYN_TIMEOUT) { - // received own AUTO-SYN symbol back again: act as AUTO-SYN generator now - m_generateSynInterval = SYN_TIMEOUT; - logNotice(lf_bus, "acting as AUTO-SYN generator"); - } - m_remainLockCount = 0; - m_lastSynReceiveTime = recvTime; - sentAutoSyn = true; - setState(bs_ready, RESULT_OK); - } - switch (arbitrationState) { - case as_lost: - case as_timeout: - logDebug(lf_bus, arbitrationState == as_lost ? "arbitration lost" : "arbitration lost (timed out)"); - if (m_currentRequest == nullptr) { - BusRequest *startRequest = m_nextRequests.peek(); - if (startRequest != nullptr && m_nextRequests.remove(startRequest)) { - m_currentRequest = startRequest; // force the failed request to be notified - } - } - setState(m_state, RESULT_ERR_BUS_LOST); - break; - case as_won: // implies RESULT_OK - if (m_currentRequest != nullptr) { - logNotice(lf_bus, "arbitration won while handling another request"); - setState(bs_ready, RESULT_OK); // force the current request to be notified - } else { - BusRequest *startRequest = m_nextRequests.peek(); - if (m_state != bs_ready || startRequest == nullptr || !m_nextRequests.remove(startRequest)) { - logNotice(lf_bus, "arbitration won in invalid state %s", getStateCode(m_state)); - setState(bs_ready, RESULT_ERR_TIMEOUT); - } else { - logDebug(lf_bus, "arbitration won"); - m_currentRequest = startRequest; - sendSymbol = m_currentRequest->m_master[0]; - sending = true; - } - } - break; - case as_running: - break; - case as_error: - logError(lf_bus, "arbitration start error"); - // cancel request - if (!m_currentRequest) { - BusRequest *startRequest = m_nextRequests.peek(); - if (startRequest && m_nextRequests.remove(startRequest)) { - m_currentRequest = startRequest; - } - } - if (m_currentRequest) { - setState(m_state, RESULT_ERR_BUS_LOST); - } - break; - default: // only as_none - break; - } - if (sentAutoSyn && !sending) { - return RESULT_OK; - } - time_t now; - time(&now); - if (result != RESULT_OK) { - if ((m_generateSynInterval != SYN_TIMEOUT && difftime(now, m_lastReceive) > 1) - // at least one full second has passed since last received symbol - || m_state == bs_noSignal) { - return setState(bs_noSignal, result); - } - return setState(bs_skip, result); - } - - m_lastReceive = now; - if ((recvSymbol == SYN) && (m_state != bs_sendSyn)) { - if (!sending && m_remainLockCount > 0 && m_command.size() != 1) { - m_remainLockCount--; - } else if (!sending && m_remainLockCount == 0 && m_command.size() == 1) { - m_remainLockCount = 1; // wait for next AUTO-SYN after SYN / address / SYN (bus locked for own priority) - } - clockGettime(&m_lastSynReceiveTime); - return setState(bs_ready, m_state == bs_skip ? RESULT_OK : RESULT_ERR_SYN); - } - - if (sending && m_state != bs_ready) { // check received symbol for equality if not in arbitration - if (recvSymbol != sendSymbol) { - return setState(bs_skip, RESULT_ERR_SYMBOL); - } - measureLatency(&sentTime, &recvTime); - } - - switch (m_state) { - case bs_ready: - case bs_recvCmd: - case bs_recvRes: - case bs_sendCmd: - case bs_sendRes: - SymbolString::updateCrc(recvSymbol, &m_crc); - break; - default: - break; - } - - if (m_escape) { - // check escape/unescape state - if (sending) { - if (sendSymbol == ESC) { - return RESULT_OK; - } - sendSymbol = recvSymbol = m_escape; - } else { - if (recvSymbol > 0x01) { - return setState(bs_skip, RESULT_ERR_ESC); - } - recvSymbol = recvSymbol == 0x00 ? ESC : SYN; - } - m_escape = 0; - } else if (!sending && recvSymbol == ESC) { - m_escape = ESC; - return RESULT_OK; - } - - switch (m_state) { - case bs_noSignal: - return setState(bs_skip, RESULT_OK); - - case bs_skip: - return RESULT_OK; - - case bs_ready: - if (m_currentRequest != nullptr && sending) { - // check arbitration - if (recvSymbol == sendSymbol) { // arbitration successful - // measure arbitration delay - int64_t latencyLong = (sentTime.tv_sec*1000000000 + sentTime.tv_nsec - - m_lastSynReceiveTime.tv_sec*1000000000 - m_lastSynReceiveTime.tv_nsec)/1000; - if (latencyLong >= 0 && latencyLong <= 10000) { // skip clock skew or out of reasonable range - auto latency = static_cast(latencyLong); - logDebug(lf_bus, "arbitration delay %d micros", latency); - if (m_arbitrationDelayMin < 0 || (latency < m_arbitrationDelayMin || latency > m_arbitrationDelayMax)) { - if (m_arbitrationDelayMin == -1 || latency < m_arbitrationDelayMin) { - m_arbitrationDelayMin = latency; - } - if (m_arbitrationDelayMax == -1 || latency > m_arbitrationDelayMax) { - m_arbitrationDelayMax = latency; +void BusHandler::notifyProtocolStatus(ProtocolState state, result_t result) { + if (state == ps_empty && m_pollInterval > 0) { // check for poll/scan + time_t now; + time(&now); + if (m_lastPoll == 0 || difftime(now, m_lastPoll) > m_pollInterval) { + Message* message = m_messages->getNextPoll(); + if (message != nullptr) { + m_lastPoll = now; + if (difftime(now, message->getLastUpdateTime()) > m_pollInterval) { + // only poll this message if it was not updated already by other means within the interval + auto request = new PollRequest(message); + result_t ret = request->prepare(m_protocol->getOwnMasterAddress()); + if (ret != RESULT_OK) { + logError(lf_bus, "prepare poll message: %s", getResultCode(ret)); + delete request; + } else { + ret = m_protocol->addRequest(request, false); + if (ret != RESULT_OK) { + logError(lf_bus, "push poll message: %s", getResultCode(ret)); + delete request; } - logInfo(lf_bus, "arbitration delay %d - %d micros", m_arbitrationDelayMin, m_arbitrationDelayMax); } } - m_nextSendPos = 1; - m_repeat = false; - return setState(bs_sendCmd, RESULT_OK); - } - // arbitration lost. if same priority class found, try again after next AUTO-SYN - m_remainLockCount = isMaster(recvSymbol) ? 2 : 1; // number of SYN to wait for before next send try - if ((recvSymbol & 0x0f) != (sendSymbol & 0x0f) && m_lockCount > m_remainLockCount) { - // if different priority class found, try again after N AUTO-SYN symbols (at least next AUTO-SYN) - m_remainLockCount = m_lockCount; - } - setState(m_state, RESULT_ERR_BUS_LOST); // try again later - } - m_command.push_back(recvSymbol); - m_repeat = false; - return setState(bs_recvCmd, RESULT_OK); - - case bs_recvCmd: - m_command.push_back(recvSymbol); - if (m_command.isComplete()) { // all data received - return setState(bs_recvCmdCrc, RESULT_OK); - } - return RESULT_OK; - - case bs_recvCmdCrc: - m_crcValid = recvSymbol == m_crc; - if (m_command[1] == BROADCAST) { - if (m_crcValid) { - addSeenAddress(m_command[0]); - messageCompleted(); - return setState(bs_skip, RESULT_OK); - } - return setState(bs_skip, RESULT_ERR_CRC); - } - if (m_answer) { - symbol_t dstAddress = m_command[1]; - if (dstAddress == m_ownMasterAddress || dstAddress == m_ownSlaveAddress) { - if (m_crcValid) { - addSeenAddress(m_command[0]); - m_currentAnswering = true; - return setState(bs_sendCmdAck, RESULT_OK); - } - return setState(bs_sendCmdAck, RESULT_ERR_CRC); - } - } - if (m_crcValid) { - addSeenAddress(m_command[0]); - return setState(bs_recvCmdAck, RESULT_OK); - } - if (m_repeat) { - return setState(bs_skip, RESULT_ERR_CRC); - } - return setState(bs_recvCmdAck, RESULT_ERR_CRC); - - case bs_recvCmdAck: - if (recvSymbol == ACK) { - if (!m_crcValid) { - return setState(bs_skip, RESULT_ERR_ACK); - } - if (m_currentRequest != nullptr) { - if (isMaster(m_currentRequest->m_master[1])) { - messageCompleted(); - return setState(bs_sendSyn, RESULT_OK); - } - } else if (isMaster(m_command[1])) { - messageCompleted(); - return setState(bs_skip, RESULT_OK); - } - - m_repeat = false; - return setState(bs_recvRes, RESULT_OK); - } - if (recvSymbol == NAK) { - if (!m_repeat) { - m_repeat = true; - m_crc = 0; - m_nextSendPos = 0; - m_command.clear(); - if (m_currentRequest != nullptr) { - return setState(bs_sendCmd, RESULT_ERR_NAK, true); - } - return setState(bs_recvCmd, RESULT_ERR_NAK); - } - return setState(bs_skip, RESULT_ERR_NAK); - } - return setState(bs_skip, RESULT_ERR_ACK); - - case bs_recvRes: - m_response.push_back(recvSymbol); - if (m_response.isComplete()) { // all data received - return setState(bs_recvResCrc, RESULT_OK); - } - return RESULT_OK; - - case bs_recvResCrc: - m_crcValid = recvSymbol == m_crc; - if (m_crcValid) { - if (m_currentRequest != nullptr) { - return setState(bs_sendResAck, RESULT_OK); - } - return setState(bs_recvResAck, RESULT_OK); - } - if (m_repeat) { - if (m_currentRequest != nullptr) { - return setState(bs_sendSyn, RESULT_ERR_CRC); - } - return setState(bs_skip, RESULT_ERR_CRC); - } - if (m_currentRequest != nullptr) { - return setState(bs_sendResAck, RESULT_ERR_CRC); - } - return setState(bs_recvResAck, RESULT_ERR_CRC); - - case bs_recvResAck: - if (recvSymbol == ACK) { - if (!m_crcValid) { - return setState(bs_skip, RESULT_ERR_ACK); - } - messageCompleted(); - return setState(bs_skip, RESULT_OK); - } - if (recvSymbol == NAK) { - if (!m_repeat) { - m_repeat = true; - if (m_currentAnswering) { - m_nextSendPos = 0; - return setState(bs_sendRes, RESULT_ERR_NAK, true); - } - m_response.clear(); - return setState(bs_recvRes, RESULT_ERR_NAK, true); - } - return setState(bs_skip, RESULT_ERR_NAK); - } - return setState(bs_skip, RESULT_ERR_ACK); - - case bs_sendCmd: - if (!sending || m_currentRequest == nullptr) { - return setState(bs_skip, RESULT_ERR_INVALID_ARG); - } - m_nextSendPos++; - if (m_nextSendPos >= m_currentRequest->m_master.size()) { - return setState(bs_sendCmdCrc, RESULT_OK); - } - return RESULT_OK; - - case bs_sendCmdCrc: - if (m_currentRequest->m_master[1] == BROADCAST) { - messageCompleted(); - return setState(bs_sendSyn, RESULT_OK); - } - m_crcValid = true; - return setState(bs_recvCmdAck, RESULT_OK); - - case bs_sendResAck: - if (!sending || m_currentRequest == nullptr) { - return setState(bs_skip, RESULT_ERR_INVALID_ARG); - } - if (!m_crcValid) { - if (!m_repeat) { - m_repeat = true; - m_response.clear(); - return setState(bs_recvRes, RESULT_ERR_NAK, true); } - return setState(bs_sendSyn, RESULT_ERR_ACK); } - messageCompleted(); - return setState(bs_sendSyn, RESULT_OK); - - case bs_sendCmdAck: - if (!sending || !m_answer) { - return setState(bs_skip, RESULT_ERR_INVALID_ARG); - } - if (!m_crcValid) { - if (!m_repeat) { - m_repeat = true; - m_crc = 0; - m_command.clear(); - return setState(bs_recvCmd, RESULT_ERR_NAK, true); - } - return setState(bs_skip, RESULT_ERR_ACK); - } - if (isMaster(m_command[1])) { - messageCompleted(); // TODO decode command and store value into database of internal variables - return setState(bs_skip, RESULT_OK); - } - - m_nextSendPos = 0; - m_repeat = false; - { - Message* message; - message = m_messages->find(m_command); - if (message == nullptr) { - message = m_messages->find(m_command, true); - if (message != nullptr && message->getSrcAddress() != SYN) { - message = nullptr; - } - } - if (message == nullptr || message->isWrite()) { - // don't know this request or definition has wrong direction, deny - return setState(bs_skip, RESULT_ERR_INVALID_ARG); - } - istringstream input; // TODO create input from database of internal variables - if (message == m_messages->getScanMessage() || message == m_messages->getScanMessage(m_ownSlaveAddress)) { - input.str(SCAN_ANSWER); - } - // build response and store in m_response for sending back to requesting master - m_response.clear(); - result = message->prepareSlave(&input, &m_response); - if (result != RESULT_OK) { - return setState(bs_skip, result); - } - } - return setState(bs_sendRes, RESULT_OK); - - case bs_sendRes: - if (!sending || !m_answer) { - return setState(bs_skip, RESULT_ERR_INVALID_ARG); - } - m_nextSendPos++; - if (m_nextSendPos >= m_response.size()) { - // slave data completely sent - return setState(bs_sendResCrc, RESULT_OK); - } - return RESULT_OK; - - case bs_sendResCrc: - if (!sending || !m_answer) { - return setState(bs_skip, RESULT_ERR_INVALID_ARG); - } - return setState(bs_recvResAck, RESULT_OK); - - case bs_sendSyn: - if (!sending) { - return setState(bs_skip, RESULT_ERR_INVALID_ARG); - } - return setState(bs_skip, RESULT_OK); } - return RESULT_OK; } -result_t BusHandler::setState(BusState state, result_t result, bool firstRepetition) { - if (m_currentRequest != nullptr) { - if (result == RESULT_ERR_BUS_LOST && m_currentRequest->m_busLostRetries < m_busLostRetries) { - logDebug(lf_bus, "%s during %s, retry", getResultCode(result), getStateCode(m_state)); - m_currentRequest->m_busLostRetries++; - m_nextRequests.push(m_currentRequest); // repeat - m_currentRequest = nullptr; - } else if (state == bs_sendSyn || (result != RESULT_OK && !firstRepetition)) { - logDebug(lf_bus, "notify request: %s", getResultCode(result)); - bool restart = m_currentRequest->notify( - result == RESULT_ERR_SYN && (m_state == bs_recvCmdAck || m_state == bs_recvRes) - ? RESULT_ERR_TIMEOUT : result, m_response); - if (restart) { - m_currentRequest->m_busLostRetries = 0; - m_nextRequests.push(m_currentRequest); - } else if (m_currentRequest->m_deleteOnFinish) { - delete m_currentRequest; - } else { - m_finishedRequests.push(m_currentRequest); - } - m_currentRequest = nullptr; - } - if (state == bs_skip) { - m_device->startArbitration(SYN); // reset arbitration state - } - } - - if (state == bs_noSignal) { // notify all requests - m_response.clear(); // notify with empty response - while ((m_currentRequest = m_nextRequests.pop()) != nullptr) { - bool restart = m_currentRequest->notify(RESULT_ERR_NO_SIGNAL, m_response); - if (restart) { // should not occur with no signal - m_currentRequest->m_busLostRetries = 0; - m_nextRequests.push(m_currentRequest); - } else if (m_currentRequest->m_deleteOnFinish) { - delete m_currentRequest; - } else { - m_finishedRequests.push(m_currentRequest); - } - } - } - - m_escape = 0; - if (state == m_state) { - return result; - } - if ((result < RESULT_OK && !(result == RESULT_ERR_TIMEOUT && state == bs_skip && m_state == bs_ready)) - || (result != RESULT_OK && state == bs_skip && m_state != bs_ready)) { - logDebug(lf_bus, "%s during %s, switching to %s", getResultCode(result), getStateCode(m_state), - getStateCode(state)); - } else if (m_currentRequest != nullptr || state == bs_sendCmd || state == bs_sendCmdCrc || state == bs_sendCmdAck - || state == bs_sendRes || state == bs_sendResCrc || state == bs_sendResAck || state == bs_sendSyn) { - logDebug(lf_bus, "switching from %s to %s", getStateCode(m_state), getStateCode(state)); - } - if (state == bs_noSignal) { - if (m_generateSynInterval == 0 || m_state != bs_skip) { - logError(lf_bus, "signal lost"); - } - } else if (m_state == bs_noSignal) { - if (m_generateSynInterval == 0 || state != bs_skip) { - logNotice(lf_bus, "signal acquired"); - } - } - m_state = state; - - if (state == bs_ready || state == bs_skip) { - m_command.clear(); - m_crc = 0; - m_crcValid = false; - m_response.clear(); - m_nextSendPos = 0; - m_currentAnswering = false; - } else if (state == bs_recvRes || state == bs_sendRes) { - m_crc = 0; - } - return result; +void BusHandler::notifyProtocolSeenAddress(symbol_t address) { + m_seenAddresses[address] |= SEEN; } -void BusHandler::measureLatency(struct timespec* sentTime, struct timespec* recvTime) { - int64_t latencyLong = (recvTime->tv_sec*1000000000 + recvTime->tv_nsec - - sentTime->tv_sec*1000000000 - sentTime->tv_nsec)/1000000; - if (latencyLong < 0 || latencyLong > 1000) { - return; // clock skew or out of reasonable range - } - auto latency = static_cast(latencyLong); - logDebug(lf_bus, "send/receive symbol latency %d ms", latency); - if (m_symbolLatencyMin >= 0 && (latency >= m_symbolLatencyMin && latency <= m_symbolLatencyMax)) { - return; - } - if (m_symbolLatencyMin == -1 || latency < m_symbolLatencyMin) { - m_symbolLatencyMin = latency; - } - if (m_symbolLatencyMax == -1 || latency > m_symbolLatencyMax) { - m_symbolLatencyMax = latency; - } - logInfo(lf_bus, "send/receive symbol latency %d - %d ms", m_symbolLatencyMin, m_symbolLatencyMax); -} - -bool BusHandler::addSeenAddress(symbol_t address) { - if (!isValidAddress(address, false)) { - return false; - } - bool hadConflict = m_addressConflict; - if (!isMaster(address)) { - if (!m_device->isReadOnly() && address == m_ownSlaveAddress) { - if (!m_addressConflict) { - m_addressConflict = true; - logError(lf_bus, "own slave address %2.2x is used by another participant", address); - } - } - m_seenAddresses[address] |= SEEN; - address = getMasterAddress(address); - if (address == SYN) { - return m_addressConflict && !hadConflict; - } - } - if ((m_seenAddresses[address]&SEEN) == 0) { - if (!m_device->isReadOnly() && address == m_ownMasterAddress) { - if (!m_addressConflict) { - m_addressConflict = true; - logError(lf_bus, "own master address %2.2x is used by another participant", address); - } - } else { - m_masterCount++; - if (m_autoLockCount && m_masterCount > m_lockCount) { - m_lockCount = m_masterCount; - } - logNotice(lf_bus, "new master %2.2x, master count %d", address, m_masterCount); - } - m_seenAddresses[address] |= SEEN; - } - return m_addressConflict && !hadConflict; -} - -void BusHandler::messageCompleted() { - const char* prefix = m_currentRequest ? "sent" : "received"; - if (m_currentRequest) { - m_command = m_currentRequest->m_master; - } - symbol_t srcAddress = m_command[0], dstAddress = m_command[1]; - if (srcAddress == dstAddress) { - logError(lf_bus, "invalid self-addressed message from %2.2x", srcAddress); - return; - } - if (!m_currentAnswering) { - addSeenAddress(dstAddress); - } - +void BusHandler::notifyProtocolMessage(MessageDirection direction, const MasterSymbolString& command, + const SlaveSymbolString& response) { + symbol_t srcAddress = command[0], dstAddress = command[1]; bool master = isMaster(dstAddress); if (dstAddress == BROADCAST) { - logInfo(lf_update, "%s BC cmd: %s", prefix, m_command.getStr().c_str()); - if (m_command.getDataSize() >= 10 && m_command[2] == 0x07 && m_command[3] == 0x04) { + if (command.getDataSize() >= 10 && command[2] == 0x07 && command[3] == 0x04) { symbol_t slaveAddress = getSlaveAddress(srcAddress); - addSeenAddress(slaveAddress); + notifyProtocolSeenAddress(slaveAddress); Message* message = m_messages->getScanMessage(slaveAddress); if (message && (message->getLastUpdateTime() == 0 || message->getLastSlaveData().getDataSize() < 10)) { // e.g. 10fe07040a b5564149303001248901 MasterSymbolString dummyMaster; istringstream input; - result_t result = message->prepareMaster(0, m_ownMasterAddress, SYN, UI_FIELD_SEPARATOR, &input, + result_t result = message->prepareMaster(0, m_protocol->getOwnMasterAddress(), SYN, UI_FIELD_SEPARATOR, &input, &dummyMaster); if (result == RESULT_OK) { SlaveSymbolString idData; idData.push_back(10); for (size_t i = 0; i < 10; i++) { - idData.push_back(m_command.dataAt(i)); + idData.push_back(command.dataAt(i)); } result = message->storeLastData(0, idData); if (result == RESULT_OK) { ostringstream output; - result = message->decodeLastData(true, nullptr, -1, OF_NONE, &output); + result = message->decodeLastData(pt_any, true, nullptr, -1, OF_NONE, &output); if (result == RESULT_OK) { string str = output.str(); setScanResult(slaveAddress, 0, str); @@ -1219,17 +414,14 @@ void BusHandler::messageCompleted() { logNotice(lf_update, "store broadcast ident: %s", getResultCode(result)); } } - } else if (master) { - logInfo(lf_update, "%s MM cmd: %s", prefix, m_command.getStr().c_str()); - } else { - logInfo(lf_update, "%s MS cmd: %s / %s", prefix, m_command.getStr().c_str(), m_response.getStr().c_str()); - if (m_command.size() >= 5 && m_command[2] == 0x07 && m_command[3] == 0x04) { + } else if (!master) { + if (command.size() >= 5 && command[2] == 0x07 && command[3] == 0x04) { Message* message = m_messages->getScanMessage(dstAddress); if (message && (message->getLastUpdateTime() == 0 || message->getLastSlaveData().getDataSize() < 10)) { - result_t result = message->storeLastData(m_command, m_response); + result_t result = message->storeLastData(command, response); if (result == RESULT_OK) { ostringstream output; - result = message->decodeLastData(true, nullptr, -1, OF_NONE, &output); + result = message->decodeLastData(pt_any, true, nullptr, -1, OF_NONE, &output); if (result == RESULT_OK) { string str = output.str(); setScanResult(dstAddress, 0, str); @@ -1239,24 +431,40 @@ void BusHandler::messageCompleted() { } } } - Message* message = m_messages->find(m_command); + Message* message = m_messages->find(command); if (m_grabMessages) { uint64_t key; if (message) { key = message->getKey(); } else { - key = Message::createKey(m_command, m_command[1] == BROADCAST ? 1 : 4); // up to 4 DD bytes (1 for broadcast) + key = Message::createKey(command, command[1] == BROADCAST ? 1 : 4); // up to 4 DD bytes (1 for broadcast) + } + m_grabbedMessages[key].setLastData(command, response); + } + if (direction == md_answer) { + size_t idLen = command.getDataSize(); + size_t resLen = response.getDataSize(); + if (master && idLen >= resLen) { + // build MS auto-answer from MM with same ID + SlaveSymbolString answer; + answer.push_back(0); // room for length + idLen -= resLen; + for (size_t pos = idLen; pos < resLen; pos++) { + answer.push_back(command.dataAt(pos)); + } + answer.adjustHeader(); + m_protocol->setAnswer(SYN, getSlaveAddress(dstAddress), command[2], command[3], command.data() + 5, idLen, + answer); + // TODO could use loaded messages for identifying MM/MS message pair } - m_grabbedMessages[key].setLastData(m_command, m_response); } + const char* prefix = direction == md_answer ? "answered" : direction == md_send ? "sent" : "received"; if (message == nullptr) { - if (dstAddress == BROADCAST) { - logNotice(lf_update, "%s unknown BC cmd: %s", prefix, m_command.getStr().c_str()); - } else if (master) { - logNotice(lf_update, "%s unknown MM cmd: %s", prefix, m_command.getStr().c_str()); + if (dstAddress == BROADCAST || master) { + logNotice(lf_update, "%s unknown %s cmd: %s", prefix, master ? "MM" : "BC", command.getStr().c_str()); } else { - logNotice(lf_update, "%s unknown MS cmd: %s / %s", prefix, m_command.getStr().c_str(), - m_response.getStr().c_str()); + logNotice(lf_update, "%s unknown MS cmd: %s / %s", prefix, command.getStr().c_str(), + response.getStr().c_str()); } } else { m_messages->invalidateCache(message); @@ -1266,19 +474,19 @@ void BusHandler::messageCompleted() { : message->isPassive() ? message->isWrite() ? "update-write" : "update-read" : message->getPollPriority() > 0 ? message->isWrite() ? "poll-write" : "poll-read" : message->isWrite() ? "write" : "read"; - result_t result = message->storeLastData(m_command, m_response); + result_t result = message->storeLastData(command, response); ostringstream output; if (result == RESULT_OK) { - result = message->decodeLastData(false, nullptr, -1, OF_NONE, &output); + result = message->decodeLastData(pt_any, false, nullptr, -1, OF_NONE, &output); } if (result < RESULT_OK) { logError(lf_update, "unable to parse %s %s %s from %s / %s: %s", mode, circuit.c_str(), name.c_str(), - m_command.getStr().c_str(), m_response.getStr().c_str(), getResultCode(result)); + command.getStr().c_str(), response.getStr().c_str(), getResultCode(result)); } else { string data = output.str(); - if (m_answer && dstAddress == (master ? m_ownMasterAddress : m_ownSlaveAddress)) { + if (m_protocol->isOwnAddress(dstAddress)) { logNotice(lf_update, "%s %s self-update %s %s QQ=%2.2x: %s", prefix, mode, circuit.c_str(), name.c_str(), - srcAddress, data.c_str()); // TODO store in database of internal variables + srcAddress, data.c_str()); } else if (message->getDstAddress() == SYN) { // any destination if (message->getSrcAddress() == SYN) { // any destination and any source logNotice(lf_update, "%s %s %s %s QQ=%2.2x ZZ=%2.2x: %s", prefix, mode, circuit.c_str(), name.c_str(), @@ -1303,7 +511,7 @@ result_t BusHandler::prepareScan(symbol_t slave, bool full, const string& levels if (scanMessage == nullptr) { return RESULT_ERR_NOTFOUND; } - if (m_device->isReadOnly()) { + if (m_protocol->isReadOnly()) { return RESULT_OK; } deque messages; @@ -1349,7 +557,7 @@ result_t BusHandler::prepareScan(symbol_t slave, bool full, const string& levels return RESULT_OK; } *request = new ScanRequest(slave == SYN, m_messages, messages, slaves, this, *reload ? 0 : 1); - result_t result = (*request)->prepare(m_ownMasterAddress); + result_t result = (*request)->prepare(m_protocol->getOwnMasterAddress()); if (result < RESULT_OK) { delete *request; *request = nullptr; @@ -1373,8 +581,8 @@ result_t BusHandler::startScan(bool full, const string& levels) { } m_scanResults.clear(); m_runningScans++; - m_nextRequests.push(request); - return RESULT_OK; + // request is deleted by ProtocolHandler after finish + return m_protocol->addRequest(request, false); } void BusHandler::setScanResult(symbol_t dstAddress, size_t index, const string& str) { @@ -1433,7 +641,7 @@ void BusHandler::formatScanResult(ostringstream* output) const { *output << endl; } *output << hex << setw(2) << setfill('0') << static_cast(slave); - message->decodeLastData(true, nullptr, -1, OF_NONE, output); + message->decodeLastData(pt_any, true, nullptr, -1, OF_NONE, output); } } } @@ -1443,7 +651,7 @@ void BusHandler::formatScanResult(ostringstream* output) const { void BusHandler::formatSeenInfo(ostringstream* output) const { symbol_t address = 0; for (int index = 0; index < 256; index++, address++) { - bool ownAddress = !m_device->isReadOnly() && (address == m_ownMasterAddress || address == m_ownSlaveAddress); + bool ownAddress = m_protocol->isOwnAddress(address); if (!isValidAddress(address, false) || ((m_seenAddresses[address]&SEEN) == 0 && !ownAddress)) { continue; } @@ -1461,12 +669,12 @@ void BusHandler::formatSeenInfo(ostringstream* output) const { } if (ownAddress) { *output << ", ebusd"; - if (m_answer) { - *output << " (answering)"; - } - if (m_addressConflict && (m_seenAddresses[address]&SEEN) != 0) { - *output << ", conflict"; - } + } + if (m_protocol->hasAnswer(address)) { + *output << " (answering)"; + } + if (ownAddress && m_protocol->isAddressConflict(address)) { + *output << ", conflict"; } if ((m_seenAddresses[address]&SCAN_DONE) != 0) { *output << ", scanned"; @@ -1474,7 +682,7 @@ void BusHandler::formatSeenInfo(ostringstream* output) const { if (message != nullptr && message->getLastUpdateTime() > 0) { // add detailed scan info: Manufacturer ID SW HW *output << " \""; - result_t result = message->decodeLastData(false, nullptr, -1, OF_NAMES, output); + result_t result = message->decodeLastData(pt_any, false, nullptr, -1, OF_NAMES, output); if (result != RESULT_OK) { *output << "\" error: " << getResultCode(result); } else { @@ -1507,14 +715,14 @@ void BusHandler::formatSeenInfo(ostringstream* output) const { } void BusHandler::formatUpdateInfo(ostringstream* output) const { - if (hasSignal()) { - *output << ",\"s\":" << m_maxSymPerSec; + if (m_protocol->hasSignal()) { + *output << ",\"s\":" << m_protocol->getMaxSymPerSec(); } - *output << ",\"c\":" << m_masterCount + *output << ",\"c\":" << m_protocol->getMasterCount() << ",\"m\":" << m_messages->size() - << ",\"ro\":" << (m_device->isReadOnly() ? 1 : 0) - << ",\"an\":" << (m_answer ? 1 : 0) - << ",\"co\":" << (m_addressConflict ? 1 : 0); + << ",\"ro\":" << (m_protocol->isReadOnly() ? 1 : 0) + << ",\"an\":" << (m_protocol->isAnswering() ? 1 : 0) + << ",\"co\":" << (m_protocol->isAddressConflict(SYN) ? 1 : 0); if (m_grabMessages) { size_t unknownCnt = 0; *output << ",\"gm\":["; @@ -1530,9 +738,12 @@ void BusHandler::formatUpdateInfo(ostringstream* output) const { } *output << "],\"gu\":" << unknownCnt; } + if (!m_messages->getPreferLanguage().empty()) { + *output << ",\"lc\":\"" << m_messages->getPreferLanguage() << "\""; + } unsigned char address = 0; for (int index = 0; index < 256; index++, address++) { - bool ownAddress = !m_device->isReadOnly() && (address == m_ownMasterAddress || address == m_ownSlaveAddress); + bool ownAddress = m_protocol->isOwnAddress(address); if (!isValidAddress(address, false) || ((m_seenAddresses[address]&SEEN) == 0 && !ownAddress)) { continue; } @@ -1550,7 +761,7 @@ void BusHandler::formatUpdateInfo(ostringstream* output) const { Message* message = m_messages->getScanMessage(address); if (message != nullptr && message->getLastUpdateTime() > 0) { // add detailed scan info: Manufacturer ID SW HW - message->decodeLastData(true, nullptr, -1, OF_NAMES|OF_NUMERIC|OF_JSON|OF_SHORT, output); + message->decodeLastData(pt_any, true, nullptr, -1, OF_NAMES|OF_NUMERIC|OF_JSON|OF_SHORT, output); } } const vector& loadedFiles = m_messages->getLoadedFiles(address); @@ -1619,9 +830,11 @@ result_t BusHandler::scanAndWait(symbol_t dstAddress, bool loadScanConfig, bool m_scanResults[dstAddress].resize(1); } m_runningScans++; - m_nextRequests.push(request); - requestExecuted = m_finishedRequests.remove(request, true); - result = requestExecuted ? request->m_result : RESULT_ERR_TIMEOUT; + result = m_protocol->addRequest(request, true); + requestExecuted = result == RESULT_OK; + if (requestExecuted) { + result = request->m_result; + } delete request; request = nullptr; } @@ -1630,14 +843,14 @@ result_t BusHandler::scanAndWait(symbol_t dstAddress, bool loadScanConfig, bool bool timedOut = result == RESULT_ERR_TIMEOUT; bool loadFailed = false; if (timedOut || result == RESULT_OK) { - result = loadScanConfigFile(m_messages, dstAddress, false, &file); // try to load even if one message timed out + result = m_scanHelper->loadScanConfigFile(dstAddress, &file); // try to load even if one message timed out loadFailed = result != RESULT_OK; if (timedOut && loadFailed) { result = RESULT_ERR_TIMEOUT; // back to previous result } } if (result == RESULT_OK) { - executeInstructions(m_messages); + m_scanHelper->executeInstructions(this); setScanConfigLoaded(dstAddress, file); if (!hasAdditionalScanMessages && m_messages->hasAdditionalScanMessages()) { // additional scan messages now available diff --git a/src/ebusd/bushandler.h b/src/ebusd/bushandler.h index 58441d9c1..ecff82da8 100755 --- a/src/ebusd/bushandler.h +++ b/src/ebusd/bushandler.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,70 +19,26 @@ #ifndef EBUSD_BUSHANDLER_H_ #define EBUSD_BUSHANDLER_H_ -#include #include #include #include #include #include +#include "ebusd/scan.h" #include "lib/ebus/message.h" #include "lib/ebus/data.h" #include "lib/ebus/symbol.h" #include "lib/ebus/result.h" -#include "lib/ebus/device.h" -#include "lib/utils/queue.h" -#include "lib/utils/thread.h" +#include "lib/ebus/protocol.h" namespace ebusd { /** @file ebusd/bushandler.h - * Classes, functions, and constants related to handling of symbols on the eBUS. - * - * The following table shows the possible states, symbols, and state transition - * depending on the kind of message to send/receive: - * @image html states.png "ebusd BusHandler states" + * Classes, functions, and constants related to handling messages on the eBUS. */ using std::string; -/** the default time [ms] for retrieving a symbol from an addressed slave. */ -#define SLAVE_RECV_TIMEOUT 15 - -/** the maximum allowed time [ms] for retrieving the AUTO-SYN symbol (45ms + 2*1,2% + 1 Symbol). */ -#define SYN_TIMEOUT 51 - -/** the time [ms] for determining bus signal availability (AUTO-SYN timeout * 5). */ -#define SIGNAL_TIMEOUT 250 - -/** the maximum duration [us] of a single symbol (Start+8Bit+Stop+Extra @ 2400Bd-2*1,2%). */ -#define SYMBOL_DURATION_MICROS 4700 - -/** the maximum duration [ms] of a single symbol (Start+8Bit+Stop+Extra @ 2400Bd-2*1,2%). */ -#define SYMBOL_DURATION 5 - -/** the maximum allowed time [ms] for retrieving back a sent symbol (2x symbol duration). */ -#define SEND_TIMEOUT ((int)((2*SYMBOL_DURATION_MICROS+999)/1000)) - -/** the possible bus states. */ -enum BusState { - bs_noSignal, //!< no signal on the bus - bs_skip, //!< skip all symbols until next @a SYN - bs_ready, //!< ready for next master (after @a SYN symbol, send/receive QQ) - bs_recvCmd, //!< receive command (ZZ, PBSB, master data) [passive set] - bs_recvCmdCrc, //!< receive command CRC [passive set] - bs_recvCmdAck, //!< receive command ACK/NACK [passive set + active set+get] - bs_recvRes, //!< receive response (slave data) [passive set + active get] - bs_recvResCrc, //!< receive response CRC [passive set + active get] - bs_recvResAck, //!< receive response ACK/NACK [passive set] - bs_sendCmd, //!< send command (ZZ, PBSB, master data) [active set+get] - bs_sendCmdCrc, //!< send command CRC [active set+get] - bs_sendResAck, //!< send response ACK/NACK [active get] - bs_sendCmdAck, //!< send command ACK/NACK [passive get] - bs_sendRes, //!< send response (slave data) [passive get] - bs_sendResCrc, //!< send response CRC [passive get] - bs_sendSyn, //!< send SYN for completed transfer [active set+get] -}; - /** bit for the seen state: seen. */ #define SEEN 0x01 @@ -100,47 +56,6 @@ enum BusState { class BusHandler; -/** - * Generic request for sending to and receiving from the bus. - */ -class BusRequest { - friend class BusHandler; - - public: - /** - * Constructor. - * @param master the master data @a MasterSymbolString to send. - * @param deleteOnFinish whether to automatically delete this @a BusRequest when finished. - */ - BusRequest(const MasterSymbolString& master, bool deleteOnFinish) - : m_master(master), m_busLostRetries(0), - m_deleteOnFinish(deleteOnFinish) {} - - /** - * Destructor. - */ - virtual ~BusRequest() {} - - /** - * Notify the request of the specified result. - * @param result the result of the request. - * @param slave the @a SlaveSymbolString received. - * @return true if the request needs to be restarted. - */ - virtual bool notify(result_t result, const SlaveSymbolString& slave) = 0; - - - protected: - /** the master data @a MasterSymbolString to send. */ - const MasterSymbolString& m_master; - - /** the number of times a send is repeated due to lost arbitration. */ - unsigned int m_busLostRetries; - - /** whether to automatically delete this @a BusRequest when finished. */ - const bool m_deleteOnFinish; -}; - /** * A poll @a BusRequest handled by @a BusHandler itself. @@ -258,39 +173,6 @@ class ScanRequest : public BusRequest { }; -/** - * An active @a BusRequest that can be waited for. - */ -class ActiveBusRequest : public BusRequest { - friend class BusHandler; - - public: - /** - * Constructor. - * @param master the master data @a MasterSymbolString to send. - * @param slave reference to @a SlaveSymbolString for filling in the received slave data. - */ - ActiveBusRequest(const MasterSymbolString& master, SlaveSymbolString* slave) - : BusRequest(master, false), m_result(RESULT_ERR_NO_SIGNAL), m_slave(slave) {} - - /** - * Destructor. - */ - virtual ~ActiveBusRequest() {} - - // @copydoc - bool notify(result_t result, const SlaveSymbolString& slave) override; - - - private: - /** the result of handling the request. */ - result_t m_result; - - /** reference to @a SlaveSymbolString for filling in the received slave data. */ - SlaveSymbolString* m_slave; -}; - - /** * Helper class for keeping track of grabbed messages. */ @@ -329,6 +211,12 @@ class GrabbedMessage { */ MasterSymbolString& getLastMasterData() { return m_lastMaster; } + /** + * Get the last @a SlaveSymbolString. + * @return the last @a SlaveSymbolString. + */ + SlaveSymbolString& getLastSlaveData() { return m_lastSlave; } + /** * Dump the last received data and message count to the output. * @param unknown whether to dump only if this message is unknown. @@ -361,93 +249,43 @@ class GrabbedMessage { /** * Handles input from and output to the bus with respect to the eBUS protocol. */ -class BusHandler : public WaitThread { +class BusHandler : public ProtocolListener { public: /** * Construct a new instance. - * @param device the @a Device instance for accessing the bus. * @param messages the @a MessageMap instance with all known @a Message instances. - * @param ownAddress the own master address. - * @param answer whether to answer queries for the own master/slave address. - * @param busLostRetries the number of times a send is repeated due to lost arbitration. - * @param failedSendRetries the number of times a failed send is repeated (other than lost arbitration). - * @param busAcquireTimeout the maximum time in milliseconds for bus acquisition. - * @param slaveRecvTimeout the maximum time in milliseconds an addressed slave is expected to acknowledge. - * @param lockCount the number of AUTO-SYN symbols before sending is allowed after lost arbitration, or 0 for auto detection. - * @param generateSyn whether to enable AUTO-SYN symbol generation. + * @param scanHelper the @a ScanHelper instance. * @param pollInterval the interval in seconds in which poll messages are cycled, or 0 if disabled. */ - BusHandler(Device* device, MessageMap* messages, - symbol_t ownAddress, bool answer, - unsigned int busLostRetries, unsigned int failedSendRetries, - unsigned int busAcquireTimeout, unsigned int slaveRecvTimeout, - unsigned int lockCount, bool generateSyn, + BusHandler(MessageMap* messages, ScanHelper* scanHelper, unsigned int pollInterval) - : WaitThread(), m_device(device), m_reconnect(false), m_messages(messages), - m_ownMasterAddress(ownAddress), m_ownSlaveAddress(getSlaveAddress(ownAddress)), - m_answer(answer), m_addressConflict(false), - m_busLostRetries(busLostRetries), m_failedSendRetries(failedSendRetries), - m_busAcquireTimeout(busAcquireTimeout), m_slaveRecvTimeout(slaveRecvTimeout), - m_masterCount(device->isReadOnly()?0:1), m_autoLockCount(lockCount == 0), - m_lockCount(lockCount <= 3 ? 3 : lockCount), m_remainLockCount(m_autoLockCount ? 1 : 0), - m_generateSynInterval(generateSyn ? SYN_TIMEOUT*getMasterNumber(ownAddress)+SYMBOL_DURATION : 0), - m_pollInterval(pollInterval), m_symbolLatencyMin(-1), m_symbolLatencyMax(-1), m_arbitrationDelayMin(-1), - m_arbitrationDelayMax(-1), m_lastReceive(0), m_lastPoll(0), - m_currentRequest(nullptr), m_currentAnswering(false), m_runningScans(0), m_nextSendPos(0), - m_symPerSec(0), m_maxSymPerSec(0), - m_state(bs_noSignal), m_escape(0), m_crc(0), m_crcValid(false), m_repeat(false), + : m_protocol(nullptr), m_messages(messages), m_scanHelper(scanHelper), + m_pollInterval(pollInterval), m_lastPoll(0), m_runningScans(0), m_grabMessages(true) { memset(m_seenAddresses, 0, sizeof(m_seenAddresses)); - m_lastSynReceiveTime.tv_sec = 0; - m_lastSynReceiveTime.tv_nsec = 0; } /** * Destructor. */ virtual ~BusHandler() { - stop(); - join(); - BusRequest* req; - while ((req = m_finishedRequests.pop()) != nullptr) { - delete req; - } - while ((req = m_nextRequests.pop()) != nullptr) { - if (req->m_deleteOnFinish) { - delete req; - } - } - if (m_currentRequest != nullptr) { - delete m_currentRequest; - m_currentRequest = nullptr; - } } /** - * Clear stored values (e.g. scan results). + * Set the @a ProtocolHandler instance for accessing the bus. + * @param protocol the @a ProtocolHandler instance for accessing the bus. */ - void clear(); + void setProtocol(ProtocolHandler* protocol) { m_protocol = protocol; } /** - * Inject a message from outside and treat it as regularly retrieved from the bus. - * @param master the @a MasterSymbolString with the master data. - * @param slave the @a SlaveSymbolString with the slave data. + * @return the @a ProtocolHandler instance for accessing the bus. */ - void injectMessage(const MasterSymbolString& master, const SlaveSymbolString& slave) { - m_command = master; - m_response = slave; - m_addressConflict = true; // avoid conflict messages - messageCompleted(); - m_addressConflict = false; - } + ProtocolHandler* getProtocol() const { return m_protocol; } /** - * Send a message on the bus and wait for the answer. - * @param master the @a MasterSymbolString with the master data to send. - * @param slave the @a SlaveSymbolString that will be filled with retrieved slave data. - * @return the result code. + * Clear stored values (e.g. scan results). */ - result_t sendAndWait(const MasterSymbolString& master, SlaveSymbolString* slave); + void clear(); /** * Prepare the master part for the @a Message, send it to the bus and wait for the answer. @@ -460,11 +298,6 @@ class BusHandler : public WaitThread { result_t readFromBus(Message* message, const string& inputStr, symbol_t dstAddress = SYN, symbol_t srcAddress = SYN); - /** - * Main thread entry. - */ - virtual void run(); - /** * Initiate a scan of the slave addresses. * @param full true for a full scan (all slaves), false for scanning only already seen slaves. @@ -486,6 +319,12 @@ class BusHandler : public WaitThread { */ void setScanFinished(); + /** + * Get the number of scan requests currently running. + * @eturn the number of scan requests currently running. + */ + unsigned int getRunningScans() const { return m_runningScans; } + /** * Format the scan result for a single slave to the @a ostringstream. * @param slave the slave address for which to format the scan result. @@ -547,59 +386,6 @@ class BusHandler : public WaitThread { void formatGrabResult(bool unknown, OutputFormat outputFormat, ostringstream* output, bool isDirectMode = false, time_t since = 0, time_t until = 0) const; - /** - * Return true when a signal on the bus is available. - * @return true when a signal on the bus is available. - */ - bool hasSignal() const { return m_state != bs_noSignal; } - - /** - * Reconnect the device. - */ - void reconnect() { m_reconnect = true; } - - /** - * Return the current symbol rate. - * @return the number of received symbols in the last second. - */ - unsigned int getSymbolRate() const { return m_symPerSec; } - - /** - * Return the maximum seen symbol rate. - * @return the maximum number of received symbols per second ever seen. - */ - unsigned int getMaxSymbolRate() const { return m_maxSymPerSec; } - - /** - * Return the minimal measured latency between send and receive of a symbol. - * @return the minimal measured latency between send and receive of a symbol in milliseconds, -1 if not yet known. - */ - int getMinSymbolLatency() const { return m_symbolLatencyMin; } - - /** - * Return the maximal measured latency between send and receive of a symbol. - * @return the maximal measured latency between send and receive of a symbol in milliseconds, -1 if not yet known. - */ - int getMaxSymbolLatency() const { return m_symbolLatencyMax; } - - /** - * Return the minimal measured delay between received SYN and sent own master address in microseconds. - * @return the minimal measured delay between received SYN and sent own master address in microseconds, -1 if not yet known. - */ - int getMinArbitrationDelay() const { return m_arbitrationDelayMin; } - - /** - * Return the maximal measured delay between received SYN and sent own master address in microseconds. - * @return the maximal measured delay between received SYN and sent own master address in microseconds, -1 if not yet known. - */ - int getMaxArbitrationDelay() const { return m_arbitrationDelayMax; } - - /** - * Return the number of masters already seen. - * @return the number of masters already seen (including ebusd itself). - */ - unsigned int getMasterCount() const { return m_masterCount; } - /** * Get the next slave address that still needs to be scanned or loaded. * @param lastAddress the last returned slave address, or 0 for returning the first one. @@ -615,42 +401,17 @@ class BusHandler : public WaitThread { */ void setScanConfigLoaded(symbol_t address, const string& file); + // @copydoc + void notifyProtocolStatus(ProtocolState state, result_t result) override; - private: - /** - * Handle the next symbol on the bus. - * @return RESULT_OK on success, or an error code. - */ - result_t handleSymbol(); - - /** - * Set a new @a BusState and add a log message if necessary. - * @param state the new @a BusState. - * @param result the result code. - * @param firstRepetition true if the first repetition of a message part is being started. - * @return the result code. - */ - result_t setState(BusState state, result_t result, bool firstRepetition = false); - - /** - * Add a seen bus address. - * @param address the seen bus address. - * @return true if a conflict with the own addresses was detected, false otherwise. - */ - bool addSeenAddress(symbol_t address); - - /** - * Called to measure the latency between send and receive of a symbol. - * @param sentTime the time the symbol was sent. - * @param recvTime the time the symbol was received. - */ - void measureLatency(struct timespec* sentTime, struct timespec* recvTime); + // @copydoc + void notifyProtocolSeenAddress(symbol_t address) override; - /** - * Called when a message sending or reception was successfully completed. - */ - void messageCompleted(); + // @copydoc + void notifyProtocolMessage(MessageDirection direction, const MasterSymbolString& master, + const SlaveSymbolString& slave) override; + private: /** * Prepare a @a ScanRequest. * @param slave the single slave address to scan, or @a SYN for multiple. @@ -662,130 +423,24 @@ class BusHandler : public WaitThread { */ result_t prepareScan(symbol_t slave, bool full, const string& levels, bool* reload, ScanRequest** request); - /** the @a Device instance for accessing the bus. */ - Device* m_device; - - /** set to @p true when the device shall be reconnected. */ - bool m_reconnect; + /** the @a ProtocolHandler instance for accessing the bus (loosely coupled but set quickly after construction). */ + ProtocolHandler* m_protocol; /** the @a MessageMap instance with all known @a Message instances. */ MessageMap* m_messages; - /** the own master address. */ - const symbol_t m_ownMasterAddress; - - /** the own slave address. */ - const symbol_t m_ownSlaveAddress; - - /** whether to answer queries for the own master/slave address. */ - const bool m_answer; - - /** set to @p true once an address conflict with the own addresses was detected. */ - bool m_addressConflict; - - /** the number of times a send is repeated due to lost arbitration. */ - const unsigned int m_busLostRetries; - - /** the number of times a failed send is repeated (other than lost arbitration). */ - const unsigned int m_failedSendRetries; - - /** the maximum time in milliseconds for bus acquisition. */ - const unsigned int m_busAcquireTimeout; - - /** the maximum time in milliseconds an addressed slave is expected to acknowledge. */ - const unsigned int m_slaveRecvTimeout; - - /** the number of masters already seen. */ - unsigned int m_masterCount; - - /** whether m_lockCount shall be detected automatically. */ - const bool m_autoLockCount; - - /** the number of AUTO-SYN symbols before sending is allowed after lost arbitration. */ - unsigned int m_lockCount; - - /** the remaining number of AUTO-SYN symbols before sending is allowed again. */ - unsigned int m_remainLockCount; - - /** the interval in milliseconds after which to generate an AUTO-SYN symbol, or 0 if disabled. */ - unsigned int m_generateSynInterval; + /** the @a ScanHelper instance. */ + ScanHelper* m_scanHelper; /** the interval in seconds in which poll messages are cycled, or 0 if disabled. */ const unsigned int m_pollInterval; - /** the minimal measured latency between send and receive of a symbol in milliseconds, -1 if not yet known. */ - int m_symbolLatencyMin; - - /** the maximal measured latency between send and receive of a symbol in milliseconds, -1 if not yet known. */ - int m_symbolLatencyMax; - - /** - * the minimal measured delay between received SYN and sent own master address in microseconds, - * -1 if not yet known. - */ - int m_arbitrationDelayMin; - - /** - * the maximal measured delay between received SYN and sent own master address in microseconds, - * -1 if not yet known. - */ - int m_arbitrationDelayMax; - - /** the time of the last received SYN symbol, or 0 for never. */ - struct timespec m_lastSynReceiveTime; - - /** the time of the last received symbol, or 0 for never. */ - time_t m_lastReceive; - /** the time of the last poll, or 0 for never. */ time_t m_lastPoll; - /** the queue of @a BusRequests that shall be handled. */ - Queue m_nextRequests; - - /** the currently handled BusRequest, or nullptr. */ - BusRequest* m_currentRequest; - - /** whether currently answering a request from another participant. */ - bool m_currentAnswering; - - /** the queue of @a BusRequests that are already finished. */ - Queue m_finishedRequests; - - /** the number of scan request currently running. */ + /** the number of scan requests currently running. */ unsigned int m_runningScans; - /** the offset of the next symbol that needs to be sent from the command or response, - * (only relevant if m_request is set and state is @a bs_command or @a bs_response). */ - size_t m_nextSendPos; - - /** the number of received symbols in the last second. */ - unsigned int m_symPerSec; - - /** the maximum number of received symbols per second ever seen. */ - unsigned int m_maxSymPerSec; - - /** the current @a BusState. */ - BusState m_state; - - /** 0 when not escaping/unescaping, or @a ESC when receiving, or the original value when sending. */ - symbol_t m_escape; - - /** the calculated CRC. */ - symbol_t m_crc; - - /** whether the CRC matched. */ - bool m_crcValid; - - /** whether the current message part is being repeated. */ - bool m_repeat; - - /** the received command @a MasterSymbolString. */ - MasterSymbolString m_command; - - /** the received response @a SlaveSymbolString or response to send. */ - SlaveSymbolString m_response; - /** the participating bus addresses seen so far (0 if not seen yet, or combination of @a SEEN bits). */ symbol_t m_seenAddresses[256]; diff --git a/src/ebusd/datahandler.cpp b/src/ebusd/datahandler.cpp index 264c3b80f..d3c82b3a9 100755 --- a/src/ebusd/datahandler.cpp +++ b/src/ebusd/datahandler.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -30,11 +30,11 @@ namespace ebusd { -/** the final @a argp_child structure. */ -static const struct argp_child g_last_argp_child = {nullptr, 0, nullptr, 0}; +/** the final @a argParseChildOpt structure. */ +static const argParseChildOpt g_last_arg_child = {nullptr, nullptr}; -/** the list of @a argp_child structures. */ -static struct argp_child g_argp_children[ +/** the list of @a argParseChildOpt structures. */ +static argParseChildOpt g_arg_children[ 1 #ifdef HAVE_MQTT +1 @@ -44,17 +44,17 @@ static struct argp_child g_argp_children[ #endif ]; -const struct argp_child* datahandler_getargs() { +const argParseChildOpt* datahandler_getargs() { size_t count = 0; #ifdef HAVE_MQTT - g_argp_children[count++] = *mqtthandler_getargs(); + g_arg_children[count++] = *mqtthandler_getargs(); #endif #ifdef HAVE_KNX - g_argp_children[count++] = *knxhandler_getargs(); + g_arg_children[count++] = *knxhandler_getargs(); #endif if (count > 0) { - g_argp_children[count] = g_last_argp_child; - return g_argp_children; + g_arg_children[count] = g_last_arg_child; + return g_arg_children; } return nullptr; } @@ -75,8 +75,11 @@ bool datahandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap return success; } -void DataSink::notifyUpdate(Message* message) { +void DataSink::notifyUpdate(Message* message, bool changed) { if (message && message->hasLevel(m_levels)) { + if (m_changedOnly && !changed) { + return; + } m_updatedMessages[message->getKey()]++; } } diff --git a/src/ebusd/datahandler.h b/src/ebusd/datahandler.h index a4f161e87..ff977eda8 100755 --- a/src/ebusd/datahandler.h +++ b/src/ebusd/datahandler.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,12 +19,12 @@ #ifndef EBUSD_DATAHANDLER_H_ #define EBUSD_DATAHANDLER_H_ -#include #include #include #include #include "ebusd/bushandler.h" #include "lib/ebus/message.h" +#include "lib/utils/arg.h" namespace ebusd { @@ -49,10 +49,10 @@ enum scanStatus_t { /** - * Helper function for getting the argp definition for all known @a DataHandler instances. - * @return a pointer to the argp_child structure, or nullptr. + * Helper function for getting the arg definition for all known @a DataHandler instances. + * @return a pointer to the child argument options, or nullptr. */ -const struct argp_child* datahandler_getargs(); +const argParseChildOpt* datahandler_getargs(); /** * Registration function that is called once during initialization. @@ -143,8 +143,9 @@ class DataSink : virtual public DataHandler { * Constructor. * @param userInfo the @a UserInfo instance. * @param user the user name for determining the allowed access levels (fall back to default levels). + * @param changedOnly whether to handle changed messages only in the updates. */ - DataSink(const UserInfo* userInfo, const string& user) { + DataSink(const UserInfo* userInfo, const string& user, bool changedOnly) : m_changedOnly(changedOnly) { m_levels = userInfo->getLevels(userInfo->hasUser(user) ? user : ""); } @@ -159,8 +160,9 @@ class DataSink : virtual public DataHandler { /** * Notify the sink of an updated @a Message (not necessarily changed though). * @param message the updated @a Message. + * @param changed whether the message data changed since the last notification. */ - virtual void notifyUpdate(Message* message); + virtual void notifyUpdate(Message* message, bool changed); /** * Notify the sink of the latest update check result. @@ -178,6 +180,9 @@ class DataSink : virtual public DataHandler { /** the allowed access levels. */ string m_levels; + /** whether to handle changed messages only in the updates. */ + bool m_changedOnly; + /** a map of updated @p Message keys. */ map m_updatedMessages; }; diff --git a/src/ebusd/knxhandler.cpp b/src/ebusd/knxhandler.cpp index 082421d2a..cdeecaac2 100644 --- a/src/ebusd/knxhandler.cpp +++ b/src/ebusd/knxhandler.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2022 John Baier + * Copyright (C) 2022-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -52,21 +52,21 @@ using std::dec; #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", OPTION_ARG_OPTIONAL, "URL to open (i.e. \"[multicast][@interface]\" for KNXnet/IP" +static const argDef g_knx_argDefs[] = { + {nullptr, 0, nullptr, 0, "KNX options:"}, + {"knxurl", O_URL, "URL", af_optional, "URL to open (i.e. \"[multicast][@interface]\" for KNXnet/IP" #ifdef HAVE_KNXD " or \"ip:host[:port]\" / \"local:/socketpath\" for knxd" #endif - ") (no default)", 0 }, + ") (no default)"}, {"knxrage", O_AGR, "SEC", 0, "Maximum age in seconds for using the last value of read messages (0=disable)" - " [5]", 0 }, + " [5]"}, {"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 variable(s) to the read KNX integration settings", 0 }, + " (0=disable) [99999999]"}, + {"knxint", O_INT, "FILE", 0, "Read KNX integration settings from FILE [/etc/ebusd/knx.cfg]"}, + {"knxvar", O_VAR, "NAME=VALUE[,...]", 0, "Add variable(s) to the read KNX integration settings"}, - {nullptr, 0, nullptr, 0, nullptr, 0 }, + {nullptr, 0, nullptr, 0, nullptr}, }; static const char* g_url = nullptr; //!< URL of KNX daemon @@ -79,11 +79,11 @@ static vector* g_integrationVars = nullptr; //!< the integration settin /** * The KNX argument parsing function. - * @param key the key from @a g_knx_argp_options. + * @param key the key from @a g_knx_arg_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) { +static int knx_parse_opt(int key, char *arg, const argParseOpt *parseOpt, void *userArg) { result_t result; unsigned int value; switch (key) { @@ -94,12 +94,12 @@ static error_t knx_parse_opt(int key, char *arg, struct argp_state *state) { case O_AGR: // --knxrage=5 if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid knxrage value"); + argParseError(parseOpt, "invalid knxrage value"); return EINVAL; } value = parseInt(arg, 10, 0, 99999999, &result); if (result != RESULT_OK) { - argp_error(state, "invalid knxrage"); + argParseError(parseOpt, "invalid knxrage"); return EINVAL; } g_maxReadAge = value; @@ -107,12 +107,12 @@ static error_t knx_parse_opt(int key, char *arg, struct argp_state *state) { case O_AGW: // --knxwage=5 if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid knxwage value"); + argParseError(parseOpt, "invalid knxwage value"); return EINVAL; } value = parseInt(arg, 10, 0, 99999999, &result); if (result != RESULT_OK) { - argp_error(state, "invalid knxwage"); + argParseError(parseOpt, "invalid knxwage"); return EINVAL; } g_maxWriteAge = value; @@ -120,7 +120,7 @@ static error_t knx_parse_opt(int key, char *arg, struct argp_state *state) { case O_INT: // --knxint=/etc/ebusd/knx.cfg if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid knxint file"); + argParseError(parseOpt, "invalid knxint file"); return EINVAL; } g_integrationFile = arg; @@ -129,7 +129,7 @@ static error_t knx_parse_opt(int key, char *arg, struct argp_state *state) { case O_VAR: // --knxvar=NAME=VALUE[,NAME=VALUE]* { if (arg == nullptr || arg[0] == 0 || !strchr(arg, '=')) { - argp_error(state, "invalid knxvar"); + argParseError(parseOpt, "invalid knxvar"); return EINVAL; } if (!g_integrationVars) { @@ -143,18 +143,15 @@ static error_t knx_parse_opt(int key, char *arg, struct argp_state *state) { } default: - return ARGP_ERR_UNKNOWN; + return ESRCH; } 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}; +static const argParseChildOpt g_knx_arg_child = { g_knx_argDefs, knx_parse_opt }; - -const struct argp_child* knxhandler_getargs() { - return &g_knx_argp_child; +const argParseChildOpt* knxhandler_getargs() { + return &g_knx_arg_child; } bool knxhandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages, @@ -166,7 +163,7 @@ bool knxhandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* } KnxHandler::KnxHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages) - : DataSink(userInfo, "knx"), DataSource(busHandler), WaitThread(), m_messages(messages), + : DataSink(userInfo, "knx", true), 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); @@ -320,42 +317,6 @@ result_t getFieldLength(const SingleDataField *field, dtlf_t *length) { return RESULT_OK; } -uint32_t floatToInt16(float val) { - // (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent - if (val == 0) { - return 0; - } - bool negative = val < 0; - if (negative) { - val = -val; - } - val *= 100; - int exp = ilogb(val)-10; - if (exp < -10 || exp > 15) { - return 0x7fff; // invalid value DPT 9 - } - auto shift = exp > 0 ? exp : 0; - auto sig = static_cast(val * exp2(-shift)); - uint32_t value = static_cast(shift << 11) | sig; - if (negative) { - return value | 0x8000; - } - return value; -} - -float int16ToFloat(uint16_t val) { - if (val == 0) { - return 0; - } - if (val == 0x7fff) { - return static_cast(0xffffffff); // NaN - } - bool negative = val&0x8000; - int exp = (val>>11)&0xf; - int sig = val&0x7ff; - return static_cast(sig * exp2(exp) * (negative ? -0.01 : 0.01)); -} - result_t KnxHandler::sendGroupValue(knx_addr_t dest, apci_t apci, dtlf_t& lengthFlag, unsigned int value, const SingleDataField *field) const { if (!m_con || !m_con->isConnected() || !m_con->getAddress()) { @@ -385,7 +346,7 @@ const SingleDataField *field) const { 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); + value = floatToUint16(fval); } else if (lengthFlag.length == 4) { // convert to IEEE 754 value = floatToUint(fval); @@ -451,10 +412,10 @@ void KnxHandler::sendGlobalValue(global_t index, unsigned int value, bool respon } result_t KnxHandler::receiveTelegram(int maxlen, knx_transfer_t* typ, uint8_t *buf, int *recvlen, - knx_addr_t *src, knx_addr_t *dest) { + knx_addr_t *src, knx_addr_t *dest, bool wait) { struct timespec tdiff = { - .tv_sec = 2, - .tv_nsec = 0, + .tv_sec = wait ? 2 : 0, // 2 seconds when waiting + .tv_nsec = wait ? 0 : 1000000, // 1 millisecond when not waiting }; if (!m_con->isConnected()) { return RESULT_ERR_GENERIC_IO; @@ -612,7 +573,7 @@ void KnxHandler::handleGroupTelegram(knx_addr_t src, knx_addr_t dest, int len, c sendGlobalValue(GLOBAL_UPTIME, static_cast(time(nullptr) - m_start), true); break; case GLOBAL_SIGNAL: - sendGlobalValue(GLOBAL_SIGNAL, m_busHandler->hasSignal() ? 1 : 0, true); + sendGlobalValue(GLOBAL_SIGNAL, m_busHandler->getProtocol()->hasSignal() ? 1 : 0, true); break; case GLOBAL_SCAN: sendGlobalValue(GLOBAL_SCAN, m_lastScanStatus == SCAN_STATUS_RUNNING ? 1 : 0, true); @@ -689,8 +650,8 @@ void KnxHandler::handleGroupTelegram(knx_addr_t src, knx_addr_t dest, int len, c if (lengthFlag.isFloat || lengthFlag.hasDivisor) { float fval; if (lengthFlag.length == 2) { - // convert from (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent - fval = int16ToFloat(static_cast(value)); + // convert from (0.01*m)(2^e) format + fval = uint16ToFloat(static_cast(value)); } else if (lengthFlag.length == 4) { // convert from IEEE 754 bool negative = (value & (1u << 31)) != 0; @@ -748,7 +709,7 @@ void KnxHandler::handleGroupTelegram(knx_addr_t src, knx_addr_t dest, int len, c #define UPTIME_INTERVAL 3600 void KnxHandler::run() { - time_t lastTaskRun, now, lastSignal = 0, lastUptime = 0, lastUpdates = 0; + time_t lastTaskRun, now, lastSignal = 0, lastUptime = 0; bool signal = false; result_t result = RESULT_OK; time(&now); @@ -799,7 +760,7 @@ void KnxHandler::run() { } if (m_con->isConnected()) { deque messages; - m_messages->findAll("", "", m_levels, false, true, true, true, true, true, 0, 0, true, &messages); + m_messages->findAll("", "", m_levels, false, true, true, true, true, true, 0, 0, false, &messages); int addCnt = 0; for (const auto& message : messages) { const auto mit = m_subscribedMessages.find(message->getKey()); @@ -809,10 +770,14 @@ void KnxHandler::run() { 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 + if (message->getCreateTime() <= definitionsSince // only newer defined + && (!message->isConditional() // unless conditional + || message->getAvailableSinceTime() <= definitionsSince)) { continue; } + logOtherDebug("knx", "checking association to %s %s", message->getCircuit().c_str(), + message->getName().c_str()); + bool isWrite = message->isWrite() && !message->isPassive(); // from KNX perspective ssize_t fieldCount = static_cast(message->getFieldCount()); if (isWrite && fieldCount > 1) { // impossible with more than one field @@ -896,7 +861,7 @@ void KnxHandler::run() { time(&lastTaskRun); } if (sendSignal) { - if (m_busHandler->hasSignal()) { + if (m_busHandler->getProtocol()->hasSignal()) { lastSignal = now; if (!signal || reconnected) { signal = true; @@ -919,18 +884,19 @@ void KnxHandler::run() { 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); + // limit number of read telegrams in order to give back control to outer loop for checking updates etc + for (int count = 0; count < 10; count++) { + // wait for telegram on first iteration only + result_t res = receiveTelegram(sizeof(data), &typ, data, &len, &src, &dest, count == 0); if (res != RESULT_OK) { if (res == RESULT_ERR_GENERIC_IO) { m_con->close(); } - } else { - needsWait = false; - handleReceivedTelegram(typ, src, dest, len, data); + break; } - } while (res == RESULT_OK); + needsWait = false; + handleReceivedTelegram(typ, src, dest, len, data); + } } if (!m_updatedMessages.empty()) { m_messages->lock(); @@ -938,21 +904,18 @@ void KnxHandler::run() { for (auto it = m_updatedMessages.begin(); it != m_updatedMessages.end(); ) { const vector* messages = m_messages->getByKey(it->first); if (!messages) { + it = m_updatedMessages.erase(it); continue; } for (const auto& message : *messages) { - if (message->getLastChangeTime() <= 0) { + time_t changeTime = message->getLastChangeTime(); + if (changeTime <= 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()) { @@ -971,7 +934,6 @@ void KnxHandler::run() { } it = m_updatedMessages.erase(it); } - time(&lastUpdates); } else { m_updatedMessages.clear(); } diff --git a/src/ebusd/knxhandler.h b/src/ebusd/knxhandler.h index 25f81cb52..26c23338d 100644 --- a/src/ebusd/knxhandler.h +++ b/src/ebusd/knxhandler.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2022 John Baier + * Copyright (C) 2022-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,7 @@ #include "lib/ebus/message.h" #include "lib/ebus/stringhelper.h" #include "lib/knx/knx.h" +#include "lib/utils/arg.h" namespace ebusd { @@ -41,10 +42,10 @@ using std::string; using std::vector; /** - * Helper function for getting the argp definition for KNX. - * @return a pointer to the argp_child structure. + * Helper function for getting the arg definition for KNX. + * @return a pointer to the child argument options, or nullptr. */ -const struct argp_child* knxhandler_getargs(); +const argParseChildOpt* knxhandler_getargs(); /** * Registration function that is called once during initialization. @@ -170,11 +171,12 @@ class KnxHandler : public DataSink, public DataSource, public WaitThread { * @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. + * @param wait true to wait up to 2 seconds for a new telegram, false to not wait. * @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); + knx_addr_t *dest, bool wait = true); /** * Handle a received KNX telegram. diff --git a/src/ebusd/main.cpp b/src/ebusd/main.cpp index 1a6b9e324..c5f449b05 100644 --- a/src/ebusd/main.cpp +++ b/src/ebusd/main.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,673 +23,55 @@ #include "ebusd/main.h" #include #include -#include +#include #include #include #include #include #include #include +#include "ebusd/bushandler.h" #include "ebusd/mainloop.h" +#include "ebusd/network.h" #include "lib/utils/log.h" #include "lib/utils/httpclient.h" - - -/** the version string of the program. */ -const char *argp_program_version = "" PACKAGE_STRING "." REVISION ""; - -/** the report bugs to address of the program. */ -const char *argp_program_bug_address = "" PACKAGE_BUGREPORT ""; +#include "lib/utils/tcpsocket.h" +#include "ebusd/scan.h" namespace ebusd { -using std::dec; -using std::hex; -using std::setfill; -using std::setw; -using std::nouppercase; using std::cout; -/** the path and name of the PID file. */ -#ifdef PACKAGE_PIDFILE -#define PID_FILE_NAME PACKAGE_PIDFILE -#else -#define PID_FILE_NAME "/var/run/ebusd.pid" -#endif - -/** the path and name of the log file. */ -#ifdef PACKAGE_LOGFILE -#define LOG_FILE_NAME PACKAGE_LOGFILE -#else -#define LOG_FILE_NAME "/var/log/ebusd.log" -#endif - -/** the config path part behind the scheme (scheme without "://"). */ -#define CONFIG_PATH_SUFFIX "://cfg.ebusd.eu/" - -/** the previous config path part to rewrite to the current one. */ -#define PREVIOUS_CONFIG_PATH_SUFFIX "://ebusd.eu/config/" - -/** the default path of the configuration files. */ -#ifdef HAVE_SSL -#define CONFIG_PATH "https" CONFIG_PATH_SUFFIX -#else -#define CONFIG_PATH "http" CONFIG_PATH_SUFFIX -#endif +/** the previous config path suffixes to rewrite to the current one. */ +#define PREVIOUS_CONFIG_PATH_SUFFIXES {"ebusd.eu/config/", "cfg.ebusd.eu/"} /** the opened PID file, or nullptr. */ static FILE* s_pidFile = nullptr; /** the program options. */ -static struct options s_opt = { - "/dev/ttyUSB0", // device - false, // noDeviceCheck - false, // readOnly - false, // initialSend - 0, // extraLatency - - false, // scanConfig - 0, // initialScan - getenv("LANG"), // preferLanguage - false, // checkConfig - OF_NONE, // dumpConfig - nullptr, // dumpConfigTo - 5, // pollInterval - false, // injectMessages - false, // stopAfterInject - nullptr, // caFile - nullptr, // caPath - - 0x31, // address - false, // answer - 10, // acquireTimeout - 3, // acquireRetries - 2, // sendRetries - SLAVE_RECV_TIMEOUT*5/3, // receiveTimeout - 0, // masterCount - false, // generateSyn - - "", // accessLevel - "", // aclFile - false, // foreground - false, // enableHex - false, // enableDefine - PID_FILE_NAME, // pidFile - 8888, // port - false, // localOnly - 0, // httpPort - "/var/" PACKAGE "/html", // htmlPath - true, // updateCheck - - PACKAGE_LOGFILE, // logFile - -1, // logAreas - ll_COUNT, // logLevel - false, // multiLog - - 0, // logRaw - PACKAGE_LOGFILE, // logRawFile - 100, // logRawSize - - false, // dump - "/tmp/" PACKAGE "_dump.bin", // dumpFile - 100, // dumpSize - false, // dumpFlush -}; +static struct options s_opt; /** the @a MessageMap instance, or nullptr. */ static MessageMap* s_messageMap = nullptr; -/** the @a MainLoop instance, or nullptr. */ -static MainLoop* s_mainLoop = nullptr; - -/** the (optionally corrected) config path for retrieving configuration files from. */ -static string s_configPath = CONFIG_PATH; - -/** the path prefix (including trailing "/") for retrieving configuration files from local files (empty for HTTPS). */ -static string s_configLocalPrefix = ""; - -/** the URI prefix (including trailing "/") for retrieving configuration files from HTTPS (empty for local files). */ -static string s_configUriPrefix = ""; - -/** the @a HttpClient for retrieving configuration files from HTTPS. */ -static HttpClient* s_configHttpClient = nullptr; - -/** the documentation of the program. */ -static const char argpdoc[] = - "A daemon for communication with eBUS heating systems."; - -#define O_INISND -2 -#define O_DEVLAT (O_INISND-1) -#define O_CFGLNG (O_DEVLAT-1) -#define O_CHKCFG (O_CFGLNG-1) -#define O_DMPCFG (O_CHKCFG-1) -#define O_DMPCTO (O_DMPCFG-1) -#define O_POLINT (O_DMPCTO-1) -#define O_CAFILE (O_POLINT-1) -#define O_CAPATH (O_CAFILE-1) -#define O_ANSWER (O_CAPATH-1) -#define O_ACQTIM (O_ANSWER-1) -#define O_ACQRET (O_ACQTIM-1) -#define O_SNDRET (O_ACQRET-1) -#define O_RCVTIM (O_SNDRET-1) -#define O_MASCNT (O_RCVTIM-1) -#define O_GENSYN (O_MASCNT-1) -#define O_ACLDEF (O_GENSYN-1) -#define O_ACLFIL (O_ACLDEF-1) -#define O_HEXCMD (O_ACLFIL-1) -#define O_DEFCMD (O_HEXCMD-1) -#define O_PIDFIL (O_DEFCMD-1) -#define O_LOCAL (O_PIDFIL-1) -#define O_HTTPPT (O_LOCAL-1) -#define O_HTMLPA (O_HTTPPT-1) -#define O_UPDCHK (O_HTMLPA-1) -#define O_LOG (O_UPDCHK-1) -#define O_LOGARE (O_LOG-1) -#define O_LOGLEV (O_LOGARE-1) -#define O_RAW (O_LOGLEV-1) -#define O_RAWFIL (O_RAW-1) -#define O_RAWSIZ (O_RAWFIL-1) -#define O_DMPFIL (O_RAWSIZ-1) -#define O_DMPSIZ (O_DMPFIL-1) -#define O_DMPFLU (O_DMPSIZ-1) - -/** the definition of the known program arguments. */ -static const struct argp_option argpoptions[] = { - {nullptr, 0, nullptr, 0, "Device options:", 1 }, - {"device", 'd', "DEV", 0, "Use DEV as eBUS device (" - "\"enh:DEVICE\" or \"enh:IP:PORT\" for enhanced device, " - "\"ens:DEVICE\" for enhanced high speed serial device, " - "\"DEVICE\" for serial device, or \"[udp:]IP:PORT\" for network device) [/dev/ttyUSB0]", 0 }, - {"nodevicecheck", 'n', nullptr, 0, "Skip serial eBUS device test", 0 }, - {"readonly", 'r', nullptr, 0, "Only read from device, never write to it", 0 }, - {"initsend", O_INISND, nullptr, 0, "Send an initial escape symbol after connecting device", 0 }, - {"latency", O_DEVLAT, "MSEC", 0, "Extra transfer latency in ms [0]", 0 }, +/** the @a ScanHelper instance, or nullptr. */ +static ScanHelper* s_scanHelper = nullptr; - {nullptr, 0, nullptr, 0, "Message configuration options:", 2 }, - {"configpath", 'c', "PATH", 0, "Read CSV config files from PATH (local folder or HTTPS URL) [" - CONFIG_PATH "]", 0 }, - {"scanconfig", 's', "ADDR", OPTION_ARG_OPTIONAL, "Pick CSV config files matching initial scan (ADDR=" - "\"none\" or empty for no initial scan message, \"full\" for full scan, or a single hex address to scan, " - "default is broadcast ident message). If combined with --checkconfig, you can add scan message data as " - "arguments for checking a particular scan configuration, e.g. \"FF08070400/0AB5454850303003277201\".", 0 }, - {"configlang", O_CFGLNG, "LANG", 0, - "Prefer LANG in multilingual configuration files [system default language]", 0 }, - {"checkconfig", O_CHKCFG, nullptr, 0, "Check config files, then stop", 0 }, - {"dumpconfig", O_DMPCFG, "FORMAT", OPTION_ARG_OPTIONAL, - "Check and dump config files in FORMAT (\"json\" or \"csv\"), then stop", 0 }, - {"dumpconfigto", O_DMPCTO, "FILE", 0, "Dump config files to FILE", 0 }, - {"pollinterval", O_POLINT, "SEC", 0, "Poll for data every SEC seconds (0=disable) [5]", 0 }, - {"inject", 'i', "stop", OPTION_ARG_OPTIONAL, "Inject remaining arguments as already seen messages (e.g. " - "\"FF08070400/0AB5454850303003277201\"), optionally stop afterwards", 0 }, -#ifdef HAVE_SSL - {"cafile", O_CAFILE, "FILE", 0, "Use CA FILE for checking certificates (uses defaults," - " \"#\" for insecure)", 0 }, - {"capath", O_CAPATH, "PATH", 0, "Use CA PATH for checking certificates (uses defaults)", 0 }, -#endif // HAVE_SSL - - {nullptr, 0, nullptr, 0, "eBUS options:", 3 }, - {"address", 'a', "ADDR", 0, "Use ADDR as own bus address [31]", 0 }, - {"answer", O_ANSWER, nullptr, 0, "Actively answer to requests from other masters", 0 }, - {"acquiretimeout", O_ACQTIM, "MSEC", 0, "Stop bus acquisition after MSEC ms [10]", 0 }, - {"acquireretries", O_ACQRET, "COUNT", 0, "Retry bus acquisition COUNT times [3]", 0 }, - {"sendretries", O_SNDRET, "COUNT", 0, "Repeat failed sends COUNT times [2]", 0 }, - {"receivetimeout", O_RCVTIM, "MSEC", 0, "Expect a slave to answer within MSEC ms [25]", 0 }, - {"numbermasters", O_MASCNT, "COUNT", 0, "Expect COUNT masters on the bus, 0 for auto detection [0]", 0 }, - {"generatesyn", O_GENSYN, nullptr, 0, "Enable AUTO-SYN symbol generation", 0 }, - - {nullptr, 0, nullptr, 0, "Daemon options:", 4 }, - {"accesslevel", O_ACLDEF, "LEVEL", 0, "Set default access level to LEVEL (\"*\" for everything) [\"\"]", 0 }, - {"aclfile", O_ACLFIL, "FILE", 0, "Read access control list from FILE", 0 }, - {"foreground", 'f', nullptr, 0, "Run in foreground", 0 }, - {"enablehex", O_HEXCMD, nullptr, 0, "Enable hex command", 0 }, - {"enabledefine", O_DEFCMD, nullptr, 0, "Enable define command", 0 }, - {"pidfile", O_PIDFIL, "FILE", 0, "PID file name (only for daemon) [" PID_FILE_NAME "]", 0 }, - {"port", 'p', "PORT", 0, "Listen for command line connections on PORT [8888]", 0 }, - {"localhost", O_LOCAL, nullptr, 0, "Listen for command line connections on 127.0.0.1 interface only", 0 }, - {"httpport", O_HTTPPT, "PORT", 0, "Listen for HTTP connections on PORT, 0 to disable [0]", 0 }, - {"htmlpath", O_HTMLPA, "PATH", 0, "Path for HTML files served by HTTP port [/var/ebusd/html]", 0 }, - {"updatecheck", O_UPDCHK, "MODE", 0, "Set automatic update check to MODE (on|off) [on]", 0 }, - - {nullptr, 0, nullptr, 0, "Log options:", 5 }, - {"logfile", 'l', "FILE", 0, "Write log to FILE (only for daemon, empty string for using syslog) [" - PACKAGE_LOGFILE "]", 0 }, - {"log", O_LOG, "AREAS:LEVEL", 0, "Only write log for matching AREA(S) below or equal to LEVEL" - " (alternative to --logareas/--logevel, may be used multiple times) [all:notice]", 0 }, - {"logareas", O_LOGARE, "AREAS", 0, "Only write log for matching AREA(S): main|network|bus|update|other" - "|all [all]", 0 }, - {"loglevel", O_LOGLEV, "LEVEL", 0, "Only write log below or equal to LEVEL: error|notice|info|debug" - " [notice]", 0 }, - - {nullptr, 0, nullptr, 0, "Raw logging options:", 6 }, - {"lograwdata", O_RAW, "bytes", OPTION_ARG_OPTIONAL, - "Log messages or all received/sent bytes on the bus", 0 }, - {"lograwdatafile", O_RAWFIL, "FILE", 0, "Write raw log to FILE [" PACKAGE_LOGFILE "]", 0 }, - {"lograwdatasize", O_RAWSIZ, "SIZE", 0, "Make raw log file no larger than SIZE kB [100]", 0 }, - - {nullptr, 0, nullptr, 0, "Binary dump options:", 7 }, - {"dump", 'D', nullptr, 0, "Enable binary dump of received bytes", 0 }, - {"dumpfile", O_DMPFIL, "FILE", 0, "Dump received bytes to FILE [/tmp/" PACKAGE "_dump.bin]", 0 }, - {"dumpsize", O_DMPSIZ, "SIZE", 0, "Make dump file no larger than SIZE kB [100]", 0 }, - {"dumpflush", O_DMPFLU, nullptr, 0, "Flush each byte", 0 }, +/** the @a ProtocolHandler instance, or nullptr. */ +static ProtocolHandler* s_protocol = nullptr; - {nullptr, 0, nullptr, 0, nullptr, 0 }, -}; +/** the @a BusHandler instance, or nullptr. */ +static BusHandler* s_busHandler = nullptr; -/** the global @a DataFieldTemplates. */ -static DataFieldTemplates s_globalTemplates; - -/** - * the loaded @a DataFieldTemplates by relative path (may also carry - * @a globalTemplates as replacement for missing file). - */ -static map s_templatesByPath; - -/** - * The program argument parsing function. - * @param key the key from @a argpoptions. - * @param arg the option argument, or nullptr. - * @param state the parsing state. - */ -error_t parse_opt(int key, char *arg, struct argp_state *state) { - struct options *opt = (struct options*)state->input; - result_t result = RESULT_OK; - unsigned int value; +/** the @a Request @a Queue instance, or nullptr. */ +static Queue* s_requestQueue = nullptr; - switch (key) { - // Device options: - case 'd': // --device=/dev/ttyUSB0 - if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid device"); - return EINVAL; - } - opt->device = arg; - break; - case 'n': // --nodevicecheck - opt->noDeviceCheck = true; - break; - case 'r': // --readonly - if (opt->answer || opt->generateSyn || opt->initialSend - || (opt->scanConfig && opt->initialScan != 0 && opt->initialScan != ESC)) { - argp_error(state, "cannot combine readonly with answer/generatesyn/initsend/scanconfig=*"); - return EINVAL; - } - opt->readOnly = true; - break; - case O_INISND: // --initsend - if (opt->readOnly) { - argp_error(state, "cannot combine readonly with answer/generatesyn/initsend/scanconfig=*"); - return EINVAL; - } - opt->initialSend = true; - break; - case O_DEVLAT: // --latency=10 - value = parseInt(arg, 10, 0, 200000, &result); // backwards compatible (micros) - if (result != RESULT_OK || (value <= 1000 && value > 200)) { // backwards compatible (micros) - argp_error(state, "invalid latency"); - return EINVAL; - } - opt->extraLatency = value > 1000 ? value/1000 : value; // backwards compatible (micros) - break; - - // Message configuration options: - case 'c': // --configpath=https://cfg.ebusd.eu/ - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid configpath"); - return EINVAL; - } - s_configPath = arg; - break; - case 's': // --scanconfig[=ADDR] (ADDR=|full|) - { - if (opt->pollInterval == 0) { - argp_error(state, "scanconfig without polling may lead to invalid files included for certain products!"); - return EINVAL; - } - symbol_t initialScan = ESC; - if (!arg || arg[0] == 0 || strcmp("none", arg) == 0) { - // no further setting needed - } else if (strcmp("full", arg) == 0) { - initialScan = SYN; - } else { - auto address = (symbol_t)parseInt(arg, 16, 0x00, 0xff, &result); - if (result != RESULT_OK || !isValidAddress(address)) { - argp_error(state, "invalid initial scan address"); - return EINVAL; - } - if (isMaster(address)) { - initialScan = getSlaveAddress(address); - } else { - initialScan = address; - } - } - if (opt->readOnly && initialScan != ESC) { - argp_error(state, "cannot combine readonly with answer/generatesyn/initsend/scanconfig=*"); - return EINVAL; - } - opt->scanConfig = true; - opt->initialScan = initialScan; - break; - } - case O_CFGLNG: // --configlang=LANG - opt->preferLanguage = arg; - break; - case O_CHKCFG: // --checkconfig - if (opt->injectMessages) { - argp_error(state, "invalid checkconfig"); - return EINVAL; - } - opt->checkConfig = true; - break; - case O_DMPCFG: // --dumpconfig[=json|csv] - if (opt->injectMessages) { - argp_error(state, "invalid checkconfig"); - return EINVAL; - } - if (!arg || arg[0] == 0 || strcmp("csv", arg) == 0) { - // no further flags - opt->dumpConfig = OF_DEFINITION; - } else if (strcmp("json", arg) == 0) { - opt->dumpConfig = OF_DEFINITION | OF_NAMES | OF_UNITS | OF_COMMENTS | OF_VALUENAME | OF_ALL_ATTRS | OF_JSON; - } else { - argp_error(state, "invalid dumpconfig"); - return EINVAL; - } - opt->checkConfig = true; - break; - case O_DMPCTO: // --dumpconfigto=FILE - if (!arg || arg[0] == 0) { - argp_error(state, "invalid dumpconfigto"); - return EINVAL; - } - opt->dumpConfigTo = arg; - break; - case O_POLINT: // --pollinterval=5 - value = parseInt(arg, 10, 0, 3600, &result); - if (result != RESULT_OK) { - argp_error(state, "invalid pollinterval"); - return EINVAL; - } - if (value == 0 && opt->scanConfig) { - argp_error(state, "scanconfig without polling may lead to invalid files included for certain products!"); - return EINVAL; - } - opt->pollInterval = value; - break; - case 'i': // --inject[=stop] - if (opt->injectMessages || opt->checkConfig) { - argp_error(state, "invalid inject"); - return EINVAL; - } - opt->injectMessages = true; - opt->stopAfterInject = arg && strcmp("stop", arg) == 0; - break; - case O_CAFILE: // --cafile=FILE - opt->caFile = arg; - break; - case O_CAPATH: // --capath=PATH - opt->caPath = arg; - break; - - // eBUS options: - case 'a': // --address=31 - { - auto address = (symbol_t)parseInt(arg, 16, 0, 0xff, &result); - if (result != RESULT_OK || !isMaster(address)) { - argp_error(state, "invalid address"); - return EINVAL; - } - opt->address = address; - break; - } - case O_ANSWER: // --answer - if (opt->readOnly) { - argp_error(state, "cannot combine readonly with answer/generatesyn/initsend/scanconfig=*"); - return EINVAL; - } - opt->answer = true; - break; - case O_ACQTIM: // --acquiretimeout=10 - value = parseInt(arg, 10, 1, 100000, &result); // backwards compatible (micros) - if (result != RESULT_OK || (value <= 1000 && value > 100)) { // backwards compatible (micros) - argp_error(state, "invalid acquiretimeout"); - return EINVAL; - } - opt->acquireTimeout = value > 1000 ? value/1000 : value; // backwards compatible (micros) - break; - case O_ACQRET: // --acquireretries=3 - value = parseInt(arg, 10, 0, 10, &result); - if (result != RESULT_OK) { - argp_error(state, "invalid acquireretries"); - return EINVAL; - } - opt->acquireRetries = value; - break; - case O_SNDRET: // --sendretries=2 - value = parseInt(arg, 10, 0, 10, &result); - if (result != RESULT_OK) { - argp_error(state, "invalid sendretries"); - return EINVAL; - } - opt->sendRetries = value; - break; - case O_RCVTIM: // --receivetimeout=25 - value = parseInt(arg, 10, 1, 100000, &result); // backwards compatible (micros) - if (result != RESULT_OK || (value <= 1000 && value > 100)) { // backwards compatible (micros) - argp_error(state, "invalid receivetimeout"); - return EINVAL; - } - opt->receiveTimeout = value > 1000 ? value/1000 : value; // backwards compatible (micros) - break; - case O_MASCNT: // --numbermasters=0 - value = parseInt(arg, 10, 0, 25, &result); - if (result != RESULT_OK) { - argp_error(state, "invalid numbermasters"); - return EINVAL; - } - opt->masterCount = value; - break; - case O_GENSYN: // --generatesyn - if (opt->readOnly) { - argp_error(state, "cannot combine readonly with answer/generatesyn/initsend/scanconfig=*"); - return EINVAL; - } - opt->generateSyn = true; - break; - - // Daemon options: - case O_ACLDEF: // --accesslevel=* - if (arg == nullptr) { - argp_error(state, "invalid accesslevel"); - return EINVAL; - } - opt->accessLevel = arg; - break; - case O_ACLFIL: // --aclfile=/etc/ebusd/acl - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid aclfile"); - return EINVAL; - } - opt->aclFile = arg; - break; - case 'f': // --foreground - opt->foreground = true; - break; - case O_HEXCMD: // --enablehex - opt->enableHex = true; - break; - case O_DEFCMD: // --enabledefine - opt->enableDefine = true; - break; - case O_PIDFIL: // --pidfile=/var/run/ebusd.pid - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid pidfile"); - return EINVAL; - } - opt->pidFile = arg; - break; - case 'p': // --port=8888 - value = parseInt(arg, 10, 1, 65535, &result); - if (result != RESULT_OK) { - argp_error(state, "invalid port"); - return EINVAL; - } - opt->port = (uint16_t)value; - break; - case O_LOCAL: // --localhost - opt->localOnly = true; - break; - case O_HTTPPT: // --httpport=0 - value = parseInt(arg, 10, 1, 65535, &result); - if (result != RESULT_OK) { - argp_error(state, "invalid httpport"); - return EINVAL; - } - opt->httpPort = (uint16_t)value; - break; - case O_HTMLPA: // --htmlpath=/var/ebusd/html - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid htmlpath"); - return EINVAL; - } - opt->htmlPath = arg; - break; - case O_UPDCHK: // --updatecheck=on - if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid updatecheck"); - return EINVAL; - } - if (strcmp("on", arg) == 0) { - opt->updateCheck = true; - } else if (strcmp("off", arg) == 0) { - opt->updateCheck = false; - } else { - argp_error(state, "invalid updatecheck"); - return EINVAL; - } - break; - - // Log options: - case 'l': // --logfile=/var/log/ebusd.log - if (arg == nullptr || strcmp("/", arg) == 0) { - argp_error(state, "invalid logfile"); - return EINVAL; - } - opt->logFile = arg; - break; - case O_LOG: // --log=area(s):level - { - char* pos = strchr(arg, ':'); - if (pos == nullptr) { - pos = strchr(arg, ' '); - if (pos == nullptr) { - argp_error(state, "invalid log"); - return EINVAL; - } - } - *pos = 0; - int facilities = parseLogFacilities(arg); - if (facilities == -1) { - argp_error(state, "invalid log: areas"); - return EINVAL; - } - LogLevel level = parseLogLevel(pos + 1); - if (level == ll_COUNT) { - argp_error(state, "invalid log: level"); - return EINVAL; - } - if (opt->logAreas != -1 || opt->logLevel != ll_COUNT) { - argp_error(state, "invalid log (combined with logareas or loglevel)"); - return EINVAL; - } - setFacilitiesLogLevel(facilities, level); - opt->multiLog = true; - break; - } - case O_LOGARE: // --logareas=all - { - int facilities = parseLogFacilities(arg); - if (facilities == -1) { - argp_error(state, "invalid logareas"); - return EINVAL; - } - if (opt->multiLog) { - argp_error(state, "invalid logareas (combined with log)"); - return EINVAL; - } - opt->logAreas = facilities; - break; - } - case O_LOGLEV: // --loglevel=notice - { - LogLevel logLevel = parseLogLevel(arg); - if (logLevel == ll_COUNT) { - argp_error(state, "invalid loglevel"); - return EINVAL; - } - if (opt->multiLog) { - argp_error(state, "invalid loglevel (combined with log)"); - return EINVAL; - } - opt->logLevel = logLevel; - break; - } - - // Raw logging options: - case O_RAW: // --lograwdata - opt->logRaw = arg && strcmp("bytes", arg) == 0 ? 2 : 1; - break; - case O_RAWFIL: // --lograwdatafile=/var/log/ebusd.log - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid lograwdatafile"); - return EINVAL; - } - opt->logRawFile = arg; - break; - case O_RAWSIZ: // --lograwdatasize=100 - value = parseInt(arg, 10, 1, 1000000, &result); - if (result != RESULT_OK) { - argp_error(state, "invalid lograwdatasize"); - return EINVAL; - } - opt->logRawSize = value; - break; - - - // Binary dump options: - case 'D': // --dump - opt->dump = true; - break; - case O_DMPFIL: // --dumpfile=/tmp/ebusd_dump.bin - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid dumpfile"); - return EINVAL; - } - opt->dumpFile = arg; - break; - case O_DMPSIZ: // --dumpsize=100 - value = parseInt(arg, 10, 1, 1000000, &result); - if (result != RESULT_OK) { - argp_error(state, "invalid dumpsize"); - return EINVAL; - } - opt->dumpSize = value; - break; - case O_DMPFLU: // --dumpflush - opt->dumpFlush = true; - break; +/** the @a MainLoop instance, or nullptr. */ +static MainLoop* s_mainLoop = nullptr; - case ARGP_KEY_ARG: - if (opt->injectMessages || (opt->checkConfig && opt->scanConfig)) { - return ARGP_ERR_UNKNOWN; - } - argp_error(state, "invalid arguments starting with \"%s\"", arg); - return EINVAL; - default: - return ARGP_ERR_UNKNOWN; - } +/** the @a Network instance, or nullptr. */ +static Network* s_network = nullptr; - return 0; -} void shutdown(bool error = false); void daemonize() { @@ -749,24 +131,37 @@ void daemonize() { * Clean up all dynamically allocated and stop main loop and all dependent components. */ void cleanup() { - if (s_mainLoop) { + if (s_network != nullptr) { + delete s_network; + s_network = nullptr; + } + if (s_mainLoop != nullptr) { delete s_mainLoop; s_mainLoop = nullptr; } - if (s_messageMap) { + if (s_requestQueue != nullptr) { + Request* msg; + while ((msg = s_requestQueue->pop()) != nullptr) { + delete msg; + } + delete s_requestQueue; + s_requestQueue = nullptr; + } + if (s_protocol != nullptr) { + delete s_protocol; + s_protocol = nullptr; + } + if (s_busHandler != nullptr) { + delete s_busHandler; + s_busHandler = nullptr; + } + if (s_messageMap != nullptr) { delete s_messageMap; s_messageMap = nullptr; } - // free templates - for (const auto& it : s_templatesByPath) { - if (it.second != &s_globalTemplates) { - delete it.second; - } - } - s_templatesByPath.clear(); - if (s_configHttpClient) { - delete s_configHttpClient; - s_configHttpClient = nullptr; + if (s_scanHelper != nullptr) { + delete s_scanHelper; + s_scanHelper = nullptr; } } @@ -830,618 +225,130 @@ void signalHandler(int sig) { } } -/** - * Lazy create the s_configHttpClient if not already done. - * @return true (always). - */ -bool lazyHttpClient() { - if (!s_configHttpClient) { - s_configHttpClient = new HttpClient(s_opt.caFile, s_opt.caPath); - } - return true; -} - -/** - * Collect configuration files matching the prefix and extension from the specified path. - * @param relPath the relative path from which to collect the files (without trailing "/"). - * @param prefix the filename prefix the files have to match, or empty. - * @param extension the filename extension the files have to match. - * @param files the @a vector to which to add the matching files. - * @param query the query string suffix for HTTPS retrieval starting with "&", or empty. - * @param dirs the @a vector to which to add found directories (without any name check), or nullptr to ignore. - * @param hasTemplates the bool to set when the templates file was found in the path, or nullptr to ignore. - * @return the result code. - */ -static result_t collectConfigFiles(const string& relPath, const string& prefix, const string& extension, - vector* files, - bool ignoreAddressPrefix = false, const string& query = "", - vector* dirs = nullptr, bool* hasTemplates = nullptr) { - const string relPathWithSlash = relPath.empty() ? "" : relPath + "/"; - if (!s_configUriPrefix.empty()) { - string uri = s_configUriPrefix + relPathWithSlash + "?t=" + extension.substr(1) + query; - string names; - if (!lazyHttpClient() || !s_configHttpClient->get(uri, "", &names)) { - return RESULT_ERR_NOTFOUND; - } - istringstream stream(names); - string name; - while (getline(stream, name)) { - if (name.empty()) { - continue; - } - if (name == "_templates"+extension) { - if (hasTemplates) { - *hasTemplates = true; - } - continue; - } - if (prefix.length() == 0 ? (!ignoreAddressPrefix || name.length() < 3 || name.find_first_of('.') != 2) - : (name.length() >= prefix.length() && name.substr(0, prefix.length()) == prefix)) { - files->push_back(relPathWithSlash + name); - } - } - return RESULT_OK; - } - const string path = s_configLocalPrefix + relPathWithSlash; - logDebug(lf_main, "reading directory %s", path.c_str()); - DIR* dir = opendir(path.c_str()); - if (dir == nullptr) { - return RESULT_ERR_NOTFOUND; - } - dirent* d; - while ((d = readdir(dir)) != nullptr) { - string name = d->d_name; - if (name == "." || name == "..") { - continue; - } - const string p = path + name; - struct stat stat_buf = {}; - if (stat(p.c_str(), &stat_buf) != 0) { - logError(lf_main, "unable to stat file %s", p.c_str()); - continue; - } - logDebug(lf_main, "file type of %s is %s", p.c_str(), - S_ISDIR(stat_buf.st_mode) ? "dir" : S_ISREG(stat_buf.st_mode) ? "file" : "other"); - if (S_ISDIR(stat_buf.st_mode)) { - if (dirs != nullptr) { - dirs->push_back(relPathWithSlash + name); - } - } else if (S_ISREG(stat_buf.st_mode) && name.length() >= extension.length() - && name.substr(name.length()-extension.length()) == extension) { - if (name == "_templates"+extension) { - if (hasTemplates) { - *hasTemplates = true; - } - continue; - } - if (prefix.length() == 0 ? (!ignoreAddressPrefix || name.length() < 3 || name.find_first_of('.') != 2) - : (name.length() >= prefix.length() && name.substr(0, prefix.length()) == prefix)) { - files->push_back(relPathWithSlash + name); - } - } - } - closedir(dir); - - return RESULT_OK; -} - -DataFieldTemplates* getTemplates(const string& filename) { - if (filename == "*") { - unsigned long maxLength = 0; - DataFieldTemplates* best = nullptr; - for (auto it : s_templatesByPath) { - if (it.first.size() > maxLength) { - best = it.second; - } - } - if (best) { - return best; - } - } else { - string path; - size_t pos = filename.find_last_of('/'); - if (pos != string::npos) { - path = filename.substr(0, pos); - } - const auto it = s_templatesByPath.find(path); - if (it != s_templatesByPath.end()) { - return it->second; - } - } - return &s_globalTemplates; -} - -/** - * Read the @a DataFieldTemplates for the specified path if necessary. - * @param relPath the relative path from which to read the files (without trailing "/"). - * @param extension the filename extension of the files to read. - * @param available whether the templates file is available in the path. - * @param verbose whether to verbosely log problems. - * @return false when the templates for the path were already loaded before, true when the templates for the path were added (independent from @a available). - * @return the @a DataFieldTemplates. - */ -static bool readTemplates(const string relPath, const string extension, bool available, bool verbose = false) { - const auto it = s_templatesByPath.find(relPath); - if (it != s_templatesByPath.end()) { - return false; - } - DataFieldTemplates* templates; - if (relPath.empty() || !available) { - templates = &s_globalTemplates; - } else { - templates = new DataFieldTemplates(s_globalTemplates); - } - s_templatesByPath[relPath] = templates; - if (!available) { - // global templates are stored as replacement in order to determine whether the directory was already loaded - return true; - } - string errorDescription; - string logPath = relPath.empty() ? "/" : relPath; - logInfo(lf_main, "reading templates %s", logPath.c_str()); - string file = (relPath.empty() ? "" : relPath + "/") + "_templates" + extension; - result_t result = loadDefinitionsFromConfigPath(templates, file, verbose, nullptr, &errorDescription, true); - if (result == RESULT_OK) { - logInfo(lf_main, "read templates in %s", logPath.c_str()); - return true; - } - logError(lf_main, "error reading templates in %s: %s, last error: %s", logPath.c_str(), getResultCode(result), - errorDescription.c_str()); - return false; -} - -/** - * Read the configuration files from the specified path. - * @param relPath the relative path from which to read the files (without trailing "/"). - * @param extension the filename extension of the files to read. - * @param messages the @a MessageMap to load the messages into. - * @param recursive whether to load all files recursively. - * @param verbose whether to verbosely log problems. - * @param errorDescription a string in which to store the error description in case of error. - * @return the result code. - */ -static result_t readConfigFiles(const string& relPath, const string& extension, bool recursive, - bool verbose, string* errorDescription, MessageMap* messages) { - vector files, dirs; - bool hasTemplates = false; - result_t result = collectConfigFiles(relPath, "", extension, &files, false, "", &dirs, &hasTemplates); - if (result != RESULT_OK) { - return result; - } - readTemplates(relPath, extension, hasTemplates, verbose); - for (const auto& name : files) { - logInfo(lf_main, "reading file %s", name.c_str()); - result = loadDefinitionsFromConfigPath(messages, name, verbose, nullptr, errorDescription); - if (result != RESULT_OK) { - return result; - } - logInfo(lf_main, "successfully read file %s", name.c_str()); - } - if (recursive) { - for (const auto& name : dirs) { - logInfo(lf_main, "reading dir %s", name.c_str()); - result = readConfigFiles(name, extension, true, verbose, errorDescription, messages); - if (result != RESULT_OK) { - return result; - } - logInfo(lf_main, "successfully read dir %s", name.c_str()); - } - } - return RESULT_OK; -} - -/** - * Helper method for immediate reading of a @a Message from the bus. - * @param message the @a Message to read. - */ -void readMessage(Message* message) { - if (!s_mainLoop || !message) { - return; - } - BusHandler* busHandler = s_mainLoop->getBusHandler(); - result_t result = busHandler->readFromBus(message, ""); - if (result != RESULT_OK) { - logError(lf_main, "error reading message %s %s: %s", message->getCircuit().c_str(), message->getName().c_str(), - getResultCode(result)); - } -} - -result_t executeInstructions(MessageMap* messages, bool verbose) { - string errorDescription; - result_t result = messages->resolveConditions(verbose, &errorDescription); - if (result != RESULT_OK) { - logError(lf_main, "error resolving conditions: %s, last error: %s", getResultCode(result), - errorDescription.c_str()); - } - ostringstream log; - result = messages->executeInstructions(readMessage, &log); - if (result != RESULT_OK) { - logError(lf_main, "error executing instructions: %s, last error: %s", getResultCode(result), - log.str().c_str()); - } else if (verbose && log.tellp() > 0) { - logInfo(lf_main, log.str().c_str()); - } - logNotice(lf_main, "found messages: %d (%d conditional on %d conditions, %d poll, %d update)", messages->size(), - messages->sizeConditional(), messages->sizeConditions(), messages->sizePoll(), messages->sizePassive()); - return result; -} - -result_t loadDefinitionsFromConfigPath(FileReader* reader, const string& filename, bool verbose, - map* defaults, string* errorDescription, bool replace) { - istream* stream = nullptr; - time_t mtime = 0; - if (s_configUriPrefix.empty()) { - stream = FileReader::openFile(s_configLocalPrefix + filename, errorDescription, &mtime); - } else { - string content; - if (lazyHttpClient() && s_configHttpClient->get(s_configUriPrefix + filename, "", &content, &mtime)) { - stream = new istringstream(content); - } - } - result_t result; - if (stream) { - result = reader->readFromStream(stream, filename, mtime, verbose, defaults, errorDescription, replace); - delete(stream); - } else { - result = RESULT_ERR_NOTFOUND; - } - return result; -} - -result_t loadConfigFiles(MessageMap* messages, bool verbose, bool denyRecursive) { - logInfo(lf_main, "loading configuration files from %s", s_configPath.c_str()); - messages->lock(); - messages->clear(); - s_globalTemplates.clear(); - for (auto& it : s_templatesByPath) { - if (it.second != &s_globalTemplates) { - delete it.second; - } - it.second = nullptr; - } - s_templatesByPath.clear(); - - string errorDescription; - result_t result = readConfigFiles("", ".csv", - (!s_opt.scanConfig || s_opt.checkConfig) && !denyRecursive, verbose, &errorDescription, messages); - if (result == RESULT_OK) { - logInfo(lf_main, "read config files, got %d messages", messages->size()); - } else { - logError(lf_main, "error reading config files from %s: %s, last error: %s", s_configPath.c_str(), - getResultCode(result), errorDescription.c_str()); - } - messages->unlock(); - return s_opt.checkConfig ? result : RESULT_OK; -} - -result_t loadScanConfigFile(MessageMap* messages, symbol_t address, bool verbose, string* relativeFile) { - Message* message = messages->getScanMessage(address); - if (!message || message->getLastUpdateTime() == 0) { - return RESULT_ERR_NOTFOUND; - } - const SlaveSymbolString& data = message->getLastSlaveData(); - if (data.getDataSize() < 1+5+2+2) { - logError(lf_main, "unable to load scan config %2.2x: slave part too short (%d)", address, data.getDataSize()); - return RESULT_EMPTY; - } - DataFieldSet* identFields = DataFieldSet::getIdentFields(); - string manufStr, addrStr, ident; // path: cfgpath/MANUFACTURER, prefix: ZZ., ident: C[C[C[C[C]]]], SW: xxxx, HW: xxxx - unsigned int sw = 0, hw = 0; - ostringstream out; - size_t offset = 0; - size_t field = 0; - bool fromLocal = s_configUriPrefix.empty(); - // manufacturer name - result_t result = (*identFields)[field]->read(data, offset, false, nullptr, -1, OF_NONE, -1, &out); - if (result == RESULT_ERR_NOTFOUND && fromLocal) { - result = (*identFields)[field]->read(data, offset, false, nullptr, -1, OF_NUMERIC, -1, &out); // manufacturer name - } - if (result == RESULT_OK) { - manufStr = out.str(); - transform(manufStr.begin(), manufStr.end(), manufStr.begin(), ::tolower); - out.str(""); - out << setw(2) << hex << setfill('0') << nouppercase << static_cast(address); - addrStr = out.str(); - out.str(""); - out.clear(); - offset += (*identFields)[field++]->getLength(pt_slaveData, MAX_LEN); - result = (*identFields)[field]->read(data, offset, false, nullptr, -1, OF_NONE, -1, &out); // identification string - } - if (result == RESULT_OK) { - ident = out.str(); - out.str(""); - out.clear(); - offset += (*identFields)[field++]->getLength(pt_slaveData, MAX_LEN); - result = (*identFields)[field]->read(data, offset, nullptr, -1, &sw); // software version number - if (result == RESULT_ERR_OUT_OF_RANGE) { - sw = (data.dataAt(offset) << 16) | data.dataAt(offset+1); // use hex value instead - result = RESULT_OK; - } - } - if (result == RESULT_OK) { - offset += (*identFields)[field++]->getLength(pt_slaveData, MAX_LEN); - result = (*identFields)[field]->read(data, offset, nullptr, -1, &hw); // hardware version number - if (result == RESULT_ERR_OUT_OF_RANGE) { - hw = (data.dataAt(offset) << 16) | data.dataAt(offset+1); // use hex value instead - result = RESULT_OK; - } - } - if (result != RESULT_OK) { - logError(lf_main, "unable to load scan config %2.2x: decode field %s %s", address, - identFields->getName(field).c_str(), getResultCode(result)); - return result; - } - bool hasTemplates = false; - string best; - map bestDefaults; - vector files; - auto it = ident.begin(); - while (it != ident.end()) { - if (*it != '_' && !::isalnum(*it)) { - it = ident.erase(it); - } else { - *it = static_cast(::tolower(*it)); - it++; - } - } - // find files matching MANUFACTURER/ZZ.*csv in cfgpath - string query; - if (!fromLocal) { - out << "&a=" << addrStr << "&i=" << ident << "&h=" << dec << static_cast(hw) << "&s=" << dec - << static_cast(sw); - query = out.str(); - out.str(""); - out.clear(); - } - result = collectConfigFiles(manufStr, addrStr + ".", ".csv", &files, false, query, nullptr, &hasTemplates); - if (result != RESULT_OK) { - logError(lf_main, "unable to load scan config %2.2x: list files in %s %s", address, manufStr.c_str(), - getResultCode(result)); - return result; - } - if (files.empty()) { - logError(lf_main, "unable to load scan config %2.2x: no file from %s with prefix %s found", address, - manufStr.c_str(), addrStr.c_str()); - return RESULT_ERR_NOTFOUND; - } - logDebug(lf_main, "found %d matching scan config files from %s with prefix %s: %s", files.size(), manufStr.c_str(), - addrStr.c_str(), getResultCode(result)); - // complete name: cfgpath/MANUFACTURER/ZZ[.C[C[C[C[C]]]]][.circuit][.suffix][.*][.SWxxxx][.HWxxxx][.*].csv - size_t bestMatch = 0; - for (const auto& name : files) { - symbol_t checkDest; - unsigned int checkSw, checkHw; - map defaults; - const string filename = name.substr(manufStr.length()+1); - if (!messages->extractDefaultsFromFilename(filename, &defaults, &checkDest, &checkSw, &checkHw)) { - continue; - } - if (address != checkDest || (checkSw != UINT_MAX && sw != checkSw) || (checkHw != UINT_MAX && hw != checkHw)) { - continue; - } - size_t match = 1; - string checkIdent = defaults["name"]; - if (!checkIdent.empty()) { - string remain = ident; - bool matches = false; - while (remain.length() > 0 && remain.length() >= checkIdent.length()) { - if (checkIdent == remain) { - matches = true; - break; - } - if (!::isdigit(remain[remain.length()-1])) { - break; - } - remain.erase(remain.length()-1); // remove trailing digit - } - if (!matches) { - continue; // IDENT mismatch - } - match += remain.length(); - } - if (match >= bestMatch) { - bestMatch = match; - best = name; - bestDefaults = defaults; - } - } - - if (best.empty()) { - logError(lf_main, - "unable to load scan config %2.2x: no file from %s with prefix %s matches ID \"%s\", SW%4.4d, HW%4.4d", - address, manufStr.c_str(), addrStr.c_str(), ident.c_str(), sw, hw); - return RESULT_ERR_NOTFOUND; - } - - // found the right file. load the templates if necessary, then load the file itself - bool readCommon = readTemplates(manufStr, ".csv", hasTemplates, s_opt.checkConfig); - if (readCommon) { - result = collectConfigFiles(manufStr, "", ".csv", &files, true, "&a=-"); - if (result == RESULT_OK && !files.empty()) { - for (const auto& name : files) { - string baseName = name.substr(manufStr.length()+1, name.length()-manufStr.length()-strlen(".csv")); // *. - if (baseName == "_templates.") { // skip templates - continue; - } - if (baseName.length() < 3 || baseName.find_first_of('.') != 2) { // different from the scheme "ZZ." - string errorDescription; - result = loadDefinitionsFromConfigPath(messages, name, verbose, nullptr, &errorDescription); - if (result == RESULT_OK) { - logNotice(lf_main, "read common config file %s", name.c_str()); - } else { - logError(lf_main, "error reading common config file %s: %s, %s", name.c_str(), getResultCode(result), - errorDescription.c_str()); - } - } - } - } - } - bestDefaults["name"] = ident; - string errorDescription; - result = loadDefinitionsFromConfigPath(messages, best, verbose, &bestDefaults, &errorDescription); - if (result != RESULT_OK) { - logError(lf_main, "error reading scan config file %s for ID \"%s\", SW%4.4d, HW%4.4d: %s, %s", best.c_str(), - ident.c_str(), sw, hw, getResultCode(result), errorDescription.c_str()); - return result; - } - logNotice(lf_main, "read scan config file %s for ID \"%s\", SW%4.4d, HW%4.4d", best.c_str(), ident.c_str(), sw, hw); - *relativeFile = best; - return RESULT_OK; -} - -/** - * Helper method for parsing a master/slave message pair from a command line argument. - * @param arg the argument to parse. - * @param onlyMasterSlave true to parse only a MS message, false to also parse MM and BC message. - * @param master the @a MasterSymbolString to parse into. - * @param slave the @a SlaveSymbolString to parse into. - * @return true when the argument was valid, false otherwise. - */ -bool parseMessage(const string& arg, bool onlyMasterSlave, MasterSymbolString* master, SlaveSymbolString* slave) { - size_t pos = arg.find_first_of('/'); - if (pos == string::npos) { - logError(lf_main, "invalid message %s: missing \"/\"", arg.c_str()); - return false; - } - result_t result = master->parseHex(arg.substr(0, pos)); - if (result == RESULT_OK) { - result = slave->parseHex(arg.substr(pos+1)); - } - if (result != RESULT_OK) { - logError(lf_main, "invalid message %s: %s", arg.c_str(), getResultCode(result)); - return false; - } - if (master->size() < 5) { // skip QQ ZZ PB SB NN - logError(lf_main, "invalid message %s: master part too short", arg.c_str()); - return false; - } - if (!isMaster((*master)[0])) { - logError(lf_main, "invalid message %s: QQ is no master", arg.c_str()); - return false; - } - if (!isValidAddress((*master)[1], !onlyMasterSlave) || (onlyMasterSlave && isMaster((*master)[1]))) { - logError(lf_main, "invalid message %s: ZZ is invalid", arg.c_str()); - return false; - } - return true; -} /** * Main function. * @param argc the number of command line arguments. * @param argv the command line arguments. + * @param envp the environment variables. * @return the exit code. */ -int main(int argc, char* argv[]) { - struct argp aargp = { argpoptions, parse_opt, nullptr, argpdoc, datahandler_getargs(), nullptr, nullptr }; - setenv("ARGP_HELP_FMT", "no-dup-args-note", 0); - - char envname[32] = "--"; // needs to cover at least max length of any option name plus "--" - char* envopt = envname+2; - for (char ** env = environ; *env; env++) { - char* pos = strchr(*env, '='); - if (!pos || strncmp(*env, "EBUSD_", sizeof("EBUSD_")-1) != 0) { - continue; - } - char* start = *env+sizeof("EBUSD_")-1; - size_t len = pos-start; - if (len <= 1 || len > sizeof(envname)-3) { // no single char long args - continue; - } - for (size_t i=0; i < len; i++) { - envopt[i] = static_cast(tolower(start[i])); - } - envopt[len] = 0; - if (strcmp(envopt, "version") == 0 || strcmp(envopt, "image") == 0 || strcmp(envopt, "arch") == 0 - || strcmp(envopt, "opts") == 0 || strcmp(envopt, "inject") == 0 - || strcmp(envopt, "checkconfig") == 0 || strncmp(envopt, "dumpconfig", 10) == 0 - ) { - // ignore those defined in Dockerfile, EBUSD_OPTS, those with final args, and interactive ones - continue; - } - char* envargv[] = {envname, pos+1}; - int cnt = pos[1] ? 2 : 1; - if (pos[1] && strlen(*env) < sizeof(envname)-3 - && (strcmp(envopt, "scanconfig") == 0 || strcmp(envopt, "lograwdata") == 0)) { - // only really special case: OPTION_ARG_OPTIONAL with non-empty arg needs to use "=" syntax - cnt = 1; - strcat(envopt, pos); - } - int idx = -1; - s_opt.injectMessages = true; // for skipping unknown values via ARGP_ERR_UNKNOWN - error_t err = argp_parse(&aargp, cnt, envargv, ARGP_PARSE_ARGV0|ARGP_SILENT|ARGP_IN_ORDER, &idx, &s_opt); - if (err != 0 && idx == -1) { // ignore args for non-arg boolean options - if (err == ESRCH) { // special value to abort immediately - logError(lf_main, "invalid argument in env: %s", envopt); - return EINVAL; - } - logError(lf_main, "invalid/unknown argument in env (ignored): %s", envopt); - } - s_opt.injectMessages = false; // restore (was not parsed from cmdline args yet) +int main(int argc, char* argv[], char* envp[]) { + switch (parse_main_args(argc, argv, envp, &s_opt)) { + case 0: // OK + break; + case '?': // help printed + return 0; + case 'V': + printf("" PACKAGE_STRING "." REVISION "\n"); + return 0; + default: + logWrite(lf_main, ll_error, "invalid arguments"); // force logging on exit + return EINVAL; } - int arg_index = -1; - if (argp_parse(&aargp, argc, argv, ARGP_IN_ORDER, &arg_index, &s_opt) != 0) { - logError(lf_main, "invalid arguments"); - return EINVAL; + if (s_opt.logAreas != -1 || s_opt.logLevel != ll_COUNT) { + setFacilitiesLogLevel(1 << lf_COUNT, ll_none); + setFacilitiesLogLevel(s_opt.logAreas, s_opt.logLevel); } - if (!s_configPath.empty() && s_configPath[s_configPath.length()-1] != '/') { - s_configPath += "/"; - } - if (s_configPath.find("://") == string::npos) { - s_configLocalPrefix = s_configPath; + const string lang = MappedFileReader::normalizeLanguage( + s_opt.preferLanguage == nullptr || !s_opt.preferLanguage[0] ? "" : s_opt.preferLanguage); + string configLocalPrefix, configUriPrefix, configLangQuery; +#ifdef HAVE_SSL + HttpClient::initialize(s_opt.caFile, s_opt.caPath); +#else // HAVE_SSL + HttpClient::initialize(nullptr, nullptr); +#endif // HAVE_SSL + HttpClient* configHttpClient = nullptr; + string configPath = s_opt.configPath; + if (configPath.find("://") == string::npos) { + configLocalPrefix = s_opt.configPath; } else { - if (!s_opt.scanConfig) { - logError(lf_main, "invalid configpath without scanconfig"); + if (!s_opt.scanConfig && s_opt.dumpConfig == OF_NONE) { + logWrite(lf_main, ll_error, "invalid configpath without scanconfig"); // force logging on exit return EINVAL; } - size_t pos = s_configPath.find(PREVIOUS_CONFIG_PATH_SUFFIX); - if (pos != string::npos) { - string newPath = s_configPath.substr(0, pos) + CONFIG_PATH_SUFFIX - + s_configPath.substr(pos+strlen(PREVIOUS_CONFIG_PATH_SUFFIX)); - logNotice(lf_main, "replaced old configPath %s with new one: %s", s_configPath.c_str(), newPath.c_str()); - s_configPath = newPath; + for (auto prevSuffix : PREVIOUS_CONFIG_PATH_SUFFIXES) { + size_t pos = configPath.find(prevSuffix); + if (pos == string::npos || (pos >= 3 && configPath.substr(pos-3, 3) != "://")) { + continue; + } + configPath = CONFIG_PATH; + logNotice(lf_main, "replaced old configPath %s with new one: %s", s_opt.configPath, configPath.c_str()); + break; } - uint16_t configPort = 80; + uint16_t configPort = 443; string proto, configHost; - if (!HttpClient::parseUrl(s_configPath, &proto, &configHost, &configPort, &s_configUriPrefix)) { + if (!HttpClient::parseUrl(configPath, &proto, &configHost, &configPort, &configUriPrefix)) { #ifndef HAVE_SSL - if (proto=="https") { - logError(lf_main, "invalid configPath URL (HTTPS not supported)"); + if (proto == "https") { + logWrite(lf_main, ll_error, "invalid configPath URL (HTTPS not supported)"); // force logging on exit return EINVAL; } -#endif - logError(lf_main, "invalid configPath URL"); +#endif // HAVE_SSL + logWrite(lf_main, ll_error, "invalid configPath URL"); // force logging on exit return EINVAL; } - if (!lazyHttpClient() - || !s_configHttpClient->connect(configHost, configPort, proto == "https", PACKAGE_NAME "/" PACKAGE_VERSION)) { - logError(lf_main, "invalid configPath URL (connect)"); + bool isCdn = configHost == CONFIG_HOST; + string suffix; + if (isCdn) { + suffix = (lang != "en" && lang != "tt" ? "de" : lang) + "/"; + configUriPrefix += suffix; + configPath += suffix; // only for informational purposes + } else { + configLangQuery = lang.empty() ? lang : "?l=" + lang; + } + configHttpClient = new HttpClient(); + if ( + !configHttpClient->connect(configHost, configPort, proto == "https", PACKAGE_NAME "/" PACKAGE_VERSION) + // if that did not work, issue a single retry with higher timeout: + && !configHttpClient->connect(configHost, configPort, proto == "https", PACKAGE_NAME "/" PACKAGE_VERSION, 8) + ) { + logWrite(lf_main, ll_error, "invalid configPath URL (connect)"); // force logging on exit + delete configHttpClient; cleanup(); return EINVAL; } - s_configHttpClient->disconnect(); - } - if (!s_opt.readOnly && s_opt.scanConfig && s_opt.initialScan == 0) { - s_opt.initialScan = BROADCAST; - } - if (s_opt.logAreas != -1 || s_opt.logLevel != ll_COUNT) { - setFacilitiesLogLevel(LF_ALL, ll_none); - setFacilitiesLogLevel(s_opt.logAreas, s_opt.logLevel); + if (isCdn) { + // check load balancing redirection + string redirPath; + uint16_t redirPort = 443; + string redirProto, redirHost, redirUriPrefix; + bool json = true; + if (configHttpClient->get("/redirect.json", "", &redirPath, nullptr, nullptr, &json) && !json + && HttpClient::parseUrl(redirPath, &redirProto, &redirHost, &redirPort, &redirUriPrefix) + && redirHost != configHost) { + configHttpClient->disconnect(); + ebusd::HttpClient* redirClient = new HttpClient(); + if (redirClient->connect(redirHost, redirPort, redirProto == "https", PACKAGE_NAME "/" PACKAGE_VERSION)) { + configUriPrefix = redirUriPrefix + suffix; + configPath = redirPath + suffix; // only for informational purposes + delete configHttpClient; + configHttpClient = redirClient; + logNotice(lf_main, "configPath URL redirected to %s", configPath.c_str()); + } + } + } + logInfo(lf_main, "configPath URL is valid"); + configHttpClient->disconnect(); } - s_messageMap = new MessageMap(s_opt.checkConfig); + s_messageMap = new MessageMap(s_opt.checkConfig, lang); + s_scanHelper = new ScanHelper(s_messageMap, configPath, configLocalPrefix, configUriPrefix, + configLangQuery, configHttpClient, s_opt.checkConfig); + s_messageMap->setResolver(s_scanHelper); if (s_opt.checkConfig) { logNotice(lf_main, PACKAGE_STRING "." REVISION " performing configuration check..."); - result_t result = loadConfigFiles(s_messageMap, true, s_opt.scanConfig && arg_index < argc); - result_t overallResult = executeInstructions(s_messageMap, true); + result_t result = s_scanHelper->loadConfigFiles(!s_opt.scanConfig || s_opt.injectCount <= 0); + result_t overallResult = s_scanHelper->executeInstructions(nullptr); MasterSymbolString master; SlaveSymbolString slave; + int arg_index = argc - s_opt.injectCount; while (result == RESULT_OK && s_opt.scanConfig && arg_index < argc) { // check scan config for each passed ident message - if (!parseMessage(argv[arg_index++], true, &master, &slave)) { + if (!s_scanHelper->parseMessage(argv[arg_index++], true, &master, &slave)) { continue; } symbol_t address = master[1]; @@ -1454,8 +361,8 @@ int main(int argc, char* argv[]) { } else { message->storeLastData(master, slave); string file; - result_t res = loadScanConfigFile(s_messageMap, address, true, &file); - result_t instrRes = executeInstructions(s_messageMap, true); + result_t res = s_scanHelper->loadScanConfigFile(address, &file); + result_t instrRes = s_scanHelper->executeInstructions(nullptr); if (res == RESULT_OK) { logInfo(lf_main, "scan config %2.2x: file %s loaded", address, file.c_str()); } else if (overallResult == RESULT_OK) { @@ -1485,28 +392,116 @@ int main(int argc, char* argv[]) { } else { logNotice(lf_main, "configuration dump:"); } - s_messageMap->dump(true, s_opt.dumpConfig, out); + if (s_opt.dumpConfig & OF_JSON) { + *out << "{\"datatypes\":["; + DataTypeList::getInstance()->dump(s_opt.dumpConfig, out); + *out << "],\"templates\":["; + const auto tmpl = s_scanHelper->getTemplates(""); + tmpl->dump(s_opt.dumpConfig, out); + *out << "],\"messages\":"; + s_messageMap->dump(true, s_opt.dumpConfig, out); + *out << "}"; + } else { + s_messageMap->dump(true, s_opt.dumpConfig, out); + } if (fout.is_open()) { fout.close(); } } - + if (overallResult == RESULT_OK && s_messageMap->getMaxIdLength() > 7) { + logNotice(lf_main, "max message ID length exceeded"); + overallResult = RESULT_CONTINUE; + } cleanup(); return overallResult == RESULT_OK ? EXIT_SUCCESS : EXIT_FAILURE; } - // open the device - Device *device = Device::create(s_opt.device, s_opt.extraLatency, !s_opt.noDeviceCheck, s_opt.readOnly, - s_opt.initialSend); - if (device == nullptr) { - logError(lf_main, "unable to create device %s", s_opt.device); + const char* device = s_opt.device; + if (!s_opt.checkConfig && strncmp(device, "mdns:", 5) == 0) { + // auto discovery + mdns_oneshot_t address; + #define MAX_ADDRESSES 10 + mdns_oneshot_t addresses[MAX_ADDRESSES]; + size_t otherCount = MAX_ADDRESSES; + int ret = 0; + #define MAX_MDNS_RUNS 3 + for (int i=0; i < MAX_MDNS_RUNS && ret == 0; i++) { // 3*up to 5 seconds = 15 seconds max + logWrite(lf_main, ll_notice, "discovering device from \"%s\", try %d/%d", device, i+1, MAX_MDNS_RUNS); + ret = resolveMdnsOneShot(device+5, &address, addresses, &otherCount); + } + if (ret < 0) { + logWrite(lf_main, ll_error, "unable to discover device \"%s\", error %d", device, ret); + cleanup(); + return EINVAL; + } + const char *ip; + for (size_t pos=0; pos < otherCount; pos++) { + mdns_oneshot_t *addr = addresses+pos; + ip = inet_ntoa(addr->address); + logWrite(lf_main, ll_info, "discovered another device with ID %s and device string %s:%s", + addr->id, addr->proto, ip); + } + if (ret == 0) { + logWrite(lf_main, ll_error, "unable to discover device \"%s\", %s found", device, otherCount ? "ID not" : "none"); + cleanup(); + return EINVAL; + } + if (ret > 1) { + ip = inet_ntoa(address.address); + logWrite(lf_main, ll_info, "discovered another device with ID %s and device string %s:%s", + address.id, address.proto, ip); + logWrite(lf_main, ll_error, + "found several devices from \"%s\". use e.g. \"--device=mdns:%s\" to limit it to the desired one", device, + address.id); + cleanup(); + return EINVAL; + } + ip = inet_ntoa(address.address); + char *mdnsDevice = reinterpret_cast(malloc(4*4+3+1)); // ens:xxx.xxx.xxx.xxx + snprintf(mdnsDevice, 4*4+3+1, "%s:%s", address.proto, ip); + device = mdnsDevice; + logWrite(lf_main, ll_notice, "using discovered device with ID %s and device string %s", address.id, device); + } + + s_busHandler = new BusHandler(s_messageMap, s_scanHelper, s_opt.pollInterval); + + // create the protocol and open the device + ebus_protocol_config_t config = { + .device = device, + .noDeviceCheck = s_opt.noDeviceCheck, + .readOnly = s_opt.readOnly, + .extraLatency = s_opt.extraLatency, + .ownAddress = s_opt.address, + .answer = s_opt.answer, + .busLostRetries = s_opt.acquireRetries, + .failedSendRetries = s_opt.sendRetries, + .busAcquireTimeout = s_opt.acquireTimeout, + .slaveRecvTimeout = s_opt.receiveTimeout, + .lockCount = s_opt.masterCount, + .generateSyn = s_opt.generateSyn, + .initialSend = s_opt.initialSend, + }; + s_protocol = ProtocolHandler::create(config, s_busHandler); + if (s_protocol == nullptr) { + logWrite(lf_main, ll_error, "unable to create protocol/device %s", config.device); // force logging on exit cleanup(); return EINVAL; } + s_busHandler->setProtocol(s_protocol); + if (s_opt.answer) { + istringstream input; + input.str("ebusd.eu;" PACKAGE_NAME ";" SCAN_VERSION ";100"); + Message* message = s_messageMap->getScanMessage(); + SlaveSymbolString response; + if (message && message->prepareSlave(&input, &response) == RESULT_OK) { + s_protocol->setAnswer(SYN, s_protocol->getOwnSlaveAddress(), message->getPrimaryCommand(), + message->getSecondaryCommand(), nullptr, 0, response); + } + } if (!s_opt.foreground) { if (!setLogFile(s_opt.logFile)) { - logError(lf_main, "unable to open log file %s", s_opt.logFile); + logWrite(lf_main, ll_error, "unable to open log file %s", s_opt.logFile); // force logging on exit cleanup(); return EINVAL; } @@ -1518,37 +513,100 @@ int main(int argc, char* argv[]) { signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); - logNotice(lf_main, PACKAGE_STRING "." REVISION " started%s%s on%s device %s", - device->isReadOnly() ? " read only" : "", + if (s_opt.dumpFile[0]) { + s_protocol->setDumpFile(s_opt.dumpFile, s_opt.dumpSize, s_opt.dumpFlush); + if (s_opt.dump) { + s_protocol->toggleDump(); + } + } + if (s_opt.logRawFile[0] && strcmp(s_opt.logRawFile, s_opt.logFile) != 0) { + s_protocol->setLogRawFile(s_opt.logRawFile, s_opt.logRawSize); + } + if (s_opt.logRaw != 0) { + s_protocol->toggleLogRaw(s_opt.logRaw == 2); + } + + // open Device + s_protocol->open(); + + // create the MainLoop + s_requestQueue = new Queue(); + s_mainLoop = new MainLoop(s_opt, s_busHandler, s_messageMap, s_scanHelper, s_requestQueue); + + ostringstream ostream; + s_protocol->formatInfo(&ostream, false, true); + string deviceInfoStr = ostream.str(); + logNotice(lf_main, PACKAGE_STRING "." REVISION " started%s on device: %s", s_opt.scanConfig ? s_opt.initialScan == ESC ? " with auto scan" : s_opt.initialScan == BROADCAST ? " with broadcast scan" : s_opt.initialScan == SYN ? " with full scan" : " with single scan" : "", - device->isEnhancedProto() ? " enhanced" : "", - device->getName()); + deviceInfoStr.c_str()); // load configuration files - loadConfigFiles(s_messageMap); - - // create the MainLoop and start it - s_mainLoop = new MainLoop(s_opt, device, s_messageMap); - if (s_opt.injectMessages) { - BusHandler* busHandler = s_mainLoop->getBusHandler(); - while (arg_index < argc) { - // add each passed message + s_scanHelper->loadConfigFiles(!s_opt.scanConfig); + + // start the MainLoop + if (s_opt.injectCommands) { + int scanAdrCount = 0; + bool scanAddresses[256] = {}; + for (int arg_index = argc - s_opt.injectCount; arg_index < argc; arg_index++) { + string arg = argv[arg_index]; + if (arg.empty()) { + continue; + } + if (arg.find_first_of(' ') != string::npos || arg.find_first_of('/') == string::npos) { + RequestImpl req(false); + req.add(argv[arg_index]); + bool connected = true; + RequestMode reqMode = {}; + string user; + bool reload = false; + ostringstream ostream; + result_t ret = s_mainLoop->decodeRequest(&req, &connected, &reqMode, &user, &reload, &ostream); + string output = ostream.str(); + if (ret != RESULT_OK || output.substr(0, 3) == "ERR" || output.substr(0, 5) == "usage") { + if (output.empty()) { + output = getResultCode(ret); + } + logError(lf_main, "executing command \"%s\" failed: %s", argv[arg_index], output.c_str()); + } else if (s_opt.stopAfterInject) { + logNotice(lf_main, "executed command \"%s\": %s", argv[arg_index], output.c_str()); + } + continue; + } + // old style inject message MasterSymbolString master; SlaveSymbolString slave; - if (!parseMessage(argv[arg_index++], false, &master, &slave)) { + if (!s_scanHelper->parseMessage(arg, false, &master, &slave)) { continue; } - busHandler->injectMessage(master, slave); + s_protocol->injectMessage(master, slave); + if (s_opt.scanConfig && master.size() >= 5 && master[4] == 0 && master[2] == 0x07 && master[3] == 0x04 + && isValidAddress(master[1], false) && !isMaster(master[1]) && !scanAddresses[master[1]]) { + // scan message, simulate scanning + scanAddresses[master[1]] = true; + scanAdrCount++; + } + } + s_protocol->start("bushandler"); + for (symbol_t address = 0; scanAdrCount > 0; address++) { + if (scanAddresses[address]) { + scanAdrCount--; + s_busHandler->scanAndWait(address, true); + } } if (s_opt.stopAfterInject) { shutdown(); return 0; } + } else { + s_protocol->start("bushandler"); } s_mainLoop->start("mainloop"); + s_network = new Network(s_opt.localOnly, s_opt.port, s_opt.httpPort, s_requestQueue); + s_network->start("network"); + // wait for end of MainLoop s_mainLoop->join(); @@ -1559,6 +617,6 @@ int main(int argc, char* argv[]) { } // namespace ebusd -int main(int argc, char* argv[]) { - return ebusd::main(argc, argv); +int main(int argc, char* argv[], char* envp[]) { + return ebusd::main(argc, argv, envp); } diff --git a/src/ebusd/main.h b/src/ebusd/main.h old mode 100644 new mode 100755 index c83e66600..1060befe3 --- a/src/ebusd/main.h +++ b/src/ebusd/main.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,7 +23,6 @@ #include #include #include "lib/ebus/data.h" -#include "lib/ebus/message.h" #include "lib/ebus/result.h" #include "lib/utils/log.h" @@ -33,28 +32,44 @@ namespace ebusd { * The main entry method doing all the startup handling. */ +/** the default host of the configuration files. */ +#define CONFIG_HOST "ebus.github.io" + +/** the default location of the configuration files (without language suffix). */ +#define CONFIG_PATH "https://" CONFIG_HOST "/" + /** A structure holding all program options. */ -struct options { - const char* device; //!< eBUS device (serial device or [udp:]ip:port) [/dev/ttyUSB0] +typedef struct options { + const char* device; //!< eBUS device (serial device or mdns:[id] or [udp:]ip[:port]) [mdns:] bool noDeviceCheck; //!< skip serial eBUS device test bool readOnly; //!< read-only access to the device bool initialSend; //!< send an initial escape symbol after connecting device unsigned int extraLatency; //!< extra transfer latency in ms [0 for USB, 10 for IP] + const char* configPath; //!< the configuration files path or URL + bool scanConfigOrPathSet; //!< whether scanConfig or configPath were set by arguments. bool scanConfig; //!< pick configuration files matching initial scan - /** the initial address to scan for scanconfig - * (@a ESC=none, 0xfe=broadcast ident, @a SYN=full scan, else: single slave address). */ + /** + * initial address(es) to scan: + * @a ESC=none (no explicit active scanning), + * 0xfe=broadcast ident, + * @a SYN=full scan (all slave addresses), + * else: single slave address. + */ symbol_t initialScan; + int scanRetries; //!< number of retries for scanning devices [10] const char* preferLanguage; //!< preferred language in configuration files bool checkConfig; //!< check config files, then stop OutputFormat dumpConfig; //!< dump config files, then stop const char* dumpConfigTo; //!< file to dump config to unsigned int pollInterval; //!< poll interval in seconds, 0 to disable [5] - bool injectMessages; //!< inject remaining arguments as already seen messages - bool stopAfterInject; //!< only inject messages once, then stop + bool injectCommands; //!< inject remaining arguments as commands or already seen messages + bool stopAfterInject; //!< only inject arguments once, then stop + int injectCount; //!< number of arguments to inject, or 0 +#ifdef HAVE_SSL const char* caFile; //!< the CA file to use (uses defaults if neither caFile nor caPath are set), or "#" for insecure const char* caPath; //!< the path with CA files to use (uses defaults if neither caFile nor caPath are set) - +#endif // HAVE_SSL symbol_t address; //!< own bus address [31] bool answer; //!< answer to requests from other masters unsigned int acquireTimeout; //!< bus acquisition timeout in ms [10] @@ -67,7 +82,7 @@ struct options { const char* accessLevel; //!< default access level const char* aclFile; //!< ACL file name bool foreground; //!< run in foreground - bool enableHex; //!< enable hex command + bool enableHex; //!< enable hex/inject/answer commands bool enableDefine; //!< enable define command const char* pidFile; //!< PID file name [/var/run/ebusd.pid] uint16_t port; //!< port to listen for command line connections [8888] @@ -89,57 +104,18 @@ struct options { const char* dumpFile; //!< name of dump file [/tmp/ebusd_dump.bin] unsigned int dumpSize; //!< maximum size of dump file in kB [100] bool dumpFlush; //!< flush each byte -}; - -/** - * Get the @a DataFieldTemplates for the specified configuration file. - * @param filename the full name of the configuration file, or "*" to get the non-root templates with the longest name - * or the root templates if not available. - * @return the @a DataFieldTemplates. - */ -DataFieldTemplates* getTemplates(const string& filename); - -/** - * Load the message definitions from configuration files. - * @param messages the @a MessageMap to load the messages into. - * @param verbose whether to verbosely log problems. - * @param denyRecursive whether to avoid loading all files recursively (e.g. for scan config check). - * @return the result code. - */ -result_t loadConfigFiles(MessageMap* messages, bool verbose = false, bool denyRecursive = false); - -/** - * Load the message definitions from a configuration file matching the scan result. - * @param messages the @a MessageMap to load the messages into. - * @param address the address of the scan participant - * (either master for broadcast master data or slave for read slave data). - * @param data the scan @a SlaveSymbolString for which to load the configuration file. - * @param verbose whether to verbosely log problems. - * @param relativeFile the string in which the name of the configuration file is stored on success. - * @return the result code. - */ -result_t loadScanConfigFile(MessageMap* messages, symbol_t address, bool verbose, string* relativeFile); - -/** - * Helper method for executing all loaded and resolvable instructions. - * @param messages the @a MessageMap instance. - * @param verbose whether to verbosely log all problems. - * @return the result code. - */ -result_t executeInstructions(MessageMap* messages, bool verbose = false); +} options_t; /** - * Helper method for loading definitions from a relative file from the config path/URL. - * @param reader the @a FileReader instance to load with the definitions. - * @param filename the relative name of the file being read. - * @param verbose whether to verbosely log problems. - * @param defaults the default values by name (potentially overwritten by file name), or nullptr to not use defaults. - * @param errorDescription a string in which to store the error description in case of error. - * @param replace whether to replace an already existing entry. - * @return @a RESULT_OK on success, or an error code. + * Parse the main command line arguments in @a argv. + * @param argc the number of command line arguments. + * @param argv the command line arguments. + * @param envp the environment variables to parse before the args, or nullptr. + * @param opt pointer to the parsed arguments (will be initialized to defaults first). + * @return 0 on success, '!' for an invalid argument value, ':' for a missing argument value, + * '?' when "-?" was given, or the result of the parse function if non-zero. */ -result_t loadDefinitionsFromConfigPath(FileReader* reader, const string& filename, bool verbose, - map* defaults, string* errorDescription, bool replace = false); +int parse_main_args(int argc, char* argv[], char* envp[], options_t* opt); } // namespace ebusd diff --git a/src/ebusd/main_args.cpp b/src/ebusd/main_args.cpp new file mode 100755 index 000000000..baca6a2d9 --- /dev/null +++ b/src/ebusd/main_args.cpp @@ -0,0 +1,696 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "ebusd/main.h" +#include +#include "ebusd/datahandler.h" +#include "lib/utils/log.h" +#include "lib/utils/arg.h" +#include "ebusd/scan.h" + +namespace ebusd { + +/** the default program options. */ +static const options_t s_default_opt = { + .device = "mdns:", + .noDeviceCheck = false, + .readOnly = false, + .initialSend = false, + .extraLatency = 0, + + .configPath = nullptr, + .scanConfigOrPathSet = false, + .scanConfig = false, + .initialScan = 0, + .scanRetries = 5, + .preferLanguage = getenv("LANG"), + .checkConfig = false, + .dumpConfig = OF_NONE, + .dumpConfigTo = nullptr, + .pollInterval = 5, + .injectCommands = false, + .stopAfterInject = false, + .injectCount = 0, +#ifdef HAVE_SSL + .caFile = nullptr, + .caPath = nullptr, +#endif // HAVE_SSL + .address = 0x31, + .answer = false, + .acquireTimeout = 10, + .acquireRetries = 3, + .sendRetries = 2, + .receiveTimeout = SLAVE_RECV_TIMEOUT*5/3, + .masterCount = 0, + .generateSyn = false, + + .accessLevel = "", + .aclFile = "", + .foreground = false, + .enableHex = false, + .enableDefine = false, + .pidFile = PACKAGE_PIDFILE, + .port = 8888, + .localOnly = false, + .httpPort = 0, + .htmlPath = "/var/" PACKAGE "/html", + .updateCheck = true, + + .logFile = PACKAGE_LOGFILE, + .logAreas = -1, + .logLevel = ll_COUNT, + .multiLog = false, + + .logRaw = 0, + .logRawFile = PACKAGE_LOGFILE, + .logRawSize = 100, + + .dump = false, + .dumpFile = "/tmp/" PACKAGE "_dump.bin", + .dumpSize = 100, + .dumpFlush = false +}; + +/** the (optionally corrected) config path for retrieving configuration files from. */ +static string s_configPath = CONFIG_PATH; + +#define O_INISND -2 +#define O_DEVLAT (O_INISND-1) +#define O_SCNRET (O_DEVLAT-1) +#define O_CFGLNG (O_SCNRET-1) +#define O_CHKCFG (O_CFGLNG-1) +#define O_DMPCFG (O_CHKCFG-1) +#define O_DMPCTO (O_DMPCFG-1) +#define O_POLINT (O_DMPCTO-1) +#define O_CAFILE (O_POLINT-1) +#define O_CAPATH (O_CAFILE-1) +#define O_ANSWER (O_CAPATH-1) +#define O_ACQTIM (O_ANSWER-1) +#define O_ACQRET (O_ACQTIM-1) +#define O_SNDRET (O_ACQRET-1) +#define O_RCVTIM (O_SNDRET-1) +#define O_MASCNT (O_RCVTIM-1) +#define O_GENSYN (O_MASCNT-1) +#define O_ACLDEF (O_GENSYN-1) +#define O_ACLFIL (O_ACLDEF-1) +#define O_HEXCMD (O_ACLFIL-1) +#define O_DEFCMD (O_HEXCMD-1) +#define O_PIDFIL (O_DEFCMD-1) +#define O_LOCAL (O_PIDFIL-1) +#define O_HTTPPT (O_LOCAL-1) +#define O_HTMLPA (O_HTTPPT-1) +#define O_UPDCHK (O_HTMLPA-1) +#define O_LOG (O_UPDCHK-1) +#define O_LOGARE (O_LOG-1) +#define O_LOGLEV (O_LOGARE-1) +#define O_RAW (O_LOGLEV-1) +#define O_RAWFIL (O_RAW-1) +#define O_RAWSIZ (O_RAWFIL-1) +#define O_DMPFIL (O_RAWSIZ-1) +#define O_DMPSIZ (O_DMPFIL-1) +#define O_DMPFLU (O_DMPSIZ-1) +#define O_INJPOS 0x100 + +#define ARG_NO_ENV (af_max << 1) + +/** the definition of the known program arguments. */ +static const argDef argDefs[] = { + {nullptr, 0, nullptr, 0, "Device options:"}, + {"device", 'd', "DEV", 0, "Use DEV as eBUS device [mdns:]\n" + "- \"mdns:\" for auto discovery via mDNS with optional suffix \"[ID][@INTF]\" for using a specific" + " hardware ID and/or IP interface INTF for the discovery (only for eBUS Adapter Shield;" + " on docker, the network device needs to support multicast routing e.g. like the host network), or\n" + "- prefix \"ens:\" for enhanced high speed device,\n" + "- prefix \"enh:\" for enhanced device, or\n" + "- no prefix for plain device, and\n" + "- suffix \"IP[:PORT]\" for network device, or\n" + "- suffix \"DEVICE\" for serial device"}, + {"nodevicecheck", 'n', nullptr, 0, "Skip serial eBUS device test"}, + {"readonly", 'r', nullptr, 0, "Only read from device, never write to it"}, + {"initsend", O_INISND, nullptr, 0, "Send an initial escape symbol after connecting device"}, + {"latency", O_DEVLAT, "MSEC", 0, "Extra transfer latency in ms [0]"}, + + {nullptr, 0, nullptr, 0, "Message configuration options:"}, + {"configpath", 'c', "PATH", 0, "Read CSV config files from PATH (local folder or HTTPS URL) [" + CONFIG_PATH "]"}, + {"scanconfig", 's', "ADDR", af_optional, "Pick CSV config files matching initial scan ADDR: " + "empty for broadcast ident message (default when neither configpath nor dumpconfig is given), " + "\"none\" for no initial scan message, " + "\"full\" for full scan, " + "a single hex address to scan, or " + "\"off\" for not picking CSV files by scan result (default when configpath is given).\n" + "If combined with --checkconfig and --inject, you can add scan message data as " + "arguments for checking a particular scan configuration, e.g. \"FF08070400/0AB5454850303003277201\"."}, + {"scanretries", O_SCNRET, "COUNT", 0, "Retry scanning devices COUNT times [5]"}, + {"configlang", O_CFGLNG, "LANG", 0, + "Prefer LANG in multilingual configuration files [system default language, DE as fallback]"}, + {"checkconfig", O_CHKCFG, nullptr, ARG_NO_ENV, "Check config files, then stop"}, + {"dumpconfig", O_DMPCFG, "FORMAT", af_optional|ARG_NO_ENV, + "Check and dump config files in FORMAT (\"json\", \"csv\", or \"csvall\" for CSV with all attributes), then stop"}, + {"dumpconfigto", O_DMPCTO, "FILE", 0, "Dump config files to FILE"}, + {"pollinterval", O_POLINT, "SEC", 0, "Poll for data every SEC seconds (0=disable) [5]"}, + {"inject", 'i', "stop", af_optional|ARG_NO_ENV, "Inject remaining arguments as commands or already seen messages " + "(e.g. \"FF08070400/0AB5454850303003277201\"), optionally stop afterwards"}, + {nullptr, O_INJPOS, "INJECT", af_optional|af_multiple, "Commands and/or messages to inject " + "(if --inject was given)"}, +#ifdef HAVE_SSL + {"cafile", O_CAFILE, "FILE", 0, "Use CA FILE for checking certificates (uses defaults," + " \"#\" for insecure)"}, + {"capath", O_CAPATH, "PATH", 0, "Use CA PATH for checking certificates (uses defaults)"}, +#endif // HAVE_SSL + + {nullptr, 0, nullptr, 0, "eBUS options:"}, + {"address", 'a', "ADDR", 0, "Use hex ADDR as own master bus address [31]"}, + {"answer", O_ANSWER, nullptr, 0, "Actively answer to requests from other masters"}, + {"acquiretimeout", O_ACQTIM, "MSEC", 0, "Stop bus acquisition after MSEC ms [10]"}, + {"acquireretries", O_ACQRET, "COUNT", 0, "Retry bus acquisition COUNT times [3]"}, + {"sendretries", O_SNDRET, "COUNT", 0, "Repeat failed sends COUNT times [2]"}, + {"receivetimeout", O_RCVTIM, "MSEC", 0, "Expect a slave to answer within MSEC ms [25]"}, + {"numbermasters", O_MASCNT, "COUNT", 0, "Expect COUNT masters on the bus, 0 for auto detection [0]"}, + {"generatesyn", O_GENSYN, nullptr, 0, "Enable AUTO-SYN symbol generation"}, + + {nullptr, 0, nullptr, 0, "Daemon options:"}, + {"accesslevel", O_ACLDEF, "LEVEL", 0, "Set default access level to LEVEL (\"*\" for everything) [\"\"]"}, + {"aclfile", O_ACLFIL, "FILE", 0, "Read access control list from FILE"}, + {"foreground", 'f', nullptr, 0, "Run in foreground"}, + {"enablehex", O_HEXCMD, nullptr, 0, "Enable hex/inject/answer commands"}, + {"enabledefine", O_DEFCMD, nullptr, 0, "Enable define command"}, + {"pidfile", O_PIDFIL, "FILE", 0, "PID file name (only for daemon) [" PACKAGE_PIDFILE "]"}, + {"port", 'p', "PORT", 0, "Listen for command line connections on PORT [8888]"}, + {"localhost", O_LOCAL, nullptr, 0, "Listen for command line connections on 127.0.0.1 interface only"}, + {"httpport", O_HTTPPT, "PORT", 0, "Listen for HTTP connections on PORT, 0 to disable [0]"}, + {"htmlpath", O_HTMLPA, "PATH", 0, "Path for HTML files served by HTTP port [/var/ebusd/html]"}, + {"updatecheck", O_UPDCHK, "MODE", 0, "Set automatic update check to MODE (on|off) [on]"}, + + {nullptr, 0, nullptr, 0, "Log options:"}, + {"logfile", 'l', "FILE", 0, "Write log to FILE (only for daemon, empty string for using syslog) [" + PACKAGE_LOGFILE "]"}, + {"log", O_LOG, "AREAS:LEVEL", 0, "Only write log for matching AREA(S) up to LEVEL" + " (alternative to --logareas/--logevel, may be used multiple times) [all:notice]"}, + {"logareas", O_LOGARE, "AREAS", 0, "Only write log for matching AREA(S): main|network|bus|device|update" + "|other|all [all]"}, + {"loglevel", O_LOGLEV, "LEVEL", 0, "Only write log up to LEVEL: error|notice|info|debug" + " [notice]"}, + + {nullptr, 0, nullptr, 0, "Raw logging options:"}, + {"lograwdata", O_RAW, "bytes", af_optional, + "Log messages or all received/sent bytes on the bus"}, + {"lograwdatafile", O_RAWFIL, "FILE", 0, "Write raw log to FILE [" PACKAGE_LOGFILE "]"}, + {"lograwdatasize", O_RAWSIZ, "SIZE", 0, "Make raw log file no larger than SIZE kB [100]"}, + + {nullptr, 0, nullptr, 0, "Binary dump options:"}, + {"dump", 'D', nullptr, 0, "Enable binary dump of received bytes"}, + {"dumpfile", O_DMPFIL, "FILE", 0, "Dump received bytes to FILE [/tmp/" PACKAGE "_dump.bin]"}, + {"dumpsize", O_DMPSIZ, "SIZE", 0, "Make dump file no larger than SIZE kB [100]"}, + {"dumpflush", O_DMPFLU, nullptr, 0, "Flush each byte"}, + + {nullptr, 0, nullptr, 0, nullptr}, +}; + +/** + * The program argument parsing function. + * @param key the key from @a argDefs. + * @param arg the option argument, or nullptr. + * @param parseOpt the parse options. + */ +static int parse_opt(int key, char *arg, const argParseOpt *parseOpt, struct options *opt) { + result_t result = RESULT_OK; + unsigned int value; + + switch (key) { + // Device options: + case 'd': // --device=mdns: + if (arg == nullptr || arg[0] == 0) { + argParseError(parseOpt, "invalid device"); + return EINVAL; + } + opt->device = arg; + break; + case 'n': // --nodevicecheck + opt->noDeviceCheck = true; + break; + case 'r': // --readonly + opt->readOnly = true; + break; + case O_INISND: // --initsend + opt->initialSend = true; + break; + case O_DEVLAT: // --latency=10 + value = parseInt(arg, 10, 0, 200000, &result); // backwards compatible (micros) + if (result != RESULT_OK || (value <= 1000 && value > 200)) { // backwards compatible (micros) + argParseError(parseOpt, "invalid latency"); + return EINVAL; + } + opt->extraLatency = value > 1000 ? value/1000 : value; // backwards compatible (micros) + break; + + // Message configuration options: + case 'c': // --configpath=https://ebus.github.io/ + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid configpath"); + return EINVAL; + } + s_configPath = arg; + opt->scanConfigOrPathSet = true; + break; + case 's': // --scanconfig[=ADDR] (ADDR=|none|full||off) + { + symbol_t initialScan = 0; + if (!arg || arg[0] == 0) { + initialScan = BROADCAST; // default for no or empty argument + } else if (strcmp("none", arg) == 0) { + initialScan = ESC; + } else if (strcmp("full", arg) == 0) { + initialScan = SYN; + } else if (strcmp("off", arg) == 0) { + // zero turns scanConfig off + } else { + auto address = (symbol_t)parseInt(arg, 16, 0x00, 0xff, &result); + if (result != RESULT_OK || !isValidAddress(address)) { + argParseError(parseOpt, "invalid initial scan address"); + return EINVAL; + } + if (isMaster(address)) { + initialScan = getSlaveAddress(address); + } else { + initialScan = address; + } + } + opt->scanConfig = initialScan != 0; + opt->initialScan = initialScan; + opt->scanConfigOrPathSet = true; + break; + } + case O_SCNRET: // --scanretries=10 + value = parseInt(arg, 10, 0, 100, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid scanretries"); + return EINVAL; + } + opt->scanRetries = value; + break; + case O_CFGLNG: // --configlang=LANG + opt->preferLanguage = arg; + break; + case O_CHKCFG: // --checkconfig + opt->checkConfig = true; + break; + case O_DMPCFG: // --dumpconfig[=json|csv|csvall] + if (!arg || arg[0] == 0 || strcmp("csv", arg) == 0) { + // no further flags + opt->dumpConfig = OF_DEFINITION; + } else if (strcmp("csvall", arg) == 0) { + opt->dumpConfig = OF_DEFINITION | OF_ALL_ATTRS; + } else if (strcmp("json", arg) == 0) { + opt->dumpConfig = OF_DEFINITION | OF_NAMES | OF_UNITS | OF_COMMENTS | OF_VALUENAME | OF_ALL_ATTRS | OF_JSON; + } else { + argParseError(parseOpt, "invalid dumpconfig"); + return EINVAL; + } + opt->checkConfig = true; + break; + case O_DMPCTO: // --dumpconfigto=FILE + if (!arg || arg[0] == 0) { + argParseError(parseOpt, "invalid dumpconfigto"); + return EINVAL; + } + opt->dumpConfigTo = arg; + break; + case O_POLINT: // --pollinterval=5 + value = parseInt(arg, 10, 0, 3600, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid pollinterval"); + return EINVAL; + } + opt->pollInterval = value; + break; + case 'i': // --inject[=stop] + opt->injectCommands = true; + opt->stopAfterInject = arg && strcmp("stop", arg) == 0; + break; +#ifdef HAVE_SSL + case O_CAFILE: // --cafile=FILE + opt->caFile = arg; + break; + case O_CAPATH: // --capath=PATH + opt->caPath = arg; + break; +#endif // HAVE_SSL + // eBUS options: + case 'a': // --address=31 + { + auto address = (symbol_t)parseInt(arg, 16, 0, 0xff, &result); + if (result != RESULT_OK || !isMaster(address)) { + argParseError(parseOpt, "invalid address"); + return EINVAL; + } + opt->address = address; + break; + } + case O_ANSWER: // --answer + opt->answer = true; + break; + case O_ACQTIM: // --acquiretimeout=10 + value = parseInt(arg, 10, 1, 100000, &result); // backwards compatible (micros) + if (result != RESULT_OK || (value <= 1000 && value > 100)) { // backwards compatible (micros) + argParseError(parseOpt, "invalid acquiretimeout"); + return EINVAL; + } + opt->acquireTimeout = value > 1000 ? value/1000 : value; // backwards compatible (micros) + break; + case O_ACQRET: // --acquireretries=3 + value = parseInt(arg, 10, 0, 10, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid acquireretries"); + return EINVAL; + } + opt->acquireRetries = value; + break; + case O_SNDRET: // --sendretries=2 + value = parseInt(arg, 10, 0, 10, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid sendretries"); + return EINVAL; + } + opt->sendRetries = value; + break; + case O_RCVTIM: // --receivetimeout=25 + value = parseInt(arg, 10, 1, 100000, &result); // backwards compatible (micros) + if (result != RESULT_OK || (value <= 1000 && value > 100)) { // backwards compatible (micros) + argParseError(parseOpt, "invalid receivetimeout"); + return EINVAL; + } + opt->receiveTimeout = value > 1000 ? value/1000 : value; // backwards compatible (micros) + break; + case O_MASCNT: // --numbermasters=0 + value = parseInt(arg, 10, 0, 25, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid numbermasters"); + return EINVAL; + } + opt->masterCount = value; + break; + case O_GENSYN: // --generatesyn + opt->generateSyn = true; + break; + + // Daemon options: + case O_ACLDEF: // --accesslevel=* + if (arg == nullptr) { + argParseError(parseOpt, "invalid accesslevel"); + return EINVAL; + } + opt->accessLevel = arg; + break; + case O_ACLFIL: // --aclfile=/etc/ebusd/acl + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid aclfile"); + return EINVAL; + } + opt->aclFile = arg; + break; + case 'f': // --foreground + opt->foreground = true; + break; + case O_HEXCMD: // --enablehex + opt->enableHex = true; + break; + case O_DEFCMD: // --enabledefine + opt->enableDefine = true; + break; + case O_PIDFIL: // --pidfile=/var/run/ebusd.pid + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid pidfile"); + return EINVAL; + } + opt->pidFile = arg; + break; + case 'p': // --port=8888 + value = parseInt(arg, 10, 1, 65535, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid port"); + return EINVAL; + } + opt->port = (uint16_t)value; + break; + case O_LOCAL: // --localhost + opt->localOnly = true; + break; + case O_HTTPPT: // --httpport=0 + value = parseInt(arg, 10, 1, 65535, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid httpport"); + return EINVAL; + } + opt->httpPort = (uint16_t)value; + break; + case O_HTMLPA: // --htmlpath=/var/ebusd/html + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid htmlpath"); + return EINVAL; + } + opt->htmlPath = arg; + break; + case O_UPDCHK: // --updatecheck=on + if (arg == nullptr || arg[0] == 0) { + argParseError(parseOpt, "invalid updatecheck"); + return EINVAL; + } + if (strcmp("on", arg) == 0) { + opt->updateCheck = true; + } else if (strcmp("off", arg) == 0) { + opt->updateCheck = false; + } else { + argParseError(parseOpt, "invalid updatecheck"); + return EINVAL; + } + break; + + // Log options: + case 'l': // --logfile=/var/log/ebusd.log + if (arg == nullptr || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid logfile"); + return EINVAL; + } + opt->logFile = arg; + break; + case O_LOG: // --log=area(s):level + { + char* pos = strchr(arg, ':'); + if (pos == nullptr) { + pos = strchr(arg, ' '); + if (pos == nullptr) { + argParseError(parseOpt, "invalid log"); + return EINVAL; + } + } + *pos = 0; + int facilities = parseLogFacilities(arg); + if (facilities == -1) { + argParseError(parseOpt, "invalid log: areas"); + return EINVAL; + } + LogLevel level = parseLogLevel(pos + 1); + if (level == ll_COUNT) { + argParseError(parseOpt, "invalid log: level"); + return EINVAL; + } + if (opt->logAreas != -1 || opt->logLevel != ll_COUNT) { + argParseError(parseOpt, "invalid log (combined with logareas or loglevel)"); + return EINVAL; + } + setFacilitiesLogLevel(facilities, level); + opt->multiLog = true; + break; + } + case O_LOGARE: // --logareas=all + { + int facilities = parseLogFacilities(arg); + if (facilities == -1) { + argParseError(parseOpt, "invalid logareas"); + return EINVAL; + } + if (opt->multiLog) { + argParseError(parseOpt, "invalid logareas (combined with log)"); + return EINVAL; + } + opt->logAreas = facilities; + break; + } + case O_LOGLEV: // --loglevel=notice + { + LogLevel logLevel = parseLogLevel(arg); + if (logLevel == ll_COUNT) { + argParseError(parseOpt, "invalid loglevel"); + return EINVAL; + } + if (opt->multiLog) { + argParseError(parseOpt, "invalid loglevel (combined with log)"); + return EINVAL; + } + opt->logLevel = logLevel; + break; + } + + // Raw logging options: + case O_RAW: // --lograwdata + opt->logRaw = arg && strcmp("bytes", arg) == 0 ? 2 : 1; + break; + case O_RAWFIL: // --lograwdatafile=/var/log/ebusd.log + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid lograwdatafile"); + return EINVAL; + } + opt->logRawFile = arg; + break; + case O_RAWSIZ: // --lograwdatasize=100 + value = parseInt(arg, 10, 1, 1000000, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid lograwdatasize"); + return EINVAL; + } + opt->logRawSize = value; + break; + + // Binary dump options: + case 'D': // --dump + opt->dump = true; + break; + case O_DMPFIL: // --dumpfile=/tmp/ebusd_dump.bin + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid dumpfile"); + return EINVAL; + } + opt->dumpFile = arg; + break; + case O_DMPSIZ: // --dumpsize=100 + value = parseInt(arg, 10, 1, 1000000, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid dumpsize"); + return EINVAL; + } + opt->dumpSize = value; + break; + case O_DMPFLU: // --dumpflush + opt->dumpFlush = true; + break; + + default: + if (key >= O_INJPOS) { // INJECT + if (!opt->injectCommands || !arg || !arg[0]) { + return ESRCH; + } + opt->injectCount++; + } else { + return ESRCH; + } + } + + // check for invalid arg combinations + if (opt->readOnly && (opt->answer || opt->generateSyn || opt->initialSend + || (opt->scanConfig && opt->initialScan != ESC))) { + argParseError(parseOpt, "cannot combine readonly with answer/generatesyn/initsend/scanconfig"); + return EINVAL; + } + if (opt->scanConfig && opt->pollInterval == 0) { + argParseError(parseOpt, "scanconfig without polling may lead to invalid files included for certain products!"); + return EINVAL; + } + return 0; +} + +int parse_main_args(int argc, char* argv[], char* envp[], options_t *opt) { + *opt = s_default_opt; + const argParseOpt parseOpt = { + argDefs, + reinterpret_cast(parse_opt), + 0, + "A daemon for communication with eBUS heating systems.", + "Report bugs to " PACKAGE_BUGREPORT " .", + datahandler_getargs() + }; + + char envname[32] = "--"; // needs to cover at least max length of any option name plus "--" + char* envopt = envname+2; + for (char ** env = envp; env && *env; env++) { + char* pos = strchr(*env, '='); + if (!pos || strncmp(*env, "EBUSD_", sizeof("EBUSD_")-1) != 0) { + continue; + } + char* start = *env+sizeof("EBUSD_")-1; + size_t len = pos-start; + if (len <= 1 || len > sizeof(envname)-3) { // no single char long args + continue; + } + for (size_t i=0; i < len; i++) { + envopt[i] = static_cast(tolower(start[i])); + } + envopt[len] = 0; + if (strcmp(envopt, "version") == 0 || strcmp(envopt, "image") == 0 || strcmp(envopt, "arch") == 0 + || strcmp(envopt, "opts") == 0 + ) { + // ignore those defined in Dockerfile, EBUSD_OPTS + continue; + } + const argDef* found = argFind(&parseOpt, envopt); + if (found && found->flags & ARG_NO_ENV) { + // ignore those with final args and interactive ones + continue; + } + char* envargv[] = {argv[0], envname, pos+1}; + int cnt = pos[1] ? 2 : 1; + if (pos[1] && strlen(*env) < sizeof(envname)-3 + && found && found->flags & af_optional + ) { + // only really special case: af_optional with non-empty arg needs to use "=" syntax + cnt = 1; + strcat(envopt, pos); + } + int err = argParse(&parseOpt, 1+cnt, envargv, reinterpret_cast(opt)); + if (err != 0) { + if (err == ESRCH) { // special value to abort immediately + logWrite(lf_main, ll_error, "invalid argument in env: %s", *env); // force logging on exit + return EINVAL; + } + logWrite(lf_main, ll_notice, "invalid/unknown argument in env (ignored): %s", *env); // force logging + } + } + + int ret = argParse(&parseOpt, argc, argv, reinterpret_cast(opt)); + if (ret != 0) { + return ret; + } + + if (!opt->readOnly && !opt->scanConfigOrPathSet && opt->dumpConfig == OF_NONE) { + opt->scanConfig = true; + opt->initialScan = BROADCAST; + } + if (!s_configPath.empty()) { + if (s_configPath[s_configPath.length()-1] != '/') { + s_configPath += "/"; + } + opt->configPath = s_configPath.c_str(); // OK as s_configPath is kept + } + return 0; +} + +} // namespace ebusd diff --git a/src/ebusd/mainloop.cpp b/src/ebusd/mainloop.cpp index 970f21b9b..2af372029 100644 --- a/src/ebusd/mainloop.cpp +++ b/src/ebusd/mainloop.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,14 +25,13 @@ #include #include #include "ebusd/main.h" +#include "ebusd/scan.h" #include "lib/utils/log.h" #include "lib/ebus/data.h" namespace ebusd { using std::dec; -using std::hex; -using std::setfill; using std::setw; using std::endl; using std::ifstream; @@ -104,39 +103,19 @@ result_t UserList::addFromFile(const string& filename, unsigned int lineNo, map< #define VERBOSITY_4 (VERBOSITY_3 | OF_ALL_ATTRS) -MainLoop::MainLoop(const struct options& opt, Device *device, MessageMap* messages) - : Thread(), m_device(device), m_reconnectCount(0), m_userList(opt.accessLevel), m_messages(messages), - m_address(opt.address), m_scanConfig(opt.scanConfig), m_initialScan(opt.readOnly ? ESC : opt.initialScan), - m_polling(opt.pollInterval > 0), m_enableHex(opt.enableHex), m_shutdown(false), m_runUpdateCheck(opt.updateCheck), - m_httpClient(opt.caFile, opt.caPath) { - m_device->setListener(this); - // open Device - result_t result = m_device->open(); - if (result != RESULT_OK) { - logError(lf_bus, "unable to open %s: %s", m_device->getName(), getResultCode(result)); - } else if (!m_device->isValid()) { - logError(lf_bus, "device %s not available", m_device->getName()); - } - if (opt.dumpFile[0]) { - m_dumpFile = new RotateFile(opt.dumpFile, opt.dumpSize, false, opt.dumpFlush ? 1 : 16); - m_dumpFile->setEnabled(opt.dump); - } else { - m_dumpFile = nullptr; - } - m_logRawEnabled = opt.logRaw != 0; - if (opt.logRawFile[0] && strcmp(opt.logRawFile, opt.logFile) != 0) { - m_logRawFile = new RotateFile(opt.logRawFile, opt.logRawSize, true); - m_logRawFile->setEnabled(m_logRawEnabled); - } else { - m_logRawFile = nullptr; - } - m_logRawBytes = opt.logRaw == 2; - m_logRawLastReceived = true; - m_logRawLastSymbol = SYN; +MainLoop::MainLoop(const struct options& opt, BusHandler* busHandler, + MessageMap* messages, ScanHelper* scanHelper, Queue* requestQueue) + : Thread(), m_busHandler(busHandler), m_protocol(busHandler->getProtocol()), m_reconnectCount(0), + m_userList(opt.accessLevel), m_messages(messages), + m_scanHelper(scanHelper), m_address(opt.address), m_scanConfig(opt.scanConfig), + m_initialScan(opt.readOnly ? (symbol_t)ESC : opt.initialScan), m_scanRetries(opt.scanRetries), + m_scanStatus(SCAN_STATUS_NONE), m_polling(opt.pollInterval > 0), m_enableHex(opt.enableHex), + m_shutdown(false), m_runUpdateCheck(opt.updateCheck), m_httpClient(), m_requestQueue(requestQueue) { if (opt.aclFile[0]) { string errorDescription; time_t mtime = 0; istream* stream = FileReader::openFile(opt.aclFile, &errorDescription, &mtime); + result_t result; if (stream) { result = m_userList.readFromStream(stream, opt.aclFile, mtime, false, nullptr, &errorDescription); delete(stream); @@ -147,26 +126,20 @@ MainLoop::MainLoop(const struct options& opt, Device *device, MessageMap* messag logError(lf_main, "error reading ACL file \"%s\": %s", opt.aclFile, getResultCode(result)); } } - // create BusHandler - m_busHandler = new BusHandler(m_device, m_messages, - m_address, opt.answer, - opt.acquireRetries, opt.sendRetries, - opt.acquireTimeout, opt.receiveTimeout, - opt.masterCount, opt.generateSyn, - opt.pollInterval); - m_busHandler->start("bushandler"); - // create network m_htmlPath = opt.htmlPath; - m_network = new Network(opt.localOnly, opt.port, opt.httpPort, &m_netQueue); - m_network->start("network"); logInfo(lf_main, "registering data handlers"); if (datahandler_register(&m_userList, m_busHandler, messages, &m_dataHandlers)) { logInfo(lf_main, "registered data handlers"); } else { logError(lf_main, "error registering data handlers"); } - m_newlyDefinedMessages = opt.enableDefine ? new MessageMap(true, "", false) : nullptr; + if (opt.enableDefine) { + m_newlyDefinedMessages = new MessageMap(true, "", false); + m_newlyDefinedMessages->setResolver(scanHelper); + } else { + m_newlyDefinedMessages = nullptr; + } } MainLoop::~MainLoop() { @@ -177,36 +150,19 @@ MainLoop::~MainLoop() { delete dataHandler; } m_dataHandlers.clear(); - if (m_dumpFile) { - delete m_dumpFile; - m_dumpFile = nullptr; - } - if (m_logRawFile) { - delete m_logRawFile; - m_logRawFile = nullptr; - } - if (m_network != nullptr) { - delete m_network; - m_network = nullptr; - } - if (m_busHandler != nullptr) { - delete m_busHandler; - m_busHandler = nullptr; - } - if (m_device != nullptr) { - delete m_device; - m_device = nullptr; - } - NetMessage* msg; - while ((msg = m_netQueue.pop()) != nullptr) { - delete msg; - } if (m_newlyDefinedMessages) { delete m_newlyDefinedMessages; m_newlyDefinedMessages = nullptr; } } +void MainLoop::shutdown() { + m_shutdown = true; + if (m_requestQueue != nullptr) { + m_requestQueue->push(nullptr); // just to notify potentially waiting thread + } +} + /** the delay for running the update check. */ #define CHECK_DELAY (24*3600) @@ -221,8 +177,9 @@ void MainLoop::run() { time_t lastTaskRun, now, start, lastSignal = 0, since, sinkSince = 1, nextCheckRun; int taskDelay = 5; symbol_t lastScanAddress = 0; // 0 is known to be a master - scanStatus_t lastScanStatus = SCAN_STATUS_NONE; + scanStatus_t lastScanStatus = m_scanStatus; int scanCompleted = 0; + int scanRetry = 0; time(&now); start = now; lastTaskRun = now; @@ -238,8 +195,8 @@ void MainLoop::run() { dataHandler->startHandler(); } while (!m_shutdown) { - // pick the next message to handle - NetMessage* netMessage = m_netQueue.pop(taskDelay); + // pick the next request to handle + Request* req = m_requestQueue->pop(taskDelay); time(&now); if (now < lastTaskRun) { // clock skew @@ -249,39 +206,34 @@ void MainLoop::run() { lastTaskRun = now; } else if (!m_shutdown && now > lastTaskRun+taskDelay) { logDebug(lf_main, "performing regular tasks"); - if (m_busHandler->hasSignal()) { + if (m_protocol->hasSignal()) { lastSignal = now; } else if (lastSignal && now > lastSignal+RECONNECT_MISSING_SIGNAL) { lastSignal = 0; - m_busHandler->reconnect(); + m_protocol->reconnect(); m_reconnectCount++; } - if (m_scanConfig) { + if (m_scanConfig && scanRetry <= m_scanRetries) { bool loadDelay = false; - scanStatus_t scanStatus = lastScanStatus; - if (m_initialScan != ESC && reload && m_busHandler->hasSignal()) { + if (m_initialScan != ESC && reload && m_protocol->hasSignal()) { loadDelay = true; result_t result; if (m_initialScan == SYN) { logNotice(lf_main, "starting initial full scan"); result = m_busHandler->startScan(true, "*"); if (result == RESULT_OK) { - scanStatus = SCAN_STATUS_RUNNING; + m_scanStatus = SCAN_STATUS_RUNNING; } } else if (m_initialScan == BROADCAST) { logNotice(lf_main, "starting initial broadcast scan"); - Message* message = m_messages->getScanMessage(BROADCAST); - if (message) { - MasterSymbolString master; - SlaveSymbolString slave; - istringstream input; - result = message->prepareMaster(0, m_address, SYN, UI_FIELD_SEPARATOR, &input, &master); - if (result == RESULT_OK) { - result = m_busHandler->sendAndWait(master, &slave); - } - } else { - result = RESULT_ERR_NOTFOUND; - } + MasterSymbolString master; + SlaveSymbolString slave; + master.push_back(m_address); + master.push_back(BROADCAST); + master.push_back(0x07); + master.push_back(0xfe); // query existance message + master.adjustHeader(); + result = m_protocol->sendAndWait(master, &slave); } else { logNotice(lf_main, "starting initial scan for %2.2x", m_initialScan); result = m_busHandler->scanAndWait(m_initialScan, true); @@ -290,7 +242,7 @@ void MainLoop::run() { if (m_busHandler->formatScanResult(m_initialScan, false, &ret)) { logNotice(lf_main, "initial scan result: %s", ret.str().c_str()); } - scanStatus = SCAN_STATUS_RUNNING; + m_scanStatus = SCAN_STATUS_RUNNING; } } if (result != RESULT_OK) { @@ -299,38 +251,41 @@ void MainLoop::run() { reload = false; } } - if (!loadDelay) { + if (!loadDelay && m_protocol->hasSignal()) { lastScanAddress = m_busHandler->getNextScanAddress(lastScanAddress, scanCompleted >= SCAN_REPEAT_COUNT); if (lastScanAddress == SYN) { taskDelay = 5; lastScanAddress = 0; - scanStatus = SCAN_STATUS_FINISHED; + m_scanStatus = SCAN_STATUS_FINISHED; scanCompleted++; if (scanCompleted > SCAN_REPEAT_COUNT) { // repeat failed scan only every Nth time scanCompleted = 0; + scanRetry++; + logNotice(lf_main, "scan completed %d time(s), %s", scanRetry, + scanRetry <= m_scanRetries ? "check again" : "end"); } } else { - scanStatus = SCAN_STATUS_RUNNING; - nextCheckRun = now + CHECK_INITIAL_DELAY; + m_scanStatus = SCAN_STATUS_RUNNING; result_t result = m_busHandler->scanAndWait(lastScanAddress, true); taskDelay = (result == RESULT_ERR_NO_SIGNAL) ? 10 : 1; if (result != RESULT_OK) { logError(lf_main, "scan config %2.2x: %s", lastScanAddress, getResultCode(result)); } else { logInfo(lf_main, "scan config %2.2x message received", lastScanAddress); + nextCheckRun = now + CHECK_INITIAL_DELAY; // delay update check due to new scan data } } } - if (scanStatus != lastScanStatus && !dataSinks.empty()) { - lastScanStatus = scanStatus; + if (lastScanStatus != m_scanStatus && !dataSinks.empty()) { + lastScanStatus = m_scanStatus; for (const auto dataSink : dataSinks) { - dataSink->notifyScanStatus(scanStatus); + dataSink->notifyScanStatus(lastScanStatus); } } - } else if (reload && m_busHandler->hasSignal()) { + } else if (reload && m_protocol->hasSignal()) { reload = false; // execute initial instructions - executeInstructions(m_messages); + m_scanHelper->executeInstructions(m_busHandler); if (m_messages->sizeConditions() > 0 && !m_polling) { logError(lf_main, "conditions require a poll interval > 0"); } @@ -343,11 +298,12 @@ void MainLoop::run() { if (!m_httpClient.connect("upd.ebusd.eu", #ifdef HAVE_SSL 443, true, -#else +#else // HAVE_SSL 80, false, -#endif +#endif // HAVE_SSL PACKAGE_NAME "/" PACKAGE_VERSION)) { logError(lf_main, "update check connect error"); + nextCheckRun = now + CHECK_INITIAL_DELAY; } else { ostringstream ostr; ostr << "{\"v\":\"" PACKAGE_VERSION "\",\"r\":\"" REVISION << "\"" @@ -365,14 +321,21 @@ void MainLoop::run() { << ",\"a\":\"other\"" #endif << ",\"u\":" << (now-start); + const string configPathCDN = m_scanHelper->getConfigPathCDN(); + if (!configPathCDN.empty()) { + ostr << ",\"cp\":\"" << configPathCDN << "\""; + } + m_protocol->formatInfoJson(&ostr); if (m_reconnectCount) { ostr << ",\"rc\":" << m_reconnectCount; } m_busHandler->formatUpdateInfo(&ostr); ostr << "}"; string response; - if (!m_httpClient.post("/", ostr.str(), &response)) { + bool repeat = false; + if (!m_httpClient.post("/", ostr.str(), &response, &repeat)) { logError(lf_main, "update check error: %s", response.c_str()); + nextCheckRun = now + (repeat ? CHECK_INITIAL_DELAY : CHECK_DELAY); } else { m_updateCheck = response.empty() ? "unknown" : response; logNotice(lf_main, "update check: %s", response.c_str()); @@ -381,9 +344,9 @@ void MainLoop::run() { dataSink->notifyUpdateCheckResult(response == "OK" ? "" : m_updateCheck); } } + nextCheckRun = now + CHECK_DELAY; } } - nextCheckRun = now + CHECK_DELAY; } time(&lastTaskRun); } @@ -393,175 +356,94 @@ void MainLoop::run() { m_messages->lock(); m_messages->findAll("", "", "*", false, true, true, true, true, true, sinkSince, now, false, &messages); for (const auto message : messages) { + bool changed = message->getLastChangeTime() >= sinkSince; for (const auto dataSink : dataSinks) { - dataSink->notifyUpdate(message); + dataSink->notifyUpdate(message, changed); } } m_messages->unlock(); sinkSince = now; } - if (netMessage == nullptr) { + if (req == nullptr) { continue; } if (m_shutdown) { - netMessage->setResult("ERR: shutdown", "", nullptr, now, true); + req->setResult("ERR: shutdown", "", nullptr, now, true); break; } - string request = netMessage->getRequest(); - string user = netMessage->getUser(); - ClientSettings settings = netMessage->getSettings(&since); - if (!netMessage->isListeningMode()) { + string user = req->getUser(); + RequestMode reqMode = req->getMode(&since); + if (reqMode.listenMode == lm_none) { since = now; } ostringstream ostream; bool connected = true; - if (request.length() > 0) { - logDebug(lf_main, ">>> %s", request.c_str()); - result_t result = decodeMessage(request, netMessage->isHttp(), &connected, &settings, &user, &reload, &ostream); - if (!netMessage->isHttp() && (ostream.tellp() == 0 || result != RESULT_OK)) { - if (settings.mode != cm_direct) { - ostream.str(""); + if (!req->empty()) { + req->log(); + bool currentReload = reload; + result_t result = decodeRequest(req, &connected, &reqMode, &user, &reload, &ostream); + if (reload && !currentReload) { + scanRetry = 0; // restart scan counting + } + if (!req->isHttp() && (ostream.tellp() == 0 || result != RESULT_OK)) { + string suffix; + if (result == RESULT_EMPTY && ostream.tellp() > 0) { + suffix = ostream.str(); } + ostream.str(""); ostream << getResultCode(result); + if (!suffix.empty()) { + ostream << " " << suffix; + } } - if (ostream.tellp() > 100) { - logDebug(lf_main, "<<< %s ...", ostream.str().substr(0, 100).c_str()); - } else { - logDebug(lf_main, "<<< %s", ostream.str().c_str()); - } + const auto resp = ostream.str(); + req->log(&resp); if (ostream.tellp() == 0) { ostream << "\n"; // only for HTTP - } else if (!netMessage->isHttp()) { - ostream << (settings.mode == cm_direct ? "\n" : "\n\n"); + } else if (!req->isHttp()) { + ostream << (reqMode.listenMode == lm_direct ? "\n" : "\n\n"); } } - if (settings.mode == cm_listen) { - if (!settings.listenOnlyUnknown) { + if (reqMode.listenMode == lm_listen) { + if (!reqMode.listenOnlyUnknown) { string levels = getUserLevels(user); messages.clear(); m_messages->findAll("", "", levels, false, true, true, true, true, true, since, now, true, &messages); for (const auto message : messages) { ostream << message->getCircuit() << " " << message->getName() << " = " << dec; - message->decodeLastData(false, nullptr, -1, settings.format, &ostream); + message->decodeLastData(pt_any, false, nullptr, -1, reqMode.format, &ostream); ostream << endl; } } - if (settings.listenWithUnknown || settings.listenOnlyUnknown) { + if (reqMode.listenWithUnknown || reqMode.listenOnlyUnknown) { if (m_busHandler->isGrabEnabled()) { m_busHandler->formatGrabResult(true, OF_NONE, &ostream, true, since, now); } else { m_busHandler->enableGrab(true); // needed for listening to all messages } } - } else if (settings.mode == cm_direct) { + } else if (reqMode.listenMode == lm_direct) { if (m_busHandler->isGrabEnabled()) { m_busHandler->formatGrabResult(false, OF_NONE, &ostream, true, since, now); } } // send result to client - netMessage->setResult(ostream.str(), user, &settings, now, !connected); + req->setResult(ostream.str(), user, &reqMode, now, !connected); } } -void MainLoop::notifyDeviceData(symbol_t symbol, bool received) { - if (received && m_dumpFile) { - m_dumpFile->write(&symbol, 1); - } - if (!m_logRawFile && !m_logRawEnabled) { - return; - } - if (m_logRawBytes) { - if (m_logRawFile) { - m_logRawFile->write(&symbol, 1, received); - } else if (m_logRawEnabled) { - if (received) { - logNotice(lf_bus, "<%02x", symbol); - } else { - logNotice(lf_bus, ">%02x", symbol); - } - } - return; - } - if (symbol != SYN) { - if (received && !m_logRawLastReceived && symbol == m_logRawLastSymbol) { - return; // skip received echo of previously sent symbol - } - if (m_logRawBuffer.tellp() == 0 || received != m_logRawLastReceived) { - m_logRawLastReceived = received; - if (m_logRawBuffer.tellp() == 0 && m_logRawLastSymbol != SYN) { - m_logRawBuffer << "..."; - } - m_logRawBuffer << (received ? "<" : ">"); - } - m_logRawBuffer << setw(2) << setfill('0') << hex << static_cast(symbol); - } - m_logRawLastSymbol = symbol; - if (m_logRawBuffer.tellp() > (symbol == SYN ? 0 : 64)) { // flush: direction+5 hdr+24 max data+crc+direction+ack+1 - if (symbol != SYN) { - m_logRawBuffer << "..."; - } - const string bufStr = m_logRawBuffer.str(); - const char* str = bufStr.c_str(); - if (m_logRawFile) { - m_logRawFile->write((const unsigned char*)str, strlen(str), received, false); - } else { - logNotice(lf_bus, str); - } - m_logRawBuffer.str(""); - } -} - -void MainLoop::notifyStatus(bool error, const char* message) { - if (error) { - logError(lf_bus, "device status: %s", message); - } else { - logNotice(lf_bus, "device status: %s", message); - } -} - -result_t MainLoop::decodeMessage(const string &data, bool isHttp, bool* connected, ClientSettings* settings, +result_t MainLoop::decodeRequest(Request* req, bool* connected, RequestMode* reqMode, string* user, bool* reload, ostringstream* ostream) { - string token, previous; - istringstream stream(data); vector args; - char escaped = 0; - - char delim = ' '; - while (getline(stream, token, delim)) { - if (!isHttp) { - if (escaped) { - args.pop_back(); - if (token.length() > 0 && token[token.length()-1] == escaped) { - token.erase(token.length() - 1, 1); - escaped = 0; - } - token = previous + " " + token; - } else if (token.length() == 0) { // allow multiple space chars for a single delimiter - continue; - } else if (token[0] == '"' || token[0] == '\'') { - escaped = token[0]; - token.erase(0, 1); - if (token.length() > 0 && token[token.length()-1] == escaped) { - token.erase(token.length() - 1, 1); - escaped = 0; - } - } - } - args.push_back(token); - previous = token; - if (isHttp) { - delim = (args.size() == 1) ? '?' : '\n'; - } - } - - if (isHttp) { + req->split(&args); + string cmd = args.size() > 0 ? args[0] : ""; + if (req->isHttp()) { if (args.size() < 2) { *connected = false; *ostream << "HTTP/1.0 400 Bad Request\r\n\r\n"; return RESULT_OK; } - const char* str = args.size() > 0 ? args[0].c_str() : ""; - if (strcmp(str, "GET") == 0) { + if (cmd == "GET") { return executeGet(args, connected, ostream); } *connected = false; @@ -569,7 +451,6 @@ result_t MainLoop::decodeMessage(const string &data, bool isHttp, bool* connecte return RESULT_OK; } - string cmd = args.size() > 0 ? args[0] : ""; transform(cmd.begin(), cmd.end(), cmd.begin(), ::toupper); if (cmd == "?" || cmd == "H" || cmd == "HELP") { // found "HELP CMD" @@ -577,8 +458,8 @@ result_t MainLoop::decodeMessage(const string &data, bool isHttp, bool* connecte transform(cmd.begin(), cmd.end(), cmd.begin(), ::toupper); args.clear(); // empty args is used as command help indicator } - if (settings->mode == cm_direct) { - return executeDirect(args, &settings->mode, ostream); + if (reqMode->listenMode == lm_direct) { + return executeDirect(args, reqMode, ostream); } if (cmd.empty() && args.size() == 0) { return executeHelp(ostream); @@ -606,14 +487,28 @@ result_t MainLoop::decodeMessage(const string &data, bool isHttp, bool* connecte *ostream << "ERR: command not enabled"; return RESULT_OK; } + if (cmd == "INJECT") { + if (m_enableHex) { + return executeInject(args, ostream); + } + *ostream << "ERR: command not enabled"; + return RESULT_OK; + } + if (cmd == "ANSWER") { + if (m_enableHex && m_protocol->isAnswering()) { + return executeAnswer(args, ostream); + } + *ostream << "ERR: command not enabled"; + return RESULT_OK; + } if (cmd == "F" || cmd == "FIND") { return executeFind(args, getUserLevels(*user), ostream); } if (cmd == "L" || cmd == "LISTEN") { - return executeListen(args, settings, ostream); + return executeListen(args, reqMode, ostream); } if (cmd == "DIRECT") { - return executeDirect(args, &settings->mode, ostream); + return executeDirect(args, reqMode, ostream); } if (cmd == "S" || cmd == "STATE") { return executeState(args, ostream); @@ -722,7 +617,7 @@ result_t MainLoop::executeAuth(const vector& args, string* user, ostring result_t MainLoop::executeRead(const vector& args, const string& levels, ostringstream* ostream) { size_t argPos = 1; - bool hex = false, newDefinition = false; + bool hex = false, newDefinition = false, writeDirection = false; OutputFormat verbosity = OF_NONE; time_t maxAge = 5*60; string circuit, params; @@ -736,6 +631,9 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, *ostream << "ERR: option not enabled"; return RESULT_OK; } + if (newDefinition) { + writeDirection = true; + } newDefinition = true; } else if (args[argPos] == "-f") { maxAge = 0; @@ -762,8 +660,15 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, } } else if (args[argPos] == "-vv") { verbosity |= VERBOSITY_2; - } else if (args[argPos] == "-vvv" || args[argPos] == "-V") { + } else if (args[argPos] == "-vvv") { verbosity |= VERBOSITY_3; + } else if (args[argPos] == "-V") { + if ((verbosity & VERBOSITY_4) == VERBOSITY_4) { + verbosity |= OF_RAWDATA; + } + verbosity |= VERBOSITY_4; + } else if (args[argPos] == "-VV") { + verbosity |= VERBOSITY_4 | OF_RAWDATA; } else if (args[argPos] == "-n") { verbosity = (verbosity & ~OF_VALUENAME) | OF_NUMERIC; } else if (args[argPos] == "-N") { @@ -790,7 +695,7 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, if (dest) { dstAddress = address; } else { - srcAddress = address == m_address ? SYN : address; + srcAddress = address == m_address ? (symbol_t)SYN : address; } } else if (args[argPos] == "-p") { argPos++; @@ -837,14 +742,15 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, " -d ZZ override destination address ZZ\n" " -p PRIO set the message poll priority (1-9)\n" " -v increase verbosity (include names/units/comments)\n" - " -V be very verbose (include names, units, and comments)\n" + " -V be very verbose (all attributes, plus raw data if given more than once)\n" " -n use numeric value of value=name pairs\n" " -N use numeric and named value of value=name pairs\n" " -i VALUE read additional message parameters from VALUE\n" " NAME NAME of the message to send\n" " FIELD only retrieve the field named FIELD\n" " N only retrieve the N'th field named FIELD (0-based)\n" - " -def read with explicit message definition (only if enabled):\n" + " -def read with explicit message definition (only if enabled, allow write direction if given more" + " than once):\n" " DEFINITION message definition to use instead of known definition\n" " -h send hex read message (or answer from cache):\n" " ZZ destination address\n" @@ -893,13 +799,13 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, // send message SlaveSymbolString slave; - ret = m_busHandler->sendAndWait(master, &slave); + ret = m_protocol->sendAndWait(master, &slave); if (ret == RESULT_OK) { ret = message->storeLastData(master, slave); ostringstream result; if (ret == RESULT_OK) { - ret = message->decodeLastData(false, nullptr, -1, OF_NONE, &result); + ret = message->decodeLastData(pt_slaveData, false, nullptr, -1, OF_NONE, &result); } if (ret >= RESULT_OK) { logInfo(lf_main, "read hex %s %s cache update: %s", message->getCircuit().c_str(), message->getName().c_str(), @@ -944,9 +850,10 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, return RESULT_OK; } deque messages; - m_newlyDefinedMessages->findAll("", "", levels, false, true, false, false, true, false, 0, 0, false, &messages); + m_newlyDefinedMessages->findAll("", "", levels, false, true, writeDirection, writeDirection, true, false, 0, 0, + false, &messages); if (messages.empty()) { - *ostream << "ERR: bad definition: no read message"; + *ostream << "ERR: bad definition: no read" << (writeDirection?"/write":"") << " message"; return RESULT_OK; } message = *messages.begin(); @@ -969,13 +876,21 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, if (verbosity & OF_NAMES) { *ostream << cacheMessage->getCircuit() << " " << cacheMessage->getName() << " "; } - ret = cacheMessage->decodeLastData(false, fieldIndex == -2 ? nullptr : fieldName.c_str(), fieldIndex, verbosity, - ostream); - if (ret != RESULT_OK) { - if (ret < RESULT_OK) { - logError(lf_main, "read %s %s cached: %s", cacheMessage->getCircuit().c_str(), - cacheMessage->getName().c_str(), getResultCode(ret)); + ret = cacheMessage->decodeLastData( + hasCache && (cacheMessage->isWrite() || cacheMessage->isPassive()) ? pt_any : pt_slaveData, + false, fieldIndex == -2 ? nullptr : fieldName.c_str(), fieldIndex, verbosity, ostream); + if (ret < RESULT_OK) { + logError(lf_main, "read %s %s cached: decode %s", cacheMessage->getCircuit().c_str(), + cacheMessage->getName().c_str(), getResultCode(ret)); + const auto str = ostream->str(); + ostream->str(""); + *ostream << getResultCode(ret) << " in decode"; + if (!str.empty()) { + *ostream << ": " << str; } + return RESULT_OK; + } + if (ret > RESULT_OK) { return ret; } logInfo(lf_main, "read %s %s cached: %s", cacheMessage->getCircuit().c_str(), cacheMessage->getName().c_str(), @@ -1002,13 +917,17 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, if (verbosity & OF_NAMES) { *ostream << message->getCircuit() << " " << message->getName() << " "; } - ret = message->decodeLastData(false, false, fieldIndex == -2 ? nullptr : fieldName.c_str(), fieldIndex, verbosity, - ostream); + ret = message->decodeLastData(pt_slaveData, false, fieldIndex == -2 ? nullptr : fieldName.c_str(), fieldIndex, + verbosity, ostream); if (ret < RESULT_OK) { logError(lf_main, "read %s %s: decode %s", message->getCircuit().c_str(), message->getName().c_str(), getResultCode(ret)); + const auto str = ostream->str(); ostream->str(""); *ostream << getResultCode(ret) << " in decode"; + if (!str.empty()) { + *ostream << ": " << str; + } return RESULT_OK; } if (ret > RESULT_OK) { @@ -1021,11 +940,19 @@ result_t MainLoop::executeRead(const vector& args, const string& levels, result_t MainLoop::executeWrite(const vector& args, const string levels, ostringstream* ostream) { size_t argPos = 1; bool hex = false, newDefinition = false; + OutputFormat verbosity = OF_NONE; string circuit; symbol_t srcAddress = SYN, dstAddress = SYN; while (args.size() > argPos && args[argPos][0] == '-') { if (args[argPos] == "-h") { hex = true; + } else if (args[argPos] == "-V") { + if ((verbosity & VERBOSITY_4) == VERBOSITY_4) { + verbosity |= OF_RAWDATA; + } + verbosity |= VERBOSITY_4; + } else if (args[argPos] == "-VV") { + verbosity |= VERBOSITY_4 | OF_RAWDATA; } else if (args[argPos] == "-def") { if (!m_newlyDefinedMessages) { *ostream << "ERR: option not enabled"; @@ -1047,7 +974,7 @@ result_t MainLoop::executeWrite(const vector& args, const string levels, if (dest) { dstAddress = address; } else { - srcAddress = address == m_address ? SYN : address; + srcAddress = address == m_address ? (symbol_t)SYN : address; } } else if (args[argPos] == "-c") { argPos++; @@ -1078,6 +1005,7 @@ result_t MainLoop::executeWrite(const vector& args, const string levels, " -s QQ override source address QQ\n" " -d ZZ override destination address ZZ\n" " -c CIRCUIT CIRCUIT of the message to send\n" + " -V be very verbose (all attributes, plus raw data if given more than once)\n" " NAME NAME of the message to send\n" " VALUE a single field VALUE\n" " -def write with explicit message definition (only if enabled):\n" @@ -1110,19 +1038,20 @@ result_t MainLoop::executeWrite(const vector& args, const string levels, if (!message->isWrite()) { return RESULT_ERR_INVALID_ARG; } - if (circuit.length() > 0 && circuit != message->getCircuit()) { + if (!circuit.empty() && circuit != message->getCircuit()) { return RESULT_ERR_INVALID_ARG; // non-matching circuit } + // send message SlaveSymbolString slave; - ret = m_busHandler->sendAndWait(master, &slave); + ret = m_protocol->sendAndWait(master, &slave); if (ret == RESULT_OK) { // also update read messages ret = message->storeLastData(master, slave); ostringstream result; if (ret == RESULT_OK) { - ret = message->decodeLastData(false, nullptr, -1, OF_NONE, &result); + ret = message->decodeLastData(pt_slaveData, false, nullptr, -1, verbosity, &result); } if (ret >= RESULT_OK) { logInfo(lf_main, "write hex %s %s cache update: %s", message->getCircuit().c_str(), @@ -1160,7 +1089,7 @@ result_t MainLoop::executeWrite(const vector& args, const string levels, return RESULT_OK; } deque messages; - m_newlyDefinedMessages->findAll("", "", levels, false, false, true, false, true, false, 0, 0, false, &messages); + m_newlyDefinedMessages->findAll("", "", levels, false, false, true, true, true, false, 0, 0, false, &messages); if (messages.empty()) { *ostream << "ERR: bad definition: no write message"; return RESULT_OK; @@ -1183,32 +1112,26 @@ result_t MainLoop::executeWrite(const vector& args, const string levels, getResultCode(ret)); return ret; } - dstAddress = message->getLastMasterData().dataAt(1); - if (dstAddress == BROADCAST || isMaster(dstAddress)) { - logNotice(lf_main, "write %s %s: %s", message->getCircuit().c_str(), message->getName().c_str(), - getResultCode(ret)); - if (dstAddress == BROADCAST) { - *ostream << "done broadcast"; - } - return RESULT_OK; - } + dstAddress = message->getLastMasterData()[1]; - ret = message->decodeLastData(false, false, nullptr, -1, OF_NONE, ostream); // decode data - if (ret >= RESULT_OK && ostream->str().empty()) { - logNotice(lf_main, "write %s %s: decode %s", message->getCircuit().c_str(), message->getName().c_str(), - getResultCode(ret)); - return RESULT_OK; - } - if (ret != RESULT_OK) { + ret = message->decodeLastData(pt_slaveData, false, nullptr, -1, verbosity, ostream); // decode data + if (ret < RESULT_OK) { logError(lf_main, "write %s %s: decode %s", message->getCircuit().c_str(), message->getName().c_str(), getResultCode(ret)); ostream->str(""); *ostream << getResultCode(ret) << " in decode"; return RESULT_OK; } - logNotice(lf_main, "write %s %s: %s", message->getCircuit().c_str(), message->getName().c_str(), - ostream->str().c_str()); - return RESULT_OK; + if (dstAddress == BROADCAST && ostream->tellp() == 0) { + if (ret == RESULT_OK) { + *ostream << getResultCode(ret) << " "; + } + *ostream << "broadcast"; + } + string code = ret == RESULT_OK ? "" : (string(getResultCode(ret)) + " "); + logNotice(lf_main, "write %s %s: %s%s", message->getCircuit().c_str(), message->getName().c_str(), + code.c_str(), ostream->str().c_str()); + return ret; } result_t MainLoop::parseHexAndSend(const vector& args, size_t& argPos, bool isDirectMode, @@ -1223,7 +1146,7 @@ result_t MainLoop::parseHexAndSend(const vector& args, size_t& argPos, b if (ret != RESULT_OK || !isValidAddress(address, false) || !isMaster(address)) { return RESULT_ERR_INVALID_ADDR; } - srcAddress = address == m_address ? SYN : address; + srcAddress = address == m_address ? (symbol_t)SYN : address; } else if (args[argPos] == "-n") { autoLength = true; } else { @@ -1247,7 +1170,7 @@ result_t MainLoop::parseHexAndSend(const vector& args, size_t& argPos, b // send message SlaveSymbolString slave; - ret = m_busHandler->sendAndWait(master, &slave); + ret = m_protocol->sendAndWait(master, &slave); if (ret == RESULT_OK) { if (master[1] == BROADCAST) { @@ -1283,10 +1206,108 @@ result_t MainLoop::executeHex(const vector& args, ostringstream* ostream return RESULT_OK; } -result_t MainLoop::executeDirect(const vector& args, ClientMode* mode, ostringstream* ostream) { - if (*mode != cm_direct) { +result_t MainLoop::executeInject(const vector& args, ostringstream* ostream) { + size_t argPos = 1; + if (argPos < args.size()) { + MasterSymbolString master; + SlaveSymbolString slave; + if (!m_scanHelper->parseMessage(args[argPos++], false, &master, &slave)) { + return RESULT_ERR_INVALID_ARG; + } + m_busHandler->notifyProtocolMessage(md_recv, master, slave); + return RESULT_OK; + } + *ostream << "usage: inject QQZZPBSBNN[DD]*/[NN[DD]*]\n" + " Inject hex data (without sending to bus).\n" + " QQ source address\n" + " ZZ destination address\n" + " PB SB primary/secondary command byte\n" + " NN number of following data bytes\n" + " DD data byte(s)"; + return RESULT_OK; +} + +result_t MainLoop::executeAnswer(const vector& args, ostringstream* ostream) { + size_t argPos = 1; + symbol_t srcAddress = SYN; + symbol_t dstAddress = SYN; + bool master = false; + while (args.size() > argPos && args[argPos][0] == '-') { + if (args[argPos] == "-s" && argPos + 1 < args.size()) { + result_t ret; + argPos++; + symbol_t address = (symbol_t)parseInt(args[argPos].c_str(), 16, 0, 0xff, &ret); + if (ret != RESULT_OK || !isValidAddress(address, false) || !isMaster(address)) { + return RESULT_ERR_INVALID_ADDR; + } + srcAddress = address; + } else if (args[argPos] == "-d" && argPos + 1 < args.size()) { + result_t ret; + argPos++; + symbol_t address = (symbol_t)parseInt(args[argPos].c_str(), 16, 0, 0xff, &ret); + if (ret != RESULT_OK || !isValidAddress(address)) { + return RESULT_ERR_INVALID_ADDR; + } + dstAddress = address; + } else if (args[argPos] == "-m") { + master = true; + } else { + argPos = 0; // print usage + break; + } + argPos++; + } + MasterSymbolString id; + if (argPos > 0 && argPos < args.size()) { + result_t ret = id.parseHex(args[argPos++]); + if (ret != RESULT_OK) { + return ret; + } + if (id.size() < 2 || id.size() > 6) { + return RESULT_ERR_INVALID_POS; + } + } + SlaveSymbolString answer; + answer.push_back(0); // room for length byte + if (argPos > 0 && argPos < args.size()) { + result_t ret = answer.parseHex(args[argPos++]); + if (ret != RESULT_OK) { + return ret; + } + if (answer.size() > 16) { + return RESULT_ERR_INVALID_POS; + } + } + answer.adjustHeader(); + if (argPos < args.size()) { + argPos = 0; // print usage + } + if (argPos <= 1) { + *ostream << "usage: answer [-m] [-s QQ] [-d ZZ] PBSB[ID]* [DD]*\n" + " Answer to a message from the bus.\n" + " -m destination is a master\n" + " -s QQ source address to limit to\n" + " -d ZZ override destination address (instead of own address)\n" + " PB SB primary/secondary command byte\n" + " ID further ID bytes\n" + " DD data bytes (only length used with -m)"; + return RESULT_OK; + } + if (isMaster(dstAddress)) { + master = true; + } else if (dstAddress == SYN) { + dstAddress = master ? m_address : getSlaveAddress(m_address); + } + if (!m_protocol->setAnswer(srcAddress, dstAddress, id[0], id[1], id.data()+2, id.size()-2, answer)) { + return RESULT_ERR_INVALID_ARG; + } + return RESULT_OK; +} + +result_t MainLoop::executeDirect(const vector& args, RequestMode* reqMode, ostringstream* ostream) { + if (reqMode->listenMode != lm_direct) { if (args.size() == 1) { - *mode = cm_direct; + reqMode->listenMode = lm_direct; m_busHandler->enableGrab(true); // needed for listening to all messages *ostream << "direct mode started"; return RESULT_OK; @@ -1298,7 +1319,7 @@ result_t MainLoop::executeDirect(const vector& args, ClientMode* mode, o if (args.size() > 0) { string firstArg = args[0]; if (firstArg == "stop") { - *mode = cm_normal; + reqMode->listenMode = lm_none; *ostream << "direct mode stopped"; return RESULT_OK; } @@ -1311,7 +1332,7 @@ result_t MainLoop::executeDirect(const vector& args, ClientMode* mode, o } *ostream << ":"; if (!m_enableHex) { - *ostream << "ERR: command not enabled"; + *ostream << "ERR: hex command not enabled"; return RESULT_OK; } size_t argPos = 0; @@ -1502,7 +1523,7 @@ result_t MainLoop::executeFind(const vector& args, const string& levels, } else if (hexFormat) { *ostream << message->getLastMasterData().getStr() << " / " << message->getLastSlaveData().getStr(); } else { - result_t ret = message->decodeLastData(false, nullptr, -1, verbosity, ostream); + result_t ret = message->decodeLastData(pt_any, false, nullptr, -1, verbosity, ostream); if (ret != RESULT_OK) { *ostream << " (" << getResultCode(ret) << " for " << message->getLastMasterData().getStr() @@ -1550,7 +1571,7 @@ result_t MainLoop::executeFind(const vector& args, const string& levels, return RESULT_OK; } -result_t MainLoop::executeListen(const vector& args, ClientSettings* settings, ostringstream* ostream) { +result_t MainLoop::executeListen(const vector& args, RequestMode* reqMode, ostringstream* ostream) { size_t argPos = 1; OutputFormat verbosity = OF_NONE; bool listenWithUnknown = false; @@ -1584,17 +1605,17 @@ result_t MainLoop::executeListen(const vector& args, ClientSettings* set argPos++; } if (argPos > 0 && args.size() == argPos) { - settings->format = verbosity; - settings->listenWithUnknown = listenWithUnknown; - settings->listenOnlyUnknown = listenOnlyUnknown; + reqMode->format = verbosity; + reqMode->listenWithUnknown = listenWithUnknown; + reqMode->listenOnlyUnknown = listenOnlyUnknown; if (listenWithUnknown || listenOnlyUnknown) { m_busHandler->enableGrab(true); // needed for listening to all messages } - if (settings->mode == cm_listen) { + if (reqMode->listenMode == lm_listen) { *ostream << "listen continued"; return RESULT_OK; } - settings->mode = cm_listen; + reqMode->listenMode = lm_listen; *ostream << "listen started"; return RESULT_OK; } @@ -1610,7 +1631,7 @@ result_t MainLoop::executeListen(const vector& args, ClientSettings* set " -U only show unknown messages"; return RESULT_OK; } - settings->mode = cm_normal; + reqMode->listenMode = lm_none; *ostream << "listen stopped"; return RESULT_OK; } @@ -1621,11 +1642,11 @@ result_t MainLoop::executeState(const vector& args, ostringstream* ostre " Report bus state."; return RESULT_OK; } - if (m_busHandler->hasSignal()) { + if (m_protocol->hasSignal()) { *ostream << "signal acquired, " - << m_busHandler->getSymbolRate() << " symbols/sec (" - << m_busHandler->getMaxSymbolRate() << " max), " - << m_busHandler->getMasterCount() << " masters"; + << m_protocol->getSymbolRate() << " symbols/sec (" + << m_protocol->getMaxSymbolRate() << " max), " + << m_protocol->getMasterCount() << " masters"; return RESULT_OK; } return RESULT_ERR_NO_SIGNAL; @@ -1699,6 +1720,7 @@ result_t MainLoop::executeDefine(const vector& args, ostringstream* ostr result_t MainLoop::executeDecode(const vector& args, ostringstream* ostream) { size_t argPos = 1; OutputFormat verbosity = OF_NONE; + bool toMaster = false; while (args.size() > argPos && args[argPos][0] == '-') { if (args[argPos] == "-v") { if ((verbosity & VERBOSITY_3) == VERBOSITY_0) { @@ -1712,6 +1734,8 @@ result_t MainLoop::executeDecode(const vector& args, ostringstream* ostr verbosity |= VERBOSITY_2; } else if (args[argPos] == "-vvv" || args[argPos] == "-V") { verbosity |= VERBOSITY_3; + } else if (args[argPos] == "-m") { + toMaster = true; } else if (args[argPos] == "-n") { verbosity = (verbosity & ~OF_VALUENAME) | OF_NUMERIC; } else if (args[argPos] == "-N") { @@ -1728,10 +1752,11 @@ result_t MainLoop::executeDecode(const vector& args, ostringstream* ostr if (argPos == 0 || args.size() != argPos + 2) { *ostream << - "usage: decode [-v|-V] [-n|-N] DEFINITION DD[DD]*\n" + "usage: decode [-v|-V] [-m] [-n|-N] DEFINITION DD[DD]*\n" " Decode field(s) by definition and hex data.\n" " -v increase verbosity (include names/units/comments)\n" " -V be very verbose (include names, units, and comments)\n" + " -m target a master instead of a slave\n" " -n use numeric value of value=name pairs\n" " -N use numeric and named value of value=name pairs\n" " DEFINITION field definition (type,divisor/values,unit,comment,...)\n" @@ -1743,29 +1768,46 @@ result_t MainLoop::executeDecode(const vector& args, ostringstream* ostr time(&now); istringstream defstr("#\n" + args[argPos]); // ensure first line is not used for determining col names string errorDescription; - DataFieldTemplates* templates = getTemplates("*"); - LoadableDataFieldSet fields("", templates); + DataFieldTemplates* templates = m_scanHelper->getTemplates("*"); + LoadableDataFieldSet fields("", templates, toMaster); result_t ret = fields.readFromStream(&defstr, "temporary", now, true, nullptr, &errorDescription); if (ret != RESULT_OK) { return ret; } - SlaveSymbolString slave; - slave.push_back(0); // dummy length - ret = slave.parseHex(args[argPos+1]); + SlaveSymbolString sdata; + MasterSymbolString mdata; + SymbolString* data = toMaster ? &mdata : (SymbolString*)&sdata; + data->adjustHeader(); + ret = data->parseHex(args[argPos+1]); if (ret != RESULT_OK) { return ret; } - slave.adjustHeader(); - return fields.read(slave, 0, false, nullptr, -1, verbosity, -1, ostream); + data->adjustHeader(); + return fields.read(*data, 0, false, nullptr, -1, verbosity, -1, ostream); } result_t MainLoop::executeEncode(const vector& args, ostringstream* ostream) { size_t argPos = 1; - if (argPos == 0 || args.size() != argPos + 2) { + bool toMaster = false; + while (args.size() > argPos && args[argPos][0] == '-') { + if (args[argPos] == "-m") { + toMaster = true; + } else { + argPos = 0; // print usage + break; + } + argPos++; + } + if (args.size() != argPos + 2) { + argPos = 0; // print usage + } + + if (argPos == 0) { *ostream << - "usage: encode DEFINITION VALUE[;VALUE]*\n" + "usage: encode [-m] DEFINITION VALUE[;VALUE]*\n" " Encode field(s) by definition and decoded value(s).\n" + " -m target a master instead of a slave\n" " DEFINITION field definition (type,divisor/values,unit,comment,...)\n" " VALUE single field VALUE to encode"; return RESULT_OK; @@ -1775,19 +1817,22 @@ result_t MainLoop::executeEncode(const vector& args, ostringstream* ostr time(&now); istringstream defstr("#\n" + args[argPos]); // ensure first line is not used for determining col names string errorDescription; - DataFieldTemplates* templates = getTemplates("*"); - LoadableDataFieldSet fields("", templates); + DataFieldTemplates* templates = m_scanHelper->getTemplates("*"); + LoadableDataFieldSet fields("", templates, toMaster); result_t ret = fields.readFromStream(&defstr, "temporary", now, true, nullptr, &errorDescription); if (ret != RESULT_OK) { return ret; } istringstream datastr(args[argPos+1]); - SlaveSymbolString slave; - ret = fields.write(UI_FIELD_SEPARATOR, 0, &datastr, &slave, nullptr); + SlaveSymbolString sdata; + MasterSymbolString mdata; + SymbolString* data = toMaster ? &mdata : (SymbolString*)&sdata; + data->adjustHeader(); + ret = fields.write(UI_FIELD_SEPARATOR, 0, &datastr, data, nullptr); if (ret != RESULT_OK) { return ret; } - *ostream << slave.getStr(1); + *ostream << data->getStr(toMaster ? 5 : 1); return ret; } @@ -1819,6 +1864,28 @@ result_t MainLoop::executeScan(const vector& args, const string& levels, return RESULT_OK; } + if (args[1] == "status") { + switch (m_scanStatus) { + case SCAN_STATUS_RUNNING: + *ostream << "running"; + break; + case SCAN_STATUS_FINISHED: + *ostream << "finished"; + break; + default: + *ostream << "unused"; + break; + } + unsigned int running = m_busHandler->getRunningScans(); + if (running > 0) { + *ostream << ", some messages pending"; + } + return RESULT_OK; + } + + if (!m_protocol->hasSignal()) { + return RESULT_ERR_NO_SIGNAL; + } result_t result; symbol_t dstAddress = (symbol_t)parseInt(args[1].c_str(), 16, 0, 0xff, &result); if (result == RESULT_OK && !isValidAddress(dstAddress, false)) { @@ -1839,7 +1906,8 @@ result_t MainLoop::executeScan(const vector& args, const string& levels, *ostream << "usage: scan [full|ZZ]\n" " or: scan result\n" - " Scan seen slaves, all slaves (full), a single slave (address ZZ), or report scan result."; + " or: scan status\n" + " Scan seen slaves, all slaves (full), a single slave (address ZZ), or report scan result or status."; return RESULT_OK; } @@ -1877,15 +1945,7 @@ result_t MainLoop::executeRaw(const vector& args, ostringstream* ostream " Toggle logging of messages or each byte."; return RESULT_OK; } - bool enabled; - m_logRawBytes = bytes; - if (m_logRawFile) { - enabled = !m_logRawFile->isEnabled(); - m_logRawFile->setEnabled(enabled); - } else { - enabled = !m_logRawEnabled; - m_logRawEnabled = enabled; - } + bool enabled = m_protocol->toggleLogRaw(bytes); *ostream << (enabled ? "raw logging enabled" : "raw logging disabled"); return RESULT_OK; } @@ -1896,12 +1956,11 @@ result_t MainLoop::executeDump(const vector& args, ostringstream* ostrea " Toggle binary dump of received bytes."; return RESULT_OK; } - if (!m_dumpFile) { + if (!m_protocol->hasDumpFile()) { *ostream << "dump not configured"; return RESULT_OK; } - bool enabled = !m_dumpFile->isEnabled(); - m_dumpFile->setEnabled(enabled); + bool enabled = m_protocol->toggleDump(); *ostream << (enabled ? "dump enabled" : "dump disabled"); return RESULT_OK; } @@ -1913,7 +1972,8 @@ result_t MainLoop::executeReload(const vector& args, ostringstream* ostr return RESULT_OK; } m_busHandler->clear(); - return loadConfigFiles(m_messages); + m_scanHelper->loadConfigFiles(!m_scanConfig); + return RESULT_OK; } result_t MainLoop::executeInfo(const vector& args, const string& user, ostringstream* ostream) { @@ -1927,22 +1987,8 @@ result_t MainLoop::executeInfo(const vector& args, const string& user, o if (!m_updateCheck.empty()) { *ostream << "update check: " << m_updateCheck << "\n"; } - *ostream << "device: " << m_device->getName(); - if (m_device->isEnhancedProto()) { - *ostream << ", enhanced"; - } - if (m_device->isReadOnly()) { - *ostream << ", readonly"; - } - if (!m_device->isValid()) { - *ostream << ", invalid"; - } - if (verbose) { - string info = m_device->getEnhancedInfos(); - if (!info.empty()) { - *ostream << ", " << info; - } - } + *ostream << "device: "; + m_protocol->formatInfo(ostream, verbose, false); *ostream << "\n"; if (!user.empty()) { *ostream << "user: " << user << "\n"; @@ -1951,27 +1997,38 @@ result_t MainLoop::executeInfo(const vector& args, const string& user, o if (!user.empty() || !levels.empty()) { *ostream << "access: " << levels << "\n"; } - if (m_busHandler->hasSignal()) { + if (m_protocol->hasSignal()) { *ostream << "signal: acquired\n" - << "symbol rate: " << m_busHandler->getSymbolRate() << "\n" - << "max symbol rate: " << m_busHandler->getMaxSymbolRate() << "\n"; - if (m_busHandler->getMinArbitrationDelay() >= 0) { - *ostream << "min arbitration micros: " << m_busHandler->getMinArbitrationDelay() << "\n" - << "max arbitration micros: " << m_busHandler->getMaxArbitrationDelay() << "\n"; + << "symbol rate: " << m_protocol->getSymbolRate() << "\n" + << "max symbol rate: " << m_protocol->getMaxSymbolRate() << "\n"; + if (m_protocol->getMinArbitrationDelay() >= 0) { + *ostream << "min arbitration micros: " << m_protocol->getMinArbitrationDelay() << "\n" + << "max arbitration micros: " << m_protocol->getMaxArbitrationDelay() << "\n"; + } + if (m_protocol->getMinSymbolLatency() >= 0) { + *ostream << "min symbol latency: " << m_protocol->getMinSymbolLatency() << "\n" + << "max symbol latency: " << m_protocol->getMaxSymbolLatency() << "\n"; } - if (m_busHandler->getMinSymbolLatency() >= 0) { - *ostream << "min symbol latency: " << m_busHandler->getMinSymbolLatency() << "\n" - << "max symbol latency: " << m_busHandler->getMaxSymbolLatency() << "\n"; + if (m_scanStatus != SCAN_STATUS_NONE) { + *ostream << "scan: " << (m_scanStatus == SCAN_STATUS_FINISHED ? "finished" : "running"); + unsigned int running = m_busHandler->getRunningScans(); + if (running > 0) { + *ostream << ", some messages pending"; + } + *ostream << "\n"; } } else { *ostream << "signal: no signal\n"; } *ostream << "reconnects: " << m_reconnectCount << "\n" - << "masters: " << m_busHandler->getMasterCount() << "\n" + << "masters: " << m_protocol->getMasterCount() << "\n" << "messages: " << m_messages->size() << "\n" << "conditional: " << m_messages->sizeConditional() << "\n" << "poll: " << m_messages->sizePoll() << "\n" << "update: " << m_messages->sizePassive(); + if (verbose) { + *ostream << "\nconfig path: " << m_scanHelper->getConfigPath(); + } m_busHandler->formatSeenInfo(ostream); return RESULT_OK; } @@ -1998,20 +2055,23 @@ result_t MainLoop::executeHelp(ostringstream* ostream) { " Write by new def.: write [-s QQ] [-d ZZ] -def DEFINITION [VALUE[;VALUE]*] (if enabled)\n" " Write hex message: write [-s QQ] [-c CIRCUIT] -h ZZPBSBNN[DD]*\n" " auth|a Authenticate user: auth USER SECRET\n" - " hex Send hex data: hex [-s QQ] [-n] ZZPBSB[NN][DD]* (if enabled)\n" " find|f Find message(s): find [-v|-V] [-r] [-w] [-p] [-a] [-d] [-h] [-i ID] [-f] [-F COL[,COL]*] [-e]" " [-c CIRCUIT] [-l LEVEL] [NAME]\n" " listen|l Listen for updates: listen [-v|-V] [-n|-N] [-u|-U] [stop]\n" + " hex Send hex data: hex [-s QQ] [-n] ZZPBSB[NN][DD]* (if enabled)\n" + " inject Inject hex data: inject QQZZPBSBNN[DD]*/[NN[DD]*] (if enabled)\n" + " answer Answer a message: answer [-m] [-s QQ] [-d ZZ] PBSB[ID]* [DD]* (if enabled)\n" " direct Enter direct mode\n" " state|s Report bus state\n" " info|i Report information about the daemon, configuration, seen participants, and the device.\n" " grab|g Grab messages: grab [stop]\n" " Report the messages: grab result [all|decode]\n" - " define Define new message: define [-r] DEFINITION\n" + " define Define new message: define [-r] DEFINITION (if enabled)\n" " decode|d Decode field(s): decode [-v|-V] [-n|-N] DEFINITION DD[DD]*\n" " encode|e Encode field(s): encode DEFINITION VALUE[;VALUE]*\n" " scan Scan slaves: scan [full|ZZ]\n" " Report scan result: scan result\n" + " Report scan status: scan status\n" " log Set log area level: log [AREA[,AREA]* LEVEL]\n" " raw Toggle logging of messages or each byte.\n" " dump Toggle binary dump of received bytes\n" @@ -2045,7 +2105,7 @@ result_t MainLoop::executeGet(const vector& args, bool* connected, ostri circuit = uri.substr(6, pos - 6); name = uri.substr(pos + 1); } - bool required = false, full = false, withWrite = false, raw = false; + bool required = false, full = false, withWrite = false; bool withDefinition = false; string newDefinition; OutputFormat verbosity = OF_NAMES; @@ -2099,7 +2159,9 @@ result_t MainLoop::executeGet(const vector& args, bool* connected, ostri } else if (qname == "write") { withWrite = parseBoolQuery(value); } else if (qname == "raw") { - raw = parseBoolQuery(value); + if (parseBoolQuery(value)) { + verbosity |= OF_RAWDATA; + } } else if (qname == "def") { withDefinition = parseBoolQuery(value); } else if (qname == "define") { @@ -2191,7 +2253,7 @@ result_t MainLoop::executeGet(const vector& args, bool* connected, ostri Message* next = *(it+1); same = next->getCircuit() == lastCircuit && next->getName() == name; } - message->decodeJson(!first, same, true, raw, verbosity, ostream); + message->decodeJson(!first, same, true, verbosity, ostream); lastName = name; first = false; } @@ -2211,24 +2273,24 @@ result_t MainLoop::executeGet(const vector& args, bool* connected, ostri if (!user.empty() || !levels.empty()) { *ostream << ",\n \"access\": \"" << levels << "\""; } - *ostream << ",\n \"signal\": " << (m_busHandler->hasSignal() ? "true" : "false"); - if (m_busHandler->hasSignal()) { - *ostream << ",\n \"symbolrate\": " << m_busHandler->getSymbolRate() - << ",\n \"maxsymbolrate\": " << m_busHandler->getMaxSymbolRate(); - if (m_busHandler->getMinArbitrationDelay() >= 0) { - *ostream << ",\n \"minarbitrationmicros\": " << m_busHandler->getMinArbitrationDelay() - << ",\n \"maxarbitrationmicros\": " << m_busHandler->getMaxArbitrationDelay(); + *ostream << ",\n \"signal\": " << (m_protocol->hasSignal() ? "true" : "false"); + if (m_protocol->hasSignal()) { + *ostream << ",\n \"symbolrate\": " << m_protocol->getSymbolRate() + << ",\n \"maxsymbolrate\": " << m_protocol->getMaxSymbolRate(); + if (m_protocol->getMinArbitrationDelay() >= 0) { + *ostream << ",\n \"minarbitrationmicros\": " << m_protocol->getMinArbitrationDelay() + << ",\n \"maxarbitrationmicros\": " << m_protocol->getMaxArbitrationDelay(); } - if (m_busHandler->getMinSymbolLatency() >= 0) { - *ostream << ",\n \"minsymbollatency\": " << m_busHandler->getMinSymbolLatency() - << ",\n \"maxsymbollatency\": " << m_busHandler->getMaxSymbolLatency(); + if (m_protocol->getMinSymbolLatency() >= 0) { + *ostream << ",\n \"minsymbollatency\": " << m_protocol->getMinSymbolLatency() + << ",\n \"maxsymbollatency\": " << m_protocol->getMaxSymbolLatency(); } } - if (!m_device->isReadOnly()) { + if (!m_protocol->isReadOnly()) { *ostream << ",\n \"qq\": " << static_cast(m_address); } *ostream << ",\n \"reconnects\": " << m_reconnectCount - << ",\n \"masters\": " << m_busHandler->getMasterCount() + << ",\n \"masters\": " << m_protocol->getMasterCount() << ",\n \"messages\": " << m_messages->size() << ",\n \"lastup\": " << static_cast(maxLastUp) << "\n }" @@ -2242,7 +2304,19 @@ result_t MainLoop::executeGet(const vector& args, bool* connected, ostri if (uri == "/datatypes") { *ostream << "["; OutputFormat verbosity = OF_NAMES|OF_JSON|OF_ALL_ATTRS; - DataTypeList::getInstance()->dump(verbosity, true, ostream); + DataTypeList::getInstance()->dump(verbosity, ostream); + *ostream << "\n]"; + type = 6; + *connected = false; + return formatHttpResult(ret, type, ostream); + } + + if (uri == "/templates" || uri.substr(0, 11) == "/templates/") { + *ostream << "["; + OutputFormat verbosity = OF_NAMES|OF_JSON|OF_ALL_ATTRS; + string name = uri == "/templates" ? "" : uri.substr(11) + "/"; + const auto tmpl = m_scanHelper->getTemplates(name); + tmpl->dump(verbosity, ostream); *ostream << "\n]"; type = 6; *connected = false; @@ -2325,8 +2399,8 @@ result_t MainLoop::executeGet(const vector& args, bool* connected, ostri time(&now); istringstream defstr("#\n" + def); // ensure first line is not used for determining col names string errorDescription; - DataFieldTemplates* templates = getTemplates("*"); - LoadableDataFieldSet fields("", templates); + DataFieldTemplates* templates = m_scanHelper->getTemplates("*"); + LoadableDataFieldSet fields("", templates, false); ret = fields.readFromStream(&defstr, "temporary", now, true, nullptr, &errorDescription); if (ret == RESULT_OK && fields.size()) { SlaveSymbolString slave; diff --git a/src/ebusd/mainloop.h b/src/ebusd/mainloop.h index 1092182ce..142ad89c2 100644 --- a/src/ebusd/mainloop.h +++ b/src/ebusd/mainloop.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,10 +26,11 @@ #include #include "ebusd/bushandler.h" #include "ebusd/datahandler.h" -#include "ebusd/network.h" +#include "ebusd/request.h" +#include "ebusd/scan.h" #include "lib/ebus/filereader.h" #include "lib/ebus/message.h" -#include "lib/utils/rotatefile.h" +#include "lib/ebus/protocol.h" #include "lib/utils/httpclient.h" namespace ebusd { @@ -98,15 +99,18 @@ class UserList : public UserInfo, public MappedFileReader { /** * The main loop handling requests from connected clients. */ -class MainLoop : public Thread, DeviceListener { +class MainLoop : public Thread { public: /** - * Construct the main loop and create network and bus handling components. + * Construct the main loop and create bus handling components. * @param opt the program options. - * @param device the @a Device instance. + * @param busHandler @a BusHandler instance. * @param messages the @a MessageMap instance. + * @param scanHelper the @a ScanHelper instance. + * @param requestQueue the reference to the @a Request @a Queue. */ - MainLoop(const struct options& opt, Device *device, MessageMap* messages); + MainLoop(const struct options& opt, BusHandler* busHandler, + MessageMap* messages, ScanHelper* scanHelper, Queue* requestQueue); /** * Destructor. @@ -116,25 +120,7 @@ class MainLoop : public Thread, DeviceListener { /** * Shutdown the main loop. */ - void shutdown() { m_shutdown = true; } - - /** - * Get the @a BusHandler instance. - * @return the created @a BusHandler instance. - */ - BusHandler* getBusHandler() { return m_busHandler; } - - /** - * Add a client @a NetMessage to the queue. - * @param message the client @a NetMessage to handle. - */ - void addMessage(NetMessage* message) { m_netQueue.push(message); } - - // @copydoc - void notifyDeviceData(symbol_t symbol, bool received) override; - - // @copydoc - void notifyStatus(bool error, const char* message) override; + void shutdown(); protected: @@ -142,21 +128,21 @@ class MainLoop : public Thread, DeviceListener { void run() override; - private: + public: /** - * Decode and execute client message. - * @param data the data string to decode (may be empty). + * Decode and execute client request. + * @param req the @a Request to decode. * @param connected set to false when the client connection shall be closed. - * @param isHttp true for HTTP message. - * @param settings set to the new client settings. + * @param reqMode the @a RequestMode to use and update. * @param user set to the new user name when changed by authentication. * @param reload set to true when the configuration files were reloaded. * @param ostream the @a ostringstream to format the result string to. * @return the result code. */ - result_t decodeMessage(const string& data, bool isHttp, bool* connected, ClientSettings* settings, + result_t decodeRequest(Request* req, bool* connected, RequestMode* reqMode, string* user, bool* reload, ostringstream* ostream); + private: /** * Parse the hex master message from the remaining arguments. * @param args the arguments passed to the command. @@ -222,14 +208,31 @@ class MainLoop : public Thread, DeviceListener { */ result_t executeHex(const vector& args, ostringstream* ostream); + /** + * Execute the inject command. + * @param args the arguments passed to the command (starting with the command itself), or empty for help. + * @param ostream the @a ostringstream to format the result string to. + * @return the result code. + */ + result_t executeInject(const vector& args, ostringstream* ostream); + /** * Execute the direct command. * @param args the arguments passed to the command (starting with the command itself), or empty for help. - * @param mode set to the new client mode. + * @param reqMode the @a RequestMode to use and update. + * @param ostream the @a ostringstream to format the result string to. + * @return the result code. + */ + result_t executeDirect(const vector& args, RequestMode* reqMode, ostringstream* ostream); + + /** + * Execute the answer command. + * @param args the arguments passed to the command (starting with the command itself), or empty for help. + * @param reqMode the @a RequestMode to use and update. * @param ostream the @a ostringstream to format the result string to. * @return the result code. */ - result_t executeDirect(const vector& args, ClientMode* mode, ostringstream* ostream); + result_t executeAnswer(const vector& args, ostringstream* ostream); /** * Execute the find command. @@ -243,11 +246,11 @@ class MainLoop : public Thread, DeviceListener { /** * Execute the listen command. * @param args the arguments passed to the command (starting with the command itself), or empty for help. - * @param settings set to the new client settings. + * @param reqMode the @a RequestMode to use and update. * @param ostream the @a ostringstream to format the result string to. * @return the result code. */ - result_t executeListen(const vector& args, ClientSettings* settings, ostringstream* ostream); + result_t executeListen(const vector& args, RequestMode* reqMode, ostringstream* ostream); /** * Execute the state command. @@ -373,39 +376,24 @@ class MainLoop : public Thread, DeviceListener { */ result_t formatHttpResult(result_t ret, int type, ostringstream* ostream); - /** the @a Device instance. */ - Device* m_device; + /** the @a BusHandler instance. */ + BusHandler* m_busHandler; + + /** the @a ProtocolHandler instance. */ + ProtocolHandler* m_protocol; /** the number of reconnects requested from the @a Device. */ unsigned int m_reconnectCount; - /** the @a RotateFile for writing sent/received bytes in log format, or nullptr. */ - RotateFile* m_logRawFile; - - /** whether raw logging to @p logNotice is enabled (only relevant if m_logRawFile is nullptr). */ - bool m_logRawEnabled; - - /** whether to log raw bytes instead of messages with @a m_logRawEnabled. */ - bool m_logRawBytes; - - /** the buffer for building log raw message. */ - ostringstream m_logRawBuffer; - - /** true when the last byte in @a m_logRawBuffer was receive, false if it was sent. */ - bool m_logRawLastReceived; - - /** the last sent/received symbol.*/ - symbol_t m_logRawLastSymbol; - - /** the @a RotateFile for dumping received data, or nullptr. */ - RotateFile* m_dumpFile; - /** the @a UserList instance. */ UserList m_userList; /** the @a MessageMap instance. */ MessageMap* m_messages; + /** the @a ScanHelper instance. */ + ScanHelper* m_scanHelper; + /** the own master address for sending on the bus. */ const symbol_t m_address; @@ -416,10 +404,16 @@ class MainLoop : public Thread, DeviceListener { * (@a ESC=none, 0xfe=broadcast ident, @a SYN=full scan, else: single slave address). */ const symbol_t m_initialScan; + /** number of retries for scanning a device. */ + const int m_scanRetries; + + /** the current scan status. */ + scanStatus_t m_scanStatus; + /** true when the poll interval is non zero. */ const bool m_polling; - /** whether to enable the hex command. */ + /** whether to enable the hex, inject, and answer commands. */ const bool m_enableHex; /** the MessageMap for handling newly defined messages for testing (if enabled), or nullptr. */ @@ -434,14 +428,8 @@ class MainLoop : public Thread, DeviceListener { /** the @a HttpClient for performing the update check. */ HttpClient m_httpClient; - /** the created @a BusHandler instance. */ - BusHandler* m_busHandler; - - /** the created @a Network instance. */ - Network* m_network; - - /** the @a NetMessage @a Queue. */ - Queue m_netQueue; + /** the reference to the @a Request @a Queue. */ + Queue* m_requestQueue; /** the path for HTML files served by the HTTP port. */ string m_htmlPath; diff --git a/src/ebusd/mqttclient.cpp b/src/ebusd/mqttclient.cpp new file mode 100644 index 000000000..3cefb1a12 --- /dev/null +++ b/src/ebusd/mqttclient.cpp @@ -0,0 +1,34 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include "ebusd/mqttclient.h" +#include "ebusd/mqttclient_mosquitto.h" + +namespace ebusd { + +// @copydoc +MqttClient* MqttClient::create(mqtt_client_config_t config, MqttClientListener *listener) { + return new MqttClientMosquitto(config, listener); +} + +} // namespace ebusd diff --git a/src/ebusd/mqttclient.h b/src/ebusd/mqttclient.h new file mode 100755 index 000000000..9ac00aad6 --- /dev/null +++ b/src/ebusd/mqttclient.h @@ -0,0 +1,158 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EBUSD_MQTTCLIENT_H_ +#define EBUSD_MQTTCLIENT_H_ + +#include +#include +#include +#include +#include +#include + +namespace ebusd { + +/** \file ebusd/mqttclient.h + * An abstraction for an MQTT client. + */ + +using std::map; +using std::pair; +using std::string; +using std::vector; + +/** settings for the connection to an MQTT broker. */ +typedef struct mqtt_client_config { + const char* host; //!< host name or IP address of MQTT broker + uint16_t port; //!< optional port of MQTT broker + const char* clientId; //!< optional clientid override for MQTT broker + const char* username; //!< optional user name for MQTT broker + const char* password; //!< optional password for MQTT broker + bool logEvents; //!< whether to log library events + bool version311; //!< true to use protocol version 3.1.1 + bool ignoreInvalidParams; //!< ignore invalid parameters during init + const char* cafile; //!< optional CA file for TLS + const char* capath; //!< optional CA path for TLS + const char* certfile; //!< optional client certificate file for TLS + const char* keyfile; //!< optional client key file for TLS + const char* keypass; //!< optional client key file password for TLS + bool insecure; //!< whether to allow insecure TLS connection + const char* lastWillTopic; //!< optional last will topic. + const char* lastWillData; //!< optional last will data. +} mqtt_client_config_t; + + +/** + * Interface for listening to MQTT client events. + */ +class MqttClientListener { + public: + /** + * Destructor. + */ + virtual ~MqttClientListener() {} + + /** + * Notification of status of connection to the broker. + */ + virtual void notifyMqttStatus(bool connected) = 0; // abstract + + /** + * Notification of a received MQTT message. + * @param topic the topic string. + * @param data the data string. + */ + virtual void notifyMqttTopic(const string& topic, const string& data) = 0; // abstract +}; + + +/** + * An abstract MQTT client. + */ +class MqttClient { + public: + /** + * Constructor. + * @param config the client configuration to use. + * @param listener the client listener to use. + */ + MqttClient(const mqtt_client_config_t config, MqttClientListener *listener) + : m_config(config), m_listener(listener) {} + + /** + * Destructor. + */ + virtual ~MqttClient() {} + + /** + * Create a new instance. + * @param config the client configuration to use. + * @param listener the client listener to use. + * @return the new MqttClient, or @a nullptr on error. + */ + static MqttClient* create(mqtt_client_config_t config, MqttClientListener *listener); + + /** + * Connect to the broker and start handling MQTT traffic. + * @param isAsync set to true if the asynchronous client was started and @a run() does not + * have to be called at all, false if the client is synchronous and does + * it's work in @a run() only. + * @param connected set to true if the connection was already established. + * @return true on success, false if connection failed and the client is no longer usable (i.e. should be destroyed). + */ + virtual bool connect(bool &isAsync, bool &connected) = 0; // abstract + + /** + * Called regularly to handle MQTT traffic. + * @param allowReconnect true when reconnecting to the broker is allowed. + * @return true on error for waiting a bit until next call, or false otherwise. + */ + virtual bool run(bool allowReconnect, bool &connected) = 0; // abstract + + /** + * Publish a topic update. + * @param topic the topic string. + * @param data the data string. + * @param retain whether the topic shall be retained. + */ + virtual void publishTopic(const string& topic, const string& data, int qos, bool retain = false) = 0; // abstract + + /** + * Publish a topic update without any data. + * @param topic the topic string. + */ + virtual void publishEmptyTopic(const string& topic, int qos, bool retain = false) = 0; // abstract + + /** + * Subscribe to the specified topic pattern. + * @param topic the topic pattern string to subscribe to. + */ + virtual void subscribeTopic(const string& topic) = 0; // abstract + + public: + /** the client configuration to use. */ + const mqtt_client_config_t m_config; + + /** the @a MqttClientListener instance. */ + MqttClientListener* m_listener; +}; + +} // namespace ebusd + +#endif // EBUSD_MQTTCLIENT_H_ diff --git a/src/ebusd/mqttclient_mosquitto.cpp b/src/ebusd/mqttclient_mosquitto.cpp new file mode 100755 index 000000000..852a0a975 --- /dev/null +++ b/src/ebusd/mqttclient_mosquitto.cpp @@ -0,0 +1,319 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "ebusd/mqttclient.h" +#include "ebusd/mqttclient_mosquitto.h" +#include +#include +#include +#include +#include "lib/utils/log.h" +#include "lib/ebus/symbol.h" + +namespace ebusd { + + +bool check(int code, const char* method) { + if (code == MOSQ_ERR_SUCCESS) { + return true; + } + if (code == MOSQ_ERR_ERRNO) { + char* error = strerror(errno); + logOtherError("mqtt", "%s: errno %d=%s", method, errno, error); + return false; + } +#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) + const char* msg = mosquitto_strerror(code); + logOtherError("mqtt", "%s: %s", method, msg); +#else + logOtherError("mqtt", "%s: error code %d", method, code); +#endif + return false; +} + + + +#if (LIBMOSQUITTO_MAJOR >= 1) +int on_keypassword(char *buf, int size, int rwflag, void *userdata) { + MqttClientMosquitto* client = reinterpret_cast(userdata); + if (!client || !client->m_config.keypass) { + return 0; + } + int len = static_cast(strlen(client->m_config.keypass)); + if (len > size) { + len = size; + } + memcpy(buf, client->m_config.keypass, len); + return len; +} +#endif + +void on_connect( +#if (LIBMOSQUITTO_MAJOR >= 1) + struct mosquitto *mosq, +#endif + void *obj, int rc) { + if (rc == 0) { + logOtherNotice("mqtt", "connection established"); + MqttClientMosquitto* client = reinterpret_cast(obj); + if (client) { + client->m_listener->notifyMqttStatus(true); + } + } else { + if (rc >= 1 && rc <= 3) { + logOtherError("mqtt", "connection refused: %s", + rc == 1 ? "wrong protocol" : (rc == 2 ? "wrong username/password" : "broker down")); + } else { + logOtherError("mqtt", "connection refused: %d", rc); + } + } +} + +#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) +void on_log(struct mosquitto *mosq, void *obj, int level, const char* msg) { + switch (level) { + case MOSQ_LOG_DEBUG: + logOtherDebug("mqtt", "log %s", msg); + break; + case MOSQ_LOG_INFO: + logOtherInfo("mqtt", "log %s", msg); + break; + case MOSQ_LOG_NOTICE: + logOtherNotice("mqtt", "log %s", msg); + break; + case MOSQ_LOG_WARNING: + logOtherNotice("mqtt", "log warning %s", msg); + break; + case MOSQ_LOG_ERR: + logOtherError("mqtt", "log %s", msg); + break; + default: + logOtherError("mqtt", "log other %s", msg); + break; + } +} +#endif + +void on_message( +#if (LIBMOSQUITTO_MAJOR >= 1) + struct mosquitto *mosq, +#endif + void *obj, const struct mosquitto_message *message) { + MqttClientMosquitto* client = reinterpret_cast(obj); + if (!client || !message) { + return; + } + string topic(message->topic); + string data(message->payloadlen > 0 ? reinterpret_cast(message->payload) : ""); + client->m_listener->notifyMqttTopic(topic, data); +} + +MqttClientMosquitto::MqttClientMosquitto(mqtt_client_config_t config, MqttClientListener *listener) + : MqttClient(config, listener), + m_mosquitto(nullptr), + m_initialConnectFailed(false), + m_lastErrorLogTime(0) { + int major = -1; + int minor = -1; + int revision = -1; + mosquitto_lib_version(&major, &minor, &revision); + if (major < LIBMOSQUITTO_MAJOR) { + logOtherError("mqtt", "invalid mosquitto version %d instead of %d, will try connecting anyway", major, + LIBMOSQUITTO_MAJOR); + } + logOtherInfo("mqtt", "mosquitto version %d.%d.%d (compiled with %d.%d.%d)", major, minor, revision, + LIBMOSQUITTO_MAJOR, LIBMOSQUITTO_MINOR, LIBMOSQUITTO_REVISION); + if (check(mosquitto_lib_init(), "unable to initialize")) { + signal(SIGPIPE, SIG_IGN); // needed before libmosquitto v. 1.1.3 +#if (LIBMOSQUITTO_MAJOR >= 1) + m_mosquitto = mosquitto_new(config.clientId, true, this); +#else + m_mosquitto = mosquitto_new(config.clientId, this); +#endif + if (!m_mosquitto) { + logOtherError("mqtt", "unable to instantiate"); + } + } + if (m_mosquitto) { +#if (LIBMOSQUITTO_VERSION_NUMBER >= 1004001) + check(mosquitto_threaded_set(m_mosquitto, true), "threaded_set"); + int version = config.version311 ? MQTT_PROTOCOL_V311 : MQTT_PROTOCOL_V31; + check(mosquitto_opts_set(m_mosquitto, MOSQ_OPT_PROTOCOL_VERSION, reinterpret_cast(&version)), + "opts_set protocol version"); +#else + if (config.version311) { + logOtherError("mqtt", "version 3.1.1 not supported"); + } +#endif + if (config.username || config.password) { + if (mosquitto_username_pw_set(m_mosquitto, config.username, config.password) != MOSQ_ERR_SUCCESS) { + logOtherError("mqtt", "unable to set username/password, trying without"); + } + } + if (config.lastWillTopic) { + size_t len = config.lastWillData ? strlen(config.lastWillData) : 0; +#if (LIBMOSQUITTO_MAJOR >= 1) + mosquitto_will_set(m_mosquitto, config.lastWillTopic, (uint32_t)len, + reinterpret_cast(config.lastWillData), 0, true); +#else + mosquitto_will_set(m_mosquitto, true, config.lastWillTopic, (uint32_t)len, + reinterpret_cast(config.lastWillData), 0, true); +#endif + } + + if (config.cafile || config.capath) { +#if (LIBMOSQUITTO_MAJOR >= 1) + mosquitto_user_data_set(m_mosquitto, this); + int ret; + ret = mosquitto_tls_set(m_mosquitto, config.cafile, config.capath, config.certfile, config.keyfile, + on_keypassword); + if (ret != MOSQ_ERR_SUCCESS) { + logOtherError("mqtt", "unable to set TLS: %d", ret); + } else if (config.insecure) { + ret = mosquitto_tls_insecure_set(m_mosquitto, true); + if (ret != MOSQ_ERR_SUCCESS) { + logOtherError("mqtt", "unable to set TLS insecure: %d", ret); + } + } +#else + logOtherError("mqtt", "use of TLS not supported"); +#endif + } + if (config.logEvents) { +#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) + mosquitto_log_callback_set(m_mosquitto, on_log); +#else + logOtherError("mqtt", "logging of library events not supported"); +#endif + } + mosquitto_connect_callback_set(m_mosquitto, on_connect); + // mosquitto_disconnect_callback_set(m_mosquitto, on_disconnect); + mosquitto_message_callback_set(m_mosquitto, on_message); + } +} + +bool MqttClientMosquitto::connect(bool &isAsync, bool &connected) { + isAsync = false; + if (!m_mosquitto) { + connected = false; + return false; + } + int ret; +#if (LIBMOSQUITTO_MAJOR >= 1) + ret = mosquitto_connect(m_mosquitto, m_config.host, m_config.port, 60); +#else + ret = mosquitto_connect(m_mosquitto, config.host, config.port, 60, true); +#endif + if (ret == MOSQ_ERR_INVAL && !m_config.ignoreInvalidParams) { + logOtherError("mqtt", "unable to connect (invalid parameters)"); + mosquitto_destroy(m_mosquitto); + m_mosquitto = nullptr; + connected = false; + return false; // never try again + } + if (!check(ret, "unable to connect, retrying")) { + connected = false; + m_initialConnectFailed = m_config.ignoreInvalidParams; + return true; + } + connected = true; // assume success until connect_callback says otherwise + logOtherDebug("mqtt", "connection requested"); + return true; +} + +MqttClientMosquitto::~MqttClientMosquitto() { + if (m_mosquitto) { + mosquitto_destroy(m_mosquitto); + m_mosquitto = nullptr; + } + mosquitto_lib_cleanup(); +} + +bool MqttClientMosquitto::run(bool allowReconnect, bool &connected) { + if (!m_mosquitto) { + return false; + } + int ret; +#if (LIBMOSQUITTO_MAJOR >= 1) + ret = mosquitto_loop(m_mosquitto, -1, 1); // waits up to 1 second for network traffic +#else + ret = mosquitto_loop(m_mosquitto, -1); // waits up to 1 second for network traffic +#endif + if (!connected && (ret == MOSQ_ERR_NO_CONN || ret == MOSQ_ERR_CONN_LOST) && allowReconnect) { + if (m_initialConnectFailed) { +#if (LIBMOSQUITTO_MAJOR >= 1) + ret = mosquitto_connect(m_mosquitto, m_config.host, m_config.port, 60); +#else + ret = mosquitto_connect(m_mosquitto, g_host, g_port, 60, true); +#endif + if (ret == MOSQ_ERR_INVAL) { + logOtherError("mqtt", "unable to connect (invalid parameters), retrying"); + } + if (ret == MOSQ_ERR_SUCCESS) { + m_initialConnectFailed = false; + } + } else { + ret = mosquitto_reconnect(m_mosquitto); + } + } + if (!connected && ret == MOSQ_ERR_SUCCESS) { + connected = true; + logOtherNotice("mqtt", "connection re-established"); + } + if (!connected || ret == MOSQ_ERR_SUCCESS) { + return false; + } + if (ret == MOSQ_ERR_NO_CONN || ret == MOSQ_ERR_CONN_LOST || ret == MOSQ_ERR_CONN_REFUSED) { + logOtherError("mqtt", "communication error: %s", ret == MOSQ_ERR_NO_CONN ? "not connected" + : (ret == MOSQ_ERR_CONN_LOST ? "connection lost" : "connection refused")); + connected = false; + } else { + time_t now; + time(&now); + if (now > m_lastErrorLogTime + 10) { // log at most every 10 seconds + m_lastErrorLogTime = now; + check(ret, "communication error"); + } + } + return true; +} + +void MqttClientMosquitto::publishTopic(const string& topic, const string& data, int qos, bool retain) { + const char* topicStr = topic.c_str(); + const char* dataStr = data.c_str(); + const size_t len = strlen(dataStr); + logOtherDebug("mqtt", "publish %s %s", topicStr, dataStr); + check(mosquitto_publish(m_mosquitto, nullptr, topicStr, (uint32_t)len, + reinterpret_cast(dataStr), qos, retain), "publish"); +} + +void MqttClientMosquitto::publishEmptyTopic(const string& topic, int qos, bool retain) { + const char* topicStr = topic.c_str(); + logOtherDebug("mqtt", "publish empty %s", topicStr); + check(mosquitto_publish(m_mosquitto, nullptr, topicStr, 0, nullptr, qos, retain), "publish empty"); +} + +void MqttClientMosquitto::subscribeTopic(const string& topic) { + check(mosquitto_subscribe(m_mosquitto, nullptr, topic.c_str(), 0), "subscribe"); +} + +} // namespace ebusd diff --git a/src/ebusd/mqttclient_mosquitto.h b/src/ebusd/mqttclient_mosquitto.h new file mode 100755 index 000000000..447b22c1b --- /dev/null +++ b/src/ebusd/mqttclient_mosquitto.h @@ -0,0 +1,81 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EBUSD_MQTTCLIENT_MOSQUITTO_H_ +#define EBUSD_MQTTCLIENT_MOSQUITTO_H_ + +#include "ebusd/mqttclient.h" +#include +#include +#include +#include +#include +#include +#include + +namespace ebusd { + +/** \file ebusd/mqttclient.h + * An abstraction for an MQTT client. + */ + +using std::map; +using std::pair; +using std::string; +using std::vector; + +class MqttClientMosquitto : public MqttClient { + public: + /** + * Constructor. + * @param config the client configuration to use. + * @param listener the client listener to use. + */ + MqttClientMosquitto(mqtt_client_config_t config, MqttClientListener *listener); + + virtual ~MqttClientMosquitto(); + + // @copydoc + bool connect(bool &isAsync, bool &connected) override; + + // @copydoc + bool run(bool allowReconnect, bool &connected) override; + + // @copydoc + void publishTopic(const string& topic, const string& data, int qos, bool retain = false) override; + + // @copydoc + void publishEmptyTopic(const string& topic, int qos, bool retain = false) override; + + // @copydoc + void subscribeTopic(const string& topic) override; + + private: + /** the mosquitto structure if initialized, or nullptr. */ + struct mosquitto* m_mosquitto; + + /** whether the initial connect failed. */ + bool m_initialConnectFailed; + + /** the last system time when a communication error was logged. */ + time_t m_lastErrorLogTime; +}; + +} // namespace ebusd + +#endif // EBUSD_MQTTCLIENT_MOSQUITTO_H_ diff --git a/src/ebusd/mqtthandler.cpp b/src/ebusd/mqtthandler.cpp index a67086abc..024d3e92d 100755 --- a/src/ebusd/mqtthandler.cpp +++ b/src/ebusd/mqtthandler.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -56,51 +56,60 @@ using std::dec; #define O_VERB (O_INSE+1) /** the definition of the MQTT arguments. */ -static const struct argp_option g_mqtt_argp_options[] = { - {nullptr, 0, nullptr, 0, "MQTT options:", 1 }, - {"mqtthost", O_HOST, "HOST", 0, "Connect to MQTT broker on HOST [localhost]", 0 }, - {"mqttport", O_PORT, "PORT", 0, "Connect to MQTT broker on PORT (usually 1883), 0 to disable [0]", 0 }, +static const argDef g_mqtt_argDefs[] = { + {nullptr, 0, nullptr, 0, "MQTT options:"}, + {"mqtthost", O_HOST, "HOST", 0, "Connect to MQTT broker on HOST [localhost]"}, + {"mqttport", O_PORT, "PORT", 0, "Connect to MQTT broker on PORT (usually 1883), 0 to disable [0]"}, {"mqttclientid", O_CLID, "ID", 0, "Set client ID for connection to MQTT broker [" PACKAGE_NAME "_" - PACKAGE_VERSION "_]", 0 }, - {"mqttuser", O_USER, "USER", 0, "Connect as USER to MQTT broker (no default)", 0 }, - {"mqttpass", O_PASS, "PASSWORD", 0, "Use PASSWORD when connecting to MQTT broker (no default)", 0 }, + PACKAGE_VERSION "_]"}, + {"mqttuser", O_USER, "USER", 0, "Connect as USER to MQTT broker (no default)"}, + {"mqttpass", O_PASS, "PASSWORD", 0, "Use PASSWORD when connecting to MQTT broker (no default)"}, {"mqtttopic", O_TOPI, "TOPIC", 0, - "Use MQTT TOPIC (prefix before /%circuit/%name or complete format) [ebusd]", 0 }, + "Use MQTT TOPIC (prefix before /%circuit/%name or complete format) [ebusd]"}, {"mqttglobal", O_GTOP, "TOPIC", 0, - "Use TOPIC for global data (default is \"global/\" suffix to mqtttopic prefix)", 0 }, - {"mqttretain", O_RETA, nullptr, 0, "Retain all topics instead of only selected global ones", 0 }, - {"mqttqos", O_PQOS, "QOS", 0, "Set the QoS value for all topics (0-2) [0]", 0 }, - {"mqttint", O_INTF, "FILE", 0, "Read MQTT integration settings from FILE (no default)", 0 }, - {"mqttvar", O_IVAR, "NAME=VALUE[,...]", 0, "Add variable(s) to the read MQTT integration settings", 0 }, - {"mqttjson", O_JSON, "short", OPTION_ARG_OPTIONAL, - "Publish in JSON format instead of strings, optionally in short (value directly below field key)", 0 }, - {"mqttverbose", O_VERB, nullptr, 0, "Publish all available attributes", 0 }, -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) - {"mqttlog", O_LOGL, nullptr, 0, "Log library events", 0 }, -#endif -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1004001) - {"mqttversion", O_VERS, "VERSION", 0, "Use protocol VERSION [3.1]", 0 }, -#endif + "Use TOPIC for global data (default is \"global/\" suffix to mqtttopic prefix)"}, + {"mqttretain", O_RETA, nullptr, 0, "Retain all topics instead of only selected global ones"}, + {"mqttqos", O_PQOS, "QOS", 0, "Set the QoS value for all topics (0-2) [0]"}, + {"mqttint", O_INTF, "FILE", 0, "Read MQTT integration settings from FILE (no default)"}, + {"mqttvar", O_IVAR, "NAME[+]=VALUE[,...]", 0, "Add variable(s) to the read MQTT integration settings " + "(append to already existing value with \"NAME+=VALUE\")"}, + {"mqttjson", O_JSON, "short", af_optional, + "Publish in JSON format instead of strings, optionally in short (value directly below field key)"}, + {"mqttverbose", O_VERB, nullptr, 0, "Publish all available attributes"}, + {"mqttlog", O_LOGL, nullptr, 0, "Log library events"}, + {"mqttversion", O_VERS, "VERSION", 0, "Use protocol VERSION [3.1]"}, {"mqttignoreinvalid", O_IGIN, nullptr, 0, - "Ignore invalid parameters during init (e.g. for DNS not resolvable yet)", 0 }, - {"mqttchanges", O_CHGS, nullptr, 0, "Whether to only publish changed messages instead of all received", 0 }, + "Ignore invalid parameters during init (e.g. for DNS not resolvable yet)"}, + {"mqttchanges", O_CHGS, nullptr, 0, "Whether to only publish changed messages instead of all received"}, -#if (LIBMOSQUITTO_MAJOR >= 1) - {"mqttca", O_CAFI, "CA", 0, "Use CA file or dir (ending with '/') for MQTT TLS (no default)", 0 }, - {"mqttcert", O_CERT, "CERTFILE", 0, "Use CERTFILE for MQTT TLS client certificate (no default)", 0 }, - {"mqttkey", O_KEYF, "KEYFILE", 0, "Use KEYFILE for MQTT TLS client certificate (no default)", 0 }, - {"mqttkeypass", O_KEPA, "PASSWORD", 0, "Use PASSWORD for the encrypted KEYFILE (no default)", 0 }, - {"mqttinsecure", O_INSE, nullptr, 0, "Allow insecure TLS connection (e.g. using a self signed certificate)", 0 }, -#endif + {"mqttca", O_CAFI, "CA", 0, "Use CA file or dir (ending with '/') for MQTT TLS (no default)"}, + {"mqttcert", O_CERT, "CERTFILE", 0, "Use CERTFILE for MQTT TLS client certificate (no default)"}, + {"mqttkey", O_KEYF, "KEYFILE", 0, "Use KEYFILE for MQTT TLS client certificate (no default)"}, + {"mqttkeypass", O_KEPA, "PASSWORD", 0, "Use PASSWORD for the encrypted KEYFILE (no default)"}, + {"mqttinsecure", O_INSE, nullptr, 0, "Allow insecure TLS connection (e.g. using a self signed certificate)"}, - {nullptr, 0, nullptr, 0, nullptr, 0 }, + {nullptr, 0, nullptr, 0, nullptr}, }; -static const char* g_host = "localhost"; //!< host name of MQTT broker [localhost] -static uint16_t g_port = 0; //!< optional port of MQTT broker, 0 to disable [0] -static const char* g_clientId = nullptr; //!< optional clientid override for MQTT broker -static const char* g_username = nullptr; //!< optional user name for MQTT broker (no default) -static const char* g_password = nullptr; //!< optional password for MQTT broker (no default) +// options for the MQTT client +static mqtt_client_config_t g_opt = { + .host = "localhost", + .port = 0, + .clientId = nullptr, + .username = nullptr, + .password = nullptr, + .logEvents = false, + .version311 = false, + .ignoreInvalidParams = false, + .cafile = nullptr, + .capath = nullptr, + .certfile = nullptr, + .keyfile = nullptr, + .keypass = nullptr, + .insecure = false, + .lastWillTopic = nullptr, + .lastWillData = nullptr, +}; static const char* g_topic = nullptr; //!< optional topic template static const char* g_globalTopic = nullptr; //!< optional global topic static const char* g_integrationFile = nullptr; //!< the integration settings file @@ -108,24 +117,8 @@ static vector* g_integrationVars = nullptr; //!< the integration settin static bool g_retain = false; //!< whether to retail all topics static int g_qos = 0; //!< the qos value for all topics static OutputFormat g_publishFormat = OF_NONE; //!< the OutputFormat for publishing messages -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) -static bool g_logFromLib = false; //!< log library events -#endif -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1004001) -static int g_version = MQTT_PROTOCOL_V31; //!< protocol version to use -#endif -static bool g_ignoreInvalidParams = false; //!< ignore invalid parameters during init static bool g_onlyChanges = false; //!< whether to only publish changed messages instead of all received -#if (LIBMOSQUITTO_MAJOR >= 1) -static const char* g_cafile = nullptr; //!< CA file for TLS -static const char* g_capath = nullptr; //!< CA path for TLS -static const char* g_certfile = nullptr; //!< client certificate file for TLS -static const char* g_keyfile = nullptr; //!< client key file for TLS -static const char* g_keypass = nullptr; //!< client key file password for TLS -static bool g_insecure = false; //!< whether to allow insecure TLS connection -#endif - /** * Replace all characters in the string with a space and return a copy of the original string. * @param arg the string to replace. @@ -144,73 +137,73 @@ void splitFields(const string& str, vector* row); /** * The MQTT argument parsing function. - * @param key the key from @a g_mqtt_argp_options. + * @param key the key from @a g_mqtt_argDefs. * @param arg the option argument, or nullptr. * @param state the parsing state. */ -static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) { +static int mqtt_parse_opt(int key, char *arg, const argParseOpt *parseOpt, void *userArg) { result_t result = RESULT_OK; unsigned int value; switch (key) { case O_HOST: // --mqtthost=localhost if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid mqtthost"); + argParseError(parseOpt, "invalid mqtthost"); return EINVAL; } - g_host = arg; + g_opt.host = arg; break; case O_PORT: // --mqttport=1883 value = parseInt(arg, 10, 1, 65535, &result); if (result != RESULT_OK) { - argp_error(state, "invalid mqttport"); + argParseError(parseOpt, "invalid mqttport"); return EINVAL; } - g_port = (uint16_t)value; + g_opt.port = (uint16_t)value; break; case O_CLID: // --mqttclientid=clientid if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid mqttclientid"); + argParseError(parseOpt, "invalid mqttclientid"); return EINVAL; } - g_clientId = arg; + g_opt.clientId = arg; break; case O_USER: // --mqttuser=username if (arg == nullptr) { - argp_error(state, "invalid mqttuser"); + argParseError(parseOpt, "invalid mqttuser"); return EINVAL; } - g_username = arg; + g_opt.username = arg; break; case O_PASS: // --mqttpass=password if (arg == nullptr) { - argp_error(state, "invalid mqttpass"); + argParseError(parseOpt, "invalid mqttpass"); return EINVAL; } - g_password = replaceSecret(arg); + g_opt.password = replaceSecret(arg); break; case O_TOPI: // --mqtttopic=ebusd { - if (arg == nullptr || arg[0] == 0 || strchr(arg, '+') || arg[strlen(arg)-1] == '/') { - argp_error(state, "invalid mqtttopic"); + if (arg == nullptr || arg[0] == 0 || arg[0] == '/' || strchr(arg, '+') || arg[strlen(arg)-1] == '/') { + argParseError(parseOpt, "invalid mqtttopic"); return EINVAL; } char *pos = strchr(arg, '#'); if (pos && (pos == arg || pos[1])) { // allow # only at very last position (to indicate not using any default) - argp_error(state, "invalid mqtttopic"); + argParseError(parseOpt, "invalid mqtttopic"); return EINVAL; } if (g_topic) { - argp_error(state, "duplicate mqtttopic"); + argParseError(parseOpt, "duplicate mqtttopic"); return EINVAL; } StringReplacer replacer; if (!replacer.parse(arg, true)) { - argp_error(state, "malformed mqtttopic"); + argParseError(parseOpt, "malformed mqtttopic"); return ESRCH; // abort in any case due to the above potentially being destructive } g_topic = arg; @@ -219,7 +212,7 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) { case O_GTOP: // --mqttglobal=global/ if (arg == nullptr || strchr(arg, '+') || strchr(arg, '#')) { - argp_error(state, "invalid mqttglobal"); + argParseError(parseOpt, "invalid mqttglobal"); return EINVAL; } g_globalTopic = arg; @@ -232,7 +225,7 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) { case O_PQOS: // --mqttqos=0 value = parseInt(arg, 10, 0, 2, &result); if (result != RESULT_OK) { - argp_error(state, "invalid mqttqos value"); + argParseError(parseOpt, "invalid mqttqos value"); return EINVAL; } g_qos = static_cast(value); @@ -240,7 +233,7 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) { case O_INTF: // --mqttint=/etc/ebusd/mqttint.cfg if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid mqttint file"); + argParseError(parseOpt, "invalid mqttint file"); return EINVAL; } g_integrationFile = arg; @@ -248,7 +241,7 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) { case O_IVAR: // --mqttvar=NAME=VALUE[,NAME=VALUE]* if (arg == nullptr || arg[0] == 0 || !strchr(arg, '=')) { - argp_error(state, "invalid mqttvar"); + argParseError(parseOpt, "invalid mqttvar"); return EINVAL; } if (!g_integrationVars) { @@ -268,199 +261,91 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) { g_publishFormat = (g_publishFormat & ~OF_SHORT) | OF_NAMES|OF_UNITS|OF_COMMENTS|OF_ALL_ATTRS; break; -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) case O_LOGL: - g_logFromLib = true; + g_opt.logEvents = true; break; -#endif -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1004001) case O_VERS: // --mqttversion=3.1.1 if (arg == nullptr || arg[0] == 0 || (strcmp(arg, "3.1") != 0 && strcmp(arg, "3.1.1") != 0)) { - argp_error(state, "invalid mqttversion"); + argParseError(parseOpt, "invalid mqttversion"); return EINVAL; } - g_version = strcmp(arg, "3.1.1") == 0 ? MQTT_PROTOCOL_V311 : MQTT_PROTOCOL_V31; + g_opt.version311 = strcmp(arg, "3.1.1") == 0; break; -#endif case O_IGIN: - g_ignoreInvalidParams = true; + g_opt.ignoreInvalidParams = true; break; case O_CHGS: g_onlyChanges = true; break; -#if (LIBMOSQUITTO_MAJOR >= 1) - case O_CAFI: // --mqttca=file or --mqttca=dir/ - if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid mqttca"); - return EINVAL; - } - if (arg[strlen(arg)-1] == '/') { - g_cafile = nullptr; - g_capath = arg; - } else { - g_cafile = arg; - g_capath = nullptr; - } - break; + case O_CAFI: // --mqttca=file or --mqttca=dir/ + if (arg == nullptr || arg[0] == 0) { + argParseError(parseOpt, "invalid mqttca"); + return EINVAL; + } + if (arg[strlen(arg)-1] == '/') { + g_opt.cafile = nullptr; + g_opt.capath = arg; + } else { + g_opt.cafile = arg; + g_opt.capath = nullptr; + } + break; - case O_CERT: // --mqttcert=CERTFILE - if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid mqttcert"); - return EINVAL; - } - g_certfile = arg; - break; + case O_CERT: // --mqttcert=CERTFILE + if (arg == nullptr || arg[0] == 0) { + argParseError(parseOpt, "invalid mqttcert"); + return EINVAL; + } + g_opt.certfile = arg; + break; - case O_KEYF: // --mqttkey=KEYFILE - if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid mqttkey"); - return EINVAL; - } - g_keyfile = arg; - break; + case O_KEYF: // --mqttkey=KEYFILE + if (arg == nullptr || arg[0] == 0) { + argParseError(parseOpt, "invalid mqttkey"); + return EINVAL; + } + g_opt.keyfile = arg; + break; - case O_KEPA: // --mqttkeypass=PASSWORD - if (arg == nullptr) { - argp_error(state, "invalid mqttkeypass"); - return EINVAL; - } - g_keypass = replaceSecret(arg); - break; - case O_INSE: // --mqttinsecure - g_insecure = true; - break; -#endif + case O_KEPA: // --mqttkeypass=PASSWORD + if (arg == nullptr) { + argParseError(parseOpt, "invalid mqttkeypass"); + return EINVAL; + } + g_opt.keypass = replaceSecret(arg); + break; + case O_INSE: // --mqttinsecure + g_opt.insecure = true; + break; default: - return ARGP_ERR_UNKNOWN; + return EINVAL; } return 0; } -static const struct argp g_mqtt_argp = { g_mqtt_argp_options, mqtt_parse_opt, nullptr, nullptr, nullptr, nullptr, - nullptr }; -static const struct argp_child g_mqtt_argp_child = {&g_mqtt_argp, 0, "", 1}; +static const argParseChildOpt g_mqtt_arg_child = { + g_mqtt_argDefs, mqtt_parse_opt +}; -const struct argp_child* mqtthandler_getargs() { - return &g_mqtt_argp_child; -} - -bool check(int code, const char* method) { - if (code == MOSQ_ERR_SUCCESS) { - return true; - } - if (code == MOSQ_ERR_ERRNO) { - char* error = strerror(errno); - logOtherError("mqtt", "%s: errno %d=%s", method, errno, error); - return false; - } -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) - const char* msg = mosquitto_strerror(code); - logOtherError("mqtt", "%s: %s", method, msg); -#else - logOtherError("mqtt", "%s: error code %d", method, code); -#endif - return false; +const argParseChildOpt* mqtthandler_getargs() { + return &g_mqtt_arg_child; } bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages, list* handlers) { - if (g_port > 0) { - int major = -1; - int minor = -1; - int revision = -1; - mosquitto_lib_version(&major, &minor, &revision); - if (major < LIBMOSQUITTO_MAJOR) { - logOtherError("mqtt", "invalid mosquitto version %d instead of %d, will try connecting anyway", major, - LIBMOSQUITTO_MAJOR); - } - logOtherInfo("mqtt", "mosquitto version %d.%d.%d (compiled with %d.%d.%d)", major, minor, revision, - LIBMOSQUITTO_MAJOR, LIBMOSQUITTO_MINOR, LIBMOSQUITTO_REVISION); + if (g_opt.port > 0) { handlers->push_back(new MqttHandler(userInfo, busHandler, messages)); } return true; } -#if (LIBMOSQUITTO_MAJOR >= 1) -int on_keypassword(char *buf, int size, int rwflag, void *userdata) { - if (!g_keypass) { - return 0; - } - int len = static_cast(strlen(g_keypass)); - if (len > size) { - len = size; - } - memcpy(buf, g_keypass, len); - return len; -} -#endif - -void on_connect( -#if (LIBMOSQUITTO_MAJOR >= 1) - struct mosquitto *mosq, -#endif - void *obj, int rc) { - if (rc == 0) { - logOtherNotice("mqtt", "connection established"); - MqttHandler* handler = reinterpret_cast(obj); - if (handler) { - handler->notifyConnected(); - } - } else { - if (rc >= 1 && rc <= 3) { - logOtherError("mqtt", "connection refused: %s", - rc == 1 ? "wrong protocol" : (rc == 2 ? "wrong username/password" : "broker down")); - } else { - logOtherError("mqtt", "connection refused: %d", rc); - } - } -} - -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) -void on_log(struct mosquitto *mosq, void *obj, int level, const char* msg) { - switch (level) { - case MOSQ_LOG_DEBUG: - logOtherDebug("mqtt", "log %s", msg); - break; - case MOSQ_LOG_INFO: - logOtherInfo("mqtt", "log %s", msg); - break; - case MOSQ_LOG_NOTICE: - logOtherNotice("mqtt", "log %s", msg); - break; - case MOSQ_LOG_WARNING: - logOtherNotice("mqtt", "log warning %s", msg); - break; - case MOSQ_LOG_ERR: - logOtherError("mqtt", "log %s", msg); - break; - default: - logOtherError("mqtt", "log other %s", msg); - break; - } -} -#endif - -void on_message( -#if (LIBMOSQUITTO_MAJOR >= 1) - struct mosquitto *mosq, -#endif - void *obj, const struct mosquitto_message *message) { - MqttHandler* handler = reinterpret_cast(obj); - if (!handler || !message || !handler->isRunning()) { - return; - } - string topic(message->topic); - string data(message->payloadlen > 0 ? reinterpret_cast(message->payload) : ""); - handler->notifyTopic(topic, data); -} - /** * possible data type names. */ @@ -484,11 +369,11 @@ string removeTrailingNonTopicPart(const string& str) { } MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages) - : DataSink(userInfo, "mqtt"), DataSource(busHandler), WaitThread(), m_messages(messages), m_connected(false), - m_initialConnectFailed(false), m_lastUpdateCheckResult("."), m_lastScanStatus(SCAN_STATUS_NONE), - m_lastErrorLogTime(0) { + : DataSink(userInfo, "mqtt", g_onlyChanges), DataSource(busHandler), WaitThread(), + m_messages(messages), m_connected(false), + m_lastUpdateCheckResult("."), m_lastScanStatus(SCAN_STATUS_NONE) { m_definitionsSince = 0; - m_mosquitto = nullptr; + m_client = nullptr; bool hasIntegration = false; if (g_integrationFile != nullptr) { if (!m_replacers.parseFile(g_integrationFile)) { @@ -588,6 +473,9 @@ MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* } } } + if (!m_typeSwitches.empty()) { + splitFields(m_replacers["type_switch-names"], &m_typeSwitchNames); + } m_hasDefinitionTopic = !m_replacers.get("definition-topic", true, false).empty(); m_hasDefinitionFieldsPayload = m_replacers.uses("fields_payload"); m_subscribeConfigRestartTopic = m_replacers.get("config_restart-topic", false, false); @@ -603,126 +491,66 @@ MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* m_globalTopic.compress(values); } m_subscribeTopic = getTopic(nullptr, "#"); - if (check(mosquitto_lib_init(), "unable to initialize")) { - signal(SIGPIPE, SIG_IGN); // needed before libmosquitto v. 1.1.3 + if (!g_opt.clientId) { ostringstream clientId; - if (g_clientId) { - clientId << g_clientId; - } else { - clientId << PACKAGE_NAME << '_' << PACKAGE_VERSION << '_' << static_cast(getpid()); - } -#if (LIBMOSQUITTO_MAJOR >= 1) - m_mosquitto = mosquitto_new(clientId.str().c_str(), true, this); -#else - m_mosquitto = mosquitto_new(clientId.str().c_str(), this); -#endif - if (!m_mosquitto) { - logOtherError("mqtt", "unable to instantiate"); - } + clientId << PACKAGE_NAME << '_' << PACKAGE_VERSION << '_' << static_cast(getpid()); + g_opt.clientId = strdup(clientId.str().c_str()); } - if (m_mosquitto) { -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1004001) - check(mosquitto_threaded_set(m_mosquitto, true), "threaded_set"); - check(mosquitto_opts_set(m_mosquitto, MOSQ_OPT_PROTOCOL_VERSION, reinterpret_cast(&g_version)), - "opts_set protocol version"); -#endif - if (g_username || g_password) { - if (!g_username) { - g_username = PACKAGE; - } - if (mosquitto_username_pw_set(m_mosquitto, g_username, g_password) != MOSQ_ERR_SUCCESS) { - logOtherError("mqtt", "unable to set username/password, trying without"); - } - } - string willTopic = m_globalTopic.get("", "running"); - string willData = "false"; - size_t len = willData.length(); -#if (LIBMOSQUITTO_MAJOR >= 1) - mosquitto_will_set(m_mosquitto, willTopic.c_str(), (uint32_t)len, - reinterpret_cast(willData.c_str()), 0, true); -#else - mosquitto_will_set(m_mosquitto, true, willTopic.c_str(), (uint32_t)len, - reinterpret_cast(willData.c_str()), 0, true); -#endif - -#if (LIBMOSQUITTO_MAJOR >= 1) - if (g_cafile || g_capath) { - int ret; - ret = mosquitto_tls_set(m_mosquitto, g_cafile, g_capath, g_certfile, g_keyfile, on_keypassword); - if (ret != MOSQ_ERR_SUCCESS) { - logOtherError("mqtt", "unable to set TLS: %d", ret); - } else if (g_insecure) { - ret = mosquitto_tls_insecure_set(m_mosquitto, true); - if (ret != MOSQ_ERR_SUCCESS) { - logOtherError("mqtt", "unable to set TLS insecure: %d", ret); - } - } - } -#endif -#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001) - if (g_logFromLib) { - mosquitto_log_callback_set(m_mosquitto, on_log); - } -#endif - mosquitto_connect_callback_set(m_mosquitto, on_connect); - mosquitto_message_callback_set(m_mosquitto, on_message); - int ret; -#if (LIBMOSQUITTO_MAJOR >= 1) - ret = mosquitto_connect(m_mosquitto, g_host, g_port, 60); -#else - ret = mosquitto_connect(m_mosquitto, g_host, g_port, 60, true); -#endif - if (ret == MOSQ_ERR_INVAL && !g_ignoreInvalidParams) { - logOtherError("mqtt", "unable to connect (invalid parameters)"); - mosquitto_destroy(m_mosquitto); - m_mosquitto = nullptr; - } else if (!check(ret, "unable to connect, retrying")) { - m_connected = false; - m_initialConnectFailed = g_ignoreInvalidParams; - } else { - m_connected = true; // assume success until connect_callback says otherwise - logOtherDebug("mqtt", "connection requested"); - } + if (g_opt.password && !g_opt.username) { + g_opt.username = PACKAGE; + } + string willTopic = m_globalTopic.get("", "running"); + if (!willTopic.empty()) { + g_opt.lastWillTopic = strdup(willTopic.c_str()); + g_opt.lastWillData = "false"; + } + m_client = MqttClient::create(g_opt, this); + m_isAsync = false; + bool ret = m_client->connect(m_isAsync, m_connected); + if (!ret) { + logOtherError("mqtt", "unable to connect (invalid parameters)"); + delete m_client; + m_client = nullptr; } } MqttHandler::~MqttHandler() { join(); - if (m_mosquitto) { - mosquitto_destroy(m_mosquitto); - m_mosquitto = nullptr; + if (m_client) { + delete m_client; + m_client = nullptr; } - mosquitto_lib_cleanup(); } void MqttHandler::startHandler() { - if (m_mosquitto) { + if (m_client) { WaitThread::start("MQTT"); } } -void MqttHandler::notifyConnected() { - if (m_mosquitto && isRunning()) { +void MqttHandler::notifyMqttStatus(bool connected) { + if (connected && m_client && isRunning()) { const string sep = (g_publishFormat & OF_JSON) ? "\"" : ""; if (m_globalTopic.has("name")) { - publishTopic(m_globalTopic.get("", "version"), sep + (PACKAGE_STRING "." REVISION) + sep, true); + m_client->publishTopic(m_globalTopic.get("", "version"), sep + (PACKAGE_STRING "." REVISION) + sep, true); } publishTopic(m_globalTopic.get("", "running"), "true", true); if (!m_staticTopic) { - check(mosquitto_subscribe(m_mosquitto, nullptr, m_subscribeTopic.c_str(), 0), "subscribe"); + m_client->subscribeTopic(m_subscribeTopic); if (!m_subscribeConfigRestartTopic.empty()) { - check(mosquitto_subscribe(m_mosquitto, nullptr, m_subscribeConfigRestartTopic.c_str(), 0), "subscribe def."); + m_client->subscribeTopic(m_subscribeConfigRestartTopic); } } } } -void MqttHandler::notifyTopic(const string& topic, const string& data) { +void MqttHandler::notifyMqttTopic(const string& topic, const string& data) { size_t pos = topic.rfind('/'); if (pos == string::npos) { return; } if (!m_subscribeConfigRestartTopic.empty() && topic == m_subscribeConfigRestartTopic) { + logOtherDebug("mqtt", "received restart topic %s with data %s", topic.c_str(), data.c_str()); if (m_subscribeConfigRestartPayload.empty() || data == m_subscribeConfigRestartPayload) { m_definitionsSince = 0; } @@ -873,7 +701,7 @@ void splitFields(const string& str, vector* row) { } void MqttHandler::run() { - time_t lastTaskRun, now, start, lastSignal = 0, lastUpdates = 0; + time_t lastTaskRun, now, start, lastSignal = 0; bool signal = false; bool globalHasName = m_globalTopic.has("name"); string signalTopic = m_globalTopic.get("", "signal"); @@ -883,7 +711,6 @@ void MqttHandler::run() { unsigned int filterSeen = 0; string filterCircuit, filterNonCircuit, filterName, filterNonName, filterField, filterNonField, filterLevel, filterDirection; - vector typeSwitchNames; if (m_hasDefinitionTopic) { result_t result = RESULT_OK; filterPriority = parseInt(m_replacers["filter-priority"].c_str(), 10, 0, 9, &result); @@ -910,16 +737,13 @@ void MqttHandler::run() { FileReader::tolower(&filterLevel); filterDirection = m_replacers["filter-direction"]; FileReader::tolower(&filterDirection); - if (!m_typeSwitches.empty()) { - splitFields(m_replacers["type_switch-names"], &typeSwitchNames); - } } time(&now); start = lastTaskRun = now; bool allowReconnect = false; while (isRunning()) { bool wasConnected = m_connected; - bool needsWait = handleTraffic(allowReconnect); + bool needsWait = m_isAsync || handleTraffic(allowReconnect); bool reconnected = !wasConnected && m_connected; allowReconnect = false; time(&now); @@ -952,6 +776,10 @@ void MqttHandler::run() { publishDefinition(m_replacers, "def_global_uptime-", uptimeTopic, "global", "uptime", "def_global-"); publishDefinition(m_replacers, "def_global_updatecheck-", m_globalTopic.get("", "updatecheck"), "global", "updatecheck", "def_global-"); + if (m_busHandler->getProtocol()->supportsUpdateCheck()) { + publishDefinition(m_replacers, "def_global_updatecheck_device-", m_globalTopic.get("", "updatecheck"), + "global", "updatecheck_device", ""); + } publishDefinition(m_replacers, "def_global_scan-", m_globalTopic.get("", "scan"), "global", "scan", "def_global-"); } @@ -961,15 +789,20 @@ void MqttHandler::run() { ostringstream ostr; deque messages; m_messages->findAll("", "", m_levels, false, true, true, true, true, true, 0, 0, false, &messages); + bool includeActiveWrite = FileReader::matches("w", filterDirection, true, true); for (const auto& message : messages) { bool checkPollAdjust = false; + bool isWrite = message->isWrite(); + bool isPassive = message->isPassive(); + // at least treat as passive read if write direction is excluded + bool treatAsPassiveRead = !includeActiveWrite && isWrite; if (filterSeen > 0) { if (message->getLastUpdateTime() == 0) { - if (message->isPassive()) { + if (isPassive || treatAsPassiveRead) { // only wait for data on passive messages continue; // no data ever } - if (!message->isWrite()) { + if (!isWrite) { // only wait for data on read messages or set their poll prio if (filterSeen > 1 && (!message->getPollPriority() || message->getPollPriority() > filterSeen) && (filterPriority == 0 || filterSeen <= filterPriority) @@ -988,7 +821,9 @@ void MqttHandler::run() { } } message->setDataHandlerState(1, true); - } else if (message->getCreateTime() <= m_definitionsSince) { // only newer defined + } else if (message->getCreateTime() <= m_definitionsSince // only newer defined + && (!message->isConditional() // unless conditional + || message->getAvailableSinceTime() <= m_definitionsSince)) { continue; } if (!FileReader::matches(message->getCircuit(), filterCircuit, true, true) @@ -998,7 +833,7 @@ void MqttHandler::run() { || !FileReader::matches(message->getLevel(), filterLevel, true, true)) { continue; } - const string direction = directionNames[(message->isWrite() ? 2 : 0) + (message->isPassive() ? 1 : 0)]; + const string direction = treatAsPassiveRead ? "uw" : directionNames[(isWrite ? 2 : 0) + (isPassive ? 1 : 0)]; if (!FileReader::matches(direction, filterDirection, true, true)) { continue; } @@ -1011,18 +846,31 @@ void MqttHandler::run() { if (filterPriority > 0 && (message->getPollPriority() == 0 || message->getPollPriority() > filterPriority)) { continue; } - + if (includeActiveWrite) { + if (isWrite) { + bool skipMultiFieldWrite = (!m_hasDefinitionFieldsPayload || m_publishByField) + && !isPassive && message->getFieldCount() > 1; + if (skipMultiFieldWrite) { + // multi-field message is not writable when publishing by field or combining + // multiple fields in one definition, so skip it + continue; + } + } else { + // check for existance of write message with same name + Message* write = m_messages->find(message->getCircuit(), message->getName(), "", true); + if (write) { + bool skipMultiFieldWrite = (!m_hasDefinitionFieldsPayload || m_publishByField) + && write->getFieldCount() > 1; + if (!skipMultiFieldWrite) { + continue; // avoid sending definition of read AND write message with the same key + } + // else: multi-field write message is not writable when publishing by field or combining + // multiple fields in one definition, so skip it + } + } + } StringReplacers msgValues = m_replacers; // need a copy here as the contents are manipulated - msgValues.set("circuit", message->getCircuit()); - msgValues.set("name", message->getName()); - msgValues.set("priority", static_cast(message->getPollPriority())); - msgValues.set("level", message->getLevel()); - msgValues.set("direction", direction); - msgValues.set("messagecomment", message->getAttribute("comment")); - msgValues.reduce(true); - string str = msgValues.get("direction_map-"+direction, false, false); - msgValues.set("direction_map", str); - msgValues.reduce(true); + prepareDefinition(message, direction, &msgValues); ostringstream fields; size_t fieldCount = message->getFieldCount(); for (size_t index = 0; index < fieldCount; index++) { @@ -1038,91 +886,10 @@ void MqttHandler::run() { || (!filterNonField.empty() && FileReader::matches(fieldName, filterNonField, true, true))) { continue; } - const DataType* dataType = field->getDataType(); - string typeStr; - if (dataType->isNumeric()) { - if (field->isList()) { - typeStr = "list"; - } else { - typeStr = "number"; - } - } else if (dataType->hasFlag(DAT)) { - auto dt = dynamic_cast(dataType); - if (dt->hasDate()) { - typeStr = dt->hasDate() ? "datetime" : "date"; - } else { - typeStr = "time"; - } - } else { - typeStr = "string"; - } - ostr.str(""); - ostr << "type_map-" << direction << "-" << typeStr; - str = msgValues.get(ostr.str(), false, false); - if (str.empty()) { - ostr.str(""); - ostr << "type_map-" << typeStr; - str = msgValues.get(ostr.str(), false, false); - } - if (str.empty()) { + StringReplacers values; + if (!prepareDefinition(direction, msgValues, fieldCount, index, fieldName, field, &values)) { continue; } - StringReplacers values = msgValues; // need a copy here as the contents are manipulated - values.set("index", static_cast(index)); - values.set("field", fieldName); - values.set("fieldname", field->getName(-1)); - values.set("type", typeStr); - values.set("type_map", str); - values.set("basetype", dataType->getId()); - values.set("comment", field->getAttribute("comment")); - values.set("unit", field->getAttribute("unit")); - if (dataType->isNumeric()) { - auto dt = dynamic_cast(dataType); - ostr.str(""); - if (dt->getMinMax(false, g_publishFormat, &ostr) == RESULT_OK) { - values.set("min", ostr.str()); - ostr.str(""); - } - if (dt->getMinMax(true, g_publishFormat, &ostr) == RESULT_OK) { - values.set("max", ostr.str()); - } - } - if (!m_typeSwitches.empty()) { - values.reduce(true); - str = values.get("type_switch-by", false, false); - string typeSwitch; - for (int i = 0; i < 2; i++) { - ostr.str(""); - if (i == 0) { - ostr << direction << '-'; - } - ostr << typeStr; - const string key = ostr.str(); - for (auto const &check : m_typeSwitches[key]) { - if (FileReader::matches(str, check.second, true, true)) { - typeSwitch = check.first; - i = 2; // early exit - break; - } - } - } - values.set("type_switch", typeSwitch); - if (!typeSwitchNames.empty()) { - vector strs; - splitFields(typeSwitch, &strs); - for (size_t pos = 0; pos < strs.size() && pos < typeSwitchNames.size(); pos++) { - values.set(typeSwitchNames[pos], strs[pos]); - } - } - } - values.reduce(true); - string typePartSuffix = values["type_part-by"]; - if (typePartSuffix.empty()) { - typePartSuffix = typeStr; - } - str = values.get("type_part-" + typePartSuffix, false, false); - values.set("type_part", str); - values.reduce(); if (m_hasDefinitionFieldsPayload) { string value = values["field_payload"]; if (!value.empty()) { @@ -1156,7 +923,7 @@ void MqttHandler::run() { time(&lastTaskRun); } if (sendSignal) { - if (m_busHandler->hasSignal()) { + if (m_busHandler->getProtocol()->hasSignal()) { lastSignal = now; if (!signal || reconnected) { signal = true; @@ -1180,8 +947,8 @@ void MqttHandler::run() { const vector* messages = m_messages->getByKey(it->first); if (messages) { for (const auto& message : *messages) { - if (message->getLastChangeTime() > 0 && message->isAvailable() - && (!g_onlyChanges || message->getLastChangeTime() > lastUpdates)) { + time_t changeTime = message->getLastChangeTime(); + if (changeTime > 0 && message->isAvailable()) { updates.str(""); updates.clear(); updates << dec; @@ -1191,7 +958,6 @@ void MqttHandler::run() { } it = m_updatedMessages.erase(it); } - time(&lastUpdates); } else { m_updatedMessages.clear(); } @@ -1207,6 +973,155 @@ void MqttHandler::run() { } } +void MqttHandler::prepareDefinition(const Message* message, const string& direction, StringReplacers* msgValues) const { + msgValues->set("circuit", message->getCircuit()); + msgValues->set("name", message->getName()); + msgValues->set("priority", static_cast(message->getPollPriority())); + msgValues->set("level", message->getLevel()); + msgValues->set("direction", direction); + msgValues->set("messagecomment", message->getAttribute("comment")); + msgValues->reduce(true); + string str = msgValues->get("direction_map-"+direction, false, false); + msgValues->set("direction_map", str); + msgValues->reduce(true); +} + +bool MqttHandler::prepareDefinition(const string& direction, const StringReplacers& msgValues, size_t fieldCount, size_t index, const string& fieldName, const SingleDataField* field, StringReplacers* values) const { + const DataType* dataType = field->getDataType(); + string typeStr; + if (dataType->isNumeric()) { + if (field->isList()) { + typeStr = "list"; + } else { + typeStr = "number"; + } + } else if (dataType->hasFlag(DAT)) { + auto dt = dynamic_cast(dataType); + if (dt->hasDate()) { + typeStr = dt->hasTime() ? "datetime" : "date"; + } else { + typeStr = "time"; + } + } else { + typeStr = "string"; + } + ostringstream ostr; + ostr << "type_map-" << direction << "-" << typeStr; + string str = msgValues.get(ostr.str(), false, false); + if (str.empty()) { + ostr.str(""); + ostr << "type_map-" << typeStr; + str = msgValues.get(ostr.str(), false, false); + } + if (str.empty()) { + return false; + } + *values = msgValues; // need a copy here as the contents are manipulated + values->set("index", static_cast(index)); + values->set("field", fieldName); + string fieldNameNonUnique = field->getName(-1); + values->set("fieldname", fieldNameNonUnique); + values->set("fieldnamemult", fieldCount == 1 ? "" : fieldNameNonUnique); + values->set("type", typeStr); + values->set("type_map", str); + values->set("basetype", dataType->getId()); + values->set("comment", field->getAttribute("comment")); + values->set("unit", field->getAttribute("unit")); + if (dataType->isNumeric() && !dataType->hasFlag(EXP)) { + auto dt = dynamic_cast(dataType); + ostr.str(""); + if (dt->getMinMax(false, g_publishFormat, &ostr) == RESULT_OK) { + values->set("min", ostr.str()); + ostr.str(""); + } + if (dt->getMinMax(true, g_publishFormat, &ostr) == RESULT_OK) { + values->set("max", ostr.str()); + ostr.str(""); + } + if (dt->getStep(g_publishFormat, &ostr) != RESULT_OK) { + // fallback method, when smallest number didn't work + int divisor = dt->getDivisor(); + float step = 1.0f; + if (divisor > 1) { + step /= static_cast(divisor); + } else if (divisor < 0) { + step *= static_cast(-divisor); + } + ostr << static_cast(step); + } + values->set("step", ostr.str()); + } + if (dataType->isNumeric() && field->isList() && !(*values)["field_values-entry"].empty()) { + auto vl = (dynamic_cast(field))->getList(); + string entryFormat = (*values)["field_values-entry"]; + string::size_type pos = -1; + while ((pos = entryFormat.find('$', pos+1)) != string::npos) { + if (entryFormat.substr(pos+1, 4) == "text" || entryFormat.substr(pos+1, 5) == "value") { + entryFormat.replace(pos, 1, "%"); + } + } + entryFormat.replace(0, 0, "entry = "); + string result = (*values)["field_values-prefix"]; + bool first = true; + for (const auto& it : vl) { + StringReplacers entry; + entry.parseLine(entryFormat); + entry.set("value", it.first); + entry.set("text", it.second); + entry.reduce(); + if (first) { + first = false; + } else { + result += (*values)["field_values-separator"]; + } + result += entry.get("entry", false, false); + } + result += (*values)["field_values-suffix"]; + values->set("field_values", result); + } + if (!m_typeSwitches.empty()) { + values->reduce(true); + str = values->get("type_switch-by", false, false); + string typeSwitch; + for (int i = 0; i < 2; i++) { + ostr.str(""); + if (i == 0) { + ostr << direction << '-'; + } + ostr << typeStr; + const string key = ostr.str(); + auto const tsIt = m_typeSwitches.find(key); + if (tsIt != m_typeSwitches.cend()) { + for (auto const &check : tsIt->second) { + if (FileReader::matches(str, check.second, true, true)) { + typeSwitch = check.first; + i = 2; // early exit + break; + } + } + } + } + values->set("type_switch", typeSwitch); + if (!m_typeSwitchNames.empty()) { + vector strs; + splitFields(typeSwitch, &strs); + for (size_t pos = 0; pos < m_typeSwitchNames.size(); pos++) { + values->set(m_typeSwitchNames[pos], pos < strs.size() ? strs[pos] : ""); + } + } + } + values->reduce(true); + string typePartSuffix = (*values)["type_part-by"]; + if (typePartSuffix.empty()) { + typePartSuffix = typeStr; + } + str = values->get("type_part-" + typePartSuffix, false, false); + values->set("type_part", str); + values->reduce(); + return true; +} + + void MqttHandler::publishDefinition(StringReplacers values, const string& prefix, const string& topic, const string& circuit, const string& name, const string& fallbackPrefix) { bool reduce = false; @@ -1255,52 +1170,10 @@ void MqttHandler::publishDefinition(const StringReplacers& values) { } bool MqttHandler::handleTraffic(bool allowReconnect) { - if (!m_mosquitto) { - return false; - } - int ret; -#if (LIBMOSQUITTO_MAJOR >= 1) - ret = mosquitto_loop(m_mosquitto, -1, 1); // waits up to 1 second for network traffic -#else - ret = mosquitto_loop(m_mosquitto, -1); // waits up to 1 second for network traffic -#endif - if (!m_connected && (ret == MOSQ_ERR_NO_CONN || ret == MOSQ_ERR_CONN_LOST) && allowReconnect) { - if (m_initialConnectFailed) { -#if (LIBMOSQUITTO_MAJOR >= 1) - ret = mosquitto_connect(m_mosquitto, g_host, g_port, 60); -#else - ret = mosquitto_connect(m_mosquitto, g_host, g_port, 60, true); -#endif - if (ret == MOSQ_ERR_INVAL) { - logOtherError("mqtt", "unable to connect (invalid parameters), retrying"); - } - if (ret == MOSQ_ERR_SUCCESS) { - m_initialConnectFailed = false; - } - } else { - ret = mosquitto_reconnect(m_mosquitto); - } - } - if (!m_connected && ret == MOSQ_ERR_SUCCESS) { - m_connected = true; - logOtherNotice("mqtt", "connection re-established"); - } - if (!m_connected || ret == MOSQ_ERR_SUCCESS) { + if (!m_client) { return false; } - if (ret == MOSQ_ERR_NO_CONN || ret == MOSQ_ERR_CONN_LOST || ret == MOSQ_ERR_CONN_REFUSED) { - logOtherError("mqtt", "communication error: %s", ret == MOSQ_ERR_NO_CONN ? "not connected" - : (ret == MOSQ_ERR_CONN_LOST ? "connection lost" : "connection refused")); - m_connected = false; - } else { - time_t now; - time(&now); - if (now > m_lastErrorLogTime + 10) { // log at most every 10 seconds - m_lastErrorLogTime = now; - check(ret, "communication error"); - } - } - return true; + return m_client->run(allowReconnect, m_connected); } string MqttHandler::getTopic(const Message* message, const string& suffix, const string& fieldName) { @@ -1328,7 +1201,11 @@ void MqttHandler::publishMessage(const Message* message, ostringstream* updates, } else if (m_staticTopic) { *updates << message->getCircuit() << UI_FIELD_SEPARATOR << message->getName() << UI_FIELD_SEPARATOR; } - result_t result = message->decodeLastData(false, nullptr, -1, outputFormat, updates); + result_t result = message->decodeLastData(pt_any, false, nullptr, -1, outputFormat, updates); + if (result == RESULT_EMPTY) { + publishEmptyTopic(getTopic(message)); // alternatively: , json ? "null" : ""); + return; + } if (result != RESULT_OK) { logOtherError("mqtt", "decode %s %s: %s", message->getCircuit().c_str(), message->getName().c_str(), getResultCode(result)); @@ -1352,7 +1229,7 @@ void MqttHandler::publishMessage(const Message* message, ostringstream* updates, publishEmptyTopic(getTopic(message, "", name)); // alternatively: , json ? "null" : ""); continue; } - result_t result = message->decodeLastData(false, nullptr, index, outputFormat, updates); + result_t result = message->decodeLastData(pt_any, false, nullptr, index, outputFormat, updates); if (result != RESULT_OK) { logOtherError("mqtt", "decode %s %s %s: %s", message->getCircuit().c_str(), message->getName().c_str(), name.c_str(), getResultCode(result)); @@ -1367,16 +1244,14 @@ void MqttHandler::publishMessage(const Message* message, ostringstream* updates, void MqttHandler::publishTopic(const string& topic, const string& data, bool retain) { const char* topicStr = topic.c_str(); const char* dataStr = data.c_str(); - const size_t len = strlen(dataStr); logOtherDebug("mqtt", "publish %s %s", topicStr, dataStr); - check(mosquitto_publish(m_mosquitto, nullptr, topicStr, (uint32_t)len, - reinterpret_cast(dataStr), g_qos, g_retain || retain), "publish"); + m_client->publishTopic(topicStr, data, g_qos, g_retain || retain); } void MqttHandler::publishEmptyTopic(const string& topic) { const char* topicStr = topic.c_str(); logOtherDebug("mqtt", "publish empty %s", topicStr); - check(mosquitto_publish(m_mosquitto, nullptr, topicStr, 0, nullptr, 0, g_retain), "publish empty"); + m_client->publishEmptyTopic(topic, 0, g_retain); } } // namespace ebusd diff --git a/src/ebusd/mqtthandler.h b/src/ebusd/mqtthandler.h old mode 100644 new mode 100755 index d4f4ad78b..8bbf8259f --- a/src/ebusd/mqtthandler.h +++ b/src/ebusd/mqtthandler.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,6 @@ #ifndef EBUSD_MQTTHANDLER_H_ #define EBUSD_MQTTHANDLER_H_ -#include #include #include #include @@ -27,8 +26,10 @@ #include #include "ebusd/datahandler.h" #include "ebusd/bushandler.h" +#include "ebusd/mqttclient.h" #include "lib/ebus/message.h" #include "lib/ebus/stringhelper.h" +#include "lib/utils/arg.h" namespace ebusd { @@ -42,10 +43,10 @@ using std::string; using std::vector; /** - * Helper function for getting the argp definition for MQTT. - * @return a pointer to the argp_child structure. + * Helper function for getting the arg definition for MQTT. + * @return a pointer to the child argument options, or nullptr. */ -const struct argp_child* mqtthandler_getargs(); +const argParseChildOpt* mqtthandler_getargs(); /** * Registration function that is called once during initialization. @@ -62,7 +63,7 @@ bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap /** * The main class supporting MQTT data handling. */ -class MqttHandler : public DataSink, public DataSource, public WaitThread { +class MqttHandler : public DataSink, public DataSource, public WaitThread, public MqttClientListener { public: /** * Constructor. @@ -81,17 +82,11 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread { // @copydoc void startHandler() override; - /** - * Notify the handler of a (re-)established connection to the broker. - */ - void notifyConnected(); + // @copydoc + void notifyMqttStatus(bool connected) override; - /** - * Notify the handler of a received MQTT message. - * @param topic the topic string. - * @param data the data string. - */ - void notifyTopic(const string& topic, const string& data); + // @copydoc + void notifyMqttTopic(const string& topic, const string& data) override; // @copydoc void notifyUpdateCheckResult(const string& checkResult) override; @@ -100,6 +95,26 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread { void notifyScanStatus(scanStatus_t scanStatus) override; protected: + /** + * Prepare the message part of a definition topic. + * @param message the @a Message instance to prepare for. + * @param direction the direction string. + * @param msgValues the values with the message specification. + */ + void prepareDefinition(const Message* message, const string& direction, StringReplacers* msgValues) const; + + /** + * Prepare the field part of a definition topic. + * @param direction the direction string. + * @param msgValues the prepared values from the message specification. + * @param fieldCount the total field count (non-ignored only). + * @param index the field index. + * @param field the @a SingleDataField instance. + * @param values the values to update on success. + * @return true if the values were updated successfully, false otherwise. + */ + bool prepareDefinition(const string& direction, const StringReplacers& msgValues, size_t fieldCount, size_t index, const string& fieldName, const SingleDataField* field, StringReplacers* values) const; + // @copydoc void run() override; @@ -189,6 +204,9 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread { /** map of type name to a list of pairs of wildcard string and mapped value. */ map>> m_typeSwitches; + /** prepared list of type switch names. */ + vector m_typeSwitchNames; + /** the subscribed configuration restart topic, or empty. */ string m_subscribeConfigRestartTopic; @@ -198,23 +216,24 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread { /** the last system time when the message definitions were published. */ time_t m_definitionsSince; - /** the mosquitto structure if initialized, or nullptr. */ - struct mosquitto* m_mosquitto; + /** the @a MqttClient instance. */ + MqttClient* m_client; + + /** + * true if the client is asynchronous and its @a run() method does not + * have to be called at all, false if the client is synchronous and does + * it's work in its @a run() method only. + */ + bool m_isAsync; /** whether the connection to the broker is established. */ bool m_connected; - /** whether the initial connect failed. */ - bool m_initialConnectFailed; - /** the last update check result. */ string m_lastUpdateCheckResult; /** the last scan status. */ scanStatus_t m_lastScanStatus; - - /** the last system time when a communication error was logged. */ - time_t m_lastErrorLogTime; }; } // namespace ebusd diff --git a/src/ebusd/network.cpp b/src/ebusd/network.cpp index 6bc27bd03..c816f098e 100644 --- a/src/ebusd/network.cpp +++ b/src/ebusd/network.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2014-2025 John Baier , Roland Jax 2012-2014 * * 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 @@ -37,39 +37,6 @@ int Connection::m_ids = 0; #define POLLRDHUP 0 #endif -bool NetMessage::add(const char* request) { - if (request && request[0]) { - string add = request; - add.erase(remove(add.begin(), add.end(), '\r'), add.end()); - m_request.append(add); - } - size_t pos = m_request.find(m_isHttp ? "\n\n" : "\n"); - if (pos != string::npos) { - if (m_isHttp) { - pos = m_request.find("\n"); - m_request.resize(pos); // reduce to first line - // typical first line: GET /ehp/outsidetemp HTTP/1.1 - pos = m_request.rfind(" HTTP/"); - if (pos != string::npos) { - m_request.resize(pos); // remove "HTTP/x.x" suffix - } - pos = 0; - while ((pos=m_request.find('%', pos)) != string::npos && pos+2 <= m_request.length()) { - unsigned int value1, value2; - if (sscanf("%1x%1x", m_request.c_str()+pos+1, &value1, &value2) < 2) { - break; - } - m_request[pos] = static_cast(((value1&0x0f) << 4) | (value2&0x0f)); - m_request.erase(pos+1, 2); - } - } else if (pos+1 == m_request.length()) { - m_request.resize(pos); // reduce to complete lines - } - return true; - } - return m_request.length() == 0 && isListeningMode(); -} - void Connection::run() { int ret; @@ -108,7 +75,7 @@ void Connection::run() { #endif bool closed = false; - NetMessage message(m_isHttp); + RequestImpl req(m_isHttp); while (!closed) { #ifdef HAVE_PPOLL @@ -146,7 +113,7 @@ void Connection::run() { #endif } - if (newData || message.isListeningMode()) { + if (newData || req.getMode().listenMode != lm_none) { char data[256]; if (!m_socket->isValid()) { @@ -165,21 +132,24 @@ void Connection::run() { } // decode client data - if (message.add(data)) { - m_netQueue->push(&message); + if (req.add(data)) { + m_requestQueue->push(&req); // wait for result logDebug(lf_network, "[%05d] wait for result", getID()); string result; - message.getResult(&result); + bool disconnect = req.waitResponse(&result); if (!m_socket->isValid()) { break; } m_socket->send(result.c_str(), result.size()); + if (disconnect) { + break; + } } - if (message.isDisconnect() || !m_socket->isValid()) { + if (!m_socket->isValid()) { break; } } @@ -193,16 +163,20 @@ void Connection::run() { } -Network::Network(const bool local, const uint16_t port, const uint16_t httpPort, Queue* netQueue) - : Thread(), m_netQueue(netQueue), m_listening(false) { +Network::Network(const bool local, const uint16_t port, const uint16_t httpPort, Queue* requestQueue) + : Thread(), m_requestQueue(requestQueue), m_listening(false) { m_tcpServer = new TCPServer(port, local ? "127.0.0.1" : "0.0.0.0"); if (m_tcpServer != nullptr && m_tcpServer->start() == 0) { m_listening = true; + } else { + logError(lf_network, "unable to start TCP server on port %d: error %d", port, errno); } if (httpPort > 0) { m_httpServer = new TCPServer(httpPort, "0.0.0.0"); - m_httpServer->start(); + if (m_httpServer->start() != 0) { + logError(lf_network, "unable to start HTTP server on port %d: error %d", httpPort, errno); + } } else { m_httpServer = nullptr; } @@ -210,9 +184,9 @@ Network::Network(const bool local, const uint16_t port, const uint16_t httpPort, Network::~Network() { stop(); - NetMessage* netMsg; - while ((netMsg = m_netQueue->pop()) != nullptr) { - netMsg->setResult("ERR: shutdown", "", nullptr, 0, true); + Request* req; + while ((req = m_requestQueue->pop()) != nullptr) { + req->setResult("ERR: shutdown", "", nullptr, 0, true); } while (!m_connections.empty()) { Connection* connection = m_connections.back(); @@ -329,7 +303,7 @@ void Network::run() { if (socket == nullptr) { continue; } - Connection* connection = new Connection(socket, isHttp, m_netQueue); + Connection* connection = new Connection(socket, isHttp, m_requestQueue); string ip = socket->getIP(); connection->start("connection"); m_connections.push_back(connection); diff --git a/src/ebusd/network.h b/src/ebusd/network.h index e972bc72f..d6bf375d7 100644 --- a/src/ebusd/network.h +++ b/src/ebusd/network.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2014-2025 John Baier , Roland Jax 2012-2014 * * 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 @@ -23,6 +23,7 @@ #include #include #include +#include "ebusd/request.h" #include "lib/ebus/datatype.h" #include "lib/utils/tcpsocket.h" #include "lib/utils/queue.h" @@ -35,188 +36,9 @@ namespace ebusd { * The TCP and HTTP client request handling. */ -/** Forward declaration for @a Connection. */ -class Connection; - -/** the possible client modes. */ -enum ClientMode { - cm_normal, //!< normal mode - cm_listen, //!< listening mode - cm_direct, //!< direct mode -}; - -/** - * Combination of client settings. - */ -struct ClientSettings { - ClientMode mode; //!< the current client mode - OutputFormat format; //!< the output format settings for listen mode - bool listenWithUnknown; //!< include unknown messages in listen mode - bool listenOnlyUnknown; //!< only print unknown messages in listen mode -}; - -/** - * Class for data/message transfer between @a Connection and @a MainLoop. - */ -class NetMessage { - public: - /** - * Constructor. - * @param isHttp whether this is a HTTP message. - */ - explicit NetMessage(bool isHttp) - : m_isHttp(isHttp), m_resultSet(false), m_disconnect(false), m_listenSince(0) { - m_settings.mode = cm_normal; - m_settings.format = OF_NONE; - m_settings.listenWithUnknown = false; - m_settings.listenOnlyUnknown = false; - pthread_mutex_init(&m_mutex, nullptr); - pthread_cond_init(&m_cond, nullptr); - } - - /** - * Destructor. - */ - ~NetMessage() { - m_resultSet = true; - pthread_mutex_destroy(&m_mutex); - pthread_cond_destroy(&m_cond); - } - - - private: - /** - * Hidden copy constructor. - * @param src the object to copy from. - */ - NetMessage(const NetMessage& src); - - - public: - /** - * Add request data received from the client. - * @param request the request data from the client. - * @return true when the request is complete and the response shall be prepared. - */ - bool add(const char* request); - - /** - * Return whether this is a HTTP message. - * @return whether this is a HTTP message. - */ - bool isHttp() const { return m_isHttp; } - - /** - * Return the request string. - * @return the request string. - */ - const string& getRequest() const { return m_request; } - - /** - * Return the current user name. - * @return the current user name. - */ - const string& getUser() const { return m_user; } - - /** - * Wait for the result being set and return the result string. - * @param result the variable in which to store the result string. - */ - void getResult(string* result) { - pthread_mutex_lock(&m_mutex); - - if (!m_resultSet) { - pthread_cond_wait(&m_cond, &m_mutex); - } - m_request.clear(); - *result = m_result; - m_result.clear(); - m_resultSet = false; - pthread_mutex_unlock(&m_mutex); - } - - /** - * Set the result string and notify the waiting thread. - * @param result the result string. - * @param user the new user name. - * @param settings the new client settings. - * @param listenUntil the end time to which to updates were added (exclusive). - * @param disconnect true when the client shall be disconnected. - */ - void setResult(const string& result, const string& user, ClientSettings* settings, time_t listenUntil, - bool disconnect) { - pthread_mutex_lock(&m_mutex); - m_result = result; - m_user = user; - m_disconnect = disconnect; - if (settings) { - m_settings = *settings; - } - m_listenSince = listenUntil; - m_resultSet = true; - pthread_cond_signal(&m_cond); - pthread_mutex_unlock(&m_mutex); - } - - /** - * Return the client settings. - * @param listenSince set listening to the specified start time from which to add updates (inclusive). - * @return the client settings. - */ - ClientSettings getSettings(time_t* listenSince = nullptr) { - if (listenSince) { - *listenSince = m_listenSince; - } - return m_settings; - } - - /** - * Return whether this instance is in one of the listening modes. - * @return whether this instance is in one of the listening modes. - */ - bool isListeningMode() { return m_settings.mode == cm_listen || m_settings.mode == cm_direct; } - - /** - * Return whether the client shall be disconnected. - * @return true when the client shall be disconnected. - */ - bool isDisconnect() { return m_disconnect; } - - - private: - /** whether this is a HTTP message. */ - const bool m_isHttp; - - /** the request string. */ - string m_request; - - /** the current user name. */ - string m_user; - - /** whether the result was already set. */ - bool m_resultSet; - - /** the result string. */ - string m_result; - - /** set to true when the client shall be disconnected. */ - bool m_disconnect; - - /** mutex variable for exclusive lock. */ - pthread_mutex_t m_mutex; - - /** condition variable for exclusive lock. */ - pthread_cond_t m_cond; - - /** the client settings. */ - ClientSettings m_settings; - - /** start timestamp of listening update. */ - time_t m_listenSince; -}; /** - * class connection which handle client and baseloop communication. + * Instance of a connected client, either TCP or HTTP. */ class Connection : public Thread { public: @@ -224,10 +46,10 @@ class Connection : public Thread { * Constructor. * @param socket the @a TCPSocket for communication. * @param isHttp whether this is a HTTP message. - * @param netQueue the reference to the @a NetMessage @a Queue. + * @param requestQueue the reference to the @a Request @a Queue. */ - Connection(TCPSocket* socket, const bool isHttp, Queue* netQueue) - : Thread(), m_isHttp(isHttp), m_socket(socket), m_netQueue(netQueue), m_endedAt(0) { + Connection(TCPSocket* socket, const bool isHttp, Queue* requestQueue) + : Thread(), m_isHttp(isHttp), m_socket(socket), m_requestQueue(requestQueue), m_endedAt(0) { m_id = ++m_ids; } @@ -267,8 +89,8 @@ class Connection : public Thread { /** the @a TCPSocket for communication. */ TCPSocket* m_socket; - /** the reference to the @a NetMessage @a Queue. */ - Queue* m_netQueue; + /** the reference to the @a Request @a Queue. */ + Queue* m_requestQueue; /** notification object for shutdown procedure. */ Notify m_notify; @@ -284,7 +106,7 @@ class Connection : public Thread { }; /** - * class network which listening on tcp socket for incoming connections. + * Handler for all TCP and HTTP client connections and registry of active connections. */ class Network : public Thread { public: @@ -293,9 +115,9 @@ class Network : public Thread { * @param local true to accept connections only for local host. * @param port the port to listen for command line connections. * @param httpPort the port to listen for HTTP connections, or 0. - * @param netQueue the reference to the @a NetMessage @a Queue. + * @param requestQueue the reference to the @a Request @a Queue. */ - Network(const bool local, const uint16_t port, const uint16_t httpPort, Queue* netQueue); + Network(const bool local, const uint16_t port, const uint16_t httpPort, Queue* requestQueue); /** * destructor. @@ -317,8 +139,8 @@ class Network : public Thread { /** the list of active @a Connection instances. */ list m_connections; - /** the reference to the @a NetMessage @a Queue. */ - Queue* m_netQueue; + /** the reference to the @a Request @a Queue. */ + Queue* m_requestQueue; /** the command line @a TCPServer instance. */ TCPServer* m_tcpServer; diff --git a/src/ebusd/request.cpp b/src/ebusd/request.cpp new file mode 100644 index 000000000..f960c2104 --- /dev/null +++ b/src/ebusd/request.cpp @@ -0,0 +1,143 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "ebusd/request.h" +#include +#include +#include +#include "lib/utils/log.h" + +namespace ebusd { + +RequestImpl::RequestImpl(bool isHttp) + : Request(), m_isHttp(isHttp), m_resultSet(false), m_disconnect(false), m_listenSince(0) { + m_mode.listenMode = lm_none; + m_mode.format = OF_NONE; + m_mode.listenWithUnknown = false; + m_mode.listenOnlyUnknown = false; + pthread_mutex_init(&m_mutex, nullptr); + pthread_cond_init(&m_cond, nullptr); +} + +RequestImpl::~RequestImpl() { + m_resultSet = true; + pthread_mutex_destroy(&m_mutex); + pthread_cond_destroy(&m_cond); +} + +bool RequestImpl::add(const char* request) { + if (request && request[0]) { + string add = request; + add.erase(remove(add.begin(), add.end(), '\r'), add.end()); + m_request.append(add); + } + size_t pos = m_request.find(m_isHttp ? "\n\n" : "\n"); + if (pos != string::npos) { + if (m_isHttp) { + pos = m_request.find("\n"); + m_request.resize(pos); // reduce to first line + // typical first line: GET /ehp/outsidetemp HTTP/1.1 + pos = m_request.rfind(" HTTP/"); + if (pos != string::npos) { + m_request.resize(pos); // remove "HTTP/x.x" suffix + } + pos = 0; + while ((pos=m_request.find('%', pos)) != string::npos && pos+2 <= m_request.length()) { + unsigned int value1, value2; + if (sscanf("%1x%1x", m_request.c_str()+pos+1, &value1, &value2) < 2) { + break; + } + m_request[pos] = static_cast(((value1&0x0f) << 4) | (value2&0x0f)); + m_request.erase(pos+1, 2); + } + } else if (pos+1 == m_request.length()) { + m_request.resize(pos); // reduce to complete lines + } + return true; + } + return m_request.length() == 0 && m_mode.listenMode != lm_none; +} + +void RequestImpl::split(vector* args) { + string token, previous; + istringstream stream(m_request); + char escaped = 0; + + char delim = ' '; + while (getline(stream, token, delim)) { + if (!m_isHttp) { + if (escaped) { + args->pop_back(); + if (token.length() > 0 && token[token.length()-1] == escaped) { + token.erase(token.length() - 1, 1); + escaped = 0; + } + token = previous + " " + token; + } else if (token.length() == 0) { // allow multiple space chars for a single delimiter + continue; + } else if (token[0] == '"' || token[0] == '\'') { + escaped = token[0]; + token.erase(0, 1); + if (token.length() > 0 && token[token.length()-1] == escaped) { + token.erase(token.length() - 1, 1); + escaped = 0; + } + } + } + args->push_back(token); + previous = token; + if (m_isHttp) { + delim = (args->size() == 1) ? '?' : '\n'; + } + } +} + +bool RequestImpl::waitResponse(string* result) { + pthread_mutex_lock(&m_mutex); + + if (!m_resultSet) { + pthread_cond_wait(&m_cond, &m_mutex); + } + m_request.clear(); + *result = m_result; + m_result.clear(); + m_resultSet = false; + pthread_mutex_unlock(&m_mutex); + return m_disconnect; +} + +void RequestImpl::setResult(const string& result, const string& user, RequestMode* mode, time_t listenUntil, + bool disconnect) { + pthread_mutex_lock(&m_mutex); + m_result = result; + m_user = user; + m_disconnect = disconnect; + if (mode) { + m_mode = *mode; + } + m_listenSince = listenUntil; + m_resultSet = true; + pthread_cond_signal(&m_cond); + pthread_mutex_unlock(&m_mutex); +} + +} // namespace ebusd diff --git a/src/ebusd/request.h b/src/ebusd/request.h new file mode 100644 index 000000000..27c0d9bd5 --- /dev/null +++ b/src/ebusd/request.h @@ -0,0 +1,230 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EBUSD_REQUEST_H_ +#define EBUSD_REQUEST_H_ + +#include +#include +#include +#include +#include "lib/ebus/datatype.h" +#include "lib/utils/queue.h" +#include "lib/utils/notify.h" +#include "lib/utils/thread.h" +#include "lib/utils/log.h" + +namespace ebusd { + +/** \file ebusd/request.h + * Abstraction of ebusd client requests. + */ + +/** the request listen mode. */ +enum ListenMode { + lm_none, //!< normal mode (no listening) + lm_listen, //!< listening mode + lm_direct, //!< direct mode +}; + +/** + * Request mode info. + */ +struct RequestMode { + ListenMode listenMode; //!< whether in listening or direct mode + OutputFormat format; //!< the output format settings for listen mode + bool listenWithUnknown; //!< include unknown messages in listen/direct mode + bool listenOnlyUnknown; //!< only print unknown messages in listen/direct mode +}; + +/** + * Abstract class for request/response. + */ +class Request { + public: + /** + * Destructor. + */ + virtual ~Request() { } + + /** + * Add request data from the client. + * @param request the request data from the client. + * @return true when the request is complete and the response shall be prepared. + */ + virtual bool add(const char* request) = 0; + + /** + * @return whether the request is still empty. + */ + virtual bool empty() const = 0; + + /** + * Split the request into arguments. + * @param args the @a vector to push the arguments to. + */ + virtual void split(vector* args) = 0; + + /** + * Return whether this is a HTTP request. + * @return whether this is a HTTP request. + */ + virtual bool isHttp() const = 0; + + /** + * Log the request or the given response in debug level. + */ + virtual void log(const string* response = nullptr) const = 0; + + /** + * Return the current user name. + * @return the current user name. + */ + virtual const string& getUser() const = 0; + + /** + * Wait for the response being set and return the result string. + * @param result the variable in which to store the result string. + * @return true when the client shall be disconnected. + */ + virtual bool waitResponse(string* result) = 0; + + /** + * Set the result string and notify a waiting thread. + * @param result the result string. + * @param user the new user name. + * @param newMode the new @a RequestMode. + * @param listenUntil the end time to which to updates were added (exclusive). + * @param disconnect true when the client shall be disconnected. + */ + virtual void setResult(const string& result, const string& user, RequestMode* newMode, time_t listenUntil, + bool disconnect) = 0; + + /** + * Return the @a RequestMode. + * @param listenSince set listening to the specified start time from which to add updates (inclusive). + * @return the @a RequestMode. + */ + virtual RequestMode getMode(time_t* listenSince = nullptr) = 0; +}; + +/** + * Default @a Request implementation. + */ +class RequestImpl : public Request { + public: + /** + * Constructor. + * @param isHttp whether this is a HTTP request. + */ + explicit RequestImpl(bool isHttp); + + /** + * Destructor. + */ + virtual ~RequestImpl(); + + + private: + /** + * Hidden copy constructor. + * @param src the object to copy from. + */ + RequestImpl(const RequestImpl& src); + + + public: + // @copydoc + bool add(const char* request) override; + + // @copydoc + bool empty() const override { return m_request.empty(); } + + // @copydoc + void split(vector* args) override; + + // @copydoc + bool isHttp() const override { return m_isHttp; } + + // @copydoc + void log(const string* response = nullptr) const override { + if (response) { + if (response->length() > 100) { + logDebug(lf_main, "<<< %s ...", response->substr(0, 100).c_str()); + } else { + logDebug(lf_main, "<<< %s", response->c_str()); + } + } else { + logDebug(lf_main, ">>> %s", m_request.c_str()); + } + } + + // @copydoc + const string& getUser() const override { return m_user; } + + // @copydoc + bool waitResponse(string* result) override; + + // @copydoc + void setResult(const string& result, const string& user, RequestMode* mode, time_t listenUntil, + bool disconnect) override; + + // @copydoc + RequestMode getMode(time_t* listenSince = nullptr) override { + if (listenSince) { + *listenSince = m_listenSince; + } + return m_mode; + } + + + private: + /** whether this is a HTTP message. */ + const bool m_isHttp; + + /** the request string. */ + string m_request; + + /** the current user name. */ + string m_user; + + /** whether the result was already set. */ + bool m_resultSet; + + /** the result string. */ + string m_result; + + /** set to true when the client shall be disconnected. */ + bool m_disconnect; + + /** mutex variable for exclusive lock. */ + pthread_mutex_t m_mutex; + + /** condition variable for exclusive lock. */ + pthread_cond_t m_cond; + + /** the @a RequestMode. */ + RequestMode m_mode; + + /** start timestamp of listening update. */ + time_t m_listenSince; +}; + +} // namespace ebusd + +#endif // EBUSD_REQUEST_H_ diff --git a/src/ebusd/scan.cpp b/src/ebusd/scan.cpp new file mode 100644 index 000000000..28ebbbe6a --- /dev/null +++ b/src/ebusd/scan.cpp @@ -0,0 +1,564 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2014-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "ebusd/scan.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "ebusd/bushandler.h" +#include "lib/utils/log.h" + + +namespace ebusd { + +using std::dec; +using std::hex; +using std::setfill; +using std::setw; +using std::nouppercase; + + +ScanHelper::~ScanHelper() { + // free templates + for (const auto& it : m_templatesByPath) { + if (it.second != &m_globalTemplates) { + delete it.second; + } + } + m_templatesByPath.clear(); + if (m_configHttpClient) { + delete m_configHttpClient; + m_configHttpClient = nullptr; + } +} + +// the time slice to sleep when repeating an HTTP request +#define REPEAT_NANOS 1000000 + +result_t ScanHelper::collectConfigFiles(const string& relPath, const string& prefix, const string& extension, + vector* files, + bool ignoreAddressPrefix, const string& query, + vector* dirs, bool* hasTemplates) { + const string relPathWithSlash = relPath.empty() ? "" : relPath + "/"; + if (!m_configUriPrefix.empty()) { + string uri = m_configUriPrefix + relPathWithSlash + m_configLangQuery + (m_configLangQuery.empty() ? "?" : "&") + + "t=" + extension.substr(1) + query; + string names; + bool repeat = false; + bool json = true; + if (!m_configHttpClient->get(uri, "", &names, &repeat, nullptr, &json)) { + if (!names.empty() || json) { + logError(lf_main, "HTTP failure%s: %s", repeat ? ", repeating" : "", names.c_str()); + names = ""; + } + if (!repeat) { + return RESULT_ERR_NOTFOUND; + } + usleep(REPEAT_NANOS); + if (!m_configHttpClient->get(uri, "", &names)) { + return RESULT_ERR_NOTFOUND; + } + } else if (!json && names[0] == '<') { // html + uri = m_configUriPrefix + relPathWithSlash + "index.json"; + json = true; + logDebug(lf_main, "trying index.json"); + if (!m_configHttpClient->get(uri, "", &names, nullptr, nullptr, &json)) { + return RESULT_ERR_NOTFOUND; + } + } + istringstream stream(names); + string name; + while (getline(stream, name)) { + if (name.empty()) { + continue; + } + if (name == "_templates"+extension) { + if (hasTemplates) { + *hasTemplates = true; + } + continue; + } + if (name.back() == '/') { + // directory + if (dirs != nullptr) { + dirs->push_back(relPathWithSlash + name.substr(0, name.length() - 1)); + } + continue; + } + if (prefix.length() == 0 ? (!ignoreAddressPrefix || name.length() < 3 || name.find_first_of('.') != 2) + : (name.length() >= prefix.length() && name.substr(0, prefix.length()) == prefix)) { + files->push_back(relPathWithSlash + name); + } + } + return RESULT_OK; + } + const string path = m_configLocalPrefix + relPathWithSlash; + logDebug(lf_main, "reading directory %s", path.c_str()); + DIR* dir = opendir(path.c_str()); + if (dir == nullptr) { + return RESULT_ERR_NOTFOUND; + } + dirent* d; + while ((d = readdir(dir)) != nullptr) { + string name = d->d_name; + if (name == "." || name == "..") { + continue; + } + const string p = path + name; + struct stat stat_buf = {}; + if (stat(p.c_str(), &stat_buf) != 0) { + logError(lf_main, "unable to stat file %s", p.c_str()); + continue; + } + logDebug(lf_main, "file type of %s is %s", p.c_str(), + S_ISDIR(stat_buf.st_mode) ? "dir" : S_ISREG(stat_buf.st_mode) ? "file" : "other"); + if (S_ISDIR(stat_buf.st_mode)) { + if (dirs != nullptr) { + dirs->push_back(relPathWithSlash + name); + } + } else if (S_ISREG(stat_buf.st_mode) && name.length() >= extension.length() + && name.substr(name.length()-extension.length()) == extension) { + if (name == "_templates"+extension) { + if (hasTemplates) { + *hasTemplates = true; + } + continue; + } + if (prefix.length() == 0 ? (!ignoreAddressPrefix || name.length() < 3 || name.find_first_of('.') != 2) + : (name.length() >= prefix.length() && name.substr(0, prefix.length()) == prefix)) { + files->push_back(relPathWithSlash + name); + } + } + } + closedir(dir); + + return RESULT_OK; +} + +DataFieldTemplates* ScanHelper::getTemplates(const string& filename) { + if (filename == "*") { + size_t maxLength = 0; + DataFieldTemplates* best = nullptr; + for (auto it : m_templatesByPath) { + if (it.first.size() > maxLength) { + best = it.second; + } + } + if (best) { + return best; + } + } else { + string path; + size_t pos = filename.find_last_of('/'); + if (pos != string::npos) { + path = filename.substr(0, pos); + } + const auto it = m_templatesByPath.find(path); + if (it != m_templatesByPath.end()) { + return it->second; + } + } + return &m_globalTemplates; +} + +bool ScanHelper::readTemplates(const string relPath, const string extension, bool available) { + const auto it = m_templatesByPath.find(relPath); + if (it != m_templatesByPath.end()) { + return false; + } + DataFieldTemplates* templates; + if (relPath.empty() || !available) { + templates = &m_globalTemplates; + } else { + templates = new DataFieldTemplates(m_globalTemplates); + } + m_templatesByPath[relPath] = templates; + if (!available) { + // global templates are stored as replacement in order to determine whether the directory was already loaded + return true; + } + string errorDescription; + string logPath = relPath.empty() ? "/" : relPath; + logInfo(lf_main, "reading templates %s", logPath.c_str()); + string file = (relPath.empty() ? "" : relPath + "/") + "_templates" + extension; + result_t result = loadDefinitionsFromConfigPath(templates, file, nullptr, &errorDescription, true); + if (result == RESULT_OK) { + logInfo(lf_main, "read templates in %s", logPath.c_str()); + return true; + } + logError(lf_main, "error reading templates in %s: %s, last error: %s", logPath.c_str(), getResultCode(result), + errorDescription.c_str()); + return false; +} + +void ScanHelper::dumpTemplates(OutputFormat outputFormat, ostream* output) const { + bool prependSeparator = false; + for (auto it : m_templatesByPath) { + if (prependSeparator) { + *output << ","; + } + const auto templates = it.second; + if (templates->dump(outputFormat, output)) { + prependSeparator = true; + } + } +} + +result_t ScanHelper::readConfigFiles(const string& relPath, const string& extension, bool recursive, + string* errorDescription) { + vector files, dirs; + bool hasTemplates = false; + result_t result = collectConfigFiles(relPath, "", extension, &files, false, "", &dirs, &hasTemplates); + if (result != RESULT_OK) { + return result; + } + readTemplates(relPath, extension, hasTemplates); + for (const auto& name : files) { + logInfo(lf_main, "reading file %s", name.c_str()); + result = loadDefinitionsFromConfigPath(m_messages, name, nullptr, errorDescription); + if (result != RESULT_OK) { + return result; + } + logInfo(lf_main, "successfully read file %s", name.c_str()); + } + if (recursive) { + for (const auto& name : dirs) { + logInfo(lf_main, "reading dir %s", name.c_str()); + result = readConfigFiles(name, extension, true, errorDescription); + if (result != RESULT_OK) { + return result; + } + logInfo(lf_main, "successfully read dir %s", name.c_str()); + } + } + return RESULT_OK; +} + +static BusHandler* executeInstructionsBusHandlerInstance = nullptr; + +/** + * Helper method for immediate reading of a @a Message from the bus. + * @param message the @a Message to read. + */ +static void readMessage(Message* message) { + if (!executeInstructionsBusHandlerInstance || !message) { + return; + } + result_t result = executeInstructionsBusHandlerInstance->readFromBus(message, ""); + if (result != RESULT_OK) { + logError(lf_main, "error reading message %s %s: %s", message->getCircuit().c_str(), message->getName().c_str(), + getResultCode(result)); + } +} + +result_t ScanHelper::executeInstructions(BusHandler* busHandler) { + string errorDescription; + result_t result = m_messages->resolveConditions(m_verbose, &errorDescription); + if (result != RESULT_OK) { + logError(lf_main, "error resolving conditions: %s, last error: %s", getResultCode(result), + errorDescription.c_str()); + } + ostringstream log; + executeInstructionsBusHandlerInstance = busHandler; + result = m_messages->executeInstructions(&readMessage, &log); + if (result != RESULT_OK) { + logError(lf_main, "error executing instructions: %s, last error: %s", getResultCode(result), + log.str().c_str()); + } else if (m_verbose && log.tellp() > 0) { + logInfo(lf_main, log.str().c_str()); + } + logNotice(lf_main, "found messages: %d (%d conditional on %d conditions, %d poll, %d update)", m_messages->size(), + m_messages->sizeConditional(), m_messages->sizeConditions(), m_messages->sizePoll(), m_messages->sizePassive()); + return result; +} + +result_t ScanHelper::loadDefinitionsFromConfigPath(FileReader* reader, const string& filename, + map* defaults, string* errorDescription, bool replace) { + istream* stream = nullptr; + time_t mtime = 0; + if (m_configUriPrefix.empty()) { + stream = FileReader::openFile(m_configLocalPrefix + filename, errorDescription, &mtime); + } else if (m_configHttpClient) { + string uri = m_configUriPrefix + filename + m_configLangQuery; + string content; + bool repeat = false; + if (m_configHttpClient->get(uri, "", &content, &repeat, &mtime)) { + stream = new istringstream(content); + } else { + if (!content.empty()) { + logError(lf_main, "HTTP failure%s: %s", repeat ? ", repeating" : "", content.c_str()); + content = ""; + } + if (repeat) { + usleep(REPEAT_NANOS); + if (m_configHttpClient->get(uri, "", &content, nullptr, &mtime)) { + stream = new istringstream(content); + } + } + } + } + result_t result; + if (stream) { + result = reader->readFromStream(stream, filename, mtime, m_verbose, defaults, errorDescription, replace); + delete(stream); + } else { + result = RESULT_ERR_NOTFOUND; + } + return result; +} + +result_t ScanHelper::loadConfigFiles(bool recursive) { + logInfo(lf_main, "loading configuration files from %s", m_configPath.c_str()); + m_messages->lock(); + m_messages->clear(); + m_globalTemplates.clear(); + for (auto& it : m_templatesByPath) { + if (it.second != &m_globalTemplates) { + delete it.second; + } + it.second = nullptr; + } + m_templatesByPath.clear(); + + string errorDescription; + result_t result = readConfigFiles("", ".csv", recursive, &errorDescription); + if (result == RESULT_OK) { + logInfo(lf_main, "read config files, got %d messages", m_messages->size()); + } else { + logError(lf_main, "error reading config files from %s: %s, last error: %s", m_configPath.c_str(), + getResultCode(result), errorDescription.c_str()); + } + m_messages->unlock(); + return result; +} + +result_t ScanHelper::loadScanConfigFile(symbol_t address, string* relativeFile) { + Message* message = m_messages->getScanMessage(address); + if (!message || message->getLastUpdateTime() == 0) { + return RESULT_ERR_NOTFOUND; + } + const SlaveSymbolString& data = message->getLastSlaveData(); + if (data.getDataSize() < 1+5+2+2) { + logError(lf_main, "unable to load scan config %2.2x: slave part too short (%d)", address, data.getDataSize()); + return RESULT_EMPTY; + } + DataFieldSet* identFields = DataFieldSet::getIdentFields(); + string manufStr, addrStr, ident; // path: cfgpath/MANUFACTURER, prefix: ZZ., ident: C[C[C[C[C]]]], SW: xxxx, HW: xxxx + unsigned int sw = 0, hw = 0; + ostringstream out; + size_t offset = 0; + size_t field = 0; + bool fromLocal = m_configUriPrefix.empty(); + // manufacturer name + result_t result = (*identFields)[field]->read(data, offset, false, nullptr, -1, OF_NONE, -1, &out); + if (result == RESULT_ERR_NOTFOUND && fromLocal) { + result = (*identFields)[field]->read(data, offset, false, nullptr, -1, OF_NUMERIC, -1, &out); // manufacturer name + } + if (result == RESULT_OK) { + manufStr = out.str(); + transform(manufStr.begin(), manufStr.end(), manufStr.begin(), ::tolower); + out.str(""); + out << setw(2) << hex << setfill('0') << nouppercase << static_cast(address); + addrStr = out.str(); + out.str(""); + out.clear(); + offset += (*identFields)[field++]->getLength(pt_slaveData, MAX_LEN); + result = (*identFields)[field]->read(data, offset, false, nullptr, -1, OF_NONE, -1, &out); // identification string + } + if (result == RESULT_OK) { + ident = out.str(); + out.str(""); + out.clear(); + offset += (*identFields)[field++]->getLength(pt_slaveData, MAX_LEN); + result = (*identFields)[field]->read(data, offset, nullptr, -1, &sw); // software version number + if (result == RESULT_ERR_OUT_OF_RANGE) { + sw = (data.dataAt(offset) << 16) | data.dataAt(offset+1); // use hex value instead + result = RESULT_OK; + } + } + if (result == RESULT_OK) { + offset += (*identFields)[field++]->getLength(pt_slaveData, MAX_LEN); + result = (*identFields)[field]->read(data, offset, nullptr, -1, &hw); // hardware version number + if (result == RESULT_ERR_OUT_OF_RANGE) { + hw = (data.dataAt(offset) << 16) | data.dataAt(offset+1); // use hex value instead + result = RESULT_OK; + } + } + if (result != RESULT_OK) { + logError(lf_main, "unable to load scan config %2.2x: decode field %s %s", address, + identFields->getName(field).c_str(), getResultCode(result)); + return result; + } + bool hasTemplates = false; + string best; + map bestDefaults; + vector files; + auto it = ident.begin(); + while (it != ident.end()) { + if (*it != '_' && !::isalnum(*it)) { + it = ident.erase(it); + } else { + *it = static_cast(::tolower(*it)); + it++; + } + } + // find files matching MANUFACTURER/ZZ.*csv in cfgpath + string query; + if (!fromLocal) { + out << "&a=" << addrStr << "&i=" << ident << "&h=" << dec << static_cast(hw) << "&s=" << dec + << static_cast(sw); + query = out.str(); + out.str(""); + out.clear(); + } + result = collectConfigFiles(manufStr, addrStr + ".", ".csv", &files, false, query, nullptr, &hasTemplates); + if (result != RESULT_OK) { + logError(lf_main, "unable to load scan config %2.2x: list files in %s %s", address, manufStr.c_str(), + getResultCode(result)); + return result; + } + if (files.empty()) { + logError(lf_main, "unable to load scan config %2.2x: no file from %s with prefix %s found", address, + manufStr.c_str(), addrStr.c_str()); + return RESULT_ERR_NOTFOUND; + } + logDebug(lf_main, "found %d matching scan config files from %s with prefix %s: %s", files.size(), manufStr.c_str(), + addrStr.c_str(), getResultCode(result)); + // complete name: cfgpath/MANUFACTURER/ZZ[.C[C[C[C[C]]]]][.circuit][.suffix][.*][.SWxxxx][.HWxxxx][.*].csv + size_t bestMatch = 0; + for (const auto& name : files) { + symbol_t checkDest; + unsigned int checkSw, checkHw; + map defaults; + const string filename = name.substr(manufStr.length()+1); + if (!m_messages->extractDefaultsFromFilename(filename, &defaults, &checkDest, &checkSw, &checkHw)) { + continue; + } + if (address != checkDest || (checkSw != UINT_MAX && sw != checkSw) || (checkHw != UINT_MAX && hw != checkHw)) { + continue; + } + size_t match = 1; + string checkIdent = defaults["name"]; + if (!checkIdent.empty()) { + string remain = ident; + bool matches = false; + while (remain.length() > 0 && remain.length() >= checkIdent.length()) { + if (checkIdent == remain) { + matches = true; + break; + } + if (!::isdigit(remain[remain.length()-1])) { + break; + } + remain.erase(remain.length()-1); // remove trailing digit + } + if (!matches) { + continue; // IDENT mismatch + } + match += remain.length(); + } + if (match > bestMatch || (match == bestMatch && name.length() > best.length())) { + bestMatch = match; + best = name; + bestDefaults = defaults; + } + } + + if (best.empty()) { + logError(lf_main, + "unable to load scan config %2.2x: no file from %s with prefix %s matches ID \"%s\", SW%4.4d, HW%4.4d", + address, manufStr.c_str(), addrStr.c_str(), ident.c_str(), sw, hw); + return RESULT_ERR_NOTFOUND; + } + + // found the right file. load the templates if necessary, then load the file itself + bool readCommon = readTemplates(manufStr, ".csv", hasTemplates); + if (readCommon) { + result = collectConfigFiles(manufStr, "", ".csv", &files, true, "&a=-"); + if (result == RESULT_OK && !files.empty()) { + for (const auto& name : files) { + string baseName = name.substr(manufStr.length()+1, name.length()-manufStr.length()-strlen(".csv")); // *. + if (baseName == "_templates.") { // skip templates + continue; + } + if (baseName.length() < 3 || baseName.find_first_of('.') != 2) { // different from the scheme "ZZ." + string errorDescription; + result = loadDefinitionsFromConfigPath(m_messages, name, nullptr, &errorDescription); + if (result == RESULT_OK) { + logNotice(lf_main, "read common config file %s", name.c_str()); + } else { + logError(lf_main, "error reading common config file %s: %s, %s", name.c_str(), getResultCode(result), + errorDescription.c_str()); + } + } + } + } + } + bestDefaults["name"] = ident; + string errorDescription; + result = loadDefinitionsFromConfigPath(m_messages, best, &bestDefaults, &errorDescription); + if (result != RESULT_OK) { + logError(lf_main, "error reading scan config file %s for ID \"%s\", SW%4.4d, HW%4.4d: %s, %s", best.c_str(), + ident.c_str(), sw, hw, getResultCode(result), errorDescription.c_str()); + return result; + } + logNotice(lf_main, "read scan config file %s for ID \"%s\", SW%4.4d, HW%4.4d", best.c_str(), ident.c_str(), sw, hw); + *relativeFile = best; + return RESULT_OK; +} + +bool ScanHelper::parseMessage(const string& arg, bool onlyMasterSlave, MasterSymbolString* master, + SlaveSymbolString* slave) { + size_t pos = arg.find_first_of('/'); + if (pos == string::npos) { + logError(lf_main, "invalid message %s: missing \"/\"", arg.c_str()); + return false; + } + result_t result = master->parseHex(arg.substr(0, pos)); + if (result == RESULT_OK) { + result = slave->parseHex(arg.substr(pos+1)); + } + if (result != RESULT_OK) { + logError(lf_main, "invalid message %s: %s", arg.c_str(), getResultCode(result)); + return false; + } + if (master->size() < 5) { // skip QQ ZZ PB SB NN + logError(lf_main, "invalid message %s: master part too short", arg.c_str()); + return false; + } + if (!isMaster((*master)[0])) { + logError(lf_main, "invalid message %s: QQ is no master", arg.c_str()); + return false; + } + if (!isValidAddress((*master)[1], !onlyMasterSlave) || (onlyMasterSlave && isMaster((*master)[1]))) { + logError(lf_main, "invalid message %s: ZZ is invalid", arg.c_str()); + return false; + } + return true; +} + +} // namespace ebusd diff --git a/src/ebusd/scan.h b/src/ebusd/scan.h new file mode 100644 index 000000000..413a5974a --- /dev/null +++ b/src/ebusd/scan.h @@ -0,0 +1,222 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2014-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EBUSD_SCAN_H_ +#define EBUSD_SCAN_H_ + +#include +#include +#include +#include +#include "lib/ebus/data.h" +#include "lib/ebus/message.h" +#include "lib/ebus/result.h" +#include "lib/utils/httpclient.h" +#include "lib/utils/log.h" + +namespace ebusd { + +/** \file ebusd/scan.h + * Helpers for handling device scanning and config loading. + */ + +class BusHandler; + +/** + * Helper class for handling device scanning and config loading. + */ +class ScanHelper : public Resolver { + public: + /** + * Constructor. + * @param messages the @a MessageMap to load the messages into. + * @param configPath the (optionally corrected) config path for retrieving configuration files from. + * @param configLocalPrefix the path prefix (including trailing "/") for retrieving configuration files from local files (empty for HTTPS). + * @param configUriPrefix the URI prefix (including trailing "/") for retrieving configuration files from HTTPS (empty for local files). + * @param configLangQuery the optional language query part for retrieving configuration files from HTTPS (empty for local files). + * @param configHttpClient the @a HttpClient for retrieving configuration files from HTTPS. + * @param verbose whether to verbosely log problems. + */ + ScanHelper(MessageMap* messages, + const string configPath, const string configLocalPrefix, + const string configUriPrefix, const string configLangQuery, + HttpClient* configHttpClient, bool verbose) + : Resolver(), m_messages(messages), + m_configPath(configPath), m_configLocalPrefix(configLocalPrefix), + m_configUriPrefix(configUriPrefix), m_configLangQuery(configLangQuery), + m_configHttpClient(configHttpClient), m_verbose(verbose) {} + + /** + * Destructor. + */ + virtual ~ScanHelper(); + + /** + * @return the (optionally corrected) config path for retrieving configuration files from. + */ + const string getConfigPath() const { return m_configPath; } + + /** + * @return the config path when pointing to CDN, empty otherwise. + */ + const string getConfigPathCDN() const { return m_configUriPrefix.empty() ? "" : m_configPath; } + + /** + * Try to connect to the specified server. + * @param host the host name to connect to. + * @param port the port to connect to. + * @param https true for HTTPS, false for HTTP. + * @param timeout the timeout in seconds, defaults to 5 seconds. + * @return true on success, false on connect failure. + */ + bool connect(const string& host, uint16_t port, bool https = false, int timeout = 5); + + /** + * Get the @a DataFieldTemplates for the specified configuration file. + * @param filename the full name of the configuration file, or "*" to get the non-root templates with the longest name + * or the root templates if not available. + * @return the @a DataFieldTemplates. + */ + virtual DataFieldTemplates* getTemplates(const string& filename); + + /** + * Load the message definitions from configuration files. + * @param recursive whether to load all files recursively. + * @return the result code. + */ + result_t loadConfigFiles(bool recursive = true); + + /** + * Load the message definitions from a configuration file matching the scan result. + * @param address the address of the scan participant + * (either master for broadcast master data or slave for read slave data). + * @param data the scan @a SlaveSymbolString for which to load the configuration file. + * @param relativeFile the string in which the name of the configuration file is stored on success. + * @return the result code. + */ + result_t loadScanConfigFile(symbol_t address, string* relativeFile); + + /** + * Helper method for executing all loaded and resolvable instructions. + * @param busHandler the @a BusHandler instance. + * @return the result code. + */ + result_t executeInstructions(BusHandler* busHandler); + + /** + * Helper method for loading definitions from a relative file from the config path/URL. + * @param reader the @a FileReader instance to load with the definitions. + * @param filename the relative name of the file being read. + * @param defaults the default values by name (potentially overwritten by file name), or nullptr to not use defaults. + * @param errorDescription a string in which to store the error description in case of error. + * @param replace whether to replace an already existing entry. + * @return @a RESULT_OK on success, or an error code. + */ + virtual result_t loadDefinitionsFromConfigPath(FileReader* reader, const string& filename, + map* defaults, string* errorDescription, bool replace = false); + + /** + * Helper method for parsing a master/slave message pair from a command line argument. + * @param arg the argument to parse. + * @param onlyMasterSlave true to parse only a MS message, false to also parse MM and BC message. + * @param master the @a MasterSymbolString to parse into. + * @param slave the @a SlaveSymbolString to parse into. + * @return true when the argument was valid, false otherwise. + */ + bool parseMessage(const string& arg, bool onlyMasterSlave, MasterSymbolString* master, SlaveSymbolString* slave); + + + private: + /** + * Collect configuration files matching the prefix and extension from the specified path. + * @param relPath the relative path from which to collect the files (without trailing "/"). + * @param prefix the filename prefix the files have to match, or empty. + * @param extension the filename extension the files have to match. + * @param files the @a vector to which to add the matching files. + * @param query the query string suffix for HTTPS retrieval starting with "&", or empty. + * @param dirs the @a vector to which to add found directories (without any name check), or nullptr to ignore. + * @param hasTemplates the bool to set when the templates file was found in the path, or nullptr to ignore. + * @return the result code. + */ + result_t collectConfigFiles(const string& relPath, const string& prefix, const string& extension, + vector* files, + bool ignoreAddressPrefix = false, const string& query = "", + vector* dirs = nullptr, bool* hasTemplates = nullptr); + + /** + * Read the @a DataFieldTemplates for the specified path if necessary. + * @param relPath the relative path from which to read the files (without trailing "/"). + * @param extension the filename extension of the files to read. + * @param available whether the templates file is available in the path. + * @return false when the templates for the path were already loaded before, true when the templates for the path were added (independent from @a available). + * @return the @a DataFieldTemplates. + */ + bool readTemplates(const string relPath, const string extension, bool available); + + /** + * Dump the loaded @a DataFieldTemplates to the output. + * @param outputFormat the @a OutputFormat options. + * @param output the @a ostream to dump to. + */ + void dumpTemplates(OutputFormat outputFormat, ostream* output) const; + + /** + * Read the configuration files from the specified path. + * @param relPath the relative path from which to read the files (without trailing "/"). + * @param extension the filename extension of the files to read. + * @param recursive whether to load all files recursively. + * @param errorDescription a string in which to store the error description in case of error. + * @return the result code. + */ + result_t readConfigFiles(const string& relPath, const string& extension, bool recursive, + string* errorDescription); + + /** the @a MessageMap instance. */ + MessageMap* m_messages; + + /** the (optionally corrected) config path for retrieving configuration files from. */ + const string m_configPath; + + /** the path prefix (including trailing "/") for retrieving configuration files from local files (empty for HTTPS). */ + const string m_configLocalPrefix; + + /** the URI prefix (including trailing "/") for retrieving configuration files from HTTPS (empty for local files). */ + const string m_configUriPrefix; + + /** the optional language query part for retrieving configuration files from HTTPS (empty for local files). */ + const string m_configLangQuery; + + /** the @a HttpClient for retrieving configuration files from HTTPS. */ + HttpClient* m_configHttpClient; + + /** whether to verbosely log problems. */ + const bool m_verbose; + + /** the global @a DataFieldTemplates. */ + DataFieldTemplates m_globalTemplates; + + /** + * the loaded @a DataFieldTemplates by relative path (may also carry + * @a globalTemplates as replacement for missing file). + */ + map m_templatesByPath; +}; + +} // namespace ebusd + +#endif // EBUSD_SCAN_H_ diff --git a/src/lib/ebus/CMakeLists.txt b/src/lib/ebus/CMakeLists.txt index cf69c5e38..dc617ed9b 100644 --- a/src/lib/ebus/CMakeLists.txt +++ b/src/lib/ebus/CMakeLists.txt @@ -6,7 +6,11 @@ set(libebus_a_SOURCES filereader.h filereader.cpp datatype.h datatype.cpp data.h data.cpp - device.h device.cpp + device.h device_enhanced.h + device_trans.h device_trans.cpp + transport.h transport.cpp + protocol.h protocol.cpp + protocol_direct.h protocol_direct.cpp message.h message.cpp stringhelper.h stringhelper.cpp ) @@ -16,6 +20,7 @@ if(HAVE_CONTRIB) endif(HAVE_CONTRIB) add_library(ebus ${libebus_a_SOURCES}) +target_link_libraries(ebus utils ${libebus_a_LIBS}) if(BUILD_TESTING) add_subdirectory(test) diff --git a/src/lib/ebus/Makefile.am b/src/lib/ebus/Makefile.am index 245ad7542..cc4f386a9 100644 --- a/src/lib/ebus/Makefile.am +++ b/src/lib/ebus/Makefile.am @@ -10,7 +10,11 @@ libebus_a_SOURCES = \ filereader.h filereader.cpp \ datatype.h datatype.cpp \ data.h data.cpp \ - device.h device.cpp \ + device.h device_enhanced.h \ + device_trans.h device_trans.cpp \ + transport.h transport.cpp \ + protocol.h protocol.cpp \ + protocol_direct.h protocol_direct.cpp \ message.h message.cpp \ stringhelper.h stringhelper.cpp diff --git a/src/lib/ebus/contrib/contrib.cpp b/src/lib/ebus/contrib/contrib.cpp index 587353b20..6aaaaecdb 100755 --- a/src/lib/ebus/contrib/contrib.cpp +++ b/src/lib/ebus/contrib/contrib.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/ebus/contrib/contrib.h b/src/lib/ebus/contrib/contrib.h index 8352001c5..f8a25a478 100755 --- a/src/lib/ebus/contrib/contrib.h +++ b/src/lib/ebus/contrib/contrib.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/ebus/contrib/tem.cpp b/src/lib/ebus/contrib/tem.cpp index ab60bde32..3a4f1f8fe 100755 --- a/src/lib/ebus/contrib/tem.cpp +++ b/src/lib/ebus/contrib/tem.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -72,9 +72,11 @@ result_t TemParamDataType::readSymbols(size_t offset, size_t length, const Symbo } int grp = 0, num = 0; if (input.isMaster()) { - grp = (value & 0x1f); // grp in bits 0...5 - num = ((value >> 8) & 0x7f); // num in bits 8...13 + // bits are distributed like this in 2 bytes: xNNN NNNN xxxG GGGG + grp = (value & 0x1f); // grp in bits 0...4 + num = ((value >> 8) & 0x7f); // num in bits 8...14 } else { + // bits are distributed like this in 2 bytes: xxxx GGGG GNNN NNNN grp = ((value >> 7) & 0x1f); // grp in bits 7...11 num = (value & 0x7f); // num in bits 0...6 } @@ -126,13 +128,16 @@ result_t TemParamDataType::writeSymbols(const size_t offset, const size_t length return RESULT_ERR_OUT_OF_RANGE; // value out of range } if (output->isMaster()) { - value = grp | (num << 8); // grp in bits 0...5, num in bits 8...13 + // bits are distributed like this in 2 bytes: xNNN NNNN xxxG GGGG + value = grp | (num << 8); // grp in bits 0...4, num in bits 8...14 } else { + // bits are distributed like this in 2 bytes: xxxx GGGG GNNN NNNN value = (grp << 7) | num; // grp in bits 7...11, num in bits 0...6 } } - if (value < getMinValue() || value > getMaxValue()) { - return RESULT_ERR_OUT_OF_RANGE; // value out of range + result_t ret = checkValueRange(value); + if (ret != RESULT_OK) { + return ret; } return writeRawValue(value, offset, length, output, usedLength); } diff --git a/src/lib/ebus/contrib/tem.h b/src/lib/ebus/contrib/tem.h index bc2e3c093..08bd2b84d 100755 --- a/src/lib/ebus/contrib/tem.h +++ b/src/lib/ebus/contrib/tem.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/ebus/contrib/test/test_tem.cpp b/src/lib/ebus/contrib/test/test_tem.cpp index ab4a1c497..887c31a10 100755 --- a/src/lib/ebus/contrib/test/test_tem.cpp +++ b/src/lib/ebus/contrib/test/test_tem.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/ebus/data.cpp b/src/lib/ebus/data.cpp index 7b8cf1252..3d142a26b 100644 --- a/src/lib/ebus/data.cpp +++ b/src/lib/ebus/data.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,7 +32,6 @@ namespace ebusd { using std::dec; -using std::hex; using std::setw; /** the week day names. */ @@ -192,6 +191,34 @@ string AttributedItem::getAttribute(const string& name) const { } +bool isValidIdentifierChar(char ch, bool first, bool allowFirstDigit) { + return ((ch >= '0' && ch <= '9') && (!first || allowFirstDigit)) + || (ch >= 'a' && ch <= 'z') + || (ch >= 'A' && ch <= 'Z') + || ch == '_' || ch == '$' + // todo '.' is the only excuse for now and should be removed some day + || (ch == '.' && !first); +} + +bool DataField::checkIdentifier(const string& name, bool allowFirstDigit) { + for (size_t i = 0; i < name.size(); i++) { + char ch = name[i]; + if (!isValidIdentifierChar(ch, i == 0, allowFirstDigit)) { + return false; + } + } + return true; +} + +void DataField::normalizeIdentifier(string& name, bool allowFirstDigit) { + for (size_t i = 0; i < name.size(); i++) { + char ch = name[i]; + if (!isValidIdentifierChar(ch, i == 0, allowFirstDigit)) { + name[i] = '_'; + } + } +} + result_t DataField::create(bool isWriteMessage, bool isTemplate, bool isBroadcastOrMasterDestination, size_t maxFieldLength, const DataFieldTemplates* templates, vector< map >* rows, string* errorDescription, const DataField** returnField) { @@ -246,6 +273,7 @@ result_t DataField::create(bool isWriteMessage, bool isTemplate, bool isBroadcas string divisorStr = pluck("divisor", &row); string valuesStr = pluck("values", &row); + string rangeStr = pluck("range", &row); if (divisorStr.empty() && valuesStr.empty()) { divisorStr = pluck("divisor/values", &row); // [divisor|values] if (divisorStr.find('=') != string::npos) { @@ -282,27 +310,22 @@ result_t DataField::create(bool isWriteMessage, bool isTemplate, bool isBroadcas while (getline(stream, token, VALUE_SEPARATOR)) { FileReader::trim(&token); const char* str = token.c_str(); - char* strEnd = nullptr; - unsigned long id; - if (strncasecmp(str, "0x", 2) == 0) { - str += 2; - id = strtoul(str, &strEnd, 16); // hexadecimal - } else { - id = strtoul(str, &strEnd, 10); // decimal - } - if (strEnd == nullptr || strEnd == str || id > MAX_VALUE) { + size_t len = 0; + unsigned int id = parseInt(str, 0, 0, MAX_VALUE, &result, &len, true); + if (result != RESULT_OK) { *errorDescription = "value "+token+" in field "+formatInt(fieldIndex); result = RESULT_ERR_INVALID_LIST; break; } + str += len; // remove blanks around '=' sign - while (*strEnd == ' ') strEnd++; - if (*strEnd != '=') { + while (*str == ' ') str++; + if (*str != '=') { *errorDescription = "value "+token+" in field "+formatInt(fieldIndex); result = RESULT_ERR_INVALID_LIST; break; } - token = string(strEnd + 1); + token = string(str + 1); FileReader::trim(&token); values[(unsigned int)id] = token; } @@ -343,9 +366,63 @@ result_t DataField::create(bool isWriteMessage, bool isTemplate, bool isBroadcas } transform(typeName.begin(), typeName.end(), typeName.begin(), ::toupper); const DataType* dataType = DataTypeList::getInstance()->get(typeName, length == REMAIN_LEN ? 0 : length); + if (dataType && dataType->isNumeric() && !rangeStr.empty()) { + const NumberDataType* numType = reinterpret_cast(dataType); + if (divisor != 1 && divisor != 0) { + result = numType->derive(divisor, numType->getBitCount(), &numType); + divisor = 1; + } + // either from-to or from-to:step + size_t sepPos = rangeStr.find('-', 1); + string part = rangeStr.substr(0, sepPos); + FileReader::trim(&part); + unsigned int from; + if (result == RESULT_OK) { + result = numType->parseInput(part, &from); + } + unsigned int to = from; + unsigned int inc = 0; + if (result == RESULT_OK) { + part = rangeStr.substr(sepPos+1); + FileReader::trim(&part); + sepPos = part.find(':'); + if (sepPos != string::npos) { + string incStr = part.substr(sepPos + 1); + FileReader::trim(&incStr); + part = part.substr(0, sepPos); + FileReader::trim(&part); + result = numType->parseInput(incStr, &inc); + } + if (result == RESULT_OK) { + result = numType->parseInput(part, &to); + } + } + float ffrom = 0, fto = 0; + if (result == RESULT_OK) { + result = numType->getFloatFromRawValue(from, &ffrom); + } + if (result == RESULT_OK) { + result = numType->getFloatFromRawValue(to, &fto); + } + if (result == RESULT_OK && ffrom > fto) { + result = RESULT_ERR_INVALID_LIST; + } + if (result == RESULT_OK) { + result = numType->derive(from, to, inc, &numType); + } + if (result != RESULT_OK) { + *errorDescription = "\""+rangeStr+"\" in field "+formatInt(fieldIndex); + result = RESULT_ERR_OUT_OF_RANGE; + break; + } + dataType = numType; + } if (!dataType) { result = RESULT_ERR_NOTFOUND; *errorDescription = "field type "+typeName+" in field "+formatInt(fieldIndex); + } else if (firstType && !name.empty() && !DataField::checkIdentifier(name)) { + *errorDescription = "field name "+name; + result = RESULT_ERR_INVALID_ARG; } else { SingleDataField* add = nullptr; result = SingleDataField::create(firstType ? name : "", row, dataType, partType, length, divisor, @@ -371,14 +448,19 @@ result_t DataField::create(bool isWriteMessage, bool isTemplate, bool isBroadcas } else { fieldName = (firstType && lastType) ? name : ""; } - if (lastType) { - result = templ->derive(fieldName, partType, divisor, values, &row, &fields); + if (!fieldName.empty() && !DataField::checkIdentifier(fieldName)) { + *errorDescription = "field name "+fieldName; + result = RESULT_ERR_INVALID_ARG; } else { - map attrs = row; // don't let DataField::derive() consume the row - result = templ->derive(fieldName, partType, divisor, values, &attrs, &fields); - } - if (result != RESULT_OK) { - *errorDescription = "derive field "+fieldName+" in field "+formatInt(fieldIndex); + if (lastType) { + result = templ->derive(fieldName, partType, divisor, values, &row, &fields); + } else { + map attrs = row; // don't let DataField::derive() consume the row + result = templ->derive(fieldName, partType, divisor, values, &attrs, &fields); + } + if (result != RESULT_OK) { + *errorDescription = "derive field "+fieldName+" in field "+formatInt(fieldIndex); + } } } if (firstType && !lastType) { @@ -413,6 +495,31 @@ const char* DataField::getDayName(int day) { return dayNames[day]; } +bool DataField::addRaw(size_t offset, size_t length, const SymbolString& input, bool isJson, ostream* output) { + size_t size = input.getDataSize(); + if (offset >= size) { + return false; + } + if (isJson) { + *output << ", \"raw\": ["; + for (size_t pos = 0; pos < length && offset+pos < size; pos++) { + if (pos > 0) { + *output << ", "; + } + *output << dec << static_cast(input.dataAt(offset+pos)); + } + *output << "]"; + } else { + *output << "["; + for (size_t pos = 0; pos < length && offset+pos < size; pos++) { + *output << setw(2) << hex + << setfill('0') << static_cast(input.dataAt(offset+pos)); + } + *output << "]"; + } + return true; +} + result_t SingleDataField::create(const string& name, const map& attributes, const DataType* dataType, PartType partType, size_t length, int divisor, const string& constantValue, @@ -457,8 +564,11 @@ result_t SingleDataField::create(const string& name, const map& *returnField = new SingleDataField(name, attributes, numType, partType, byteCount); return RESULT_OK; } - if (values->begin()->first < numType->getMinValue() || values->rbegin()->first > numType->getMaxValue()) { - return RESULT_ERR_OUT_OF_RANGE; + for (auto& it : *values) { + result_t ret = numType->checkValueRange(it.first); + if (ret != RESULT_OK) { + return ret; + } } *returnField = new ValueListDataField(name, attributes, numType, partType, byteCount, *values); return RESULT_OK; @@ -482,7 +592,10 @@ void SingleDataField::dumpPrefix(bool prependFieldSeparator, OutputFormat output dumpString(prependFieldSeparator, m_name, output); } if (outputFormat & OF_JSON) { - *output << ", \"slave\": " << (m_partType == pt_slaveData ? "true" : "false") << ", "; + if (m_partType != pt_any) { + *output << ", \"slave\": " << (m_partType == pt_slaveData ? "true" : "false"); + } + *output << ", "; } else { *output << FIELD_SEPARATOR; if (m_partType == pt_masterData) { @@ -492,7 +605,7 @@ void SingleDataField::dumpPrefix(bool prependFieldSeparator, OutputFormat output } *output << FIELD_SEPARATOR; } - m_dataType->dump(outputFormat, m_length, true, output); + m_dataType->dump(outputFormat, m_length, ad_normal, output); } void SingleDataField::dumpSuffix(OutputFormat outputFormat, ostream* output) const { @@ -545,7 +658,8 @@ result_t SingleDataField::read(const SymbolString& data, size_t offset, return RESULT_EMPTY; } bool shortFormat = outputFormat & OF_SHORT; - if (outputFormat & OF_JSON) { + bool isJson = outputFormat & OF_JSON; + if (isJson) { if (leadingSeparator) { *output << ","; } @@ -576,6 +690,9 @@ result_t SingleDataField::read(const SymbolString& data, size_t offset, } } + if (!shortFormat && (outputFormat & OF_RAWDATA) && !isJson) { + addRaw(offset, m_length, data, isJson, output); + } result_t result = readSymbols(data, offset, outputFormat, output); if (result != RESULT_OK) { return result; @@ -583,7 +700,10 @@ result_t SingleDataField::read(const SymbolString& data, size_t offset, if (!shortFormat) { appendAttributes(outputFormat, output); } - if (!shortFormat && (outputFormat & OF_JSON)) { + if (!shortFormat && isJson) { + if (outputFormat & OF_RAWDATA) { + addRaw(offset, m_length, data, isJson, output); + } *output << "}"; } return RESULT_OK; @@ -710,8 +830,11 @@ result_t ValueListDataField::derive(const string& name, PartType partType, int d } const NumberDataType* num = reinterpret_cast(m_dataType); if (!values.empty()) { - if (values.begin()->first < num->getMinValue() || values.rbegin()->first > num->getMaxValue()) { - return RESULT_ERR_INVALID_ARG; // cannot use divisor != 1 for value list field + for (auto& it : values) { + result_t ret = num->checkValueRange(it.first); + if (ret != RESULT_OK) { + return RESULT_ERR_INVALID_ARG; + } } fields->push_back(new ValueListDataField(useName, *attributes, num, partType, m_length, values)); @@ -793,9 +916,9 @@ result_t ValueListDataField::writeSymbols(size_t offset, istringstream* input, return numType->writeRawValue(numType->getReplacement(), offset, m_length, output, usedLength); } - for (map::const_iterator it = m_values.begin(); it != m_values.end(); ++it) { - if (it->second == inputStr) { - return numType->writeRawValue(it->first, offset, m_length, output, usedLength); + for (const auto& it : m_values) { + if (it.second == inputStr) { + return numType->writeRawValue(it.first, offset, m_length, output, usedLength); } } const char* str = inputStr.c_str(); @@ -842,7 +965,7 @@ void ConstantDataField::dump(bool prependFieldSeparator, OutputFormat outputForm dumpPrefix(prependFieldSeparator, outputFormat, output); // no divisor appended since it is not allowed for ConstantDataField if (outputFormat & OF_JSON) { - appendJson(false, "value", m_value, true, output); + appendJson(true, "value", m_value, true, output); *output << ", \"verify\": " << (m_verify ? "true" : "false"); } else { *output << (m_verify?"==":"=") << m_value; @@ -857,13 +980,16 @@ result_t ConstantDataField::readSymbols(const SymbolString& input, size_t offset if (result != RESULT_OK) { return result; } + string value = coutput.str(); + FileReader::trim(&value); if (m_verify) { - string value = coutput.str(); - FileReader::trim(&value); if (value != m_value) { return RESULT_ERR_OUT_OF_RANGE; } } + if (outputFormat & OF_JSON) { + *output << value; + } return RESULT_OK; } @@ -1249,7 +1375,7 @@ result_t LoadableDataFieldSet::getFieldMap(const string& preferLanguage, vector< result_t LoadableDataFieldSet::addFromFile(const string& filename, unsigned int lineNo, map* row, vector< map >* subRows, string* errorDescription, bool replace) { const DataField* field = nullptr; - result_t result = DataField::create(false, false, false, MAX_POS, m_templates, subRows, errorDescription, &field); + result_t result = DataField::create(m_isWrite, false, false, MAX_POS, m_templates, subRows, errorDescription, &field); if (result != RESULT_OK) { return result; } @@ -1459,4 +1585,27 @@ const DataField* DataFieldTemplates::get(const string& name) const { return ref->second; } +bool DataFieldTemplates::dump(OutputFormat outputFormat, ostream* output) const { + bool prependFieldSeparator = false; + for (const auto &it : m_fieldsByName) { + const DataField *dataField = it.second; + if (outputFormat & OF_JSON) { + if (dataField->isSet()) { + if (prependFieldSeparator) { + *output << ",\n"; + } + *output << "{\"name\":\"" << dataField->getName(-1) << "\", \"sequence\": ["; + dataField->dump(false, outputFormat, output); + *output << "]}"; + } else { + dataField->dump(prependFieldSeparator, outputFormat, output); + } + } else { + dataField->dump(prependFieldSeparator, outputFormat, output); + } + prependFieldSeparator = true; + } + return !prependFieldSeparator; +} + } // namespace ebusd diff --git a/src/lib/ebus/data.h b/src/lib/ebus/data.h index d154febfc..3cfbc175f 100755 --- a/src/lib/ebus/data.h +++ b/src/lib/ebus/data.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -215,6 +215,22 @@ class DataField : public AttributedItem { */ virtual bool isList() const { return false; } + /** + * Check if the given name is a valid identifier. + * @param name the name to check (and optionally normalize). + * @param allowFirstDigit whether to additionally allow the name to start with a digit. + * @param normalize whether to replace invalid characters with an underscore. + * @return true if the name is valid (or was normalized), false if invalid. + */ + static bool checkIdentifier(const string& name, bool allowFirstDigit = false); + + /** + * Normalize the given name to be a valid identifier. + * @param name the name to check and normalize. + * @param allowFirstDigit whether to additionally allow the name to start with a digit. + */ + static void normalizeIdentifier(string& name, bool allowFirstDigit = false); + /** * Factory method for creating new instances. * @param isWriteMessage whether the field is part of a write message (default false). @@ -239,6 +255,17 @@ class DataField : public AttributedItem { */ static const char* getDayName(int day); + /** + * Add raw data to the output (excluding the length field). + * @param offset the offset in the data part of the @a SymbolString. + * @param length the maximum number of symbols to dump. + * @param input the @a SymbolString to dump from. + * @param isJson true for JSON format, false for text. + * @param output the ostream to append the raw data to. + * @return true when something was added to the output. + */ + static bool addRaw(size_t offset, size_t length, const SymbolString& input, bool isJson, ostream* output); + /** * Returns the length of this field (or contained fields) in bytes. * @param partType the message part of the contained fields to limit the length calculation to. @@ -551,6 +578,10 @@ class ValueListDataField : public SingleDataField { // @copydoc void dump(bool prependFieldSeparator, OutputFormat outputFormat, ostream* output) const override; + /** + * @return the value=text assignments. + */ + const map& getList() const { return m_values; } protected: // @copydoc @@ -761,10 +792,11 @@ class LoadableDataFieldSet : public DataFieldSet, public MappedFileReader { * Constructs a new instance. * @param name the field name. * @param templates the @a DataFieldTemplates instance to use. + * @param isWrite true for a write message, false for read. */ - LoadableDataFieldSet(const string& name, DataFieldTemplates* templates) - : DataFieldSet(name, vector()), MappedFileReader(false), m_templates(templates) { - } + LoadableDataFieldSet(const string& name, DataFieldTemplates* templates, bool isWrite) + : DataFieldSet(name, vector()), MappedFileReader(false), m_templates(templates), + m_isWrite(isWrite) {} // @copydoc result_t getFieldMap(const string& preferLanguage, vector* row, string* errorDescription) const override; @@ -776,6 +808,9 @@ class LoadableDataFieldSet : public DataFieldSet, public MappedFileReader { private: /** the @a DataFieldTemplates instance to use. */ DataFieldTemplates* m_templates; + + /** true for a write message, false for read. */ + bool m_isWrite; }; @@ -832,6 +867,14 @@ class DataFieldTemplates : public MappedFileReader { */ const DataField* get(const string& name) const; + /** + * Dump the templates to the output. + * @param outputFormat the @a OutputFormat options. + * @param output the @a ostream to dump to. + * @return true when a template was written to the output. + */ + bool dump(OutputFormat outputFormat, ostream* output) const; + private: /** the known template @a DataField instances by name. */ diff --git a/src/lib/ebus/datatype.cpp b/src/lib/ebus/datatype.cpp index 86146348e..c62f3c6b0 100755 --- a/src/lib/ebus/datatype.cpp +++ b/src/lib/ebus/datatype.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,13 +34,11 @@ namespace ebusd { using std::dec; -using std::defaultfloat; using std::hex; using std::fixed; using std::setfill; using std::setprecision; using std::setw; -using std::endl; using std::isfinite; @@ -97,8 +95,36 @@ uint32_t floatToUint(float val) { #endif } +float uint16ToFloat(uint16_t val) { + if (val == 0) { + return 0; + } + if (val == 0x7fff) { + return NAN; + } + bool negative = val&0x8000; + int exp = (val>>11)&0xf; + int sig = val&0x7ff; + return static_cast((negative ? sig-0x800 : sig) * exp2(exp) * 0.01); +} -bool DataType::dump(OutputFormat outputFormat, size_t length, bool appendDivisor, ostream* output) const { +uint16_t floatToUint16(float value) { + // (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent + if (value == 0) { + return 0; + } + bool negative = value < 0; + double val = round(value*(negative ? -100.0 : 100.0)); + int exp = ilogb(val)-10; + if (exp < -10 || exp > 15) { + return 0x7fff; // invalid value DPT 9 + } + auto shift = exp > 0 ? exp : 0; + auto sig = static_cast(val * exp2(-shift)); + return static_cast((shift << 11) | (negative ? 0x8000 | (0x800-sig) : sig)); +} + +bool DataType::dump(OutputFormat outputFormat, size_t length, AppendDivisor appendDivisor, ostream* output) const { if (outputFormat & OF_JSON) { *output << "\"type\": \"" << m_id << "\", \"isbits\": " << (getBitCount() < 8 ? "true" : "false"); @@ -123,7 +149,7 @@ bool DataType::dump(OutputFormat outputFormat, size_t length, bool appendDivisor *output << static_cast(length); } } - if (appendDivisor) { + if (appendDivisor != ad_none) { *output << FIELD_SEPARATOR; } } @@ -131,7 +157,8 @@ bool DataType::dump(OutputFormat outputFormat, size_t length, bool appendDivisor } -bool StringDataType::dump(OutputFormat outputFormat, size_t length, bool appendDivisor, ostream* output) const { +bool StringDataType::dump(OutputFormat outputFormat, size_t length, AppendDivisor appendDivisor, ostream* output +) const { DataType::dump(outputFormat, length, appendDivisor, output); if ((outputFormat & OF_JSON) && (outputFormat & OF_ALL_ATTRS)) { *output << ", \"result\": \"" << (isIgnored() ? "void" : "string") << "\""; @@ -274,7 +301,8 @@ result_t StringDataType::writeSymbols(size_t offset, size_t length, istringstrea } -bool DateTimeDataType::dump(OutputFormat outputFormat, size_t length, bool appendDivisor, ostream* output) const { +bool DateTimeDataType::dump(OutputFormat outputFormat, size_t length, AppendDivisor appendDivisor, ostream* output +) const { DataType::dump(outputFormat, length, appendDivisor, output); if ((outputFormat & OF_JSON) && (outputFormat & OF_ALL_ATTRS)) { *output << ", \"result\": \"" << (hasDate() ? hasTime() ? "datetime" : "date" : "time") << "\""; @@ -319,11 +347,11 @@ result_t DateTimeDataType::readSymbols(size_t offset, size_t length, const Symbo } switch (type) { case 2: // date only - if (!hasFlag(REQ) && symbol == m_replacement) { + if (!hasFlag(REQ) && (symbol == m_replacement || (!hasFlag(REZ) && symbol == 0))) { if (i + 1 != length) { *output << NULL_VALUE << "."; break; - } else if (last == m_replacement) { + } else if (last == m_replacement || (!hasFlag(REZ) && last == 0)) { if (length == 2) { // number of days since 01.01.1900 *output << NULL_VALUE << "."; } @@ -565,7 +593,8 @@ result_t DateTimeDataType::writeSymbols(size_t offset, size_t length, istringstr if (result != RESULT_OK) { return result; // invalid time part } - if ((i == 0 && value > 24) || (i > 0 && (last == 24 && value > 0) )) { + if ((i == (m_hasDate ? 2 : 0) && value > 24) + || (i > (m_hasDate ? 2 : 0) && (last == 24 && value > 0) )) { return RESULT_ERR_OUT_OF_RANGE; // invalid time part } if (hasFlag(SPE)) { // minutes since midnight @@ -645,7 +674,8 @@ size_t NumberDataType::calcPrecision(int divisor) { return precision; } -bool NumberDataType::dump(OutputFormat outputFormat, size_t length, bool appendDivisor, ostream* output) const { +bool NumberDataType::dump(OutputFormat outputFormat, size_t length, AppendDivisor appendDivisor, ostream* output +) const { if (m_bitCount < 8) { DataType::dump(outputFormat, m_bitCount, appendDivisor, output); } else { @@ -654,11 +684,17 @@ bool NumberDataType::dump(OutputFormat outputFormat, size_t length, bool appendD if ((outputFormat & OF_JSON) && (outputFormat & OF_ALL_ATTRS)) { *output << ", \"result\": \"number\""; } - if (!appendDivisor) { + if (appendDivisor == ad_none) { return false; } bool ret = false; - if (m_baseType) { + if (appendDivisor == ad_full && m_divisor != 1) { + if (outputFormat & OF_JSON) { + *output << ", \"divisor\": "; + } + *output << m_divisor; + ret = true; + } else if (m_baseType) { if (m_baseType->m_divisor != m_divisor) { if (outputFormat & OF_JSON) { *output << ", \"divisor\": "; @@ -666,15 +702,17 @@ bool NumberDataType::dump(OutputFormat outputFormat, size_t length, bool appendD *output << (m_divisor / m_baseType->m_divisor); ret = true; } - } else if (m_divisor != 1) { - if (outputFormat & OF_JSON) { - *output << ", \"divisor\": "; - } - *output << m_divisor; - ret = true; } - if (ret && (outputFormat & OF_JSON) && (outputFormat & OF_ALL_ATTRS)) { - *output << ", \"precision\": " << static_cast(getPrecision()); + if ((outputFormat & OF_JSON) && (outputFormat & OF_ALL_ATTRS)) { + if (ret) { + *output << ", \"precision\": " << static_cast(getPrecision()); + } + *output << ", \"min\": "; + getMinMax(false, OF_JSON, output); + *output << ", \"max\": "; + getMinMax(true, OF_JSON, output); + *output << ", \"step\": "; + getStep(OF_JSON, output); } return ret; } @@ -720,19 +758,119 @@ result_t NumberDataType::derive(int divisor, size_t bitCount, const NumberDataTy } else { return RESULT_ERR_INVALID_ARG; } + ostringstream str; + str << m_id << ',' << static_cast(bitCount) << ',' << static_cast(divisor); + string key = str.str(); + *derived = static_cast(DataTypeList::getInstance()->get(key)); + if (*derived == nullptr) { + if (m_bitCount < 8) { + *derived = new NumberDataType(m_id, bitCount, m_flags, m_replacement, + m_firstBit, divisor, m_baseType ? m_baseType : this); + } else { + *derived = new NumberDataType(m_id, bitCount, m_flags, m_replacement, + m_minValue, m_maxValue, divisor, m_baseType ? m_baseType : this); + } + DataTypeList::getInstance()->add(*derived, key); + } + return RESULT_OK; +} + +result_t NumberDataType::derive(unsigned int min, unsigned int max, unsigned int inc, const NumberDataType** derived) +const { if (m_bitCount < 8) { - *derived = new NumberDataType(m_id, bitCount, m_flags, m_replacement, - m_firstBit, divisor, m_baseType ? m_baseType : this); - } else { - *derived = new NumberDataType(m_id, bitCount, m_flags, m_replacement, - m_minValue, m_maxValue, divisor, m_baseType ? m_baseType : this); + return RESULT_ERR_INVALID_ARG; + } + if (min == m_minValue && max == m_maxValue && (inc == 0 || inc == m_incValue)) { + *derived = this; + return RESULT_OK; + } + if (checkValueRange(min) != RESULT_OK || checkValueRange(max) != RESULT_OK) { + return RESULT_ERR_OUT_OF_RANGE; + } + ostringstream str; + str << m_id << ',' << static_cast(m_bitCount) << ',' << static_cast(m_divisor) + << ',' << static_cast(min)<< ',' << static_cast(max)<< ',' << static_cast(inc); + string key = str.str(); + *derived = static_cast(DataTypeList::getInstance()->get(key)); + if (*derived == nullptr) { + *derived = new NumberDataType(m_id, m_bitCount, m_flags, m_replacement, + min, max, inc, m_divisor, m_baseType ? m_baseType : this); + DataTypeList::getInstance()->add(*derived, key); } - DataTypeList::getInstance()->addCleanup(*derived); return RESULT_OK; } result_t NumberDataType::getMinMax(bool getMax, const OutputFormat outputFormat, ostream* output) const { - return readFromRawValue(getMax ? m_maxValue : m_minValue, outputFormat, output); + return readFromRawValue(getMax ? m_maxValue : m_minValue, outputFormat, output, true); +} + +result_t NumberDataType::getStep(const OutputFormat outputFormat, ostream* output) const { + return readFromRawValue(m_incValue ? m_incValue : hasFlag(EXP) ? floatToUint(1.0f) : 1, outputFormat, output, true); +} + +result_t NumberDataType::checkValueRange(unsigned int value, bool* pnegative) const { + bool negative; + if (hasFlag(SIG)) { // signed value + unsigned int negBit = 1 << (m_bitCount - 1); + negative = (value & negBit) != 0; + if (hasFlag(EXP)) { + float fval = uintToFloat(value, negative); + if (!isfinite(fval)) { + return RESULT_EMPTY; + } + float cval = uintToFloat(m_minValue, (m_minValue & negBit) != 0); + if (!isfinite(cval)) { + return RESULT_EMPTY; + } + if (fval < cval) { + return RESULT_ERR_OUT_OF_RANGE; + } + cval = uintToFloat(m_maxValue, (m_maxValue & negBit) != 0); + if (!isfinite(cval)) { + return RESULT_EMPTY; + } + if (fval > cval) { + return RESULT_ERR_OUT_OF_RANGE; + } + } else { + if (m_minValue & negBit) { + // negative min + if (negative && value < m_minValue) { + // e.g. SCH val=0xfc=-4 min=0xff=-1 + return RESULT_ERR_OUT_OF_RANGE; + } + } else { + // positive min + if (negative || value < m_minValue) { + // e.g. SCH val=0xfc=-4 min=0x01=+1 + // e.g. SCH val=0x00=0 min=0x01=+1 + return RESULT_ERR_OUT_OF_RANGE; + } + } + if (m_maxValue & negBit) { + // negative max + if (!negative || value > m_maxValue) { + // e.g. SCH val=0x00=0 max=0xff=-1 + // e.g. SCH val=0xff=-1 max=0xfe=-2 + return RESULT_ERR_OUT_OF_RANGE; + } + } else { + // positive max + if (!negative && value > m_maxValue) { + // e.g. SCH val=0x04=+4 max=0x01=+1 + return RESULT_ERR_OUT_OF_RANGE; + } + } + } + } else if (value < m_minValue || value > m_maxValue) { + return RESULT_ERR_OUT_OF_RANGE; + } else { + negative = false; + } + if (pnegative) { + *pnegative = negative; + } + return RESULT_OK; } result_t NumberDataType::readRawValue(size_t offset, size_t length, const SymbolString& input, @@ -799,22 +937,10 @@ result_t NumberDataType::getFloatFromRawValue(unsigned int value, float* output) return RESULT_EMPTY; } - bool negative; - if (hasFlag(SIG)) { // signed value - negative = (value & (1 << (m_bitCount - 1))) != 0; - if (!hasFlag(EXP)) { - 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; + bool negative = false; + result_t ret = checkValueRange(value, &negative); + if (ret != RESULT_OK) { + return ret; } int signedValue; if (m_bitCount == 32) { @@ -834,7 +960,7 @@ result_t NumberDataType::getFloatFromRawValue(unsigned int value, float* output) val /= static_cast(m_divisor); } } - *output = static_cast(val); + *output = val; return RESULT_OK; } if (!negative) { @@ -864,7 +990,7 @@ result_t NumberDataType::getFloatFromRawValue(unsigned int value, float* output) } result_t NumberDataType::readFromRawValue(unsigned int value, - OutputFormat outputFormat, ostream* output) const { + OutputFormat outputFormat, ostream* output, bool skipRangeCheck) const { size_t length = (m_bitCount < 8) ? 1 : (m_bitCount/8); // initialize output *output << setw(0) << std::resetiosflags(output->flags()) << dec << std::skipws << setprecision(6); @@ -878,22 +1004,10 @@ result_t NumberDataType::readFromRawValue(unsigned int value, return RESULT_OK; } - bool negative; - if (hasFlag(SIG)) { // signed value - negative = (value & (1 << (m_bitCount - 1))) != 0; - if (!hasFlag(EXP)) { - 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; + bool negative = false; + result_t ret = checkValueRange(value, &negative); + if (!skipRangeCheck && ret != RESULT_OK) { + return ret; } int signedValue; if (m_bitCount == 32) { @@ -1024,7 +1138,7 @@ result_t NumberDataType::getRawValueFromFloat(float val, unsigned int* output) c } else { if (m_divisor == 1) { if (hasFlag(SIG)) { - long signedValue = static_cast(val); // TODO static_c? + long signedValue = static_cast(val); if (signedValue < 0 && m_bitCount != 32) { value = (unsigned int)(signedValue + (1 << m_bitCount)); } else { @@ -1060,106 +1174,110 @@ result_t NumberDataType::getRawValueFromFloat(float val, unsigned int* output) c 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 - } + } + result_t ret = checkValueRange(value); + if (ret != RESULT_OK) { + return ret; } *output = value; return RESULT_OK; } -result_t NumberDataType::writeSymbols(size_t offset, size_t length, istringstream* input, - SymbolString* output, size_t* usedLength) const { +result_t NumberDataType::parseInput(const string inputStr, unsigned int* parsedValue) const { unsigned int value; - const string inputStr = input->str(); if (!hasFlag(REQ) && (isIgnored() || inputStr == NULL_VALUE)) { value = m_replacement; // replacement value } else if (inputStr.empty()) { return RESULT_ERR_EOF; // input too short - } else if (hasFlag(EXP)) { // IEEE 754 binary32 - const char* str = inputStr.c_str(); - char* strEnd = nullptr; - double dvalue = strtod(str, &strEnd); - if (strEnd == nullptr || strEnd == str || *strEnd != 0) { - return RESULT_ERR_INVALID_NUM; // invalid value - } - if (m_divisor < 0) { - dvalue /= -m_divisor; - } else if (m_divisor > 1) { - dvalue *= m_divisor; - } - value = floatToUint(static_cast(dvalue)); - if (value == 0xffffffff) { - return RESULT_ERR_INVALID_NUM; - } } else { - const char* str = inputStr.c_str(); - char* strEnd = nullptr; - if (m_divisor == 1) { - if (hasFlag(SIG)) { - long signedValue = strtol(str, &strEnd, 10); - if (signedValue < 0 && m_bitCount != 32) { - value = (unsigned int)(signedValue + (1 << m_bitCount)); - } else { - value = (unsigned int)signedValue; - } - } else { - value = (unsigned int)strtoul(str, &strEnd, 10); - } - if (strEnd == nullptr || strEnd == str || (*strEnd != 0 && *strEnd != '.')) { - return RESULT_ERR_INVALID_NUM; // invalid value - } - } else { + if (hasFlag(EXP)) { // IEEE 754 binary32 + const char* str = inputStr.c_str(); + char* strEnd = nullptr; double dvalue = strtod(str, &strEnd); - if (strEnd == nullptr || strEnd == str || *strEnd != 0) { + if (errno == ERANGE || strEnd == nullptr || strEnd == str || *strEnd != 0) { return RESULT_ERR_INVALID_NUM; // invalid value } if (m_divisor < 0) { - dvalue = round(dvalue / -m_divisor); - } else { - dvalue = round(dvalue * m_divisor); + dvalue /= -m_divisor; + } else if (m_divisor > 1) { + dvalue *= m_divisor; } - if (hasFlag(SIG)) { - if (dvalue < -exp2((8 * static_cast(length)) - 1) - || dvalue >= exp2((8 * static_cast(length)) - 1)) { - return RESULT_ERR_OUT_OF_RANGE; // value out of range - } - if (dvalue < 0 && m_bitCount != 32) { - value = static_cast(dvalue + (1 << m_bitCount)); + value = floatToUint(static_cast(dvalue)); + if (value == 0xffffffff) { + return RESULT_ERR_INVALID_NUM; + } + } else { + const char* str = inputStr.c_str(); + char* strEnd = nullptr; + if (m_divisor == 1) { + if (hasFlag(SIG)) { + long signedValue = strtol(str, &strEnd, 0); + if (errno == ERANGE + || (m_bitCount != 32 && (signedValue < 0L ? (signedValue < -(1L << (m_bitCount - 1))) + : (signedValue >= (1L << (m_bitCount - 1))) + ))) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + if (signedValue < 0 && m_bitCount != 32) { + value = (unsigned int)(signedValue + (1L << m_bitCount)); + } else { + value = (unsigned int)signedValue; + } } else { - value = static_cast(dvalue); + value = (unsigned int)strtoul(str, &strEnd, 0); + if (errno == ERANGE || (m_bitCount != 32 && value >= (1U << m_bitCount))) { + return RESULT_ERR_OUT_OF_RANGE; + } + } + if (strEnd == nullptr || strEnd == str || (*strEnd != 0 && *strEnd != '.')) { + return RESULT_ERR_INVALID_NUM; // invalid value } } else { - if (dvalue < 0.0 || dvalue >= exp2(8 * static_cast(length))) { - return RESULT_ERR_OUT_OF_RANGE; // value out of range + double dvalue = strtod(str, &strEnd); + if (errno == ERANGE || strEnd == nullptr || strEnd == str || *strEnd != 0) { + return RESULT_ERR_INVALID_NUM; // invalid value } - 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 + if (m_divisor < 0) { + dvalue = round(dvalue / -m_divisor); + } else { + dvalue = round(dvalue * m_divisor); + } + if (hasFlag(SIG)) { + double max = exp2(m_bitCount - 1); + if (dvalue < 0.0 ? (dvalue < -max) : (dvalue >= max)) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + if (dvalue < 0.0 && m_bitCount != 32) { + value = static_cast(dvalue + (1 << m_bitCount)); + } else { + value = static_cast(dvalue); + } + } else { + if (dvalue < 0.0 || dvalue >= exp2(m_bitCount)) { + return RESULT_ERR_OUT_OF_RANGE; // value out of range + } + value = (unsigned int)dvalue; } - } 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 + } + result_t ret = checkValueRange(value); + if (ret != RESULT_OK) { + return ret; } } + *parsedValue = 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; + const string inputStr = input->str(); + result_t ret = parseInput(inputStr, &value); + if (ret != RESULT_OK) { + return ret; + } return writeRawValue(value, offset, length, output, usedLength); } @@ -1176,6 +1294,7 @@ DataTypeList::DataTypeList() { // unsigned decimal in BCD, 0000 - 9999 (fixed length) add(new NumberDataType("PIN", 16, FIX|BCD|REV, 0xffff, 0, 0x9999, 1)); add(new NumberDataType("UCH", 8, 0, 0xff, 0, 0xfe, 1)); // unsigned integer, 0 - 254 + add(new NumberDataType("U1L", 8, REQ, 0, 0, 0xff, 1)); // unsigned 1-byte, 0 - 255 (no replacement) add(new StringDataType("IGN", MAX_LEN*8, IGN|ADJ, 0)); // >= 1 byte ignored data // >= 1 byte character string filled up with 0x00 (null terminated string) add(new StringDataType("NTS", MAX_LEN*8, ADJ, 0)); @@ -1197,7 +1316,7 @@ DataTypeList::DataTypeList() { // date, 01.01.2000 - 31.12.2099 (0x01,0x01,0x00 - 0x1f,0x0c,0x63, replacement 0xff) add(new DateTimeDataType("HDA:3", 24, 0, 0xff, true, false, 0)); // date, days since 01.01.1900, 01.01.1900 - 06.06.2079 (0x00,0x00 - 0xff,0xff) - add(new DateTimeDataType("DAY", 16, 0, 0xff, true, false, 0)); + add(new DateTimeDataType("DAY", 16, REZ, 0xff, true, false, 0)); // date+time in minutes since 01.01.2009, 01.01.2009 - 31.12.2099 (0x00,0x00,0x00,0x00 - 0x02,0xda,0x4e,0x1f) add(new DateTimeDataType("DTM", 32, REQ, 0x100, true, true, 0)); // time in BCD, 00:00:00 - 23:59:59 (0x00,0x00,0x00 - 0x59,0x59,0x23) @@ -1233,6 +1352,7 @@ DataTypeList::DataTypeList() { add(new NumberDataType("HCD:2", 16, HCD|BCD|REQ, 0, 0, 9999, 1)); // unsigned decimal in HCD, 0 - 9999 add(new NumberDataType("HCD:3", 24, HCD|BCD|REQ, 0, 0, 999999, 1)); // unsigned decimal in HCD, 0 - 999999 add(new NumberDataType("SCH", 8, SIG, 0x80, 0x81, 0x7f, 1)); // signed integer, -127 - +127 + add(new NumberDataType("S1L", 8, SIG|REQ, 0, 0x80, 0x7f, 1)); // signed integer, -128 - +127 (no replacement) add(new NumberDataType("D1B", 8, SIG, 0x80, 0x81, 0x7f, 1)); // signed integer, -127 - +127 // unsigned number (fraction 1/2), 0 - 100 (0x00 - 0xc8, replacement 0xff) add(new NumberDataType("D1C", 8, 0, 0xff, 0x00, 0xc8, 2)); @@ -1245,33 +1365,57 @@ DataTypeList::DataTypeList() { // signed number (fraction 1/1000), -32.767 - +32.767, big endian add(new NumberDataType("FLR", 16, SIG|REV, 0x8000, 0x8001, 0x7fff, 1000)); // signed number (IEEE 754 binary32: 1 bit sign, 8 bits exponent, 23 bits significand), little endian - add(new NumberDataType("EXP", 32, SIG|EXP, 0x7f800000, 0xfeffffff, 0x7effffff, 1)); + add(new NumberDataType("EXP", 32, SIG|EXP, 0x7fc00000, 0xfeffffff, 0x7effffff, 1)); // signed number (IEEE 754 binary32: 1 bit sign, 8 bits exponent, 23 bits significand), big endian - add(new NumberDataType("EXR", 32, SIG|EXP|REV, 0x7f800000, 0xfeffffff, 0x7effffff, 1)); + add(new NumberDataType("EXR", 32, SIG|EXP|REV, 0x7fc00000, 0xfeffffff, 0x7effffff, 1)); // unsigned integer, 0 - 65534, little endian add(new NumberDataType("UIN", 16, 0, 0xffff, 0, 0xfffe, 1)); // unsigned integer, 0 - 65534, big endian add(new NumberDataType("UIR", 16, REV, 0xffff, 0, 0xfffe, 1)); + // unsigned integer, 0 - 65535, little endian (no replacement) + add(new NumberDataType("U2L", 16, REQ, 0, 0, 0xffff, 1)); + // unsigned integer, 0 - 65535, big endian (no replacement) + add(new NumberDataType("U2B", 16, REQ|REV, 0, 0, 0xffff, 1)); // signed integer, -32767 - +32767, little endian add(new NumberDataType("SIN", 16, SIG, 0x8000, 0x8001, 0x7fff, 1)); // signed integer, -32767 - +32767, big endian add(new NumberDataType("SIR", 16, SIG|REV, 0x8000, 0x8001, 0x7fff, 1)); + // signed integer, -32768 - +32767, little endian (no replacement) + add(new NumberDataType("S2L", 16, SIG|REQ, 0, 0x8000, 0x7fff, 1)); + // signed integer, -32768 - +32767, big endian (no replacement) + add(new NumberDataType("S2B", 16, SIG|REQ|REV, 0, 0x8000, 0x7fff, 1)); // unsigned 3 bytes int, 0 - 16777214, little endian add(new NumberDataType("U3N", 24, 0, 0xffffff, 0, 0xfffffe, 1)); // unsigned 3 bytes int, 0 - 16777214, big endian add(new NumberDataType("U3R", 24, REV, 0xffffff, 0, 0xfffffe, 1)); + // unsigned 3 bytes int, 0 - 16777215, little endian (no replacement) + add(new NumberDataType("U3L", 24, REQ, 0, 0, 0xffffff, 1)); + // unsigned 3 bytes int, 0 - 16777215, big endian (no replacement) + add(new NumberDataType("U3B", 24, REQ|REV, 0, 0, 0xffffff, 1)); // signed 3 bytes int, -8388607 - +8388607, little endian - add(new NumberDataType("S3N", 24, SIG, 0x800000, 0x800001, 0xffffff, 1)); + add(new NumberDataType("S3N", 24, SIG, 0x800000, 0x800001, 0x7fffff, 1)); // signed 3 bytes int, -8388607 - +8388607, big endian - add(new NumberDataType("S3R", 24, SIG|REV, 0x800000, 0x800001, 0xffffff, 1)); + add(new NumberDataType("S3R", 24, SIG|REV, 0x800000, 0x800001, 0x7fffff, 1)); + // signed 3 bytes int, -8388608 - +8388607, little endian (no replacement) + add(new NumberDataType("S3L", 24, SIG|REQ, 0, 0x800000, 0x7fffff, 1)); + // signed 3 bytes int, -8388608 - +8388607, big endian (no replacement) + add(new NumberDataType("S3B", 24, SIG|REQ|REV, 0, 0x800000, 0x7fffff, 1)); // unsigned integer, 0 - 4294967294, little endian add(new NumberDataType("ULG", 32, 0, 0xffffffff, 0, 0xfffffffe, 1)); // unsigned integer, 0 - 4294967294, big endian add(new NumberDataType("ULR", 32, REV, 0xffffffff, 0, 0xfffffffe, 1)); + // unsigned integer, 0 - 4294967295, little endian (no replacement) + add(new NumberDataType("U4L", 32, REQ, 0, 0, 0xffffffff, 1)); + // unsigned integer, 0 - 4294967295, big endian (no replacement) + add(new NumberDataType("U4B", 32, REQ|REV, 0, 0, 0xffffffff, 1)); // signed integer, -2147483647 - +2147483647, little endian - add(new NumberDataType("SLG", 32, SIG, 0x80000000, 0x80000001, 0xffffffff, 1)); + add(new NumberDataType("SLG", 32, SIG, 0x80000000, 0x80000001, 0x7fffffff, 1)); // signed integer, -2147483647 - +2147483647, big endian - add(new NumberDataType("SLR", 32, SIG|REV, 0x80000000, 0x80000001, 0xffffffff, 1)); + add(new NumberDataType("SLR", 32, SIG|REV, 0x80000000, 0x80000001, 0x7fffffff, 1)); + // signed integer, -2147483648 - +2147483647, little endian (no replacement) + add(new NumberDataType("S4L", 32, SIG|REQ, 0, 0x80000000, 0x7fffffff, 1)); + // signed integer, -2147483648 - +2147483647, big endian (no replacement) + add(new NumberDataType("S4B", 32, SIG|REQ|REV, 0, 0x80000000, 0x7fffffff, 1)); add(new NumberDataType("BI0", 7, ADJ|REQ, 0, 0, 1)); // bit 0 (up to 7 bits until bit 6) add(new NumberDataType("BI1", 7, ADJ|REQ, 0, 1, 1)); // bit 1 (up to 7 bits until bit 7) add(new NumberDataType("BI2", 6, ADJ|REQ, 0, 2, 1)); // bit 2 (up to 6 bits until bit 7) @@ -1286,7 +1430,7 @@ DataTypeList* DataTypeList::getInstance() { return &s_instance; } -void DataTypeList::dump(OutputFormat outputFormat, bool appendDivisor, ostream* output) const { +void DataTypeList::dump(OutputFormat outputFormat, ostream* output) const { bool json = outputFormat & OF_JSON; string sep = "\n"; for (const auto &it : m_typesById) { @@ -1298,9 +1442,9 @@ void DataTypeList::dump(OutputFormat outputFormat, bool appendDivisor, ostream* *output << sep << " {"; } if ((dataType->getBitCount() % 8) != 0) { - dataType->dump(outputFormat, dataType->getBitCount(), appendDivisor, output); + dataType->dump(outputFormat, dataType->getBitCount(), ad_full, output); } else { - dataType->dump(outputFormat, dataType->getBitCount() / 8, appendDivisor, output); + dataType->dump(outputFormat, dataType->getBitCount() / 8, ad_full, output); } if (json) { *output << "}"; @@ -1319,12 +1463,15 @@ void DataTypeList::clear() { m_typesById.clear(); } -result_t DataTypeList::add(const DataType* dataType) { - if (m_typesById.find(dataType->getId()) != m_typesById.end()) { +result_t DataTypeList::add(const DataType* dataType, const string derivedKey) { + string key = derivedKey.empty() ? dataType->getId() : derivedKey; + if (m_typesById.find(key) != m_typesById.end()) { return RESULT_ERR_DUPLICATE_NAME; // duplicate key } - m_typesById[dataType->getId()] = dataType; - m_cleanupTypes.push_back(dataType); + m_typesById[key] = dataType; + if (std::find(m_cleanupTypes.begin(), m_cleanupTypes.end(), dataType) == m_cleanupTypes.end()) { + m_cleanupTypes.push_back(dataType); + } return RESULT_OK; } diff --git a/src/lib/ebus/datatype.h b/src/lib/ebus/datatype.h index 388a4126d..6d6c97df0 100755 --- a/src/lib/ebus/datatype.h +++ b/src/lib/ebus/datatype.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -110,6 +110,9 @@ enum OutputFormat : OutputFormatBaseType { /** bit flag for @a OutputFormat: include message/field definition. */ OF_DEFINITION = 1 << 8, + + /** bit flag for @a OutputFormat: include raw data. */ + OF_RAWDATA = 1 << 9, }; constexpr inline enum OutputFormat operator| (enum OutputFormat self, enum OutputFormat other) { @@ -134,6 +137,13 @@ constexpr inline enum OutputFormat operator~ (enum OutputFormat self) { return (enum OutputFormat)(~(OutputFormatBaseType)self); } +/** whether divisor should be appended to a dump. */ +enum AppendDivisor { + ad_none, //!< no dump of divisor + ad_normal, //!< regular dump of divisor (i.e. not for base types) + ad_full, //!< full dump of divisor (i.e. also for base types) +}; + /** the message part in which a data field is stored. */ enum PartType { pt_any, //!< stored in any data (master or slave) @@ -184,6 +194,9 @@ enum PartType { /** bit flag for @a DataType: stored duplicate for backwards compatibility, not to be traversed in lists any more. */ #define DUP 0x2000 +/** bit flag for @a DataType: special marker for non-tolerated secondary replacement value of zero (date only). */ +#define REZ 0x4000 + /** * Parse a float value from the 32 bit representation (IEEE 754). * @param value the 32 bit representation of the float value. @@ -194,10 +207,26 @@ float uintToFloat(unsigned int value, bool negative); /** * Format a float value to the 32 bit representation (IEEE 754). - * @param val the float value. + * @param value the float value. * @return the 32 bit representation of the float value, or 0xffffffff if NaN. */ -uint32_t floatToUint(float val); +uint32_t floatToUint(float value); + +/** + * Parse a float value with precision of 2 decimal from 16 bit format with + * sign, 11 bit mantissa, 4 bit exponent as (0.01*m)(2^e). + * @param value the 16 bit representation of the float value. + * @return the float value. + */ +float uint16ToFloat(uint16_t value); + +/** + * Format a float value with precision of 2 decimal from 16 bit format with + * sign, 11 bit mantissa, 4 bit exponent as (0.01*m)(2^e). + * @param value the float value. + * @return the 16 bit representation of the float value, or 0xffff if NaN. + */ +uint16_t floatToUint16(float value); /** * Base class for all kinds of data types. @@ -267,7 +296,7 @@ class DataType { * @param output the @a ostream to dump to. * @return true when a non-default divisor was written to the output. */ - virtual bool dump(OutputFormat outputFormat, size_t length, bool appendDivisor, ostream* output) const; + virtual bool dump(OutputFormat outputFormat, size_t length, AppendDivisor appendDivisor, ostream* output) const; /** * Internal method for reading the numeric raw value from a @a SymbolString. @@ -344,7 +373,7 @@ class StringDataType : public DataType { virtual ~StringDataType() {} // @copydoc - bool dump(OutputFormat outputFormat, size_t length, bool appendDivisor, ostream* output) const override; + bool dump(OutputFormat outputFormat, size_t length, AppendDivisor appendDivisor, ostream* output) const override; // @copydoc result_t readRawValue(size_t offset, size_t length, const SymbolString& input, @@ -391,7 +420,7 @@ class DateTimeDataType : public DataType { virtual ~DateTimeDataType() {} // @copydoc - bool dump(OutputFormat outputFormat, size_t length, bool appendDivisor, ostream* output) const override; + bool dump(OutputFormat outputFormat, size_t length, AppendDivisor appendDivisor, ostream* output) const override; /** * @return true if date part is present. @@ -452,7 +481,25 @@ class NumberDataType : public DataType { NumberDataType(const string& id, size_t bitCount, uint16_t flags, unsigned int replacement, unsigned int minValue, unsigned int maxValue, int divisor, const NumberDataType* baseType = nullptr) - : DataType(id, bitCount, flags|NUM, replacement), m_minValue(minValue), m_maxValue(maxValue), + : DataType(id, bitCount, flags|NUM, replacement), m_minValue(minValue), m_maxValue(maxValue), m_incValue(0), + m_divisor(divisor == 0 ? 1 : divisor), m_precision(calcPrecision(divisor)), m_firstBit(0), m_baseType(baseType) {} + + /** + * Constructs a new instance for multiple of 8 bits with increment value. + * @param id the type identifier. + * @param bitCount the number of bits (maximum length if #ADJ flag is set). + * @param flags the combination of flags (like #BCD). + * @param replacement the replacement value (no replacement if equal to minValue). + * @param minValue the minimum raw value. + * @param maxValue the maximum raw value. + * @param incValue the smallest step value for increment/decrement, or 0 for auto. + * @param divisor the divisor (negative for reciprocal). + * @param baseType the base @a NumberDataType for derived instances, or nullptr. + */ + NumberDataType(const string& id, size_t bitCount, uint16_t flags, unsigned int replacement, + unsigned int minValue, unsigned int maxValue, unsigned int incValue, int divisor, + const NumberDataType* baseType = nullptr) + : DataType(id, bitCount, flags|NUM, replacement), m_minValue(minValue), m_maxValue(maxValue), m_incValue(incValue), m_divisor(divisor == 0 ? 1 : divisor), m_precision(calcPrecision(divisor)), m_firstBit(0), m_baseType(baseType) {} /** @@ -467,7 +514,7 @@ class NumberDataType : public DataType { */ NumberDataType(const string& id, size_t bitCount, uint16_t flags, unsigned int replacement, int16_t firstBit, int divisor, const NumberDataType* baseType = nullptr) - : DataType(id, bitCount, flags|NUM, replacement), m_minValue(0), m_maxValue((1 << bitCount)-1), + : DataType(id, bitCount, flags|NUM, replacement), m_minValue(0), m_maxValue((1 << bitCount)-1), m_incValue(0), m_divisor(divisor == 0 ? 1 : divisor), m_precision(0), m_firstBit(firstBit), m_baseType(baseType) {} /** @@ -484,7 +531,7 @@ class NumberDataType : public DataType { static size_t calcPrecision(int divisor); // @copydoc - bool dump(OutputFormat outputFormat, size_t length, bool appendDivisor, ostream* output) const override; + bool dump(OutputFormat outputFormat, size_t length, AppendDivisor appendDivisor, ostream* output) const override; /** * Derive a new @a NumberDataType from this. @@ -498,6 +545,18 @@ class NumberDataType : public DataType { */ virtual result_t derive(int divisor, size_t bitCount, const NumberDataType** derived) const; + /** + * Derive a new @a NumberDataType from this. + * @param min the minimum raw value. + * @param max the minimum raw value. + * @param inc the smallest step value for increment/decrement, or 0 to keep the current increment (or calculate + * automatically). + * @param derived the derived @a NumberDataType, or this if derivation is + * not necessary. + * @return @a RESULT_OK on success, or an error code. + */ + virtual result_t derive(unsigned int min, unsigned int max, unsigned int inc, const NumberDataType** derived) const; + /** * @return the minimum raw value. */ @@ -517,6 +576,22 @@ class NumberDataType : public DataType { */ result_t getMinMax(bool getMax, const OutputFormat outputFormat, ostream* output) const; + /** + * Check the value against the minimum and maximum value. + * @param value the raw value. + * @param negative optional variable in which to store the negative flag. + * @return @a RESULT_OK on success, or an error code. + */ + result_t checkValueRange(unsigned int value, bool* negative = nullptr) const; + + /** + * Get the smallest step value for increment/decrement. + * @param outputFormat the @a OutputFormat options to use. + * @param output the ostream to append the formatted value to. + * @return @a RESULT_OK on success, or an error code. + */ + result_t getStep(const OutputFormat outputFormat, ostream* output) const; + /** * @return the divisor (negative for reciprocal). */ @@ -561,10 +636,19 @@ class NumberDataType : public DataType { * @param value the numeric raw value. * @param outputFormat the @a OutputFormat options to use. * @param output the ostream to append the formatted value to. + * @param skipRangeCheck whether to skip the value range check. * @return @a RESULT_OK on success, or an error code. */ result_t readFromRawValue(unsigned int value, - OutputFormat outputFormat, ostream* output) const; + OutputFormat outputFormat, ostream* output, bool skipRangeCheck = false) const; + + /** + * Internal method for parsing an input string to the coorresponding raw value. + * @param inputStr the input string to parse the formatted value from. + * @param parsedValue the variable in which to store the parsed raw value. + * @return @a RESULT_OK on success, or an error code. + */ + result_t parseInput(const string inputStr, unsigned int* parsedValue) const; /** * Internal method for writing the numeric raw value to a @a SymbolString. @@ -591,6 +675,9 @@ class NumberDataType : public DataType { /** the maximum raw value. */ const unsigned int m_maxValue; + /** the smallest step value for increment/decrement, or 0 for auto. */ + const unsigned int m_incValue; + /** the divisor (negative for reciprocal). */ const int m_divisor; @@ -631,10 +718,9 @@ class DataTypeList { /** * Dump the type list optionally including the divisor to the output. * @param outputFormat the @a OutputFormat options. - * @param appendDivisor whether to append the divisor (if available). * @param output the @a ostream to dump to. */ - void dump(OutputFormat outputFormat, bool appendDivisor, ostream* output) const; + void dump(OutputFormat outputFormat, ostream* output) const; /** * Removes all @a DataType instances. @@ -644,10 +730,11 @@ class DataTypeList { /** * Adds a @a DataType instance to this map. * @param dataType the @a DataType instance to add. + * @param derivedKey optional speicla key for derived instances. * @return @a RESULT_OK on success, or an error code. * Note: the caller may not free the added instance on success. */ - result_t add(const DataType* dataType); + result_t add(const DataType* dataType, const string derivedKey = ""); /** * Adds a @a DataType instance for later cleanup. @@ -668,13 +755,13 @@ class DataTypeList { * Returns an iterator pointing to the first ID/@a DataType pair. * @return an iterator pointing to the first ID/@a DataType pair. */ - map::const_iterator begin() const { return m_typesById.begin(); } + map::const_iterator begin() const { return m_typesById.cbegin(); } /** * Returns an iterator pointing one past the last ID/@a DataType pair. * @return an iterator pointing one past the last ID/@a DataType pair. */ - map::const_iterator end() const { return m_typesById.end(); } + map::const_iterator end() const { return m_typesById.cend(); } private: /** the known @a DataType instances by ID (e.g. "ID:BITS" or just "ID"). diff --git a/src/lib/ebus/device.cpp b/src/lib/ebus/device.cpp deleted file mode 100755 index 6896422d5..000000000 --- a/src/lib/ebus/device.cpp +++ /dev/null @@ -1,910 +0,0 @@ -/* - * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2015-2022 John Baier - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifdef HAVE_CONFIG_H -# include -#endif - -#include "lib/ebus/device.h" -#include -#include -#include -#include -#ifdef HAVE_LINUX_SERIAL -# include -#endif -#ifdef HAVE_FREEBSD_UFTDI -# include -#endif -#ifdef HAVE_PPOLL -# include -#endif -#include -#include -#include -#include -#include -#include -#include "lib/ebus/data.h" -#include "lib/utils/tcpsocket.h" - -namespace ebusd { - -#define MTU 1540 - -#ifndef POLLRDHUP -#define POLLRDHUP 0 -#endif - -// ebusd enhanced protocol IDs: -#define ENH_REQ_INIT ((uint8_t)0x0) -#define ENH_RES_RESETTED ((uint8_t)0x0) -#define ENH_REQ_SEND ((uint8_t)0x1) -#define ENH_RES_RECEIVED ((uint8_t)0x1) -#define ENH_REQ_START ((uint8_t)0x2) -#define ENH_RES_STARTED ((uint8_t)0x2) -#define ENH_REQ_INFO ((uint8_t)0x3) -#define ENH_RES_INFO ((uint8_t)0x3) -#define ENH_RES_FAILED ((uint8_t)0xa) -#define ENH_RES_ERROR_EBUS ((uint8_t)0xb) -#define ENH_RES_ERROR_HOST ((uint8_t)0xc) - -// ebusd enhanced error codes for the ERROR_* responses -#define ENH_ERR_FRAMING ((uint8_t)0x00) -#define ENH_ERR_OVERRUN ((uint8_t)0x01) - -#define ENH_BYTE_FLAG ((uint8_t)0x80) -#define ENH_BYTE_MASK ((uint8_t)0xc0) -#define ENH_BYTE1 ((uint8_t)0xc0) -#define ENH_BYTE2 ((uint8_t)0x80) -#define makeEnhancedSequence(cmd, data) {(uint8_t)(ENH_BYTE1 | ((cmd)<<2) | (((data)&0xc0)>>6)), (uint8_t)(ENH_BYTE2 | ((data)&0x3f))} - -Device::Device(const char* name, bool checkDevice, unsigned int latency, bool readOnly, bool initialSend, - bool enhancedProto) - : m_name(name), m_checkDevice(checkDevice), - m_latency(HOST_LATENCY_MS+(enhancedProto?ENHANCED_LATENCY_MS:0)+latency), m_readOnly(readOnly), - m_initialSend(initialSend), m_enhancedProto(enhancedProto), m_fd(-1), m_resetRequested(false), - m_listener(nullptr), m_arbitrationMaster(SYN), - m_arbitrationCheck(0), m_bufSize(((MAX_LEN+1+3)/4)*4), m_bufLen(0), m_bufPos(0), - m_extraFatures(0), m_infoId(0xff), m_infoLen(0), m_infoPos(0) { - m_buffer = reinterpret_cast(malloc(m_bufSize)); - if (!m_buffer) { - m_bufSize = 0; - } -} - -Device::~Device() { - close(); - if (m_buffer) { - free(m_buffer); - } -} - -Device* Device::create(const char* name, unsigned int extraLatency, bool checkDevice, bool readOnly, bool initialSend) { - bool highSpeed = strncmp(name, "ens:", 4) == 0; - bool enhanced = highSpeed || strncmp(name, "enh:", 4) == 0; - if (enhanced) { - name += 4; - } - if (strchr(name, '/') == nullptr && strchr(name, ':') != nullptr) { - char* in = strdup(name); - bool udp = false; - char* addrpos = in; - char* portpos = strchr(addrpos, ':'); - if (!enhanced && portpos >= addrpos+3 && strncmp(addrpos, "enh", 3) == 0) { - enhanced = true; // support enhtcp:: and enhudp:: - addrpos += 3; - if (portpos == addrpos) { - addrpos++; - portpos = strchr(addrpos, ':'); - } - } // else: support enh:: defaulting to TCP - if (portpos == addrpos+3 && (strncmp(addrpos, "tcp", 3) == 0 || (udp=(strncmp(addrpos, "udp", 3) == 0)))) { - addrpos += 4; - portpos = strchr(addrpos, ':'); - } - if (portpos == nullptr) { - free(in); - return nullptr; // invalid protocol or missing port - } - result_t result = RESULT_OK; - uint16_t port = (uint16_t)parseInt(portpos+1, 10, 1, 65535, &result); - if (result != RESULT_OK) { - free(in); - return nullptr; // invalid port - } - *portpos = 0; - char* hostOrIp = strdup(addrpos); - free(in); - return new NetworkDevice(name, hostOrIp, port, extraLatency, readOnly, initialSend, udp, enhanced); - } - // support enh:/dev/, ens:/dev/, and /dev/ - return new SerialDevice(name, checkDevice, extraLatency, readOnly, initialSend, enhanced, highSpeed); -} - -result_t Device::open() { - close(); - return m_bufSize == 0 ? RESULT_ERR_DEVICE : RESULT_OK; -} - -result_t Device::afterOpen() { - m_bufLen = 0; - m_extraFatures = 0; - if (m_enhancedProto) { - symbol_t buf[2] = makeEnhancedSequence(ENH_REQ_INIT, 0x01); // extra feature: info -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw enhanced > %2.2x %2.2x\n", buf[0], buf[1]); - fflush(stdout); -#endif - if (::write(m_fd, buf, 2) != 2) { - return RESULT_ERR_SEND; - } - if (m_listener != nullptr) { - m_listener->notifyStatus(false, "resetting"); - } - m_resetRequested = true; - } else if (m_initialSend && !write(ESC)) { - return RESULT_ERR_SEND; - } - return RESULT_OK; -} - -void Device::close() { - if (m_fd != -1) { - ::close(m_fd); - m_fd = -1; - } - m_bufLen = 0; // flush read buffer -} - -bool Device::isValid() { - if (m_fd == -1) { - return false; - } - if (m_checkDevice) { - checkDevice(); - } - return m_fd != -1; -} - -result_t Device::requestEnhancedInfo(symbol_t infoId) { - if (!m_enhancedProto || m_extraFatures == 0 || infoId == 0xff) { - return RESULT_ERR_INVALID_ARG; - } - for (unsigned int i = 0; i < 4; i++) { - if (m_infoId == 0xff) { - break; - } - usleep(40000 + i*40000); - } - if (m_infoId != 0xff) { - return RESULT_ERR_DUPLICATE; - } - symbol_t buf[2] = makeEnhancedSequence(ENH_REQ_INFO, infoId); -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw enhanced > %2.2x %2.2x\n", buf[0], buf[1]); - fflush(stdout); -#endif - m_infoPos = 0; - m_infoId = infoId; - if (::write(m_fd, buf, 2) != 2) { - return RESULT_ERR_DEVICE; - } - return RESULT_OK; -} - -string Device::getEnhancedInfos() { - if (!m_enhancedProto || m_extraFatures == 0) { - return ""; - } - result_t res; - if (m_enhInfoTemperature.empty()) { - res = requestEnhancedInfo(0); - if (res != RESULT_OK) { - return "cannot request version"; - } - res = requestEnhancedInfo(1); - if (res != RESULT_OK) { - return "cannot request ID"; - } - res = requestEnhancedInfo(2); - if (res != RESULT_OK) { - return "cannot request config"; - } - } - res = requestEnhancedInfo(6); - if (res != RESULT_OK) { - return "cannot request reset info"; - } - res = requestEnhancedInfo(3); - if (res != RESULT_OK) { - return "cannot request temperature"; - } - res = requestEnhancedInfo(4); - if (res != RESULT_OK) { - return "cannot request supply voltage"; - } - res = requestEnhancedInfo(5); - if (res != RESULT_OK) { - return "cannot request bus voltage"; - } - usleep(8*40000); - if (m_infoPos == 0) { - return "did not get info"; - } - return m_enhInfoTemperature + ", " + m_enhInfoSupplyVoltage + ", " + m_enhInfoBusVoltage; -} - -result_t Device::send(symbol_t value) { - if (!isValid()) { - return RESULT_ERR_DEVICE; - } - if (m_readOnly || !write(value)) { - return RESULT_ERR_SEND; - } - if (m_listener != nullptr) { - m_listener->notifyDeviceData(value, false); - } - return RESULT_OK; -} - -/** - * the maximum duration in milliseconds to wait for an enhanced sequence to complete after the first part was already - * retrieved (3ms rounded up to the next 10ms): 2* (Start+8Bit+Stop+Extra @ 9600Bd) - */ -#define ENHANCED_COMPLETE_WAIT_DURATION 10 - - -bool Device::cancelRunningArbitration(ArbitrationState* arbitrationState) { - if (m_enhancedProto && m_arbitrationMaster != SYN) { - *arbitrationState = as_error; - m_arbitrationMaster = SYN; - m_arbitrationCheck = 0; - write(SYN, true); - return true; - } - if (m_enhancedProto || m_arbitrationMaster == SYN) { - return false; - } - *arbitrationState = as_error; - m_arbitrationMaster = SYN; - m_arbitrationCheck = 0; - return true; -} - -result_t Device::recv(unsigned int timeout, symbol_t* value, ArbitrationState* arbitrationState) { - if (m_arbitrationMaster != SYN) { - *arbitrationState = as_running; - } - if (!isValid()) { - cancelRunningArbitration(arbitrationState); - return RESULT_ERR_DEVICE; - } - bool repeated = false; - timeout += m_latency; - do { - bool isAvailable = available(); - if (!isAvailable && timeout > 0) { - int ret; - struct timespec tdiff; - - // set select timeout - tdiff.tv_sec = timeout/1000; - tdiff.tv_nsec = (timeout%1000)*1000000; - -#ifdef HAVE_PPOLL - nfds_t nfds = 1; - struct pollfd fds[nfds]; - - memset(fds, 0, sizeof(fds)); - - fds[0].fd = m_fd; - fds[0].events = POLLIN | POLLERR | POLLHUP | POLLRDHUP; - ret = ppoll(fds, nfds, &tdiff, nullptr); - if (ret >= 0 && fds[0].revents & (POLLERR | POLLHUP | POLLRDHUP)) { - ret = -1; - } -#else -#ifdef HAVE_PSELECT - fd_set readfds, exceptfds; - - FD_ZERO(&readfds); - FD_ZERO(&exceptfds); - FD_SET(m_fd, &readfds); - - ret = pselect(m_fd + 1, &readfds, nullptr, &exceptfds, &tdiff, nullptr); - if (ret >= 1 && FD_ISSET(m_fd, &exceptfds)) { - ret = -1; - } -#else - ret = 1; // ignore timeout if neither ppoll nor pselect are available -#endif -#endif - if (ret == -1) { -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "poll error %d\n", errno); -#endif - close(); - cancelRunningArbitration(arbitrationState); - return RESULT_ERR_DEVICE; - } - if (ret == 0) { - return RESULT_ERR_TIMEOUT; - } - } - - // directly read byte from device - bool incomplete = false; - if (read(value, isAvailable, arbitrationState, &incomplete)) { - break; // don't repeat on successful read - } - if (!isAvailable && incomplete && !repeated) { - // for a two-byte transfer another poll is needed - repeated = true; - timeout = m_latency+ENHANCED_COMPLETE_WAIT_DURATION; - continue; - } - return RESULT_ERR_TIMEOUT; - } while (true); - if (m_enhancedProto || *value != SYN || m_arbitrationMaster == SYN || m_arbitrationCheck) { - if (m_listener != nullptr) { - m_listener->notifyDeviceData(*value, true); - } - if (!m_enhancedProto && m_arbitrationMaster != SYN) { - if (m_arbitrationCheck) { - *arbitrationState = *value == m_arbitrationMaster ? as_won : as_lost; - m_arbitrationMaster = SYN; - m_arbitrationCheck = 0; - } else { - *arbitrationState = m_arbitrationMaster == SYN ? as_none : as_start; - } - } - return RESULT_OK; - } - // non-enhanced: arbitration executed by ebusd itself - bool wrote = write(m_arbitrationMaster); // send as fast as possible - if (m_listener != nullptr) { - m_listener->notifyDeviceData(*value, true); - } - if (!wrote) { - cancelRunningArbitration(arbitrationState); - return RESULT_OK; - } - if (m_listener != nullptr) { - m_listener->notifyDeviceData(m_arbitrationMaster, false); - } - m_arbitrationCheck = 1; - *arbitrationState = as_running; - return RESULT_OK; -} - -result_t Device::startArbitration(symbol_t masterAddress) { - if (m_arbitrationCheck) { - if (masterAddress != SYN) { - return RESULT_ERR_ARB_RUNNING; // should not occur - } - m_arbitrationCheck = 0; - m_arbitrationMaster = SYN; - if (m_enhancedProto) { - // cancel running arbitration - if (!write(SYN, true)) { - return RESULT_ERR_SEND; - } - } - return RESULT_OK; - } - if (m_readOnly) { - return RESULT_ERR_SEND; - } - m_arbitrationMaster = masterAddress; - if (m_enhancedProto && masterAddress != SYN) { - if (!write(masterAddress, true)) { - m_arbitrationMaster = SYN; - return RESULT_ERR_SEND; - } - m_arbitrationCheck = 1; - } - return RESULT_OK; -} - -bool Device::write(symbol_t value, bool startArbitration) { - if (m_enhancedProto) { - symbol_t buf[2] = makeEnhancedSequence(startArbitration ? ENH_REQ_START : ENH_REQ_SEND, value); -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw enhanced > %2.2x %2.2x\n", buf[0], buf[1]); - fflush(stdout); -#endif - return ::write(m_fd, buf, 2) == 2; - } -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw > %2.2x\n", value); - fflush(stdout); -#endif -#ifdef SIMULATE_NON_WRITABILITY - return true; -#else - return ::write(m_fd, &value, 1) == 1; -#endif -} - -bool Device::available() { - if (m_bufLen <= 0) { - return false; - } - if (!m_enhancedProto) { - return true; - } - // peek into the received enhanced proto bytes to determine symbol availability - for (size_t pos = 0; pos < m_bufLen; pos++) { - symbol_t ch = m_buffer[(pos+m_bufPos)%m_bufSize]; - if (!(ch&ENH_BYTE_FLAG)) { -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw avail direct @%d+%d %2.2x\n", m_bufPos, pos, ch); - fflush(stdout); -#endif - return true; - } - if ((ch&ENH_BYTE_MASK) == ENH_BYTE1) { - if (pos+1 >= m_bufLen) { - return false; - } - // peek into next byte to check if enhanced sequence is ok - ch = m_buffer[(pos+m_bufPos+1)%m_bufSize]; - if (!(ch&ENH_BYTE_FLAG) || (ch&ENH_BYTE_MASK) != ENH_BYTE2) { -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw avail enhanced following bad @%d+%d %2.2x %2.2x\n", m_bufPos, pos, - m_buffer[(pos+m_bufPos)%m_bufSize], ch); - fflush(stdout); -#endif - if (m_listener != nullptr) { - m_listener->notifyStatus(true, "unexpected available enhanced following byte 1"); - } - // drop first byte of invalid sequence - m_bufPos = (m_bufPos + 1) % m_bufSize; - m_bufLen--; - pos--; - continue; - } -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw avail enhanced @%d+%d %2.2x %2.2x\n", m_bufPos, pos, m_buffer[(pos+m_bufPos)%m_bufSize], ch); - fflush(stdout); -#endif - return true; - } -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw avail enhanced bad @%d+%d %2.2x\n", m_bufPos, pos, ch); - fflush(stdout); -#endif - if (m_listener != nullptr) { - m_listener->notifyStatus(true, "unexpected available enhanced byte 2"); - } - // skip byte from erroneous protocol - m_bufPos = (m_bufPos+1)%m_bufSize; - m_bufLen--; - pos--; - } - return false; -} - -bool Device::read(symbol_t* value, bool isAvailable, ArbitrationState* arbitrationState, bool* incomplete) { - if (!isAvailable) { - if (m_bufLen > 0 && m_bufPos != 0) { - if (m_bufLen > m_bufSize / 2) { - // more than half of input buffer consumed is taken as signal that ebusd is too slow - m_bufLen = 0; - if (m_listener != nullptr) { - m_listener->notifyStatus(true, "buffer overflow"); - } - } else { - size_t tail; - if (m_bufPos+m_bufLen > m_bufSize) { - // move wrapped tail away - tail = (m_bufPos+m_bufLen) % m_bufSize; - size_t head = m_bufLen-tail; - memmove(m_buffer+head, m_buffer, tail); -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw move tail %d @0 to @%d\n", tail, head); - fflush(stdout); -#endif - } else { - tail = 0; - } - // move head to first position - memmove(m_buffer, m_buffer + m_bufPos, m_bufLen - tail); -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw move head %d @%d to 0\n", m_bufLen - tail, m_bufPos); - fflush(stdout); -#endif - } - } - m_bufPos = 0; - // fill up the buffer - ssize_t size = ::read(m_fd, m_buffer + m_bufLen, m_bufSize - m_bufLen); - if (size <= 0) { - return false; - } -#ifdef DEBUG_RAW_TRAFFIC - fprintf(stdout, "raw %ld+%ld <", m_bufLen, size); - for (int pos=0; pos < size; pos++) { - fprintf(stdout, " %2.2x", m_buffer[(m_bufLen+pos)%m_bufSize]); - } - fprintf(stdout, "\n"); - fflush(stdout); -#endif - m_bufLen += size; - } - if (!available()) { - if (incomplete) { - *incomplete = m_enhancedProto && m_bufLen > 0; - } - return false; - } - if (!m_enhancedProto) { - *value = m_buffer[m_bufPos]; - m_bufPos = (m_bufPos+1)%m_bufSize; - m_bufLen--; - return true; - } - while (m_bufLen > 0) { - symbol_t ch = m_buffer[m_bufPos]; - if (!(ch&ENH_BYTE_FLAG)) { - *value = ch; - m_bufPos = (m_bufPos+1)%m_bufSize; - m_bufLen--; - return true; - } - uint8_t kind = ch&ENH_BYTE_MASK; - if (kind == ENH_BYTE1 && m_bufLen < 2) { - return false; // transfer not complete yet - } - m_bufPos = (m_bufPos+1)%m_bufSize; - m_bufLen--; - if (kind == ENH_BYTE2) { - if (m_listener != nullptr) { - m_listener->notifyStatus(true, "unexpected enhanced byte 2"); - } - return false; - } - // kind is ENH_BYTE1 - symbol_t ch2 = m_buffer[m_bufPos]; - m_bufPos = (m_bufPos + 1) % m_bufSize; - m_bufLen--; - if ((ch2 & ENH_BYTE_MASK) != ENH_BYTE2) { - if (m_listener != nullptr) { - m_listener->notifyStatus(true, "missing enhanced byte 2"); - } - return false; - } - symbol_t data = (symbol_t)(((ch&0x03) << 6) | (ch2&0x3f)); - symbol_t cmd = (ch >> 2)&0xf; - switch (cmd) { - case ENH_RES_STARTED: - *arbitrationState = as_won; - if (m_listener != NULL) { - m_listener->notifyDeviceData(data, false); - } - m_arbitrationMaster = SYN; - m_arbitrationCheck = 0; - *value = data; - return true; - case ENH_RES_FAILED: - *arbitrationState = as_lost; - if (m_listener != NULL) { - m_listener->notifyDeviceData(m_arbitrationMaster, false); - } - m_arbitrationMaster = SYN; - m_arbitrationCheck = 0; - *value = data; - return true; - case ENH_RES_RECEIVED: - *value = data; - if (data == SYN && *arbitrationState == as_running && m_arbitrationCheck) { - if (m_arbitrationCheck < 3) { // wait for three SYN symbols before switching to timeout - m_arbitrationCheck++; - } else { - *arbitrationState = as_timeout; - m_arbitrationMaster = SYN; - m_arbitrationCheck = 0; - } - } - return true; - case ENH_RES_RESETTED: - if (*arbitrationState != as_none) { - *arbitrationState = as_error; - m_arbitrationMaster = SYN; - m_arbitrationCheck = 0; - } - m_enhInfoTemperature = ""; - m_enhInfoSupplyVoltage = ""; - m_enhInfoBusVoltage = ""; - m_infoId = 0xff; - if (m_resetRequested) { - m_resetRequested = false; - } else { - close(); // on self-reset of device close and reopen it to have a clean startup - cancelRunningArbitration(arbitrationState); - } - m_extraFatures = data; - if (m_listener != nullptr) { - m_listener->notifyStatus(false, (m_extraFatures&0x01) ? "reset, supports info" : "reset"); - } - break; - case ENH_RES_INFO: - if (m_infoLen == 0) { - if (data <= 16) { // max length - m_infoLen = data; - m_infoPos = 0; - } - } else if (m_infoPos < m_infoLen) { - m_infoBuf[m_infoPos++] = data; - if (m_infoPos >= m_infoLen) { - unsigned int val; - ostringstream stream; - switch ((m_infoLen << 8) | m_infoId) { - case 0x0200: - case 0x0500: // with firmware version and jumper info - case 0x0800: // with firmware version, jumper info, and bootloader version - stream << "firmware " << static_cast(m_infoBuf[0]) << "." // version minor - << std::hex << static_cast(m_infoBuf[1]); // features mask - if (m_infoLen >= 5) { - stream << " [" << std::setfill('0') << std::setw(2) << std::hex << static_cast(m_infoBuf[2]) - << std::setw(2) << static_cast(m_infoBuf[3]) << "]"; - stream << ", jumpers 0x" << std::setw(2) << static_cast(m_infoBuf[4]); - stream << std::setfill(' '); // reset - } - if (m_infoLen >= 8) { - stream << ", bootloader " << std::dec << static_cast(m_infoBuf[5]); - stream << " [" << std::setfill('0') << std::setw(2) << std::hex << static_cast(m_infoBuf[6]) - << std::setw(2) << static_cast(m_infoBuf[7]) << "]"; - } - break; - case 0x0901: - case 0x0802: - stream << (m_infoId == 1 ? "ID" : "config"); - stream << std::hex << std::setfill('0'); - for (uint8_t pos = 0; pos < m_infoPos; pos++) { - stream << " " << std::setw(2) << static_cast(m_infoBuf[pos]); - } - if (m_infoId == 2 && (m_infoBuf[2]&0x3f) != 0x3f) { - // non-default arbitration delay - val = (m_infoBuf[2]&0x3f)*10; // steps of 10us - stream << ", arbitration delay " << std::dec << static_cast(val) << " us"; - } - break; - case 0x0203: - val = (static_cast(m_infoBuf[0]) << 8) | static_cast(m_infoBuf[1]); - stream << "temperature " << static_cast(val) << " °C"; - m_enhInfoTemperature = stream.str(); - break; - case 0x0204: - val = (static_cast(m_infoBuf[0]) << 8) | static_cast(m_infoBuf[1]); - stream << "supply voltage " << static_cast(val) << " mV"; - m_enhInfoSupplyVoltage = stream.str(); - break; - case 0x0205: - stream << "bus voltage " << std::fixed << std::setprecision(1) - << static_cast(m_infoBuf[1] / 10.0) << " V - " - << static_cast(m_infoBuf[0] / 10.0) << " V"; - m_enhInfoBusVoltage = stream.str(); - break; - case 0x0206: - stream << "reset cause "; - if (m_infoBuf[0]) { - stream << static_cast(m_infoBuf[0]) << "="; - switch (m_infoBuf[0]) { - case 1: stream << "power-on"; break; - case 2: stream << "brown-out"; break; - case 3: stream << "watchdog"; break; - case 4: stream << "clear"; break; - case 5: stream << "reset"; break; - case 6: stream << "stack"; break; - case 7: stream << "memory"; break; - default: stream << "other"; break; - } - stream << ", restart count " << static_cast(m_infoBuf[1]); - } else { - stream << "unknown"; - } - break; - default: - stream << "unknown 0x" << std::hex << std::setfill('0') << std::setw(2) - << static_cast(m_infoId) << ", len " << std::dec << std::setw(0) - << static_cast(m_infoPos); - break; - } - m_listener->notifyStatus(false, ("extra info: "+stream.str()).c_str()); - m_infoLen = 0; - m_infoId = 0xff; - } - } - break; - case ENH_RES_ERROR_EBUS: - case ENH_RES_ERROR_HOST: - if (m_listener != nullptr) { - ostringstream stream; - stream << (cmd == ENH_RES_ERROR_EBUS ? "eBUS comm error: " : "host comm error: "); - switch (data) { - case ENH_ERR_FRAMING: - stream << "framing"; - break; - case ENH_ERR_OVERRUN: - stream << "overrun"; - break; - default: - stream << "unknown 0x" << std::setw(2) << std::setfill('0') << std::hex << static_cast(data); - break; - } - string str = stream.str(); - m_listener->notifyStatus(true, str.c_str()); - } - cancelRunningArbitration(arbitrationState); - break; - default: - if (m_listener != nullptr) { - ostringstream stream; - stream << "unexpected enhanced command 0x" << std::setw(2) << std::setfill('0') << std::hex - << static_cast(cmd); - string str = stream.str(); - m_listener->notifyStatus(true, str.c_str()); - } - return false; - } - } - return false; -} - - -result_t SerialDevice::open() { - result_t result = Device::open(); - if (result != RESULT_OK) { - return result; - } - struct termios newSettings; - - // open file descriptor - m_fd = ::open(m_name, O_RDWR | O_NOCTTY | O_NDELAY); - - if (m_fd < 0) { - return RESULT_ERR_NOTFOUND; - } - if (isatty(m_fd) == 0) { - close(); - return RESULT_ERR_NOTFOUND; - } - - if (flock(m_fd, LOCK_EX|LOCK_NB)) { - close(); - return RESULT_ERR_DEVICE; - } - -#ifdef HAVE_LINUX_SERIAL - struct serial_struct serial; - if (ioctl(m_fd, TIOCGSERIAL, &serial) == 0) { - serial.flags |= ASYNC_LOW_LATENCY; - ioctl(m_fd, TIOCSSERIAL, &serial); - } -#endif - -#ifdef HAVE_FREEBSD_UFTDI - int param = 0; - // flush tx/rx and set low latency on uftdi device - if (ioctl(m_fd, UFTDIIOC_GET_LATENCY, ¶m) == 0) { - ioctl(m_fd, UFTDIIOC_RESET_IO, ¶m); - param = 1; - ioctl(m_fd, UFTDIIOC_SET_LATENCY, ¶m); - } -#endif - - // save current settings - tcgetattr(m_fd, &m_oldSettings); - - // create new settings - memset(&newSettings, 0, sizeof(newSettings)); - - cfsetspeed(&newSettings, m_enhancedProto ? (m_enhancedHighSpeed ? B115200 : B9600) : B2400); - newSettings.c_cflag |= (CS8 | CLOCAL | CREAD); - newSettings.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // non-canonical mode - newSettings.c_iflag |= IGNPAR; // ignore parity errors - newSettings.c_oflag &= ~OPOST; - - // non-canonical mode: read() blocks until at least one byte is available - newSettings.c_cc[VMIN] = 1; - newSettings.c_cc[VTIME] = 0; - - // empty device buffer - tcflush(m_fd, TCIFLUSH); - - // activate new settings of serial device - if (tcsetattr(m_fd, TCSANOW, &newSettings)) { - close(); - return RESULT_ERR_DEVICE; - } - - // set serial device into blocking mode - fcntl(m_fd, F_SETFL, fcntl(m_fd, F_GETFL) & ~O_NONBLOCK); - - return afterOpen(); -} - -void SerialDevice::close() { - if (m_fd != -1) { - // empty device buffer - tcflush(m_fd, TCIOFLUSH); - - // restore previous settings of the device - tcsetattr(m_fd, TCSANOW, &m_oldSettings); - } - Device::close(); -} - -void SerialDevice::checkDevice() { - int port; - if (ioctl(m_fd, TIOCMGET, &port) == -1) { - close(); - } -} - -#ifdef __CYGWIN__ - #ifndef TCP_KEEPCNT - #define TCP_KEEPCNT 8 - #endif - #ifndef TCP_KEEPINTVL - #define TCP_KEEPINTVL 150 - #endif - #ifndef TCP_KEEPIDLE - #define TCP_KEEPIDLE 14400 - #endif -#endif - -result_t NetworkDevice::open() { - result_t result = Device::open(); - if (result != RESULT_OK) { - return result; - } - m_fd = socketConnect(m_hostOrIp, m_port, m_udp, nullptr, 5, 2); // wait up to 5 seconds for established connection - if (m_fd < 0) { - return RESULT_ERR_GENERIC_IO; - } - if (!m_udp) { - usleep(25000); // wait 25ms for potential initial garbage - } - int cnt; - symbol_t buf[MTU]; - int ioerr; - while ((ioerr=ioctl(m_fd, FIONREAD, &cnt)) >= 0 && cnt > 1) { - // skip buffered input - ssize_t read = ::read(m_fd, &buf, MTU); - if (read <= 0) { - break; - } - } - if (ioerr < 0) { - close(); - return RESULT_ERR_GENERIC_IO; - } - return afterOpen(); -} - -void NetworkDevice::checkDevice() { - int cnt; - if (ioctl(m_fd, FIONREAD, &cnt) < 0) { - close(); - } -} - -} // namespace ebusd diff --git a/src/lib/ebus/device.h b/src/lib/ebus/device.h index 33ca04e3c..037d794ea 100755 --- a/src/lib/ebus/device.h +++ b/src/lib/ebus/device.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2015-2022 John Baier + * Copyright (C) 2015-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,13 +19,7 @@ #ifndef LIB_EBUS_DEVICE_H_ #define LIB_EBUS_DEVICE_H_ -#include -#include -#include -#include -#include -#include -#include +#include #include #include "lib/ebus/result.h" #include "lib/ebus/symbol.h" @@ -35,25 +29,11 @@ namespace ebusd { /** @file lib/ebus/device.h * Classes providing access to the eBUS. * - * A @a Device is either a @a SerialDevice directly connected to a local tty - * port or a remote @a NetworkDevice handled via a TCP socket. It allows to - * send and receive bytes to/from the eBUS while optionally dumping the data - * to a file and/or forwarding it to a logging function. + * A @a Device allows to send and receive data to/from a local or remote eBUS + * device while optionally dumping the data to a file and/or forwarding it to + * a logging function. */ -/** the transfer latency of the network device [ms]. */ -#define NETWORK_LATENCY_MS 30 - -/** the extra transfer latency to take into account for enhanced protocol. */ -#define ENHANCED_LATENCY_MS 10 - -/** the latency of the host [ms]. */ -#if defined(__CYGWIN__) || defined(_WIN32) -#define HOST_LATENCY_MS 20 -#else -#define HOST_LATENCY_MS 10 -#endif - /** the arbitration state handled by @a Device. */ enum ArbitrationState { as_none, //!< no arbitration in process @@ -76,18 +56,19 @@ class DeviceListener { virtual ~DeviceListener() {} /** - * Listener method that is called when a symbol was received/sent. - * @param symbol the received/sent symbol. + * Listener method that is called when symbols were received from/sent to eBUS. + * @param data the received/sent data. + * @param len the length of received/sent data. * @param received @a true on reception, @a false on sending. */ - virtual void notifyDeviceData(symbol_t symbol, bool received) = 0; // abstract + virtual void notifyDeviceData(const symbol_t* data, size_t len, bool received) = 0; // abstract /** * Called to notify a status message from the device. * @param error true for an error message, false for an info message. * @param message the message string. */ - virtual void notifyStatus(bool error, const char* message) = 0; // abstract + virtual void notifyDeviceStatus(bool error, const char* message) = 0; // abstract }; @@ -98,64 +79,67 @@ class Device { protected: /** * Construct a new instance. - * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). - * @param checkDevice whether to regularly check the device availability. - * @param latency the bus transfer latency in milliseconds. - * @param readOnly whether to allow read access to the device only. - * @param initialSend whether to send an initial @a ESC symbol in @a open(). - * @param enhancedProto whether to use the ebusd enhanced protocol. */ - Device(const char* name, bool checkDevice, unsigned int latency, bool readOnly, bool initialSend, - bool enhancedProto = false); + Device() + : m_listener(nullptr) { + } public: /** * Destructor. */ - virtual ~Device(); + virtual ~Device() { + } + + /** + * Get the device name. + * @return the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). + */ + virtual const char* getName() const = 0; // abstract /** - * Factory method for creating a new instance. - * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). - * @param extraLatency the extra bus transfer latency in milliseconds. - * @param checkDevice whether to regularly check the device availability (only for serial devices). - * @param readOnly whether to allow read access to the device only. - * @param initialSend whether to send an initial @a ESC symbol in @a open(). - * @return the new @a Device, or nullptr on error. - * Note: the caller needs to free the created instance. + * Set the @a DeviceListener. + * @param listener the @a DeviceListener. */ - static Device* create(const char* name, unsigned int extraLatency = 0, bool checkDevice = true, - bool readOnly = false, bool initialSend = false); + void setListener(DeviceListener* listener) { m_listener = listener; } /** - * Get the transfer latency of this device. - * @return the transfer latency in milliseconds. + * Format device infos in plain text. + * @param output the @a ostringstream to append the infos to. + * @param verbose whether to add verbose infos. + * @param prefix true for the synchronously retrievable prefix, false for the potentially asynchronous suffix. */ - virtual unsigned int getLatency() const { return m_latency; } + virtual void formatInfo(ostringstream* output, bool verbose, bool prefix) = 0; // abstract /** - * Open the file descriptor. - * @return the @a result_t code. + * Format device infos in JSON format. + * @param output the @a ostringstream to append the infos to. */ - virtual result_t open(); + virtual void formatInfoJson(ostringstream* output) {} /** - * Has to be called by subclasses upon successful opening the device as last action in open(). + * @return whether the device supports checking for version updates. + */ + virtual bool supportsUpdateCheck() const { return false; } + + /** + * Open the file descriptor. * @return the @a result_t code. */ - result_t afterOpen(); + virtual result_t open() = 0; // abstract /** - * Close the file descriptor if opened. + * Return whether the device is opened and available. + * @return whether the device is opened and available. */ - virtual void close(); + virtual bool isValid() = 0; // abstract /** * Write a single byte to the device. * @param value the byte value to write. * @return the @a result_t code. */ - result_t send(symbol_t value); + virtual result_t send(symbol_t value) = 0; // abstract /** * Read a single byte from the device. @@ -165,7 +149,7 @@ class Device { * @a as_won, the received byte is the master address that was successfully arbitrated with. * @return the result_t code. */ - result_t recv(unsigned int timeout, symbol_t* value, ArbitrationState* arbitrationState); + virtual result_t recv(unsigned int timeout, symbol_t* value, ArbitrationState* arbitrationState) = 0; // abstract /** * Start the arbitration with the specified master address. A subsequent request while an arbitration is currently in @@ -173,258 +157,24 @@ class Device { * @param masterAddress the master address, or @a SYN to cancel a previous arbitration request. * @return the result_t code. */ - result_t startArbitration(symbol_t masterAddress); + virtual result_t startArbitration(symbol_t masterAddress) = 0; // abstract /** * Return whether the device is currently in arbitration. * @return true when the device is currently in arbitration. */ - bool isArbitrating() const { return m_arbitrationMaster != SYN; } - - /** - * Return the device name. - * @return the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). - */ - const char* getName() { return m_name; } - - /** - * Return whether the device is opened and available. - * @return whether the device is opened and available. - */ - bool isValid(); - - /** - * Return whether to allow read access to the device only. - * @return whether to allow read access to the device only. - */ - bool isReadOnly() const { return m_readOnly; } - - /** - * Return whether the device supports the ebusd enhanced protocol. - * @return whether the device supports the ebusd enhanced protocol. - */ - bool isEnhancedProto() const { return m_enhancedProto; } - - /** - * Set the @a DeviceListener. - * @param listener the @a DeviceListener. - */ - void setListener(DeviceListener* listener) { m_listener = listener; } - - /** - * Send a request for extra infos to enhanced device. - * @param infoId the ID of the info to request. - * @return @a RESULT_OK on success, or an error code otherwise. - */ - result_t requestEnhancedInfo(symbol_t infoId); - - /** - * Retrieve/update all extra infos from an enhanced device. - * @return @a a string with the extra infos, or empty. - */ - string getEnhancedInfos(); - - protected: - /** - * Check if the device is still available and close it if not. - */ - virtual void checkDevice() = 0; // abstract + virtual bool isArbitrating() const = 0; // abstract /** * Cancel a running arbitration. * @param arbitrationState the reference in which @a as_error is stored when cancelled. * @return true if it was cancelled, false if not. */ - bool cancelRunningArbitration(ArbitrationState* arbitrationState); - - /** - * Write a single byte. - * @param value the byte value to write. - * @param startArbitration true to start arbitration. - * @return true on success, false on error. - */ - virtual bool write(symbol_t value, bool startArbitration = false); - - /** - * Check whether a symbol is available for reading immediately (without waiting). - * @return true when a symbol is available for reading immediately. - */ - virtual bool available(); - - /** - * Read a single byte. - * @param value the reference in which the read byte value is stored. - * @param isAvailable the result of the immediately preceding call to @a available(). - * @param arbitrationState the variable in which to store the current/received arbitration state (mandatory for enhanced proto). - * @param incomplete the variable in which to store when a partial transfer needs another poll. - * @return true on success, false on error. - */ - virtual bool read(symbol_t* value, bool isAvailable, ArbitrationState* arbitrationState = nullptr, - bool* incomplete = nullptr); - - /** the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). */ - const char* m_name; + virtual bool cancelRunningArbitration(ArbitrationState* arbitrationState) = 0; // abstract - /** whether to regularly check the device availability. */ - const bool m_checkDevice; - - /** the bus transfer latency in milliseconds. */ - const unsigned int m_latency; - - /** whether to allow read access to the device only. */ - const bool m_readOnly; - - /** whether to send an initial @a ESC symbol in @a open(). */ - const bool m_initialSend; - - /** whether the device supports the ebusd enhanced protocol. */ - const bool m_enhancedProto; - - /** the opened file descriptor, or -1. */ - int m_fd; - - /** whether the reset of an enhanced device was already requested. */ - bool m_resetRequested; - - private: + protected: /** the @a DeviceListener, or nullptr. */ DeviceListener* m_listener; - - /** the arbitration master address to send when in arbitration, or @a SYN. */ - symbol_t m_arbitrationMaster; - - /** >0 when in arbitration and the next received symbol needs to be checked against the sent master address, - * incremented with each received SYN when arbitration was not performed as expected and needs to be stopped. */ - size_t m_arbitrationCheck; - - /** the read buffer. */ - symbol_t* m_buffer; - - /** the read buffer size (multiple of 4). */ - size_t m_bufSize; - - /** the read buffer fill length. */ - size_t m_bufLen; - - /** the read buffer read position. */ - size_t m_bufPos; - - /** the extra features supported by the device. */ - symbol_t m_extraFatures; - - /** the ID of the last requested info. */ - symbol_t m_infoId; - - /** the info buffer expected length. */ - size_t m_infoLen; - - /** the info buffer write position. */ - size_t m_infoPos; - - /** the info buffer. */ - symbol_t m_infoBuf[16]; - - /** a string describing the enhanced device temperature. */ - string m_enhInfoTemperature; - - /** a string describing the enhanced device supply voltage. */ - string m_enhInfoSupplyVoltage; - - /** a string describing the enhanced device bus voltage. */ - string m_enhInfoBusVoltage; -}; - - -/** - * The @a Device for directly connected serial interfaces (tty). - */ -class SerialDevice : public Device { - public: - /** - * Construct a new instance. - * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). - * @param checkDevice whether to regularly check the device availability. - * @param extraLatency the extra bus transfer latency in milliseconds. - * @param readOnly whether to allow read access to the device only. - * @param initialSend whether to send an initial @a ESC symbol in @a open(). - * @param enhancedProto whether to use the ebusd enhanced protocol. - * @param enhancedHighSpeed whether to use ebusd enhanced protocol in high speed mode. - */ - SerialDevice(const char* name, bool checkDevice, unsigned int extraLatency, bool readOnly, bool initialSend, - bool enhancedProto = false, bool enhancedHighSpeed = false) - : Device(name, checkDevice, extraLatency, readOnly, initialSend, enhancedProto), - m_enhancedHighSpeed(enhancedHighSpeed) { - } - - // @copydoc - result_t open() override; - - // @copydoc - void close() override; - - - protected: - // @copydoc - void checkDevice() override; - - - private: - /** whether to use ebusd enhanced protocol in high speed mode. */ - bool m_enhancedHighSpeed; - - /** the previous settings of the device for restoring. */ - termios m_oldSettings; -}; - -/** - * The @a Device for remote network interfaces. - */ -class NetworkDevice : public Device { - public: - /** - * Construct a new instance. - * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). - * @param address the socket address of the device. - * @param hostOrIp the host name or IP address of the device. - * @param port the TCP or UDP port of the device. - * @param extraLatency the extra bus transfer latency in milliseconds. - * @param readOnly whether to allow read access to the device only. - * @param initialSend whether to send an initial @a ESC symbol in @a open(). - * @param udp true for UDP, false to TCP. - * @param enhancedProto whether to use the ebusd enhanced protocol. - */ - NetworkDevice(const char* name, const char* hostOrIp, uint16_t port, unsigned int extraLatency, bool readOnly, - bool initialSend, bool udp, bool enhancedProto = false) - : Device(name, true, NETWORK_LATENCY_MS+extraLatency, readOnly, initialSend, enhancedProto), - m_hostOrIp(hostOrIp), m_port(port), m_udp(udp) {} - - /** - * Destructor. - */ - ~NetworkDevice() override { - if (m_hostOrIp) { - free((void*)m_hostOrIp); - } - } - - // @copydoc - result_t open() override; - - - protected: - // @copydoc - void checkDevice() override; - - - private: - /** the host name or IP address of the device. */ - const char* m_hostOrIp; - - /** the TCP or UDP port of the device. */ - const uint16_t m_port; - - /** true for UDP, false to TCP. */ - const bool m_udp; }; } // namespace ebusd diff --git a/src/lib/ebus/device_enhanced.h b/src/lib/ebus/device_enhanced.h new file mode 100755 index 000000000..969b2345f --- /dev/null +++ b/src/lib/ebus/device_enhanced.h @@ -0,0 +1,93 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2015-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIB_EBUS_DEVICE_ENHANCED_H_ +#define LIB_EBUS_DEVICE_ENHANCED_H_ + +#include +#include +#include "lib/ebus/result.h" +#include "lib/ebus/symbol.h" + +namespace ebusd { + +/** @file lib/ebus/device_enhanced.h + * Enhanced protocol definitions for @a Device instances. + */ + +// ebusd enhanced protocol IDs: +#define ENH_REQ_INIT ((uint8_t)0x0) +#define ENH_RES_RESETTED ((uint8_t)0x0) +#define ENH_REQ_SEND ((uint8_t)0x1) +#define ENH_RES_RECEIVED ((uint8_t)0x1) +#define ENH_REQ_START ((uint8_t)0x2) +#define ENH_RES_STARTED ((uint8_t)0x2) +#define ENH_REQ_INFO ((uint8_t)0x3) +#define ENH_RES_INFO ((uint8_t)0x3) +#define ENH_RES_FAILED ((uint8_t)0xa) +#define ENH_RES_ERROR_EBUS ((uint8_t)0xb) +#define ENH_RES_ERROR_HOST ((uint8_t)0xc) + +// ebusd enhanced error codes for the ENH_RES_ERROR_* responses +#define ENH_ERR_FRAMING ((uint8_t)0x00) +#define ENH_ERR_OVERRUN ((uint8_t)0x01) + +#define ENH_BYTE_FLAG ((uint8_t)0x80) +#define ENH_BYTE_MASK ((uint8_t)0xc0) +#define ENH_BYTE1 ((uint8_t)0xc0) +#define ENH_BYTE2 ((uint8_t)0x80) +#define makeEnhancedByte1(cmd, data) (uint8_t)(ENH_BYTE1 | ((cmd) << 2) | (((data)&0xc0) >> 6)) +#define makeEnhancedByte2(cmd, data) (uint8_t)(ENH_BYTE2 | ((data)&0x3f)) +#define makeEnhancedSequence(cmd, data) {makeEnhancedByte1(cmd, data), makeEnhancedByte2(cmd, data)} + + +/** + * Interface for an enhanced @a Device. + */ +class EnhancedDeviceInterface { + public: + /** + * Destructor. + */ + virtual ~EnhancedDeviceInterface() {} + + /** + * Check for a running extra infos request, wait for it to complete, + * and then send a new request for extra infos to enhanced device. + * @param infoId the ID of the info to request. + * @param wait true to wait for a running request to complete, false to send right away. + * @return @a RESULT_OK on success, or an error code otherwise. + */ + virtual result_t requestEnhancedInfo(symbol_t infoId, bool wait = true) = 0; // abstract + + /** + * Get the enhanced device version. + * @return @a a string with the version infos, or empty. + */ + virtual string getEnhancedVersion() const = 0; // abstract + + /** + * Retrieve/update all extra infos from an enhanced device. + * @return @a a string with the extra infos, or empty. + */ + virtual string getEnhancedInfos() = 0; // abstract +}; + +} // namespace ebusd + +#endif // LIB_EBUS_DEVICE_ENHANCED_H_ diff --git a/src/lib/ebus/device_trans.cpp b/src/lib/ebus/device_trans.cpp new file mode 100755 index 000000000..d23a09958 --- /dev/null +++ b/src/lib/ebus/device_trans.cpp @@ -0,0 +1,654 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2015-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "lib/ebus/device_trans.h" +#include +#include +#include +#include "lib/utils/clock.h" + +namespace ebusd { + +using std::hex; +using std::dec; +using std::setfill; +using std::setw; +using std::setprecision; +using std::fixed; + + + +result_t BaseDevice::startArbitration(symbol_t masterAddress) { + if (m_arbitrationCheck) { + if (masterAddress != SYN) { + return RESULT_ERR_ARB_RUNNING; // should not occur + } + return RESULT_OK; + } + m_arbitrationMaster = masterAddress; + return RESULT_OK; +} + +bool BaseDevice::cancelRunningArbitration(ArbitrationState* arbitrationState) { + if (m_arbitrationMaster == SYN) { + return false; + } + if (arbitrationState) { + *arbitrationState = as_error; + } + m_arbitrationMaster = SYN; + m_arbitrationCheck = 0; + return true; +} + + +result_t PlainDevice::send(symbol_t value) { + result_t result = m_transport->write(&value, 1); + if (result == RESULT_OK && m_listener != nullptr) { + m_listener->notifyDeviceData(&value, 1, false); + } + return result; +} + +result_t PlainDevice::recv(unsigned int timeout, symbol_t* value, ArbitrationState* arbitrationState) { + if (m_arbitrationMaster != SYN && arbitrationState) { + *arbitrationState = as_running; + } + uint64_t until = timeout == 0 ? 0 : clockGetMillis() + timeout + m_transport->getLatency(); + const uint8_t* data = nullptr; + size_t len = 0; + result_t result; + do { + result = m_transport->read(timeout, &data, &len); + if (result == RESULT_OK) { + break; + } + if (result != RESULT_ERR_TIMEOUT) { + cancelRunningArbitration(arbitrationState); + return result; + } + if (timeout == 0) { + break; + } + uint64_t now = clockGetMillis(); + if (timeout == 0 || now >= until) { + break; + } + timeout = static_cast(until - now); + } while (true); + if (result == RESULT_OK && len > 0 && data) { + *value = *data; + m_transport->readConsumed(1); + if (m_listener != nullptr) { + m_listener->notifyDeviceData(value, 1, true); + } + if (len > 1) { + result = RESULT_CONTINUE; + } + if (*value != SYN || m_arbitrationMaster == SYN || m_arbitrationCheck) { + if (m_arbitrationMaster != SYN && arbitrationState) { + if (m_arbitrationCheck) { + *arbitrationState = *value == m_arbitrationMaster ? as_won : as_lost; + m_arbitrationMaster = SYN; + m_arbitrationCheck = 0; + } else { + *arbitrationState = m_arbitrationMaster == SYN ? as_none : as_start; + } + } + return result; + } + if (len == 1 && arbitrationState) { + // arbitration executed by ebusd itself + bool wrote = m_transport->write(&m_arbitrationMaster, 1) == RESULT_OK; // send as fast as possible + if (!wrote) { + cancelRunningArbitration(arbitrationState); + return result; + } + if (m_listener != nullptr) { + m_listener->notifyDeviceData(&m_arbitrationMaster, 1, false); + } + m_arbitrationCheck = 1; + *arbitrationState = as_running; + } + } + return result; +} + + +/** the features requested. */ +#define REQUEST_FEATURES 0x01 + +void EnhancedDevice::formatInfo(ostringstream* ostream, bool verbose, bool prefix) { + BaseDevice::formatInfo(ostream, verbose, prefix); + if (prefix) { + *ostream << ", enhanced"; + return; + } + bool infoAdded = false; + if (verbose) { + string info = getEnhancedInfos(); + if (!info.empty()) { + *ostream << ", " << info; + infoAdded = true; + } + } + if (!infoAdded) { + string ver = getEnhancedVersion(); + if (!ver.empty()) { + *ostream << ", firmware " << ver; + } + } +} + +void EnhancedDevice::formatInfoJson(ostringstream* ostream) { + string ver = getEnhancedVersion(); + if (!ver.empty()) { + *ostream << ",\"dv\":\"" << ver << "\""; + if (m_enhInfoIdRequestNeeded) { + if (!m_enhInfoIdRequested) { + result_t ret = requestEnhancedInfo(1); + if (ret == RESULT_OK) { + requestEnhancedInfo(0xff); + } + } + if (!m_enhInfoId.empty()) { + *ostream << ",\"di\":\"" << m_enhInfoId << "\""; + } + } + } +} + +result_t EnhancedDevice::requestEnhancedInfo(symbol_t infoId, bool wait) { + if (m_extraFeatures == 0) { + return RESULT_ERR_INVALID_ARG; + } + if (wait) { + for (unsigned int i = 0; i < 4; i++) { + if (m_infoLen == 0) { + break; + } + usleep(40000 + i*40000); + } + if (m_infoLen > 0) { + if (m_infoReqTime > 0 && time(NULL) > m_infoReqTime+5) { + // request timed out + if (m_listener != nullptr) { + m_listener->notifyDeviceStatus(false, "info request timed out"); + } + m_infoLen = 0; + m_infoReqTime = 0; + } else { + return RESULT_ERR_DUPLICATE; + } + } + } + if (infoId == 0xff) { + // just waited for completion + return RESULT_OK; + } + uint8_t buf[] = makeEnhancedSequence(ENH_REQ_INFO, infoId); + result_t result = m_transport->write(buf, 2); + if (result == RESULT_OK) { + m_enhInfoIdRequested = m_enhInfoIdRequested || (infoId == 1); + m_infoBuf[0] = infoId; + m_infoLen = 1; + m_infoPos = 1; + time(&m_infoReqTime); + } else { + m_infoLen = 0; + m_infoPos = 0; + } + return result; +} + +string EnhancedDevice::getEnhancedInfos() { + if (m_extraFeatures == 0) { + return ""; + } + result_t res; + string fails = ""; + if (m_enhInfoTemperature.empty()) { // use empty temperature for potential refresh after reset + res = requestEnhancedInfo(0); + if (res != RESULT_OK) { + return "cannot request version"; + } + res = requestEnhancedInfo(1); + if (res != RESULT_OK) { + return "cannot request ID"; + } + res = requestEnhancedInfo(2); + if (res != RESULT_OK) { + fails += ", cannot request config"; + requestEnhancedInfo(0xff); // wait for completion + m_infoLen = 0; // cancel anyway + } + } + res = requestEnhancedInfo(6); + if (res != RESULT_OK) { + return "cannot request reset info"; + } + res = requestEnhancedInfo(3); + if (res != RESULT_OK) { + return "cannot request temperature"; + } + res = requestEnhancedInfo(4); + if (res != RESULT_OK) { + return "cannot request supply voltage"; + } + res = requestEnhancedInfo(5); + if (res != RESULT_OK) { + fails += ", cannot request bus voltage"; + } + if (m_enhInfoIsWifi) { + res = requestEnhancedInfo(7); + if (res != RESULT_OK) { + fails += ", cannot request rssi"; + } + } + res = requestEnhancedInfo(0xff); // wait for completion + if (res != RESULT_OK) { + m_enhInfoBusVoltage = "bus voltage unknown"; + m_infoLen = 0; // cancel anyway + } + return "firmware " + m_enhInfoVersion + ", " + m_enhInfoTemperature + ", " + m_enhInfoSupplyVoltage + ", " + + m_enhInfoBusVoltage; +} + +result_t EnhancedDevice::send(symbol_t value) { + uint8_t buf[] = makeEnhancedSequence(ENH_REQ_SEND, value); + result_t result = m_transport->write(buf, 2); + if (result == RESULT_OK && m_listener != nullptr) { + m_listener->notifyDeviceData(&value, 1, false); + } + return result; +} + +result_t EnhancedDevice::recv(unsigned int timeout, symbol_t* value, ArbitrationState* arbitrationState) { + if (arbitrationState && m_arbitrationMaster != SYN) { + *arbitrationState = as_running; + } + uint64_t until = timeout == 0 ? 0 : clockGetMillis() + timeout + m_transport->getLatency(); + const uint8_t* data = nullptr; + size_t len = 0; + result_t result; + do { + result = m_transport->read(timeout, &data, &len); + if (result == RESULT_OK) { + result = handleEnhancedBufferedData(data, len, value, arbitrationState); + if (result >= RESULT_OK) { + break; + } + } + if (result != RESULT_ERR_TIMEOUT) { + cancelRunningArbitration(arbitrationState); + return result; + } + if (timeout == 0) { + break; + } + uint64_t now = clockGetMillis(); + if (timeout == 0 || now >= until) { + break; + } + timeout = static_cast(until - now); + } while (true); + return result; +} + +result_t EnhancedDevice::startArbitration(symbol_t masterAddress) { + if (m_arbitrationCheck) { + if (masterAddress != SYN) { + return RESULT_ERR_ARB_RUNNING; // should not occur + } + if (!cancelRunningArbitration(nullptr)) { + return RESULT_ERR_SEND; + } + return RESULT_OK; + } + m_arbitrationMaster = masterAddress; + if (masterAddress != SYN) { + uint8_t buf[] = makeEnhancedSequence(ENH_REQ_START, masterAddress); + result_t result = m_transport->write(buf, 2); + if (result != RESULT_OK) { + m_arbitrationMaster = SYN; + return result; + } + m_arbitrationCheck = 1; + } + return RESULT_OK; +} + +bool EnhancedDevice::cancelRunningArbitration(ArbitrationState* arbitrationState) { + if (!BaseDevice::cancelRunningArbitration(arbitrationState)) { + return false; + } + symbol_t buf[2] = makeEnhancedSequence(ENH_REQ_START, SYN); + return m_transport->write(buf, 2) == RESULT_OK; +} + +result_t EnhancedDevice::notifyTransportStatus(bool opened) { + result_t result = BaseDevice::notifyTransportStatus(opened); // always OK + if (opened) { + symbol_t buf[2] = makeEnhancedSequence(ENH_REQ_INIT, REQUEST_FEATURES); // extra feature: info + result = m_transport->write(buf, 2); + if (result != RESULT_OK) { + return result; + } + m_resetTime = time(NULL); + m_resetRequested = true; + } else { + // reset state + m_resetTime = 0; + m_extraFeatures = 0; + m_infoLen = 0; + m_enhInfoVersion = ""; + m_enhInfoIsWifi = false; + m_enhInfoIdRequestNeeded = false; + m_enhInfoIdRequested = false; + m_enhInfoId = ""; + m_enhInfoTemperature = ""; + m_enhInfoSupplyVoltage = ""; + m_enhInfoBusVoltage = ""; + m_arbitrationMaster = SYN; + m_arbitrationCheck = 0; + } + return result; +} + +result_t EnhancedDevice::handleEnhancedBufferedData(const uint8_t* data, size_t len, +symbol_t* value, ArbitrationState* arbitrationState) { + bool valueSet = false; + bool sent = false; + bool more = false; + size_t pos; + for (pos = 0; pos < len; pos++) { + symbol_t ch = data[pos]; + if (!(ch&ENH_BYTE_FLAG)) { + if (valueSet) { + more = true; + break; + } + *value = ch; + valueSet = true; + continue; + } + uint8_t kind = ch&ENH_BYTE_MASK; + if (kind == ENH_BYTE1 && len < pos + 2) { + break; // transfer not complete yet + } + if (kind == ENH_BYTE2) { + if (m_listener != nullptr) { + m_listener->notifyDeviceStatus(true, "unexpected enhanced byte 2"); + } + continue; + } + // kind is ENH_BYTE1 + pos++; + symbol_t ch2 = data[pos]; + if ((ch2 & ENH_BYTE_MASK) != ENH_BYTE2) { + if (m_listener != nullptr) { + m_listener->notifyDeviceStatus(true, "missing enhanced byte 2"); + } + continue; + } + symbol_t data = (symbol_t)(((ch&0x03) << 6) | (ch2&0x3f)); + symbol_t cmd = (ch >> 2)&0xf; + switch (cmd) { + case ENH_RES_STARTED: + case ENH_RES_FAILED: + if (valueSet) { + more = true; + pos--; // keep ENH_BYTE1 for later run + len = 0; // abort outer loop + break; + } + sent = cmd == ENH_RES_STARTED; + if (arbitrationState) { + *arbitrationState = sent ? as_won : as_lost; + } + m_arbitrationMaster = SYN; + m_arbitrationCheck = 0; + *value = data; + valueSet = true; + break; + case ENH_RES_RECEIVED: + if (valueSet) { + more = true; + pos--; // keep ENH_BYTE1 for later run + len = 0; // abort outer loop + break; + } + *value = data; + if (data == SYN && arbitrationState && *arbitrationState == as_running && m_arbitrationCheck) { + if (m_arbitrationCheck < 3) { // wait for three SYN symbols before switching to timeout + m_arbitrationCheck++; + } else { + *arbitrationState = as_timeout; + m_arbitrationMaster = SYN; + m_arbitrationCheck = 0; + } + } + valueSet = true; + break; + case ENH_RES_RESETTED: + if (arbitrationState && *arbitrationState != as_none) { + *arbitrationState = as_error; + m_arbitrationMaster = SYN; + m_arbitrationCheck = 0; + } + m_enhInfoTemperature = ""; + m_enhInfoSupplyVoltage = ""; + m_enhInfoBusVoltage = ""; + m_infoLen = 0; + if (!m_resetRequested && m_resetTime+3 >= time(NULL)) { + if (data == m_extraFeatures) { + // skip explicit response to init request + valueSet = false; + break; + } + // response to init request had different feature flags + m_resetRequested = true; + } + m_extraFeatures = data; + if (m_listener != nullptr) { + m_listener->notifyDeviceStatus(false, (m_extraFeatures&0x01) ? "reset, supports info" : "reset"); + } + if (m_resetRequested) { + m_resetRequested = false; + if (m_extraFeatures&0x01) { + requestEnhancedInfo(0, false); // request version, ignore result + } + valueSet = false; + break; + } + m_transport->close(); // on self-reset of device close and reopen it to have a clean startup + cancelRunningArbitration(arbitrationState); + break; + case ENH_RES_INFO: + if (m_infoLen == 1) { + m_infoLen = data+1; + } else if (m_infoLen && m_infoPos < m_infoLen && m_infoPos < sizeof(m_infoBuf)) { + m_infoBuf[m_infoPos++] = data; + if (m_infoPos >= m_infoLen) { + notifyInfoRetrieved(); + m_infoLen = 0; + } + } else { + m_infoLen = 0; // reset on invalid response + } + break; + case ENH_RES_ERROR_EBUS: + case ENH_RES_ERROR_HOST: + if (m_listener != nullptr) { + ostringstream stream; + stream << (cmd == ENH_RES_ERROR_EBUS ? "eBUS comm error: " : "host comm error: "); + switch (data) { + case ENH_ERR_FRAMING: + stream << "framing"; + break; + case ENH_ERR_OVERRUN: + stream << "overrun"; + break; + default: + stream << "unknown 0x" << setw(2) << setfill('0') << hex << static_cast(data); + break; + } + string str = stream.str(); + m_listener->notifyDeviceStatus(true, str.c_str()); + } + cancelRunningArbitration(arbitrationState); + break; + default: + if (m_listener != nullptr) { + ostringstream stream; + stream << "unexpected enhanced command 0x" << setw(2) << setfill('0') << hex + << static_cast(cmd); + string str = stream.str(); + m_listener->notifyDeviceStatus(true, str.c_str()); + } + len = 0; // abort outer loop + break; + } + if (len == 0) { + break; // abort received + } + } + m_transport->readConsumed(pos); + if (valueSet && m_listener != nullptr) { + m_listener->notifyDeviceData(value, 1, !sent); + } + return more ? RESULT_CONTINUE : valueSet ? RESULT_OK : RESULT_ERR_TIMEOUT; +} + +void EnhancedDevice::notifyInfoRetrieved() { + symbol_t id = m_infoBuf[0]; + symbol_t* data = m_infoBuf+1; + size_t len = m_infoLen-1; + unsigned int val; + ostringstream stream; + switch ((len << 8) | id) { + case 0x0200: + case 0x0500: // with firmware version and jumper info + case 0x0800: // with firmware version, jumper info, and bootloader version + stream << hex << static_cast(data[1]) // features mask + << "." << static_cast(data[0]); // version minor + if (len >= 5) { + stream << "[" << setfill('0') << setw(2) << hex << static_cast(data[2]) + << setw(2) << static_cast(data[3]) << "]"; + } + if (len >= 8) { + stream << "." << dec << static_cast(data[5]); + stream << "[" << setfill('0') << setw(2) << hex << static_cast(data[6]) + << setw(2) << static_cast(data[7]) << "]"; + } + m_enhInfoIdRequestNeeded = len >= 8 && data[0] == data[5] && data[2] == data[6] && data[3] == data[7]; + m_enhInfoVersion = stream.str(); + stream.str(" "); + stream << "firmware " << m_enhInfoVersion; + if (len >= 5) { + stream << ", jumpers 0x" << setw(2) << static_cast(data[4]); + m_enhInfoIsWifi = (data[4]&0x08) != 0; + } + stream << setfill(' '); // reset + break; + case 0x0901: + case 0x0802: + case 0x0302: + stream << (id == 1 ? "ID " : "config "); + stream << hex << setfill('0'); + for (size_t pos = 0; pos < len; pos++) { + stream << setw(2) << static_cast(data[pos]); + } + if (id == 1 && data[8] == 0) { + m_enhInfoId = stream.str().substr(3); + } + if (id == 2 && (data[2]&0x3f) != 0x3f) { + // non-default arbitration delay + val = (data[2]&0x3f)*10; // steps of 10us + stream << ", arbitration delay " << dec << static_cast(val) << " us"; + } + break; + case 0x0203: + val = (static_cast(data[0]) << 8) | static_cast(data[1]); + stream << "temperature " << static_cast(val) << " °C"; + m_enhInfoTemperature = stream.str(); + break; + case 0x0204: + stream << "supply voltage "; + if (data[0] | data[1]) { + val = (static_cast(data[0]) << 8) | static_cast(data[1]); + stream << static_cast(val) << " mV"; + } else { + stream << "unknown"; + } + m_enhInfoSupplyVoltage = stream.str(); + break; + case 0x0205: + stream << "bus voltage "; + if (data[0] | data[1]) { + stream << fixed << setprecision(1) + << static_cast(data[1] / 10.0) << " V - " + << static_cast(data[0] / 10.0) << " V"; + } else { + stream << "unknown"; + } + m_enhInfoBusVoltage = stream.str(); + break; + case 0x0206: + stream << "reset cause "; + if (data[0]) { + stream << static_cast(data[0]) << "="; + switch (data[0]) { + case 1: stream << "power-on"; break; + case 2: stream << "brown-out"; break; + case 3: stream << "watchdog"; break; + case 4: stream << "clear"; break; + case 5: stream << "reset"; break; + case 6: stream << "stack"; break; + case 7: stream << "memory"; break; + default: stream << "other"; break; + } + stream << ", restart count " << static_cast(data[1]); + } else { + stream << "unknown"; + } + break; + case 0x0107: + stream << "rssi "; + if (data[0]) { + stream << static_cast(reinterpret_cast(data)[0]) << " dBm"; + } else { + stream << "unknown"; + } + break; + default: + stream << "unknown 0x" << hex << setfill('0') << setw(2) + << static_cast(id) << ", len " << dec << setw(0) + << static_cast(len); + break; + } + if (m_listener != nullptr) { + m_listener->notifyDeviceStatus(false, ("extra info: "+stream.str()).c_str()); + } +} + +} // namespace ebusd diff --git a/src/lib/ebus/device_trans.h b/src/lib/ebus/device_trans.h new file mode 100755 index 000000000..c3b9a01ea --- /dev/null +++ b/src/lib/ebus/device_trans.h @@ -0,0 +1,242 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2015-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIB_EBUS_DEVICE_TRANS_H_ +#define LIB_EBUS_DEVICE_TRANS_H_ + +#include +#include +#include "lib/ebus/device.h" +#include "lib/ebus/device_enhanced.h" +#include "lib/ebus/transport.h" +#include "lib/ebus/result.h" +#include "lib/ebus/symbol.h" + +namespace ebusd { + +/** @file lib/ebus/device_trans.h + * Classes providing access to the eBUS via a @a Transport instance. + */ + +/** + * The base class for accessing an eBUS via a @a Transport instance. + */ +class BaseDevice : public Device, public TransportListener { + protected: + /** + * Construct a new instance. + * @param transport the @a Transport to use. + */ + explicit BaseDevice(Transport* transport) + : Device(), m_transport(transport), + m_arbitrationMaster(SYN), m_arbitrationCheck(0) { + transport->setListener(this); + } + + public: + /** + * Destructor. + */ + virtual ~BaseDevice() { + if (m_transport) { + delete m_transport; + m_transport = nullptr; + } + } + + // @copydoc + virtual const char* getName() const { return m_transport->getName(); } + + // @copydoc + virtual void formatInfo(ostringstream* output, bool verbose, bool prefix) { + if (prefix) { + *output << m_transport->getName() << ", " << m_transport->getTransportInfo(); + } else if (!m_transport->isValid()) { + *output << ", invalid"; + } + } + + // @copydoc + virtual void formatInfoJson(ostringstream* output) {} + + // @copydoc + virtual result_t notifyTransportStatus(bool opened) { + m_listener->notifyDeviceStatus(!opened, opened ? "transport opened" : "transport closed"); + return RESULT_OK; + } + + // @copydoc + virtual void notifyTransportMessage(bool error, const char* message) { + m_listener->notifyDeviceStatus(error, message); + } + + // @copydoc + virtual result_t open() { return m_transport->open(); } + + // @copydoc + virtual bool isValid() { return m_transport->isValid(); } + + // @copydoc + virtual result_t startArbitration(symbol_t masterAddress); + + // @copydoc + virtual bool isArbitrating() const { return m_arbitrationMaster != SYN; } + + // @copydoc + virtual bool cancelRunningArbitration(ArbitrationState* arbitrationState); + + protected: + /** the @a Transport to use. */ + Transport* m_transport; + + /** the arbitration master address to send when in arbitration, or @a SYN. */ + symbol_t m_arbitrationMaster; + + /** >0 when in arbitration and the next received symbol needs to be checked against the sent master address, + * incremented with each received SYN when arbitration was not performed as expected and needs to be stopped. */ + size_t m_arbitrationCheck; +}; + + +class PlainDevice : public BaseDevice { + public: + /** + * Construct a new instance. + * @param transport the @a Transport to use. + */ + explicit PlainDevice(Transport* transport) + : BaseDevice(transport) { + } + + // @copydoc + result_t send(symbol_t value) override; + + // @copydoc + result_t recv(unsigned int timeout, symbol_t* value, ArbitrationState* arbitrationState) override; +}; + + +class EnhancedDevice : public BaseDevice, public EnhancedDeviceInterface { + public: + /** + * Construct a new instance. + * @param transport the @a Transport to use. + */ + explicit EnhancedDevice(Transport* transport) + : BaseDevice(transport), EnhancedDeviceInterface(), m_resetTime(0), m_resetRequested(false), + m_extraFeatures(0), m_infoReqTime(0), m_infoLen(0), m_infoPos(0), m_enhInfoIsWifi(false), + m_enhInfoIdRequestNeeded(false), m_enhInfoIdRequested(false) { + } + + // @copydoc + void formatInfo(ostringstream* output, bool verbose, bool prefix) override; + + // @copydoc + void formatInfoJson(ostringstream* output) override; + + // @copydoc + result_t send(symbol_t value) override; + + // @copydoc + result_t recv(unsigned int timeout, symbol_t* value, ArbitrationState* arbitrationState) override; + + // @copydoc + result_t startArbitration(symbol_t masterAddress) override; + + // @copydoc + virtual result_t notifyTransportStatus(bool opened); + + // @copydoc + bool supportsUpdateCheck() const override { return m_extraFeatures & 0x01; } + + // @copydoc + virtual result_t requestEnhancedInfo(symbol_t infoId, bool wait = true); + + // @copydoc + virtual string getEnhancedVersion() const { return m_enhInfoVersion; } + + // @copydoc + virtual string getEnhancedInfos(); + + // @copydoc + virtual bool cancelRunningArbitration(ArbitrationState* arbitrationState); + + private: + /** + * Handle the already buffered enhanced data. + * @param value the reference in which the read byte value is stored. + * @param arbitrationState the variable in which to store the current/received arbitration state. + * @return the @a result_t code, especially RESULT_CONTINE if the value was set and more data is available immediately. + */ + result_t handleEnhancedBufferedData(const uint8_t* data, size_t len, symbol_t* value, + ArbitrationState* arbitrationState); + + /** + * Called when reception of an info ID was completed. + */ + void notifyInfoRetrieved(); + + /** the time when the transport was resetted. */ + time_t m_resetTime; + + /** whether the reset of the device was already requested. */ + bool m_resetRequested; + + /** the extra features supported by the device. */ + symbol_t m_extraFeatures; + + /** the time of the last info request. */ + time_t m_infoReqTime; + + /** the info buffer expected length. */ + size_t m_infoLen; + + /** the info buffer write position. */ + size_t m_infoPos; + + /** the info buffer. */ + symbol_t m_infoBuf[16+1]; + + /** a string describing the enhanced device version. */ + string m_enhInfoVersion; + + /** whether the device is known to be connected via WIFI. */ + bool m_enhInfoIsWifi; + + /** whether the device ID request is needed. */ + bool m_enhInfoIdRequestNeeded; + + /** whether the device ID was already requested. */ + bool m_enhInfoIdRequested; + + /** a string with the ID of the enhanced device. */ + string m_enhInfoId; + + /** a string describing the enhanced device temperature. */ + string m_enhInfoTemperature; + + /** a string describing the enhanced device supply voltage. */ + string m_enhInfoSupplyVoltage; + + /** a string describing the enhanced device bus voltage. */ + string m_enhInfoBusVoltage; +}; + +} // namespace ebusd + +#endif // LIB_EBUS_DEVICE_TRANS_H_ diff --git a/src/lib/ebus/filereader.cpp b/src/lib/ebus/filereader.cpp index d6c4f04fb..d971ba6c5 100755 --- a/src/lib/ebus/filereader.cpp +++ b/src/lib/ebus/filereader.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,11 +32,9 @@ using std::ifstream; using std::ostringstream; using std::cout; using std::endl; -using std::setw; -using std::dec; -istream* FileReader::openFile(const string& filename, string* errorDescription, time_t* time) { +istream* FileReader::openFile(const string& filename, string* errorDescription, time_t* time, bool *isLink) { struct stat st; if (stat(filename.c_str(), &st) != 0) { *errorDescription = filename; @@ -56,6 +54,9 @@ istream* FileReader::openFile(const string& filename, string* errorDescription, if (time) { *time = st.st_mtime; } + if (isLink) { + *isLink = lstat(filename.c_str(), &st) == 0 && S_ISLNK(st.st_mode); + } return stream; } diff --git a/src/lib/ebus/filereader.h b/src/lib/ebus/filereader.h index d34a8db48..b0ff26acd 100755 --- a/src/lib/ebus/filereader.h +++ b/src/lib/ebus/filereader.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,6 +44,10 @@ using std::string; using std::map; using std::ostream; using std::istream; +using std::hex; +using std::dec; +using std::setw; +using std::setfill; /** the separator character used between fields. */ #define FIELD_SEPARATOR ',' @@ -80,9 +84,10 @@ class FileReader { * @param filename the name of the file being read. * @param errorDescription a string in which to store the error description in case of error. * @param time optional pointer to a @a time_t value for storing the modification time of the file, or nullptr. + * @param isLink optional pointer for strong whether the file is a link, or nullptr. * @return the opened @a istream on success, or nullptr on error. */ - static istream* openFile(const string& filename, string* errorDescription, time_t* time = nullptr); + static istream* openFile(const string& filename, string* errorDescription, time_t* time = nullptr, bool *isLink = nullptr); /** * Read the definitions from a stream. @@ -174,7 +179,7 @@ class FileReader { * @param stream the @a ostream to write to. */ static void formatHash(size_t hash, ostream* stream) { - *stream << std::hex << std::setw(8) << std::setfill('0') << (hash & 0xffffffff) << std::dec << std::setw(0); + *stream << hex << setw(8) << setfill('0') << (hash & 0xffffffff) << dec << setw(0); } /** @@ -222,6 +227,11 @@ class MappedFileReader : public FileReader { */ static const string normalizeLanguage(const string& lang); + /** + * @return the preferred language code (up to 2 characters), or empty. + */ + const string getPreferLanguage() const { return m_preferLanguage; } + // @copydoc result_t readFromStream(istream* stream, const string& filename, const time_t& mtime, bool verbose, map* defaults, string* errorDescription, bool replace = false, size_t* hash = nullptr, diff --git a/src/lib/ebus/message.cpp b/src/lib/ebus/message.cpp index 36d009fed..7ab2fb67e 100644 --- a/src/lib/ebus/message.cpp +++ b/src/lib/ebus/message.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -70,11 +70,11 @@ using std::endl; #define POLL_PRIORITY_CONDITION 5 /** the field name constant for the message level. */ -static const char* FIELNAME_LEVEL = "level"; +static const char* FIELDNAME_LEVEL = "level"; /** the known full length field names. */ static const char* knownFieldNamesFull[] = { - "type", "circuit", FIELNAME_LEVEL, "name", "comment", "qq", "zz", "pbsb", "id", "fields", + "type", "circuit", FIELDNAME_LEVEL, "name", "comment", "qq", "zz", "pbsb", "id", "fields", }; /** the known full length field names. */ @@ -94,11 +94,6 @@ static const char* defaultMessageFieldMap[] = { // access level not included in /** the m_pollOrder of the last polled message. */ static unsigned int g_lastPollOrder = 0; -extern DataFieldTemplates* getTemplates(const string& filename); - -extern result_t loadDefinitionsFromConfigPath(FileReader* reader, const string& filename, bool verbose, - map* defaults, string* errorDescription, bool replace = false); - Message::Message(const string& filename, const string& circuit, const string& level, const string& name, bool isWrite, bool isPassive, const map& attributes, @@ -114,9 +109,9 @@ Message::Message(const string& filename, const string& circuit, const string& le m_id(id), m_key(createKey(id, isWrite, isPassive, srcAddress, dstAddress)), m_data(data), m_deleteData(deleteData), m_pollPriority(pollPriority), - m_usedByCondition(false), m_isScanMessage(false), m_condition(condition), + m_usedByCondition(false), m_isScanMessage(false), m_condition(condition), m_availableSinceTime(0), m_dataHandlerState(0), m_lastUpdateTime(0), m_lastChangeTime(0), m_pollOrder(0), m_lastPollTime(0) { - if (circuit == "scan") { + if (strcasecmp(circuit.c_str(), "scan") == 0) { setScanMessage(); m_pollPriority = 0; } @@ -134,7 +129,7 @@ Message::Message(const string& circuit, const string& level, const string& name, m_id({pb, sb}), m_key(createKey(pb, sb, broadcast)), m_data(data), m_deleteData(deleteData), m_pollPriority(0), - m_usedByCondition(false), m_isScanMessage(true), m_condition(nullptr), + m_usedByCondition(false), m_isScanMessage(true), m_condition(nullptr), m_availableSinceTime(0), m_lastUpdateTime(0), m_lastChangeTime(0), m_pollOrder(0), m_lastPollTime(0) { time(&m_createTime); } @@ -185,7 +180,7 @@ uint64_t Message::createKey(const vector& id, bool isWrite, bool isPas int exp = 5; for (const auto it : id) { key ^= (uint64_t)it << (8 * exp--); - if (exp == 0) { + if (exp < 0) { exp = 3; } } @@ -205,13 +200,13 @@ uint64_t Message::createKey(const MasterSymbolString& master, size_t maxIdLength } uint64_t key = (uint64_t)idLength << (8 * 7 + 5); key |= (uint64_t)getMasterNumber(master[0]) << (8 * 7); // QQ address for passive message - key |= (uint64_t)(anyDestination ? SYN : master[1]) << (8 * 6); // ZZ address + key |= (uint64_t)(anyDestination ? (symbol_t)SYN : master[1]) << (8 * 6); // ZZ address key |= (uint64_t)master[2] << (8 * 5); // PB key |= (uint64_t)master[3] << (8 * 4); // SB int exp = 3; for (size_t i = 0; i < idLength; i++) { key ^= (uint64_t)master.dataAt(i) << (8 * exp--); - if (exp == 0) { + if (exp < 0) { exp = 3; } } @@ -306,11 +301,19 @@ result_t Message::create(const string& filename, const DataFieldTemplates* templ *errorDescription = "circuit"; return RESULT_ERR_MISSING_ARG; // empty circuit } + if (!DataField::checkIdentifier(circuit, true)) { + *errorDescription = "circuit name "+circuit; + return RESULT_ERR_INVALID_ARG; // invalid circuit name + } string name = getDefault(pluck("name", row), defaults, "name", true, true); // name if (name.empty()) { *errorDescription = "name"; return RESULT_ERR_MISSING_ARG; // empty name } + if (!DataField::checkIdentifier(name)) { + *errorDescription = "name "+name; + return RESULT_ERR_INVALID_ARG; // invalid message name + } string comment = getDefault(pluck("comment", row), defaults, "comment", true); // [comment] if (!comment.empty()) { (*row)["comment"] = comment; @@ -465,7 +468,7 @@ result_t Message::create(const string& filename, const DataFieldTemplates* templ return result; } } - if (id.size() + data->getLength(pt_masterData, maxLength) > 2 + maxLength + if (id.size() + data->getLength(pt_masterData, maxLength == MAX_POS ? MAX_POS-id.size() : maxLength) > 2 + maxLength || data->getLength(pt_slaveData, maxLength) > maxLength) { // max NN exceeded delete data; @@ -657,6 +660,19 @@ bool Message::isAvailable() { return (m_condition == nullptr) || m_condition->isTrue(); } +time_t Message::getAvailableSinceTime() { + if (m_condition == nullptr) { + return m_createTime; + } + if (!m_condition->isTrue()) { + return 0; + } + if (m_availableSinceTime == 0) { // was not yet available + m_availableSinceTime = m_condition->getLastCheckTime(); + } + return m_availableSinceTime; +} + bool Message::hasField(const char* fieldName, bool numeric) const { return m_data->hasField(fieldName, numeric); } @@ -761,36 +777,25 @@ result_t Message::storeLastData(size_t index, const SlaveSymbolString& data) { return RESULT_OK; } -result_t Message::decodeLastData(bool master, bool leadingSeparator, const char* fieldName, - ssize_t fieldIndex, OutputFormat outputFormat, ostream* output) const { - result_t result; - if (master) { - result = m_data->read(m_lastMasterData, getIdLength(), leadingSeparator, fieldName, fieldIndex, - outputFormat, -1, output); - } else { - result = m_data->read(m_lastSlaveData, 0, leadingSeparator, fieldName, fieldIndex, - outputFormat, -1, output); - } - if (result < RESULT_OK) { - return result; - } - if (result == RESULT_EMPTY && (fieldName != nullptr || fieldIndex >= 0)) { - return RESULT_ERR_NOTFOUND; - } - return result; -} - -result_t Message::decodeLastData(bool leadingSeparator, const char* fieldName, +result_t Message::decodeLastData(PartType part, bool leadingSeparator, const char* fieldName, ssize_t fieldIndex, const OutputFormat outputFormat, ostream* output) const { + if ((outputFormat & OF_RAWDATA) && !(outputFormat & OF_JSON)) { + *output << "[" << m_lastMasterData.getStr(2, 0, false) + << "/" << m_lastSlaveData.getStr(0, 0, false) + << "] "; + } ostream::pos_type startPos = output->tellp(); - result_t result = m_data->read(m_lastMasterData, getIdLength(), leadingSeparator, fieldName, fieldIndex, - outputFormat, -1, output); - if (result < RESULT_OK) { - return result; + result_t result = RESULT_EMPTY; + bool skipSlaveData = part == pt_masterData; + if (part == pt_any || skipSlaveData) { + result = m_data->read(m_lastMasterData, getIdLength(), leadingSeparator, fieldName, fieldIndex, + outputFormat, -1, output); + if (result < RESULT_OK) { + return result; + } } bool empty = result == RESULT_EMPTY; - bool skipSlaveData = false; - if (fieldIndex >= 0) { + if (!skipSlaveData && fieldIndex >= 0) { fieldIndex -= m_data->getCount(pt_masterData, fieldName); if (fieldIndex < 0) { skipSlaveData = true; @@ -859,7 +864,7 @@ void Message::dump(const vector* fieldNames, bool withConditions, Output bool first = true; if (fieldNames == nullptr) { for (const auto& fieldName : knownFieldNamesFull) { - if (fieldName == FIELNAME_LEVEL) { + if (fieldName == FIELDNAME_LEVEL) { continue; // access level not included in default dump format } if (first) { @@ -936,6 +941,9 @@ void Message::dumpField(const string& fieldName, bool withConditions, OutputForm for (auto it = m_id.begin()+2; it < m_id.end(); it++) { *output << hex << setw(2) << setfill('0') << static_cast(*it); } + if (outputFormat & OF_ALL_ATTRS) { + *output << "=" << hex << setw(0) << setfill('0') << static_cast(m_key); + } return; } if (fieldName == "fields") { @@ -945,8 +953,9 @@ void Message::dumpField(const string& fieldName, bool withConditions, OutputForm dumpAttribute(false, outputFormat, fieldName, output); } -void Message::decodeJson(bool leadingSeparator, bool appendDirectionCondition, bool withData, bool addRaw, - OutputFormat outputFormat, ostringstream* output) const { +void Message::decodeJson(bool leadingSeparator, bool appendDirectionCondition, bool withData, + OutputFormat outputFormat, ostringstream* output, + AddAttributes* addAttrs) const { outputFormat |= OF_JSON; if (leadingSeparator) { *output << ",\n"; @@ -975,9 +984,11 @@ void Message::decodeJson(bool leadingSeparator, bool appendDirectionCondition, b *output << ",\n \"pollprio\": " << setw(0) << dec << getPollPriority(); } if (isConditional()) { - *output << ",\n \"condition\": \""; - m_condition->dump(false, output); - *output << "\""; + *output << ",\n \"condition\": "; + m_condition->dumpJson(output); + } + if (addAttrs) { + addAttrs->addAttrsTo(this, output); } } if (withData) { @@ -990,25 +1001,20 @@ void Message::decodeJson(bool leadingSeparator, bool appendDirectionCondition, b } *output << ",\n \"zz\": " << dec << static_cast(m_dstAddress); if (withDefinition) { - *output << ",\n \"id\": [" << dec; - for (auto it = m_id.begin(); it < m_id.end(); it++) { - if (it > m_id.begin()) { - *output << ", "; - } - *output << dec << static_cast(*it); - } + *output << ",\n \"id\": ["; + dumpIdsJson(output); *output << "]"; } appendAttributes(outputFormat, output); if (hasData) { - if (addRaw) { + if (outputFormat & OF_RAWDATA) { m_lastMasterData.dumpJson(true, output); m_lastSlaveData.dumpJson(true, output); *output << dec; } size_t pos = (size_t)output->tellp(); *output << ",\n \"fields\": {"; - result_t dret = decodeLastData(false, nullptr, -1, outputFormat, output); + result_t dret = decodeLastData(pt_any, false, nullptr, -1, outputFormat, output); if (dret == RESULT_OK) { *output << "\n }"; } else { @@ -1027,6 +1033,15 @@ void Message::decodeJson(bool leadingSeparator, bool appendDirectionCondition, b *output << "\n }"; } +void Message::dumpIdsJson(ostringstream* output) const { + for (auto it = m_id.begin(); it < m_id.end(); it++) { + if (it > m_id.begin()) { + *output << ", "; + } + *output << dec << static_cast(*it); + } +} + bool Message::setDataHandlerState(int state, bool addBits) { if (addBits ? state == (m_dataHandlerState&state) : state == m_dataHandlerState) { return false; @@ -1301,6 +1316,22 @@ void ChainedMessage::dumpField(const string& fieldName, bool withConditions, Out } } +void ChainedMessage::dumpIdsJson(ostringstream* output) const { + for (auto idsit = m_ids.begin(); idsit < m_ids.end(); idsit++) { + if (idsit > m_ids.begin()) { + *output << ","; + } + *output << "["; + for (auto it = (*idsit).begin(); it < (*idsit).end(); it++) { + if (it > (*idsit).begin()) { + *output << ", "; + } + *output << dec << static_cast(*it); + } + *output << "]"; + } +} + /** * Get the first available @a Message from the list. @@ -1384,8 +1415,10 @@ result_t splitValues(const string& valueList, vector* valueRanges) valueRanges->push_back(0); } bool inclusive = str[1] == '='; - unsigned int val = parseInt(str.substr(inclusive?2:1).c_str(), 10, inclusive?0:1, - inclusive?UINT_MAX:(UINT_MAX-1), &result); + unsigned int val = parseInt(str.substr(inclusive?2:1).c_str(), 10, + inclusive || !upto ? 0 : 1, + inclusive || upto ? UINT_MAX : (UINT_MAX-1), + &result); if (result != RESULT_OK) { return result; } @@ -1532,6 +1565,27 @@ void SimpleCondition::dump(bool matched, ostream* output) const { } } +void SimpleCondition::dumpJson(ostream* output) const { + *output << "{\"name\": \"" << m_refName << "\""; + if (!m_circuit.empty()) { + *output << ",\"circuit\":\"" << m_circuit << "\""; + } + *output << ",\"message\":\"" << (m_name.empty() ? "scan" : m_name) << "\""; + if (m_dstAddress != SYN) { + *output << ",\"zz\":" << dec << static_cast(m_dstAddress); + } + if (!m_field.empty()) { + *output << ",\"field\":\"" << m_field << "\""; + } + if (m_hasValues) { + *output << ",\"value\":["; + dumpValuesJson(output); + *output << "]"; + } + *output << "}"; +} + + CombinedCondition* SimpleCondition::combineAnd(Condition* other) { CombinedCondition* ret = new CombinedCondition(); return ret->combineAnd(this)->combineAnd(other); @@ -1630,10 +1684,22 @@ bool SimpleNumericCondition::checkValue(const Message* message, const string& fi return false; } +void SimpleNumericCondition::dumpValuesJson(ostream* output) const { + bool first = true; + for (const auto value : m_valueRanges) { + if (!first) { + *output << ","; + } + *output << static_cast(value); + first = false; + } +} + bool SimpleStringCondition::checkValue(const Message* message, const string& field) { ostringstream output; - result_t result = message->decodeLastData(false, field.length() == 0 ? nullptr : field.c_str(), -1, OF_NONE, &output); + result_t result = message->decodeLastData(pt_any, false, field.length() == 0 ? nullptr : field.c_str(), -1, + OF_NONE, &output); if (result == RESULT_OK) { string value = output.str(); for (size_t i = 0; i < m_values.size(); i++) { @@ -1646,6 +1712,17 @@ bool SimpleStringCondition::checkValue(const Message* message, const string& fie return false; } +void SimpleStringCondition::dumpValuesJson(ostream* output) const { + bool first = true; + for (const auto& value : m_values) { + if (!first) { + *output << ","; + } + *output << "\"" << value << "\""; + first = false; + } +} + void CombinedCondition::dump(bool matched, ostream* output) const { for (const auto condition : m_conditions) { @@ -1653,6 +1730,19 @@ void CombinedCondition::dump(bool matched, ostream* output) const { } } +void CombinedCondition::dumpJson(ostream* output) const { + *output << "["; + bool first = true; + for (const auto condition : m_conditions) { + if (!first) { + *output << ","; + } + condition->dumpJson(output); + first = false; + } + *output << "]"; +} + result_t CombinedCondition::resolve(void (*readMessageFunc)(Message* message), MessageMap* messages, ostringstream* errorMessage) { for (const auto condition : m_conditions) { @@ -1738,7 +1828,11 @@ void Instruction::getDestination(ostringstream* ostream) const { result_t LoadInstruction::execute(MessageMap* messages, ostringstream* log) { string errorDescription; - result_t result = loadDefinitionsFromConfigPath(messages, m_filename, false, &m_defaults, &errorDescription); + Resolver* resolver = messages->getResolver(); + if (!resolver) { + return RESULT_ERR_MISSING_ARG; + } + result_t result = resolver->loadDefinitionsFromConfigPath(messages, m_filename, &m_defaults, &errorDescription); if (log->tellp() > 0) { *log << ", "; } @@ -2306,7 +2400,10 @@ result_t MessageMap::addFromFile(const string& filename, unsigned int lineNo, ma return RESULT_ERR_INVALID_ARG; } result = RESULT_ERR_EOF; - DataFieldTemplates* templates = getTemplates(filename); + if (!m_resolver) { + return result; + } + DataFieldTemplates* templates = m_resolver->getTemplates(filename); bool hasMulti = types.find(VALUE_SEPARATOR) != string::npos; istringstream stream(types); string type; @@ -2393,67 +2490,72 @@ result_t MessageMap::resolveCondition(void (*readMessageFunc)(Message* message), result_t MessageMap::executeInstructions(void (*readMessageFunc)(Message* message), ostringstream* log) { result_t overallResult = RESULT_OK; - vector remove; - for (auto& it : m_instructions) { - auto instructions = it.second; - bool removeSingletons = false; - vector remain; - for (const auto instruction : instructions) { - if (!m_addAll && removeSingletons && instruction->isSingleton()) { - delete instruction; - continue; - } - Condition* condition = instruction->getCondition(); - bool execute = m_addAll || condition == nullptr; - if (!execute) { - string errorDescription; - result_t result = resolveCondition(instruction->isSingleton()?readMessageFunc:nullptr, condition, - &errorDescription); - if (result != RESULT_OK) { - overallResult = result; - *log << "error resolving condition for \""; - instruction->getDestination(log); - *log << "\": " << getResultCode(result); - if (!errorDescription.empty()) { - *log << " " << errorDescription; - } - } else if (condition->isTrue()) { - execute = true; + size_t maxRounds = 3, cntBefore, cntAfter; + do { + cntBefore = cntAfter = m_instructions.size(); + vector remove; + for (auto& it : m_instructions) { + auto instructions = it.second; + bool removeSingletons = false; + vector remain; + for (const auto instruction : instructions) { + if (!m_addAll && removeSingletons && instruction->isSingleton()) { + delete instruction; + continue; } - } - if (execute) { - if (!m_addAll && instruction->isSingleton()) { - removeSingletons = true; + Condition* condition = instruction->getCondition(); + bool execute = m_addAll || condition == nullptr; + if (!execute) { + string errorDescription; + result_t result = resolveCondition(instruction->isSingleton()?readMessageFunc:nullptr, condition, + &errorDescription); + if (result != RESULT_OK) { + overallResult = result; + *log << "error resolving condition for \""; + instruction->getDestination(log); + *log << "\": " << getResultCode(result); + if (!errorDescription.empty()) { + *log << " " << errorDescription; + } + } else if (condition->isTrue()) { + execute = true; + } } - result_t result = instruction->execute(this, log); - if (result != RESULT_OK) { - overallResult = result; + if (execute) { + if (!m_addAll && instruction->isSingleton()) { + removeSingletons = true; + } + result_t result = instruction->execute(this, log); + if (result != RESULT_OK) { + overallResult = result; + } + delete instruction; + } else { + remain.push_back(instruction); } - delete instruction; - } else { - remain.push_back(instruction); } - } - if (removeSingletons && !remain.empty()) { - instructions = remain; - remain.clear(); - for (const auto instruction : instructions) { - if (!instruction->isSingleton()) { - remain.push_back(instruction); - continue; + if (removeSingletons && !remain.empty()) { + instructions = remain; + remain.clear(); + for (const auto instruction : instructions) { + if (!instruction->isSingleton()) { + remain.push_back(instruction); + continue; + } + delete instruction; } - delete instruction; + } + if (remain.empty()) { + remove.push_back(it.first); + } else { + it.second = remain; } } - if (remain.empty()) { - remove.push_back(it.first); - } else { - it.second = remain; + cntAfter = m_instructions.size(); + for (const auto& it : remove) { + m_instructions.erase(it); } - } - for (const auto& it : remove) { - m_instructions.erase(it); - } + } while (overallResult == RESULT_OK && cntAfter>cntBefore && --maxRounds>0); return overallResult; } @@ -2622,13 +2724,12 @@ Message* MessageMap::find(const MasterSymbolString& master, bool anyDestination, if (anyDestination && master.size() >= 5 && master[4] == 0 && master[2] == 0x07 && master[3] == 0x04) { return m_scanMessage; } - uint64_t baseKey = Message::createKey(master, - anyDestination || master[1] != BROADCAST ? m_maxIdLength : m_maxBroadcastIdLength, anyDestination); + size_t maxIdLength = anyDestination || master[1] != BROADCAST ? m_maxIdLength : m_maxBroadcastIdLength; + uint64_t baseKey = Message::createKey(master, maxIdLength, anyDestination); if (baseKey == INVALID_KEY) { return nullptr; } bool isWriteDest = isMaster(master[1]) || master[1] == BROADCAST; - size_t maxIdLength = Message::getKeyLength(baseKey); for (size_t idLength = maxIdLength; true; idLength--) { uint64_t key = baseKey; if (idLength == maxIdLength) { @@ -2638,7 +2739,7 @@ Message* MessageMap::find(const MasterSymbolString& master, bool anyDestination, int exp = 3; for (size_t i = 0; i < idLength; i++) { key ^= (uint64_t)master.dataAt(i) << (8 * exp--); - if (exp == 0) { + if (exp < 0) { exp = 3; } } @@ -2729,7 +2830,6 @@ void MessageMap::clear() { m_loadedFileInfos.clear(); // clear poll messages while (!m_pollMessages.empty()) { - m_pollMessages.top(); m_pollMessages.pop(); } // free message instances by name @@ -2811,13 +2911,17 @@ Message* MessageMap::getNextPoll() { return ret; } -void MessageMap::dump(bool withConditions, OutputFormat outputFormat, ostream* output) const { +void MessageMap::dump(bool withConditions, OutputFormat outputFormat, ostream* output, AddAttributes* addAttrs) const { bool first = true; bool isJson = (outputFormat & OF_JSON) != 0; if (isJson) { - *output << (m_addAll ? "[" : "}"); + *output << (m_addAll ? "[" : "{"); } else { Message::dumpHeader(nullptr, output); + if (m_addAll) { + *output << endl << "# max ID length: " << static_cast(m_maxIdLength) + << " (broadcast only: " << static_cast(m_maxBroadcastIdLength) << ")"; + } } if (!(outputFormat & OF_SHORT)) { *output << endl; @@ -2840,7 +2944,7 @@ void MessageMap::dump(bool withConditions, OutputFormat outputFormat, ostream* o } if (isJson) { ostringstream str; - message->decodeJson(false, false, false, false, outputFormat, &str); + message->decodeJson(false, false, false, outputFormat, &str, addAttrs); string add = str.str(); size_t pos = add.find('{'); *output << " {\n \"circuit\": \"" << message->getCircuit() << "\", " << add.substr(pos+1); @@ -2861,7 +2965,7 @@ void MessageMap::dump(bool withConditions, OutputFormat outputFormat, ostream* o } if (isJson) { ostringstream str; - message->decodeJson(!wasFirst, true, false, false, outputFormat, &str); + message->decodeJson(!wasFirst, true, false, outputFormat, &str); *output << str.str(); } else { message->dump(nullptr, withConditions, outputFormat, output); diff --git a/src/lib/ebus/message.h b/src/lib/ebus/message.h index 70c6c36f8..8aa89b245 100644 --- a/src/lib/ebus/message.h +++ b/src/lib/ebus/message.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,13 +58,13 @@ namespace ebusd { * template class. */ -using std::binary_function; using std::priority_queue; using std::deque; class Condition; class SimpleCondition; class CombinedCondition; +class AddAttributes; class MessageMap; @@ -159,13 +159,6 @@ class Message : public AttributedItem { */ static uint64_t createKey(symbol_t pb, symbol_t sb, bool broadcast); - /** - * Get the length field from the key. - * @param key the key. - * @return the length field from the key. - */ - static size_t getKeyLength(uint64_t key) { return (size_t)(key >> (8 * 7 + 5)); } - /** * Parse an ID part from the input @a string. * @param input the input @a string, hex digits optionally separated by space. @@ -400,6 +393,12 @@ class Message : public AttributedItem { */ bool isAvailable(); + /** + * Get the time when this (potentially conditional) message first became available. + * @return the time when this message first became available, or 0 if it is not available. + */ + time_t getAvailableSinceTime(); + /** * Return whether the field is available. * @param fieldName the name of the field to find, or nullptr for any. @@ -473,8 +472,8 @@ class Message : public AttributedItem { virtual result_t storeLastData(size_t index, const SlaveSymbolString& data); /** - * Decode the value from the last stored master or slave data. - * @param master true for decoding the master data, false for slave. + * Decode value(s) from the last stored data. + * @param part the part to decode. * @param leadingSeparator whether to prepend a separator before the formatted value. * @param fieldName the optional name of a field to limit the output to. * @param fieldIndex the optional index of the field to limit the output to (either named or overall), or -1. @@ -482,20 +481,8 @@ class Message : public AttributedItem { * @param output the @a ostream to append the formatted value to. * @return @a RESULT_OK on success, or an error code. */ - virtual result_t decodeLastData(bool master, bool leadingSeparator, const char* fieldName, - ssize_t fieldIndex, OutputFormat outputFormat, ostream* output) const; - - /** - * Decode the value from the last stored master and slave data. - * @param leadingSeparator whether to prepend a separator before the formatted value. - * @param fieldName the optional name of a field to limit the output to. - * @param fieldIndex the optional index of the field to limit the output to (either named or overall), or -1. - * @param outputFormat the @a OutputFormat options to use. - * @param output the @a ostream to append the formatted value to. - * @return @a RESULT_OK on success, or an error code. - */ - virtual result_t decodeLastData(bool leadingSeparator, const char* fieldName, - ssize_t fieldIndex, OutputFormat outputFormat, ostream* output) const; + virtual result_t decodeLastData(PartType part, bool leadingSeparator, const char* fieldName, + ssize_t fieldIndex, const OutputFormat outputFormat, ostream* output) const; /** * Decode a particular numeric field value from the last stored data. @@ -594,14 +581,21 @@ class Message : public AttributedItem { * @param leadingSeparator whether to prepend a separator before the first value. * @param appendDirectionCondition whether to append the direction and condition to the name key. * @param withData whether to add the last data as well. - * @param addRaw whether to add the raw symbols as well. * @param outputFormat the @a OutputFormat options to use. * @param output the @a ostringstream to append the decoded value(s) to. + * @param addAttrs an @a AddAttributes instance to use as well, or nullptr. */ - virtual void decodeJson(bool leadingSeparator, bool appendDirectionCondition, bool withData, bool addRaw, - OutputFormat outputFormat, ostringstream* output) const; + virtual void decodeJson(bool leadingSeparator, bool appendDirectionCondition, bool withData, + OutputFormat outputFormat, ostringstream* output, + AddAttributes* addAttrs = nullptr) const; protected: + /** + * Dump the ID(s) to the output in JSON format. + * @param output the @a ostringstream to append to. + */ + virtual void dumpIdsJson(ostringstream* output) const; + /** the source filename. */ const string m_filename; @@ -668,6 +662,9 @@ class Message : public AttributedItem { /** the @a Condition for this message, or nullptr. */ Condition* m_condition; + /** the time when the @a Condition first became available, or 0. */ + time_t m_availableSinceTime; + /** the last seen @a MasterSymbolString. */ MasterSymbolString m_lastMasterData; @@ -749,6 +746,8 @@ class ChainedMessage : public Message { result_t prepareMasterPart(size_t index, const char separator, istringstream* input, MasterSymbolString* master) override; + // @copydoc + void dumpIdsJson(ostringstream* output) const override; public: // @copydoc @@ -885,6 +884,18 @@ class Condition { */ virtual void dump(bool matched, ostream* output) const = 0; + /** + * Write the condition definition in JSON to the @a ostream. + * @param output the @a ostream to append to. + */ + virtual void dumpJson(ostream* output) const = 0; + + /** + * Write the values part of the condition definition in JSON to the @a ostream. + * @param output the @a ostream to append to. + */ + virtual void dumpValuesJson(ostream* output) const { /* empty on top level*/ } + /** * Combine this condition with another instance using a logical and. * @param other the @a Condition to combine with. @@ -908,6 +919,12 @@ class Condition { */ virtual bool isTrue() = 0; + /** + * Get the system time when the condition was last checked. + * @return the system time when the condition was last checked, 0 for never. + */ + time_t getLastCheckTime() const { return m_lastCheckTime; } + protected: /** the system time when the condition was last checked, 0 for never. */ @@ -952,6 +969,9 @@ class SimpleCondition : public Condition { // @copydoc void dump(bool matched, ostream* output) const override; + // @copydoc + void dumpJson(ostream* output) const override; + // @copydoc CombinedCondition* combineAnd(Condition* other) override; @@ -1044,6 +1064,9 @@ class SimpleNumericCondition : public SimpleCondition { // @copydoc bool checkValue(const Message* message, const string& field) override; + // @copydoc + void dumpValuesJson(ostream* output) const override; + private: /** the valid value ranges (pairs of from/to inclusive), empty for @a m_message seen check. */ @@ -1085,6 +1108,9 @@ class SimpleStringCondition : public SimpleCondition { // @copydoc bool checkValue(const Message* message, const string& field) override; + // @copydoc + void dumpValuesJson(ostream* output) const override; + private: /** the valid values. */ @@ -1111,6 +1137,9 @@ class CombinedCondition : public Condition { // @copydoc void dump(bool matched, ostream* output) const override; + // @copydoc + void dumpJson(ostream* output) const override; + // @copydoc CombinedCondition* combineAnd(Condition* other) override { m_conditions.push_back(other); return this; } @@ -1255,6 +1284,67 @@ class LoadedFileInfo { }; +/** + * Interface for resolving templates and loading additional message definitions. + */ +class Resolver { + public: + /** + * Constructor. + */ + Resolver() {} + + /** + * Destructor. + */ + virtual ~Resolver() {} + + /** + * Get the @a DataFieldTemplates for the specified configuration file. + * @param filename the full name of the configuration file, or "*" to get the non-root templates with the longest name + * or the root templates if not available. + * @return the @a DataFieldTemplates. + */ + virtual DataFieldTemplates* getTemplates(const string& filename) = 0; + + /** + * Load definitions from a relative file from the config path/URL. + * @param reader the @a FileReader instance to load with the definitions. + * @param filename the relative name of the file being read. + * @param defaults the default values by name (potentially overwritten by file name), or nullptr to not use defaults. + * @param errorDescription a string in which to store the error description in case of error. + * @param replace whether to replace an already existing entry. + * @return @a RESULT_OK on success, or an error code. + */ + virtual result_t loadDefinitionsFromConfigPath(FileReader* reader, const string& filename, + map* defaults, string* errorDescription, bool replace = false) = 0; +}; + + +/** + * Interface for adding instance specific message attributes. + */ +class AddAttributes { + public: + /** + * Constructor. + */ + AddAttributes() {} + + /** + * Destructor. + */ + virtual ~AddAttributes() {} + + /** + * Append attributes for the @a Message specific to this instance as JSON to the output. + * @param message the @a Message instance. + * @param output the @a ostream to append the attributes to. + */ + virtual void addAttrsTo(const Message* message, ostream* output) const { } +}; + + /** * Holds a map of all known @a Message instances. */ @@ -1267,7 +1357,7 @@ class MessageMap : public MappedFileReader { * @param deleteData whether to delete the scan message @a DataField during @a Message destruction. */ explicit MessageMap(bool addAll = false, const string& preferLanguage = "", bool deleteData = true) - : MappedFileReader::MappedFileReader(true), + : MappedFileReader::MappedFileReader(true, preferLanguage), m_resolver(nullptr), m_addAll(addAll), m_additionalScanMessages(false), m_maxIdLength(0), m_maxBroadcastIdLength(0), m_messageCount(0), m_conditionalMessageCount(0), m_passiveMessageCount(0) { m_scanMessage = Message::createScanMessage(false, deleteData); @@ -1289,6 +1379,17 @@ class MessageMap : public MappedFileReader { } } + /** + * Set the @a Resolver instance. + * @param the @a Resolver instance. + */ + void setResolver(Resolver* resolver) { m_resolver = resolver; } + + /** + * @return the @a Resolver instance. + */ + Resolver* getResolver() const { return m_resolver; } + /** * Add a @a Message instance to this set. * @param message the @a Message instance to add. @@ -1567,14 +1668,23 @@ class MessageMap : public MappedFileReader { * @param withConditions whether to include the optional conditions prefix. * @param outputFormat the @a OutputFormat options. * @param output the @a ostream to append the formatted messages to. + * @param addAttrs an @a AddAttributes instance to use as well, or nullptr. */ - void dump(bool withConditions, OutputFormat outputFormat, ostream* output) const; + void dump(bool withConditions, OutputFormat outputFormat, ostream* output, + AddAttributes* addAttrs = nullptr) const; + /** + * @return the maximum ID length used by any of the known @a Message instances. + */ + size_t getMaxIdLength() const { return m_maxIdLength; } private: /** empty vector for @a getLoadedFiles(). */ static vector s_noFiles; + /** the @a Resolver instance. */ + Resolver* m_resolver; + /** whether to add all messages, even if duplicate. */ const bool m_addAll; diff --git a/src/lib/ebus/protocol.cpp b/src/lib/ebus/protocol.cpp new file mode 100644 index 000000000..c4502b763 --- /dev/null +++ b/src/lib/ebus/protocol.cpp @@ -0,0 +1,340 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2014-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include "lib/ebus/device_trans.h" +#include "lib/ebus/protocol.h" +#include "lib/ebus/protocol_direct.h" +#include "lib/utils/log.h" + +namespace ebusd { + +using std::hex; +using std::setfill; +using std::setw; + +const char* getProtocolStateCode(ProtocolState state) { + switch (state) { + case ps_noSignal: return "no signal"; + case ps_idle: return "idle"; + case ps_idleSYN: return "idle, SYN generator"; + case ps_recv: return "receive"; + case ps_send: return "send"; + case ps_empty: return "idle, empty"; + default: return "unknown"; + } +} + +bool ActiveBusRequest::notify(result_t result, const SlaveSymbolString& slave) { + if (result == RESULT_OK) { + string str = slave.getStr(); + logDebug(lf_bus, "read res: %s", str.c_str()); + } + m_result = result; + *m_slave = slave; + return false; +} + +ProtocolHandler* ProtocolHandler::create(const ebus_protocol_config_t config, + ProtocolListener* listener) { + const char* name = config.device; + bool enhanced = false; + uint8_t speed = 0; + if (name[0] == 'e' && name[1] && name[2] && name[3] == ':') { + speed = name[2] == 's' ? 2 : name[2] == 'h' ? 1 : 0; + enhanced = speed > 0 && name[1] == 'n'; + if (enhanced) { + name += 4; + } else { + speed = 0; + } + } + Transport* transport; + // symlink device name may contain colon, so only check for absence of slash only + if (strchr(name, '/') == nullptr) { + char* in = strdup(name); + bool udp = false; + char* addrpos = in; + char* portpos = strchr(addrpos, ':'); + // support tcp:[:] and udp:[:] + if (portpos == addrpos+3 && (strncmp(addrpos, "tcp", 3) == 0 || (udp=(strncmp(addrpos, "udp", 3) == 0)))) { + addrpos += 4; + portpos = strchr(addrpos, ':'); + } + uint16_t port; + if (portpos == nullptr) { + port = 9999; + } else { + result_t result = RESULT_OK; + port = (uint16_t)parseInt(portpos+1, 10, 1, 65535, &result); + if (result != RESULT_OK) { + free(in); + return nullptr; // invalid port + } + *portpos = 0; + } + char* hostOrIp = strdup(addrpos); + free(in); + transport = new NetworkTransport(name, config.extraLatency, hostOrIp, port, udp); + } else { + // support ens:/dev/, enh:/dev/, and /dev/ + // as well as symlinks like /dev/serial/by-id/...Espressif_00:01:02:03... + transport = new SerialTransport(name, config.extraLatency, !config.noDeviceCheck, speed); + } + Device* device; + if (enhanced) { + device = new EnhancedDevice(transport); + } else { + device = new PlainDevice(transport); + } + return new DirectProtocolHandler(config, device, listener); +} + +result_t ProtocolHandler::open() { + result_t result = m_device->open(); + if (result != RESULT_OK) { + logError(lf_bus, "unable to open %s: %s", m_device->getName(), getResultCode(result)); + } else if (!m_device->isValid()) { + logError(lf_bus, "device %s not available", m_device->getName()); + } + return result; +} + +void ProtocolHandler::formatInfo(ostringstream* ostream, bool verbose, bool noWait) { + m_device->formatInfo(ostream, verbose, true); + if (isReadOnly()) { + *ostream << ", readonly"; + } + if (noWait) { + return; + } + m_device->formatInfo(ostream, verbose, false); +} + +void ProtocolHandler::formatInfoJson(ostringstream* ostream) const { + m_device->formatInfoJson(ostream); +} + +void ProtocolHandler::notifyDeviceData(const symbol_t* data, size_t len, bool received) { + if (received && m_dumpFile) { + m_dumpFile->write(data, len); + } + if (!m_logRawFile && !m_logRawEnabled) { + return; + } + if (m_logRawBytes) { + if (m_logRawFile) { + m_logRawFile->write(data, len, received); + } else if (m_logRawEnabled) { + for (size_t pos = 0; pos < len; pos++) { + logNotice(lf_bus, "%c%02x", received ? '<' : '>', data[pos]); + } + } + return; + } + for (size_t pos = 0; pos < len; pos++) { + symbol_t symbol = data[pos]; + if (symbol != SYN) { + if (received && !m_logRawLastReceived && symbol == m_logRawLastSymbol) { + continue; // skip received echo of previously sent symbol + } + if (m_logRawBuffer.tellp() == 0 || received != m_logRawLastReceived) { + m_logRawLastReceived = received; + if (m_logRawBuffer.tellp() == 0 && m_logRawLastSymbol != SYN) { + m_logRawBuffer << "..."; + } + m_logRawBuffer << (received ? "<" : ">"); + } + m_logRawBuffer << setw(2) << setfill('0') << hex << static_cast(symbol); + } + m_logRawLastSymbol = symbol; + if (m_logRawBuffer.tellp() > (symbol == SYN ? 0 : 64)) { // flush: direction+5 hdr+24 max data+crc+direction+ack+1 + if (symbol != SYN) { + m_logRawBuffer << "..."; + } + const string bufStr = m_logRawBuffer.str(); + const char* str = bufStr.c_str(); + if (m_logRawFile) { + m_logRawFile->write((const unsigned char*)str, strlen(str), received, false); + } else { + logNotice(lf_bus, str); + } + m_logRawBuffer.str(""); + } + } +} + +void ProtocolHandler::notifyDeviceStatus(bool error, const char* message) { + if (error) { + logError(lf_device, message); + } else { + logNotice(lf_device, message); + } +} + + +void ProtocolHandler::clear() { + memset(m_seenAddresses, 0, sizeof(m_seenAddresses)); + m_masterCount = 1; +} + +result_t ProtocolHandler::addRequest(BusRequest* request, bool wait) { + if (m_config.readOnly) { + return RESULT_ERR_DEVICE; + } + m_nextRequests.push(request); + if (!wait || m_finishedRequests.remove(request, true)) { + return RESULT_OK; + } + return RESULT_ERR_TIMEOUT; +} + +result_t ProtocolHandler::sendAndWait(const MasterSymbolString& master, SlaveSymbolString* slave) { + if (!hasSignal()) { + return RESULT_ERR_NO_SIGNAL; // don't wait when there is no signal + } + result_t result = RESULT_ERR_NO_SIGNAL; + slave->clear(); + ActiveBusRequest request(master, slave); + logInfo(lf_bus, "send message: %s", master.getStr().c_str()); + + for (int sendRetries = m_config.failedSendRetries + 1; sendRetries > 0; sendRetries--) { + result = addRequest(&request, true); + bool success = result == RESULT_OK; + if (success) { + result = request.m_result; + } + if (result == RESULT_OK) { + break; + } + if (!success || result == RESULT_ERR_NO_SIGNAL || result == RESULT_ERR_SEND || result == RESULT_ERR_DEVICE) { + logError(lf_bus, "send to %2.2x: %s, give up", master[1], getResultCode(result)); + break; + } + logError(lf_bus, "send to %2.2x: %s%s", master[1], getResultCode(result), sendRetries > 1 ? ", retry" : ""); + request.m_busLostRetries = 0; + } + return result; +} + +void ProtocolHandler::measureLatency(struct timespec* sentTime, struct timespec* recvTime) { + int64_t latencyLong = (recvTime->tv_sec*1000000000 + recvTime->tv_nsec + - sentTime->tv_sec*1000000000 - sentTime->tv_nsec)/1000000; + if (latencyLong < 0 || latencyLong > 1000) { + return; // clock skew or out of reasonable range + } + auto latency = static_cast(latencyLong); + logDebug(lf_bus, "send/receive symbol latency %d ms", latency); + if (m_symbolLatencyMin >= 0 && (latency >= m_symbolLatencyMin && latency <= m_symbolLatencyMax)) { + return; + } + if (m_symbolLatencyMin == -1 || latency < m_symbolLatencyMin) { + m_symbolLatencyMin = latency; + } + if (m_symbolLatencyMax == -1 || latency > m_symbolLatencyMax) { + m_symbolLatencyMax = latency; + } + logInfo(lf_bus, "send/receive symbol latency %d - %d ms", m_symbolLatencyMin, m_symbolLatencyMax); +} + +bool ProtocolHandler::addSeenAddress(symbol_t address) { + if (!isValidAddress(address, false)) { + return false; + } + if (!isMaster(address)) { + if (!m_config.readOnly && address == m_ownSlaveAddress) { + if (!m_addressConflict) { + m_addressConflict = true; + logError(lf_bus, "own slave address %2.2x is used by another participant", address); + } + } + if (!m_seenAddresses[address]) { + m_listener->notifyProtocolSeenAddress(address); + } + m_seenAddresses[address] = true; + address = getMasterAddress(address); + if (address == SYN) { + return false; + } + } + if (m_seenAddresses[address]) { + return false; + } + bool ret = false; + if (!m_config.readOnly && address == m_ownMasterAddress) { + if (!m_addressConflict) { + m_addressConflict = true; + logError(lf_bus, "own master address %2.2x is used by another participant", address); + } + } else { + m_masterCount++; + ret = true; + logNotice(lf_bus, "new master %2.2x, master count %d", address, m_masterCount); + } + m_listener->notifyProtocolSeenAddress(address); + m_seenAddresses[address] = true; + return ret; +} + +void ProtocolHandler::setDumpFile(const char* dumpFile, unsigned int dumpSize, bool dumpFlush) { + if (m_dumpFile) { + delete m_dumpFile; + m_dumpFile = nullptr; + } + if (dumpFile && dumpFile[0]) { + m_dumpFile = new RotateFile(dumpFile, dumpSize, false, dumpFlush ? 1 : 16); + } +} + +bool ProtocolHandler::toggleDump() { + if (!m_dumpFile) { + return false; + } + bool enabled = !m_dumpFile->isEnabled(); + m_dumpFile->setEnabled(enabled); + return enabled; +} + +void ProtocolHandler::setLogRawFile(const char* logRawFile, unsigned int logRawSize) { + if (logRawFile[0]) { + m_logRawFile = new RotateFile(logRawFile, logRawSize, true); + m_logRawFile->setEnabled(m_logRawEnabled); + } else { + m_logRawFile = nullptr; + } +} + +bool ProtocolHandler::toggleLogRaw(bool bytes) { + bool enabled; + m_logRawBytes = bytes; + if (m_logRawFile) { + enabled = !m_logRawFile->isEnabled(); + m_logRawFile->setEnabled(enabled); + } else { + enabled = !m_logRawEnabled; + m_logRawEnabled = enabled; + } + return enabled; +} + +} // namespace ebusd diff --git a/src/lib/ebus/protocol.h b/src/lib/ebus/protocol.h new file mode 100755 index 000000000..0a58dfec1 --- /dev/null +++ b/src/lib/ebus/protocol.h @@ -0,0 +1,627 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2014-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIB_EBUS_PROTOCOL_H_ +#define LIB_EBUS_PROTOCOL_H_ + +#include "lib/ebus/symbol.h" +#include "lib/ebus/result.h" +#include "lib/ebus/device.h" +#include "lib/utils/queue.h" +#include "lib/utils/rotatefile.h" +#include "lib/utils/thread.h" + +namespace ebusd { + +/** @file lib/ebus/protocol.h + * Classes, functions, and constants related to handling the eBUS protocol. + */ + +/** the default time [ms] for retrieving a symbol from an addressed slave. */ +#define SLAVE_RECV_TIMEOUT 15 + +/** the desired delay time [ms] for sending the AUTO-SYN symbol after last seen symbol. */ +#define SYN_INTERVAL 40 + +/** the maximum allowed time [ms] for retrieving the AUTO-SYN symbol (45ms + 2*1,2% + 1 Symbol). */ +#define SYN_TIMEOUT 51 + +/** the time [ms] for determining bus signal availability (AUTO-SYN timeout * 5). */ +#define SIGNAL_TIMEOUT 250 + +/** the maximum duration [us] of a single symbol (Start+8Bit+Stop+Extra @ 2400Bd-2*1,2%). */ +#define SYMBOL_DURATION_MICROS 4700 + +/** the maximum duration [ms] of a single symbol (Start+8Bit+Stop+Extra @ 2400Bd-2*1,2%). */ +#define SYMBOL_DURATION 5 + +/** the maximum allowed time [ms] for retrieving back a sent symbol (2x symbol duration). */ +#define SEND_TIMEOUT ((int)((2*SYMBOL_DURATION_MICROS+999)/1000)) + + +/** settings for the eBUS protocol handler. */ +typedef struct ebus_protocol_config { + /** eBUS device string (serial device or [udp:]ip[:port]) with optional protocol prefix (enh: or ens:). */ + const char* device; + /** whether to skip serial eBUS device test. */ + bool noDeviceCheck; + /** whether to allow read access to the device only. */ + bool readOnly; + /** extra transfer latency in ms. */ + unsigned int extraLatency; + /** the own master address. */ + symbol_t ownAddress; + /** whether to answer queries for the own master/slave address. */ + bool answer; + /** the number of times a send is repeated due to lost arbitration. */ + unsigned int busLostRetries; + /** the number of times a failed send is repeated (other than lost arbitration). */ + unsigned int failedSendRetries; + /** the maximum time in milliseconds for bus acquisition. */ + unsigned int busAcquireTimeout; + /** the maximum time in milliseconds an addressed slave is expected to acknowledge. */ + unsigned int slaveRecvTimeout; + /** the number of AUTO-SYN symbols before sending is allowed after lost arbitration, or 0 for auto detection. */ + unsigned int lockCount; + /** whether to enable AUTO-SYN symbol generation. */ + bool generateSyn; + /** whether to send an initial escape symbol after connecting device. */ + bool initialSend; +} ebus_protocol_config_t; + + +/** the possible protocol states. */ +enum ProtocolState { + ps_noSignal, //!< no signal on the bus + ps_idle, //!< idle (after @a SYN symbol) + ps_idleSYN, //!< idle (after sent SYN symbol in acting as SYN generator) + ps_recv, //!< receiving + ps_send, //!< sending + ps_empty, //!< idle, no more lock remaining, and no other request queued +}; + +/** + * Return the string corresponding to the @a ProtocolState. + * @param state the @a ProtocolState. + * @return the string corresponding to the @a ProtocolState. + */ +const char* getProtocolStateCode(ProtocolState state); + +class ProtocolHandler; + +/** + * Generic request for sending to and receiving from the bus. + */ +class BusRequest { + friend class ProtocolHandler; + + public: + /** + * Constructor. + * @param master the master data @a MasterSymbolString to send. + * @param deleteOnFinish whether to automatically delete this @a BusRequest when finished. + */ + BusRequest(const MasterSymbolString& master, bool deleteOnFinish) + : m_master(master), m_busLostRetries(0), + m_deleteOnFinish(deleteOnFinish) {} + + /** + * Destructor. + */ + virtual ~BusRequest() {} + + /** + * @return the master data @a MasterSymbolString to send. + */ + const MasterSymbolString& getMaster() const { return m_master; } + + /** + * @return the number of times a send was repeated due to lost arbitration. + */ + unsigned int getBusLostRetries() const { return m_busLostRetries; } + + /** + * Increment the number of times a send was repeated due to lost arbitration. + */ + void incrementBusLostRetries() { m_busLostRetries++; } + + /** + * Reset the number of times a send was repeated due to lost arbitration. + */ + void resetBusLostRetries() { m_busLostRetries = 0; } + + /** + * @return whether to automatically delete this @a BusRequest when finished. + */ + bool deleteOnFinish() const { return m_deleteOnFinish; } + + /** + * Notify the request of the specified result. + * @param result the result of the request. + * @param slave the @a SlaveSymbolString received. + * @return true if the request needs to be restarted. + */ + virtual bool notify(result_t result, const SlaveSymbolString& slave) = 0; // abstract + + + protected: + /** the master data @a MasterSymbolString to send. */ + const MasterSymbolString& m_master; + + /** the number of times a send was repeated due to lost arbitration. */ + unsigned int m_busLostRetries; + + /** whether to automatically delete this @a BusRequest when finished. */ + const bool m_deleteOnFinish; +}; + + +/** + * An active @a BusRequest that can be waited for. + */ +class ActiveBusRequest : public BusRequest { + friend class ProtocolHandler; + + public: + /** + * Constructor. + * @param master the master data @a MasterSymbolString to send. + * @param slave reference to @a SlaveSymbolString for filling in the received slave data. + */ + ActiveBusRequest(const MasterSymbolString& master, SlaveSymbolString* slave) + : BusRequest(master, false), m_result(RESULT_ERR_NO_SIGNAL), m_slave(slave) {} + + /** + * Destructor. + */ + virtual ~ActiveBusRequest() {} + + // @copydoc + bool notify(result_t result, const SlaveSymbolString& slave) override; + + + private: + /** the result of handling the request. */ + result_t m_result; + + /** reference to @a SlaveSymbolString for filling in the received slave data. */ + SlaveSymbolString* m_slave; +}; + + +/** the possible message directions. */ +enum MessageDirection { + md_recv, //!< message received from bus + md_send, //!< message sent to bus + md_answer, //!< answered to a message received from bus +}; + +/** + * Interface for listening to eBUS protocol data. + */ +class ProtocolListener { + public: + /** + * Destructor. + */ + virtual ~ProtocolListener() {} + + /** + * Called to notify a status update from the protocol. + * @param state the current protocol state. + * @param result the error code reason for the state change, or @a RESULT_OK. + */ + virtual void notifyProtocolStatus(ProtocolState state, result_t result) = 0; // abstract + + /** + * Called to notify a new valid seen address on the bus. + * @param address the seen address. + */ + virtual void notifyProtocolSeenAddress(symbol_t address) = 0; // abstract + + /** + * Listener method that is called when a message was sent or received. + * @param direction the message direction. + * @param master the @a MasterSymbolString received/sent. + * @param slave the @a SlaveSymbolString received/sent or the answer passed to @a ProtocolHandler::setAnswer() with + * the the length of the data part following the ID as master. + */ + virtual void notifyProtocolMessage(MessageDirection direction, const MasterSymbolString& master, + const SlaveSymbolString& slave) = 0; // abstract +}; + + + +/** + * Handles input from and output to eBUS with respect to the eBUS protocol. + */ +class ProtocolHandler : public WaitThread, public DeviceListener { + public: + /** + * Construct a new instance. + * @param config the configuration to use. + * @param device the @a Device instance for accessing the bus. + * @param listener the @a ProtocolListener. + */ + ProtocolHandler(const ebus_protocol_config_t config, + Device* device, ProtocolListener* listener) + : WaitThread(), m_config(config), m_device(device), m_listener(listener), + m_listenerState(ps_noSignal), m_reconnect(false), + m_ownMasterAddress(config.ownAddress), m_ownSlaveAddress(getSlaveAddress(config.ownAddress)), + m_addressConflict(false), + m_masterCount(config.readOnly ? 0 : 1), + m_symbolLatencyMin(-1), m_symbolLatencyMax(-1), m_arbitrationDelayMin(-1), + m_arbitrationDelayMax(-1), m_lastReceive(0), + m_symPerSec(0), m_maxSymPerSec(0), + m_logRawFile(nullptr), m_logRawEnabled(false), m_logRawBytes(false), + m_logRawLastSymbol(SYN), m_dumpFile(nullptr) { + memset(m_seenAddresses, 0, sizeof(m_seenAddresses)); + device->setListener(this); + m_logRawLastReceived = true; + m_logRawLastSymbol = SYN; + } + + /** + * Destructor. + */ + virtual ~ProtocolHandler() { + join(); + BusRequest* req; + while ((req = m_finishedRequests.pop()) != nullptr) { + delete req; + } + while ((req = m_nextRequests.pop()) != nullptr) { + if (req->m_deleteOnFinish) { + delete req; + } + } + if (m_dumpFile) { + delete m_dumpFile; + m_dumpFile = nullptr; + } + if (m_logRawFile) { + delete m_logRawFile; + m_logRawFile = nullptr; + } + if (m_device != nullptr) { + delete m_device; + m_device = nullptr; + } + } + + /** + * Create a new instance. + * @param config the configuration to use. + * @param listener the @a ProtocolListener. + * @return the new ProtocolHandler, or @a nullptr on error. + */ + static ProtocolHandler* create(const ebus_protocol_config_t config, ProtocolListener* listener); + + /** + * Open the device. + * @return the @a result_t code. + */ + virtual result_t open(); + + /** + * Format device/protocol infos in plain text. + * @param output the @a ostringstream to append the infos to. + * @param verbose whether to add verbose infos. + * @param noWait true to not wait for any response asynchronously and return immediately. + */ + virtual void formatInfo(ostringstream* output, bool verbose, bool noWait); + + /** + * Format device/protocol infos in JSON format. + * @param output the @a ostringstream to append the infos to. + */ + virtual void formatInfoJson(ostringstream* output) const; + + /** + * @return whether to allow read access to the device only. + */ + bool isReadOnly() const { return m_config.readOnly; } + + /** + * @return the own master address. + */ + symbol_t getOwnMasterAddress() const { return m_ownMasterAddress; } + + /** + * @return the own slave address. + */ + symbol_t getOwnSlaveAddress() const { return m_ownSlaveAddress; } + + /** + * @return @p true if answering queries (if not readonly). + */ + virtual bool isAnswering() const { return false; } + + /** + * Add a message to be answered. + * @param srcAddress the source address to limit to, or @a SYN for any. + * @param dstAddress the destination address (either master or slave address). + * @param pb the primary ID byte. + * @param sb the secondary ID byte. + * @param id optional further ID bytes. + * @param idLen the length of the further ID bytes (maximum 4). + * @param answer the sequence to respond when addressed as slave or the length of the data part following the ID as master. + * @return @p true on success, @p false on error (e.g. invalid address, read only, or too long id). + */ + virtual bool setAnswer(symbol_t srcAddress, symbol_t dstAddress, symbol_t pb, symbol_t sb, const symbol_t* id, + size_t idLen, const SlaveSymbolString& answer) { return false; } + + /** + * @return @p true if an answer was set for the destination address. + */ + virtual bool hasAnswer(symbol_t dstAddress) const { return false; } + + /** + * @param address the address to check. + * @return @p true when the address is the own master or slave address (if not readonly). + */ + bool isOwnAddress(symbol_t address) const { + return !m_config.readOnly && (address == m_ownMasterAddress || address == m_ownSlaveAddress); + } + + /** + * @param address the own address to check for conflict or @a SYN for any. + * @return @p true when an address conflict with any of the own addresses or the specified own address was detected. + */ + bool isAddressConflict(symbol_t address) const { + return m_addressConflict && (address == SYN || m_seenAddresses[address]); + } + + /** + * @return the maximum number of received symbols per second ever seen. + */ + unsigned int getMaxSymPerSec() const { return m_maxSymPerSec; } + + /** + * @return whether the device supports checking for version updates. + */ + virtual bool supportsUpdateCheck() const { return m_device->supportsUpdateCheck(); } + + // @copydoc + virtual void notifyDeviceData(const symbol_t* data, size_t len, bool received); + + // @copydoc + void notifyDeviceStatus(bool error, const char* message) override; + + /** + * Clear stored values (e.g. scan results). + */ + virtual void clear(); + + /** + * Inject a message from outside and treat it as regularly retrieved from the bus. + * This may only be called before bus handling was actually started. + * @param master the @a MasterSymbolString with the master data. + * @param slave the @a SlaveSymbolString with the slave data. + */ + virtual void injectMessage(const MasterSymbolString& master, const SlaveSymbolString& slave) = 0; // abstract + + /** + * Add a @a BusRequest to the internal queue and optionally wait for it to complete. + * @param request the @a BusRequest to add. + * @param wait true to wait for it to complete, false to return immediately. + * @return the result code of adding the request (i.e. RESULT_OK when it was not waited for or when it was completed). + */ + virtual result_t addRequest(BusRequest* request, bool wait); + + /** + * Send a message on the bus and wait for the answer. + * @param master the @a MasterSymbolString with the master data to send. + * @param slave the @a SlaveSymbolString that will be filled with retrieved slave data. + * @return the result code. + */ + virtual result_t sendAndWait(const MasterSymbolString& master, SlaveSymbolString* slave); + + /** + * Main thread entry. + */ + virtual void run() = 0; // abstract + + /** + * Return true when a signal on the bus is available. + * @return true when a signal on the bus is available. + */ + virtual bool hasSignal() const = 0; // abstract + + /** + * Reconnect the device. + */ + virtual void reconnect() { m_reconnect = true; } + + /** + * Return the current symbol rate. + * @return the number of received symbols in the last second. + */ + unsigned int getSymbolRate() const { return m_symPerSec; } + + /** + * Return the maximum seen symbol rate. + * @return the maximum number of received symbols per second ever seen. + */ + unsigned int getMaxSymbolRate() const { return m_maxSymPerSec; } + + /** + * Return the minimal measured latency between send and receive of a symbol. + * @return the minimal measured latency between send and receive of a symbol in milliseconds, -1 if not yet known. + */ + int getMinSymbolLatency() const { return m_symbolLatencyMin; } + + /** + * Return the maximal measured latency between send and receive of a symbol. + * @return the maximal measured latency between send and receive of a symbol in milliseconds, -1 if not yet known. + */ + int getMaxSymbolLatency() const { return m_symbolLatencyMax; } + + /** + * Return the minimal measured delay between received SYN and sent own master address in microseconds. + * @return the minimal measured delay between received SYN and sent own master address in microseconds, -1 if not yet known. + */ + int getMinArbitrationDelay() const { return m_arbitrationDelayMin; } + + /** + * Return the maximal measured delay between received SYN and sent own master address in microseconds. + * @return the maximal measured delay between received SYN and sent own master address in microseconds, -1 if not yet known. + */ + int getMaxArbitrationDelay() const { return m_arbitrationDelayMax; } + + /** + * Return the number of masters already seen. + * @return the number of masters already seen (including ebusd itself). + */ + unsigned int getMasterCount() const { return m_masterCount; } + + /** + * Set the dump file to use. + * @param dumpFile the dump file to use, or nullptr. + * @param dumpSize the maximum file size. + * @param dumpFlush true to early flush the file. + */ + void setDumpFile(const char* dumpFile, unsigned int dumpSize, bool dumpFlush); + + /** + * @return whether a dump file is set. + */ + bool hasDumpFile() const { return m_dumpFile; } + + /** + * Toggle dumping to file. + * @return true if dumping is now enabled. + */ + bool toggleDump(); + + /** + * Set the log raw data file to use. + * @param logRawFile the log raw file to use, or nullptr. + * @param logRawSize the maximum file size. + */ + void setLogRawFile(const char* logRawFile, unsigned int logRawSize); + + /** + * Toggle logging raw data. + * @return true if logging raw data is now enabled. + */ + bool toggleLogRaw(bool bytes); + + protected: + /** + * Called to measure the latency between send and receive of a symbol. + * @param sentTime the time the symbol was sent. + * @param recvTime the time the symbol was received. + */ + virtual void measureLatency(struct timespec* sentTime, struct timespec* recvTime); + + /** + * Add a seen bus address. + * @param address the seen bus address. + * @return true if a conflict with the own addresses was detected, false otherwise. + */ + virtual bool addSeenAddress(symbol_t address); + + /** the configuration to use. */ + const ebus_protocol_config_t m_config; + + /** the @a Device instance for accessing the bus. */ + Device* m_device; + + /** the @a ProtocolListener. */ + ProtocolListener *m_listener; + + /** the last state the listener was informed with. */ + ProtocolState m_listenerState; + + /** set to @p true when the device shall be reconnected. */ + bool m_reconnect; + + /** the own master address. */ + const symbol_t m_ownMasterAddress; + + /** the own slave address. */ + const symbol_t m_ownSlaveAddress; + + /** set to @p true once an address conflict with the own addresses was detected. */ + bool m_addressConflict; + + /** the number of masters already seen. */ + unsigned int m_masterCount; + + /** the minimal measured latency between send and receive of a symbol in milliseconds, -1 if not yet known. */ + int m_symbolLatencyMin; + + /** the maximal measured latency between send and receive of a symbol in milliseconds, -1 if not yet known. */ + int m_symbolLatencyMax; + + /** + * the minimal measured delay between received SYN and sent own master address in microseconds, + * -1 if not yet known. + */ + int m_arbitrationDelayMin; + + /** + * the maximal measured delay between received SYN and sent own master address in microseconds, + * -1 if not yet known. + */ + int m_arbitrationDelayMax; + + /** the time of the last received symbol, or 0 for never. */ + time_t m_lastReceive; + + /** the queue of @a BusRequests that shall be handled. */ + Queue m_nextRequests; + + /** the queue of @a BusRequests that are already finished. */ + Queue m_finishedRequests; + + /** the number of received symbols in the last second. */ + unsigned int m_symPerSec; + + /** the maximum number of received symbols per second ever seen. */ + unsigned int m_maxSymPerSec; + + /** the participating bus addresses seen so far. */ + bool m_seenAddresses[256]; + + /** the @a RotateFile for writing sent/received bytes in log format, or nullptr. */ + RotateFile* m_logRawFile; + + /** whether raw logging to @p logNotice is enabled (only relevant if m_logRawFile is nullptr). */ + bool m_logRawEnabled; + + /** whether to log raw bytes instead of messages with @a m_logRawEnabled. */ + bool m_logRawBytes; + + /** the buffer for building log raw message. */ + ostringstream m_logRawBuffer; + + /** true when the last byte in @a m_logRawBuffer was receive, false if it was sent. */ + bool m_logRawLastReceived; + + /** the last sent/received symbol.*/ + symbol_t m_logRawLastSymbol; + + /** the @a RotateFile for dumping received data, or nullptr. */ + RotateFile* m_dumpFile; +}; + +} // namespace ebusd + +#endif // LIB_EBUS_PROTOCOL_H_ diff --git a/src/lib/ebus/protocol_direct.cpp b/src/lib/ebus/protocol_direct.cpp new file mode 100644 index 000000000..2fc78243f --- /dev/null +++ b/src/lib/ebus/protocol_direct.cpp @@ -0,0 +1,895 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2014-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "lib/ebus/protocol_direct.h" +#include "lib/utils/log.h" + +namespace ebusd { + +/** + * Return the string corresponding to the @a BusState. + * @param state the @a BusState. + * @return the string corresponding to the @a BusState. + */ +const char* getStateCode(BusState state) { + switch (state) { + case bs_noSignal: return "no signal"; + case bs_skip: return "skip"; + case bs_ready: return "ready"; + case bs_sendCmd: return "send command"; + case bs_recvCmdCrc: return "receive command CRC"; + case bs_recvCmdAck: return "receive command ACK"; + case bs_recvRes: return "receive response"; + case bs_recvResCrc: return "receive response CRC"; + case bs_sendResAck: return "send response ACK"; + case bs_recvCmd: return "receive command"; + case bs_recvResAck: return "receive response ACK"; + case bs_sendCmdCrc: return "send command CRC"; + case bs_sendCmdAck: return "send command ACK"; + case bs_sendRes: return "send response"; + case bs_sendResCrc: return "send response CRC"; + case bs_sendSyn: return "send SYN"; + default: return "unknown"; + } +} + +/** + * The @a ProtocolState value by internal @a BusState value index. + */ +static const ProtocolState protocolStateByBusState[] = { + ps_noSignal, // bs_noSignal + ps_idle, // bs_skip + ps_idle, // bs_ready + ps_recv, // bs_recvCmd + ps_recv, // bs_recvCmdCrc + ps_recv, // bs_recvCmdAck + ps_recv, // bs_recvRes + ps_recv, // bs_recvResCrc + ps_recv, // bs_recvResAck + ps_send, // bs_sendCmd + ps_send, // bs_sendCmdCrc + ps_send, // bs_sendResAck + ps_send, // bs_sendCmdAck + ps_send, // bs_sendRes + ps_send, // bs_sendResCrc + ps_send, // bs_sendSyn +}; + + +void DirectProtocolHandler::run() { + unsigned int symCount = 0; + time_t now, lastTime; + time(&lastTime); + lastTime += 2; + logNotice(lf_bus, "bus started with own address %2.2x/%2.2x%s", m_ownMasterAddress, m_ownSlaveAddress, + m_config.answer?" in answer mode":""); + do { + bool valid = m_device->isValid(); + if (valid && !m_reconnect) { + unsigned int recvTimeout = 0; + symbol_t sentSymbol = ESC; + struct timespec sentTime; + result_t result = handleSend(&recvTimeout, &sentSymbol, &sentTime); + bool sent = result == RESULT_CONTINUE; + do { + if (result >= RESULT_OK) { + result = handleReceive(recvTimeout, sent, sentSymbol, &sentTime); + } + time(&now); + if (result != RESULT_ERR_TIMEOUT && now >= lastTime) { + symCount++; + } + if (now > lastTime) { + m_symPerSec = symCount / (unsigned int)(now-lastTime); + if (m_symPerSec > m_maxSymPerSec) { + m_maxSymPerSec = m_symPerSec; + if (m_maxSymPerSec > 100) { + logNotice(lf_bus, "max. symbols per second: %d", m_maxSymPerSec); + } + } + lastTime = now; + symCount = 0; + } + recvTimeout = 0; // for further buffered bytes + sent = false; + } while (result == RESULT_CONTINUE); + } else { + if (!valid) { + logNotice(lf_bus, "device invalid"); + setState(bs_noSignal, RESULT_ERR_DEVICE); + } + if (!Wait(5)) { + break; + } + m_reconnect = false; + result_t result = m_device->open(); + if (result == RESULT_OK) { + logNotice(lf_bus, "re-opened %s", m_device->getName()); + if (m_config.initialSend && !m_config.readOnly) { + m_device->send(ESC); + } + } else { + logError(lf_bus, "unable to open %s: %s", m_device->getName(), getResultCode(result)); + setState(bs_noSignal, result); + } + symCount = 0; + m_symbolLatencyMin = m_symbolLatencyMax = m_arbitrationDelayMin = m_arbitrationDelayMax = -1; + time(&lastTime); + lastTime += 2; + } + } while (isRunning()); +} + +#ifndef FALLTHROUGH +#if defined(__GNUC__) && __GNUC__ >= 7 +#define FALLTHROUGH [[fallthrough]]; +#else +#define FALLTHROUGH +#endif +#endif + +result_t DirectProtocolHandler::handleSend(unsigned int* recvTimeout, symbol_t* sentSymbol, +struct timespec* sentTime) { + unsigned int timeout = SYN_TIMEOUT; + symbol_t sendSymbol = ESC; + bool sending = false; + + // check if another symbol has to be sent and determine timeout for receive + switch (m_state) { + case bs_noSignal: + timeout = m_generateSynInterval > 0 ? m_generateSynInterval : SIGNAL_TIMEOUT; + break; + + case bs_skip: + timeout = SYN_TIMEOUT; + FALLTHROUGH + case bs_ready: + if (m_currentRequest != nullptr) { + setState(bs_ready, RESULT_ERR_TIMEOUT); // just to be sure an old BusRequest is cleaned up + } + if (!m_device->isArbitrating() && m_currentRequest == nullptr && m_remainLockCount == 0) { + BusRequest* startRequest = m_nextRequests.peek(); + if (startRequest == nullptr) { + m_listener->notifyProtocolStatus(ps_empty, RESULT_OK); + startRequest = m_nextRequests.peek(); + } + if (startRequest != nullptr) { // initiate arbitration + symbol_t master = startRequest->getMaster()[0]; + logDebug(lf_bus, "start request %2.2x", master); + result_t ret = m_device->startArbitration(master); + if (ret == RESULT_OK) { + logDebug(lf_bus, "arbitration start with %2.2x", master); + } else { + logError(lf_bus, "arbitration start: %s", getResultCode(ret)); + m_nextRequests.remove(startRequest); + m_currentRequest = startRequest; + setState(bs_ready, ret); // force the failed request to be notified + } + } + } + break; + + case bs_recvCmd: + case bs_recvCmdCrc: + timeout = m_config.slaveRecvTimeout; + break; + + case bs_recvCmdAck: + timeout = m_config.slaveRecvTimeout; + break; + + case bs_recvRes: + case bs_recvResCrc: + if (m_response.size() > 0 || m_config.slaveRecvTimeout > SYN_TIMEOUT) { + timeout = m_config.slaveRecvTimeout; + } else { + timeout = SYN_TIMEOUT; + } + break; + + case bs_recvResAck: + timeout = m_config.slaveRecvTimeout; + break; + + case bs_sendCmd: + if (m_currentRequest != nullptr) { + sendSymbol = m_currentRequest->getMaster()[m_nextSendPos]; // unescaped command + sending = true; + } + break; + + case bs_sendCmdCrc: + if (m_currentRequest != nullptr) { + sendSymbol = m_crc; + sending = true; + } + break; + + case bs_sendResAck: + if (m_currentRequest != nullptr) { + sendSymbol = m_crcValid ? ACK : NAK; + sending = true; + } + break; + + case bs_sendCmdAck: + if (m_currentAnswering) { + sendSymbol = m_crcValid ? ACK : NAK; + sending = true; + } + break; + + case bs_sendRes: + if (m_currentAnswering) { + sendSymbol = m_response[m_nextSendPos]; // unescaped response + sending = true; + } + break; + + case bs_sendResCrc: + if (m_currentAnswering) { + sendSymbol = m_crc; + sending = true; + } + break; + + case bs_sendSyn: + sendSymbol = SYN; + sending = true; + break; + } + + // send symbol if necessary + if (sending && !m_config.readOnly) { + if (m_state != bs_sendSyn && (sendSymbol == ESC || sendSymbol == SYN)) { + if (m_escape) { + sendSymbol = (symbol_t)(sendSymbol == ESC ? 0x00 : 0x01); + } else { + m_escape = sendSymbol; + sendSymbol = ESC; + } + } + result_t result = m_device->send(sendSymbol); + clockGettime(sentTime); + if (result == RESULT_OK) { + if (m_state == bs_ready) { + timeout = m_config.busAcquireTimeout; + } else { + timeout = SEND_TIMEOUT; + } + *sentSymbol = sendSymbol; + } else { + sending = false; + timeout = SYN_TIMEOUT; + setState(bs_skip, result); + } + *recvTimeout = timeout; + return sending ? RESULT_CONTINUE : result; + } else { + clockGettime(sentTime); // for measuring arbitration delay in enhanced protocol + } + *recvTimeout = timeout; + return RESULT_OK; +} + +result_t DirectProtocolHandler::handleReceive(unsigned int timeout, bool sending, symbol_t sentSymbol, +struct timespec* sentTime) { + // receive next symbol (optionally check reception of sent symbol) + symbol_t recvSymbol; + struct timespec recvTime; + ArbitrationState arbitrationState = as_none; + result_t result = m_device->recv(timeout, &recvSymbol, &arbitrationState); + bool sentAutoSyn = false; + if (sending) { + clockGettime(&recvTime); + } else if (!m_config.readOnly && result == RESULT_ERR_TIMEOUT && m_generateSynInterval > 0 + && timeout >= m_generateSynInterval && (m_state == bs_noSignal || m_state == bs_skip)) { + // check if acting as AUTO-SYN generator is required + result = m_device->send(SYN); + if (result != RESULT_OK) { + return setState(bs_skip, result); + } + clockGettime(sentTime); + recvSymbol = ESC; + result = m_device->recv(SEND_TIMEOUT, &recvSymbol, &arbitrationState); + clockGettime(&recvTime); + if (result < RESULT_OK) { + logError(lf_bus, "unable to receive sent AUTO-SYN symbol: %s", getResultCode(result)); + return setState(bs_noSignal, result); + } + if (recvSymbol != SYN) { + logError(lf_bus, "received %2.2x instead of AUTO-SYN symbol", recvSymbol); + return setState(bs_noSignal, result); + } + measureLatency(sentTime, &recvTime); + if (m_generateSynInterval != SYN_INTERVAL) { + // received own AUTO-SYN symbol back again: act as AUTO-SYN generator now + m_generateSynInterval = SYN_INTERVAL; + logNotice(lf_bus, "acting as AUTO-SYN generator"); + } + m_remainLockCount = 0; + m_lastSynReceiveTime = recvTime; + sentAutoSyn = true; + setState(bs_ready, RESULT_OK); + } + switch (arbitrationState) { + case as_lost: + case as_timeout: + logDebug(lf_bus, arbitrationState == as_lost ? "arbitration lost" : "arbitration lost (timed out)"); + if (m_currentRequest == nullptr) { + BusRequest *startRequest = m_nextRequests.peek(); + if (startRequest != nullptr && m_nextRequests.remove(startRequest)) { + m_currentRequest = startRequest; // force the failed request to be notified + } + } + setState(m_state, RESULT_ERR_BUS_LOST); + break; + case as_won: // implies RESULT_OK + if (m_currentRequest != nullptr) { + logNotice(lf_bus, "arbitration won while handling another request"); + setState(bs_ready, RESULT_OK); // force the current request to be notified + } else { + BusRequest *startRequest = m_nextRequests.peek(); + if (m_state != bs_ready || startRequest == nullptr || !m_nextRequests.remove(startRequest)) { + logNotice(lf_bus, "arbitration won in invalid state %s", getStateCode(m_state)); + setState(bs_ready, RESULT_ERR_TIMEOUT); + } else { + logDebug(lf_bus, "arbitration won"); + m_currentRequest = startRequest; + sentSymbol = m_currentRequest->getMaster()[0]; + sending = true; + } + } + break; + case as_running: + break; + case as_error: + logError(lf_bus, "arbitration start error"); + // cancel request + if (!m_currentRequest) { + BusRequest *startRequest = m_nextRequests.peek(); + if (startRequest && m_nextRequests.remove(startRequest)) { + m_currentRequest = startRequest; + } + } + if (m_currentRequest) { + setState(m_state, RESULT_ERR_BUS_LOST); + } + break; + default: // only as_none + break; + } + if (sentAutoSyn && !sending) { + return result; + } + time_t now; + time(&now); + if (result < RESULT_OK) { + if ((m_generateSynInterval != SYN_INTERVAL && difftime(now, m_lastReceive) > 1) + // at least one full second has passed since last received symbol + || m_state == bs_noSignal) { + return setState(bs_noSignal, result); + } + return setState(bs_skip, result); + } + + m_lastReceive = now; + if ((recvSymbol == SYN) && (m_state != bs_sendSyn)) { + if (result == RESULT_CONTINUE) { + if (m_remainLockCount == 0) { + m_remainLockCount = 1; // avoid starting arbitration when more data is already buffered + } + } else if (!sending) { + if (m_remainLockCount > 0 && m_command.size() != 1) { + m_remainLockCount--; + } else if (m_remainLockCount == 0 && m_command.size() == 1) { + m_remainLockCount = 1; // wait for next AUTO-SYN after SYN / address / SYN (bus locked for own priority) + } + } + clockGettime(&m_lastSynReceiveTime); + return setState(bs_ready, m_state == bs_skip || m_remainLockCount > 0 ? result : RESULT_ERR_SYN); + } + + if (sending && m_state != bs_ready) { // check received symbol for equality if not in arbitration + if (recvSymbol != sentSymbol) { + return setState(bs_skip, RESULT_ERR_SYMBOL); + } + measureLatency(sentTime, &recvTime); + } + + switch (m_state) { + case bs_ready: + case bs_recvCmd: + case bs_recvRes: + case bs_sendCmd: + case bs_sendRes: + SymbolString::updateCrc(recvSymbol, &m_crc); + break; + default: + break; + } + + if (m_escape) { + // check escape/unescape state + if (sending) { + if (sentSymbol == ESC) { + return result; + } + sentSymbol = recvSymbol = m_escape; + } else { + if (recvSymbol > 0x01) { + return setState(bs_skip, RESULT_ERR_ESC); + } + recvSymbol = recvSymbol == 0x00 ? ESC : SYN; + } + m_escape = 0; + } else if (!sending && recvSymbol == ESC) { + m_escape = ESC; + return result; + } + + switch (m_state) { + case bs_noSignal: + return setState(bs_skip, result); + + case bs_skip: + return result; + + case bs_ready: + if (m_currentRequest != nullptr && sending) { + // check arbitration + if (recvSymbol == sentSymbol) { // arbitration successful + // measure arbitration delay + int64_t latencyLong = (sentTime->tv_sec*1000000000LL + sentTime->tv_nsec + - m_lastSynReceiveTime.tv_sec*1000000000LL - m_lastSynReceiveTime.tv_nsec)/1000; + if (latencyLong >= 0 && latencyLong <= 10000) { // skip clock skew or out of reasonable range + auto latency = static_cast(latencyLong); + logDebug(lf_bus, "arbitration delay %d micros", latency); + if (m_arbitrationDelayMin < 0 || (latency < m_arbitrationDelayMin || latency > m_arbitrationDelayMax)) { + if (m_arbitrationDelayMin == -1 || latency < m_arbitrationDelayMin) { + m_arbitrationDelayMin = latency; + } + if (m_arbitrationDelayMax == -1 || latency > m_arbitrationDelayMax) { + m_arbitrationDelayMax = latency; + } + logInfo(lf_bus, "arbitration delay %d - %d micros", m_arbitrationDelayMin, m_arbitrationDelayMax); + } + } + m_nextSendPos = 1; + m_repeat = false; + return setState(bs_sendCmd, result); + } + // arbitration lost. if same priority class found, try again after next AUTO-SYN + m_remainLockCount = isMaster(recvSymbol) ? 2 : 1; // number of SYN to wait for before next send try + if ((recvSymbol & 0x0f) != (sentSymbol & 0x0f) && m_lockCount > m_remainLockCount) { + // if different priority class found, try again after N AUTO-SYN symbols (at least next AUTO-SYN) + m_remainLockCount = m_lockCount; + } + setState(m_state, RESULT_ERR_BUS_LOST); // try again later + } + m_command.push_back(recvSymbol); + m_repeat = false; + return setState(bs_recvCmd, result); + + case bs_recvCmd: + if ((m_command.size() == 0 && !isMaster(recvSymbol)) + || (m_command.size() == 1 && !isValidAddress(recvSymbol))) { + return setState(bs_skip, RESULT_ERR_INVALID_ADDR); + } + m_command.push_back(recvSymbol); + if (m_command.isComplete()) { // all data received + return setState(bs_recvCmdCrc, result); + } + return result; + + case bs_recvCmdCrc: + m_crcValid = recvSymbol == m_crc; + if (m_command[1] == BROADCAST) { + if (m_crcValid) { + addSeenAddress(m_command[0]); + messageCompleted(); + return setState(bs_skip, result); + } + return setState(bs_skip, RESULT_ERR_CRC); + } + if (m_crcValid) { + addSeenAddress(m_command[0]); + m_currentAnswering = getAnswer(); + return setState(m_currentAnswering ? bs_sendCmdAck : bs_recvCmdAck, result); + } + if (m_repeat) { + return setState(bs_skip, RESULT_ERR_CRC); + } + return setState(bs_recvCmdAck, RESULT_ERR_CRC); + + case bs_recvCmdAck: + if (recvSymbol == ACK) { + if (!m_crcValid) { + return setState(bs_skip, RESULT_ERR_ACK); + } + if (m_currentRequest != nullptr) { + if (isMaster(m_currentRequest->getMaster()[1])) { + messageCompleted(); + return setState(bs_sendSyn, result); + } + } else if (isMaster(m_command[1])) { + messageCompleted(); + return setState(bs_skip, result); + } + + m_repeat = false; + return setState(bs_recvRes, result); + } + if (recvSymbol == NAK) { + if (!m_repeat) { + m_repeat = true; + m_crc = 0; + m_nextSendPos = 0; + m_command.clear(); + if (m_currentRequest != nullptr) { + return setState(bs_sendCmd, RESULT_ERR_NAK, true); + } + return setState(bs_recvCmd, RESULT_ERR_NAK); + } + return setState(bs_skip, RESULT_ERR_NAK); + } + return setState(bs_skip, RESULT_ERR_ACK); + + case bs_recvRes: + m_response.push_back(recvSymbol); + if (m_response.isComplete()) { // all data received + return setState(bs_recvResCrc, result); + } + return result; + + case bs_recvResCrc: + m_crcValid = recvSymbol == m_crc; + if (m_crcValid) { + if (m_currentRequest != nullptr) { + return setState(bs_sendResAck, result); + } + return setState(bs_recvResAck, result); + } + if (m_repeat) { + if (m_currentRequest != nullptr) { + return setState(bs_sendSyn, RESULT_ERR_CRC); + } + return setState(bs_skip, RESULT_ERR_CRC); + } + if (m_currentRequest != nullptr) { + return setState(bs_sendResAck, RESULT_ERR_CRC); + } + return setState(bs_recvResAck, RESULT_ERR_CRC); + + case bs_recvResAck: + if (recvSymbol == ACK) { + if (!m_crcValid) { + return setState(bs_skip, RESULT_ERR_ACK); + } + messageCompleted(); + return setState(bs_skip, result); + } + if (recvSymbol == NAK) { + if (!m_repeat) { + m_repeat = true; + if (m_currentAnswering) { + m_nextSendPos = 0; + return setState(bs_sendRes, RESULT_ERR_NAK, true); + } + m_response.clear(); + return setState(bs_recvRes, RESULT_ERR_NAK, true); + } + return setState(bs_skip, RESULT_ERR_NAK); + } + return setState(bs_skip, RESULT_ERR_ACK); + + case bs_sendCmd: + if (!sending || m_currentRequest == nullptr) { + return setState(bs_skip, RESULT_ERR_INVALID_ARG); + } + m_nextSendPos++; + if (m_nextSendPos >= m_currentRequest->getMaster().size()) { + return setState(bs_sendCmdCrc, result); + } + return result; + + case bs_sendCmdCrc: + if (m_currentRequest->getMaster()[1] == BROADCAST) { + messageCompleted(); + return setState(bs_sendSyn, result); + } + m_crcValid = true; + return setState(bs_recvCmdAck, result); + + case bs_sendResAck: + if (!sending || m_currentRequest == nullptr) { + return setState(bs_skip, RESULT_ERR_INVALID_ARG); + } + if (!m_crcValid) { + if (!m_repeat) { + m_repeat = true; + m_response.clear(); + return setState(bs_recvRes, RESULT_ERR_NAK, true); + } + return setState(bs_sendSyn, RESULT_ERR_ACK); + } + messageCompleted(); + return setState(bs_sendSyn, result); + + case bs_sendCmdAck: + if (!sending || !m_currentAnswering) { + return setState(bs_skip, RESULT_ERR_INVALID_ARG); + } + if (!m_crcValid) { + if (!m_repeat) { + m_repeat = true; + m_crc = 0; + m_command.clear(); + return setState(bs_recvCmd, RESULT_ERR_NAK, true); + } + return setState(bs_skip, RESULT_ERR_ACK); + } + // response to send was already prepared during bs_recvCmdCrc in m_response + if (isMaster(m_command[1])) { + messageCompleted(); + return setState(bs_skip, result); + } + + m_nextSendPos = 0; + m_repeat = false; + return setState(bs_sendRes, result); + + case bs_sendRes: + if (!sending || !m_currentAnswering) { + return setState(bs_skip, RESULT_ERR_INVALID_ARG); + } + m_nextSendPos++; + if (m_nextSendPos >= m_response.size()) { + // slave data completely sent + return setState(bs_sendResCrc, result); + } + return result; + + case bs_sendResCrc: + if (!sending || !m_currentAnswering) { + return setState(bs_skip, RESULT_ERR_INVALID_ARG); + } + return setState(bs_recvResAck, result); + + case bs_sendSyn: + if (!sending) { + return setState(bs_ready, RESULT_ERR_INVALID_ARG); + } + return setState(bs_ready, result); + } + return result; +} + +result_t DirectProtocolHandler::setState(BusState state, result_t result, bool firstRepetition) { + if (m_currentRequest != nullptr) { + if (result == RESULT_ERR_BUS_LOST && m_currentRequest->getBusLostRetries() < m_config.busLostRetries) { + logDebug(lf_bus, "%s during %s, retry", getResultCode(result), getStateCode(m_state)); + m_currentRequest->incrementBusLostRetries(); + m_nextRequests.push(m_currentRequest); // repeat + m_currentRequest = nullptr; + } else if (state == bs_sendSyn || (result < RESULT_OK && !firstRepetition)) { + logDebug(lf_bus, "notify request: %s", getResultCode(result)); + bool restart = m_currentRequest->notify( + result == RESULT_ERR_SYN && (m_state == bs_recvCmdAck || m_state == bs_recvRes) + ? RESULT_ERR_TIMEOUT : result, m_response); + if (restart) { + m_currentRequest->resetBusLostRetries(); + m_nextRequests.push(m_currentRequest); + } else if (m_currentRequest->deleteOnFinish()) { + delete m_currentRequest; + } else { + m_finishedRequests.push(m_currentRequest); + } + m_currentRequest = nullptr; + } + if (state == bs_skip) { + m_device->startArbitration(SYN); // reset arbitration state + } + } + + if (state == bs_noSignal) { // notify all requests + m_response.clear(); // notify with empty response + while ((m_currentRequest = m_nextRequests.pop()) != nullptr) { + m_currentRequest->notify(RESULT_ERR_NO_SIGNAL, m_response); + if (m_currentRequest->deleteOnFinish()) { + delete m_currentRequest; + } else { + m_finishedRequests.push(m_currentRequest); + } + } + } + + m_escape = 0; + if (state == m_state) { + if (m_listener && result < RESULT_OK && state != bs_noSignal) { + m_listener->notifyProtocolStatus(m_listenerState, result); + } + return result; + } + if ((result < RESULT_OK && !(result == RESULT_ERR_TIMEOUT && state == bs_skip && m_state == bs_ready)) + || (result < RESULT_OK && state == bs_skip && m_state != bs_ready)) { + logDebug(lf_bus, "%s during %s, switching to %s", getResultCode(result), getStateCode(m_state), + getStateCode(state)); + } else if (m_currentRequest != nullptr || state == bs_sendCmd || state == bs_sendCmdCrc || state == bs_sendCmdAck + || state == bs_sendRes || state == bs_sendResCrc || state == bs_sendResAck || state == bs_sendSyn + || m_state == bs_sendSyn) { + logDebug(lf_bus, "switching from %s to %s", getStateCode(m_state), getStateCode(state)); + } + if (state == bs_noSignal) { + if (m_generateSynInterval == 0 || m_state != bs_skip) { + logError(lf_bus, "signal lost"); + } + } else if (m_state == bs_noSignal) { + if (m_generateSynInterval == 0 || state != bs_skip) { + logNotice(lf_bus, "signal acquired"); + } + } + if (m_listener) { + ProtocolState pstate = protocolStateByBusState[state]; + if (pstate == ps_idle && m_generateSynInterval == SYN_INTERVAL) { + pstate = ps_idleSYN; + } + if (result < RESULT_OK || pstate != m_listenerState) { + m_listener->notifyProtocolStatus(pstate, result); + m_listenerState = pstate; + } + } + m_state = state; + + if (state == bs_ready || state == bs_skip) { + m_command.clear(); + m_crc = 0; + m_crcValid = false; + m_response.clear(); + m_nextSendPos = 0; + m_currentAnswering = false; + } else if (state == bs_recvRes || state == bs_sendRes) { + m_crc = 0; + } + return result; +} + +bool DirectProtocolHandler::addSeenAddress(symbol_t address) { + if (!ProtocolHandler::addSeenAddress(address)) { + return false; + } + if (m_config.lockCount == 0 && m_masterCount > m_lockCount) { + m_lockCount = m_masterCount; + } + return true; +} + +void DirectProtocolHandler::messageCompleted() { + // do an explicit copy here in case being called by another thread + const MasterSymbolString command(m_currentRequest ? m_currentRequest->getMaster() : m_command); + const SlaveSymbolString response(m_response); + symbol_t srcAddress = command[0], dstAddress = command[1]; + if (srcAddress == dstAddress) { + logError(lf_bus, "invalid self-addressed message from %2.2x", srcAddress); + return; + } + if (!m_currentAnswering || (dstAddress != m_ownMasterAddress && dstAddress != m_ownSlaveAddress)) { + // also add given answers to list of seen addresses + addSeenAddress(dstAddress); + } + + const char* prefix = m_currentAnswering ? "answered" : m_currentRequest ? "sent" : "received"; + MessageDirection direction = m_currentAnswering ? md_answer : m_currentRequest ? md_send : md_recv; + bool master = isMaster(dstAddress); + if (dstAddress == BROADCAST || master) { + logInfo(lf_update, "%s %s cmd: %s", prefix, master ? "MM" : "BC", command.getStr().c_str()); + } else { + logInfo(lf_update, "%s MS cmd: %s / %s", prefix, command.getStr().c_str(), response.getStr().c_str()); + } + m_listener->notifyProtocolMessage(direction, command, response); +} + +uint64_t DirectProtocolHandler::createAnswerKey(symbol_t srcAddress, symbol_t dstAddress, symbol_t pb, symbol_t sb, + const symbol_t* id, size_t idLen) { + uint64_t key = (uint64_t)idLen << (8 * 7 + 5); + key |= (uint64_t)getMasterNumber(srcAddress) << (8 * 7); // 0..25 + key |= (uint64_t)dstAddress << (8 * 6); + key |= (uint64_t)pb << (8 * 5); + key |= (uint64_t)sb << (8 * 4); + int exp = 3; + for (size_t pos = 0; pos < idLen; pos++) { + key |= (uint64_t)id[pos] << (8 * exp--); + } + return key; +} + +bool DirectProtocolHandler::setAnswer(symbol_t srcAddress, symbol_t dstAddress, symbol_t pb, symbol_t sb, + const symbol_t* id, size_t idLen, const SlaveSymbolString& answer) { + if (!m_config.answer || (!id && idLen > 0) || idLen > 4 || !isValidAddress(dstAddress, false) + || (srcAddress != SYN && !isMaster(srcAddress))) { + return false; + } + if (isMaster(dstAddress)) { + if (answer.size() > 7) { + return false; + } + // answer used here only for having the expected length of the MM data tail + } else { + if (!answer.isComplete()) { + return false; + } + } + uint64_t key = createAnswerKey(srcAddress, dstAddress, pb, sb, id, idLen); + m_answerByKey[key] = answer; + return true; +} + +bool DirectProtocolHandler::hasAnswer(symbol_t dstAddress) const { + if (m_answerByKey.empty()) { + return false; + } + for (auto const &answer : m_answerByKey) { + if ((answer.first >> (8 * 6)) == dstAddress) { + return true; + } + } + return false; +} + +bool DirectProtocolHandler::getAnswer() { + if (m_answerByKey.empty()) { + return false; + } + // walk through the stored answers to find the longest match + m_response.clear(); + size_t len = m_command[4]; + bool master = isMaster(m_command[1]); + uint64_t key = createAnswerKey(m_command[0], m_command[1], m_command[2], m_command[3], m_command.data()+5, len); + do { + auto it = m_answerByKey.find(key); + if (it == m_answerByKey.end()) { + it = m_answerByKey.find(key&~(0x1fLL << (8 * 7))); // without specific src + } + if (it != m_answerByKey.end()) { + // found the answer + if (master) { + if (len+it->second.getDataSize() == m_command[4]) { + m_response = it->second; // copied for having the data size only + return true; + } + // data length mismatch, find shorter one + } else { + m_response = it->second; + return true; + } + } + if (len == 0) { + break; + } + // reduce the key + len--; + key = (key&~(0x07LL << (8 * 7 + 5))&~(0xffLL << (8 * (3-len)))) | (len << (8 * 7 + 5)); + } while (true); + return false; +} + +} // namespace ebusd diff --git a/src/lib/ebus/protocol_direct.h b/src/lib/ebus/protocol_direct.h new file mode 100755 index 000000000..ba24a46ca --- /dev/null +++ b/src/lib/ebus/protocol_direct.h @@ -0,0 +1,227 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2014-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIB_EBUS_PROTOCOL_DIRECT_H_ +#define LIB_EBUS_PROTOCOL_DIRECT_H_ + +#include +#include "lib/ebus/protocol.h" + +namespace ebusd { + +/** @file lib/ebus/protocol_direct.h + * Implementation of directly handled eBUS protocol. + * + * The following table shows the possible states, symbols, and state transition + * depending on the kind of message to send/receive: + * @image html states.png "ebusd direct ProtocolHandler states" + */ + +/** the possible bus states. */ +enum BusState { + bs_noSignal, //!< no signal on the bus + bs_skip, //!< skip all symbols until next @a SYN + bs_ready, //!< ready for next master (after @a SYN symbol, send/receive QQ) + bs_recvCmd, //!< receive command (ZZ, PBSB, master data) [passive set] + bs_recvCmdCrc, //!< receive command CRC [passive set] + bs_recvCmdAck, //!< receive command ACK/NACK [passive set + active set+get] + bs_recvRes, //!< receive response (slave data) [passive set + active get] + bs_recvResCrc, //!< receive response CRC [passive set + active get] + bs_recvResAck, //!< receive response ACK/NACK [passive set] + bs_sendCmd, //!< send command (ZZ, PBSB, master data) [active set+get] + bs_sendCmdCrc, //!< send command CRC [active set+get] + bs_sendResAck, //!< send response ACK/NACK [active get] + bs_sendCmdAck, //!< send command ACK/NACK [passive get] + bs_sendRes, //!< send response (slave data) [passive get] + bs_sendResCrc, //!< send response CRC [passive get] + bs_sendSyn, //!< send SYN for completed transfer [active set+get] +}; + + + +/** + * Directly handles input from and output to eBUS with respect to the eBUS protocol. + */ +class DirectProtocolHandler : public ProtocolHandler { + public: + /** + * Construct a new instance. + * @param config the configuration to use. + * @param device the @a Device instance for accessing the bus. + * @param listener the @a ProtocolListener. + */ + DirectProtocolHandler(const ebus_protocol_config_t config, + Device* device, ProtocolListener* listener) + : ProtocolHandler(config, device, listener), + m_lockCount(config.lockCount <= 3 ? 3 : config.lockCount), + m_remainLockCount(config.lockCount == 0 ? 1 : 0), + m_generateSynInterval(config.generateSyn ? 10*getMasterNumber(config.ownAddress)+SYN_TIMEOUT : 0), + m_currentRequest(nullptr), m_currentAnswering(false), m_nextSendPos(0), + m_state(bs_noSignal), m_escape(0), m_crc(0), m_crcValid(false), m_repeat(false) { + m_lastSynReceiveTime.tv_sec = 0; + m_lastSynReceiveTime.tv_nsec = 0; + } + + /** + * Destructor. + */ + virtual ~DirectProtocolHandler() { + join(); + if (m_currentRequest != nullptr) { + delete m_currentRequest; + m_currentRequest = nullptr; + } + } + + // @copydoc + void injectMessage(const MasterSymbolString& master, const SlaveSymbolString& slave) override { + if (isRunning()) { + return; + } + m_command = master; + m_response = slave; + m_addressConflict = true; // avoid conflict messages + messageCompleted(); + m_addressConflict = false; + } + + /** + * Main thread entry. + */ + virtual void run(); + + // @copydoc + bool hasSignal() const override { return m_state != bs_noSignal; } + + // @copydoc + bool isAnswering() const override { return !m_answerByKey.empty(); } + + // @copydoc + bool setAnswer(symbol_t srcAddress, symbol_t dstAddress, symbol_t pb, symbol_t sb, + const symbol_t* id, size_t idLen, const SlaveSymbolString& answer) override; + + // @copydoc + bool hasAnswer(symbol_t dstAddress) const override; + + private: + /** + * Handle sending the next symbol to the bus. + * @param recvTimeout pointer to a variable in which to put the timeout for the receive. + * @param sentSymbol pointer to a variable in which to put the sent symbol. + * @param sentTime pointer to a variable in which to put the system time when the symbol was sent. + * @return RESULT_OK on success, RESULT_CONTINUE when a symbol was sent, or an error code. + */ + result_t handleSend(unsigned int* recvTimeout, symbol_t* sentSymbol, struct timespec* sentTime); + + /** + * Handle receiving the next symbol from the bus. + * @param timeout the timeout for the receive. + * @param sending whether a symbol was sent before entry. + * @param sentSymbol the sent symbol to verify (if sending). + * @param sentTime pointer to a variable with the system time when the symbol was sent. + * @return RESULT_OK on success, RESULT_CONTINUE when further received symbols are buffered, + * or an error code. + */ + result_t handleReceive(unsigned int timeout, bool sending, symbol_t sentSymbol, struct timespec* sentTime); + + /** + * Set a new @a BusState and add a log message if necessary. + * @param state the new @a BusState. + * @param result the result code. + * @param firstRepetition true if the first repetition of a message part is being started. + * @return the result code. + */ + result_t setState(BusState state, result_t result, bool firstRepetition = false); + + // @copydoc + bool addSeenAddress(symbol_t address) override; + + /** + * Called when a message sending or reception was successfully completed. + */ + void messageCompleted(); + + /** + * Create a key for storing an answer. + * @param srcAddress the source address, or @a SYN for any. + * @param dstAddress the destination address. + * @param pb the primary ID byte. + * @param sb the secondary ID byte. + * @param id optional further ID bytes. + * @param idLen the length of the further ID bytes. + * @return a key for storing an answer. + */ + uint64_t createAnswerKey(symbol_t srcAddress, symbol_t dstAddress, symbol_t pb, symbol_t sb, + const symbol_t* id, size_t idLen); + + /** + * Build the answer to the currently received message and store in @a m_response for sending back to requestor. + * @return @p true on success, @p false if the message is not supposed to be answered. + */ + bool getAnswer(); + + /** the number of AUTO-SYN symbols before sending is allowed after lost arbitration. */ + unsigned int m_lockCount; + + /** the remaining number of AUTO-SYN symbols before sending is allowed again. */ + unsigned int m_remainLockCount; + + /** the interval in milliseconds after which to generate an AUTO-SYN symbol, or 0 if disabled. */ + unsigned int m_generateSynInterval; + + /** the time of the last received SYN symbol, or 0 for never. */ + struct timespec m_lastSynReceiveTime; + + /** the currently handled BusRequest, or nullptr. */ + BusRequest* m_currentRequest; + + /** the answers to give by key. */ + std::map m_answerByKey; + + /** whether currently answering a request from another participant. */ + bool m_currentAnswering; + + /** the offset of the next symbol that needs to be sent from the command or response, + * (only relevant if m_request is set and state is @a bs_command or @a bs_response). */ + size_t m_nextSendPos; + + /** the current @a BusState. */ + BusState m_state; + + /** 0 when not escaping/unescaping, or @a ESC when receiving, or the original value when sending. */ + symbol_t m_escape; + + /** the calculated CRC. */ + symbol_t m_crc; + + /** whether the CRC matched. */ + bool m_crcValid; + + /** whether the current message part is being repeated. */ + bool m_repeat; + + /** the received command @a MasterSymbolString. */ + MasterSymbolString m_command; + + /** the received response @a SlaveSymbolString or response to send. */ + SlaveSymbolString m_response; +}; + +} // namespace ebusd + +#endif // LIB_EBUS_PROTOCOL_DIRECT_H_ diff --git a/src/lib/ebus/result.cpp b/src/lib/ebus/result.cpp index a77f2e4a3..690e43366 100755 --- a/src/lib/ebus/result.cpp +++ b/src/lib/ebus/result.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,7 +44,7 @@ const char* getResultCode(result_t resultCode) { case RESULT_ERR_DUPLICATE: return "ERR: duplicate entry"; case RESULT_ERR_DUPLICATE_NAME: return "ERR: duplicate name"; case RESULT_ERR_BUS_LOST: return "ERR: arbitration lost"; - case RESULT_ERR_ARB_RUNNING: return "ERR: arbitration running"; + case RESULT_ERR_ARB_RUNNING: return "ERR: arbitration running"; case RESULT_ERR_CRC: return "ERR: CRC error"; case RESULT_ERR_ACK: return "ERR: ACK error"; case RESULT_ERR_NAK: return "ERR: NAK received"; diff --git a/src/lib/ebus/result.h b/src/lib/ebus/result.h index 0ab0f9e89..b867bd1e3 100755 --- a/src/lib/ebus/result.h +++ b/src/lib/ebus/result.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/ebusd/states.png b/src/lib/ebus/states.png similarity index 100% rename from src/ebusd/states.png rename to src/lib/ebus/states.png diff --git a/src/lib/ebus/stringhelper.cpp b/src/lib/ebus/stringhelper.cpp index 2ff6e6eed..bbb60e304 100644 --- a/src/lib/ebus/stringhelper.cpp +++ b/src/lib/ebus/stringhelper.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2022 John Baier + * Copyright (C) 2022-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -142,14 +142,23 @@ const string StringReplacer::str() const { void StringReplacer::ensureDefault(const string& separator) { if (m_parts.empty()) { m_parts.emplace_back(string(PACKAGE) + separator, -1); - } else if (m_parts.size() == 1 && m_parts[0].second < 0 && m_parts[0].first.find('/') == string::npos) { - m_parts[0] = {m_parts[0].first + separator, -1}; // ensure trailing slash } if (!has("circuit")) { + if (m_parts.back().second >= 0) { + // add separator between two variables + m_parts.emplace_back(separator, -1); + } else if (m_parts.back().first.back() != '/') { + m_parts[m_parts.size() - 1] = {m_parts.back().first + separator, -1}; // ensure trailing slash + } m_parts.emplace_back("circuit", 0); // index of circuit in knownFieldNames - m_parts.emplace_back(separator, -1); } if (!has("name")) { + if (m_parts.back().second >= 0) { + // add separator between two variables + m_parts.emplace_back(separator, -1); + } else if (m_parts.back().first.back() != '/') { + m_parts[m_parts.size() - 1] = {m_parts.back().first + separator, -1}; // ensure trailing slash + } m_parts.emplace_back("name", 1); // index of name in knownFieldNames } } @@ -297,8 +306,12 @@ bool StringReplacer::checkMatchability() const { return true; } -ssize_t StringReplacer::match(const string& str, string* circuit, string* name, string* field, - const string& separator) const { +ssize_t StringReplacer::match(const string& strIn, string* circuit, string* name, string* field, + const string& separator, bool ignoreCase) const { + string str = strIn; + if (ignoreCase) { + FileReader::tolower(&str); + } size_t last = 0; size_t count = m_parts.size(); size_t idx; @@ -314,7 +327,11 @@ ssize_t StringReplacer::match(const string& str, string* circuit, string* name, } string value; if (idx+1 < count) { - size_t pos = str.find(m_parts[idx+1].first, last); + string chk = m_parts[idx+1].first; + if (ignoreCase) { + FileReader::tolower(&chk); + } + size_t pos = str.find(chk, last); if (pos == string::npos) { // next part not found, consume the rest and mark incomplete value = str.substr(last); @@ -364,16 +381,14 @@ void StringReplacers::parseLine(const string& line) { if (pos == string::npos || pos == 0) { return; } - bool emptyIfMissing = false; - string key; - if (line[pos-1] == '?') { - emptyIfMissing = true; - key = line.substr(0, pos-1); - } else { - key = line.substr(0, pos); - } + bool emptyIfMissing = line[pos-1] == '?'; + bool append = !emptyIfMissing && line[pos-1] == '+'; + string key = line.substr(0, (emptyIfMissing || append) ? pos-1 : pos); FileReader::trim(&key); string value = line.substr(pos+1); + if (append) { + value = get(key).str() + value; + } FileReader::trim(&value); if (value.find('%') == string::npos) { set(key, value); // constant value @@ -490,7 +505,7 @@ bool StringReplacers::set(const string& key, const string& value, bool removeRep } void StringReplacers::set(const string& key, int value) { - std::ostringstream str; + ostringstream str; str << static_cast(value); m_constants[key] = str.str(); } @@ -510,13 +525,13 @@ void StringReplacers::reduce(bool compress) { ++it; continue; } - bool restart = set(it->first, str, false); + string key = it->first; it = m_replacers.erase(it); + bool restart = set(key, str, false); reduced = true; if (restart) { - string upper = it->first; - transform(upper.begin(), upper.end(), upper.begin(), ::toupper); - if (m_replacers.erase(upper) > 0) { + transform(key.begin(), key.end(), key.begin(), ::toupper); + if (m_replacers.erase(key) > 0) { break; // restart as iterator is now invalid } } diff --git a/src/lib/ebus/stringhelper.h b/src/lib/ebus/stringhelper.h index b927a3a10..0b5a5fdf5 100644 --- a/src/lib/ebus/stringhelper.h +++ b/src/lib/ebus/stringhelper.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2022 John Baier + * Copyright (C) 2022-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -151,9 +151,11 @@ class StringReplacer { * @param name pointer to the string receiving the message name if present. * @param field pointer to the string receiving the field name if present. * @param separator the separator expected in the extra non-matched non-field parts (default slash). + * @param ignoreCase true to ignore case. * @return the index of the last unmatched part, or the negative index minus one for extra non-matched non-field parts. */ - ssize_t match(const string& str, string* circuit, string* name, string* field, const string& separator = "/") const; + ssize_t match(const string& str, string* circuit, string* name, string* field, const string& separator = "/", + bool ignoreCase = false) const; private: /** @@ -162,7 +164,7 @@ class StringReplacer { * the number is negative for plain strings, the index to @a knownFieldNames for a known field, or the size of * @a knownFieldNames for an unknown field. */ - vector> m_parts; + vector> m_parts; /** true when the complete result is supposed to be empty when at least one referenced variable * is empty or not defined. */ diff --git a/src/lib/ebus/symbol.cpp b/src/lib/ebus/symbol.cpp index cc8a92990..e5c188560 100755 --- a/src/lib/ebus/symbol.cpp +++ b/src/lib/ebus/symbol.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -56,12 +56,12 @@ static const symbol_t CRC_LOOKUP_TABLE[] = { unsigned int parseInt(const char* str, int base, unsigned int minValue, unsigned int maxValue, - result_t* result, size_t* length) { + result_t* result, size_t* length, bool allowIncomplete) { char* strEnd = nullptr; unsigned long ret = strtoul(str, &strEnd, base); - if (strEnd == nullptr || strEnd == str || *strEnd != 0) { + if (strEnd == nullptr || strEnd == str || (!allowIncomplete && *strEnd != 0)) { *result = RESULT_ERR_INVALID_NUM; // invalid value return 0; } @@ -83,7 +83,7 @@ int parseSignedInt(const char* str, int base, int minValue, int maxValue, long ret = strtol(str, &strEnd, base); - if (strEnd == nullptr || (!allowIncomplete && *strEnd != 0)) { + if (strEnd == nullptr || strEnd == str || (!allowIncomplete && *strEnd != 0)) { *result = RESULT_ERR_INVALID_NUM; // invalid value return 0; } @@ -145,14 +145,21 @@ result_t SymbolString::parseHexEscaped(const string& str) { return inEscape ? RESULT_ERR_ESC : RESULT_OK; } -const string SymbolString::getStr(size_t skipFirstSymbols) const { +const string SymbolString::getStr(size_t skipFirstSymbols, size_t maxLength, bool withLength) const { ostringstream sstr; + if (maxLength == 0) { + maxLength = m_data.size(); + } + size_t lengthOffset = withLength ? 254 : (m_isMaster ? 4 : 0); for (size_t i = 0; i < m_data.size(); i++) { if (skipFirstSymbols > 0) { skipFirstSymbols--; - } else { + } else if (i != lengthOffset) { sstr << nouppercase << setw(2) << hex << setfill('0') << static_cast(m_data[i]); + if (--maxLength == 0) { + break; + } } } return sstr.str(); diff --git a/src/lib/ebus/symbol.h b/src/lib/ebus/symbol.h index bae2b2c20..81180b8fc 100755 --- a/src/lib/ebus/symbol.h +++ b/src/lib/ebus/symbol.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -72,38 +72,40 @@ using std::ostringstream; /** the base type for symbols sent to/from the eBUS. */ typedef unsigned char symbol_t; -/** escape symbol, either followed by 0x00 for the value 0xA9, or 0x01 for the value 0xAA. */ -#define ESC ((symbol_t)0xA9) - -/** synchronization symbol. */ -#define SYN ((symbol_t)0xAA) - -/** positive acknowledge symbol. */ -#define ACK ((symbol_t)0x00) - -/** negative acknowledge symbol. */ -#define NAK ((symbol_t)0xFF) - -/** the broadcast destination address. */ -#define BROADCAST ((symbol_t)0xFE) +/** + * List of predefined eBUS symbols. + */ +enum PredefinedSymbol : symbol_t { + /** escape symbol, either followed by 0x00 for the value 0xA9, or 0x01 for the value 0xAA. */ + ESC = 0xA9, + /** synchronization symbol. */ + SYN = 0xAA, + /** positive acknowledge symbol. */ + ACK = 0x00, + /** negative acknowledge symbol. */ + NAK = 0xFF, + /** the broadcast destination address. */ + BROADCAST = 0xFE, +}; /** * Parse an unsigned int value. * @param str the string to parse. - * @param base the numerical base. + * @param base the numerical base (or 0 to determine from the prefix). * @param minValue the minimum resulting value. * @param maxValue the maximum resulting value. * @param result the variable in which to store an error code when parsing failed or the value is out of bounds. * @param length the optional variable in which to store the number of read characters. + * @param allowIncomplete true to allow parsing less than the complete string. * @return the parsed value. */ unsigned int parseInt(const char* str, int base, unsigned int minValue, unsigned int maxValue, - result_t* result, size_t* length = nullptr); + result_t* result, size_t* length = nullptr, bool allowIncomplete = false); /** * Parse a signed int value. * @param str the string to parse. - * @param base the numerical base. + * @param base the numerical base (or 0 to determine from the prefix). * @param minValue the minimum resulting value. * @param maxValue the maximum resulting value. * @param result the variable in which to store an error code when parsing failed or the value is out of bounds. @@ -156,9 +158,11 @@ class SymbolString { /** * Return the symbols as hex string. * @param skipFirstSymbols the number of first symbols to skip. + * @param maxLength the maximum number of symbols to include (or 0 for all). + * @param withLength whether to include the NN length field. * @return the symbols as hex string. */ - const string getStr(size_t skipFirstSymbols = 0) const; + const string getStr(size_t skipFirstSymbols = 0, size_t maxLength = 0, bool withLength = true) const; /** * Dump the data in JSON format to the output. @@ -283,7 +287,7 @@ class SymbolString { } /** - * Return the calculated number of data bytes DD (nnot yet revealed in the length field). + * Return the calculated number of data bytes DD (not yet revealed in the length field). * @return the calculated number of data bytes DD. */ size_t getCalculatedDataSize() const { @@ -294,6 +298,12 @@ class SymbolString { return m_data.size() - lengthOffset - 1; } + /** + * Return a pointer to the data bytes. + * @return a pointer to the data bytes. + */ + const symbol_t* data() const { return m_data.data(); } + /** * Return the data byte at the specified index (within DD). * @param index the index of the data byte (within DD) to return. @@ -324,7 +334,7 @@ class SymbolString { * Return whether the byte sequence is complete with regard to the header and length field. * @return true if the sequence is complete. */ - bool isComplete() { + bool isComplete() const { size_t lengthOffset = (m_isMaster ? 4 : 0); if (m_data.size() < lengthOffset + 1) { return false; @@ -370,6 +380,12 @@ class MasterSymbolString : public SymbolString { */ MasterSymbolString() : SymbolString(true) {} + /** + * Copy constructor. + * @param str the @a MasterSymbolString to copy from. + */ + MasterSymbolString(const MasterSymbolString& str) : SymbolString(str) {} + MasterSymbolString& operator=(const MasterSymbolString& other) { this->m_data = other.m_data; this->m_isMaster = true; @@ -381,13 +397,6 @@ class MasterSymbolString : public SymbolString { this->m_isMaster = true; return *this; } - - private: - /** - * Copy constructor. - * @param str the @a MasterSymbolString to copy from. - */ - MasterSymbolString(const MasterSymbolString& str) : SymbolString(str) {} }; @@ -401,6 +410,12 @@ class SlaveSymbolString : public SymbolString { */ SlaveSymbolString() : SymbolString(false) {} + /** + * Copy constructor. + * @param str the @a SlaveSymbolString to copy from. + */ + SlaveSymbolString(const SlaveSymbolString& str) : SymbolString(str) {} + SlaveSymbolString& operator=(const SlaveSymbolString& other) { this->m_data = other.m_data; this->m_isMaster = false; @@ -412,13 +427,6 @@ class SlaveSymbolString : public SymbolString { this->m_isMaster = false; return *this; } - - private: - /** - * Copy constructor. - * @param str the @a SlaveSymbolString to copy from. - */ - SlaveSymbolString(const SlaveSymbolString& str) : SymbolString(str) {} }; diff --git a/src/lib/ebus/test/test_data.cpp b/src/lib/ebus/test/test_data.cpp index 9f303805a..9082ef840 100755 --- a/src/lib/ebus/test/test_data.cpp +++ b/src/lib/ebus/test/test_data.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ #include #include +#include #include #include #include "lib/ebus/data.h" @@ -25,6 +26,8 @@ using namespace ebusd; using std::cout; using std::endl; +using std::isnan; +using std::setprecision; static bool error = false; @@ -50,15 +53,18 @@ void verify(bool expectFailMatch, string type, string input, class TestReader : public MappedFileReader { public: - TestReader(DataFieldTemplates* templates, bool isSet, bool isMasterDest) + TestReader(DataFieldTemplates* templates, bool isSet, bool isMasterDest, bool withRange) : MappedFileReader::MappedFileReader(true), m_templates(templates), m_isSet(isSet), m_isMasterDest(isMasterDest), - m_fields(nullptr) {} + m_withRange(withRange), m_fields(nullptr) {} result_t getFieldMap(const string& preferLanguage, vector* row, string* errorDescription) const override { if (row->empty()) { row->push_back("*name"); row->push_back("part"); row->push_back("type"); row->push_back("divisor/values"); + if (m_withRange) { + row->push_back("range"); + } row->push_back("unit"); row->push_back("comment"); return RESULT_OK; @@ -83,16 +89,23 @@ class TestReader : public MappedFileReader { const DataFieldTemplates* m_templates; const bool m_isSet; const bool m_isMasterDest; + const bool m_withRange; public: const DataField* m_fields; }; +typedef struct floatCheck_t { + float encval; + float decval; + uint16_t ival; +} floatCheck_t; + int main() { // entry: definition, decoded value, master data, slave data, flags // definition: name,part,type[:len][,[divisor|values][,[unit][,[comment]]]] unsigned int baseLine = __LINE__+1; - string checks[][5] = { + string checks[][6] = { {"x,,ign:10", "", "10fe07000a00000000000000000000", "00", ""}, {"x,,ign:*", "", "10fe07000a00000000000000000000", "00", "W"}, {"x,,ign,2", "", "", "", "c"}, @@ -109,6 +122,8 @@ int main() { {"x,,str:10,==dummy", "", "10fe07000a48616c6c6f2044752120", "00", "rW"}, {"x,,str:10,=dummy", "", "10fe07000a64756d6d792020202020", "00", ""}, {"x,,str:10,==dummy", "", "10fe07000a64756d6d792020202020", "00", ""}, + {",,str:5,=dummy", "\n \"0\": {\"name\": \"\", \"value\": \"dummy\"}", "10fe07000a64756d6d792020202020", "00", "j"}, + {",,str:5,==\"dummy\"","\n \"0\": {\"name\": \"\", \"value\": \"dummy\"}", "10fe07000a64756d6d792020202020", "00", "j"}, {"x,,nts:10", "Hallo, Du!", "10fe07000a48616c6c6f2c20447521", "00", ""}, {"x,,nts:10", "Hallo, Du!", "10fe07000a48616c6c6f2c20447521", "00", ""}, {"x,,nts:10", "Hallo, Du", "10fe07000a48616c6c6f2c20447500", "00", ""}, @@ -134,11 +149,13 @@ int main() { {"x,,bda", "20.02.2021", "10fe07000420020621", "00", ""}, // Saturday {"x,,bda", "31.12.2099", "10fe07000431120499", "00", ""}, // Thursday {"x,,bda", "-.-.-", "10fe070004ffff00ff", "00", ""}, + {"x,,bda", "-.-.-", "10fe07000400000000", "00", "W"}, {"x,,bda", "", "10fe07000432100014", "00", "rw"}, {"x,,bda:3", "26.10.2014", "10fe070003261014", "00", ""}, {"x,,bda:3", "01.01.2000", "10fe070003010100", "00", ""}, {"x,,bda:3", "31.12.2099", "10fe070003311299", "00", ""}, {"x,,bda:3", "-.-.-", "10fe070003ffffff", "00", ""}, + {"x,,bda:3", "-.-.-", "10fe070003000000", "00", "W"}, {"x,,bda:3", "", "10fe070003321299", "00", "rw"}, {"x,,bda,2", "", "", "", "c"}, {"x,,bdz", "26.10.2014", "10fe07000426100614", "00", ""}, // Sunday @@ -146,16 +163,19 @@ int main() { {"x,,bdz", "20.02.2021", "10fe07000420020521", "00", ""}, // Saturday {"x,,bdz", "31.12.2099", "10fe07000431120399", "00", ""}, // Thursday {"x,,bdz", "-.-.-", "10fe070004ffff00ff", "00", ""}, + {"x,,bdz", "-.-.-", "10fe07000400000000", "00", "W"}, {"x,,bdz", "", "10fe07000432100014", "00", "rw"}, {"x,,hda", "26.10.2014", "10fe0700041a0a070e", "00", ""}, // Sunday {"x,,hda", "01.01.2000", "10fe07000401010600", "00", ""}, // Saturday {"x,,hda", "31.12.2099", "10fe0700041f0c0463", "00", ""}, // Thursday {"x,,hda", "-.-.-", "10fe070004ffff00ff", "00", ""}, + {"x,,hda", "-.-.-", "10fe07000400000000", "00", "W"}, {"x,,hda", "", "10fe070004200c0463", "00", "rw"}, {"x,,hda:3", "26.10.2014", "10fe0700031a0a0e", "00", ""}, {"x,,hda:3", "01.01.2000", "10fe070003010100", "00", ""}, {"x,,hda:3", "31.12.2099", "10fe0700031f0c63", "00", ""}, {"x,,hda:3", "-.-.-", "10fe070003ffffff", "00", ""}, + {"x,,hda:3", "-.-.-", "10fe070003000000", "00", "W"}, {"x,,hda:3", "", "10fe070003200c63", "00", "rw"}, {"x,,hda,2", "", "", "", "c"}, {"x,,day", "26.10.2014", "10fe070002d0a3", "00", ""}, @@ -165,7 +185,8 @@ int main() { {"x,,day", "", "10fe0700020000", "00", "Rw"}, {"x,,dtm", "01.01.2009 00:00", "10fe07000400000000", "00", ""}, {"x,,dtm", "31.12.2099 23:59", "10fe0700041f4eda02", "00", ""}, - {"x,,dtm", "16.12.2020 16:51", "10fe07000453f85f00", "00", ""}, + {"x,,dtm", "16.12.2024 16:51", "10fe07000473128000", "00", ""}, + {"x,,dtm", "24.12.2025 16:51", "10fe07000493448800", "00", ""}, {"x,,bti", "21:04:58", "10fe070003580421", "00", ""}, {"x,,bti", "00:00:00", "10fe070003000000", "00", ""}, {"x,,bti", "23:59:59", "10fe070003595923", "00", ""}, @@ -327,6 +348,16 @@ int main() { {"x,,uch,==48", "", "10feffff01ab", "00", "rW"}, {"x,,uch,=48", "", "10feffff0130", "00", ""}, {"x,,uch,==48", "", "10feffff0130", "00", ""}, + {"x,,uch,,1-3", "2", "10feffff0102", "00", "-"}, + {"x,,uch,,1-3", "4", "10feffff0102", "00", "-Rw:ERR: argument value out of valid range"}, + {"x,,uch,,1-3", "2", "10feffff0104", "00", "-rW:ERR: argument value out of valid range"}, + {"x,,uch,,0x1-0x3", "4","10feffff0102", "00", "-Rw:ERR: argument value out of valid range"}, + {"x,,uch,,0x1-0x3", "2","10feffff0104", "00", "-rW:ERR: argument value out of valid range"}, + {"x,,uch", "\n \"x\": {\"value\": 2}", "10feffff0102", "00", "-jV", "\n { \"name\": \"x\", \"slave\": false, \"type\": \"UCH\", \"isbits\": false, \"isadjustable\": false, \"isignored\": false, \"isreverse\": false, \"length\": 1, \"result\": \"number\", \"min\": 0, \"max\": 254, \"step\": 1, \"unit\": \"\", \"comment\": \"\"}"}, + {"x,,uch,,1-3:2", "\n \"x\": {\"value\": 2}", "10feffff0102", "00", "-jV", "\n { \"name\": \"x\", \"slave\": false, \"type\": \"UCH\", \"isbits\": false, \"isadjustable\": false, \"isignored\": false, \"isreverse\": false, \"length\": 1, \"result\": \"number\", \"min\": 1, \"max\": 3, \"step\": 2, \"unit\": \"\", \"comment\": \"\"}"}, + {"x,,u1l", "0", "10feffff0100", "00", ""}, + {"x,,u1l", "255", "10feffff01ff", "00", ""}, + {"x,,u1l", "-", "10feffff01ff", "00", "Rw"}, {"x,,sch", "-90", "10feffff01a6", "00", ""}, {"x,,sch", "0", "10feffff0100", "00", ""}, {"x,,sch", "-1", "10feffff01ff", "00", ""}, @@ -335,6 +366,19 @@ int main() { {"x,,sch", "127", "10feffff017f", "00", ""}, {"x,,sch,10", "-9.0", "10feffff01a6", "00", ""}, {"x,,sch,-10", "-900", "10feffff01a6", "00", ""}, + {"x,,sch,,1-3", "2", "10feffff0102", "00", "-"}, + {"x,,sch,,1-500", "-", "10feffff0180", "00", "-c"}, + {"x,,sch,,-130-1", "-", "10feffff0180", "00", "-c"}, + {"x,,sch,,-127-127", "-", "10feffff0180", "00", "-"}, + {"x,,sch,,-127-128", "-", "10feffff0180", "00", "-c"}, + {"x,,sch,,-128-127", "-", "10feffff0180", "00", "-c"}, + {"x,,sch,,1-3", "4", "10feffff0102", "00", "-Rw:ERR: argument value out of valid range"}, + {"x,,sch,,1-3", "2", "10feffff0104", "00", "-rW:ERR: argument value out of valid range"}, + {"x,,sch,,-3--1", "-4", "10feffff01fe", "00", "-Rw:ERR: argument value out of valid range"}, + {"x,,s1l", "0", "10feffff0100", "00", ""}, + {"x,,s1l", "127", "10feffff017f", "00", ""}, + {"x,,s1l", "-128", "10feffff0180", "00", ""}, + {"x,,s1l", "-", "10feffff01ff", "00", "Rw"}, {"x,,d1b", "-90", "10feffff01a6", "00", ""}, {"x,,d1b", "0", "10feffff0100", "00", ""}, {"x,,d1b", "-1", "10feffff01ff", "00", ""}, @@ -378,6 +422,22 @@ int main() { {"x,,sir", "32767", "10feffff027fff", "00", ""}, {"x,,sir,10", "-9.0", "10feffff02ffa6", "00", ""}, {"x,,sir,-10", "-900", "10feffff02ffa6", "00", ""}, + {"x,,s2l", "-90", "10feffff02a6ff", "00", ""}, + {"x,,s2l", "0", "10feffff020000", "00", ""}, + {"x,,s2l", "-1", "10feffff02ffff", "00", ""}, + {"x,,s2l", "-32768", "10feffff020080", "00", ""}, + {"x,,s2l", "-32767", "10feffff020180", "00", ""}, + {"x,,s2l", "32767", "10feffff02ff7f", "00", ""}, + {"x,,s2l,10", "-9.0", "10feffff02a6ff", "00", ""}, + {"x,,s2l,-10", "-900", "10feffff02a6ff", "00", ""}, + {"x,,s2b", "-90", "10feffff02ffa6", "00", ""}, + {"x,,s2b", "0", "10feffff020000", "00", ""}, + {"x,,s2b", "-1", "10feffff02ffff", "00", ""}, + {"x,,s2b", "-32768", "10feffff028000", "00", ""}, + {"x,,s2b", "-32767", "10feffff028001", "00", ""}, + {"x,,s2b", "32767", "10feffff027fff", "00", ""}, + {"x,,s2b,10", "-9.0", "10feffff02ffa6", "00", ""}, + {"x,,s2b,-10", "-900", "10feffff02ffa6", "00", ""}, {"x,,u3n", "38", "10feffff03260000", "00", ""}, {"x,,u3n", "0", "10feffff03000000", "00", ""}, {"x,,u3n", "16777214", "10feffff03feffff", "00", ""}, @@ -412,6 +472,12 @@ int main() { {"x,,flt", "-", "10feffff020080", "00", ""}, {"x,,flt", "-32.767", "10feffff020180", "00", ""}, {"x,,flt", "32.767", "10feffff02ff7f", "00", ""}, + {"x,,flt,,1-3", "2.000", "10feffff02d007", "00", "-"}, + {"x,,flt,,1-3", "4.000", "10feffff02d007", "00", "-Rw:ERR: argument value out of valid range"}, + {"x,,flt,,1-3", "2.000", "10feffff02a00f", "00", "-rW:ERR: argument value out of valid range"}, + {"x,,flt,,-3--1", "-4", "10feffff0230f8", "00", "-Rw:ERR: argument value out of valid range"}, // -4:60f0, -2:30f8 + {"x,,flt,,-3.1--1.0", "-4", "10feffff0230f8", "00", "-Rw:ERR: argument value out of valid range"}, + {"x,,flt,,-3.1--1.0", "-2", "10feffff0260f0", "00", "-rW:ERR: argument value out of valid range"}, {"x,,flr", "-0.090", "10feffff02ffa6", "00", ""}, {"x,,flr", "0.000", "10feffff020000", "00", ""}, {"x,,flr", "-0.001", "10feffff02ffff", "00", ""}, @@ -421,18 +487,22 @@ int main() { {"x,,exp", "-0.09", "10feffff04ec51b8bd", "00", ""}, {"x,,exp", "0.0", "10feffff0400000000", "00", ""}, {"x,,exp", "-0.001", "10feffff046f1283ba", "00", ""}, - {"x,,exp", "-", "10feffff040000807f", "00", ""}, + {"x,,exp", "-", "10feffff040000c07f", "00", ""}, {"x,,exp", "-32.767", "10feffff04681103c2", "00", ""}, {"x,,exp,1000", "-0.000090000", "10feffff04ec51b8bd", "00", ""}, {"x,,exp,-100", "-9", "10feffff04ec51b8bd", "00", ""}, {"x,,exp", "0.25", "10feffff040000803e", "00", ""}, - {"x,,exp", "-", "10feffff040000c07f", "00", "W"}, {"x,,exp", "0.95", "10feffff043333733f", "00", ""}, {"x,,exp", "0.65", "10feffff046666263f", "00", ""}, + {"x,,exp", "0.065", "10feffff04b81e853d", "00", ""}, + {"x,,exp,,0-0.65", "0.65", "10feffff046666263f", "00", "-"}, + {"x,,exp,,0-0.5", "0.65", "10feffff046666263f", "00", "-rw:ERR: argument value out of valid range"}, + {"x,,exp,10,0-0.065", "0.0650000", "10feffff046666263f", "00", "-"}, + {"x,,exp,10,0-0.05", "0.0650000", "10feffff046666263f", "00", "-rw:ERR: argument value out of valid range"}, {"x,,exr", "-0.09", "10feffff04bdb851ec", "00", ""}, {"x,,exr", "0.0", "10feffff0400000000", "00", ""}, {"x,,exr", "-0.001", "10feffff04ba83126f", "00", ""}, - {"x,,exr", "-", "10feffff047f800000", "00", ""}, + {"x,,exr", "-", "10feffff047fc00000", "00", ""}, {"x,,exr", "-32.767", "10feffff04c2031168", "00", ""}, {"x,,exr,1000", "-0.000090000", "10feffff04bdb851ec", "00", ""}, {"x,,exr,-100", "-9", "10feffff04bdb851ec", "00", ""}, @@ -561,6 +631,12 @@ int main() { continue; } string flags = check[4]; + size_t colon = flags.find(':'); + string errStr; + if (colon != string::npos) { + errStr = flags.substr(colon+1); + flags = flags.substr(0, colon); + } bool isSet = flags.find('s') != string::npos; bool testFields = flags.find('F') != string::npos; bool failedCreate = flags.find('c') != string::npos; @@ -568,6 +644,7 @@ int main() { bool failedReadMatch = flags.find('R') != string::npos; bool failedWrite = flags.find('w') != string::npos; bool failedWriteMatch = flags.find('W') != string::npos; + string withDump = check[5]; // optional const char* findName = flags.find('I') == string::npos ? nullptr : "x"; ssize_t findIndex = -1; if (flags.find('i') != string::npos) { @@ -583,6 +660,9 @@ int main() { if (flags.find("vvv") != string::npos) { verbosity |= OF_COMMENTS; } + if (flags.find("V") != string::npos) { + verbosity |= OF_NAMES|OF_UNITS|OF_COMMENTS|OF_ALL_ATTRS; + } if (flags.find('j') != string::npos) { verbosity |= OF_JSON; } @@ -604,7 +684,8 @@ int main() { } continue; } - TestReader reader{templates, isSet, mstr[1] == BROADCAST || isMaster(mstr[1])}; + bool withRange = flags.find('-') != string::npos; + TestReader reader{templates, isSet, mstr[1] == BROADCAST || isMaster(mstr[1]), withRange}; lineNo = 0; dummystr.clear(); dummystr.str("#"); @@ -622,6 +703,9 @@ int main() { if (result == RESULT_OK) { cout << "\"" << check[0] << "\": failed create error: unexpectedly succeeded" << endl; error = true; + } else if (!errStr.empty() && errorDescription != errStr) { + cout << "\"" << check[0] << "\": failed create error: unexpected result \"" << errorDescription << "\" instead of \"" << errStr << "\"" << endl; + error = true; } else { cout << "\"" << check[0] << "\": failed create OK" << endl; } @@ -664,6 +748,10 @@ int main() { cout << " failed read " << fields->getName(-1) << " >" << check[2] << " " << check[3] << "< error: unexpectedly succeeded" << endl; error = true; + } else if (!errStr.empty() && getResultCode(result) != errStr) { + cout << " failed read " << fields->getName(-1) << " >" << check[2] << " " << check[3] + << "< error: unexpected result \""; + cout << getResultCode(result) << "\" instead of \"" << errStr << "\"" << endl; } else { cout << " failed read " << fields->getName(-1) << " >" << check[2] << " " << check[3] << "< OK" << endl; @@ -728,6 +816,10 @@ int main() { cout << " failed write " << fields->getName(-1) << " >" << expectStr << "< error: unexpectedly succeeded" << endl; error = true; + } else if (!errStr.empty() && getResultCode(result) != errStr) { + cout << " failed write " << fields->getName(-1) << " >" + << "< error: unexpected result \""; + cout << getResultCode(result) << "\" instead of \"" << errStr << "\"" << endl; } else { cout << " failed write " << fields->getName(-1) << " >" << expectStr << "< OK" << endl; @@ -744,11 +836,56 @@ int main() { writeMstr.getStr() + " " + writeSstr.getStr()); } } + if (!withDump.empty()) { + output.clear(); + output.str(""); + fields->dump(false, verbosity, &output); + bool match = output.str() == withDump; + verify(false, "dump", withDump, match, withDump, output.str()); + } delete fields; fields = nullptr; } delete templates; + const floatCheck_t floatChecks[] = { + {0.0f, 0.0f, 0}, + {0.01f, 0.01f, 1}, + {-0.01f, -0.01f, 0x8000|(0x800-1)}, + {0.09f, 0.09f, 9}, + {-0.09f, -0.09f, 0x8000|(0x800-9)}, + {0.99f, 0.99f, 99}, + {-0.99f, -0.99f, 0x8000|(0x800-99)}, + {1.0f, 1.0f, 100}, + {-1.0f, -1.0f, 0x8000|(0x800-100)}, + {670433.25f, 670433.5f, 0x7ffe}, + {-670760.94f, -671088.62f, 0xf801}, + {NAN, NAN, 0x7fff}, + // some extra tests: + {-30.0f, -30.0f, 0x8A24}, + {-327.68f, -327.68f, 0xAC00}, + {46039.04f, 46039.04f, 0x6464}, + {-9461.76f, -9461.76f, 0xC8C8}, + }; + for (unsigned int i = 0; i < sizeof(floatChecks) / sizeof(floatChecks[0]); i++) { + floatCheck_t check = floatChecks[i]; + float value = uint16ToFloat(check.ival); + cout << " parse 0x" << hex << check.ival; + if (isnan(value) ? !isnan(check.encval) : value != check.encval) { + cout << " invalid: " << setprecision(8) << value << " instead of " << setprecision(8) << check.encval << endl; + error = true; + } else { + cout << ": OK" << endl; + } + uint32_t ivalue = floatToUint16(check.decval); + cout << " format " << setprecision(8) << check.decval; + if (ivalue != check.ival) { + cout << " invalid: " << ivalue << " instead of " << check.ival << endl; + error = true; + } else { + cout << ": OK" << endl; + } + } return error ? 1 : 0; } diff --git a/src/lib/ebus/test/test_device.cpp b/src/lib/ebus/test/test_device.cpp index 2b491ad3c..c70255347 100755 --- a/src/lib/ebus/test/test_device.cpp +++ b/src/lib/ebus/test/test_device.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/ebus/test/test_filereader.cpp b/src/lib/ebus/test/test_filereader.cpp index 61ef0ec5f..4bd3164d7 100755 --- a/src/lib/ebus/test/test_filereader.cpp +++ b/src/lib/ebus/test/test_filereader.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/ebus/test/test_message.cpp b/src/lib/ebus/test/test_message.cpp index 7b46295a4..648744713 100644 --- a/src/lib/ebus/test/test_message.cpp +++ b/src/lib/ebus/test/test_message.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -54,27 +54,29 @@ DataFieldTemplates* templates = nullptr; namespace ebusd { -DataFieldTemplates* getTemplates(const string& filename) { - if (filename == "") { // avoid compiler warning +class TestResolver : public Resolver { + public: + virtual DataFieldTemplates* getTemplates(const string& filename) { + if (filename == "") { // avoid compiler warning + return templates; + } return templates; } - return templates; -} -result_t loadDefinitionsFromConfigPath(FileReader* reader, const string& filename, bool verbose, - map* defaults, string* errorDescription, bool replace = false) { - time_t mtime = 0; - istream* stream = FileReader::openFile(filename, errorDescription, &mtime); - result_t result; - if (stream) { - result = reader->readFromStream(stream, filename, mtime, verbose, defaults, errorDescription); - delete(stream); - } else { - result = RESULT_ERR_NOTFOUND; + virtual result_t loadDefinitionsFromConfigPath(FileReader* reader, const string& filename, + map* defaults, string* errorDescription, bool replace = false) { + time_t mtime = 0; + istream* stream = FileReader::openFile(filename, errorDescription, &mtime); + result_t result; + if (stream) { + result = reader->readFromStream(stream, filename, mtime, false, defaults, errorDescription); + delete(stream); + } else { + result = RESULT_ERR_NOTFOUND; + } + return result; } - return result; -} - +}; } // namespace ebusd @@ -101,15 +103,15 @@ int main() { {"tempsensor,temp;sensor,,Temperatursensor", "", "", "", "template"}, {"tempsensorc,temp;sensorc,,Temperatursensor", "", "", "", "template"}, {"r,cir,Status01,VL/RL/AussenTemp/VLWW/SpeicherTemp/Status,,08,B511,01,,,temp1;temp1;temp2;temp1;temp1;pumpstate", "28.0;24.0;4.938;35.0;41.0;4", "ff08b5110101", "093830f00446520400ff", "d"}, - {"r,message circuit,message name,message comment,,25,B509,0d2800,,,tempsensor", "temp=-14.00 Temperatursensor [Temperatur];sensor=ok [Fühlerstatus]", "ff25b509030d2800", "0320ff00", "D"}, - {"r,message circuit,message name,message comment,,25,B509,0d2800,,,tempsensor,,field unit,field comment", "temp=-14.00 field unit [field comment];sensor=ok [Fühlerstatus]", "ff25b509030d2800", "0320ff00", "D"}, - {"r,message circuit,message name,message comment,,25,B509,0d2800,,,tempsensor,,field unit,field comment", "\n \"temp\": {\"value\": -14.00},\n \"sensor\": {\"value\": \"ok\"}", "ff25b509030d2800", "0320ff00", "j"}, - {"r,message circuit,message name,message comment,,25,B509,0d2800,,,tempsensor,,field unit,field comment", "\n \"temp\": {\"value\": -14.00, \"unit\": \"field unit\", \"comment\": \"field comment\"},\n" " \"sensor\": {\"value\": \"ok\", \"comment\": \"Fühlerstatus\"}", "ff25b509030d2800", "0320ff00", "J"}, - {"r,message circuit,message name,\"message, comment\",,25,B509,0d2800,,,tempsensor,,field unit,\"field, comment\"", "\n \"temp\": {\"value\": -14.00, \"unit\": \"field unit\", \"comment\": \"field, comment\"},\n" " \"sensor\": {\"value\": \"ok\", \"comment\": \"Fühlerstatus\"}", "ff25b509030d2800", "0320ff00", "J"}, - {"r,message circuit,message name,\"message\"\",\"\" comment\",,25,B509,0d2800,,,tempsensor,,field unit,\"field\"\",\"\" comment\"", "\n \"temp\": {\"value\": -14.00, \"unit\": \"field unit\", \"comment\": \"field',' comment\"},\n" " \"sensor\": {\"value\": \"ok\", \"comment\": \"Fühlerstatus\"}", "ff25b509030d2800", "0320ff00", "J"}, - {"r,message circuit,message name,message comment,,25,B509,0d2800,,,temp,,field unit,field comment,,,sensor", "temp=-14.00 field unit [field comment];sensor=ok [Fühlerstatus]", "ff25b509030d2800", "0320ff00", "D"}, - {"r,message circuit,message name,message comment,,25,B509,0d2800,,,temp,,field unit,\"field\"\",\"\" comment\",,,sensor", "temp=-14.00 field unit [field\",\" comment];sensor=ok [Fühlerstatus]", "ff25b509030d2800", "0320ff00", "D"}, - {"r,message circuit,message name,message comment,,25,B509,0d2800,,,D2C,,°C,Temperatur,,,sensor", "\n \"0\": {\"name\": \"\", \"value\": -14.00},\n \"1\": {\"name\": \"sensor\", \"value\": \"ok\"}", "ff25b509030d2800", "0320ff00", "j"}, + {"r,message_circuit,message_name,message comment,,25,B509,0d2800,,,tempsensor", "temp=-14.00 Temperatursensor [Temperatur];sensor=ok [Fühlerstatus]", "ff25b509030d2800", "0320ff00", "D"}, + {"r,message_circuit,message_name,message comment,,25,B509,0d2800,,,tempsensor,,field unit,field comment", "temp=-14.00 field unit [field comment];sensor=ok [Fühlerstatus]", "ff25b509030d2800", "0320ff00", "D"}, + {"r,message_circuit,message_name,message comment,,25,B509,0d2800,,,tempsensor,,field unit,field comment", "\n \"temp\": {\"value\": -14.00},\n \"sensor\": {\"value\": \"ok\"}", "ff25b509030d2800", "0320ff00", "j"}, + {"r,message_circuit,message_name,message comment,,25,B509,0d2800,,,tempsensor,,field unit,field comment", "\n \"temp\": {\"value\": -14.00, \"unit\": \"field unit\", \"comment\": \"field comment\"},\n" " \"sensor\": {\"value\": \"ok\", \"comment\": \"Fühlerstatus\"}", "ff25b509030d2800", "0320ff00", "J"}, + {"r,message_circuit,message_name,\"message, comment\",,25,B509,0d2800,,,tempsensor,,field unit,\"field, comment\"", "\n \"temp\": {\"value\": -14.00, \"unit\": \"field unit\", \"comment\": \"field, comment\"},\n" " \"sensor\": {\"value\": \"ok\", \"comment\": \"Fühlerstatus\"}", "ff25b509030d2800", "0320ff00", "J"}, + {"r,message_circuit,message_name,\"message\"\",\"\" comment\",,25,B509,0d2800,,,tempsensor,,field unit,\"field\"\",\"\" comment\"", "\n \"temp\": {\"value\": -14.00, \"unit\": \"field unit\", \"comment\": \"field',' comment\"},\n" " \"sensor\": {\"value\": \"ok\", \"comment\": \"Fühlerstatus\"}", "ff25b509030d2800", "0320ff00", "J"}, + {"r,message_circuit,message_name,message comment,,25,B509,0d2800,,,temp,,field unit,field comment,,,sensor", "temp=-14.00 field unit [field comment];sensor=ok [Fühlerstatus]", "ff25b509030d2800", "0320ff00", "D"}, + {"r,message_circuit,message_name,message comment,,25,B509,0d2800,,,temp,,field unit,\"field\"\",\"\" comment\",,,sensor", "temp=-14.00 field unit [field\",\" comment];sensor=ok [Fühlerstatus]", "ff25b509030d2800", "0320ff00", "D"}, + {"r,message_circuit,message_name,message comment,,25,B509,0d2800,,,D2C,,°C,Temperatur,,,sensor", "\n \"0\": {\"name\": \"\", \"value\": -14.00},\n \"1\": {\"name\": \"sensor\", \"value\": \"ok\"}", "ff25b509030d2800", "0320ff00", "j"}, {"r,cir,name,,,25,B509,0d2800,,,tempsensorc", "-14.00", "ff25b509030d2800", "0320ff55", ""}, {"r,cir,name,,,25,B509,0d28,,m,sensorc,,,,,,temp", "-14.00", "ff25b509030d2855", "0220ff", ""}, {"u,cir,first,,,fe,0700,,x,,bda", "26.10.2014", "fffe07000426100714", "00", "p"}, @@ -182,6 +184,7 @@ int main() { {"", "19:00", "3110b51503000272", "00", "kd"}, {"*r,cir*cuit#level,na*me,com*ment,ff,75,b509,0d", "", "", "", ""}, {"r,CIRCUIT,NAME,COMMENT,,,,0100,field,,UCH", "r,cirCIRCUITcuit,naNAMEme,comCOMMENTment,ff,75,b509,0d0100,field,s,UCH,,,: field=42", "ff75b509030d0100", "012a", "DN"}, + {"r,CIRCUIT,NAME,COMMENT,,,,0100,field,,UCH", "r,cirCIRCUITcuit,naNAMEme,comCOMMENTment,ff,75,b509,0d0100,field,s,UCH,,,: [b5090d0100/2a] field=[2a]42", "ff75b509030d0100", "012a", "DNr"}, {"r,CIRCUIT,NAME,COMMENT,,,,0100,field,,UCH", " \"naNAMEme\": {\n" " \"name\": \"naNAMEme\",\n" @@ -192,13 +195,34 @@ int main() { " \"zz\": 117,\n" " \"id\": [181, 9, 13, 1, 0],\n" " \"fields\": {\n" - " \"0\": {\"name\": \"field\", \"value\": 42}\n" + " \"field\": {\"value\": 42}\n" " },\n" " \"fielddefs\": [\n" " { \"name\": \"field\", \"slave\": true, \"type\": \"UCH\", \"isbits\": false, \"length\": 1, \"unit\": \"\", \"comment\": \"\"}\n" " ]\n" " }: \n" " \"field\": {\"value\": 42}", "ff75b509030d0100", "012a", "jN"}, + {"r,CIRCUIT,NAME,COMMENT,,,,0100,field,,UCH", + " \"naNAMEme\": {\n" + " \"name\": \"naNAMEme\",\n" + " \"passive\": false,\n" + " \"write\": false,\n" + " \"lastup\": *,\n" + " \"qq\": 255,\n" + " \"zz\": 117,\n" + " \"id\": [181, 9, 13, 1, 0],\n" + " \"master\": [255, 117, 181, 9, 3, 13, 1, 0],\n" + " \"slave\": [1, 42],\n" + " \"fields\": {\n" + " \"field\": {\"value\": 42, \"raw\": [42]}\n" + " },\n" + " \"fielddefs\": [\n" + " { \"name\": \"field\", \"slave\": true, \"type\": \"UCH\", \"isbits\": false, \"length\": 1, \"unit\": \"\", \"comment\": \"\"}\n" + " ]\n" + " }: \n" + " \"field\": {\"value\": 42, \"raw\": [42]}", "ff75b509030d0100", "012a", "jNr"}, + {"r,CIRCUIT,NAME,COMMENT,,,,0100,field,,temp", "r,cirCIRCUITcuit,naNAMEme,comCOMMENTment,ff,75,b509,0d0100,field,s,D2C,,°C,Temperatur: field=42.00 °C [Temperatur]", "ff75b509030d0100", "02a002", "DN"}, + {"r,CIRCUIT,NAME,COMMENT,,,,0100,field,,D2C,,°C,Temperatur", "r,cirCIRCUITcuit,naNAMEme,comCOMMENTment,ff,75,b509,0d0100,field,s,D2C,,°C,Temperatur: field=42.00 °C [Temperatur]", "ff75b509030d0100", "02a002", "DN"}, }; templates = new DataFieldTemplates(); unsigned int lineNo = 0; @@ -208,6 +232,7 @@ int main() { templates->readLineFromStream(&dummystr, __FILE__, false, &lineNo, &row, &errorDescription, false, nullptr, nullptr); lineNo = 0; MessageMap* messages = new MessageMap(""); + messages->setResolver(new TestResolver()); dummystr.clear(); dummystr.str("#"); messages->readLineFromStream(&dummystr, __FILE__, false, &lineNo, &row, &errorDescription, false, nullptr, nullptr); @@ -232,6 +257,10 @@ int main() { bool decodeVerbose = flags.find('D') != string::npos || flags.find('J') != string::npos; bool withMessageDump = flags.find('N') != string::npos; bool decode = decodeJson || decodeVerbose || (flags.find('d') != string::npos); + OutputFormat verbosity = (decodeVerbose?OF_NAMES|OF_UNITS|OF_COMMENTS:OF_NONE)|(decodeJson?OF_NAMES|OF_JSON:OF_NONE); + if (flags.find('r') != string::npos) { + verbosity |= OF_RAWDATA; + } bool failedPrepare = flags.find('p') != string::npos; bool failedPrepareMatch = flags.find('P') != string::npos; bool multi = flags.find('*') != string::npos; @@ -405,7 +434,7 @@ int main() { ostringstream output; if (withMessageDump) { if (decodeJson) { - message->decodeJson(false, false, true, false, OF_JSON|OF_DEFINITION, &output); + message->decodeJson(false, false, true, verbosity|OF_DEFINITION, &output); string str = output.str(); size_t start = str.find("\"lastup\": "); if (start != string::npos) { @@ -422,8 +451,7 @@ int main() { } output << ": "; } - result = message->decodeLastData(false, nullptr, -1, - (decodeVerbose?OF_NAMES|OF_UNITS|OF_COMMENTS:OF_NONE)|(decodeJson?OF_NAMES|OF_JSON:OF_NONE), &output); + result = message->decodeLastData(pt_any, false, nullptr, -1, verbosity, &output); if (result != RESULT_OK) { cout << " \"" << check[2] << "\" / \"" << check[3] << "\": decode error " << (message->isWrite() ? "write: " : "read: ") << getResultCode(result) << endl; diff --git a/src/lib/ebus/test/test_symbol.cpp b/src/lib/ebus/test/test_symbol.cpp index 6da4ad4f1..dc8f1fbd8 100755 --- a/src/lib/ebus/test/test_symbol.cpp +++ b/src/lib/ebus/test/test_symbol.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/ebus/transport.cpp b/src/lib/ebus/transport.cpp new file mode 100644 index 000000000..8488012f0 --- /dev/null +++ b/src/lib/ebus/transport.cpp @@ -0,0 +1,360 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "lib/ebus/transport.h" +#include +#include +#include +#ifdef HAVE_LINUX_SERIAL +# include +#endif +#ifdef HAVE_FREEBSD_UFTDI +# include +#endif +#ifdef HAVE_PPOLL +# include +#endif +#include "lib/ebus/data.h" +#include "lib/utils/tcpsocket.h" +#ifdef DEBUG_RAW_TRAFFIC +# include "lib/utils/clock.h" +#endif + +namespace ebusd { + + +#define MTU 1540 + +#ifndef POLLRDHUP +#define POLLRDHUP 0 +#endif + + +#ifdef DEBUG_RAW_TRAFFIC + #define DEBUG_RAW_TRAFFIC_HEAD(format, args...) fprintf(stdout, "%ld raw: " format, clockGetMillis(), args) + #define DEBUG_RAW_TRAFFIC_ITEM(args...) fprintf(stdout, args) + #define DEBUG_RAW_TRAFFIC_FINAL() fprintf(stdout, "\n"); fflush(stdout) + #undef DEBUG_RAW_TRAFFIC + #define DEBUG_RAW_TRAFFIC(format, args...) fprintf(stdout, "%ld raw: " format "\n", clockGetMillis(), args); fflush(stdout) +#else + #define DEBUG_RAW_TRAFFIC_HEAD(format, args...) + #undef DEBUG_RAW_TRAFFIC_ITEM + #define DEBUG_RAW_TRAFFIC_FINAL() + #define DEBUG_RAW_TRAFFIC(format, args...) +#endif + + + +FileTransport::FileTransport(const char* name, unsigned int latency, bool checkDevice) + : Transport(name, HOST_LATENCY_MS+latency), + m_checkDevice(checkDevice), + m_fd(-1), + m_bufSize(((MAX_LEN+1+3)/4)*4), m_bufLen(0) { + m_buffer = reinterpret_cast(malloc(m_bufSize)); + if (!m_buffer) { + m_bufSize = 0; + } +} + +FileTransport::~FileTransport() { + close(); + if (m_buffer) { + free(m_buffer); + m_buffer = nullptr; + } +} + +result_t FileTransport::open() { + close(); + result_t result; + if (m_bufSize == 0) { + result = RESULT_ERR_DEVICE; + } else { + result = openInternal(); + } + if (m_listener != nullptr && result == RESULT_OK) { + result = m_listener->notifyTransportStatus(true); + } + if (result != RESULT_OK) { + close(); + } + return result; +} + +void FileTransport::close() { + if (m_fd == -1) { + return; + } + ::close(m_fd); + m_fd = -1; + m_bufLen = 0; // flush read buffer + if (m_listener != nullptr) { + m_listener->notifyTransportStatus(false); + } +} + +bool FileTransport::isValid() { + if (m_fd == -1) { + return false; + } + if (m_checkDevice) { + checkDevice(); + } + return m_fd != -1; +} + +result_t FileTransport::write(const uint8_t* data, size_t len) { + if (!isValid()) { + return RESULT_ERR_DEVICE; + } +#ifdef DEBUG_RAW_TRAFFIC_ITEM + DEBUG_RAW_TRAFFIC_HEAD("%ld >", len); + for (size_t pos=0; pos < len; pos++) { + DEBUG_RAW_TRAFFIC_ITEM(" %2.2x", data[pos]); + } + DEBUG_RAW_TRAFFIC_FINAL(); +#endif + return (::write(m_fd, data, len) == len) ? RESULT_OK : RESULT_ERR_DEVICE; +} + +result_t FileTransport::read(unsigned int timeout, const uint8_t** data, size_t* len) { + if (!isValid()) { + return RESULT_ERR_DEVICE; + } + if (timeout == 0) { + if (m_bufLen > 0) { + *data = m_buffer; + *len = m_bufLen; + return RESULT_OK; + } + return RESULT_ERR_TIMEOUT; + } + if (timeout > 0) { + timeout += m_latency; + int ret; + struct timespec tdiff; + + // set select timeout + tdiff.tv_sec = timeout/1000; + tdiff.tv_nsec = (timeout%1000)*1000000; + +#ifdef HAVE_PPOLL + nfds_t nfds = 1; + struct pollfd fds[nfds]; + + memset(fds, 0, sizeof(fds)); + + fds[0].fd = m_fd; + fds[0].events = POLLIN | POLLERR | POLLHUP | POLLRDHUP; + ret = ppoll(fds, nfds, &tdiff, nullptr); + if (ret >= 0 && fds[0].revents & (POLLERR | POLLHUP | POLLRDHUP)) { + ret = -1; + } +#else +#ifdef HAVE_PSELECT + fd_set readfds, exceptfds; + + FD_ZERO(&readfds); + FD_ZERO(&exceptfds); + FD_SET(m_fd, &readfds); + FD_SET(m_fd, &exceptfds); + + ret = pselect(m_fd + 1, &readfds, nullptr, &exceptfds, &tdiff, nullptr); + if (ret >= 1 && FD_ISSET(m_fd, &exceptfds)) { + ret = -1; + } +#else + ret = 1; // ignore timeout if neither ppoll nor pselect are available +#endif +#endif + if (ret == -1) { + DEBUG_RAW_TRAFFIC("poll error %d", errno); + close(); + return RESULT_ERR_DEVICE; + } + if (ret == 0) { + return RESULT_ERR_TIMEOUT; + } + } + + // directly read byte from device + if (m_bufLen > 0 && m_bufLen > m_bufSize - m_bufSize / 4) { + // more than 3/4 of input buffer consumed is taken as signal that ebusd is too slow + m_bufLen = 0; + if (m_listener != nullptr) { + m_listener->notifyTransportMessage(true, "buffer overflow"); + } + } + // fill up the buffer + ssize_t size = ::read(m_fd, m_buffer + m_bufLen, m_bufSize - m_bufLen); + if (size <= 0) { + return RESULT_ERR_TIMEOUT; + } +#ifdef DEBUG_RAW_TRAFFIC_ITEM + DEBUG_RAW_TRAFFIC_HEAD("%ld+%ld <", m_bufLen, size); + for (int pos=0; pos < size; pos++) { + DEBUG_RAW_TRAFFIC_ITEM(" %2.2x", m_buffer[(m_bufLen+pos)%m_bufSize]); + } + DEBUG_RAW_TRAFFIC_FINAL(); +#endif + m_bufLen += size; + *data = m_buffer; + *len = m_bufLen; + return RESULT_OK; +} + +void FileTransport::readConsumed(size_t len) { + if (len >= m_bufLen) { + m_bufLen = 0; + } else if (len > 0) { + size_t tail = m_bufLen - len; + memmove(m_buffer, m_buffer + len, tail); + DEBUG_RAW_TRAFFIC("move %ld @%ld to 0", tail, len); + m_bufLen = tail; + } +} + + +result_t SerialTransport::openInternal() { + struct termios newSettings; + + // open file descriptor + m_fd = ::open(m_name, O_RDWR | O_NOCTTY | O_NDELAY); + + if (m_fd < 0) { + return RESULT_ERR_NOTFOUND; + } + if (isatty(m_fd) == 0) { + close(); + return RESULT_ERR_NOTFOUND; + } + + if (flock(m_fd, LOCK_EX|LOCK_NB) != 0) { + close(); + return RESULT_ERR_DEVICE; + } + +#ifdef HAVE_LINUX_SERIAL + struct serial_struct serial; + if (ioctl(m_fd, TIOCGSERIAL, &serial) == 0) { + serial.flags |= ASYNC_LOW_LATENCY; + ioctl(m_fd, TIOCSSERIAL, &serial); + } +#endif + +#ifdef HAVE_FREEBSD_UFTDI + int param = 0; + // flush tx/rx and set low latency on uftdi device + if (ioctl(m_fd, UFTDIIOC_GET_LATENCY, ¶m) == 0) { + ioctl(m_fd, UFTDIIOC_RESET_IO, ¶m); + param = 1; + ioctl(m_fd, UFTDIIOC_SET_LATENCY, ¶m); + } +#endif + + // save current settings + tcgetattr(m_fd, &m_oldSettings); + + // create new settings + memset(&newSettings, 0, sizeof(newSettings)); + +#ifdef HAVE_CFSETSPEED + cfsetspeed(&newSettings, m_speed ? (m_speed > 1 ? B115200 : B9600) : B2400); +#else + cfsetispeed(&newSettings, m_speed ? (m_speed > 1 ? B115200 : B9600) : B2400); +#endif + newSettings.c_cflag |= (CS8 | CLOCAL | CREAD); + newSettings.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // non-canonical mode + newSettings.c_iflag |= IGNPAR; // ignore parity errors + newSettings.c_oflag &= ~OPOST; + + // non-canonical mode: read() blocks until at least one byte is available + newSettings.c_cc[VMIN] = 1; + newSettings.c_cc[VTIME] = 0; + + // empty device buffer + tcflush(m_fd, TCIFLUSH); + + // activate new settings of serial device + if (tcsetattr(m_fd, TCSANOW, &newSettings)) { + close(); + return RESULT_ERR_DEVICE; + } + + // set serial device into blocking mode + fcntl(m_fd, F_SETFL, fcntl(m_fd, F_GETFL) & ~O_NONBLOCK); + + return RESULT_OK; +} + +void SerialTransport::close() { + if (m_fd != -1) { + // empty device buffer + tcflush(m_fd, TCIOFLUSH); + + // restore previous settings of the device + tcsetattr(m_fd, TCSANOW, &m_oldSettings); + } + FileTransport::close(); +} + +void SerialTransport::checkDevice() { + int cnt; + if (ioctl(m_fd, FIONREAD, &cnt) == -1) { + close(); + } +} + +result_t NetworkTransport::openInternal() { + // wait up to 5 seconds for established connection + m_fd = socketConnect(m_hostOrIp, m_port, m_udp ? IPPROTO_UDP : 0, nullptr, 5, 2); + if (m_fd < 0) { + return RESULT_ERR_GENERIC_IO; + } + int cnt; + symbol_t buf[MTU]; + int ioerr; + while ((ioerr=ioctl(m_fd, FIONREAD, &cnt)) >= 0 && cnt > 1) { + if (!m_udp) { + break; // no need to skip anything on a fresh TCP connection + } + // skip buffered input + ssize_t read = ::read(m_fd, &buf, MTU); + if (read <= 0) { + break; + } + } + if (ioerr < 0) { + close(); + return RESULT_ERR_GENERIC_IO; + } + return RESULT_OK; +} + +void NetworkTransport::checkDevice() { + int cnt; + if (ioctl(m_fd, FIONREAD, &cnt) < 0) { + close(); + } +} + +} // namespace ebusd diff --git a/src/lib/ebus/transport.h b/src/lib/ebus/transport.h new file mode 100755 index 000000000..66ae1125b --- /dev/null +++ b/src/lib/ebus/transport.h @@ -0,0 +1,335 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIB_EBUS_TRANSPORT_H_ +#define LIB_EBUS_TRANSPORT_H_ + +#include +#include +#include +#include +#include "lib/ebus/result.h" +#include "lib/ebus/symbol.h" + +namespace ebusd { + +/** @file lib/ebus/transport.h + * Classes for low level transport to/from the eBUS device. + * + * A @a Transport is either a @a SerialTransport directly connected + * to a local tty port or a remote @a NetworkTransport handled via a + * socket. + */ + +/** the transfer latency of the network device [ms]. */ +#define NETWORK_LATENCY_MS 30 + +/** the latency of the host [ms]. */ +#if defined(__CYGWIN__) || defined(_WIN32) +#define HOST_LATENCY_MS 20 +#else +#define HOST_LATENCY_MS 10 +#endif + +/** + * Interface for listening to data received on/sent to a @a Transport. + */ +class TransportListener { + public: + /** + * Destructor. + */ + virtual ~TransportListener() {} + + /** + * Called to notify a status change from the @a Transport. + * @param opened true when the transport was successfully opened, false when it was closed or open failed. + * @return the result_t code (other than RESULT_OK if an extra open action was performed unsuccessfully). + */ + virtual result_t notifyTransportStatus(bool opened) = 0; // abstract + + /** + * Called to notify a message from the @a Transport. + * @param error true for an error message, false for an info message. + * @param message the message string. + */ + virtual void notifyTransportMessage(bool error, const char* message) = 0; // abstract +}; + + +/** + * The base class for low level transport to/from the eBUS device. + */ +class Transport { + protected: + /** + * Construct a new instance. + * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). + */ + Transport(const char* name, unsigned int latency) + : m_name(name), m_latency(latency), m_listener(nullptr) {} + + public: + /** + * Destructor. + */ + virtual ~Transport() { } + + /** + * Get the device name. + * @return the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). + */ + const char* getName() const { return m_name; } + + /** + * Get the transfer latency of this device. + * @return the transfer latency in milliseconds. + */ + unsigned int getLatency() const { return m_latency; } + + /** + * Get info about the transport as string. + * @return a @a string describing the transport. + */ + virtual string getTransportInfo() const = 0; // abstract + + /** + * Set the @a TransportListener. + * @param listener the @a TransportListener. + */ + void setListener(TransportListener* listener) { m_listener = listener; } + + /** + * Open the transport. + * @return the @a result_t code. + */ + virtual result_t open() = 0; // abstract + + /** + * Close the device if opened. + */ + virtual void close() = 0; // abstract + + /** + * Return whether the device is opened and available. + * @return whether the device is opened and available. + */ + virtual bool isValid() = 0; // abstract + + /** + * Write arbitrary data to the device. + * @param data the data to send. + * @param len the length of data. + * @return the @a result_t code. + */ + virtual result_t write(const uint8_t* data, size_t len) = 0; // abstract + + /** + * Read data from the device. + * @param timeout maximum time to wait for the byte in milliseconds, or 0 for returning only already buffered data. + * @param data pointer to a variable in which to put the received data. + * @param len pointer to a variable in which to put the number of available bytes. + * @return the @a result_t code. + */ + virtual result_t read(unsigned int timeout, const uint8_t** data, size_t* len) = 0; // abstract + + /** + * Needs to be called after @a read() in order to mark all or parts of the available + * bytes as consumed. + * @param len the number of bytes consumed. + */ + virtual void readConsumed(size_t len) = 0; // abstract + + protected: + /** + * Internal method for opening the device. Called from @a open(). + * @return the @a result_t code. + */ + virtual result_t openInternal() = 0; // abstract + + /** the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). */ + const char* m_name; + + /** the bus transfer latency in milliseconds. */ + const unsigned int m_latency; + + /** the @a TransportListener, or nullptr. */ + TransportListener* m_listener; +}; + + +/** + * The common base class for transport using a file descriptor. + */ +class FileTransport : public Transport { + protected: + /** + * Construct a new instance. + * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). + * @param latency the bus transfer latency in milliseconds. + * @param checkDevice whether to regularly check the device availability. + */ + FileTransport(const char* name, unsigned int latency, bool checkDevice); + + public: + /** + * Destructor. + */ + virtual ~FileTransport(); + + // @copydoc + result_t open() override; + + // @copydoc + void close() override; + + // @copydoc + bool isValid() override; + + // @copydoc + result_t write(const uint8_t* data, size_t len) override; + + // @copydoc + result_t read(unsigned int timeout, const uint8_t** data, size_t* len) override; + + // @copydoc + void readConsumed(size_t len) override; + + protected: + /** + * Check if the device is still available and close it if not. + */ + virtual void checkDevice() = 0; // abstract + + /** whether to regularly check the device availability. */ + const bool m_checkDevice; + + /** the opened file descriptor, or -1. */ + int m_fd; + + private: + /** the receive buffer. */ + symbol_t* m_buffer; + + /** the receive buffer size (multiple of 4). */ + size_t m_bufSize; + + /** the receive buffer fill length. */ + size_t m_bufLen; +}; + + +/** + * The @a Transport for a directly connected serial interface (tty). + */ +class SerialTransport : public FileTransport { + public: + /** + * Construct a new instance. + * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). + * @param extraLatency the extra bus transfer latency in milliseconds. + * @param checkDevice whether to regularly check the device availability. + * @param speed 0 for normal speed, 1 for 4x speed, or 2 for 48x speed. + */ + SerialTransport(const char* name, unsigned int extraLatency, bool checkDevice, uint8_t speed) + : FileTransport(name, extraLatency, checkDevice), m_speed(speed) { + } + + // @copydoc + string getTransportInfo() const override { + return m_speed ? (m_speed == 1 ? "serial speed" : "serial high speed") : "serial"; + } + + // @copydoc + result_t openInternal() override; + + // @copydoc + void close() override; + + + protected: + // @copydoc + void checkDevice() override; + + + private: + /** the previous settings of the device for restoring. */ + termios m_oldSettings; + + /** 0 for normal speed, 1 for 4x speed, or 2 for 48x speed. */ + const int m_speed; +}; + + +/** + * The @a Transport for a remote network interface. + */ +class NetworkTransport : public FileTransport { + public: + /** + * Construct a new instance. + * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). + * @param extraLatency the extra bus transfer latency in milliseconds. + * @param address the socket address of the device. + * @param hostOrIp the host name or IP address of the device. + * @param port the TCP or UDP port of the device. + * @param udp true for UDP, false to TCP. + */ + NetworkTransport(const char* name, unsigned int extraLatency, const char* hostOrIp, uint16_t port, + bool udp) + : FileTransport(name, NETWORK_LATENCY_MS+extraLatency, true), + m_hostOrIp(hostOrIp), m_port(port), m_udp(udp) {} + + /** + * Destructor. + */ + ~NetworkTransport() override { + if (m_hostOrIp) { + free(const_cast(m_hostOrIp)); + m_hostOrIp = nullptr; + } + } + + // @copydoc + string getTransportInfo() const override { + return m_udp ? "UDP" : "TCP"; + } + + // @copydoc + result_t openInternal() override; + + + protected: + // @copydoc + void checkDevice() override; + + + private: + /** the host name or IP address of the device. */ + const char* m_hostOrIp; + + /** the TCP or UDP port of the device. */ + const uint16_t m_port; + + /** true for UDP, false to TCP. */ + const bool m_udp; +}; + +} // namespace ebusd + +#endif // LIB_EBUS_TRANSPORT_H_ diff --git a/src/lib/knx/knx.cpp b/src/lib/knx/knx.cpp index c71178fdc..1729aca99 100644 --- a/src/lib/knx/knx.cpp +++ b/src/lib/knx/knx.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2022 John Baier + * Copyright (C) 2022-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -88,7 +88,7 @@ knx_addr_t parseAddress(const string &str, bool isGroup, bool* error) { return 0; } -// copydoc +// @copydoc KnxConnection *KnxConnection::create(const char *url) { #ifdef HAVE_KNXD if (strchr(url, ':')) { diff --git a/src/lib/knx/knx.h b/src/lib/knx/knx.h index 6dacb0f79..d711dd3b3 100644 --- a/src/lib/knx/knx.h +++ b/src/lib/knx/knx.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2022 John Baier + * Copyright (C) 2022-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,6 +22,12 @@ #include #include +#if defined(__GNUC__) +#define MAYBE_UNUSED __attribute__((unused)) +#else +#define MAYBE_UNUSED +#endif + namespace ebusd { /** @file lib/knx/knx.h @@ -160,7 +166,7 @@ class KnxConnection { /** * @param address the individual address to set. */ - virtual void setAddress(knx_addr_t address) { + virtual void setAddress(MAYBE_UNUSED knx_addr_t address) { // default implementation does nothing } @@ -176,7 +182,7 @@ class KnxConnection { * Set the programming mode. * @param on true to start programming mode, false to stop it. */ - virtual void setProgrammingMode(bool on) { + virtual void setProgrammingMode(MAYBE_UNUSED bool on) { // default implementation does nothing } }; diff --git a/src/lib/knx/knxd.h b/src/lib/knx/knxd.h index 1fb7e4609..50211cb87 100644 --- a/src/lib/knx/knxd.h +++ b/src/lib/knx/knxd.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2022 John Baier + * Copyright (C) 2022-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/knx/knxnet.h b/src/lib/knx/knxnet.h index 578ee98ce..c6d4424de 100644 --- a/src/lib/knx/knxnet.h +++ b/src/lib/knx/knxnet.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2022 John Baier + * Copyright (C) 2022-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,11 +31,16 @@ #include #include #include -#include +#ifdef __FreeBSD__ + #include +#else + #include +#endif #include #include #include #include "lib/knx/knx.h" +#include "lib/utils/tcpsocket.h" namespace ebusd { @@ -224,7 +229,7 @@ typedef struct __attribute__ ((packed)) { #define SYSTEM_MULTICAST_PORT 3671 // the default system multicast address 224.0.23.12 -#define SYSTEM_MULTICAST_IP 0xe000170c +#define SYSTEM_MULTICAST_IP_STR "224.0.23.12" #define LAST_FRAME_TIMEOUT 2 @@ -396,96 +401,23 @@ class KnxNetConnection : public KnxConnection { // @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); + string url = m_url && m_url[0] ? m_url : SYSTEM_MULTICAST_IP_STR; + if (m_url[0] == '@') { + url = SYSTEM_MULTICAST_IP_STR+url; + } + int fd = socketConnect(url.c_str(), + SYSTEM_MULTICAST_PORT, IPPROTO_UDP, &m_multicast, + // do not use connect() as it will limit incoming to the mcast src which is not the case + 0x01); if (fd < 0) { return "create socket"; } - // set non-blocking - ret = fcntl(fd, F_SETFL, O_NONBLOCK); - if (ret != 0) { + if (fcntl(fd, F_SETFL, O_NONBLOCK) != 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; } @@ -680,12 +612,12 @@ class KnxNetConnection : public KnxConnection { return nullptr; } - // copydoc + // @copydoc knx_addr_t getAddress() const override { return m_addr; } - // copydoc + // @copydoc void setAddress(knx_addr_t address) override { m_addr = address; // flush duplication check buffers @@ -693,12 +625,12 @@ class KnxNetConnection : public KnxConnection { m_lastSentFrames.reset(); } - // copydoc + // @copydoc bool isProgrammingMode() const override { return m_programmingMode; } - // copydoc + // @copydoc void setProgrammingMode(bool on) override { m_programmingMode = on; } @@ -707,15 +639,9 @@ class KnxNetConnection : public KnxConnection { /** the URL to connect to. */ const char* m_url; - /** the multicast address to join. */ + /** the multicast address to send to. */ 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; diff --git a/src/lib/utils/CMakeLists.txt b/src/lib/utils/CMakeLists.txt index 319b5d464..5fece5f1c 100755 --- a/src/lib/utils/CMakeLists.txt +++ b/src/lib/utils/CMakeLists.txt @@ -1,6 +1,7 @@ add_definitions(-Wconversion) set(libutils_a_SOURCES + arg.h arg.cpp log.h log.cpp tcpsocket.h tcpsocket.cpp thread.h thread.cpp diff --git a/src/lib/utils/Makefile.am b/src/lib/utils/Makefile.am index b48146025..a8a9e070e 100755 --- a/src/lib/utils/Makefile.am +++ b/src/lib/utils/Makefile.am @@ -5,6 +5,7 @@ AM_CXXFLAGS = -I$(top_srcdir)/src \ noinst_LIBRARIES = libutils.a libutils_a_SOURCES = \ + arg.h arg.cpp \ log.h log.cpp \ tcpsocket.h tcpsocket.cpp \ thread.h thread.cpp \ diff --git a/src/lib/utils/arg.cpp b/src/lib/utils/arg.cpp new file mode 100755 index 000000000..61b9bca73 --- /dev/null +++ b/src/lib/utils/arg.cpp @@ -0,0 +1,428 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "lib/utils/arg.h" + +#include +#include +#include +#include +#include + +namespace ebusd { + +#define isAlpha(c) (((c) >= 'a' && (c) <= 'z') || ((c) >= 'A' && (c) <= 'Z')) + +void calcCounts(const argDef *argDefs, int *count, int *shortCharsCount, int *shortOptsCount) { + for (const argDef *arg = argDefs; arg && arg->help; arg++) { + if (!arg->name) { + continue; + } + (*count)++; + if (!isAlpha(arg->key)) { + continue; + } + (*shortCharsCount)++; + (*shortOptsCount)++; + if (arg->valueName) { + (*shortOptsCount)++; + if (arg->flags & af_optional) { + (*shortOptsCount)++; + } + } + } +} + +void buildOpts(const argDef *argDefs, int *count, int *shortCharsCount, int *shortOptsCount, + struct option *longOpts, char *shortChars, int *shortIndexes, char *shortOpts, int argDefIdx) { + struct option *opt = longOpts+(*count); + for (const argDef *arg = argDefs; arg && arg->help; arg++, argDefIdx++) { + if (!arg->name) { + continue; + } + opt->name = arg->name; + opt->has_arg = arg->valueName ? ((arg->flags & af_optional) ? optional_argument : required_argument) : no_argument; + opt->flag = NULL; + opt->val = argDefIdx; + if (isAlpha(arg->key)) { + shortChars[(*shortCharsCount)] = static_cast(arg->key); + shortIndexes[(*shortCharsCount)++] = *count; + shortOpts[(*shortOptsCount)++] = static_cast(arg->key); + if (arg->valueName) { + shortOpts[(*shortOptsCount)++] = ':'; + if (arg->flags & af_optional) { + shortOpts[(*shortOptsCount)++] = ':'; + } + } + } + opt++; + (*count)++; + } +} + +static const argDef endArgDef = {nullptr, 0, nullptr, 0, nullptr}; +static const argDef helpArgDef = {"help", '?', nullptr, 0, "Give this help list"}; +static const argDef helpArgDefs[] = { + helpArgDef, + endArgDef +}; +static const argDef versionArgDef = {"version", 'V', nullptr, 0, "Print program version"}; +static const argDef versionArgDefs[] = { + versionArgDef, + endArgDef +}; + +int argParse(const argParseOpt *parseOpt, int argc, char **argv, void* userArg) { + int count = 0, shortCharsCount = 0, shortOptsCount = 0; + if (!(parseOpt->flags & af_noHelp)) { + calcCounts(helpArgDefs, &count, &shortCharsCount, &shortOptsCount); + } + if (!(parseOpt->flags & af_noVersion)) { + calcCounts(versionArgDefs, &count, &shortCharsCount, &shortOptsCount); + } + calcCounts(parseOpt->argDefs, &count, &shortCharsCount, &shortOptsCount); + for (const argParseChildOpt *child = parseOpt->childOpts; child && child->argDefs; child++) { + calcCounts(child->argDefs, &count, &shortCharsCount, &shortOptsCount); + } + struct option *longOpts = (struct option*)calloc(count+1, sizeof(struct option)); // room for EOF + char *shortChars = reinterpret_cast(calloc(shortCharsCount+1, sizeof(char))); // room for \0 + int *shortIndexes = reinterpret_cast(calloc(shortCharsCount, sizeof(int))); + char *shortOpts = reinterpret_cast(calloc(2+shortOptsCount+1, sizeof(char))); // room for +, :, and \0 + count = 0; + shortCharsCount = 0; + shortOptsCount = 0; + shortOpts[shortOptsCount++] = '+'; // posix mode to stop at first non-option + shortOpts[shortOptsCount++] = ':'; // return ':' for missing option + if (!(parseOpt->flags & af_noHelp)) { + buildOpts(helpArgDefs, &count, &shortCharsCount, &shortOptsCount, longOpts, shortChars, + shortIndexes, shortOpts, 0xff00); + } + if (!(parseOpt->flags & af_noVersion)) { + buildOpts(versionArgDefs, &count, &shortCharsCount, &shortOptsCount, longOpts, shortChars, + shortIndexes, shortOpts, 0xff01); + } + buildOpts(parseOpt->argDefs, &count, &shortCharsCount, &shortOptsCount, longOpts, shortChars, + shortIndexes, shortOpts, 0); + int children = 0; + for (const argParseChildOpt *child = parseOpt->childOpts; child && child->argDefs; child++) { + buildOpts(child->argDefs, &count, &shortCharsCount, &shortOptsCount, longOpts, shortChars, + shortIndexes, shortOpts, 0x100*(++children)); + } + optind = 1; // setting to 0 does not work + int c = 0, longIdx = -1, ret = 0; + while ((c = getopt_long(argc, argv, shortOpts, longOpts, &longIdx)) != -1) { + if (c == '?') { + // unknown option or help + if (optopt != '?') { + ret = '!'; + fprintf(stderr, "invalid argument %s\n", argv[optind - 1]); + } else { + ret = c; + } + break; + } + if (c == ':') { + // missing option + fprintf(stderr, "missing argument to %s\n", argv[optind - 1]); + ret = c; + break; + } + if (isAlpha(c)) { + // short name + int idx = static_cast(strchr(shortChars, c) - shortChars); + if (idx >= 0 && idx < shortCharsCount) { + longIdx = shortIndexes[idx]; + } else { + longIdx = -1; + } + } else if (c >= 0 && longIdx < 0) { + longIdx = c; + } + if (longIdx < 0 || longIdx >= count) { + ret = '!'; // error + break; + } + int val = longOpts[longIdx].val; + if (val == 0xff00) { // help + ret = '?'; + break; + } + if (val == 0xff01) { // version + ret = 'V'; + break; + } + const argDef *argDefs; + parse_function_t parser; + if (val & 0xff00) { + const argParseChildOpt *child = parseOpt->childOpts + ((val>>8)-1); + argDefs = child->argDefs; + parser = child->parser; + } else { + argDefs = parseOpt->argDefs; + parser = parseOpt->parser; + } + const argDef *arg = argDefs + (val & 0xff); + c = parser(arg->key, optarg, parseOpt, userArg); + if (c != 0) { + ret = c; + break; + } + } + if (ret == 0) { + // check for positionals + for (const argDef *arg = parseOpt->argDefs; arg && arg->help; arg++) { + if (arg->name || !arg->valueName) { + continue; // short/long arg or group + } + if (optind < argc) { + int key = arg->key; + do { + c = parseOpt->parser(key, argv[optind], parseOpt, userArg); + if (c != 0) { + ret = c; + break; + } + if (optind+1 >= argc || !(arg->flags & af_multiple)) { + break; + } + key++; + optind++; + } while (true); + if (ret != 0) { + break; + } + } else if (!(arg->flags & af_optional)) { + ret = ':'; // missing argument + fprintf(stderr, "missing argument\n"); + break; + } + optind++; + } + if (ret == 0 && optind < argc) { + ret = '!'; // extra unexpected argument + fprintf(stderr, "extra argument %s\n", argv[optind]); + } + } + if (ret == '?') { + argHelp(argv[0], parseOpt); + } + free(longOpts); + free(shortChars); + free(shortIndexes); + free(shortOpts); + return ret; +} + +#define MIN_INDENT 18 +#define MAX_INDENT 29 +#define MAX_BREAK 79 + +void wrap(const char* str, size_t pos, size_t indent) { + const char* end = strchr(str, 0); + const char* eol = strchr(str, '\n'); + char buf[MAX_BREAK + 1]; + bool first = true; + while (*str && str < end) { + if (!first) { + if (indent) { + printf("%*c", static_cast(indent), ' '); + } + pos = indent; + } + // start from max position backwards to find a break char + size_t cnt = MAX_BREAK - pos; + if (eol && eol < str) { + eol = strchr(str, '\n'); + } + if (eol && eol < str + cnt) { + // EOL is before latest possible break + cnt = eol - str; + } else if (end < str + cnt) { + cnt = end - str; + } + for (; cnt > 0; cnt--) { + char ch = str[cnt]; + if (ch == ' ' || ch == '\n' || ch == 0) { + // break found + buf[0] = 0; + strncat(buf, str, cnt); + printf("%s\n", buf); + str += cnt; + if (*str) { + str++; + } + break; // restart + } + } + if (cnt == 0 && *str) { + // final + printf("%s\n", str); + break; + } + first = false; + } +} + +size_t calcIndent(const argDef *argDefs) { + size_t indent = 0; + for (const argDef *arg = argDefs; arg && arg->help; arg++) { + if (!arg->name) { + continue; + } + // e.g. " -d, --device=DEV Use DEV..." + size_t length = 2 + 3 + 3 + strlen(arg->name) + 2; + if (arg->valueName) { + length += 1 + strlen(arg->valueName); + if (arg->flags & af_optional) { + length += 2; + } + } + if (length > indent) { + indent = length; + if (indent > MAX_INDENT) { + return indent; + } + } + } + return indent; +} + +void printArgs(const argDef *argDefs, size_t indent) { + for (const argDef *arg = argDefs; arg && arg->help; arg++) { + if (!arg->name && !arg->valueName) { + if (*arg->help) { + printf("\n %s\n", arg->help); + } else { + printf("\n"); + } + continue; + } + printf(" "); + if (isAlpha(arg->key) || arg->key == '?') { + printf("-%c,", arg->key); + } else { + printf(" "); + } + size_t taken = 2 + 3 + 3; + if (arg->name) { + printf(" --%s", arg->name); + taken += strlen(arg->name); + } else { + printf(" "); + } + if (arg->valueName) { + bool multi = arg->flags & af_multiple; + taken += (arg->name ? 1 : 0) + strlen(arg->valueName) + (multi ? 3 : 0); + if (arg->flags & af_optional) { + printf("[%s%s%s]", arg->name ? "=" : "", arg->valueName, multi ? "..." : ""); + taken += 2; + } else { + printf("%s%s%s", arg->name ? "=" : "", arg->valueName, multi ? "..." : ""); + } + } + if (taken > indent) { + printf(" "); + wrap(arg->help, taken+1, indent); + } else { + printf("%*c", static_cast(indent - taken), ' '); + wrap(arg->help, indent, indent); + } + } +} + +void argHelp(const char* name, const argParseOpt *parseOpt) { + size_t indent = calcIndent(parseOpt->argDefs); + if (indent < MAX_INDENT) { + for (const argParseChildOpt *child = parseOpt->childOpts; child && child->argDefs; child++) { + size_t childIndent = calcIndent(child->argDefs); + if (childIndent > indent) { + indent = childIndent; + if (indent > MAX_INDENT) { + break; + } + } + } + } + if (indent > MAX_INDENT) { + indent = MAX_INDENT; + } else if (indent < MIN_INDENT) { + indent = MIN_INDENT; + } + const char* basename = strrchr(name, '/'); + basename = basename ? basename+1 : name; + printf("Usage: %s [OPTION...]", basename); + for (const argDef *arg = parseOpt->argDefs; arg && arg->help; arg++) { + if (arg->name || !arg->valueName) { + continue; + } + bool multi = arg->flags & af_multiple; + if (arg->flags & af_optional) { + printf(" [%s%s]", arg->valueName, multi ? "..." : ""); + } else { + printf(" %s%s", arg->valueName, multi ? "..." : ""); + } + } + printf("\n"); + wrap(parseOpt->help, 0, 0); + printArgs(parseOpt->argDefs, indent); + for (const argParseChildOpt *child = parseOpt->childOpts; child && child->argDefs; child++) { + printArgs(child->argDefs, indent); + } + if (!(parseOpt->flags & (af_noHelp|af_noVersion))) { + printf("\n"); + if (!(parseOpt->flags & af_noHelp)) { + printArgs(helpArgDefs, indent); + } + if (!(parseOpt->flags & af_noVersion)) { + printArgs(versionArgDefs, indent); + } + } + if (parseOpt->suffix) { + printf("\n"); + wrap(parseOpt->suffix, 0, 0); + } + fflush(stdout); +} + +const argDef* argFindIn(const argDef *argDefs, const char* name) { + for (const argDef *arg = argDefs; arg && arg->help; arg++) { + if (arg->name && strcmp(name, arg->name) == 0) { // long option + return arg; + } + if (name[2] == 0 && arg->key >= '?' && name[0] == arg->key) { // short option + return arg; + } + } + return nullptr; +} + +const argDef* argFind(const argParseOpt *parseOpt, const char* name) { + const argDef* found = argFindIn(parseOpt->argDefs, name); + if (found) { + return found; + } + for (const argParseChildOpt *child = parseOpt->childOpts; child && child->argDefs; child++) { + found = argFindIn(child->argDefs, name); + if (found) { + return found; + } + } + return nullptr; +} + +} // namespace ebusd diff --git a/src/lib/utils/arg.h b/src/lib/utils/arg.h new file mode 100755 index 000000000..ec70d53b8 --- /dev/null +++ b/src/lib/utils/arg.h @@ -0,0 +1,106 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023-2025 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIB_UTILS_ARG_H_ +#define LIB_UTILS_ARG_H_ + +namespace ebusd { + +/** \file lib/utils/args.h */ + +/** the available arg flags. */ +enum ArgFlag { + af_optional = 1<<0, //!< optional argument value + af_multiple = 1<<1, //!< may appear multiple times (only allowed for last positional) + af_noHelp = 1<<2, //!< do not include -?/--help option + af_noVersion = 1<<3, //!< do not include -V/--version option + af_max = 1<<3, //!< maximum defined flag value +}; + +/** Definition of a single argument. */ +typedef struct argDef { + const char* name; //!< the (long) name of the argument, or nullptr for a group header or positional + int key; //!< the argument key, also used as short name if alphabetic or the question mark + // the optional argument value name, or nullptr for group header or argument without value name + const char* valueName; + int flags; //!< flags for the argument, bit combination of @a ArgFlag + const char* help; //!< help text (mandatory) +} argDef; + +struct argParseOpt; + +/** + * Function to be called for each argument. + * @param key the argument key as defined. for positional arguments with multiple flag this will be increased with each call. + * @param arg the argument value, or nullptr. + * @param parseOpt pointer to the @a argParseOpt structure. + * @param userArg pointer to user argument. + * @return 0 on success, non-zero otherwise. + */ +typedef int (*parse_function_t)(int key, char *arg, const struct argParseOpt *parseOpt, void *userArg); + +/** Options for child definitions. */ +typedef struct argParseChildOpt { + const argDef *argDefs; //!< pointer to the argument defintions (last one needs to have nullptr help as end sign) + parse_function_t parser; //!< parse function to use +} argParseChildOpt; + +/** Options to pass to @a argParse(). */ +typedef struct argParseOpt { + const argDef *argDefs; //!< pointer to the argument defintions (last one needs to have nullptr help as end sign) + parse_function_t parser; //!< parse function to use + int flags; //!< flags for the parser, bit combination of @a ArgFlag + const char* help; //!< help text for the program (second line of help output) + const char* suffix; //!< optional help suffix text + const argParseChildOpt *childOpts; //!< optional child definitions +} argParseOpt; + +/** + * Parse the arguments given in @a argv. + * @param parseOpt pointer to the @a argParseOpt structure. + * @param argc the argument count (including the full program name in index 0). + * @param argv the argument values (including the full program name in index 0). + * @param userArg pointer to user argument to pass to parser. + * @return 0 on success, '!' for an invalid argument value, ':' for a missing argument value, + * '?' when "-?" was given, or the result of the parse function if non-zero. + */ +int argParse(const argParseOpt *parseOpt, int argc, char **argv, void *userArg); + +/** + * Print the help text. + * @param name the name of the program. + * @param parseOpt pointer to the @a argParseOpt structure. + */ +void argHelp(const char* name, const argParseOpt *parseOpt); + +/** + * Find the argument with the given name. + * @param parseOpt pointer to the @a argParseOpt structure. + * @param name the name of the argument, either short or long. + * @return a pointer to the found @a argDef, or nullptr. + */ +const argDef* argFind(const argParseOpt *parseOpt, const char* name); + +/** + * Convenience macro to print an error message to stderr. +*/ +#define argParseError(argParseOpt, message) fprintf(stderr, "%s\n", message); + +} // namespace ebusd + +#endif // LIB_UTILS_ARG_H_ diff --git a/src/lib/utils/clock.cpp b/src/lib/utils/clock.cpp index 827cd0a97..35f9b05e1 100755 --- a/src/lib/utils/clock.cpp +++ b/src/lib/utils/clock.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2015-2022 John Baier + * Copyright (C) 2015-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/utils/clock.h b/src/lib/utils/clock.h index a4eeafacb..691286bba 100755 --- a/src/lib/utils/clock.h +++ b/src/lib/utils/clock.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2015-2022 John Baier + * Copyright (C) 2015-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/utils/httpclient.cpp b/src/lib/utils/httpclient.cpp index 67b54129f..356ac97dd 100755 --- a/src/lib/utils/httpclient.cpp +++ b/src/lib/utils/httpclient.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2018-2022 John Baier + * Copyright (C) 2018-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ #include #include #include +#include #ifdef HAVE_SSL #if OPENSSL_VERSION_NUMBER < 0x10101000L #include @@ -28,12 +29,17 @@ #endif // HAVE_SSL #include "lib/utils/log.h" +#if defined(__GNUC__) +#define MAYBE_UNUSED __attribute__((unused)) +#else +#define MAYBE_UNUSED +#endif + namespace ebusd { using std::string; using std::ostringstream; using std::dec; -using std::hex; #ifdef HAVE_SSL @@ -83,12 +89,10 @@ bool isError(const char* call, long result, long expected) { SSLSocket::~SSLSocket() { BIO_free_all(m_bio); - if (m_ctx) { - SSL_CTX_free(m_ctx); - } } ssize_t SSLSocket::send(const char* data, size_t len) { + time_t now = 0; do { #if OPENSSL_VERSION_NUMBER >= 0x10101000L size_t part = 0; @@ -102,18 +106,23 @@ ssize_t SSLSocket::send(const char* data, size_t len) { return static_cast(res); } #endif - if (!BIO_should_retry(m_bio)) { + if (!BIO_should_retry(m_bio) && now > 0) { // always repeat on first failure if (isError("send", true)) { return -1; } return 0; } + if ((now=time(nullptr)) > m_until) { + logError(lf_network, "HTTP send: timed out after %d sec", now-m_until); + break; + } usleep(SLEEP_NANOS); - } while (time(nullptr) < m_until); + } while (true); return -1; // timeout } ssize_t SSLSocket::recv(char* data, size_t len) { + time_t now = 0; do { #if OPENSSL_VERSION_NUMBER >= 0x10101000L size_t part = 0; @@ -127,14 +136,18 @@ ssize_t SSLSocket::recv(char* data, size_t len) { return static_cast(res); } #endif - if (!BIO_should_retry(m_bio)) { + if (!BIO_should_retry(m_bio) && now > 0) { // always repeat on first failure if (isError("recv", true)) { return -1; } return 0; } + if ((now=time(nullptr)) > m_until) { + logError(lf_network, "HTTP recv: timed out after %d sec", now-m_until); + break; + } usleep(SLEEP_NANOS); - } while (time(nullptr) < m_until); + } while (true); return -1; // timeout } @@ -142,35 +155,75 @@ bool SSLSocket::isValid() { return time(nullptr) < m_until && !BIO_eof(m_bio); } +void sslInfoCallback(const SSL *ssl, int type, int val) { + if (!needsLog(lf_network, (val == 0) ? ll_error : ll_debug)) { + return; + } + logWrite(lf_network, + (val == 0) ? ll_error : ll_debug, + "SSL state %d=%s: type 0x%x=%s%s%s%s%s%s%s%s%s val %d=%s", + SSL_get_state(ssl), + SSL_state_string_long(ssl), + type, + (type & SSL_CB_LOOP) ? "loop," : "", + (type & SSL_CB_EXIT) ? "exit," : "", + (type & SSL_CB_READ) ? "read," : "", + (type & SSL_CB_WRITE) ? "write," : "", + (type & SSL_CB_ALERT) ? "alert," : "", + (type & SSL_ST_ACCEPT) ? "accept," : "", + (type & SSL_ST_CONNECT) ? "connect," : "", + (type & SSL_CB_HANDSHAKE_START) ? "start," : "", + (type & SSL_CB_HANDSHAKE_DONE) ? "done," : "", + val, + (type & SSL_CB_ALERT) ? SSL_alert_desc_string_long(val) : "?"); +} + +int bioInfoCallback(MAYBE_UNUSED BIO *bio, int state, int res) { + logDebug(lf_network, + "SSL BIO state %d res %d", + state, + res); + return 1; +} + SSLSocket* SSLSocket::connect(const string& host, const uint16_t& port, bool https, int timeout, const char* caFile, const char* caPath) { - BIO *bio = nullptr; - SSL_CTX *ctx = nullptr; ostringstream ostr; ostr << host << ':' << static_cast(port); const string hostPort = ostr.str(); - time_t until = time(nullptr) + (timeout <= 3 ? 3 : timeout); // at least 3 seconds + time_t until = time(nullptr) + 1 + (timeout <= 5 ? 5 : timeout); // at least 5 seconds, 1 extra for rounding if (!https) { do { - bio = BIO_new_connect((char*)hostPort.c_str()); - if (isError("connect", bio)) { + BIO *bio = BIO_new_connect(static_cast(hostPort.c_str())); + if (isError("connect", bio != nullptr)) { break; } BIO_set_nbio(bio, 1); // set non-blocking - return new SSLSocket(nullptr, bio, until); + return new SSLSocket(bio, until); } while (false); - } else { - SSL *ssl = nullptr; - do { + return nullptr; + } + BIO *bio = nullptr; + static SSL_CTX *ctx = nullptr; + static int sslContextInitTries = 0; + do { + // const SSL_METHOD *method = TLS_client_method(); + static bool verifyPeer = true; + if (ctx == nullptr) { // according to openssl manpage, ctx is global and should be created once only + if (sslContextInitTries > 2) { // give it up to 3 tries to initialize the context + break; + } + sslContextInitTries++; const SSL_METHOD *method = SSLv23_method(); - if (isError("method", method)) { + if (isError("method", method != nullptr)) { break; } ctx = SSL_CTX_new(method); - if (isError("ctx_new", ctx)) { + if (isError("ctx_new", ctx != nullptr)) { break; } - bool verifyPeer = !caFile || strcmp(caFile, "#") != 0; + SSL_CTX_set_info_callback(ctx, sslInfoCallback); + verifyPeer = !caFile || strcmp(caFile, "#") != 0; SSL_CTX_set_verify(ctx, verifyPeer ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, nullptr); if (verifyPeer) { #if OPENSSL_VERSION_NUMBER >= 0x10101000L @@ -185,82 +238,105 @@ SSLSocket* SSLSocket::connect(const string& host, const uint16_t& port, bool htt } #endif if ((caFile || caPath) && isError("verify_loc", SSL_CTX_load_verify_locations(ctx, caFile, caPath), 1)) { + SSL_CTX_free(ctx); + ctx = nullptr; break; } } - const long flags = SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION; - SSL_CTX_set_options(ctx, flags); - bio = BIO_new_ssl_connect(ctx); - if (isError("new_ssl_conn", bio)) { - break; - } - if (isError("conn_hostname", BIO_set_conn_hostname(bio, hostPort.c_str()), 1)) { - break; - } - BIO_set_nbio(bio, 1); // set non-blocking - BIO_get_ssl(bio, &ssl); - if (isError("get_ssl", ssl)) { - break; - } - const char *hostname = host.c_str(); - if (isError("tls_host", SSL_set_tlsext_host_name(ssl, hostname), 1)) { - break; - } - long res = BIO_do_connect(bio); - while (res <= 0 && BIO_should_retry(bio) && time(nullptr) < until) { - usleep(SLEEP_NANOS); - res = BIO_do_connect(bio); - } - if (isError("connect", res, 1)) { - break; - } - X509 *cert = SSL_get_peer_certificate(ssl); - if (cert) { - X509_free(cert); - } - if (isError("peer_cert", cert)) { - break; - } - if (verifyPeer && isError("verify", SSL_get_verify_result(ssl), X509_V_OK)) { - break; - } - // check hostname - X509_NAME *sname = X509_get_subject_name(cert); - if (isError("get_subject", sname)) { - break; - } - char peerName[64]; - if (isError("subject name", X509_NAME_get_text_by_NID(sname, NID_commonName, peerName, sizeof(peerName)) > 0)) { + SSL_CTX_set_options(ctx, SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION); + } + bio = BIO_new_ssl_connect(ctx); + if (isError("new_ssl_conn", bio != nullptr)) { + // in this case the ctx seems to be in an invalid state and it never comes out of that again on its own + SSL_CTX_free(ctx); + ctx = nullptr; + break; + } + BIO_set_info_callback(bio, bioInfoCallback); + BIO_set_conn_ip_family(bio, 4); // force IPv4 to circumvent docker IPv6 routing issues + if (isError("conn_hostname", BIO_set_conn_hostname(bio, hostPort.c_str()), 1)) { + break; + } + isError("set_nbio", BIO_set_nbio(bio, 1), 1); // set non-blocking + SSL *ssl = nullptr; + BIO_get_ssl(bio, &ssl); + if (isError("get_ssl", ssl != nullptr)) { + break; + } + SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); + const char *hostname = host.c_str(); + if (isError("tls_host", SSL_set_tlsext_host_name(ssl, hostname), 1)) { + break; + } + long res = BIO_do_connect(bio); + time_t now = 0; + while (res <= 0 && (BIO_should_retry(bio) || now == 0)) { // always repeat on first failure + if ((now=time(nullptr)) > until) { break; } - if (isError("subject", strcmp(peerName, hostname), 0)) { + usleep(SLEEP_NANOS); + res = BIO_do_connect(bio); + } + if (res <= 0 && now > until) { + logError(lf_network, "HTTP connect: timed out after %d sec", now-until); + break; + } + if (isError("connect", res, 1)) { + break; + } + X509 *cert = SSL_get_peer_certificate(ssl); + if (isError("peer_cert", cert != nullptr)) { + break; + } + X509_free(cert); // decrement reference count incremented by above call + if (verifyPeer && isError("verify", SSL_get_verify_result(ssl), X509_V_OK)) { + break; + } + // check hostname + X509_NAME *sname = X509_get_subject_name(cert); + if (isError("get_subject", sname != nullptr)) { + break; + } + char peerName[64]; + if (isError("subject name", X509_NAME_get_text_by_NID(sname, NID_commonName, peerName, sizeof(peerName)) > 0)) { + break; + } + if (strcmp(peerName, hostname) != 0) { + char* dotpos = strchr(const_cast(hostname), '.'); + if (peerName[0] == '*' && peerName[1] == '.' && dotpos + && strcmp(peerName+2, dotpos+1) == 0) { + // wildcard matches + } else if (dotpos && strcmp(peerName, dotpos+1) == 0) { + // hostname matches + } else if (isError("subject", 1, 0)) { break; } - return new SSLSocket(ctx, bio, until); - } while (false); - } + } + return new SSLSocket(bio, until); + } while (false); if (bio) { BIO_free_all(bio); } - if (ctx) { - SSL_CTX_free(ctx); - } return nullptr; } bool HttpClient::s_initialized = false; +const char* HttpClient::s_caFile = nullptr; +const char* HttpClient::s_caPath = nullptr; -void HttpClient::initialize() { +void HttpClient::initialize(const char* caFile, const char* caPath) { if (s_initialized) { return; } s_initialized = true; + s_caFile = caFile; + s_caPath = caPath; SSL_library_init(); SSL_load_error_strings(); signal(SIGPIPE, SIG_IGN); // needed to avoid SIGPIPE when writing to a closed pipe } #else // HAVE_SSL -void HttpClient::initialize() { +void HttpClient::initialize(const char* caFile, const char* caPath) { // empty } #endif // HAVE_SSL @@ -277,11 +353,11 @@ bool HttpClient::parseUrl(const string& url, string* proto, string* host, uint16 if (!isSsl && *proto != "http") { return false; } -#else +#else // HAVE_SSL if (*proto != "http") { return false; } -#endif +#endif // HAVE_SSL size_t pos = url.find('/', hostPos); if (pos == hostPos) { return false; @@ -315,16 +391,17 @@ bool HttpClient::parseUrl(const string& url, string* proto, string* host, uint16 bool HttpClient::connect(const string& host, const uint16_t port, bool https, const string& userAgent, const int timeout) { + initialize(); disconnect(); #ifdef HAVE_SSL - m_socket = SSLSocket::connect(host, port, https, timeout, m_caFile, m_caPath); + m_socket = SSLSocket::connect(host, port, https, timeout, s_caFile, s_caPath); m_https = https; -#else +#else // HAVE_SSL if (https) { return false; } m_socket = TCPSocket::connect(host, port, timeout); -#endif +#endif // HAVE_SSL if (!m_socket) { return false; } @@ -341,10 +418,10 @@ bool HttpClient::reconnect() { return false; } #ifdef HAVE_SSL - m_socket = SSLSocket::connect(m_host, m_port, m_https, m_timeout, m_caFile, m_caPath); -#else + m_socket = SSLSocket::connect(m_host, m_port, m_https, m_timeout, s_caFile, s_caPath); +#else // HAVE_SSL m_socket = TCPSocket::connect(m_host, m_port, m_timeout); -#endif +#endif // HAVE_SSL if (!m_socket) { return false; } @@ -365,12 +442,13 @@ void HttpClient::disconnect() { } } -bool HttpClient::get(const string& uri, const string& body, string* response, time_t* time) { - return request("GET", uri, body, response, time); +bool HttpClient::get(const string& uri, const string& body, string* response, bool* repeatable, +time_t* time, bool* jsonString) { + return request("GET", uri, body, response, repeatable, time, jsonString); } -bool HttpClient::post(const string& uri, const string& body, string* response) { - return request("POST", uri, body, response); +bool HttpClient::post(const string& uri, const string& body, string* response, bool* repeatable) { + return request("POST", uri, body, response, repeatable); } const int indexToMonth[] = { @@ -380,9 +458,13 @@ const int indexToMonth[] = { -1, -1, 4, -1, -1, -1, -1, -1, // 24-31 }; -bool HttpClient::request(const string& method, const string& uri, const string& body, string* response, time_t* time) { +bool HttpClient::request(const string& method, const string& uri, const string& body, string* response, +bool* repeatable, time_t* time, bool* jsonString) { if (!ensureConnected()) { *response = "not connected"; + if (repeatable) { + *repeatable = true; + } return false; } ostringstream ostr; @@ -407,6 +489,9 @@ bool HttpClient::request(const string& method, const string& uri, const string& if (sent < 0) { disconnect(); *response = "send error"; + if (repeatable) { + *repeatable = true; + } return false; } pos += sent; @@ -431,14 +516,16 @@ bool HttpClient::request(const string& method, const string& uri, const string& return false; } string headers = result.substr(0, pos+2); // including final \r\n + transform(headers.begin(), headers.end(), headers.begin(), ::tolower); const char* hdrs = headers.c_str(); *response = result.substr(pos+4); +#if defined(HAVE_TIME_H) && defined(HAVE_TIMEGM) if (time) { - pos = headers.find("\r\nLast-Modified: "); - if (pos != string::npos && headers.substr(pos+42, 4) == " GMT") { + pos = headers.find("\r\nlast-modified: "); + if (pos != string::npos && headers.substr(pos+42, 4) == " gmt") { // Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT struct tm t; - pos += strlen("\r\nLast-Modified: ") + 5; + pos += strlen("\r\nlast-modified: ") + 5; char* strEnd = nullptr; t.tm_mday = static_cast(strtol(hdrs + pos, &strEnd, 10)); if (strEnd != hdrs + pos + 2 || t.tm_mday < 1 || t.tm_mday > 31) { @@ -472,21 +559,68 @@ bool HttpClient::request(const string& method, const string& uri, const string& } } } - pos = headers.find("\r\nContent-Length: "); - if (pos == string::npos) { +#endif + bool isJson = headers.find("\r\ncontent-type: application/json") != string::npos; + pos = headers.find("\r\ncontent-length: "); + bool noLength = pos == string::npos; + if (noLength && !isJson) { disconnect(); + if (jsonString) { + *jsonString = false; + } return true; } char* strEnd = nullptr; - unsigned long length = strtoul(hdrs + pos + strlen("\r\nContent-Length: "), &strEnd, 10); - if (strEnd == nullptr || *strEnd != '\r') { - disconnect(); - *response = "invalid content length "; - return false; + unsigned long length = 4*1024; // default max length + if (!noLength) { + length = strtoul(hdrs + pos + strlen("\r\ncontent-length: "), &strEnd, 10); + if (strEnd == nullptr || *strEnd != '\r') { + disconnect(); + *response = "invalid content length "; + return false; + } } pos = readUntil("", length, response); disconnect(); - return pos == length; + if (noLength ? pos < 1 : pos != length) { + return false; + } + if (noLength) { + length = pos; + } + if (jsonString && isJson && *jsonString && length >= 2 && response->at(0) == '"') { + // check for inline conversion of JSON to string expecting a single string to de-escape + pos = length; + while (pos > 1 && (response->at(pos-1) == '\r' || response->at(pos-1) == '\n')) { + pos--; + } + if (pos > 2 && response->at(pos-1) == '"') { + response->erase(pos-1); + response->erase(0, 1); + size_t from = 0; + while ((pos = response->find_first_of("\\", from)) != string::npos) { + response->erase(pos, 1); // pos is now pointing at the char behind the backslash + switch (response->at(pos)) { + case 'r': + response->erase(pos, 1); // removed + from = pos; + continue; + case 'n': + (*response)[pos] = '\n'; // replaced + from = pos+1; + break; + default: + from = pos+1; + break; // kept + } + } + isJson = false; + } + } + if (jsonString) { + *jsonString = isJson; + } + return true; } size_t HttpClient::readUntil(const string& delim, size_t length, string* result) { diff --git a/src/lib/utils/httpclient.h b/src/lib/utils/httpclient.h index 3c26a463b..96998bd86 100755 --- a/src/lib/utils/httpclient.h +++ b/src/lib/utils/httpclient.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2018-2022 John Baier + * Copyright (C) 2018-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,7 +32,7 @@ # include # include # include -#endif +#endif // HAVE_SSL /** typedef for referencing @a sockaddr_in within namespace. */ typedef struct sockaddr_in socketaddress; @@ -49,11 +49,10 @@ class SSLSocket { private: /** * Constructor. - * @param ctx the SSL_CTX for cleanup, or nullptr. * @param bio the BIO instance, or nullptr. * @param until the system time until the socket is allowed to be used. */ - SSLSocket(SSL_CTX *ctx, BIO *bio, time_t until) : m_ctx(ctx), m_bio(bio), m_until(until) {} + SSLSocket(BIO *bio, time_t until) : m_bio(bio), m_until(until) {} public: /** @@ -97,9 +96,6 @@ class SSLSocket { bool isValid(); private: - /** the SSL_CTX for cleanup, or nullptr. */ - SSL_CTX *m_ctx; - /** the BIO instance for communication. */ BIO *m_bio; @@ -122,18 +118,12 @@ class HttpClient { public: /** * Constructor. - * @param caFile the CA file to use (uses defaults if neither caFile nor caPath are set), or "#" for insecure. - * @param caPath the path with CA files to use (uses defaults if neither caFile nor caPath are set). */ - explicit HttpClient(const char* caFile = nullptr, const char* caPath = nullptr) : + HttpClient() : #ifdef HAVE_SSL m_https(false), - m_caFile(caFile), - m_caPath(caPath), -#endif - m_socket(nullptr), m_port(0), m_timeout(0), m_bufferSize(0), m_buffer(nullptr) - { - initialize(); +#endif // HAVE_SSL + m_socket(nullptr), m_port(0), m_timeout(0), m_bufferSize(0), m_buffer(nullptr) { } /** @@ -148,9 +138,11 @@ class HttpClient { } /** - * Initialize HttpClient. + * Initialize the underlying SSL library. + * @param caFile the CA file to use (uses defaults if neither caFile nor caPath are set), or "#" for insecure. + * @param caPath the path with CA files to use (uses defaults if neither caFile nor caPath are set). */ - static void initialize(); + static void initialize(const char* caFile = nullptr, const char* caPath = nullptr); /** * Parse an HTTP URL. @@ -196,19 +188,27 @@ class HttpClient { * @param uri the URI string. * @param body the optional body to send. * @param response the response body from the server (or the HTTP header on error). + * @param repeatable optional pointer to a bool in which to store whether the request should be repeated later on + * (e.g. due to temporary connectivity issues). * @param time optional pointer to a @a time_t value for storing the modification time of the file, or nullptr. + * @param jsonString optional pointer to a bool value. When returning, it is set to whether the retrieved + * content-type indicates JSON. When true upon entry, content is JSON, and response is a single JSON string, it + * will be de-escaped to a pure string and the value set to false. * @return true on success, false on error. */ - bool get(const string& uri, const string& body, string* response, time_t* time = nullptr); + bool get(const string& uri, const string& body, string* response, bool* repeatable = nullptr, + time_t* time = nullptr, bool* jsonString = nullptr); /** * Execute a POST request. * @param uri the URI string. * @param body the optional body to send. * @param response the response body from the server (or the HTTP header on error). + * @param repeatable optional pointer to a bool in which to store whether the request should be repeated later on + * (e.g. due to temporary connectivity issues). * @return true on success, false on error. */ - bool post(const string& uri, const string& body, string* response); + bool post(const string& uri, const string& body, string* response, bool* repeatable = nullptr); /** * Execute an arbitrary request. @@ -216,10 +216,16 @@ class HttpClient { * @param uri the URI string. * @param body the optional body to send. * @param response the response body from the server (or the HTTP header on error). + * @param repeatable optional pointer to a bool in which to store whether the request should be repeated later on + * (e.g. due to temporary connectivity issues). * @param time optional pointer to a @a time_t value for storing the modification time of the file, or nullptr. + * @param jsonString optional pointer to a bool value. When returning, it is set to whether the retrieved + * content-type indicates JSON. When true upon entry, content is JSON, and response is a single JSON string, it + * will be de-escaped to a pure string and the value set to false. * @return true on success, false on error. */ - bool request(const string& method, const string& uri, const string& body, string* response, time_t* time = nullptr); + bool request(const string& method, const string& uri, const string& body, string* response, + bool* repeatable = nullptr, time_t* time = nullptr, bool* jsonString = nullptr); private: /** @@ -240,11 +246,11 @@ class HttpClient { bool m_https; /** the CA file to use. */ - const char* m_caFile; + static const char* s_caFile; /** the path with CA files to use. */ - const char* m_caPath; -#endif + static const char* s_caPath; +#endif // HAVE_SSL /** the currently connected socket. */ SocketClass* m_socket; diff --git a/src/lib/utils/log.cpp b/src/lib/utils/log.cpp index fcc7bd720..65ee42f0f 100755 --- a/src/lib/utils/log.cpp +++ b/src/lib/utils/log.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,7 +27,9 @@ #include #include #include +#ifdef HAVE_SYSLOG_H #include +#endif #include "lib/utils/clock.h" namespace ebusd { @@ -37,6 +39,7 @@ static const char *s_facilityNames[] = { "main", "network", "bus", + "device", "update", "other", "all", @@ -53,6 +56,7 @@ static const char* s_levelNames[] = { nullptr }; +#ifdef HAVE_SYSLOG_H /** the syslog level of each @a LogLevel. */ static const int s_syslogLevels[] = { LOG_INFO, @@ -62,15 +66,18 @@ static const int s_syslogLevels[] = { LOG_DEBUG, 0 }; +#endif /** the current log level by log facility. */ -static LogLevel s_facilityLogLevel[] = { ll_notice, ll_notice, ll_notice, ll_notice, ll_notice, }; +static LogLevel s_facilityLogLevel[] = { ll_notice, ll_notice, ll_notice, ll_notice, ll_notice, ll_notice, }; /** the current log FILE, or nullptr if closed or syslog is used. */ static FILE* s_logFile = stdout; +#ifdef HAVE_SYSLOG_H /** whether to log to syslog. */ static bool s_useSyslog = false; +#endif LogFacility parseLogFacility(const char* facility) { if (!facility) { @@ -97,11 +104,7 @@ int parseLogFacilities(const char* facilities) { free(input); return -1; } - if (val == lf_COUNT) { - newFacilites = LF_ALL; - } else { - newFacilites |= 1 << val; - } + newFacilites |= 1 << val; } free(input); return newFacilites; @@ -133,7 +136,7 @@ const char* getLogLevelStr(LogLevel level) { bool setFacilitiesLogLevel(int facilities, LogLevel level) { bool changed = false; for (int val = 0; val < lf_COUNT && facilities != 0; val++) { - if ((facilities & (1 << val)) != 0 && s_facilityLogLevel[(LogFacility)val] != level) { + if ((facilities & ((1 << val)|(1 << lf_COUNT))) != 0 && s_facilityLogLevel[(LogFacility)val] != level) { s_facilityLogLevel[(LogFacility)val] = level; changed = true; } @@ -148,8 +151,12 @@ LogLevel getFacilityLogLevel(LogFacility facility) { bool setLogFile(const char* filename) { if (filename[0] == 0) { closeLogFile(); +#ifdef HAVE_SYSLOG_H openlog("ebusd", LOG_NDELAY|LOG_PID, LOG_USER); s_useSyslog = true; +#else + s_logFile = stdout; +#endif return true; } FILE* newFile = fopen(filename, "a"); @@ -168,28 +175,43 @@ void closeLogFile() { } s_logFile = nullptr; } +#ifdef HAVE_SYSLOG_H if (s_useSyslog) { closelog(); s_useSyslog = false; } +#endif } bool needsLog(const LogFacility facility, const LogLevel level) { - if (s_logFile == nullptr && !s_useSyslog) { + if (s_logFile == nullptr +#ifdef HAVE_SYSLOG_H + && !s_useSyslog +#endif + ) { return false; } return s_facilityLogLevel[facility] >= level; } void logWrite(const char* facility, const LogLevel level, const char* message, va_list ap) { - if (s_logFile == nullptr && !s_useSyslog) { + if (s_logFile == nullptr +#ifdef HAVE_SYSLOG_H + && !s_useSyslog +#endif + ) { return; } char* buf; if (vasprintf(&buf, message, ap) >= 0 && buf) { +#ifdef HAVE_SYSLOG_H if (s_useSyslog) { syslog(s_syslogLevels[level], "[%s %s] %s", facility, s_levelNames[level], buf); } else { +#endif +#ifdef LOG_WITHOUT_TIMEPREFIX + fprintf(s_logFile, "[%s %s] %s\n", +#else struct timespec ts; struct tm td; clockGettime(&ts); @@ -197,9 +219,12 @@ void logWrite(const char* facility, const LogLevel level, const char* message, v fprintf(s_logFile, "%04d-%02d-%02d %02d:%02d:%02d.%03ld [%s %s] %s\n", td.tm_year+1900, td.tm_mon+1, td.tm_mday, td.tm_hour, td.tm_min, td.tm_sec, ts.tv_nsec/1000000, +#endif facility, s_levelNames[level], buf); fflush(s_logFile); +#ifdef HAVE_SYSLOG_H } +#endif } if (buf) { free(buf); diff --git a/src/lib/utils/log.h b/src/lib/utils/log.h index 1364deeda..116a9b9ad 100755 --- a/src/lib/utils/log.h +++ b/src/lib/utils/log.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,14 +28,12 @@ enum LogFacility { lf_main = 0, //!< main loop lf_network, //!< network related lf_bus, //!< eBUS related + lf_device, //!< device related lf_update, //!< updates found while listening to the bus lf_other, //!< all other log facilities - lf_COUNT = 5 //!< number of available log facilities + lf_COUNT = 6 //!< number of available log facilities and flag for setting all }; -/** macro for all log facilities. */ -#define LF_ALL ((1 << lf_main) | (1 << lf_network) | (1 << lf_bus) | (1 << lf_update) | (1 << lf_other)) - /** the available log levels. */ enum LogLevel { ll_none = 0, //!< no level at all diff --git a/src/lib/utils/notify.h b/src/lib/utils/notify.h index 810235270..1eb216211 100755 --- a/src/lib/utils/notify.h +++ b/src/lib/utils/notify.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2014-2025 John Baier , Roland Jax 2012-2014 * * 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 diff --git a/src/lib/utils/queue.h b/src/lib/utils/queue.h index 6d2f60515..415312845 100755 --- a/src/lib/utils/queue.h +++ b/src/lib/utils/queue.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier + * Copyright (C) 2014-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -65,11 +65,13 @@ class Queue { public: /** * Add an item to the end of queue. - * @param item the item to add. + * @param item the item to add, or nullptr for notifying only. */ void push(T item) { pthread_mutex_lock(&m_mutex); - m_queue.push_back(item); + if (item) { + m_queue.push_back(item); + } pthread_cond_broadcast(&m_cond); pthread_mutex_unlock(&m_mutex); } @@ -82,15 +84,11 @@ class Queue { T pop(int timeout = 0) { T item; pthread_mutex_lock(&m_mutex); - if (timeout > 0) { + if (timeout > 0 && m_queue.empty()) { struct timespec t; clockGettime(&t); t.tv_sec += timeout; - while (m_queue.empty()) { - if (pthread_cond_timedwait(&m_cond, &m_mutex, &t) != 0) { - break; - } - } + pthread_cond_timedwait(&m_cond, &m_mutex, &t); } if (m_queue.empty()) { item = nullptr; diff --git a/src/lib/utils/rotatefile.cpp b/src/lib/utils/rotatefile.cpp index 841058bef..636a890f7 100755 --- a/src/lib/utils/rotatefile.cpp +++ b/src/lib/utils/rotatefile.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/utils/rotatefile.h b/src/lib/utils/rotatefile.h index bb1432334..ccdd80b5e 100755 --- a/src/lib/utils/rotatefile.h +++ b/src/lib/utils/rotatefile.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2016-2022 John Baier + * Copyright (C) 2016-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lib/utils/tcpsocket.cpp b/src/lib/utils/tcpsocket.cpp index 84a1e8ed2..f1b42f710 100755 --- a/src/lib/utils/tcpsocket.cpp +++ b/src/lib/utils/tcpsocket.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2015-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2015-2025 John Baier , Roland Jax 2012-2014 * * 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 @@ -46,109 +46,216 @@ bool TCPSocket::isValid() { return fcntl(m_sfd, F_GETFL) != -1; } +bool parseIp(const char* server, struct in_addr *sin_addr) { + if (inet_aton(server, sin_addr) == 1) { + return true; + } + struct hostent* he = gethostbyname(server); + if (he == nullptr) { + return false; + } + memcpy(sin_addr, he->h_addr_list[0], he->h_length); + return true; +} -int socketConnect(const char* server, uint16_t port, bool udp, socketaddress* storeAddress, int tcpConnectTimeout, -int tcpKeepAliveInterval) { +int socketConnect(const char* server, uint16_t port, int udpProto, socketaddress* storeAddress, +int tcpConnToUdpOptions, int tcpKeepAliveInterval, struct in_addr* storeIntf) { socketaddress localAddress; socketaddress* address = storeAddress ? storeAddress : &localAddress; memset(reinterpret_cast(address), 0, sizeof(*address)); - if (inet_aton(server, &address->sin_addr) == 0) { - struct hostent* he = gethostbyname(server); - if (he == nullptr) { + // parse "address[@intf]" + const char* pos = strchr(server, '@'); + struct in_addr intf; + intf.s_addr = INADDR_ANY; + if (pos) { + size_t len = strlen(server)+1; // workaround for e.g. Alpine with wrong return type on strdupa() + char* str = reinterpret_cast(malloc(len)); + strcpy(str, server); + char* ifa = strchr(str, '@'); + ifa[0] = 0; + ifa++; + if (!str[0] || !parseIp(str, &address->sin_addr)) { + free(str); return -1; } - memcpy(&address->sin_addr, he->h_addr_list[0], he->h_length); + if (!parseIp(ifa, &intf)) { + free(str); + return -1; + } + free(str); + } else if (!parseIp(server, &address->sin_addr)) { + return -1; + } + if (storeIntf) { + *storeIntf = intf; } address->sin_family = AF_INET; address->sin_port = (in_port_t)htons(port); - int sfd = socket(AF_INET, udp ? SOCK_DGRAM : SOCK_STREAM, 0); + int sfd = socket(AF_INET, udpProto ? SOCK_DGRAM : SOCK_STREAM, udpProto); if (sfd < 0) { - return -1; + return -2; } - int ret; - if (udp) { + int ret = 0; + if (udpProto) { + #define RET(chk, next) if (ret >= 0) { ret = chk; if (ret < 0) ret = next;} struct sockaddr_in bindAddress = *address; - bindAddress.sin_addr.s_addr = INADDR_ANY; - ret = bind(sfd, (struct sockaddr*)&bindAddress, sizeof(bindAddress)); - if (ret >= 0) { - ret = ::connect(sfd, (struct sockaddr*)address, sizeof(*address)); + // allow multiple processes using the same port for multicast on the same host + int optint = 1; + RET(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &optint, sizeof(optint)), -3); +#ifdef SO_REUSEPORT + RET(setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &optint, sizeof(optint)), -3); +#endif + bool isMcast = IN_MULTICAST(ntohl(address->sin_addr.s_addr)); + if (isMcast) { + // loop-back sent multicast packets + unsigned char optchar = 1; + RET(setsockopt(sfd, IPPROTO_IP, IP_MULTICAST_LOOP, &optchar, sizeof(optchar)), -3); + if (ret >= 0) { + // join the multicast inbound + ip_mreq req = {}; + req.imr_multiaddr = address->sin_addr; + req.imr_interface = intf; + RET(setsockopt(sfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req)), -7); + } + if (ret >= 0 && intf.s_addr != INADDR_ANY) { + // set outgoing interface to other than default (determined by routing table) + RET(setsockopt(sfd, IPPROTO_IP, IP_MULTICAST_IF, &intf, sizeof(intf)), -3); + } } - if (ret < 0) { - close(sfd); - return -1; + bindAddress.sin_addr = intf; + if (!(tcpConnToUdpOptions&0x01)) { + bindAddress.sin_port = 0; // do not bind to same source port for outgoing packets + } + RET(bind(sfd, (struct sockaddr*)&bindAddress, sizeof(bindAddress)), -4); + if (tcpConnToUdpOptions&0x02) { + // set the default target address for later use by send() + RET(::connect(sfd, (struct sockaddr*)address, sizeof(*address)), -5); + if (ret < 0) { + close(sfd); + return ret; + } } + return sfd; } int value = 1; ret = setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&value), sizeof(value)); if (ret < 0) { close(sfd); - return -1; + return -3; } if (tcpKeepAliveInterval > 0) { value = 1; - setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&value), sizeof(value)); + if (setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&value), sizeof(value)) != 0) { + perror("setsockopt KEEPALIVE"); + } +#ifndef TCP_KEEPIDLE + #ifdef TCP_KEEPALIVE + #define TCP_KEEPIDLE TCP_KEEPALIVE + #else + #define TCP_KEEPIDLE 4 + #endif +#endif +#ifndef TCP_KEEPINTVL + #define TCP_KEEPINTVL 5 +#endif +#ifndef TCP_KEEPCNT + #define TCP_KEEPCNT 6 +#endif value = tcpKeepAliveInterval+1; // send keepalive after interval + 1 seconds of silence - setsockopt(sfd, IPPROTO_TCP, TCP_KEEPIDLE, reinterpret_cast(&value), sizeof(value)); + if (setsockopt(sfd, IPPROTO_TCP, TCP_KEEPIDLE, reinterpret_cast(&value), sizeof(value)) != 0) { + perror("setsockopt KEEPIDLE"); + } value = tcpKeepAliveInterval; // send keepalive in given interval - setsockopt(sfd, IPPROTO_TCP, TCP_KEEPINTVL, reinterpret_cast(&value), sizeof(value)); + if (setsockopt(sfd, IPPROTO_TCP, TCP_KEEPINTVL, reinterpret_cast(&value), sizeof(value)) != 0) { + perror("setsockopt KEEPINTVL"); + } value = 2; // drop connection after 2 failed keep alive sends - setsockopt(sfd, IPPROTO_TCP, TCP_KEEPCNT, reinterpret_cast(&value), sizeof(value)); - } -#ifndef HAVE_PPOLL -#ifndef HAVE_PSELECT - timeout = 0; -#endif + if (setsockopt(sfd, IPPROTO_TCP, TCP_KEEPCNT, reinterpret_cast(&value), sizeof(value)) != 0) { + perror("setsockopt KEEPCNT"); + } +#ifdef TCP_USER_TIMEOUT + value = (2+tcpKeepAliveInterval*3)*1000; // 1 second higher than keepalive timeout + if (setsockopt(sfd, IPPROTO_TCP, TCP_USER_TIMEOUT, reinterpret_cast(&value), sizeof(value)) != 0) { + perror("setsockopt USER_TIMEOUT"); + } #endif - if (tcpConnectTimeout > 0 && fcntl(sfd, F_SETFL, O_NONBLOCK) < 0) { // set non-blocking + } + if (tcpConnToUdpOptions > 0 && fcntl(sfd, F_SETFL, O_NONBLOCK) < 0) { // set non-blocking close(sfd); - return -1; + return -4; } ret = ::connect(sfd, (struct sockaddr*)address, sizeof(*address)); if (ret != 0) { - if (ret < 0 && (tcpConnectTimeout <= 0 || errno != EINPROGRESS)) { + if (ret < 0 && (tcpConnToUdpOptions <= 0 || errno != EINPROGRESS)) { close(sfd); - return -1; + return -5; } - if (tcpConnectTimeout > 0) { - struct timespec tdiff; - tdiff.tv_sec = tcpConnectTimeout; - tdiff.tv_nsec = 0; -#ifdef HAVE_PPOLL - nfds_t nfds = 1; - struct pollfd fds[nfds]; - memset(fds, 0, sizeof(fds)); - fds[0].fd = sfd; - fds[0].events = POLLIN|POLLOUT; - ret = ppoll(fds, nfds, &tdiff, nullptr); - if (ret == 1 && fds[0].revents & POLLERR) { - ret = -1; - } -#else - fd_set readfds, writefds, exceptfds; - FD_ZERO(&readfds); - FD_ZERO(&writefds); - FD_ZERO(&exceptfds); - FD_SET(sfd, &readfds); - ret = pselect(sfd + 1, &readfds, &writefds, &exceptfds, &tdiff, nullptr); - if (ret >= 1 && FD_ISSET(sfd, &exceptfds)) { - ret = -1; - } -#endif - if (ret == -1 || ret == 0) { + if (tcpConnToUdpOptions > 0) { + ret = socketPoll(sfd, POLLIN|POLLOUT, tcpConnToUdpOptions); + if (ret <= 0) { close(sfd); - return -1; + return -6; } if (fcntl(sfd, F_SETFL, 0) < 0) { // set blocking again close(sfd); - return -1; + return -4; } } } return sfd; } +int socketPoll(int sfd, int which, int timeoutSeconds) { + int ret; +#if defined(HAVE_PPOLL) || defined(HAVE_PSELECT) + struct timespec tdiff; + tdiff.tv_sec = timeoutSeconds; + tdiff.tv_nsec = 0; +#else + struct timeval tdiff; + tdiff.tv_sec = timeoutSeconds; + tdiff.tv_usec = 0; +#endif +#ifdef HAVE_PPOLL + nfds_t nfds = 1; + struct pollfd fds[nfds]; + memset(fds, 0, sizeof(fds)); + fds[0].fd = sfd; + fds[0].events = which; + ret = ppoll(fds, nfds, &tdiff, nullptr); + if (ret >= 1 && fds[0].revents & POLLERR) { + ret = -1; + } else if (ret >= 1) { + ret = fds[0].revents; + } +#else + fd_set readfds, writefds, exceptfds; + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&exceptfds); + if (which & POLLIN) { + FD_SET(sfd, &readfds); + } + if (which & POLLOUT) { + FD_SET(sfd, &writefds); + } + FD_SET(sfd, &exceptfds); +#ifdef HAVE_PSELECT + ret = pselect(sfd + 1, &readfds, &writefds, &exceptfds, &tdiff, nullptr); +#else + ret = select(sfd + 1, &readfds, &writefds, &exceptfds, &tdiff); +#endif + if (ret >= 1 && FD_ISSET(sfd, &exceptfds)) { + ret = -1; + } else if (ret >= 1) { + ret = (FD_ISSET(sfd, &readfds) ? POLLIN : 0) | (FD_ISSET(sfd, &writefds) ? POLLOUT : 0); + } +#endif + return ret; +} + TCPSocket* TCPSocket::connect(const string& server, const uint16_t& port, int timeout) { socketaddress address; @@ -208,4 +315,351 @@ TCPSocket* TCPServer::newSocket() { return new TCPSocket(sfd, &address); } +size_t readNameRecursive(uint8_t *data, size_t len, size_t pos, size_t maxPos, int maxDepth, char* str, size_t slen, + size_t* spos) { + size_t nlen = data[pos++]; + if ((nlen&0xc0) == 0xc0) { + // pointer + size_t p = ((nlen&0x3f) << 8) | data[pos]; + if (p >= len || maxDepth < 1) { + return 0; + } + readNameRecursive(data, len, p, len, maxDepth-1, str, slen, spos); + return 2; + } + if (!nlen) { + return 1; + } + if (pos+nlen > maxPos || *spos+1+nlen > slen) { + return 0; + } + if (*spos > 0) { + str[*spos] = '.'; + *spos += 1; + } + memcpy(str+*spos, data+pos, nlen); + *spos += nlen; + pos += nlen; + size_t add; + if (pos >= maxPos || maxDepth < 1) { + add = 0; + } else { + add = readNameRecursive(data, len, pos, maxPos, maxDepth-1, str, slen, spos); + if (add == 0) { + return 0; + } + } + return 1+nlen+add; +} + +size_t readName(uint8_t *data, size_t len, size_t pos, size_t maxPos, char* str, size_t slen, size_t* spos) { + return readNameRecursive(data, len, pos, maxPos, 4, str, slen, spos); +} + +typedef struct __attribute__ ((packed)) { + uint16_t id; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool qr: 1; // 0=query, 1=answer + uint8_t opcode: 4; // 0=standard query, 1=inverse query, 2=status request + bool aa: 1; // authoritive answer + bool tc: 1; // truncation + bool rd: 1; // recursion desired +#else + bool rd: 1; // recursion desired + bool tc: 1; // truncation + bool aa: 1; // authoritive answer + uint8_t opcode: 4; // 0=standard query, 1=inverse query, 2=status request + bool qr: 1; // 0=query, 1=answer +#endif + }; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool ra: 1; // recursion available + uint8_t z: 3; // zero + uint8_t rcode: 4; // response code: 0=OK +#else + uint8_t rcode: 4; // response code: 0=OK + uint8_t z: 3; // zero + bool ra: 1; // recursion available +#endif + }; + uint16_t qdCount; // question section entry count + uint16_t anCount; // answer section entry count + uint16_t nsCount; // name server section entry count + uint16_t arCount; // additional records section entry count +} dns_query_t; + +typedef struct __attribute__ ((packed)) { + uint8_t len; + // unsigned char *name; +} dns_qname_t; + +typedef struct __attribute__ ((packed)) { + dns_qname_t qname; + uint16_t qtype; + uint16_t qclass; // top bit used for unicast-response +} dns_question_t; + +#define DNS_TYPE_A 0x01 +#define DNS_TYPE_PTR 0x0c +#define DNS_TYPE_TXT 0x10 +#define DNS_TYPE_SRV 0x21 +#define DNS_CLASS_AA 0x01 + +typedef struct __attribute__ ((packed)) { + dns_qname_t aname; + uint16_t atype; + uint16_t aclass; + uint32_t ttl; + uint16_t rdLength; + // uint8_t *rData; +} dns_answer_t; + +typedef struct __attribute__ ((packed)) { + uint16_t priority; + uint16_t weight; + uint16_t port; + dns_qname_t target; +} dns_rr_srv_t; + +int resolveMdnsOneShot(const char* url, mdns_oneshot_t *result, mdns_oneshot_t *moreResults, size_t *moreCount) { + memset(result, 0, sizeof(mdns_oneshot_t)); + socketaddress address; + const char* pos = strchr(url, '@'); + string limitId = string(url); + string device = "224.0.0.251"; + if (pos) { + limitId = limitId.substr(0, pos-url); + device += string(pos); + } + int sock = socketConnect(device.c_str(), 5353, IPPROTO_UDP, &address); + if (sock < 0) { + return -1; + } + + uint8_t record[1500]; + memset(record, 0, sizeof(record)); + dns_query_t *dnsr = reinterpret_cast(record); + dnsr->qdCount = htons(1); + size_t len = sizeof(dns_query_t); + dns_question_t *q = reinterpret_cast(reinterpret_cast(dnsr)+len); + const uint8_t serviceName[] = { + 0x06, 0x5f, 0x65, 0x62, 0x75, 0x73, 0x64, // _ebusd + 0x04, 0x5f, 0x74, 0x63, 0x70, // _tcp + 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, // local + 0x00 + }; + memcpy(&q->qname.len, serviceName, sizeof(serviceName)); + len += sizeof(serviceName)-1; // -1 for final empty qname + q = reinterpret_cast(reinterpret_cast(dnsr)+len); + q->qtype = htons(DNS_TYPE_PTR); + q->qclass = htons( + 0x8000 | // unicast response bit + DNS_CLASS_AA); + len += sizeof(dns_question_t); + ssize_t ret = sendto(sock, record, len, 0, reinterpret_cast(&address), sizeof(address)); +#ifdef DEBUG_MDNS + printf("mdns: sent %ld, err %d\n", ret, errno); +#endif + fcntl(sock, F_SETFL, O_NONBLOCK); + bool found = false, foundMore = false; + size_t moreRemain = moreResults && moreCount && *moreCount > 0 ? *moreCount : 0; + if (moreRemain > 0) { + *moreCount = 0; + } + size_t done = 0; +#ifdef DEBUG_MDNS + socketaddress aaddr; + socklen_t aaddrlen = 0; +#endif + for (int i=0; i < (found ? 3 : 5); i++) { // up to 5 seconds, at least 3 seconds + ret = socketPoll(sock, POLLIN, 1); + done = 0; + if (ret > 0 && (ret&POLLIN)) { +#ifdef DEBUG_MDNS + aaddrlen = sizeof(aaddr); + ret = recvfrom(sock, record, sizeof(record), 0, reinterpret_cast(&aaddr), &aaddrlen); +#else + ret = recv(sock, record, sizeof(record), 0); +#endif + } + if (ret < 0) { + if (errno == EAGAIN) { + continue; + } + close(sock); + return -1; + } + done = (size_t)ret; + if (done < sizeof(dns_query_t)) { + continue; + } + dnsr = reinterpret_cast(record); + // todo length check +#ifdef DEBUG_MDNS + printf("mdns: got %d from %2.2x:%d, q=%d, an=%d, ns=%d, ar=%d\n", done, aaddr.sin_addr.s_addr, + ntohs(aaddr.sin_port), ntohs(dnsr->qdCount), ntohs(dnsr->anCount), ntohs(dnsr->nsCount), + ntohs(dnsr->arCount)); +#endif + if (dnsr->qdCount || done < sizeof(dns_query_t)+sizeof(serviceName)+4*sizeof(dns_answer_t)+(26+2)+4+1+1+ + sizeof(dns_rr_srv_t)+(2+1+sizeof(mdns_oneshot_t::id)-1+1+5+1+sizeof(mdns_oneshot_t::proto)-1)+4 + // "eBUS Adapter Shield xxxxxx", "id=xxxxxxxxxxxx.proto=ens" + ) { + continue; + } + uint16_t anCnt = ntohs(dnsr->anCount); + uint16_t arCnt = ntohs(dnsr->arCount); + if (anCnt < 1 || dnsr->nsCount || arCnt < 1) { + continue; + } + len = sizeof(dns_query_t); + char name[256]; + bool validPort = false; + struct in_addr validAddress; + validAddress.s_addr = INADDR_ANY; + char id[sizeof(mdns_oneshot_t::id)] = {0}; + char proto[sizeof(mdns_oneshot_t::proto)] = {0}; + for (int i=0; i < anCnt+arCnt && len < done; i++) { + dns_answer_t *a = reinterpret_cast(reinterpret_cast(dnsr)+len); + if (i == 0) { + if (memcmp(&a->aname.len, serviceName, sizeof(serviceName)) != 0) { +#ifdef DEBUG_MDNS + printf("mdns: an 0 mismatch\n"); +#endif + anCnt = 0; + break; // skip this one + } +#ifdef DEBUG_MDNS + printf("mdns: an 0 match\n"); +#endif + len += sizeof(serviceName)-1; // -1 for final empty qname + } else { + // read name + size_t pos = 0; + size_t nlen = readName(record, done, len, done, name, sizeof(name), &pos); + if (nlen == 0) { + anCnt = 0; + break; // skip this one + } + len += nlen-1; // -1 for final empty qname / right pointer for below + name[pos] = 0; +#ifdef DEBUG_MDNS + printf("mdns: a%c %d name=%s\n", i >= anCnt ? 'r' : 'n', i >= anCnt ? i-anCnt : i, name); +#endif + } + a = reinterpret_cast(reinterpret_cast(dnsr)+len); + int atype = ntohs(a->atype); + int aclass = ntohs(a->aclass); +#ifdef DEBUG_MDNS + printf(" atype %d, aclass %d\n", atype, aclass); +#endif + if (i == 0 && (atype != DNS_TYPE_PTR + || aclass != DNS_CLASS_AA)) { + anCnt = 0; + break; // skip this one + } + len += sizeof(dns_answer_t); + uint16_t rdLen = ntohs(a->rdLength); +#ifdef DEBUG_MDNS + printf(" rd %d @%2.2x = ", rdLen, len); + for (int i=0; i < rdLen && len+i < done; i++) { + printf("%2.2x ", reinterpret_cast(dnsr)[len+i]); + } + printf("\n"); +#endif + if (atype == DNS_TYPE_PTR || atype == DNS_TYPE_TXT) { + size_t pos = 0; + if (readName(record, done, len, len+rdLen, name, sizeof(name), &pos) == 0) { + anCnt = 0; + break; // skip this one + } + name[pos] = 0; +#ifdef DEBUG_MDNS + printf(" %s=%s\n", (atype == DNS_TYPE_TXT) ? "txt" : "ptr", name); +#endif + if (atype == DNS_TYPE_TXT && name[0]) { + // parse id=xxxxxxxxxxxx[.proto=xxx] + char* sep = strchr(name, '='); + char* sep2; + if (sep && sep-name == 2 && strncmp(name, "id", 2) == 0) { + sep2 = strchr(sep+1, '.'); + if (!sep2) { + sep2 = name + pos; + } + if (sep2-sep-1 == sizeof(mdns_oneshot_t::id)-1) { + memcpy(id, sep+1, sizeof(mdns_oneshot_t::id)-1); + } else { + sep = nullptr; + } + sep = sep && sep2 < name + pos ? strchr(sep2+1, '=') : nullptr; + } else { + sep2 = name - 1; + } + if (sep && sep-sep2-1 == 5 && strncmp(sep2+1, "proto", 5) == 0) { + sep2 = strchr(sep+1, '.'); + if (!sep2) { + sep2 = name + pos; + } + if (sep2-sep-1 == sizeof(mdns_oneshot_t::proto)-1) { + memcpy(proto, sep+1, sizeof(mdns_oneshot_t::proto)-1); + } + } + } + } else if (atype == DNS_TYPE_SRV && rdLen >= sizeof(dns_rr_srv_t)) { + dns_rr_srv_t *srv = reinterpret_cast(record+len); + size_t pos = 0; + if (readName(record, done, len+sizeof(dns_rr_srv_t)-1, len+rdLen, name, sizeof(name), &pos) == 0) { + anCnt = 0; + break; // skip this one + } + name[pos] = 0; + validPort = ntohs(srv->port) == 9999; +#ifdef DEBUG_MDNS + printf(" srv port %d target %s\n", ntohs(srv->port), name); +#endif + } else if (atype == DNS_TYPE_A) { + // ipv4 address +#ifdef DEBUG_MDNS + printf(" address %d.%d.%d.%d\n", record[len], record[len+1], record[len+2], record[len+3]); +#endif + memcpy(reinterpret_cast(&validAddress.s_addr), record+len, 4); + } + len += rdLen; + } + if (!anCnt) { + continue; + } + if (validPort && validAddress.s_addr != INADDR_ANY && validAddress.s_addr != INADDR_NONE && proto[0]) { + mdns_oneshot_t *storeTo; + if (!found && (!limitId.length() || limitId.compare(id) == 0)) { + storeTo = result; + found = true; + } else if (found && strcmp(id, result->id) == 0) { + // skip duplicate answer + continue; + } else { + foundMore = !limitId.length(); + if (moreRemain > 0) { + storeTo = moreResults++; + moreRemain--; + (*moreCount)++; + } else if (!found) { + continue; + } else { + break; + } + } + storeTo->address = validAddress; + strncpy(storeTo->id, id, sizeof(mdns_oneshot_t::id)); + strncpy(storeTo->proto, proto, sizeof(mdns_oneshot_t::proto)); + if (found && (limitId.length() || !moreRemain)) { + break; // found the desired one or no more space left for others + } + } + } + close(sock); + return found ? foundMore ? 2 : 1 : 0; +} + } // namespace ebusd diff --git a/src/lib/utils/tcpsocket.h b/src/lib/utils/tcpsocket.h index 15a2c165f..aaebbd21d 100755 --- a/src/lib/utils/tcpsocket.h +++ b/src/lib/utils/tcpsocket.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2014-2025 John Baier , Roland Jax 2012-2014 * * 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 @@ -25,6 +25,11 @@ #include #include #include +#ifdef __FreeBSD__ + #include +#else + #include +#endif /** typedef for referencing @a sockaddr_in within namespace. */ typedef struct sockaddr_in socketaddress; @@ -43,16 +48,29 @@ using std::string; /** * Connect a TCP or UDP socket. - * @param server the server name or ip address to connect to. + * @param server the server name or ip address to connect to, optionally + * followed by "@intf" to bind to a certain interface address. * @param port the port number. - * @param udp true for UDP, false for TCP. + * @param udpProto the protocol to use for UDP (e.g. IPPROTO_UDP), or 0 for TCP. * @param storeAddress optional pointer to where the socket address will be stored. - * @param tcpConnectTimeout the TCP connect timeout in seconds, or 0. + * @param tcpConnectTimeoutUdpOptions the connect timeout in seconds for TCP (or 0), + * or a bit set of options for UDP (0x01 for binding to the same source port, + * 0x02 for connecting to the target address). * @param tcpKeepAliveInterval optional interval in seconds for sending TCP keepalive. + * @param storeIntf optional pointer to where the interface address will be stored. * @return the connected socket file descriptor on success, or -1 on error. */ -int socketConnect(const char* server, uint16_t port, bool udp, socketaddress* storeAddress = nullptr, -int tcpConnectTimeout = 0, int tcpKeepAliveInterval = 0); +int socketConnect(const char* server, uint16_t port, int udpProto, socketaddress* storeAddress = nullptr, +int tcpConnectTimeoutUdpOptions = 0, int tcpKeepAliveInterval = 0, struct in_addr* storeIntf = nullptr); + +/** + * Poll a socket. + * @param sfd the socket file descriptor. + * @param which the set of bits of the event(s) to wait for (e.g. POLLIN and/or POLLOUT). + * @param timeoutSeconds the poll timeout in seconds. + * @return a set of bits indicating the received event (e.g. POLLIN and/or POLLOUT), or -1 on error. + */ +int socketPoll(int sfd, int which, int timeoutSeconds); /** @@ -198,6 +216,31 @@ class TCPServer { bool m_listening; }; +/** + * Structure for resolving device address via mDNS one-shot query. + */ +typedef struct __attribute__ ((packed)) { + /** the device IP address. */ + struct in_addr address; + /** the device ID. */ + char id[6*2+1]; + /** the announced ebusd protocol. */ + char proto[3+1]; +} mdns_oneshot_t; + +/** + * Use an mDNS one-shot query to resolve an eBUS device. + * @param url the desired ID (or empty) followed by an optional host interface IP to use after an '@' sign. + * @param result pointer to an mdns_oneshot_t structure to store the result in. + * @param moreRequests optional pointer to further results not matching the desired ID. + * @param moreCount optional pointer to the size of the moreRequests argument that will be updated with the number of + * further results found upon success. + * @return 1 on success, 2 when another device was found, 0 when no device was found or no found device matched the + * desired ID, or less than 0 on error. + */ +int resolveMdnsOneShot(const char* url, mdns_oneshot_t *result, + mdns_oneshot_t *moreResults = nullptr, size_t *moreCount = nullptr); + } // namespace ebusd #endif // LIB_UTILS_TCPSOCKET_H_ diff --git a/src/lib/utils/thread.cpp b/src/lib/utils/thread.cpp index 4a5d56000..c1567161b 100755 --- a/src/lib/utils/thread.cpp +++ b/src/lib/utils/thread.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2014-2025 John Baier , Roland Jax 2012-2014 * * 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 @@ -94,6 +94,9 @@ bool WaitThread::join() { } bool WaitThread::Wait(int seconds, int millis) { + if (!isRunning()) { + return false; + } pthread_mutex_lock(&m_mutex); struct timespec t; clockGettime(&t); @@ -123,6 +126,9 @@ void NotifiableThread::notify() { } bool NotifiableThread::waitNotified(int millis) { + if (!isRunning()) { + return false; + } pthread_mutex_lock(&m_mutex); if (!m_notified) { struct timespec t; diff --git a/src/lib/utils/thread.h b/src/lib/utils/thread.h index 4690929ee..5d58f5c1e 100755 --- a/src/lib/utils/thread.h +++ b/src/lib/utils/thread.h @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2014-2025 John Baier , Roland Jax 2012-2014 * * 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 diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 7c664e73a..2537ad372 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,3 +1,5 @@ +add_definitions(-Wno-unused-parameter) + set(ebusctl_SOURCES ebusctl.cpp) set(ebuspicloader_SOURCES ebuspicloader.cpp intelhex/intelhexclass.cpp) @@ -7,13 +9,7 @@ include_directories(intelhex) add_executable(ebusctl ${ebusctl_SOURCES}) add_executable(ebuspicloader ${ebuspicloader_SOURCES}) -target_link_libraries(ebusctl utils ebus ${LIB_ARGP} ${ebusctl_LIBS}) -target_link_libraries(ebuspicloader utils ${LIB_ARGP} ${ebuspicloader_LIBS}) - -if(WITH_EBUSFEED) - set(ebusfeed_SOURCES ebusfeed.cpp) - add_executable(ebusfeed ${ebusfeed_SOURCES}) - target_link_libraries(ebusfeed ebus ${LIB_ARGP} ${ebusfeed_LIBS}) -endif(WITH_EBUSFEED) +target_link_libraries(ebusctl utils ebus ${ebusctl_LIBS}) +target_link_libraries(ebuspicloader utils ${ebuspicloader_LIBS}) -install(TARGETS ebusctl ebuspicloader EXPORT ebusd DESTINATION usr/bin) +install(TARGETS ebusctl ebuspicloader EXPORT ebusd DESTINATION bin) diff --git a/src/tools/Makefile.am b/src/tools/Makefile.am index 820489b8f..37c1058d2 100644 --- a/src/tools/Makefile.am +++ b/src/tools/Makefile.am @@ -1,5 +1,6 @@ AM_CXXFLAGS = -I$(top_srcdir)/src \ - -isystem$(top_srcdir) + -isystem$(top_srcdir) \ + -Wno-unused-parameter bin_PROGRAMS = ebusctl \ ebuspicloader @@ -10,13 +11,6 @@ ebusctl_LDADD = ../lib/utils/libutils.a ebuspicloader_SOURCES = ebuspicloader.cpp intelhex/intelhexclass.cpp ebuspicloader_LDADD = ../lib/utils/libutils.a -if WITH_EBUSFEED -bin_PROGRAMS += ebusfeed -ebusfeed_SOURCES = ebusfeed.cpp -ebusfeed_LDADD = ../lib/utils/libutils.a \ - ../lib/ebus/libebus.a -endif - distclean-local: -rm -f Makefile.in -rm -rf .libs diff --git a/src/tools/README.md b/src/tools/README.md index 23b5f5d6d..9294b9833 100644 --- a/src/tools/README.md +++ b/src/tools/README.md @@ -2,39 +2,71 @@ eBUS Adapter 3 PIC Loader ========================= This is a tool for loading new firmware to the -[eBUS adapter 3 PIC](https://adapter.ebusd.eu/) -and to configure IP settings for the Ethernet variant. +[eBUS adapter 3 PIC](https://adapter.ebusd.eu/v31) +and to configure the variant, IP settings for the Ethernet variant, and other settings. All of this is done via the bootloader on the PIC. Consequently, when the bootloader is not running, this tool can't do anything. -Check the [eBUS adapter 3 documentation](https://adapter.ebusd.eu/picfirmware) +Check the [eBUS adapter 3 documentation](https://adapter.ebusd.eu/v31/picfirmware) on how to start the bootloader. -This tool was developed due to very unreadable output of the MPLAB bootloader -host application. +The binary is part of the [release](https://github.com/john30/ebusd/releases) and a Windows binary based on Cygwin is available for download here: +[ebuspicloader-windows.zip](https://adapter.ebusd.eu/v31/firmware/ebuspicloader-windows.zip) +It can be started from within Windows `cmd.exe` after extracting the files to a folder and `cd` into that folder. +If Cygwin is already installed, only the `ebuspicloader.exe` needs to be extracted and can be called directly +from within a Cygwin shell. +In Cygwin, Windows COM ports are mapped under `/dev/ttyS*`, e.g. `COM1` would be `/dev/ttyS0`. + +This tool is an alternative to and extension of the MPLAB bootloader host application that produces a lot +of unreadable output. + Usage ----- `ebuspicloader --help` reveals the options: ``` Usage: ebuspicloader [OPTION...] PORT -A tool for loading firmware to the eBUS adapter PIC. +A tool for loading firmware to the eBUS adapter PIC and configure adjustable +settings. - -a, --arbdel=US set arbitration delay to US microseconds (0-620 in - steps of 10, default 200, since firmware 20211128) - -d, --dhcp set dynamic IP address via DHCP - -f, --flash=FILE flash the FILE to the device + IP options: + -d, --dhcp set dynamic IP address via DHCP (default) + -g, --gateway=GW set fix IP gateway to GW (if necessary and other + than net address + 1) -i, --ip=IP set fix IP address (e.g. 192.168.0.10) -m, --mask=MASK set fix IP mask (e.g. 24) -M, --macip set the MAC address suffix from the IP address + -N, --macid set the MAC address suffix from internal ID + (default) + + eBUS options: + -a, --arbdel=US set arbitration delay to US microseconds (0-620 in + steps of 10, default 200, since firmware + 20211128) + + PIC options: + -f, --flash=FILE flash the FILE to the device + -o, --pingoff disable visual ping + -p, --pingon enable visual ping (default) -r, --reset reset the device at the end on success - -s, --slow use low speed for transfer + --variant=VARIANT set the VARIANT to U=USB/RPI (high-speed), W=WIFI, + E=Ethernet, N=non-enhanced USB/RPI/WIFI, + F=non-enhanced Ethernet (lowercase to allow + hardware jumpers, default "u", since firmware + 20221206) + + Tool options: + -s, --slow low speed mode for transfer (115kBd instead of + 921kBd) -v, --verbose enable verbose output + -?, --help give this help list --usage give a short usage message -V, --version print program version -PORT is the serial port to use (e.g./dev/ttyUSB0) also supporting a trailing wildcard '*' for testing multiple ports. +PORT is either the serial port to use (e.g. /dev/ttyUSB0) that also supports a +trailing wildcard '*' for testing multiple ports, or a network port as +"ip:port" for use with e.g. socat or ebusd-esp in PIC pass-through mode. ``` Flash firmware @@ -42,16 +74,19 @@ Flash firmware For flashing a new firmware, you would typically do something like this: `ebuspicloader -f firmware.hex /dev/ttyUSB0` -On success, the output looks like this: +On success, the output looks similar to this: ``` Device ID: 30b0 (PIC16F15356) -Device revision: 0.1 -Bootloader version: 1 [0a6c] +Device revision: 0.2 +Bootloader version: 2 [73c8] Firmware version not found MAC address: ae:b0:53:26:15:80 -IP address: DHCP +IP address: DHCP (default) +Arbitration delay: 200 us (default) +Visual ping: on (default) +Variant: USB/RPI (high-speed), allow hardware jumpers (default) -New firmware version: 1 [c5e7] +New firmware version: 1 [7f16] erasing flash: done. flashing: @@ -65,21 +100,26 @@ flashing succeeded. Configure IP ------------ -For changing the IP address of an Ethernet enabled adapter, you would do -something like this: +Changing the IP address of an Ethernet enabled adapter, would be done like this: `ebuspicloader -i 192.168.1.10 -m 24 /dev/ttyUSB0` -On success, the output looks like this: +On success, the output looks similar to this: ``` Device ID: 30b0 (PIC16F15356) -Device revision: 0.1 -Bootloader version: 1 [0a6c] -Firmware version: 1 [c5e7] +Device revision: 0.2 +Bootloader version: 2 [73c8] +Firmware version: 1 [7f16] MAC address: ae:b0:53:26:15:80 -IP address: DHCP +IP address: DHCP (default) +Arbitration delay: 200 us (default) +Visual ping: on (default) +Variant: Ethernet, ignore hardware jumpers -Writing IP settings: done. -IP settings changed to: +Writing settings: done. +Settings changed to: MAC address: ae:80:53:26:15:80 IP address: 192.168.1.10/24 +Arbitration delay: 200 us (default) +Visual ping: on (default) +Variant: Ethernet, ignore hardware jumpers ``` diff --git a/src/tools/ebusctl.cpp b/src/tools/ebusctl.cpp index ea2642f81..d2b4113b9 100755 --- a/src/tools/ebusctl.cpp +++ b/src/tools/ebusctl.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2014-2025 John Baier , Roland Jax 2012-2014 * * 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 @@ -20,7 +20,6 @@ # include #endif -#include #include #ifdef HAVE_PPOLL # include @@ -30,6 +29,7 @@ #include #include #include +#include "lib/utils/arg.h" #include "lib/utils/tcpsocket.h" namespace ebusd { @@ -45,8 +45,8 @@ struct options { const char* server; //!< ebusd server host (name or ip) [localhost] uint16_t port; //!< ebusd server port [8888] uint16_t timeout; //!< ebusd connect/send/receive timeout + bool errorResponse; //!< non-zero exit on error response - char* const *args; //!< arguments to pass to ebusd unsigned int argCount; //!< number of arguments to pass to ebusd }; @@ -54,54 +54,40 @@ struct options { static struct options opt = { "localhost", // server 8888, // port - 60, // timeout + 60, // timeout + false, // non-zero exit on error response - nullptr, // args 0 // argCount }; -/** the version string of the program. */ -const char *argp_program_version = "ebusctl of """ PACKAGE_STRING ""; - -/** the report bugs to address of the program. */ -const char *argp_program_bug_address = "" PACKAGE_BUGREPORT ""; - -/** the documentation of the program. */ -static const char argpdoc[] = - "Client for acessing " PACKAGE " via TCP.\n" - "\v" - "If given, send COMMAND together with CMDOPT options to " PACKAGE ".\n" - "Use 'help' as COMMAND for help on available " PACKAGE " commands."; - -/** the description of the accepted arguments. */ -static char argpargsdoc[] = "\nCOMMAND [CMDOPT...]"; - /** the definition of the known program arguments. */ -static const struct argp_option argpoptions[] = { - {nullptr, 0, nullptr, 0, "Options:", 1 }, - {"server", 's', "HOST", 0, "Connect to " PACKAGE " on HOST (name or IP) [localhost]", 0 }, - {"port", 'p', "PORT", 0, "Connect to " PACKAGE " on PORT [8888]", 0 }, +static const argDef argDefs[] = { + {nullptr, 0, nullptr, 0, "Options:"}, + {"server", 's', "HOST", 0, "Connect to " PACKAGE " on HOST (name or IP) [localhost]"}, + {"port", 'p', "PORT", 0, "Connect to " PACKAGE " on PORT [8888]"}, {"timeout", 't', "SECS", 0, "Timeout for connecting to/receiving from " PACKAGE - ", 0 for none [60]", 0 }, + ", 0 for none [60]"}, + {"error", 'e', nullptr, 0, "Exit non-zero if the connection was fine but the response indicates non-success"}, + + {nullptr, 0x100, "COMMAND", af_optional|af_multiple, "COMMAND (and arguments) to send to " PACKAGE "."}, - {nullptr, 0, nullptr, 0, nullptr, 0 }, + {nullptr, 0, nullptr, 0, nullptr}, }; /** * The program argument parsing function. - * @param key the key from @a argpoptions. + * @param key the key from @a argDefs. * @param arg the option argument, or nullptr. - * @param state the parsing state. + * @param parseOpt the parse options. */ -error_t parse_opt(int key, char *arg, struct argp_state *state) { - struct options *opt = (struct options*)state->input; +static int parse_opt(int key, char *arg, const argParseOpt *parseOpt, struct options *opt) { char* strEnd = nullptr; unsigned int value; switch (key) { // Device settings: case 's': // --server=localhost if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid server"); + argParseError(parseOpt, "invalid server"); return EINVAL; } opt->server = arg; @@ -109,7 +95,7 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { case 'p': // --port=8888 value = strtoul(arg, &strEnd, 10); if (strEnd == nullptr || strEnd == arg || *strEnd != 0 || value < 1 || value > 65535) { - argp_error(state, "invalid port"); + argParseError(parseOpt, "invalid port"); return EINVAL; } opt->port = (uint16_t)value; @@ -117,17 +103,20 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { case 't': // --timeout=10 value = strtoul(arg, &strEnd, 10); if (strEnd == nullptr || strEnd == arg || *strEnd != 0 || value > 3600) { - argp_error(state, "invalid timeout"); + argParseError(parseOpt, "invalid timeout"); return EINVAL; } opt->timeout = (uint16_t)value; break; - case ARGP_KEY_ARGS: - opt->args = state->argv + state->next; - opt->argCount = state->argc - state->next; + case 'e': // --error + opt->errorResponse = true; break; default: - return ARGP_ERR_UNKNOWN; + if (key >= 0x100) { + opt->argCount++; + } else { + return ESRCH; + } } return 0; } @@ -332,8 +321,12 @@ bool connect(const char* host, uint16_t port, uint16_t timeout, char* const *arg } } } else { - cout << fetchData(socket, listening, timeout, errored); + string response = fetchData(socket, listening, timeout, errored); + cout << response; cout.flush(); + if (errored || (opt.errorResponse && response.substr(0, 4) == "ERR:")) { + ret = false; + } } } } while (!errored && !once && !cin.eof()); @@ -352,12 +345,25 @@ bool connect(const char* host, uint16_t port, uint16_t timeout, char* const *arg * @return the exit code. */ int main(int argc, char* argv[]) { - struct argp argp = { argpoptions, parse_opt, argpargsdoc, argpdoc, nullptr, nullptr, nullptr }; - setenv("ARGP_HELP_FMT", "no-dup-args-note", 0); - if (argp_parse(&argp, argc, argv, ARGP_IN_ORDER, nullptr, &opt) != 0) { - return EINVAL; + argParseOpt parseOpt = { + argDefs, + reinterpret_cast(parse_opt), + af_noVersion, + "Client for accessing " PACKAGE " via TCP.", + "If given, send COMMAND together with arguments to " PACKAGE ".\n" + "Use 'help' as COMMAND for help on available " PACKAGE " commands.", + nullptr, + }; + switch (argParse(&parseOpt, argc, argv, &opt)) { + case 0: // OK + break; + case '?': // help printed + return 0; + default: + return EINVAL; } - bool success = connect(opt.server, opt.port, opt.timeout, opt.args, opt.argCount); + + bool success = connect(opt.server, opt.port, opt.timeout, argv + argc - opt.argCount, opt.argCount); exit(success ? EXIT_SUCCESS : EXIT_FAILURE); } diff --git a/src/tools/ebusfeed.cpp b/src/tools/ebusfeed.cpp index 69579c381..a9f93fef2 100755 --- a/src/tools/ebusfeed.cpp +++ b/src/tools/ebusfeed.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2014-2022 John Baier , Roland Jax 2012-2014 + * Copyright (C) 2014-2025 John Baier , Roland Jax 2012-2014 * * 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 @@ -20,7 +20,6 @@ # include #endif -#include #include #include #include @@ -29,6 +28,7 @@ #include #include "lib/ebus/device.h" #include "lib/ebus/result.h" +#include "lib/utils/arg.h" namespace ebusd { @@ -58,51 +58,28 @@ static struct options opt = { "/tmp/ebus_dump.bin", // dumpFile }; -/** the version string of the program. */ -const char *argp_program_version = "ebusfeed of """ PACKAGE_STRING ""; - -/** the report bugs to address of the program. */ -const char *argp_program_bug_address = "" PACKAGE_BUGREPORT ""; - -/** the documentation of the program. */ -static const char argpdoc[] = - "Feed data from an " PACKAGE " DUMPFILE to a serial device.\n" - "\v" - "With no DUMPFILE, /tmp/ebus_dump.bin is used.\n" - "\n" - "Example for setting up two pseudo terminals with 'socat':\n" - " 1. 'socat -d -d pty,raw,echo=0 pty,raw,echo=0'\n" - " 2. create symbol links to appropriate devices, e.g.\n" - " 'ln -s /dev/pts/2 /dev/ttyUSB60'\n" - " 'ln -s /dev/pts/3 /dev/ttyUSB20'\n" - " 3. start " PACKAGE ": '" PACKAGE " -f -d /dev/ttyUSB20 --nodevicecheck'\n" - " 4. start ebusfeed: 'ebusfeed /path/to/ebus_dump.bin'\n"; - -/** the description of the accepted arguments. */ -static char argpargsdoc[] = "[DUMPFILE]"; - /** the definition of the known program arguments. */ -static const struct argp_option argpoptions[] = { - {"device", 'd', "DEV", 0, "Write to DEV (serial device) [/dev/ttyUSB60]", 0 }, - {"time", 't', "USEC", 0, "Delay each byte by USEC us [10000]", 0 }, +static const ebusd::argDef argDefs[] = { + {"device", 'd', "DEV", 0, "Write to DEV (serial device) [/dev/ttyUSB60]"}, + {"time", 't', "USEC", 0, "Delay each byte by USEC us [10000]"}, + {nullptr, 0x100, "DUMPFILE", af_optional, "Dump file to read [/tmp/ebus_dump.bin]"}, - {nullptr, 0, nullptr, 0, nullptr, 0 }, + {nullptr, 0, nullptr, 0, nullptr}, }; /** * The program argument parsing function. - * @param key the key from @a argpoptions. + * @param key the key from @a argDefs. * @param arg the option argument, or nullptr. - * @param state the parsing state. + * @param parseOpt the parse options. */ -error_t parse_opt(int key, char *arg, struct argp_state *state) { - struct options *opt = (struct options*)state->input; +static int parse_opt(int key, char *arg, const ebusd::argParseOpt *parseOpt, struct options *opt) { char* strEnd = nullptr; switch (key) { // Device settings: case 'd': // --device=/dev/ttyUSB60 if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid device"); + argParseError(parseOpt, "invalid device"); return EINVAL; } opt->device = arg; @@ -110,23 +87,20 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { case 't': // --time=10000 opt->time = (unsigned int)strtoul(arg, &strEnd, 10); if (strEnd == nullptr || strEnd == arg || *strEnd != 0 || opt->time < 1000 || opt->time > 100000000) { - argp_error(state, "invalid time"); + argParseError(parseOpt, "invalid time"); return EINVAL; } break; - case ARGP_KEY_ARG: - if (state->arg_num == 0) { - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argp_error(state, "invalid dumpfile"); - return EINVAL; - } - opt->dumpFile = arg; - } else { - return ARGP_ERR_UNKNOWN; + case 0x100: // DUMPFILE + if (arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid dumpfile"); + return EINVAL; } + opt->dumpFile = arg; break; + default: - return ARGP_ERR_UNKNOWN; + return ESRCH; } return 0; } @@ -139,12 +113,30 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { * @return the exit code. */ int main(int argc, char* argv[]) { - struct argp argp = { argpoptions, parse_opt, argpargsdoc, argpdoc, nullptr, nullptr, nullptr }; - setenv("ARGP_HELP_FMT", "no-dup-args-note", 0); - if (argp_parse(&argp, argc, argv, ARGP_IN_ORDER, nullptr, &opt) != 0) { - return EINVAL; + ebusd::argParseOpt parseOpt = { + argDefs, + reinterpret_cast(parse_opt), + af_noVersion, + "Feed data from an " PACKAGE " DUMPFILE to a serial device.", + "Example for setting up two pseudo terminals with 'socat':\n" + " 1. 'socat -d -d pty,raw,echo=0 pty,raw,echo=0'\n" + " 2. create symbol links to appropriate devices, e.g.\n" + " 'ln -s /dev/pts/2 /dev/ttyUSB60'\n" + " 'ln -s /dev/pts/3 /dev/ttyUSB20'\n" + " 3. start " PACKAGE ": '" PACKAGE " -f -d /dev/ttyUSB20 --nodevicecheck'\n" + " 4. start ebusfeed: 'ebusfeed /path/to/ebus_dump.bin'", + nullptr, + }; + switch (ebusd::argParse(&parseOpt, argc, argv, &opt)) { + case 0: // OK + break; + case '?': // help printed + return 0; + default: + return EINVAL; } - Device* device = Device::create(opt.device, false, false, false); + + Device* device = Device::create(opt.device, 0, false); if (device == nullptr) { cout << "unable to create device " << opt.device << endl; return EINVAL; diff --git a/src/tools/ebuspicloader.cpp b/src/tools/ebuspicloader.cpp index a4d195f0b..f39dbb10b 100644 --- a/src/tools/ebuspicloader.cpp +++ b/src/tools/ebuspicloader.cpp @@ -1,6 +1,6 @@ /* * ebusd - daemon for communication with eBUS heating systems. - * Copyright (C) 2020-2022 John Baier + * Copyright (C) 2020-2025 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -37,37 +36,37 @@ #include #include #include "intelhex/intelhexclass.h" +#include "lib/utils/arg.h" #include "lib/utils/tcpsocket.h" using ebusd::socketConnect; -/** the version string of the program. */ -const char *argp_program_version = "eBUS adapter PIC firmware loader"; - -/** the documentation of the program. */ -static const char argpdoc[] = - "A tool for loading firmware to the eBUS adapter PIC and configure some adjustable settings." - "\vPORT is either the serial port to use (e.g./dev/ttyUSB0) that also supports a trailing wildcard '*' for testing" - " multiple ports, or a network port as \"ip:port\" for use with e.g. socat or ebusd-esp."; - -static const char argpargsdoc[] = "PORT"; - /** the definition of the known program arguments. */ -static const struct argp_option argpoptions[] = { - {"verbose", 'v', nullptr, 0, "enable verbose output", 0 }, - {"dhcp", 'd', nullptr, 0, "set dynamic IP address via DHCP", 0 }, - {"ip", 'i', "IP", 0, "set fix IP address (e.g. 192.168.0.10)", 0 }, - {"mask", 'm', "MASK", 0, "set fix IP mask (e.g. 24)", 0 }, - {"gateway", 'g', "GW", 0, "set fix IP gateway to GW (if necessary and other than net address + 1)", 0 }, - {"macip", 'M', nullptr, 0, "set the MAC address suffix from the IP address", 0 }, +static const ebusd::argDef argDefs[] = { + {nullptr, 0, nullptr, 0, "IP options:"}, + {"dhcp", 'd', nullptr, 0, "set dynamic IP address via DHCP (default)"}, + {"ip", 'i', "IP", 0, "set fix IP address (e.g. 192.168.0.10)"}, + {"mask", 'm', "MASK", 0, "set fix IP mask (e.g. 24)"}, + {"gateway", 'g', "GW", 0, "set fix IP gateway to GW (if necessary and other than net address + 1)"}, + {"macip", 'M', nullptr, 0, "set the MAC address suffix from the IP address"}, + {"macid", 'N', nullptr, 0, "set the MAC address suffix from internal ID (default)"}, + {nullptr, 0, nullptr, 0, "eBUS options:"}, {"arbdel", 'a', "US", 0, "set arbitration delay to US microseconds (0-620 in steps of 10, default 200" - ", since firmware 20211128)", 0 }, - {"pingon", 'p', nullptr, 0, "enable visual ping", 0 }, - {"pingoff", 'o', nullptr, 0, "disable visual ping", 0 }, - {"flash", 'f', "FILE", 0, "flash the FILE to the device", 0 }, - {"reset", 'r', nullptr, 0, "reset the device at the end on success", 0 }, - {"slow", 's', nullptr, 0, "use low speed for transfer", 0 }, - {nullptr, 0, nullptr, 0, nullptr, 0 }, + ", since firmware 20211128)"}, + {nullptr, 0, nullptr, 0, "PIC options:"}, + {"pingon", 'p', nullptr, 0, "enable visual ping (default)"}, + {"pingoff", 'o', nullptr, 0, "disable visual ping"}, + {"variant", -3, "VARIANT", 0, "set the VARIANT to U=USB/RPI (high-speed), W=WIFI, E=Ethernet," + " N=non-enhanced USB/RPI/WIFI, F=non-enhanced Ethernet" + " (lowercase to allow hardware jumpers, default \"u\"" + ", since firmware 20221206)"}, + {"flash", 'f', "FILE", 0, "flash the FILE to the device"}, + {"reset", 'r', nullptr, 0, "reset the device at the end on success"}, + {nullptr, 0, nullptr, 0, "Tool options:"}, + {"verbose", 'v', nullptr, 0, "enable verbose output"}, + {"slow", 's', nullptr, 0, "low speed mode for transfer (115kBd instead of 921kBd)"}, + {nullptr, 0x100, "PORT", ebusd::af_optional, "port to connect to"}, + {nullptr, 0, nullptr, 0, nullptr}, }; static bool verbose = false; @@ -75,6 +74,7 @@ static bool setDhcp = false; static bool setIp = false; static uint8_t setIpAddress[] = {0, 0, 0, 0}; static bool setMacFromIp = false; +static bool setMacFromIpValue = true; static bool setMask = false; static uint8_t setMaskLen = 0x1f; static bool setGateway = false; @@ -83,9 +83,13 @@ static bool setArbitrationDelay = false; static uint16_t setArbitrationDelayMicros = 0; static bool setVisualPing = false; static bool setVisualPingOn = false; +static bool setVariant = false; +static uint8_t setVariantValue = 0; +static bool setVariantForced = false; static char* flashFile = nullptr; static bool reset = false; static bool lowSpeed = false; +static const char* portArg = nullptr; bool parseByte(const char *arg, uint8_t minValue, uint8_t maxValue, uint8_t *result) { char* strEnd = nullptr; @@ -117,7 +121,13 @@ bool parseShort(const char *arg, uint16_t minValue, uint16_t maxValue, uint16_t return true; } -error_t parse_opt(int key, char *arg, struct argp_state *state) { +/** + * The program argument parsing function. + * @param key the key from @a argDefs. + * @param arg the option argument, or nullptr. + * @param parseOpt the parse options. + */ +static int parse_opt(int key, char *arg, const ebusd::argParseOpt *parseOpt, void *userArg) { char *ip = nullptr, *part = nullptr; int pos = 0, sum = 0; struct stat st; @@ -127,23 +137,23 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { verbose = true; break; case 'd': // --dhcp - if (setIp || setMask) { - argp_error(state, "either DHCP or IP address is needed"); + if (setIp || setMask || setGateway) { + argParseError(parseOpt, "either DHCP or IP address is needed"); return EINVAL; } setDhcp = true; break; case 'i': // --ip=192.168.0.10 if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid IP address"); + argParseError(parseOpt, "invalid IP address"); return EINVAL; } if (setDhcp) { - argp_error(state, "either DHCP or IP address is needed"); + argParseError(parseOpt, "either DHCP or IP address is needed"); return EINVAL; } if (setIp) { - argp_error(state, "IP address was specified twice"); + argParseError(parseOpt, "IP address was specified twice"); return EINVAL; } ip = strdup(arg); @@ -158,41 +168,41 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { } free(ip); if (pos != 4 || part || sum == 0) { - argp_error(state, "invalid IP address"); + argParseError(parseOpt, "invalid IP address"); return EINVAL; } setIp = true; break; case 'm': // --mask=24 if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid IP mask"); + argParseError(parseOpt, "invalid IP mask"); return EINVAL; } if (setDhcp) { - argp_error(state, "either DHCP or IP address is needed"); + argParseError(parseOpt, "either DHCP or IP address is needed"); return EINVAL; } if (setMask) { - argp_error(state, "mask was specified twice"); + argParseError(parseOpt, "mask was specified twice"); return EINVAL; } - if (!parseByte(arg, 0, 0x1e, &setMaskLen)) { - argp_error(state, "invalid IP mask"); + if (!parseByte(arg, 1, 0x1e, &setMaskLen)) { + argParseError(parseOpt, "invalid IP mask"); return EINVAL; } setMask = true; break; case 'g': // --gateway=192.168.0.11 if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid gateway"); + argParseError(parseOpt, "invalid gateway"); return EINVAL; } if (setDhcp) { - argp_error(state, "either DHCP or IP address is needed"); + argParseError(parseOpt, "either DHCP or IP address is needed"); return EINVAL; } if (!setIp || !setMask) { - argp_error(state, "IP and mask need to be specified before gateway"); + argParseError(parseOpt, "IP and mask need to be specified before gateway"); return EINVAL; } ip = strdup(arg); @@ -209,7 +219,7 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { uint8_t maskRemain = setMaskLen-pos*8; uint8_t mask = maskRemain >= 8 ? 255 : maskRemain == 0 ? 0 : (255^((1 << (8 - maskRemain)) - 1)); if ((address & mask) != (setIpAddress[pos] & mask)) { - argp_error(state, "invalid gateway (different network)"); + argParseError(parseOpt, "invalid gateway (different network)"); free(ip); return EINVAL; } @@ -218,15 +228,15 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { } free(ip); if (pos != 4 || part || sum == 0 || setGatewayBits == 0) { - argp_error(state, "invalid gateway"); + argParseError(parseOpt, "invalid gateway"); return EINVAL; } if (setGatewayBits == hostBits) { - argp_error(state, "invalid gateway (same as address)"); + argParseError(parseOpt, "invalid gateway (same as address)"); return EINVAL; } if (!setGatewayBits || setGatewayBits == ((1 << (32 - setMaskLen)) - 1)) { - argp_error(state, "invalid gateway (net or broadcast address)"); + argParseError(parseOpt, "invalid gateway (net or broadcast address)"); return EINVAL; } if (setGatewayBits == 1) { // default @@ -241,7 +251,7 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { } if (!(setGatewayBits >> 5)) { if (!(setGatewayBits & 0x1f)) { - argp_error(state, "invalid gateway (net address)"); + argParseError(parseOpt, "invalid gateway (net address)"); return EINVAL; } // fine: host part above max gateway adjustable bits is the same and remainder non-zero @@ -255,18 +265,23 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { setGateway = true; break; } - argp_error(state, "invalid gateway (out of possible range of first/last 31 hosts in subnet)"); + argParseError(parseOpt, "invalid gateway (out of possible range of first/last 31 hosts in subnet)"); return EINVAL; case 'M': // --macip setMacFromIp = true; + setMacFromIpValue = true; + break; + case 'N': // --macid + setMacFromIp = true; + setMacFromIpValue = false; break; case 'a': // --arbdel=1000 if (arg == nullptr || arg[0] == 0) { - argp_error(state, "invalid arbitration delay"); + argParseError(parseOpt, "invalid arbitration delay"); return EINVAL; } if (!parseShort(arg, 0, 620, &setArbitrationDelayMicros)) { - argp_error(state, "invalid arbitration delay"); + argParseError(parseOpt, "invalid arbitration delay"); return EINVAL; } setArbitrationDelay = true; @@ -279,9 +294,31 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { setVisualPing = true; setVisualPingOn = false; break; + case -3: // --variant=U|W|E|F|N|u|w|e|f|n + if (arg == nullptr || arg[0] == 0) { + argParseError(parseOpt, "invalid variant"); + return EINVAL; + } + if (arg[0] == 'u' || arg[0] == 'U') { + setVariantValue = 3; + } else if (arg[0] == 'w' || arg[0] == 'W') { + setVariantValue = 2; + } else if (arg[0] == 'e' || arg[0] == 'E') { + setVariantValue = 1; + } else if (arg[0] == 'f' || arg[0] == 'F') { + setVariantValue = 4; + } else if (arg[0] == 'n' || arg[0] == 'N') { + setVariantValue = 0; + } else { + argParseError(parseOpt, "invalid variant"); + return EINVAL; + } + setVariantForced = arg[0]<'a'; + setVariant = true; + break; case 'f': // --flash=firmware.hex if (arg == nullptr || arg[0] == 0 || stat(arg, &st) != 0 || !S_ISREG(st.st_mode)) { - argp_error(state, "invalid flash file"); + argParseError(parseOpt, "invalid flash file"); return EINVAL; } flashFile = arg; @@ -292,8 +329,11 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { case 's': // --slow lowSpeed = true; break; + case 0x100: // PORT + portArg = arg; + break; default: - return ARGP_ERR_UNKNOWN; + return ESRCH; } return 0; } @@ -351,6 +391,10 @@ typedef union #define FRAME_HEADER_LEN 9 #define FRAME_MAX_LEN (FRAME_HEADER_LEN+2*WRITE_FLASH_BLOCKSIZE) #define BAUDRATE_LOW B115200 +#if !defined(B921600) && defined(__APPLE__) +// MAC OS workaround +# define B921600 921600 +#endif #define BAUDRATE_HIGH B921600 #define WAIT_BYTE_TRANSFERRED_MILLIS 200 #define WAIT_BITRATE_DETECTION_MICROS 100 @@ -757,17 +801,19 @@ int openSerial(std::string port) { close(fd); return -1; } + std::cout << "opened " << port << std::endl; return fd; } int openNet(std::string host, uint16_t port) { // open network port - int fd = socketConnect(host.c_str(), port, false, nullptr, 5); + int fd = socketConnect(host.c_str(), port, 0, nullptr, 5); if (fd < 0) { std::cerr << "unable to open " << host << std::endl; return -1; } fcntl(fd, F_SETFL, O_NONBLOCK); // set non-blocking + std::cout << "opened " << host << ":" << static_cast(port) << std::endl; return fd; } @@ -994,7 +1040,7 @@ int readSettings(int fd, uint8_t* currentData = nullptr) { } std::cout << std::endl; if (maskLen == 0x1f || (ip[0]|ip[1]|ip[2]|ip[3]) == 0) { - std::cout << "IP address: DHCP" << std::endl; + std::cout << "IP address: DHCP (default)" << std::endl; } else { std::cout << "IP address:"; for (uint8_t pos = 0, maskRemain = maskLen; pos < 4; pos++, maskRemain -= maskRemain >= 8 ? 8 : maskRemain) { @@ -1012,7 +1058,7 @@ int readSettings(int fd, uint8_t* currentData = nullptr) { // non-mask bits outside of |gw reach uint8_t mask = maskLen <= 24 ? 0 : (255^((1 << (8 - (maskLen-24))) - 1)); ip[3] |= ((~mask)^0x1f) | (gw&0x1f); - if (maskLen<24) { + if (maskLen < 24) { // more than just the last IP byte are affected: set non-mask bits to 1 as well in bytes 0-2 for (uint8_t pos = 0, maskRemain = maskLen; pos < 3; pos++, maskRemain -= maskRemain >= 8 ? 8 : maskRemain) { mask = maskRemain >= 8 ? 255 : maskRemain == 0 ? 0 : (255^((1 << (8 - maskRemain)) - 1)); @@ -1026,9 +1072,6 @@ int readSettings(int fd, uint8_t* currentData = nullptr) { for (int i=0; i < 4; i++) { std::cout << (i == 0?' ':'.') << std::dec << static_cast(ip[i]); } - if (gw == 0x3f) { - std::cout << " (default)"; - } std::cout << std::endl; } uint16_t arbitrationDelay = configData[3]&0x3f; @@ -1045,6 +1088,34 @@ int readSettings(int fd, uint8_t* currentData = nullptr) { } else { std::cout << "off" << std::endl; } + std::cout << "Variant: "; // since firmware 20221206 + switch (configData[5]&0x03) { + case 3: + std::cout << "USB/RPI (high-speed)"; + break; + case 2: + std::cout << "WIFI"; + break; + case 1: + std::cout << "Ethernet"; + break; + default: + std::cout << "non-enhanced "; + if (maskLen) { + std::cout << "Ethernet"; + } else { + std::cout << "USB/RPI/WIFI"; + } + } + if (configData[5]&0x04) { + std::cout << ", allow hardware jumpers"; + } else { + std::cout << ", ignore hardware jumpers"; + } + if ((configData[5]&0x07) == 0x07) { + std::cout << " (default)"; + } + std::cout << std::endl; return 0; } @@ -1055,10 +1126,14 @@ bool writeSettings(int fd, uint8_t* currentData = nullptr) { memcpy(configData, currentData, sizeof(configData)); } if (setMacFromIp) { - configData[1] &= ~0x20; // set useMUI + configData[1] = (configData[1]&~0x20) | (setMacFromIpValue ? 0 : 0x20); // set useMUI } - configData[1] = (configData[1]&~0x1f) | (setMaskLen&0x1f); - if (setIp) { + if (setDhcp) { + configData[1] |= 0x1f; + } else if (setIp) { + if (setMask) { + configData[1] = (configData[1]&~0x1f) | (setMaskLen&0x1f); + } for (int i = 0; i < 4; i++) { configData[i * 2] = setIpAddress[i]; } @@ -1072,6 +1147,12 @@ bool writeSettings(int fd, uint8_t* currentData = nullptr) { if (setVisualPing) { configData[5] = (configData[5]&0x1f) | (setVisualPingOn?0x20:0); } + if (setVariant) { + configData[5] = (configData[5]&0x38) | (setVariantForced?0:0x04) | (setVariantValue&0x03); + if (setVariantValue == 0) { + configData[1] = (configData[1]&~0x1f); // set mask=0 to disable Ethernet + } + } if (writeConfig(fd, 0x0000, 8, configData) != 0) { std::cerr << "failed" << std::endl; return false; @@ -1083,29 +1164,45 @@ bool writeSettings(int fd, uint8_t* currentData = nullptr) { int run(int fd); int main(int argc, char* argv[]) { - struct argp aargp = { argpoptions, parse_opt, argpargsdoc, argpdoc, nullptr, nullptr, nullptr }; - int arg_index = -1; - setenv("ARGP_HELP_FMT", "no-dup-args-note", 0); - - if (argp_parse(&aargp, argc, argv, ARGP_IN_ORDER, &arg_index, nullptr) != 0) { - std::cerr << "invalid arguments" << std::endl; - exit(EXIT_FAILURE); + ebusd::argParseOpt parseOpt = { + argDefs, + reinterpret_cast(parse_opt), + ebusd::af_noVersion, + "A tool for loading firmware to the eBUS adapter PIC and configure adjustable settings.", + "PORT is either the serial port to use (e.g. " +#ifdef __CYGWIN__ + "/dev/ttyS0 for COM1 on Windows" +#else + "/dev/ttyUSB0" +#endif + ") that also supports a trailing wildcard '*' for testing" + " multiple ports, or a network port as \"ip:port\" for use with e.g. socat or ebusd-esp in PIC pass-through mode.", + nullptr + }; + switch (ebusd::argParse(&parseOpt, argc, argv, nullptr)) { + case 0: // OK + break; + case '?': // help printed + return 0; + default: + return EINVAL; } + bool forceHelp = false; if (setIp != setMask || (setMacFromIp && !setIp)) { std::cerr << "incomplete IP arguments" << std::endl; - arg_index = argc; // force help output + forceHelp = true; } - if (argc-arg_index < 1) { + if (forceHelp || !portArg) { if (flashFile) { printFileChecksum(); exit(EXIT_SUCCESS); } else { - argp_help(&aargp, stderr, ARGP_HELP_STD_ERR, const_cast("ebuspicloader")); + ebusd::argHelp(argv[0], &parseOpt); exit(EXIT_FAILURE); } } - std::string port = argv[arg_index]; + std::string port = portArg; std::string::size_type pos = port.find('*'); if (pos == std::string::npos || pos != port.length()-1) { int fd; @@ -1151,6 +1248,7 @@ int main(int argc, char* argv[]) { continue; } run(fd); + std::cout << std::endl; } return 0; } @@ -1221,7 +1319,7 @@ int run(int fd) { success = false; } } - if (setIp || setDhcp || setArbitrationDelay || setVisualPing) { + if (setMacFromIp || setIp || setDhcp || setArbitrationDelay || setVisualPing || setVariant) { if (writeSettings(fd, useCurrentConfigData ? currentConfigData : nullptr)) { std::cout << "Settings changed to:" << std::endl; readSettings(fd); diff --git a/test_all.sh b/test_all.sh index f9e2874f7..cca8e7839 100755 --- a/test_all.sh +++ b/test_all.sh @@ -1,3 +1,3 @@ #!/bin/sh -(cd src/lib/ebus/test && make >/dev/null && ./test_filereader && ./test_data && ./test_message && ./test_symbol && echo "standard: OK!")|egrep -v "OK$" -(cd src/lib/ebus/contrib/test && make >/dev/null && ./test_contrib && echo "contrib: OK!")|egrep -v "OK$" +(cd src/lib/ebus/test && make >/dev/null && ./test_filereader && ./test_data && ./test_message && ./test_symbol && echo "standard: OK!")|grep -Ev "OK$" +(cd src/lib/ebus/contrib/test && make >/dev/null && ./test_contrib && echo "contrib: OK!")|grep -Ev "OK$" diff --git a/test_coverage.sh b/test_coverage.sh index e76ac9ca8..0e2a56d24 100755 --- a/test_coverage.sh +++ b/test_coverage.sh @@ -1,58 +1,63 @@ -#!/bin/bash -./src/ebusd/ebusd --help >/dev/null -./src/ebusd/ebusd -r -f -x >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -d "" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -d "tcp:192.168.999.999:1" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -d "enh:192.168.999.999:1" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -d "/dev/ttyUSBx9" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --nodevicecheck >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --readonly >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --scanconfig=full -r >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --latency 999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -c "" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -r --scanconfig=fe >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -r --configlang=en >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --pollinterval 999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --inject 01fe030400/ >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --address 999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --acquiretimeout 999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --acquireretries 999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --sendretries 999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --receivetimeout 9999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --numbermasters 999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -r --answer >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -r --generatesyn >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -r --initsend >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -r --scanconfig=0 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --pidfile "" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -p 999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --localhost >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --httpport 999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --htmlpath "" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --updatecheck=off >/dev/null 2>/dev/null -./src/ebusd/ebusd -f -l "" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --log "all debug" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --logareas some >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --loglevel unknown >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --lograwdata >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --lograwdata=bytes >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --lograwdatafile=/xyz >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --lograwdatasize=9999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --dump >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --dumpfile "" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --dumpsize 9999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --dumpflush >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --accesslevel=inst >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --aclfile=/ >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --enablehex >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --enabledefine >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --mqttport= >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --mqttport=9999999 >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --mqttuser=username --mqttpass=password --mqttclientid=1234 --mqttport=1883 --mqtttopic=ebusd/%circuit/%name/%field --mqttretain --mqttjson --mqttverbose --mqttlog --mqttignoreinvalid --mqttchanges --mqtthost "" >/dev/null 2>/dev/null -./src/ebusd/ebusd -f --mqttca=/cafile/ --mqttcert=/cert --mqttkey=12345678 --mqttkeypass=secret --mqttinsecure >/dev/null 2>/dev/null -./src/ebusd/ebusd -c contrib/etc/ebusd -s -f --inject=stop "ff08070400/0ab5303132333431313131" >/dev/null 2>/dev/null -./src/ebusd/ebusd -c contrib/etc/ebusd -s -f --inject=stop "ff08070400" >/dev/null 2>/dev/null -./src/ebusd/ebusd -c contrib/etc/ebusd -s -f --inject=stop "ff080704/" >/dev/null 2>/dev/null +#!/bin/bash -x +prefix=. +ebusd=$prefix/src/ebusd/ebusd +ebusctl=$prefix/src/tools/ebusctl +ebuspicloader=$prefix/src/tools/ebuspicloader + +$ebusd --help >/dev/null +$ebusd -r -f -x >/dev/null 2>/dev/null +$ebusd -f -d "" >/dev/null 2>/dev/null +$ebusd -f -d "tcp:192.168.999.999:1" --log bad >/dev/null 2>/dev/null +$ebusd -f -d "enh:192.168.999.999:1" --log bad >/dev/null 2>/dev/null +$ebusd -f -d "/dev/ttyUSBx9" --log bad >/dev/null 2>/dev/null +$ebusd -f --nodevicecheck --log bad >/dev/null 2>/dev/null +$ebusd -f --readonly >/dev/null 2>/dev/null +$ebusd -f --scanconfig=full -r >/dev/null 2>/dev/null +$ebusd -f --latency 999999 >/dev/null 2>/dev/null +$ebusd -f -c "" >/dev/null 2>/dev/null +$ebusd -f -r --scanconfig=fe >/dev/null 2>/dev/null +$ebusd -f -r --configlang=en >/dev/null 2>/dev/null +$ebusd -f --pollinterval 999999 >/dev/null 2>/dev/null +$ebusd -f --inject --checkconfig=stop 01fe030400/ >/dev/null 2>/dev/null +$ebusd -f --address 999 >/dev/null 2>/dev/null +$ebusd -f --acquiretimeout 999999 >/dev/null 2>/dev/null +$ebusd -f --acquireretries 999999 >/dev/null 2>/dev/null +$ebusd -f --sendretries 999999 >/dev/null 2>/dev/null +$ebusd -f --receivetimeout 9999999 >/dev/null 2>/dev/null +$ebusd -f --numbermasters 999999 >/dev/null 2>/dev/null +$ebusd -f -r --answer >/dev/null 2>/dev/null +$ebusd -f -r --generatesyn >/dev/null 2>/dev/null +$ebusd -f -r --initsend >/dev/null 2>/dev/null +$ebusd -f -r --scanconfig=0 >/dev/null 2>/dev/null +$ebusd -f --pidfile "" >/dev/null 2>/dev/null +$ebusd -f -p 999999 >/dev/null 2>/dev/null +$ebusd -f --localhost --log bad >/dev/null 2>/dev/null +$ebusd -f --httpport 999999 >/dev/null 2>/dev/null +$ebusd -f --htmlpath "" >/dev/null 2>/dev/null +$ebusd -f --updatecheck=off --log bad >/dev/null 2>/dev/null +$ebusd -f -l "" --log bad >/dev/null 2>/dev/null +$ebusd -f --log "all debug" --log bad >/dev/null 2>/dev/null +$ebusd -f --logareas some >/dev/null 2>/dev/null +$ebusd -f --loglevel unknown >/dev/null 2>/dev/null +$ebusd -f --lograwdata --log bad >/dev/null 2>/dev/null +$ebusd -f --lograwdata=bytes --log bad >/dev/null 2>/dev/null +$ebusd -f --lograwdatafile=/notexist/xyz --log bad >/dev/null 2>/dev/null +$ebusd -f --lograwdatasize=9999999 >/dev/null 2>/dev/null +$ebusd -f --dump --log bad >/dev/null 2>/dev/null +$ebusd -f --dumpfile "" >/dev/null 2>/dev/null +$ebusd -f --dumpsize 9999999 >/dev/null 2>/dev/null +$ebusd -f --dumpflush --log bad >/dev/null 2>/dev/null +$ebusd -f --accesslevel=inst --log bad >/dev/null 2>/dev/null +$ebusd -f --aclfile=/ >/dev/null 2>/dev/null +$ebusd -f --enablehex --log bad >/dev/null 2>/dev/null +$ebusd -f --enabledefine --log bad >/dev/null 2>/dev/null +$ebusd -f --mqttport= >/dev/null 2>/dev/null +$ebusd -f --mqttport=9999999 >/dev/null 2>/dev/null +$ebusd -f --mqttuser=username --mqttpass=password --mqttclientid=1234 --mqttport=1883 --mqtttopic=ebusd/%circuit/%name/%field --mqttretain --mqttjson --mqttverbose --mqttlog --mqttignoreinvalid --mqttchanges --mqtthost "" >/dev/null 2>/dev/null +$ebusd -f --mqttca=/cafile/ --mqttcert=/cert --mqttkey=12345678 --mqttkeypass=secret --mqttinsecure --log bad >/dev/null 2>/dev/null +$ebusd -c contrib/etc/ebusd -s -f --inject=stop "ff08070400/0ab5303132333431313131" >/dev/null 2>/dev/null +$ebusd -c contrib/etc/ebusd -s -f --inject=stop "ff08070400" >/dev/null 2>/dev/null +$ebusd -c contrib/etc/ebusd -s -f --inject=stop "ff080704/" >/dev/null 2>/dev/null cat >contrib/etc/ebusd/bad.csv </dev/null +$ebusd -c contrib/etc/ebusd --checkconfig >/dev/null rm -f contrib/etc/ebusd/bad.csv -echo > dump -./src/tools/ebusctl -s testserver -p 100000 >/dev/null 2>/dev/null -./src/tools/ebusctl -s "" >/dev/null 2>/dev/null -./src/tools/ebusctl -p "" >/dev/null 2>/dev/null -./src/tools/ebusctl -x >/dev/null 2>/dev/null -./src/tools/ebusctl --help >/dev/null -./src/tools/ebusctl 'help x' >/dev/null 2>/dev/null -./src/tools/ebuspicloader --help >/dev/null -./src/tools/ebuspicloader -i "" >/dev/null 2>/dev/null -./src/tools/ebuspicloader -i x >/dev/null 2>/dev/null -./src/tools/ebuspicloader -i 192.168.0.10.1 >/dev/null 2>/dev/null -./src/tools/ebuspicloader -i 192.168.0.10 -d >/dev/null 2>/dev/null -./src/tools/ebuspicloader -m "" >/dev/null 2>/dev/null -./src/tools/ebuspicloader -m 65 >/dev/null 2>/dev/null -./src/tools/ebuspicloader -d -i 192.168.0.10 >/dev/null 2>/dev/null -./src/tools/ebuspicloader -d -m "" >/dev/null 2>/dev/null -./src/tools/ebuspicloader -d >/dev/null 2>/dev/null -./src/tools/ebuspicloader -a 9000 >/dev/null 2>/dev/null -./src/tools/ebuspicloader -a 150 -f ./src/tools/ebuspicloader -i 192.168.0.10 -m 24 -M -r -s -v /dev/zero >/dev/null 2>/dev/null -./src/tools/ebuspicloader -f x >/dev/null 2>/dev/null +$ebusctl -s testserver -p 100000 >/dev/null 2>/dev/null +$ebusctl -s "" >/dev/null 2>/dev/null +$ebusctl -p "" >/dev/null 2>/dev/null +$ebusctl -x >/dev/null 2>/dev/null +$ebusctl --help >/dev/null +$ebusctl 'help x' >/dev/null 2>/dev/null +$ebuspicloader --help >/dev/null +$ebuspicloader -i "" >/dev/null 2>/dev/null +$ebuspicloader -i x >/dev/null 2>/dev/null +$ebuspicloader -i 192.168.0.10.1 >/dev/null 2>/dev/null +$ebuspicloader -i 192.168.0.10 -d >/dev/null 2>/dev/null +$ebuspicloader -m "" >/dev/null 2>/dev/null +$ebuspicloader -m 65 >/dev/null 2>/dev/null +$ebuspicloader -d -i 192.168.0.10 >/dev/null 2>/dev/null +$ebuspicloader -d -m "" >/dev/null 2>/dev/null +$ebuspicloader -d >/dev/null 2>/dev/null +$ebuspicloader -a 9000 >/dev/null 2>/dev/null +$ebuspicloader -a 150 -f $ebuspicloader -i 192.168.0.10 -m 24 -M -r -s -v /dev/zero >/dev/null 2>/dev/null +$ebuspicloader -f x >/dev/null 2>/dev/null echo -e ':100800008431542CAE3401347E1484314E01961E52\n:00000001FF' > firmware.hex -./src/tools/ebuspicloader -f firmware.hex >/dev/null +$ebuspicloader -f firmware.hex >/dev/null #server: -php -r 'echo "php is available";'|egrep 'php is available' +php -r 'echo "php is available";'|grep 'php is available' if [ ! "$?" = 0 ]; then echo `date` "php is not available" exit 1 @@ -243,7 +247,7 @@ elif [[ -n "$1" ]]; then pid=$(ps -C ebusd -o pid=) done else - ./src/ebusd/ebusd -d tcp:127.0.0.1:8876 --initsend --latency 10 -n -c "$PWD/contrib/etc/ebusd" --pollinterval=10 -s -a 31 --acquireretries 3 --answer --generatesyn --receivetimeout 40000 --sendretries 1 --enablehex --htmlpath "$PWD/contrib/html" --httpport 8878 --pidfile "$PWD/ebusd.pid" --localhost -p 8877 -l "$PWD/ebusd.log" --logareas all --loglevel debug --lograwdata=bytes --lograwdatafile "$PWD/ebusd.raw" --lograwdatasize 1 --dumpfile "$PWD/ebusd.dump" --dumpsize 100 -D --scanconfig --aclfile="$PWD/acl.csv" --mqttport=1883 --enablehex --enabledefine + $ebusd -d tcp:127.0.0.1:8876 --initsend --latency 10 -n -c "$PWD/contrib/etc/ebusd" --pollinterval=10 -s --scanretries=0 -a 31 --acquireretries 3 --answer --generatesyn --receivetimeout 40000 --sendretries 1 --enablehex --htmlpath "$PWD/contrib/html" --httpport 8878 --pidfile "$PWD/ebusd.pid" --localhost -p 8877 -l "$PWD/ebusd.log" --logareas all --loglevel debug --lograwdata=bytes --lograwdatafile "$PWD/ebusd.raw" --lograwdatasize 1 --dumpfile "$PWD/ebusd.dump" --dumpsize 100 -D --scanconfig --aclfile="$PWD/acl.csv" --mqttport=1883 --enablehex --enabledefine --answer sleep 3 pid=`head -n 1 "$PWD/ebusd.pid"` fi @@ -332,6 +336,11 @@ a test testpass w -c mc.5 -d 53 installparam 123 hex fe070400 hex 53070400 +hex -s f0 -n fe07fe0102 +inject 3115b509030d1e00/02a85b +answer -d 50 0704 b5454850303003277201 +answer 0705 010203 +answer -s ff -m 0706 ab dump grab result grab result all @@ -352,22 +361,21 @@ i g define -r "r,cir,nam,cmt,,08,b509,,,,UCH" decode -V -N UCH 102030 -encode UCH 10;1 +encode UCH,,,,uch 10;1 raw bytes raw -reload nocommand EOF status=1 cnt=3 function send() { -# ./src/tools/ebusctl -p 8877 -t 10 "$@" +# $ebusctl -p 8877 -t 10 "$@" echo "$@" | nc -N -w 10 localhost 8877 } while [[ ! "$status" = 0 ]] && [[ $cnt -gt 0 ]]; do sleep 5 echo `date` "check signal" - send state | egrep -q "signal acquired" + send state | grep -q "signal acquired" status=$? cnt=$((cnt - 1)) done @@ -382,9 +390,9 @@ if [ "$status" != 0 ]; then fi echo `date` "got signal" sleep 2 -echo "listen"|./src/tools/ebusctl -p 8877 & +echo "listen"|nc -w 10 localhost 8877 & lstpid=$! -./src/tools/ebusctl -p 8899 >/dev/null 2>/dev/null +$ebusctl -p 8899 >/dev/null 2>/dev/null for line in "${lines[@]}"; do if [ -n "$line" ]; then echo `date` "send: $line" @@ -408,7 +416,7 @@ while [ "$status" = 0 ]; do failed=1 break fi - echo $output | egrep -q "still running" + echo $output | grep -q "still running" status=$? scancnt=$(( scancnt + 1 )) done @@ -431,9 +439,10 @@ curl -s "http://localhost:8878/data/mc.5/installparam?poll=1&user=test&secret=te curl -s -T test_coverage.sh http://localhost:8878/data/ echo `date` "commands done" kill $lstpid -verify=`send info|egrep "^address 04:"` -if [ "x$verify" != 'xaddress 04: slave #25, scanned "MF=153;ID=BBBBB;SW=3031;HW=3031"' ]; then - echo `date` "error unexpected result from info command: $verify" +verify=`send info|grep -E "^address 04:"` +expect='address 04: slave #25, scanned "MF=153;ID=BBBBB;SW=3031;HW=3031"' +if [ "x$verify" != "x$expect" ]; then + echo -e `date` "error unexpected result from info command:\n expected: >$expect<\n got: >$verify<" ls -latr kill $pid kill $srvpid