diff --git a/README.md b/README.md index 9ab015b..c1c7bfc 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway. ## Layout +This is the list of top-level directories and their purposes, +update as the project evolves: - `apps/`: standard ESP-IDF applications for each firmware role. - `apps/gateway/main/Kconfig.projbuild`: project-visible gateway-role settings such as per-channel native/serial PHY selection, gateway ids, pin mapping, and startup transport policy. - `components/`: reusable components shared by all gateway applications. @@ -17,18 +19,28 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway. - `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack, including the gateway-owned BACnet bridge model adapter. - `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications. - `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out. - - `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, W5500 SPI Ethernet startup/teardown, ESP-Touch smartconfig, setup AP mode, ESP-NOW setup ingress, and BOOT-button Wi-Fi reset for the native gateway. + - `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, W5500 SPI Ethernet startup/teardown, ESP-Touch smartconfig, setup AP mode, ESP-NOW setup ingress, setup AP GPIO handling, and optional Wi-Fi reset GPIO handling for the native gateway. - `gateway_runtime/`: persistent runtime state, command queueing, and device info services. - `gateway_485_control/`: optional 485 Lua control bridge for framed `0x28 0x01` commands and `0x22 ... checksum` notifications at `9600 8N1`; disabled by default because UART0 must be moved off the ESP-IDF console first. - `gateway_usb_setup/`: optional USB Serial/JTAG setup bridge; disabled by default so USB remains available for debug at boot. +- `knx`: The forked OpenKNX cEMI programming support, ESP-IDF port, and KNX security storage used by the gateway. You can edit this code when necessary to support missing ETS programming features or to implement the secure-session transport path. +- `knx_dali_gw`: The forked OpenKNX DALI-GW function-property support used by the gateway. You can edit this code when necessary to support missing DALI-GW features or to fix bugs. ## Current status -The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, and BOOT-button Wi-Fi credential clearing, and an optional `gateway_485_control` bridge that claims UART0 for Lua-compatible framed command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots. +The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, setup AP GPIO entry, and optional Wi-Fi credential reset GPIO handling, and an optional `gateway_485_control` bridge that claims UART0 for Lua-compatible framed command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots. ## KNX Security -KNX Data Secure and KNXnet/IP Secure support are controlled by `GATEWAY_KNX_DATA_SECURE_SUPPORTED` and `GATEWAY_KNX_IP_SECURE_SUPPORTED`. The current KNXnet/IP Secure flag reserves and reports secure service capability, while runtime secure-session transport is still reported as not implemented until that path is wired. +KNX Data Secure and KNXnet/IP Secure support are controlled by `GATEWAY_KNX_DATA_SECURE_SUPPORTED` and `GATEWAY_KNX_IP_SECURE_SUPPORTED`. The current KNXnet/IP Secure flag reserves and reports secure service capability, while runtime secure-session transport is still reported as not implemented until that path is wired. The gateway derives its KNX serial identity from the ESP base MAC, and the development factory setup key is deterministically derived from that KNX serial so the same board keeps the same FDSK across NVS erases. + +The KNXnet/IP tunnel can start from the built-in default configuration before any ETS download. KNX TP-UART is enabled only when `GATEWAY_KNX_TP_UART_PORT` is `0`, `1`, or `2`; set that UART port to `-1` for IP-only operation. UART TX/RX GPIO values of `-1` mean use the ESP-IDF default IO routing for that UART, not disabled. Non-UART GPIO options use `-1` as disabled, including the KNX programming button, KNX programming LED, setup AP button, Wi-Fi reset button, and status LED. + +When no KNX bridge config or ETS application data has been downloaded, the KNXnet/IP router starts in commissioning mode: OpenKNX receives tunnel programming traffic from ETS, while DALI group routing and REG1-Dali function-property actions stay inactive until ETS reports a configured application. + +The bridge service exposes one shared KNXnet/IP endpoint per physical gateway on the configured UDP port. Per-channel DALI/KNX bridge runtimes keep their own group-address mappings behind that endpoint, and incoming group writes are dispatched to the matching channel instead of starting one UDP socket per DALI channel. + +KNX programming mode can be controlled locally with `GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO`, and `GATEWAY_KNX_PROGRAMMING_LED_GPIO` mirrors the current programming-mode state. The setup AP entry button is configured separately with `GATEWAY_SETUP_AP_BUTTON_GPIO`; Wi-Fi credential reset remains a separate long-press function on `GATEWAY_BOOT_BUTTON_GPIO` when enabled. When `GATEWAY_KNX_SECURITY_DEV_ENDPOINTS` is enabled, the bridge HTTP action surface exposes development-only operations for reading, writing, generating, and resetting the factory setup key, exporting the factory certificate payload, and clearing local KNX security failure diagnostics. These endpoints require explicit confirmation fields in the JSON body and should stay disabled in production builds. The default development storage mode is plain NVS via `GATEWAY_KNX_SECURITY_PLAIN_NVS`; production builds should replace that with encrypted NVS, flash encryption, and secure boot before handling real commissioning keys. diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index e40baff..4dc3897 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -71,18 +71,20 @@ config GATEWAY_CHANNEL1_NATIVE_BAUDRATE config GATEWAY_CHANNEL1_SERIAL_TX_PIN int "Serial PHY TX pin" depends on GATEWAY_CHANNEL1_PHY_UART1 || GATEWAY_CHANNEL1_PHY_UART2 - range 0 48 + range -1 48 default 1 help - ESP32-S3 GPIO used by the channel 1 serial PHY transmit pin. + ESP32-S3 GPIO used by the channel 1 serial PHY transmit pin. Set to -1 + to keep the UART driver's default TX routing. config GATEWAY_CHANNEL1_SERIAL_RX_PIN int "Serial PHY RX pin" depends on GATEWAY_CHANNEL1_PHY_UART1 || GATEWAY_CHANNEL1_PHY_UART2 - range 0 48 + range -1 48 default 2 help - ESP32-S3 GPIO used by the channel 1 serial PHY receive pin. + ESP32-S3 GPIO used by the channel 1 serial PHY receive pin. Set to -1 + to keep the UART driver's default RX routing. config GATEWAY_CHANNEL1_SERIAL_BAUDRATE int "Serial PHY baudrate" @@ -178,18 +180,20 @@ config GATEWAY_CHANNEL2_NATIVE_BAUDRATE config GATEWAY_CHANNEL2_SERIAL_TX_PIN int "Serial PHY TX pin" depends on GATEWAY_CHANNEL_COUNT >= 2 && (GATEWAY_CHANNEL2_PHY_UART1 || GATEWAY_CHANNEL2_PHY_UART2) - range 0 48 + range -1 48 default 6 help - ESP32-S3 GPIO used by the channel 2 serial PHY transmit pin. + ESP32-S3 GPIO used by the channel 2 serial PHY transmit pin. Set to -1 + to keep the UART driver's default TX routing. config GATEWAY_CHANNEL2_SERIAL_RX_PIN int "Serial PHY RX pin" depends on GATEWAY_CHANNEL_COUNT >= 2 && (GATEWAY_CHANNEL2_PHY_UART1 || GATEWAY_CHANNEL2_PHY_UART2) - range 0 48 + range -1 48 default 7 help - ESP32-S3 GPIO used by the channel 2 serial PHY receive pin. + ESP32-S3 GPIO used by the channel 2 serial PHY receive pin. Set to -1 + to keep the UART driver's default RX routing. config GATEWAY_CHANNEL2_SERIAL_BAUDRATE int "Serial PHY baudrate" @@ -542,8 +546,11 @@ config GATEWAY_MODBUS_UNIT_ID config GATEWAY_MODBUS_SERIAL_UART_PORT int "Default Modbus serial UART port" depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && (GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU || GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII) - range 0 2 + range -1 2 default 1 + help + UART used by the default Modbus serial server. Set to -1 to disable the + default serial UART runtime for this function. config GATEWAY_MODBUS_ALLOW_UART0 bool "Allow Modbus/setup to claim UART0" @@ -695,23 +702,60 @@ config GATEWAY_KNX_INDIVIDUAL_ADDRESS Raw 16-bit individual address advertised to KNXnet/IP tunnel clients. The default 4353 is 1.1.1. +config GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO + int "KNX programming button GPIO" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range -1 48 + default -1 + help + GPIO used to toggle KNX programming mode. Set to -1 to disable the local + KNX programming button. + +config GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW + bool "KNX programming button is active low" + depends on GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO >= 0 + default y + +config GATEWAY_KNX_PROGRAMMING_LED_GPIO + int "KNX programming LED GPIO" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range -1 48 + default -1 + help + GPIO used to show KNX programming mode. Set to -1 to disable the local + KNX programming LED. + +config GATEWAY_KNX_PROGRAMMING_LED_ACTIVE_HIGH + bool "KNX programming LED is active high" + depends on GATEWAY_KNX_PROGRAMMING_LED_GPIO >= 0 + default y + config GATEWAY_KNX_TP_UART_PORT int "KNX TP UART port" depends on GATEWAY_KNX_BRIDGE_SUPPORTED - range 0 2 - default 1 + range -1 2 + default -1 + help + UART used by the KNX TP-UART interface. Set to -1 to disable TP-UART + while keeping KNXnet/IP tunnelling and routing available. config GATEWAY_KNX_TP_TX_PIN int "KNX TP UART TX pin" depends on GATEWAY_KNX_BRIDGE_SUPPORTED range -1 48 default -1 + help + GPIO used by the KNX TP-UART TX pin. Set to -1 to keep the UART driver's + default TX routing. config GATEWAY_KNX_TP_RX_PIN int "KNX TP UART RX pin" depends on GATEWAY_KNX_BRIDGE_SUPPORTED range -1 48 default -1 + help + GPIO used by the KNX TP-UART RX pin. Set to -1 to keep the UART driver's + default RX routing. config GATEWAY_KNX_TP_BAUDRATE int "KNX TP UART baudrate" @@ -723,7 +767,7 @@ config GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE int "KNX/IP bridge task stack bytes" depends on GATEWAY_KNX_BRIDGE_SUPPORTED range 6144 24576 - default 8192 + default 12288 config GATEWAY_BRIDGE_KNX_TASK_PRIORITY int "KNX/IP bridge task priority" @@ -916,23 +960,44 @@ config GATEWAY_STATUS_LED_ACTIVE_HIGH default y config GATEWAY_BOOT_BUTTON_GPIO - int "BOOT button GPIO" + int "Wi-Fi reset button GPIO" range -1 48 - default 0 + default -1 help - GPIO used for Lua-compatible setup entry and Wi-Fi credential clearing. Set to -1 to disable. + GPIO used for long-press Wi-Fi credential clearing. Set to -1 to disable. config GATEWAY_BOOT_BUTTON_ACTIVE_LOW - bool "BOOT button is active low" + bool "Wi-Fi reset button is active low" depends on GATEWAY_BOOT_BUTTON_GPIO >= 0 default y config GATEWAY_BOOT_BUTTON_LONG_PRESS_MS - int "BOOT button long press ms" + int "Wi-Fi reset button long press ms" depends on GATEWAY_BOOT_BUTTON_GPIO >= 0 range 500 10000 default 3000 +config GATEWAY_SETUP_AP_BUTTON_GPIO + int "Setup AP button GPIO" + range -1 48 + default 0 + help + GPIO used for entering setup AP mode. Set to -1 to disable local setup + AP entry by GPIO. + +config GATEWAY_SETUP_AP_BUTTON_ACTIVE_LOW + bool "Setup AP button is active low" + depends on GATEWAY_SETUP_AP_BUTTON_GPIO >= 0 + default y + +config GATEWAY_BUTTON_TASK_STACK_SIZE + int "Gateway button task stack bytes" + depends on GATEWAY_BOOT_BUTTON_GPIO >= 0 || GATEWAY_SETUP_AP_BUTTON_GPIO >= 0 + range 3072 12288 + default 8192 + help + Stack used by the GPIO button task and one-shot setup AP task. + endmenu endmenu \ No newline at end of file diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 4b580b7..5ae5502 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -40,6 +40,21 @@ #define CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS 3000 #endif +#ifndef CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO +#ifdef CONFIG_GATEWAY_BOOT_BUTTON_GPIO +#define CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO CONFIG_GATEWAY_BOOT_BUTTON_GPIO +#ifdef CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW +#define CONFIG_GATEWAY_SETUP_AP_BUTTON_ACTIVE_LOW 1 +#endif +#else +#define CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO -1 +#endif +#endif + +#ifndef CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE +#define CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE 8192 +#endif + #ifndef CONFIG_GATEWAY_USB_SETUP_CHANNEL_INDEX #define CONFIG_GATEWAY_USB_SETUP_CHANNEL_INDEX 0 #endif @@ -193,7 +208,7 @@ #endif #ifndef CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE -#define CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE 8192 +#define CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE 12288 #endif #ifndef CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY @@ -216,8 +231,16 @@ #define CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS 4353 #endif +#ifndef CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO +#define CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO -1 +#endif + +#ifndef CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO +#define CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO -1 +#endif + #ifndef CONFIG_GATEWAY_KNX_TP_UART_PORT -#define CONFIG_GATEWAY_KNX_TP_UART_PORT 1 +#define CONFIG_GATEWAY_KNX_TP_UART_PORT -1 #endif #ifndef CONFIG_GATEWAY_KNX_TP_TX_PIN @@ -579,21 +602,22 @@ bool ValidateChannelBindings() { if (kKnxBridgeSupported) { const int knx_uart = CONFIG_GATEWAY_KNX_TP_UART_PORT; - if (k485ControlEnabled && knx_uart == 0) { + if (knx_uart >= 0 && k485ControlEnabled && knx_uart == 0) { ESP_LOGE(kTag, "KNX TP UART0 conflicts with the UART0 control bridge"); return false; } - if (knx_uart == 0 && kConsoleOnUart0) { + if (knx_uart >= 0 && knx_uart == 0 && kConsoleOnUart0) { ESP_LOGE(kTag, "KNX TP-UART on UART0 requires moving the ESP-IDF console off UART0"); return false; } - if (kModbusBridgeSupported && kModbusDefaultSerialTransport && + if (knx_uart >= 0 && kModbusBridgeSupported && kModbusDefaultSerialTransport && knx_uart == CONFIG_GATEWAY_MODBUS_SERIAL_UART_PORT) { ESP_LOGE(kTag, "KNX TP UART%d conflicts with default Modbus serial UART", knx_uart); return false; } for (int i = 0; i < CONFIG_GATEWAY_CHANNEL_COUNT; ++i) { - if (channels[i].enabled && channels[i].serial_phy && channels[i].uart_port == knx_uart) { + if (knx_uart >= 0 && channels[i].enabled && channels[i].serial_phy && + channels[i].uart_port == knx_uart) { ESP_LOGE(kTag, "KNX TP UART%d conflicts with DALI channel %d serial PHY", knx_uart, i + 1); return false; @@ -840,6 +864,18 @@ extern "C" void app_main(void) { default_knx.multicast_address = CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS; default_knx.individual_address = static_cast(CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS); + default_knx.programming_button_gpio = CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO; + default_knx.programming_led_gpio = CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO; + #ifdef CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW + default_knx.programming_button_active_low = true; + #else + default_knx.programming_button_active_low = false; + #endif + #ifdef CONFIG_GATEWAY_KNX_PROGRAMMING_LED_ACTIVE_HIGH + default_knx.programming_led_active_high = true; + #else + default_knx.programming_led_active_high = false; + #endif default_knx.tp_uart.uart_port = CONFIG_GATEWAY_KNX_TP_UART_PORT; default_knx.tp_uart.tx_pin = CONFIG_GATEWAY_KNX_TP_TX_PIN; default_knx.tp_uart.rx_pin = CONFIG_GATEWAY_KNX_TP_RX_PIN; @@ -898,7 +934,10 @@ extern "C" void app_main(void) { static_cast(CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE); network_config.status_led_gpio = CONFIG_GATEWAY_STATUS_LED_GPIO; network_config.boot_button_gpio = CONFIG_GATEWAY_BOOT_BUTTON_GPIO; + network_config.setup_ap_button_gpio = CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO; network_config.boot_button_long_press_ms = CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS; + network_config.boot_button_task_stack_size = + static_cast(CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE); #ifdef CONFIG_GATEWAY_STATUS_LED_ACTIVE_HIGH network_config.status_led_active_high = true; #else @@ -908,6 +947,11 @@ extern "C" void app_main(void) { network_config.boot_button_active_low = true; #else network_config.boot_button_active_low = false; + #endif + #ifdef CONFIG_GATEWAY_SETUP_AP_BUTTON_ACTIVE_LOW + network_config.setup_ap_button_active_low = true; + #else + network_config.setup_ap_button_active_low = false; #endif s_network = std::make_unique(*s_controller, *s_runtime, *s_dali_domain, network_config, diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 0470b89..5d72a29 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -596,7 +596,7 @@ CONFIG_PARTITION_TABLE_MD5=y # # Gateway App # -CONFIG_GATEWAY_CHANNEL_COUNT=2 +CONFIG_GATEWAY_CHANNEL_COUNT=1 # # Gateway Channel 1 @@ -617,17 +617,6 @@ CONFIG_GATEWAY_CHANNEL1_SERIAL_QUERY_TIMEOUT_MS=100 # # Gateway Channel 2 # -CONFIG_GATEWAY_CHANNEL2_GW_ID=4 -# CONFIG_GATEWAY_CHANNEL2_PHY_DISABLED is not set -# CONFIG_GATEWAY_CHANNEL2_PHY_NATIVE is not set -# CONFIG_GATEWAY_CHANNEL2_PHY_UART1 is not set -CONFIG_GATEWAY_CHANNEL2_PHY_UART2=y -CONFIG_GATEWAY_CHANNEL2_SERIAL_TX_PIN=6 -CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_PIN=7 -CONFIG_GATEWAY_CHANNEL2_SERIAL_BAUDRATE=9600 -CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_BUFFER=512 -CONFIG_GATEWAY_CHANNEL2_SERIAL_TX_BUFFER=512 -CONFIG_GATEWAY_CHANNEL2_SERIAL_QUERY_TIMEOUT_MS=100 # end of Gateway Channel 2 # @@ -673,7 +662,7 @@ CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS=0 CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=40 CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=-1 CONFIG_GATEWAY_ETHERNET_PHY_ADDR=1 -CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=3072 +CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=4096 # end of Gateway Wired Ethernet CONFIG_GATEWAY_BRIDGE_SUPPORTED=y @@ -698,11 +687,15 @@ CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y CONFIG_GATEWAY_KNX_UDP_PORT=3671 CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12" CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=4353 +CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO=0 +CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW=y +CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO=10 +# CONFIG_GATEWAY_KNX_PROGRAMMING_LED_ACTIVE_HIGH is not set CONFIG_GATEWAY_KNX_TP_UART_PORT=0 CONFIG_GATEWAY_KNX_TP_TX_PIN=-1 CONFIG_GATEWAY_KNX_TP_RX_PIN=-1 CONFIG_GATEWAY_KNX_TP_BAUDRATE=19200 -CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=8192 +CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=12288 CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5 CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y # CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set @@ -726,6 +719,8 @@ CONFIG_GATEWAY_STATUS_LED_GPIO=-1 CONFIG_GATEWAY_BOOT_BUTTON_GPIO=0 CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW=y CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS=3000 +CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO=-1 +CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE=8192 # end of Gateway Network Services # end of Gateway App @@ -2184,8 +2179,7 @@ CONFIG_LWIP_GARP_TMR_INTERVAL=60 CONFIG_LWIP_ESP_MLDV6_REPORT=y CONFIG_LWIP_MLDV6_TMR_INTERVAL=40 CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 -CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y -# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set +CONFIG_LWIP_DHCP_DOES_ACD_CHECK=y # CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set # CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y @@ -2204,13 +2198,16 @@ CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y CONFIG_LWIP_DHCPS_ADD_DNS=y # end of DHCP server -# CONFIG_LWIP_AUTOIP is not set +CONFIG_LWIP_AUTOIP=y +CONFIG_LWIP_AUTOIP_TRIES=2 +CONFIG_LWIP_AUTOIP_MAX_CONFLICTS=9 +CONFIG_LWIP_AUTOIP_RATE_LIMIT_INTERVAL=20 CONFIG_LWIP_IPV4=y CONFIG_LWIP_IPV6=y # CONFIG_LWIP_IPV6_AUTOCONFIG is not set CONFIG_LWIP_IPV6_NUM_ADDRESSES=3 # CONFIG_LWIP_IPV6_FORWARD is not set -# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set +CONFIG_LWIP_NETIF_STATUS_CALLBACK=y CONFIG_LWIP_NETIF_LOOPBACK=y CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 diff --git a/apps/gateway/sdkconfig.old b/apps/gateway/sdkconfig.old index 789fb78..10d5050 100644 --- a/apps/gateway/sdkconfig.old +++ b/apps/gateway/sdkconfig.old @@ -663,17 +663,17 @@ CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y # # Gateway Wired Ethernet # -CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST=1 -CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO=14 -CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO=13 -CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=12 -CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=15 -CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=4 +CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST=2 +CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO=48 +CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO=47 +CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=33 +CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=34 +CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=36 CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS=0 -CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=36 -CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=5 +CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=40 +CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=-1 CONFIG_GATEWAY_ETHERNET_PHY_ADDR=1 -CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=3072 +CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=4096 # end of Gateway Wired Ethernet CONFIG_GATEWAY_BRIDGE_SUPPORTED=y @@ -698,6 +698,10 @@ CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y CONFIG_GATEWAY_KNX_UDP_PORT=3671 CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12" CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=4353 +CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO=0 +CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW=y +CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO=10 +# CONFIG_GATEWAY_KNX_PROGRAMMING_LED_ACTIVE_HIGH is not set CONFIG_GATEWAY_KNX_TP_UART_PORT=0 CONFIG_GATEWAY_KNX_TP_TX_PIN=-1 CONFIG_GATEWAY_KNX_TP_RX_PIN=-1 @@ -726,6 +730,8 @@ CONFIG_GATEWAY_STATUS_LED_GPIO=-1 CONFIG_GATEWAY_BOOT_BUTTON_GPIO=0 CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW=y CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS=3000 +CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO=-1 +CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE=8192 # end of Gateway Network Services # end of Gateway App @@ -1075,12 +1081,12 @@ CONFIG_BT_CTRL_RX_ANTENNA_INDEX_EFF=0 # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N0 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P3 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P6 is not set -CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9=y +# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P12 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P15 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P18 is not set -# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P20 is not set -CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF=11 +CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P20=y +CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF=15 CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100 CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 @@ -1093,7 +1099,253 @@ CONFIG_BT_CTRL_SCAN_DUPL_CACHE_SIZE=100 CONFIG_BT_CTRL_DUPL_SCAN_CACHE_REFRESH_PERIOD=0 # CONFIG_BT_CTRL_BLE_MESH_SCAN_DUPL_EN is not set # CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_EN is not set -# CONFIG_IDF_EXPERIMENTAL_FEATURES is not set +CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_DIS=y +CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_EFF=0 + +# +# MODEM SLEEP Options +# +# CONFIG_BT_CTRL_MODEM_SLEEP is not set +# end of MODEM SLEEP Options + +CONFIG_BT_CTRL_SLEEP_MODE_EFF=0 +CONFIG_BT_CTRL_SLEEP_CLOCK_EFF=0 +CONFIG_BT_CTRL_HCI_TL_EFF=1 +# CONFIG_BT_CTRL_AGC_RECORRECT_EN is not set +# CONFIG_BT_CTRL_SCAN_BACKOFF_UPPERLIMITMAX is not set +# CONFIG_BT_BLE_ADV_DATA_LENGTH_ZERO_AUX is not set +CONFIG_BT_CTRL_CHAN_ASS_EN=y +CONFIG_BT_CTRL_LE_PING_EN=y + +# +# BLE disconnects when Instant Passed (0x28) occurs +# +# CONFIG_BT_CTRL_BLE_LLCP_CONN_UPDATE is not set +# CONFIG_BT_CTRL_BLE_LLCP_CHAN_MAP_UPDATE is not set +# CONFIG_BT_CTRL_BLE_LLCP_PHY_UPDATE is not set +# end of BLE disconnects when Instant Passed (0x28) occurs + +# CONFIG_BT_CTRL_RUN_IN_FLASH_ONLY is not set +CONFIG_BT_CTRL_DTM_ENABLE=y +CONFIG_BT_CTRL_BLE_MASTER=y +# CONFIG_BT_CTRL_BLE_TEST is not set +CONFIG_BT_CTRL_BLE_SCAN=y +CONFIG_BT_CTRL_BLE_SECURITY_ENABLE=y +CONFIG_BT_CTRL_BLE_ADV=y +# CONFIG_BT_CTRL_CHECK_CONNECT_IND_ACCESS_ADDRESS is not set + +# +# Controller debug log Options (Experimental) +# +# end of Controller debug log Options (Experimental) +# end of Controller Options + +# +# Common Options +# +CONFIG_BT_ALARM_MAX_NUM=50 +CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT=y +# CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS is not set + +# +# BLE Log +# +# CONFIG_BLE_LOG_ENABLED is not set +# end of BLE Log + +# CONFIG_BT_BLE_LOG_SPI_OUT_ENABLED is not set +# CONFIG_BT_BLE_LOG_UHCI_OUT_ENABLED is not set +# CONFIG_BT_LE_USED_MEM_STATISTICS_ENABLED is not set +# end of Common Options + +# CONFIG_BT_HCI_LOG_DEBUG_EN is not set +# end of Bluetooth + +# CONFIG_BLE_MESH is not set + +# +# Console Library +# +# CONFIG_CONSOLE_SORTED_HELP is not set +# end of Console Library + +# +# Driver Configurations +# + +# +# Legacy TWAI Driver Configurations +# +# CONFIG_TWAI_SKIP_LEGACY_CONFLICT_CHECK is not set +CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM=y +# end of Legacy TWAI Driver Configurations + +# +# Legacy ADC Driver Configuration +# +# CONFIG_ADC_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_ADC_SKIP_LEGACY_CONFLICT_CHECK is not set + +# +# Legacy ADC Calibration Configuration +# +# CONFIG_ADC_CALI_SUPPRESS_DEPRECATE_WARN is not set +# end of Legacy ADC Calibration Configuration +# end of Legacy ADC Driver Configuration + +# +# Legacy MCPWM Driver Configurations +# +# CONFIG_MCPWM_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_MCPWM_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy MCPWM Driver Configurations + +# +# Legacy Timer Group Driver Configurations +# +# CONFIG_GPTIMER_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_GPTIMER_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy Timer Group Driver Configurations + +# +# Legacy RMT Driver Configurations +# +# CONFIG_RMT_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_RMT_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy RMT Driver Configurations + +# +# Legacy I2S Driver Configurations +# +# CONFIG_I2S_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_I2S_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy I2S Driver Configurations + +# +# Legacy I2C Driver Configurations +# +# CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy I2C Driver Configurations + +# +# Legacy PCNT Driver Configurations +# +# CONFIG_PCNT_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_PCNT_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy PCNT Driver Configurations + +# +# Legacy SDM Driver Configurations +# +# CONFIG_SDM_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_SDM_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy SDM Driver Configurations + +# +# Legacy Temperature Sensor Driver Configurations +# +# CONFIG_TEMP_SENSOR_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_TEMP_SENSOR_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy Temperature Sensor Driver Configurations + +# +# Legacy Touch Sensor Driver Configurations +# +# CONFIG_TOUCH_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_TOUCH_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy Touch Sensor Driver Configurations +# end of Driver Configurations + +# +# eFuse Bit Manager +# +# CONFIG_EFUSE_CUSTOM_TABLE is not set +# CONFIG_EFUSE_VIRTUAL is not set +CONFIG_EFUSE_MAX_BLK_LEN=256 +# end of eFuse Bit Manager + +# +# ESP-TLS +# +CONFIG_ESP_TLS_USING_MBEDTLS=y +# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set +CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y +# CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS is not set +# CONFIG_ESP_TLS_SERVER_SESSION_TICKETS is not set +# CONFIG_ESP_TLS_SERVER_CERT_SELECT_HOOK is not set +# CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set +# CONFIG_ESP_TLS_PSK_VERIFICATION is not set +# CONFIG_ESP_TLS_INSECURE is not set +CONFIG_ESP_TLS_DYN_BUF_STRATEGY_SUPPORTED=y +# end of ESP-TLS + +# +# ADC and ADC Calibration +# +# CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM is not set +# CONFIG_ADC_CONTINUOUS_ISR_IRAM_SAFE is not set +# CONFIG_ADC_CONTINUOUS_FORCE_USE_ADC2_ON_C3_S3 is not set +# CONFIG_ADC_ENABLE_DEBUG_LOG is not set +# end of ADC and ADC Calibration + +# +# Wireless Coexistence +# +CONFIG_ESP_COEX_ENABLED=y +CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y +# CONFIG_ESP_COEX_POWER_MANAGEMENT is not set +# CONFIG_ESP_COEX_GPIO_DEBUG is not set +# end of Wireless Coexistence + +# +# Common ESP-related +# +CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +# end of Common ESP-related + +# +# ESP-Driver:Camera Controller Configurations +# +# CONFIG_CAM_CTLR_DVP_CAM_ISR_CACHE_SAFE is not set +# end of ESP-Driver:Camera Controller Configurations + +# +# ESP-Driver:GPIO Configurations +# +# CONFIG_GPIO_CTRL_FUNC_IN_IRAM is not set +# end of ESP-Driver:GPIO Configurations + +# +# ESP-Driver:GPTimer Configurations +# +CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM=y +# CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM is not set +# CONFIG_GPTIMER_ISR_CACHE_SAFE is not set +CONFIG_GPTIMER_OBJ_CACHE_SAFE=y +# CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:GPTimer Configurations + +# +# ESP-Driver:I2C Configurations +# +# CONFIG_I2C_ISR_IRAM_SAFE is not set +# CONFIG_I2C_ENABLE_DEBUG_LOG is not set +# CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2 is not set +CONFIG_I2C_MASTER_ISR_HANDLER_IN_IRAM=y +# end of ESP-Driver:I2C Configurations + +# +# ESP-Driver:I2S Configurations +# +# CONFIG_I2S_ISR_IRAM_SAFE is not set +# CONFIG_I2S_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:I2S Configurations + +# +# ESP-Driver:LEDC Configurations +# +# CONFIG_LEDC_CTRL_FUNC_IN_IRAM is not set +# end of ESP-Driver:LEDC Configurations # # ESP-Driver:MCPWM Configurations @@ -1938,8 +2190,7 @@ CONFIG_LWIP_GARP_TMR_INTERVAL=60 CONFIG_LWIP_ESP_MLDV6_REPORT=y CONFIG_LWIP_MLDV6_TMR_INTERVAL=40 CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 -CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y -# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set +CONFIG_LWIP_DHCP_DOES_ACD_CHECK=y # CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set # CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y @@ -1958,13 +2209,16 @@ CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y CONFIG_LWIP_DHCPS_ADD_DNS=y # end of DHCP server -# CONFIG_LWIP_AUTOIP is not set +CONFIG_LWIP_AUTOIP=y +CONFIG_LWIP_AUTOIP_TRIES=2 +CONFIG_LWIP_AUTOIP_MAX_CONFLICTS=9 +CONFIG_LWIP_AUTOIP_RATE_LIMIT_INTERVAL=20 CONFIG_LWIP_IPV4=y CONFIG_LWIP_IPV6=y # CONFIG_LWIP_IPV6_AUTOCONFIG is not set CONFIG_LWIP_IPV6_NUM_ADDRESSES=3 # CONFIG_LWIP_IPV6_FORWARD is not set -# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set +CONFIG_LWIP_NETIF_STATUS_CALLBACK=y CONFIG_LWIP_NETIF_LOOPBACK=y CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 @@ -2565,249 +2819,3 @@ CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y # end of Component config # CONFIG_IDF_EXPERIMENTAL_FEATURES is not set - -# Deprecated options for backward compatibility -# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set -# CONFIG_NO_BLOBS is not set -# CONFIG_APP_ROLLBACK_ENABLE is not set -# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set -# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set -# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set -CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y -# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set -# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set -CONFIG_LOG_BOOTLOADER_LEVEL=3 -# CONFIG_FLASH_ENCRYPTION_ENABLED is not set -# CONFIG_FLASHMODE_QIO is not set -# CONFIG_FLASHMODE_QOUT is not set -CONFIG_FLASHMODE_DIO=y -# CONFIG_FLASHMODE_DOUT is not set -CONFIG_MONITOR_BAUD=115200 -CONFIG_OPTIMIZATION_LEVEL_DEBUG=y -CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y -CONFIG_COMPILER_OPTIMIZATION_DEFAULT=y -# CONFIG_OPTIMIZATION_LEVEL_RELEASE is not set -# CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set -CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y -# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set -# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set -CONFIG_OPTIMIZATION_ASSERTION_LEVEL=2 -# CONFIG_CXX_EXCEPTIONS is not set -CONFIG_STACK_CHECK_NONE=y -# CONFIG_STACK_CHECK_NORM is not set -# CONFIG_STACK_CHECK_STRONG is not set -# CONFIG_STACK_CHECK_ALL is not set -# CONFIG_WARN_WRITE_STRINGS is not set -# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set -CONFIG_ESP32_APPTRACE_DEST_NONE=y -CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y -# CONFIG_BLUEDROID_ENABLED is not set -CONFIG_NIMBLE_ENABLED=y -CONFIG_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y -# CONFIG_NIMBLE_MEM_ALLOC_MODE_EXTERNAL is not set -# CONFIG_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set -CONFIG_NIMBLE_PINNED_TO_CORE=0 -CONFIG_NIMBLE_PINNED_TO_CORE_0=y -# CONFIG_NIMBLE_PINNED_TO_CORE_1 is not set -CONFIG_NIMBLE_TASK_STACK_SIZE=4096 -CONFIG_BT_NIMBLE_TASK_STACK_SIZE=4096 -CONFIG_NIMBLE_ROLE_CENTRAL=y -CONFIG_NIMBLE_ROLE_PERIPHERAL=y -CONFIG_NIMBLE_ROLE_BROADCASTER=y -CONFIG_NIMBLE_ROLE_OBSERVER=y -CONFIG_NIMBLE_SM_LEGACY=y -CONFIG_NIMBLE_SM_SC=y -# CONFIG_NIMBLE_SM_SC_DEBUG_KEYS is not set -CONFIG_BT_NIMBLE_SM_SC_LVL=0 -# CONFIG_NIMBLE_NVS_PERSIST is not set -CONFIG_NIMBLE_MAX_BONDS=3 -CONFIG_NIMBLE_RPA_TIMEOUT=900 -CONFIG_NIMBLE_MAX_CONNECTIONS=3 -CONFIG_NIMBLE_MAX_CCCDS=8 -CONFIG_NIMBLE_CRYPTO_STACK_MBEDTLS=y -# CONFIG_NIMBLE_HS_FLOW_CTRL is not set -CONFIG_NIMBLE_ATT_PREFERRED_MTU=256 -CONFIG_NIMBLE_L2CAP_COC_MAX_NUM=0 -CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT=12 -CONFIG_BT_NIMBLE_ACL_BUF_COUNT=24 -CONFIG_BT_NIMBLE_ACL_BUF_SIZE=255 -CONFIG_BT_NIMBLE_HCI_EVT_BUF_SIZE=70 -CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=30 -CONFIG_BT_NIMBLE_HCI_EVT_LO_BUF_COUNT=8 -CONFIG_NIMBLE_SVC_GAP_DEVICE_NAME="nimble" -CONFIG_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=31 -CONFIG_NIMBLE_SVC_GAP_APPEARANCE=0 -# CONFIG_NIMBLE_MESH is not set -# CONFIG_NIMBLE_DEBUG is not set -# CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_EN is not set -CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS=y -CONFIG_SW_COEXIST_ENABLE=y -CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y -CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y -# CONFIG_CAM_CTLR_DVP_CAM_ISR_IRAM_SAFE is not set -# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set -# CONFIG_MCPWM_ISR_IRAM_SAFE is not set -# CONFIG_EVENT_LOOP_PROFILING is not set -CONFIG_POST_EVENTS_FROM_ISR=y -CONFIG_POST_EVENTS_FROM_IRAM_ISR=y -CONFIG_GDBSTUB_SUPPORT_TASKS=y -CONFIG_GDBSTUB_MAX_TASKS=32 -# CONFIG_OTA_ALLOW_HTTP is not set -CONFIG_ESP32S3_DEEP_SLEEP_WAKEUP_DELAY=2000 -CONFIG_ESP_SLEEP_DEEP_SLEEP_WAKEUP_DELAY=2000 -CONFIG_ESP32S3_RTC_CLK_SRC_INT_RC=y -# CONFIG_ESP32S3_RTC_CLK_SRC_EXT_CRYS is not set -# CONFIG_ESP32S3_RTC_CLK_SRC_EXT_OSC is not set -# CONFIG_ESP32S3_RTC_CLK_SRC_INT_8MD256 is not set -CONFIG_ESP32S3_RTC_CLK_CAL_CYCLES=1024 -CONFIG_PERIPH_CTRL_FUNC_IN_IRAM=y -CONFIG_BROWNOUT_DET=y -CONFIG_ESP32S3_BROWNOUT_DET=y -CONFIG_BROWNOUT_DET_LVL_SEL_7=y -CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_7=y -# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_6 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_5 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_4 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_3 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_2 is not set -# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set -# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_1 is not set -CONFIG_BROWNOUT_DET_LVL=7 -CONFIG_ESP32S3_BROWNOUT_DET_LVL=7 -CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y -CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y -# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set -CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 -CONFIG_ESP32_PHY_MAX_TX_POWER=20 -# CONFIG_REDUCE_PHY_TX_POWER is not set -# CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set -CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y -CONFIG_PM_POWER_DOWN_TAGMEM_IN_LIGHT_SLEEP=y -CONFIG_ESP32S3_SPIRAM_SUPPORT=y -CONFIG_DEFAULT_PSRAM_CLK_IO=30 -CONFIG_DEFAULT_PSRAM_CS_IO=26 -# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set -CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160=y -# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set -CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=160 -CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 -CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 -CONFIG_MAIN_TASK_STACK_SIZE=3584 -# CONFIG_CONSOLE_UART_DEFAULT is not set -# CONFIG_CONSOLE_UART_CUSTOM is not set -# CONFIG_CONSOLE_UART_NONE is not set -# CONFIG_ESP_CONSOLE_UART_NONE is not set -CONFIG_CONSOLE_UART_NUM=-1 -CONFIG_INT_WDT=y -CONFIG_INT_WDT_TIMEOUT_MS=300 -CONFIG_INT_WDT_CHECK_CPU1=y -CONFIG_TASK_WDT=y -CONFIG_ESP_TASK_WDT=y -# CONFIG_TASK_WDT_PANIC is not set -CONFIG_TASK_WDT_TIMEOUT_S=5 -CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y -CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y -# CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set -CONFIG_ESP32S3_DEBUG_OCDAWARE=y -CONFIG_IPC_TASK_STACK_SIZE=1280 -CONFIG_TIMER_TASK_STACK_SIZE=3584 -CONFIG_ESP32_WIFI_ENABLED=y -CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 -CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 -# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set -CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y -CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 -CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 -# CONFIG_ESP32_WIFI_CSI_ENABLED is not set -CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y -CONFIG_ESP32_WIFI_TX_BA_WIN=6 -CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y -CONFIG_ESP32_WIFI_RX_BA_WIN=6 -CONFIG_ESP32_WIFI_NVS_ENABLED=y -CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y -# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set -CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 -CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 -CONFIG_ESP32_WIFI_IRAM_OPT=y -CONFIG_ESP32_WIFI_RX_IRAM_OPT=y -CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y -CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y -CONFIG_WPA_MBEDTLS_CRYPTO=y -CONFIG_WPA_MBEDTLS_TLS_CLIENT=y -# CONFIG_WPA_WAPI_PSK is not set -# CONFIG_WPA_SUITE_B_192 is not set -# CONFIG_WPA_11KV_SUPPORT is not set -# CONFIG_WPA_MBO_SUPPORT is not set -# CONFIG_WPA_DPP_SUPPORT is not set -# CONFIG_WPA_11R_SUPPORT is not set -# CONFIG_WPA_WPS_SOFTAP_REGISTRAR is not set -# CONFIG_WPA_WPS_STRICT is not set -# CONFIG_WPA_DEBUG_PRINT is not set -# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set -# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set -CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y -CONFIG_TIMER_TASK_PRIORITY=1 -CONFIG_TIMER_TASK_STACK_DEPTH=2048 -CONFIG_TIMER_QUEUE_LENGTH=10 -# CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set -CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY=y -# CONFIG_HAL_ASSERTION_SILIENT is not set -# CONFIG_L2_TO_L3_COPY is not set -CONFIG_ESP_GRATUITOUS_ARP=y -CONFIG_GARP_TMR_INTERVAL=60 -CONFIG_TCPIP_RECVMBOX_SIZE=32 -CONFIG_TCP_MAXRTX=12 -CONFIG_TCP_SYNMAXRTX=12 -CONFIG_TCP_MSS=1440 -CONFIG_TCP_MSL=60000 -CONFIG_TCP_SND_BUF_DEFAULT=5760 -CONFIG_TCP_WND_DEFAULT=5760 -CONFIG_TCP_RECVMBOX_SIZE=6 -CONFIG_TCP_QUEUE_OOSEQ=y -CONFIG_TCP_OVERSIZE_MSS=y -# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set -# CONFIG_TCP_OVERSIZE_DISABLE is not set -CONFIG_UDP_RECVMBOX_SIZE=6 -CONFIG_TCPIP_TASK_STACK_SIZE=3072 -CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y -# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set -# CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set -CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF -# CONFIG_PPP_SUPPORT is not set -CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y -# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set -# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set -# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set -# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set -CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y -# CONFIG_NEWLIB_NANO_FORMAT is not set -CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y -CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC_SYSTIMER=y -CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC_FRC1=y -# CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC is not set -# CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC is not set -# CONFIG_NEWLIB_TIME_SYSCALL_USE_HRT is not set -# CONFIG_ESP32S3_TIME_SYSCALL_USE_SYSTIMER is not set -# CONFIG_ESP32S3_TIME_SYSCALL_USE_FRC1 is not set -# CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set -# CONFIG_ESP32S3_TIME_SYSCALL_USE_NONE is not set -CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 -CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 -CONFIG_ESP32_PTHREAD_STACK_MIN=768 -CONFIG_ESP32_DEFAULT_PTHREAD_CORE_NO_AFFINITY=y -# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_0 is not set -# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_1 is not set -CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1 -CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" -CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y -# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set -# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set -CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y -CONFIG_SUPPORT_TERMIOS=y -CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 -# End of deprecated options diff --git a/components/dali_domain/src/dali_domain.cpp b/components/dali_domain/src/dali_domain.cpp index 660e4f6..0da37de 100644 --- a/components/dali_domain/src/dali_domain.cpp +++ b/components/dali_domain/src/dali_domain.cpp @@ -290,8 +290,9 @@ esp_err_t DaliDomainService::bindSerialBus(const DaliSerialBusConfig& config) { ESP_LOGE(kTag, "failed to configure uart%d: %s", config.uart_port, esp_err_to_name(err)); return err; } - err = uart_set_pin(uart, config.tx_pin, config.rx_pin, UART_PIN_NO_CHANGE, - UART_PIN_NO_CHANGE); + err = uart_set_pin(uart, config.tx_pin < 0 ? UART_PIN_NO_CHANGE : config.tx_pin, + config.rx_pin < 0 ? UART_PIN_NO_CHANGE : config.rx_pin, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to set uart%d pins tx=%d rx=%d: %s", config.uart_port, config.tx_pin, config.rx_pin, esp_err_to_name(err)); diff --git a/components/gateway_bridge/include/gateway_bridge.hpp b/components/gateway_bridge/include/gateway_bridge.hpp index d559f96..1008b20 100644 --- a/components/gateway_bridge/include/gateway_bridge.hpp +++ b/components/gateway_bridge/include/gateway_bridge.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -37,7 +38,7 @@ struct GatewayBridgeServiceConfig { std::vector reserved_uart_ports; uint32_t bacnet_task_stack_size{8192}; UBaseType_t bacnet_task_priority{5}; - uint32_t knx_task_stack_size{8192}; + uint32_t knx_task_stack_size{12288}; UBaseType_t knx_task_priority{5}; std::optional default_knx_config; }; @@ -66,6 +67,14 @@ class GatewayBridgeService { ChannelRuntime* findRuntime(uint8_t gateway_id); const ChannelRuntime* findRuntime(uint8_t gateway_id) const; + ChannelRuntime* selectKnxEndpointRuntime(); + bool isKnxEndpointRuntime(const ChannelRuntime* runtime) const; + esp_err_t startKnxEndpoint(ChannelRuntime* requested_runtime, + std::set* used_uarts = nullptr); + esp_err_t stopKnxEndpoint(ChannelRuntime* requested_runtime); + DaliBridgeResult routeKnxCemiFrame(const uint8_t* data, size_t len); + DaliBridgeResult routeKnxGroupWrite(uint16_t group_address, const uint8_t* data, + size_t len); void handleDaliRawFrame(const DaliRawFrame& frame); void collectUsedRuntimeResources(uint8_t except_gateway_id, std::set* modbus_tcp_ports, @@ -76,6 +85,7 @@ class GatewayBridgeService { GatewayCache& cache_; GatewayBridgeServiceConfig config_; std::vector> runtimes_; + ChannelRuntime* knx_endpoint_runtime_{nullptr}; }; } // namespace gateway diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 401c4ec..6c3df31 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -15,7 +15,6 @@ #include "gateway_knx.hpp" #include "gateway_modbus.hpp" #include "gateway_provisioning.hpp" -#include "openknx_idf/ets_memory_loader.h" #include "openknx_idf/security_storage.h" #include "cJSON.h" @@ -1296,12 +1295,20 @@ uart_stop_bits_t UartStopBits(int bits) { } // namespace struct GatewayBridgeService::ChannelRuntime { - explicit ChannelRuntime(DaliDomainService& domain, GatewayCache& cache, DaliChannelInfo channel, + explicit ChannelRuntime(GatewayBridgeService& service, DaliDomainService& domain, + GatewayCache& cache, DaliChannelInfo channel, GatewayBridgeServiceConfig service_config) - : domain(domain), cache(cache), channel(std::move(channel)), service_config(service_config), + : service(service), + domain(domain), + cache(cache), + channel(std::move(channel)), + service_config(service_config), lock(xSemaphoreCreateRecursiveMutex()) {} ~ChannelRuntime() { + if (knx_router != nullptr) { + knx_router->stop(); + } if (cloud != nullptr) { cloud->stop(); } @@ -1311,6 +1318,7 @@ struct GatewayBridgeService::ChannelRuntime { } } + GatewayBridgeService& service; DaliDomainService& domain; GatewayCache& cache; DaliChannelInfo channel; @@ -1427,18 +1435,17 @@ struct GatewayBridgeService::ChannelRuntime { knx = std::make_unique(*engine); knx_router = std::make_unique( *knx, [this](const uint8_t* data, size_t len) { - LockGuard guard(lock); - if (knx == nullptr) { - DaliBridgeResult result; - result.error = "KNX bridge is not ready"; - return result; - } - return knx->handleCemiFrame(data, len); + return service.routeKnxCemiFrame(data, len); }, openKnxNamespace()); - if (knx_config.has_value()) { - knx->setConfig(knx_config.value()); - knx_router->setConfig(knx_config.value()); + knx_router->setGroupWriteHandler( + [this](uint16_t group_address, const uint8_t* data, size_t len) { + return service.routeKnxGroupWrite(group_address, data, len); + }); + if (const auto active_knx = activeKnxConfigLocked(); active_knx.has_value()) { + knx->setConfig(active_knx.value()); + knx_router->setConfig(active_knx.value()); + knx_router->setCommissioningOnly(!knx_config.has_value()); } #if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED) @@ -1459,33 +1466,8 @@ struct GatewayBridgeService::ChannelRuntime { } void refreshOpenKnxEtsAssociationsLocked() { - if (!service_config.knx_enabled) { - return; - } - const auto active_config = activeKnxConfigLocked(); - if (!active_config.has_value()) { - return; - } - const auto snapshot = openknx::LoadEtsMemorySnapshot(openKnxNamespace()); - const bool has_downloaded_address = snapshot.individual_address != 0 && - snapshot.individual_address != 0xffff; - if (!snapshot.configured && !has_downloaded_address && snapshot.associations.empty()) { - return; - } - GatewayKnxConfig updated = active_config.value(); - if (has_downloaded_address) { - updated.individual_address = snapshot.individual_address; - } - updated.ets_associations.clear(); - updated.ets_associations.reserve(snapshot.associations.size()); - for (const auto& association : snapshot.associations) { - updated.ets_associations.push_back(GatewayKnxEtsAssociation{ - association.group_address, association.group_object_number}); - } - knx_config = std::move(updated); - ESP_LOGI(kTag, "gateway=%u loaded OpenKNX ETS address=0x%04x associations=%u from NVS namespace %s", - channel.gateway_id, snapshot.individual_address, - static_cast(snapshot.associations.size()), openKnxNamespace().c_str()); + // Bau07B0/OpenKNX memory restore is stack-heavy and owns TP-UART internals; + // the live KNX task restores and syncs ETS associations after startup. } std::optional diagnosticSnapshotLocked(int short_address, @@ -2056,25 +2038,41 @@ struct GatewayBridgeService::ChannelRuntime { knx_last_error = validation_error; return validation_err; } - if (config->ip_router_enabled && used_ports != nullptr) { - if (used_ports->find(config->udp_port) != used_ports->end()) { - knx_last_error = "duplicate KNXnet/IP UDP port " + std::to_string(config->udp_port); + GatewayKnxConfig runtime_config = config.value(); + if (runtime_config.ip_router_enabled && used_ports != nullptr) { + if (used_ports->find(runtime_config.udp_port) != used_ports->end()) { + knx_last_error = "KNXnet/IP UDP port " + std::to_string(runtime_config.udp_port) + + " is already owned by another runtime"; + ESP_LOGW(kTag, "gateway=%u skips duplicate KNXnet/IP UDP port %u", + channel.gateway_id, static_cast(runtime_config.udp_port)); return ESP_ERR_INVALID_STATE; } - used_ports->insert(config->udp_port); + used_ports->insert(runtime_config.udp_port); } - if (config->ip_router_enabled && used_uarts != nullptr) { - const int uart_port = config->tp_uart.uart_port; + if (GatewayKnxConfigUsesTpUart(runtime_config) && used_uarts != nullptr) { + const int uart_port = runtime_config.tp_uart.uart_port; if (used_uarts->find(uart_port) != used_uarts->end()) { - knx_last_error = "KNX TP-UART UART" + std::to_string(uart_port) + - " is already used by another runtime"; - return ESP_ERR_INVALID_STATE; + ESP_LOGW(kTag, + "gateway=%u KNX TP-UART UART%d is already owned by another runtime; " + "starting this KNX/IP runtime without opening a second UART driver", + channel.gateway_id, uart_port); + runtime_config.tp_uart.uart_port = -1; } - used_uarts->insert(uart_port); } - knx->setConfig(config.value()); - knx_router->setConfig(config.value()); - if (!config->ip_router_enabled) { + const bool commissioning_only = !knx_config.has_value(); + ESP_LOGI(kTag, + "gateway=%u KNX/IP start config namespace=%s storedConfig=%d udp=%u tunnel=%d " + "multicast=%d multicastGroup=%s mainGroup=%u tpUart=%d tx=%d rx=%d individual=0x%04x", + channel.gateway_id, openKnxNamespace().c_str(), !commissioning_only, + static_cast(runtime_config.udp_port), runtime_config.tunnel_enabled, + runtime_config.multicast_enabled, runtime_config.multicast_address.c_str(), + static_cast(runtime_config.main_group), runtime_config.tp_uart.uart_port, + runtime_config.tp_uart.tx_pin, runtime_config.tp_uart.rx_pin, + runtime_config.individual_address); + knx->setConfig(runtime_config); + knx_router->setConfig(runtime_config); + knx_router->setCommissioningOnly(commissioning_only); + if (!runtime_config.ip_router_enabled) { knx_started = false; return ESP_ERR_NOT_SUPPORTED; } @@ -2084,8 +2082,18 @@ struct GatewayBridgeService::ChannelRuntime { knx_started = err == ESP_OK; if (err != ESP_OK) { knx_last_error = knx_router->lastError().empty() - ? "failed to start KNX TP-UART router" + ? "failed to start KNXnet/IP router" : knx_router->lastError(); + ESP_LOGW(kTag, "gateway=%u KNX/IP start failed err=%s(%d) detail=%s", + channel.gateway_id, esp_err_to_name(err), static_cast(err), + knx_last_error.c_str()); + } else { + if (knx_router->tpUartOnline() && used_uarts != nullptr) { + used_uarts->insert(runtime_config.tp_uart.uart_port); + } + ESP_LOGI(kTag, "gateway=%u KNX/IP started namespace=%s udp=%u detail=%s", + channel.gateway_id, openKnxNamespace().c_str(), + static_cast(runtime_config.udp_port), knx_router->lastError().c_str()); } return err; } @@ -3014,12 +3022,15 @@ struct GatewayBridgeService::ChannelRuntime { const std::optional& candidate_knx, std::string* error_message = nullptr) const { const int uart_port = config.serial.uart_port; - if (uart_port < 0 || uart_port > 2) { + if (uart_port < -1 || uart_port > 2) { if (error_message != nullptr) { - *error_message = "Modbus serial UART port must be 0, 1, or 2"; + *error_message = "Modbus serial UART port must be -1, 0, 1, or 2"; } return ESP_ERR_INVALID_ARG; } + if (uart_port < 0) { + return ESP_OK; + } if (uart_port == 0 && !service_config.allow_modbus_uart0) { if (error_message != nullptr) { *error_message = @@ -3035,7 +3046,8 @@ struct GatewayBridgeService::ChannelRuntime { return ESP_ERR_INVALID_STATE; } if (service_config.knx_enabled && candidate_knx.has_value() && - candidate_knx->ip_router_enabled && candidate_knx->tp_uart.uart_port == uart_port) { + GatewayKnxConfigUsesTpUart(candidate_knx.value()) && + candidate_knx->tp_uart.uart_port == uart_port) { if (error_message != nullptr) { *error_message = "Modbus serial UART" + std::to_string(uart_port) + " conflicts with KNX TP-UART; choose another free UART for RS485"; @@ -3048,16 +3060,16 @@ struct GatewayBridgeService::ChannelRuntime { esp_err_t validateKnxConfigLocked(const GatewayKnxConfig& config, const std::optional& candidate_modbus, std::string* error_message = nullptr) const { - if (!config.ip_router_enabled) { - return ESP_OK; - } - const int uart_port = config.tp_uart.uart_port; - if (uart_port < 0 || uart_port > 2) { + if (config.tp_uart.uart_port < -1 || config.tp_uart.uart_port > 2) { if (error_message != nullptr) { - *error_message = "KNX TP-UART port must be 0, 1, or 2"; + *error_message = "KNX TP-UART port must be -1, 0, 1, or 2"; } return ESP_ERR_INVALID_ARG; } + if (!config.ip_router_enabled || !GatewayKnxConfigUsesTpUart(config)) { + return ESP_OK; + } + const int uart_port = config.tp_uart.uart_port; if (uart_port == 0 && !service_config.allow_knx_uart0) { if (error_message != nullptr) { *error_message = @@ -3144,7 +3156,18 @@ struct GatewayBridgeService::ChannelRuntime { if (knx_config.has_value()) { return knx_config; } - return service_config.default_knx_config; + if (!service_config.default_knx_config.has_value()) { + return std::nullopt; + } + GatewayKnxConfig config = service_config.default_knx_config.value(); + const uint8_t channel_index = channel.channel_index; + config.main_group = static_cast(std::min(31, config.main_group + channel_index)); + const uint16_t device = config.individual_address & 0x00ff; + if (device > 0 && device + channel_index <= 0x00ff) { + config.individual_address = static_cast((config.individual_address & 0xff00) | + (device + channel_index)); + } + return config; } esp_err_t saveKnxConfig(const GatewayKnxConfig& config, @@ -3165,12 +3188,7 @@ struct GatewayBridgeService::ChannelRuntime { return validation_err; } const bool restart_router = knx_started || (knx_router != nullptr && knx_router->started()); - if (restart_router && merged_config.ip_router_enabled && used_ports != nullptr && - used_ports->find(merged_config.udp_port) != used_ports->end()) { - knx_last_error = "duplicate KNXnet/IP UDP port " + std::to_string(merged_config.udp_port); - return ESP_ERR_INVALID_STATE; - } - if (restart_router && merged_config.ip_router_enabled && used_uarts != nullptr && + if (restart_router && GatewayKnxConfigUsesTpUart(merged_config) && used_uarts != nullptr && used_uarts->find(merged_config.tp_uart.uart_port) != used_uarts->end()) { knx_last_error = "KNX TP-UART UART" + std::to_string(merged_config.tp_uart.uart_port) + " is already used by another runtime"; @@ -3195,6 +3213,7 @@ struct GatewayBridgeService::ChannelRuntime { } if (knx_router != nullptr) { knx_router->setConfig(merged_config); + knx_router->setCommissioningOnly(false); } if (restart_router) { return startKnx(used_ports, used_uarts); @@ -3350,6 +3369,10 @@ struct GatewayBridgeService::ChannelRuntime { return ESP_ERR_NOT_FOUND; } if (GatewayModbusTransportIsSerial(config->transport)) { + if (config->serial.uart_port < 0) { + modbus_last_error = "Modbus serial UART disabled"; + return ESP_ERR_NOT_SUPPORTED; + } std::string validation_error; const esp_err_t serial_err = validateSerialModbusConfigLocked( config.value(), activeKnxConfigLocked(), &validation_error); @@ -3532,8 +3555,10 @@ struct GatewayBridgeService::ChannelRuntime { } const int rts_pin = config.serial.rs485.enabled ? config.serial.rs485.de_pin : UART_PIN_NO_CHANGE; - err = uart_set_pin(uart_port, config.serial.tx_pin, config.serial.rx_pin, rts_pin, - UART_PIN_NO_CHANGE); + err = uart_set_pin(uart_port, + config.serial.tx_pin < 0 ? UART_PIN_NO_CHANGE : config.serial.tx_pin, + config.serial.rx_pin < 0 ? UART_PIN_NO_CHANGE : config.serial.rx_pin, + rts_pin < 0 ? UART_PIN_NO_CHANGE : rts_pin, UART_PIN_NO_CHANGE); if (err != ESP_OK) { return err; } @@ -3794,7 +3819,7 @@ esp_err_t GatewayBridgeService::start() { const auto channels = dali_domain_.channelInfo(); runtimes_.reserve(channels.size()); for (const auto& channel : channels) { - auto runtime = std::make_unique(dali_domain_, cache_, channel, config_); + auto runtime = std::make_unique(*this, dali_domain_, cache_, channel, config_); const esp_err_t err = runtime->start(); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to start bridge runtime gateway=%u: %s", channel.gateway_id, @@ -3820,13 +3845,18 @@ esp_err_t GatewayBridgeService::start() { } if (config_.knx_enabled && config_.knx_startup_enabled) { - std::set used_knx_ports; - for (const auto& runtime : runtimes_) { - const esp_err_t err = runtime->startKnx(&used_knx_ports, &used_serial_uarts); - if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) { - ESP_LOGW(kTag, "gateway=%u KNX/IP startup skipped: %s", runtime->channel.gateway_id, - esp_err_to_name(err)); - } + ChannelRuntime* owner = selectKnxEndpointRuntime(); + const esp_err_t err = startKnxEndpoint(owner, &used_serial_uarts); + if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) { + const char* detail = owner == nullptr + ? "no KNX endpoint owner" + : (!owner->knx_last_error.empty() + ? owner->knx_last_error.c_str() + : (owner->knx_router == nullptr + ? "router unavailable" + : owner->knx_router->lastError().c_str())); + ESP_LOGW(kTag, "KNX/IP startup skipped: %s(%d) detail=%s", + esp_err_to_name(err), static_cast(err), detail); } } @@ -3863,20 +3893,170 @@ const GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime( return nullptr; } +GatewayBridgeService::ChannelRuntime* GatewayBridgeService::selectKnxEndpointRuntime() { + auto eligible = [](ChannelRuntime* runtime) { + if (runtime == nullptr) { + return false; + } + LockGuard guard(runtime->lock); + const auto config = runtime->activeKnxConfigLocked(); + return config.has_value() && config->ip_router_enabled; + }; + + if (eligible(knx_endpoint_runtime_)) { + return knx_endpoint_runtime_; + } + + ChannelRuntime* selected = nullptr; + for (const auto& runtime : runtimes_) { + if (!eligible(runtime.get())) { + continue; + } + if (selected == nullptr || runtime->channel.channel_index < selected->channel.channel_index) { + selected = runtime.get(); + } + } + knx_endpoint_runtime_ = selected; + if (selected != nullptr) { + ESP_LOGI(kTag, "gateway=%u owns shared KNXnet/IP endpoint", selected->channel.gateway_id); + } + return selected; +} + +bool GatewayBridgeService::isKnxEndpointRuntime(const ChannelRuntime* runtime) const { + return runtime != nullptr && runtime == knx_endpoint_runtime_; +} + +esp_err_t GatewayBridgeService::startKnxEndpoint(ChannelRuntime* requested_runtime, + std::set* used_uarts) { + if (!config_.knx_enabled) { + return ESP_ERR_NOT_SUPPORTED; + } + ChannelRuntime* owner = selectKnxEndpointRuntime(); + if (owner == nullptr) { + return ESP_ERR_NOT_FOUND; + } + if (requested_runtime != nullptr && requested_runtime != owner) { + ESP_LOGI(kTag, "gateway=%u requested KNX start; shared endpoint remains owned by gateway=%u", + requested_runtime->channel.gateway_id, owner->channel.gateway_id); + } + + if (used_uarts != nullptr) { + LockGuard guard(owner->lock); + const auto owner_config = owner->activeKnxConfigLocked(); + if (owner_config.has_value() && GatewayKnxConfigUsesTpUart(owner_config.value())) { + used_uarts->erase(owner_config->tp_uart.uart_port); + } + } + + std::set used_knx_ports; + for (const auto& runtime : runtimes_) { + if (runtime.get() == owner) { + continue; + } + LockGuard guard(runtime->lock); + if (runtime->knx_started || + (runtime->knx_router != nullptr && runtime->knx_router->started())) { + const auto config = runtime->activeKnxConfigLocked(); + if (config.has_value()) { + used_knx_ports.insert(config->udp_port); + } + } + } + return owner->startKnx(&used_knx_ports, used_uarts); +} + +esp_err_t GatewayBridgeService::stopKnxEndpoint(ChannelRuntime* requested_runtime) { + ChannelRuntime* owner = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_ + : selectKnxEndpointRuntime(); + if (owner == nullptr) { + return ESP_ERR_NOT_FOUND; + } + if (requested_runtime != nullptr && requested_runtime != owner) { + ESP_LOGI(kTag, "gateway=%u requested KNX stop; stopping shared endpoint owned by gateway=%u", + requested_runtime->channel.gateway_id, owner->channel.gateway_id); + } + return owner->stopKnx(); +} + +DaliBridgeResult GatewayBridgeService::routeKnxCemiFrame(const uint8_t* data, size_t len) { + std::vector matches; + for (const auto& runtime : runtimes_) { + LockGuard guard(runtime->lock); + if (runtime->knx != nullptr && runtime->knx->matchesCemiFrame(data, len)) { + matches.push_back(runtime.get()); + } + } + if (matches.empty()) { + DaliBridgeResult result; + result.error = "No DALI bridge mapping matched KNX cEMI group write"; + return result; + } + if (matches.size() > 1) { + DaliBridgeResult result; + result.error = "KNX cEMI group write matched multiple DALI bridge channels"; + ESP_LOGW(kTag, "%s", result.error.c_str()); + return result; + } + + ChannelRuntime* runtime = matches.front(); + LockGuard guard(runtime->lock); + if (runtime->knx == nullptr || !runtime->knx->matchesCemiFrame(data, len)) { + DaliBridgeResult result; + result.error = "DALI bridge mapping changed before KNX cEMI dispatch"; + return result; + } + return runtime->knx->handleCemiFrame(data, len); +} + +DaliBridgeResult GatewayBridgeService::routeKnxGroupWrite(uint16_t group_address, + const uint8_t* data, size_t len) { + std::vector matches; + for (const auto& runtime : runtimes_) { + LockGuard guard(runtime->lock); + if (runtime->knx != nullptr && runtime->knx->matchesGroupAddress(group_address)) { + matches.push_back(runtime.get()); + } + } + if (matches.empty()) { + DaliBridgeResult result; + result.error = "No DALI bridge mapping matched KNX group " + + GatewayKnxGroupAddressString(group_address); + return result; + } + if (matches.size() > 1) { + DaliBridgeResult result; + result.error = "KNX group " + GatewayKnxGroupAddressString(group_address) + + " matched multiple DALI bridge channels"; + ESP_LOGW(kTag, "%s", result.error.c_str()); + return result; + } + + ChannelRuntime* runtime = matches.front(); + LockGuard guard(runtime->lock); + if (runtime->knx == nullptr || !runtime->knx->matchesGroupAddress(group_address)) { + DaliBridgeResult result; + result.error = "DALI bridge mapping changed before KNX group dispatch"; + return result; + } + return runtime->knx->handleGroupWrite(group_address, data, len); +} + void GatewayBridgeService::handleDaliRawFrame(const DaliRawFrame& frame) { const auto update = DecodeDaliKnxStatusUpdate(frame); if (!update.has_value()) { return; } - auto* runtime = findRuntime(frame.gateway_id); - if (runtime == nullptr) { + auto* owner = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_ + : selectKnxEndpointRuntime(); + if (owner == nullptr || owner->channel.gateway_id != frame.gateway_id) { return; } - LockGuard guard(runtime->lock); - if (!runtime->knx_started || runtime->knx_router == nullptr) { + LockGuard guard(owner->lock); + if (!owner->knx_started || owner->knx_router == nullptr) { return; } - runtime->knx_router->publishDaliStatus(update->target, update->actual_level); + owner->knx_router->publishDaliStatus(update->target, update->actual_level); } void GatewayBridgeService::collectUsedRuntimeResources( @@ -3909,7 +4089,8 @@ void GatewayBridgeService::collectUsedRuntimeResources( if (knx_udp_ports != nullptr) { knx_udp_ports->insert(knx_config->udp_port); } - if (serial_uarts != nullptr) { + if (serial_uarts != nullptr && runtime->knx_router != nullptr && + runtime->knx_router->tpUartOnline()) { serial_uarts->insert(knx_config->tp_uart.uart_port); } } @@ -4214,20 +4395,21 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost( return handleGet("knx", gateway_id.value()); } if (action == "knx_start") { - std::set used_knx_ports; + ChannelRuntime* owner = selectKnxEndpointRuntime(); std::set used_serial_uarts; - collectUsedRuntimeResources(gateway_id.value(), nullptr, &used_knx_ports, - &used_serial_uarts); - const esp_err_t err = runtime->startKnx(&used_knx_ports, &used_serial_uarts); + collectUsedRuntimeResources(owner == nullptr ? gateway_id.value() : owner->channel.gateway_id, + nullptr, nullptr, &used_serial_uarts); + const esp_err_t err = startKnxEndpoint(runtime, &used_serial_uarts); if (err != ESP_OK) { - return ErrorResponse(err, runtime->knx_last_error.empty() + auto* owner = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_ : runtime; + return ErrorResponse(err, owner->knx_last_error.empty() ? "failed to start KNX/IP bridge" - : runtime->knx_last_error.c_str()); + : owner->knx_last_error.c_str()); } return handleGet("knx", gateway_id.value()); } if (action == "knx_stop") { - const esp_err_t err = runtime->stopKnx(); + const esp_err_t err = stopKnxEndpoint(runtime); if (err != ESP_OK) { return ErrorResponse(err, "failed to stop KNX/IP bridge"); } diff --git a/components/gateway_knx/CMakeLists.txt b/components/gateway_knx/CMakeLists.txt index a33ad38..975de09 100644 --- a/components/gateway_knx/CMakeLists.txt +++ b/components/gateway_knx/CMakeLists.txt @@ -1,7 +1,7 @@ idf_component_register( SRCS "src/gateway_knx.cpp" INCLUDE_DIRS "include" - REQUIRES dali_cpp esp_driver_uart freertos log lwip openknx_idf + REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip openknx_idf ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index d515ac8..f091f76 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -10,12 +10,14 @@ #include "lwip/sockets.h" #include +#include #include #include #include #include #include #include +#include #include #include @@ -62,6 +64,10 @@ struct GatewayKnxConfig { uint16_t udp_port{kGatewayKnxDefaultUdpPort}; std::string multicast_address{kGatewayKnxDefaultMulticastAddress}; uint16_t individual_address{0x1101}; + int programming_button_gpio{-1}; + bool programming_button_active_low{true}; + int programming_led_gpio{-1}; + bool programming_led_active_high{true}; std::vector ets_associations; GatewayKnxTpUartConfig tp_uart; }; @@ -111,6 +117,7 @@ struct GatewayKnxCommissioningBallast { std::optional GatewayKnxConfigFromValue(const DaliValue* value); DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config); +bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config); const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode); GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value); @@ -132,6 +139,8 @@ class GatewayKnxBridge { size_t etsBindingCount() const; std::vector describeDaliBindings() const; + bool matchesCemiFrame(const uint8_t* data, size_t len) const; + bool matchesGroupAddress(uint16_t group_address) const; DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len); DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data, size_t len); @@ -186,13 +195,19 @@ class GatewayKnxBridge { class GatewayKnxTpIpRouter { public: using CemiFrameHandler = std::function; + using GroupWriteHandler = std::function; GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler, std::string openknx_namespace = "openknx"); ~GatewayKnxTpIpRouter(); void setConfig(const GatewayKnxConfig& config); + void setCommissioningOnly(bool enabled); + void setGroupWriteHandler(GroupWriteHandler handler); const GatewayKnxConfig& config() const; + bool tpUartOnline() const; esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority); esp_err_t stop(); @@ -201,17 +216,40 @@ class GatewayKnxTpIpRouter { bool publishDaliStatus(const GatewayKnxDaliTarget& target, uint8_t actual_level); private: + static constexpr size_t kMaxTunnelClients = 16; + + struct TunnelClient { + bool connected{false}; + uint8_t channel_id{0}; + uint8_t connection_type{0}; + uint8_t received_sequence{255}; + uint8_t send_sequence{0}; + uint16_t individual_address{0}; + TickType_t last_activity_tick{0}; + ::sockaddr_in control_remote{}; + ::sockaddr_in data_remote{}; + }; + static void TaskEntry(void* arg); + esp_err_t initializeRuntime(); void taskLoop(); void finishTask(); void closeSockets(); bool configureSocket(); bool configureTpUart(); bool initializeTpUart(); + bool configureProgrammingGpio(); + void refreshNetworkInterfaces(bool force_log = false); void handleUdpDatagram(const uint8_t* data, size_t len, const ::sockaddr_in& remote); + void handleSearchRequest(uint16_t service, const uint8_t* body, size_t len, + const ::sockaddr_in& remote); + void handleDescriptionRequest(const uint8_t* body, size_t len, + const ::sockaddr_in& remote); void handleRoutingIndication(const uint8_t* body, size_t len); void handleTunnellingRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote); + void handleDeviceConfigurationRequest(const uint8_t* body, size_t len, + const ::sockaddr_in& remote); void handleConnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote); void handleConnectionStateRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote); @@ -220,47 +258,85 @@ class GatewayKnxTpIpRouter { const ::sockaddr_in& remote); void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status, const ::sockaddr_in& remote); + void sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence, uint8_t status, + const ::sockaddr_in& remote); + void sendConnectionHeaderAck(uint16_t service, uint8_t channel_id, uint8_t sequence, + uint8_t status, const ::sockaddr_in& remote); void sendSecureSessionStatus(uint8_t status, const ::sockaddr_in& remote); void sendTunnelIndication(const uint8_t* data, size_t len); + void sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, size_t len); void sendConnectionStateResponse(uint8_t channel_id, uint8_t status, const ::sockaddr_in& remote); void sendDisconnectResponse(uint8_t channel_id, uint8_t status, const ::sockaddr_in& remote); void sendConnectResponse(uint8_t channel_id, uint8_t status, - const ::sockaddr_in& remote); + const ::sockaddr_in& remote, uint8_t connection_type, + uint16_t tunnel_address); void sendRoutingIndication(const uint8_t* data, size_t len); - bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len); + void sendSearchResponse(uint16_t service, const ::sockaddr_in& remote, + const std::set& requested_dibs = {}); + void sendDescriptionResponse(const ::sockaddr_in& remote); + std::array localHpaiForRemote(const ::sockaddr_in& remote) const; + std::vector buildDeviceInfoDib(const ::sockaddr_in& remote) const; + std::vector buildSupportedServiceDib() const; + std::vector buildExtendedDeviceInfoDib() const; + std::vector buildIpConfigDib(const ::sockaddr_in& remote, bool current) const; + std::vector buildKnxAddressesDib() const; + std::vector buildTunnelingInfoDib() const; + TunnelClient* findTunnelClient(uint8_t channel_id); + const TunnelClient* findTunnelClient(uint8_t channel_id) const; + TunnelClient* allocateTunnelClient(const ::sockaddr_in& control_remote, + const ::sockaddr_in& data_remote, + uint8_t connection_type); + void resetTunnelClient(TunnelClient& client); + uint8_t nextTunnelChannelId() const; + uint16_t effectiveTunnelAddressForSlot(size_t slot) const; + void pruneStaleTunnelClients(); + bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len, + TunnelClient* response_client); bool handleOpenKnxBusFrame(const uint8_t* data, size_t len); + void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote); bool emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len); + bool shouldRouteDaliApplicationFrames() const; void syncOpenKnxConfigFromDevice(); uint16_t effectiveIndividualAddress() const; uint16_t effectiveTunnelAddress() const; void pollTpUart(); + void pollProgrammingButton(); + void updateProgrammingLed(); + void setProgrammingLed(bool on); void handleTpUartControlByte(uint8_t byte); void handleTpTelegram(const uint8_t* data, size_t len); void forwardCemiToTp(const uint8_t* data, size_t len); GatewayKnxBridge& bridge_; CemiFrameHandler handler_; + GroupWriteHandler group_write_handler_; std::string openknx_namespace_; GatewayKnxConfig config_; std::unique_ptr ets_device_; TaskHandle_t task_handle_{nullptr}; SemaphoreHandle_t openknx_lock_{nullptr}; + SemaphoreHandle_t startup_semaphore_{nullptr}; + esp_err_t startup_result_{ESP_OK}; std::atomic_bool stop_requested_{false}; std::atomic_bool started_{false}; int udp_sock_{-1}; int tp_uart_port_{-1}; - uint8_t tunnel_channel_id_{1}; - uint8_t expected_tunnel_sequence_{0}; - uint8_t tunnel_send_sequence_{0}; - bool tunnel_connected_{false}; - ::sockaddr_in tunnel_remote_{}; + std::vector multicast_joined_interfaces_; + TickType_t network_refresh_tick_{0}; + std::array tunnel_clients_{}; + uint8_t last_tunnel_channel_id_{0}; std::vector tp_rx_frame_; std::vector tp_last_sent_telegram_; TickType_t tp_uart_last_byte_tick_{0}; bool tp_uart_extended_frame_{false}; bool tp_uart_online_{false}; + bool commissioning_only_{false}; + std::atomic_bool openknx_configured_{false}; + bool programming_button_last_pressed_{false}; + bool programming_led_state_{false}; + TickType_t programming_button_last_toggle_tick_{0}; std::string last_error_; }; diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 9603e92..e311127 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -1,7 +1,10 @@ #include "gateway_knx.hpp" #include "dali_define.hpp" +#include "driver/gpio.h" #include "driver/uart.h" +#include "esp_mac.h" +#include "esp_netif.h" #include "esp_log.h" #include "lwip/inet.h" #include "lwip/sockets.h" @@ -12,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -26,12 +30,20 @@ constexpr const char* kTag = "gateway_knx"; constexpr uint8_t kCemiLDataReq = 0x11; constexpr uint8_t kCemiLDataInd = 0x29; constexpr uint8_t kCemiLDataCon = 0x2e; +constexpr uint16_t kServiceSearchRequest = 0x0201; +constexpr uint16_t kServiceSearchResponse = 0x0202; +constexpr uint16_t kServiceDescriptionRequest = 0x0203; +constexpr uint16_t kServiceDescriptionResponse = 0x0204; constexpr uint16_t kServiceConnectRequest = 0x0205; constexpr uint16_t kServiceConnectResponse = 0x0206; constexpr uint16_t kServiceConnectionStateRequest = 0x0207; constexpr uint16_t kServiceConnectionStateResponse = 0x0208; constexpr uint16_t kServiceDisconnectRequest = 0x0209; constexpr uint16_t kServiceDisconnectResponse = 0x020a; +constexpr uint16_t kServiceSearchRequestExt = 0x020b; +constexpr uint16_t kServiceSearchResponseExt = 0x020c; +constexpr uint16_t kServiceDeviceConfigurationRequest = 0x0310; +constexpr uint16_t kServiceDeviceConfigurationAck = 0x0311; constexpr uint16_t kServiceTunnellingRequest = 0x0420; constexpr uint16_t kServiceTunnellingAck = 0x0421; constexpr uint16_t kServiceRoutingIndication = 0x0530; @@ -47,11 +59,31 @@ constexpr uint8_t kKnxNoError = 0x00; constexpr uint8_t kKnxErrorConnectionId = 0x21; constexpr uint8_t kKnxErrorConnectionType = 0x22; constexpr uint8_t kKnxErrorNoMoreConnections = 0x24; +constexpr uint8_t kKnxErrorTunnellingLayer = 0x29; constexpr uint8_t kKnxErrorSequenceNumber = 0x04; constexpr uint8_t kKnxSecureStatusAuthFailed = 0x01; constexpr uint8_t kKnxSecureStatusUnauthenticated = 0x02; +constexpr uint8_t kKnxConnectionTypeDeviceManagement = 0x03; constexpr uint8_t kKnxConnectionTypeTunnel = 0x04; constexpr uint8_t kKnxTunnelLayerLink = 0x02; +constexpr uint8_t kKnxHpaiIpv4Udp = 0x01; +constexpr uint8_t kKnxDibDeviceInfo = 0x01; +constexpr uint8_t kKnxDibSupportedServices = 0x02; +constexpr uint8_t kKnxDibIpConfig = 0x03; +constexpr uint8_t kKnxDibCurrentIpConfig = 0x04; +constexpr uint8_t kKnxDibKnxAddresses = 0x05; +constexpr uint8_t kKnxDibTunnellingInfo = 0x07; +constexpr uint8_t kKnxDibExtendedDeviceInfo = 0x08; +constexpr uint8_t kKnxMediumTp1 = 0x02; +constexpr uint8_t kKnxMediumIp = 0x20; +constexpr uint8_t kKnxServiceFamilyCore = 0x02; +constexpr uint8_t kKnxServiceFamilyDeviceManagement = 0x03; +constexpr uint8_t kKnxServiceFamilyTunnelling = 0x04; +constexpr uint8_t kKnxServiceFamilyRouting = 0x05; +constexpr uint16_t kKnxManufacturerId = 0x00a4; +constexpr uint16_t kKnxDeviceDescriptor = 0x07b0; +constexpr uint8_t kKnxIpAssignmentManual = 0x01; +constexpr uint8_t kKnxIpCapabilityManual = 0x01; constexpr uint8_t kTpUartResetRequest = 0x01; constexpr uint8_t kTpUartResetIndication = 0x03; constexpr uint8_t kTpUartStateRequest = 0x02; @@ -94,6 +126,14 @@ struct DecodedGroupWrite { std::vector data; }; +struct KnxNetifInfo { + const char* key{nullptr}; + esp_netif_t* netif{nullptr}; + uint32_t address{0}; + uint32_t netmask{0}; + uint32_t gateway{0}; +}; + class SemaphoreGuard { public: explicit SemaphoreGuard(SemaphoreHandle_t semaphore) : semaphore_(semaphore) { @@ -123,11 +163,139 @@ uint16_t ReadBe16(const uint8_t* data) { return static_cast((static_cast(data[0]) << 8) | data[1]); } +std::string EspErrDetail(const std::string& message, esp_err_t err) { + return std::string(message) + ": " + esp_err_to_name(err) + "(" + std::to_string(err) + ")"; +} + +std::string ErrnoDetail(const std::string& message, int err) { + return std::string(message) + ": errno=" + std::to_string(err) + " (" + std::strerror(err) + ")"; +} + +std::string Ipv4String(uint32_t network_address) { + const uint32_t address = ntohl(network_address); + char buffer[16]{}; + std::snprintf(buffer, sizeof(buffer), "%u.%u.%u.%u", + static_cast((address >> 24) & 0xff), + static_cast((address >> 16) & 0xff), + static_cast((address >> 8) & 0xff), + static_cast(address & 0xff)); + return buffer; +} + +std::string EndpointString(const sockaddr_in& endpoint) { + return Ipv4String(endpoint.sin_addr.s_addr) + ":" + std::to_string(ntohs(endpoint.sin_port)); +} + +bool EndpointEquals(const sockaddr_in& lhs, const sockaddr_in& rhs) { + return lhs.sin_family == rhs.sin_family && lhs.sin_addr.s_addr == rhs.sin_addr.s_addr && + lhs.sin_port == rhs.sin_port; +} + +void WriteIp(uint8_t* data, uint32_t network_address) { + const uint32_t address = ntohl(network_address); + data[0] = static_cast((address >> 24) & 0xff); + data[1] = static_cast((address >> 16) & 0xff); + data[2] = static_cast((address >> 8) & 0xff); + data[3] = static_cast(address & 0xff); +} + +bool ReadBaseMac(uint8_t* data) { + if (data == nullptr) { + return false; + } + if (esp_efuse_mac_get_default(data) == ESP_OK) { + return true; + } + return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK; +} + +std::vector ActiveKnxNetifs() { + std::vector out; + constexpr std::array kIfKeys{"ETH_DEF", "WIFI_STA_DEF", "WIFI_AP_DEF"}; + for (const char* key : kIfKeys) { + esp_netif_t* netif = esp_netif_get_handle_from_ifkey(key); + if (netif == nullptr || !esp_netif_is_netif_up(netif)) { + continue; + } + esp_netif_ip_info_t ip_info{}; + if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK || ip_info.ip.addr == 0) { + continue; + } + out.push_back(KnxNetifInfo{key, netif, ip_info.ip.addr, ip_info.netmask.addr, + ip_info.gw.addr}); + } + return out; +} + +std::optional SelectKnxNetifForRemote(const sockaddr_in& remote) { + const auto netifs = ActiveKnxNetifs(); + if (netifs.empty()) { + return std::nullopt; + } + const uint32_t remote_address = remote.sin_addr.s_addr; + for (const auto& netif : netifs) { + if ((remote_address & netif.netmask) == (netif.address & netif.netmask)) { + return netif; + } + } + return netifs.front(); +} + +sockaddr_in EndpointFromHpaiAt(const uint8_t* body, size_t len, size_t offset, + const sockaddr_in& fallback) { + sockaddr_in out = fallback; + if (body == nullptr || offset + 8 > len || body[offset] != 0x08 || + body[offset + 1] != kKnxHpaiIpv4Udp) { + return out; + } + uint32_t address = 0; + std::memcpy(&address, body + offset + 2, sizeof(address)); + const uint16_t port = ReadBe16(body + offset + 6); + if (address != 0) { + out.sin_addr.s_addr = address; + } + if (port != 0) { + out.sin_port = htons(port); + } + return out; +} + +sockaddr_in ResponseEndpointFromHpai(const uint8_t* body, size_t len, + const sockaddr_in& fallback) { + return EndpointFromHpaiAt(body, len, 0, fallback); +} + void WriteBe16(uint8_t* data, uint16_t value) { data[0] = static_cast((value >> 8) & 0xff); data[1] = static_cast(value & 0xff); } +uint16_t TunnelServiceForCemi(const uint8_t* data, size_t len) { + if (data == nullptr || len == 0) { + return kServiceTunnellingRequest; + } + return (data[0] == kCemiLDataReq || data[0] == kCemiLDataCon || data[0] == kCemiLDataInd) + ? kServiceTunnellingRequest + : kServiceDeviceConfigurationRequest; +} + +std::vector CemiWithTunnelSourceAddress(const uint8_t* data, size_t len, + uint16_t source_address) { + std::vector frame(data, data + len); + if (len < 8 || frame[0] != kCemiLDataReq) { + return frame; + } + const size_t additional_info_len = frame[1]; + const size_t source_offset = 2 + additional_info_len + 2; + if (source_offset + 1 >= frame.size()) { + return frame; + } + if (ReadBe16(frame.data() + source_offset) == 0) { + WriteBe16(frame.data() + source_offset, source_address); + } + return frame; +} + std::optional ObjectIntAny(const DaliValue::Object& object, std::initializer_list keys) { for (const char* key : keys) { @@ -491,19 +659,6 @@ std::vector KnxNetIpPacket(uint16_t service, const std::vector return packet; } -std::array HpaiForRemote(const sockaddr_in& remote) { - std::array hpai{}; - hpai[0] = 0x08; - hpai[1] = 0x01; - const uint32_t address = ntohl(remote.sin_addr.s_addr); - hpai[2] = static_cast((address >> 24) & 0xff); - hpai[3] = static_cast((address >> 16) & 0xff); - hpai[4] = static_cast((address >> 8) & 0xff); - hpai[5] = static_cast(address & 0xff); - WriteBe16(hpai.data() + 6, ntohs(remote.sin_port)); - return hpai; -} - bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service, uint16_t* total_len) { if (data == nullptr || len < 6 || data[0] != kKnxNetIpHeaderSize || @@ -682,6 +837,20 @@ std::optional GatewayKnxConfigFromValue(const DaliValue* value ObjectIntAny(object, {"individualAddress", "individual_address"}) .value_or(config.individual_address), 0, 0xffff)); + config.programming_button_gpio = std::clamp( + ObjectIntAny(object, {"programmingButtonGpio", "programming_button_gpio"}) + .value_or(config.programming_button_gpio), + -1, 48); + config.programming_button_active_low = + ObjectBoolAny(object, {"programmingButtonActiveLow", "programming_button_active_low"}) + .value_or(config.programming_button_active_low); + config.programming_led_gpio = std::clamp( + ObjectIntAny(object, {"programmingLedGpio", "programming_led_gpio"}) + .value_or(config.programming_led_gpio), + -1, 48); + config.programming_led_active_high = + ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"}) + .value_or(config.programming_led_active_high); const auto* tp_uart = getObjectValue(object, "tpUart"); if (tp_uart == nullptr) { @@ -690,7 +859,7 @@ std::optional GatewayKnxConfigFromValue(const DaliValue* value if (tp_uart != nullptr && tp_uart->asObject() != nullptr) { const auto& serial = *tp_uart->asObject(); config.tp_uart.uart_port = std::clamp( - ObjectIntAny(serial, {"uartPort", "uart_port"}).value_or(config.tp_uart.uart_port), 0, + ObjectIntAny(serial, {"uartPort", "uart_port"}).value_or(config.tp_uart.uart_port), -1, 2); config.tp_uart.tx_pin = ObjectIntAny(serial, {"txPin", "tx_pin"}).value_or(config.tp_uart.tx_pin); config.tp_uart.rx_pin = ObjectIntAny(serial, {"rxPin", "rx_pin"}).value_or(config.tp_uart.rx_pin); @@ -721,6 +890,10 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { out["udpPort"] = static_cast(config.udp_port); out["multicastAddress"] = config.multicast_address; out["individualAddress"] = static_cast(config.individual_address); + out["programmingButtonGpio"] = config.programming_button_gpio; + out["programmingButtonActiveLow"] = config.programming_button_active_low; + out["programmingLedGpio"] = config.programming_led_gpio; + out["programmingLedActiveHigh"] = config.programming_led_active_high; DaliValue::Object serial; serial["uartPort"] = config.tp_uart.uart_port; serial["txPin"] = config.tp_uart.tx_pin; @@ -742,6 +915,10 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { return DaliValue(std::move(out)); } +bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config) { + return config.ip_router_enabled && config.tp_uart.uart_port >= 0; +} + const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode) { switch (mode) { case GatewayKnxMappingMode::kEtsDatabase: @@ -1042,6 +1219,36 @@ std::vector GatewayKnxBridge::describeDaliBindings() cons return bindings; } +bool GatewayKnxBridge::matchesCemiFrame(const uint8_t* data, size_t len) const { + const auto decoded = DecodeCemiGroupWrite(data, len); + return decoded.has_value() && matchesGroupAddress(decoded->group_address); +} + +bool GatewayKnxBridge::matchesGroupAddress(uint16_t group_address) const { + if (!config_.dali_router_enabled) { + return false; + } + if (config_.ets_database_enabled && + ets_bindings_by_group_address_.find(group_address) != ets_bindings_by_group_address_.end()) { + return true; + } + const uint8_t main = static_cast((group_address >> 11) & 0x1f); + const uint8_t middle = static_cast((group_address >> 8) & 0x07); + const uint8_t sub = static_cast(group_address & 0xff); + if (main != config_.main_group) { + return false; + } + if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { + const uint16_t object_number = static_cast((middle << 8) | sub); + return GwReg1BindingForObject(config_.main_group, object_number).has_value(); + } + if (config_.mapping_mode == GatewayKnxMappingMode::kManual) { + return false; + } + return GatewayKnxDaliDataTypeForMiddleGroup(middle).has_value() && + GatewayKnxDaliTargetForSubgroup(sub).has_value(); +} + DaliBridgeResult GatewayKnxBridge::handleCemiFrame(const uint8_t* data, size_t len) { const auto decoded = DecodeCemiGroupWrite(data, len); if (!decoded.has_value()) { @@ -1595,10 +1802,15 @@ GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHa handler_(std::move(handler)), openknx_namespace_(std::move(openknx_namespace)) { openknx_lock_ = xSemaphoreCreateMutex(); + startup_semaphore_ = xSemaphoreCreateBinary(); } GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); + if (startup_semaphore_ != nullptr) { + vSemaphoreDelete(startup_semaphore_); + startup_semaphore_ = nullptr; + } if (openknx_lock_ != nullptr) { vSemaphoreDelete(openknx_lock_); openknx_lock_ = nullptr; @@ -1607,43 +1819,45 @@ GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; } +void GatewayKnxTpIpRouter::setCommissioningOnly(bool enabled) { + commissioning_only_ = enabled; +} + +void GatewayKnxTpIpRouter::setGroupWriteHandler(GroupWriteHandler handler) { + group_write_handler_ = std::move(handler); +} + const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; } +bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; } + esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) { if (started_ || task_handle_ != nullptr) { return ESP_OK; } + if (openknx_lock_ == nullptr || startup_semaphore_ == nullptr) { + last_error_ = "failed to allocate KNX runtime synchronization primitives"; + return ESP_ERR_NO_MEM; + } if (!config_.ip_router_enabled) { + last_error_ = "KNXnet/IP router is disabled in config"; return ESP_ERR_NOT_SUPPORTED; } stop_requested_ = false; last_error_.clear(); + ESP_LOGI(kTag, + "starting KNXnet/IP router namespace=%s udp=%u tunnel=%d multicast=%d group=%s " + "tpUart=%d tx=%d rx=%d commissioningOnly=%d", + openknx_namespace_.c_str(), static_cast(config_.udp_port), + config_.tunnel_enabled, config_.multicast_enabled, + config_.multicast_address.c_str(), config_.tp_uart.uart_port, + config_.tp_uart.tx_pin, config_.tp_uart.rx_pin, commissioning_only_); if (!configureSocket()) { return ESP_FAIL; } - ets_device_ = std::make_unique(openknx_namespace_, - config_.individual_address); - ets_device_->setFunctionPropertyHandlers( - [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, - std::vector* response) { - return bridge_.handleFunctionPropertyCommand(object_index, property_id, data, len, response); - }, - [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, - std::vector* response) { - return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response); - }); - ets_device_->setGroupWriteHandler( - [this](uint16_t group_address, const uint8_t* data, size_t len) { - const DaliBridgeResult result = bridge_.handleGroupWrite(group_address, data, len); - if (!result.ok && !result.error.empty()) { - ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str()); - } - }); - if (!configureTpUart()) { - ets_device_.reset(); - closeSockets(); - return ESP_FAIL; + while (xSemaphoreTake(startup_semaphore_, 0) == pdTRUE) { } + startup_result_ = ESP_ERR_TIMEOUT; const BaseType_t created = xTaskCreate(&GatewayKnxTpIpRouter::TaskEntry, "gw_knx_ip", task_stack_size, this, task_priority, &task_handle_); if (created != pdPASS) { @@ -1651,8 +1865,13 @@ esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task closeSockets(); return ESP_ERR_NO_MEM; } - started_ = true; - return ESP_OK; + if (xSemaphoreTake(startup_semaphore_, pdMS_TO_TICKS(10000)) != pdTRUE) { + last_error_ = "timed out starting KNXnet/IP OpenKNX runtime"; + stop_requested_ = true; + closeSockets(); + return ESP_ERR_TIMEOUT; + } + return startup_result_; } esp_err_t GatewayKnxTpIpRouter::stop() { @@ -1672,7 +1891,7 @@ const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target, uint8_t actual_level) { - if (!started_ || !config_.ip_router_enabled) { + if (!started_ || !config_.ip_router_enabled || !shouldRouteDaliApplicationFrames()) { return false; } uint16_t switch_object = 0; @@ -1708,9 +1927,81 @@ void GatewayKnxTpIpRouter::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } +esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { + { + SemaphoreGuard guard(openknx_lock_); + ets_device_ = std::make_unique(openknx_namespace_, + config_.individual_address); + openknx_configured_.store(ets_device_->configured()); + ESP_LOGI(kTag, + "OpenKNX runtime namespace=%s configured=%d individual=0x%04x tunnelClient=0x%04x " + "commissioningOnly=%d", + openknx_namespace_.c_str(), ets_device_->configured(), + ets_device_->individualAddress(), ets_device_->tunnelClientAddress(), + commissioning_only_); + ets_device_->setFunctionPropertyHandlers( + [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, + std::vector* response) { + if (!shouldRouteDaliApplicationFrames()) { + return false; + } + return bridge_.handleFunctionPropertyCommand(object_index, property_id, data, len, + response); + }, + [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, + std::vector* response) { + if (!shouldRouteDaliApplicationFrames()) { + return false; + } + return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, + response); + }); + ets_device_->setGroupWriteHandler( + [this](uint16_t group_address, const uint8_t* data, size_t len) { + if (!shouldRouteDaliApplicationFrames()) { + return; + } + const DaliBridgeResult result = group_write_handler_ + ? group_write_handler_(group_address, data, len) + : bridge_.handleGroupWrite(group_address, data, len); + if (!result.ok && !result.error.empty()) { + ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str()); + } + }); + syncOpenKnxConfigFromDevice(); + } + if (!configureTpUart()) { + last_error_ = last_error_.empty() ? "failed to configure KNX TP-UART" : last_error_; + return ESP_FAIL; + } + if (!configureProgrammingGpio()) { + last_error_ = last_error_.empty() ? "failed to configure KNX programming GPIO" : last_error_; + return ESP_FAIL; + } + return ESP_OK; +} + void GatewayKnxTpIpRouter::taskLoop() { + startup_result_ = initializeRuntime(); + if (startup_result_ == ESP_OK) { + started_ = true; + } + if (startup_semaphore_ != nullptr) { + xSemaphoreGive(startup_semaphore_); + } + if (startup_result_ != ESP_OK || stop_requested_) { + finishTask(); + return; + } std::array buffer{}; while (!stop_requested_) { + const TickType_t now = xTaskGetTickCount(); + if (network_refresh_tick_ == 0 || + now - network_refresh_tick_ >= pdMS_TO_TICKS(1000)) { + refreshNetworkInterfaces(false); + pruneStaleTunnelClients(); + network_refresh_tick_ = now; + } sockaddr_in remote{}; socklen_t remote_len = sizeof(remote); const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0, @@ -1720,7 +2011,9 @@ void GatewayKnxTpIpRouter::taskLoop() { { SemaphoreGuard guard(openknx_lock_); if (ets_device_ != nullptr) { + pollProgrammingButton(); ets_device_->loop(); + updateProgrammingLed(); } } if (!stop_requested_) { @@ -1733,7 +2026,9 @@ void GatewayKnxTpIpRouter::taskLoop() { { SemaphoreGuard guard(openknx_lock_); if (ets_device_ != nullptr) { + pollProgrammingButton(); ets_device_->loop(); + updateProgrammingLed(); } } } @@ -1744,19 +2039,66 @@ void GatewayKnxTpIpRouter::finishTask() { closeSockets(); { SemaphoreGuard guard(openknx_lock_); + setProgrammingLed(false); ets_device_.reset(); + openknx_configured_.store(false); } started_ = false; task_handle_ = nullptr; vTaskDelete(nullptr); } +void GatewayKnxTpIpRouter::pollProgrammingButton() { + if (config_.programming_button_gpio < 0 || ets_device_ == nullptr) { + return; + } + const int level = gpio_get_level(static_cast(config_.programming_button_gpio)); + const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0; + const TickType_t now = xTaskGetTickCount(); + if (pressed && !programming_button_last_pressed_ && + now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) { + ets_device_->toggleProgrammingMode(); + ESP_LOGI(kTag, "KNX programming mode %s namespace=%s", + ets_device_->programmingMode() ? "enabled" : "disabled", + openknx_namespace_.c_str()); + programming_button_last_toggle_tick_ = now; + } + programming_button_last_pressed_ = pressed; +} + +void GatewayKnxTpIpRouter::updateProgrammingLed() { + if (config_.programming_led_gpio < 0 || ets_device_ == nullptr) { + return; + } + const bool programming_mode = ets_device_->programmingMode(); + if (programming_mode == programming_led_state_) { + return; + } + setProgrammingLed(programming_mode); +} + +void GatewayKnxTpIpRouter::setProgrammingLed(bool on) { + if (config_.programming_led_gpio < 0) { + programming_led_state_ = on; + return; + } + const bool level = config_.programming_led_active_high ? on : !on; + gpio_set_level(static_cast(config_.programming_led_gpio), level ? 1 : 0); + programming_led_state_ = on; +} + void GatewayKnxTpIpRouter::closeSockets() { if (udp_sock_ >= 0) { shutdown(udp_sock_, SHUT_RDWR); close(udp_sock_); udp_sock_ = -1; } + multicast_joined_interfaces_.clear(); + network_refresh_tick_ = 0; + for (auto& client : tunnel_clients_) { + resetTunnelClient(client); + } + last_tunnel_channel_id_ = 0; if (tp_uart_port_ >= 0) { uart_driver_delete(static_cast(tp_uart_port_)); tp_uart_port_ = -1; @@ -1766,18 +2108,26 @@ void GatewayKnxTpIpRouter::closeSockets() { bool GatewayKnxTpIpRouter::configureSocket() { udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (udp_sock_ < 0) { - last_error_ = "failed to create KNXnet/IP UDP socket"; + last_error_ = ErrnoDetail("failed to create KNXnet/IP UDP socket", errno); + ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } - int reuse = 1; - setsockopt(udp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + int broadcast = 1; + if (setsockopt(udp_sock_, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { + ESP_LOGW(kTag, "failed to enable broadcast for KNX UDP port %u: errno=%d (%s)", + static_cast(config_.udp_port), errno, std::strerror(errno)); + } sockaddr_in bind_addr{}; bind_addr.sin_family = AF_INET; bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); bind_addr.sin_port = htons(config_.udp_port); if (bind(udp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { - last_error_ = "failed to bind KNXnet/IP UDP socket"; + const int saved_errno = errno; + last_error_ = ErrnoDetail("failed to bind KNXnet/IP UDP socket on port " + + std::to_string(config_.udp_port), + saved_errno); + ESP_LOGE(kTag, "%s", last_error_.c_str()); closeSockets(); return false; } @@ -1785,26 +2135,70 @@ bool GatewayKnxTpIpRouter::configureSocket() { timeval timeout{}; timeout.tv_sec = 0; timeout.tv_usec = 20000; - setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + if (setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { + ESP_LOGW(kTag, "failed to set KNX UDP receive timeout on port %u: errno=%d (%s)", + static_cast(config_.udp_port), errno, std::strerror(errno)); + } if (config_.multicast_enabled) { uint8_t multicast_loop = 0; - setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop, - sizeof(multicast_loop)); - ip_mreq mreq{}; - mreq.imr_multiaddr.s_addr = inet_addr(config_.multicast_address.c_str()); - mreq.imr_interface.s_addr = htonl(INADDR_ANY); - if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { - ESP_LOGW(kTag, "failed to join KNX multicast group %s", config_.multicast_address.c_str()); + if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop, + sizeof(multicast_loop)) < 0) { + ESP_LOGW(kTag, "failed to disable KNX multicast loopback for %s: errno=%d (%s)", + config_.multicast_address.c_str(), errno, std::strerror(errno)); } + refreshNetworkInterfaces(true); } return true; } +void GatewayKnxTpIpRouter::refreshNetworkInterfaces(bool force_log) { + if (!config_.multicast_enabled || udp_sock_ < 0) { + return; + } + const auto netifs = ActiveKnxNetifs(); + if (netifs.empty()) { + if (force_log) { + ESP_LOGW(kTag, "KNX multicast group %s not joined yet: no IPv4 interface is up", + config_.multicast_address.c_str()); + } + return; + } + + const uint32_t multicast_address = inet_addr(config_.multicast_address.c_str()); + for (const auto& netif : netifs) { + if (std::find(multicast_joined_interfaces_.begin(), multicast_joined_interfaces_.end(), + netif.address) != multicast_joined_interfaces_.end()) { + continue; + } + ip_mreq mreq{}; + mreq.imr_multiaddr.s_addr = multicast_address; + mreq.imr_interface.s_addr = netif.address; + if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { + ESP_LOGW(kTag, + "failed to join KNX multicast group %s on %s %s UDP port %u: errno=%d (%s)", + config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(), + static_cast(config_.udp_port), errno, std::strerror(errno)); + continue; + } + multicast_joined_interfaces_.push_back(netif.address); + ESP_LOGI(kTag, "joined KNX multicast group %s on %s %s UDP port %u", + config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(), + static_cast(config_.udp_port)); + } +} + bool GatewayKnxTpIpRouter::configureTpUart() { + if (!GatewayKnxConfigUsesTpUart(config_)) { + tp_uart_port_ = -1; + tp_uart_online_ = false; + ESP_LOGI(kTag, "KNX TP-UART disabled by UART port; KNXnet/IP uses IP-only runtime"); + return true; + } const auto& serial = config_.tp_uart; if (serial.uart_port < 0 || serial.uart_port > 2) { - last_error_ = "invalid KNX TP-UART port"; + last_error_ = "invalid KNX TP-UART port " + std::to_string(serial.uart_port); + ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } uart_config_t uart_config{}; @@ -1815,22 +2209,98 @@ bool GatewayKnxTpIpRouter::configureTpUart() { uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; const uart_port_t uart_port = static_cast(serial.uart_port); - if (uart_param_config(uart_port, &uart_config) != ESP_OK) { - last_error_ = "failed to configure KNX TP-UART parameters"; + esp_err_t err = uart_param_config(uart_port, &uart_config); + if (err != ESP_OK) { + last_error_ = EspErrDetail("failed to configure KNX TP-UART parameters on UART" + + std::to_string(serial.uart_port), + err); + ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } - if (uart_set_pin(uart_port, serial.tx_pin, serial.rx_pin, UART_PIN_NO_CHANGE, - UART_PIN_NO_CHANGE) != ESP_OK) { - last_error_ = "failed to configure KNX TP-UART pins"; + err = uart_set_pin(uart_port, serial.tx_pin < 0 ? UART_PIN_NO_CHANGE : serial.tx_pin, + serial.rx_pin < 0 ? UART_PIN_NO_CHANGE : serial.rx_pin, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + last_error_ = EspErrDetail("failed to configure KNX TP-UART pins uart=" + + std::to_string(serial.uart_port) + " tx=" + + std::to_string(serial.tx_pin) + " rx=" + + std::to_string(serial.rx_pin), + err); + ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } - if (uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr, - 0) != ESP_OK) { - last_error_ = "failed to install KNX TP-UART driver"; + err = uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr, + 0); + if (err != ESP_OK) { + last_error_ = EspErrDetail("failed to install KNX TP-UART driver on UART" + + std::to_string(serial.uart_port), + err); + ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } tp_uart_port_ = serial.uart_port; - return initializeTpUart(); + if (!initializeTpUart()) { + if (ets_device_ != nullptr && !ets_device_->configured()) { + ESP_LOGW(kTag, + "%s; continuing KNXnet/IP in commissioning-only IP mode so ETS can program the " + "device", + last_error_.c_str()); + uart_driver_delete(uart_port); + tp_uart_port_ = -1; + tp_uart_online_ = false; + return true; + } + ESP_LOGE(kTag, "%s", last_error_.c_str()); + return false; + } + ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%d rx=%d baud=%u", serial.uart_port, + serial.tx_pin, serial.rx_pin, static_cast(serial.baudrate)); + return true; +} + +bool GatewayKnxTpIpRouter::configureProgrammingGpio() { + programming_button_last_pressed_ = false; + programming_button_last_toggle_tick_ = 0; + programming_led_state_ = false; + + if (config_.programming_button_gpio >= 0) { + gpio_config_t button_config{}; + button_config.pin_bit_mask = 1ULL << static_cast(config_.programming_button_gpio); + button_config.mode = GPIO_MODE_INPUT; + button_config.pull_up_en = config_.programming_button_active_low ? GPIO_PULLUP_ENABLE + : GPIO_PULLUP_DISABLE; + button_config.pull_down_en = config_.programming_button_active_low ? GPIO_PULLDOWN_DISABLE + : GPIO_PULLDOWN_ENABLE; + button_config.intr_type = GPIO_INTR_DISABLE; + const esp_err_t err = gpio_config(&button_config); + if (err != ESP_OK) { + last_error_ = EspErrDetail("failed to configure KNX programming button GPIO" + + std::to_string(config_.programming_button_gpio), + err); + ESP_LOGE(kTag, "%s", last_error_.c_str()); + return false; + } + } + + if (config_.programming_led_gpio >= 0) { + gpio_config_t led_config{}; + led_config.pin_bit_mask = 1ULL << static_cast(config_.programming_led_gpio); + led_config.mode = GPIO_MODE_OUTPUT; + led_config.pull_up_en = GPIO_PULLUP_DISABLE; + led_config.pull_down_en = GPIO_PULLDOWN_DISABLE; + led_config.intr_type = GPIO_INTR_DISABLE; + const esp_err_t err = gpio_config(&led_config); + if (err != ESP_OK) { + last_error_ = EspErrDetail("failed to configure KNX programming LED GPIO" + + std::to_string(config_.programming_led_gpio), + err); + ESP_LOGE(kTag, "%s", last_error_.c_str()); + return false; + } + setProgrammingLed(false); + } + + return true; } bool GatewayKnxTpIpRouter::initializeTpUart() { @@ -1847,7 +2317,8 @@ bool GatewayKnxTpIpRouter::initializeTpUart() { const uint8_t reset_request = kTpUartResetRequest; if (uart_write_bytes(uart_port, &reset_request, 1) != 1) { - last_error_ = "failed to send KNX TP-UART reset request"; + last_error_ = "failed to send KNX TP-UART reset request uart=" + + std::to_string(tp_uart_port_); return false; } @@ -1883,8 +2354,12 @@ bool GatewayKnxTpIpRouter::initializeTpUart() { } } - last_error_ = saw_reset ? "timed out waiting for KNX TP-UART state indication" - : "timed out waiting for KNX TP-UART reset indication"; + last_error_ = (saw_reset ? "timed out waiting for KNX TP-UART state indication" + : "timed out waiting for KNX TP-UART reset indication") + + std::string(" uart=") + std::to_string(config_.tp_uart.uart_port) + + " tx=" + std::to_string(config_.tp_uart.tx_pin) + + " rx=" + std::to_string(config_.tp_uart.rx_pin) + + " timeoutMs=1500"; return false; } @@ -1902,6 +2377,25 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, return; } switch (service) { + case kServiceSearchRequest: + case kServiceSearchRequestExt: + handleSearchRequest(service, body, body_len, remote); + break; + case kServiceDescriptionRequest: + handleDescriptionRequest(body, body_len, remote); + break; + case kServiceDeviceConfigurationRequest: + handleDeviceConfigurationRequest(body, body_len, remote); + break; + case kServiceDeviceConfigurationAck: + case kServiceTunnellingAck: + if (body_len >= 4) { + ESP_LOGD(kTag, "rx KNXnet/IP ack service=0x%04x channel=%u seq=%u status=0x%02x from %s", + static_cast(service), static_cast(body[1]), + static_cast(body[2]), static_cast(body[3]), + EndpointString(remote).c_str()); + } + break; case kServiceRoutingIndication: if (config_.multicast_enabled) { handleRoutingIndication(body, body_len); @@ -1924,16 +2418,76 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, handleDisconnectRequest(body, body_len, remote); break; default: + ESP_LOGD(kTag, "ignore KNXnet/IP service=0x%04x len=%u from %s", + static_cast(service), static_cast(body_len), + EndpointString(remote).c_str()); break; } } +void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* body, + size_t len, const sockaddr_in& remote) { + sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); + selectOpenKnxNetworkInterface(response_remote); + std::set requested_dibs; + if (service == kServiceSearchRequestExt && body != nullptr && len > 8) { + size_t offset = 8; + while (offset + 2 <= len) { + const uint8_t srp_len = body[offset]; + if (srp_len < 2 || offset + srp_len > len) { + break; + } + const uint8_t srp_type = body[offset + 1]; + if (srp_type == 0x01) { + SemaphoreGuard guard(openknx_lock_); + if (ets_device_ == nullptr || !ets_device_->programmingMode()) { + return; + } + } else if (srp_type == 0x02 && srp_len >= 8) { + uint8_t mac[6]{}; + if (!ReadBaseMac(mac) || std::memcmp(mac, body + offset + 2, 6) != 0) { + return; + } + } else if (srp_type == 0x03) { + for (size_t service_offset = offset + 2; service_offset + 1 < offset + srp_len; + service_offset += 2) { + const uint8_t family = body[service_offset]; + const uint8_t version = body[service_offset + 1]; + if ((family == kKnxServiceFamilyCore && version > 2) || + (family == kKnxServiceFamilyDeviceManagement && version > 1) || + (family == kKnxServiceFamilyTunnelling && + (!config_.tunnel_enabled || version > 1)) || + (family == kKnxServiceFamilyRouting && + (!config_.multicast_enabled || version > 1))) { + return; + } + } + } else if (srp_type == 0x04) { + for (size_t dib_offset = offset + 2; dib_offset < offset + srp_len; ++dib_offset) { + requested_dibs.insert(body[dib_offset]); + } + } + offset += srp_len; + } + } + sendSearchResponse(service == kServiceSearchRequestExt ? kServiceSearchResponseExt + : kServiceSearchResponse, + response_remote, requested_dibs); +} + +void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* body, size_t len, + const sockaddr_in& remote) { + const sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); + selectOpenKnxNetworkInterface(response_remote); + sendDescriptionResponse(response_remote); +} + void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t len) { if (body == nullptr || len == 0) { return; } const bool consumed_by_openknx = handleOpenKnxBusFrame(body, len); - if (!consumed_by_openknx) { + if (!consumed_by_openknx && shouldRouteDaliApplicationFrames()) { const DaliBridgeResult result = handler_(body, len); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str()); @@ -1942,60 +2496,278 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t l forwardCemiToTp(body, len); } +GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient( + uint8_t channel_id) { + if (channel_id == 0) { + return nullptr; + } + for (auto& client : tunnel_clients_) { + if (client.connected && client.channel_id == channel_id) { + return &client; + } + } + return nullptr; +} + +const GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient( + uint8_t channel_id) const { + if (channel_id == 0) { + return nullptr; + } + for (const auto& client : tunnel_clients_) { + if (client.connected && client.channel_id == channel_id) { + return &client; + } + } + return nullptr; +} + +void GatewayKnxTpIpRouter::resetTunnelClient(TunnelClient& client) { + if (client.connected) { + ESP_LOGI(kTag, "closed KNXnet/IP tunnel channel=%u type=0x%02x data=%s", + static_cast(client.channel_id), + static_cast(client.connection_type), + EndpointString(client.data_remote).c_str()); + } + client = TunnelClient{}; +} + +uint8_t GatewayKnxTpIpRouter::nextTunnelChannelId() const { + uint8_t candidate = last_tunnel_channel_id_; + for (int attempts = 0; attempts < 255; ++attempts) { + candidate = static_cast(candidate + 1); + if (candidate == 0) { + candidate = 1; + } + if (findTunnelClient(candidate) == nullptr) { + return candidate; + } + } + return 0; +} + +uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddressForSlot(size_t slot) const { + const uint16_t first = effectiveTunnelAddress(); + const uint16_t line = first & 0xff00; + uint16_t device = static_cast((first & 0x00ff) + slot); + if (device == 0 || device > 0xff) { + device = static_cast(1 + slot); + } + return static_cast(line | (device & 0x00ff)); +} + +GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient( + const sockaddr_in& control_remote, const sockaddr_in& data_remote, uint8_t connection_type) { + TunnelClient* free_client = nullptr; + size_t free_index = 0; + for (size_t index = 0; index < tunnel_clients_.size(); ++index) { + auto& client = tunnel_clients_[index]; + if (client.connected && client.connection_type == connection_type && + EndpointEquals(client.control_remote, control_remote) && + EndpointEquals(client.data_remote, data_remote)) { + ESP_LOGW(kTag, "replacing existing KNXnet/IP tunnel channel=%u for endpoint %s", + static_cast(client.channel_id), EndpointString(data_remote).c_str()); + resetTunnelClient(client); + free_client = &client; + free_index = index; + break; + } + if (!client.connected && free_client == nullptr) { + free_client = &client; + free_index = index; + } + } + if (free_client == nullptr) { + return nullptr; + } + const uint8_t channel_id = nextTunnelChannelId(); + if (channel_id == 0) { + return nullptr; + } + free_client->connected = true; + free_client->channel_id = channel_id; + free_client->connection_type = connection_type; + free_client->received_sequence = 255; + free_client->send_sequence = 0; + free_client->individual_address = effectiveTunnelAddressForSlot(free_index); + free_client->last_activity_tick = xTaskGetTickCount(); + free_client->control_remote = control_remote; + free_client->data_remote = data_remote; + last_tunnel_channel_id_ = channel_id; + return free_client; +} + +void GatewayKnxTpIpRouter::pruneStaleTunnelClients() { + const TickType_t now = xTaskGetTickCount(); + const TickType_t timeout = pdMS_TO_TICKS(120000); + for (auto& client : tunnel_clients_) { + if (!client.connected || client.last_activity_tick == 0) { + continue; + } + if (now - client.last_activity_tick > timeout) { + ESP_LOGW(kTag, "closing stale KNXnet/IP tunnel channel=%u after heartbeat timeout", + static_cast(client.channel_id)); + resetTunnelClient(client); + } + } +} + void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 5 || body[0] != 0x04) { + ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling request from %s len=%u", + EndpointString(remote).c_str(), static_cast(len)); return; } const uint8_t channel_id = body[1]; const uint8_t sequence = body[2]; - if (!tunnel_connected_ || channel_id != tunnel_channel_id_) { + TunnelClient* client = findTunnelClient(channel_id); + if (client == nullptr) { + ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: no connection", + static_cast(channel_id), static_cast(sequence), + EndpointString(remote).c_str()); sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } - if (sequence != expected_tunnel_sequence_) { + if (!EndpointEquals(remote, client->data_remote)) { + ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: expected data endpoint %s", + static_cast(channel_id), static_cast(sequence), + EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str()); + sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote); + return; + } + if (sequence == client->received_sequence) { + ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u", + static_cast(channel_id), static_cast(sequence)); + sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); + return; + } + if (static_cast(sequence - 1) != client->received_sequence) { + ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s", + static_cast(channel_id), static_cast(sequence), + static_cast(static_cast(client->received_sequence + 1)), + EndpointString(remote).c_str()); sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote); return; } - expected_tunnel_sequence_ = static_cast((expected_tunnel_sequence_ + 1) & 0xff); - sendTunnellingAck(channel_id, sequence, kKnxNoError, remote); - const uint8_t* cemi = body + 4; - const size_t cemi_len = len - 4; + client->received_sequence = sequence; + client->last_activity_tick = xTaskGetTickCount(); + sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); + const auto cemi_frame = CemiWithTunnelSourceAddress(body + 4, len - 4, client->individual_address); + const uint8_t* cemi = cemi_frame.data(); + const size_t cemi_len = cemi_frame.size(); + ESP_LOGI(kTag, "rx KNXnet/IP tunnelling request channel=%u seq=%u cemiLen=%u from %s", + static_cast(channel_id), static_cast(sequence), + static_cast(cemi_len), EndpointString(remote).c_str()); const bool group_frame = IsCemiGroupFrame(cemi, cemi_len); - const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len); + const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len, client); if (consumed_by_openknx) { if (group_frame) { forwardCemiToTp(cemi, cemi_len); } return; } - const DaliBridgeResult result = handler_(cemi, cemi_len); - if (!result.ok && !result.error.empty()) { - ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str()); + if (shouldRouteDaliApplicationFrames()) { + const DaliBridgeResult result = handler_(cemi, cemi_len); + if (!result.ok && !result.error.empty()) { + ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str()); + } } forwardCemiToTp(cemi, cemi_len); } +void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* body, size_t len, + const sockaddr_in& remote) { + if (body == nullptr || len < 5 || body[0] != 0x04) { + ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration request from %s len=%u", + EndpointString(remote).c_str(), static_cast(len)); + return; + } + const uint8_t channel_id = body[1]; + const uint8_t sequence = body[2]; + TunnelClient* client = findTunnelClient(channel_id); + if (client == nullptr) { + ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: no connection", + static_cast(channel_id), static_cast(sequence), + EndpointString(remote).c_str()); + sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote); + return; + } + if (!EndpointEquals(remote, client->data_remote)) { + ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: expected data endpoint %s", + static_cast(channel_id), static_cast(sequence), + EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str()); + sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote); + return; + } + client->last_activity_tick = xTaskGetTickCount(); + sendDeviceConfigurationAck(channel_id, sequence, kKnxNoError, client->data_remote); + const uint8_t* cemi = body + 4; + const size_t cemi_len = len - 4; + ESP_LOGI(kTag, "rx KNXnet/IP device-configuration request channel=%u seq=%u cemiLen=%u from %s", + static_cast(channel_id), static_cast(sequence), + static_cast(cemi_len), EndpointString(remote).c_str()); + if (!handleOpenKnxTunnelFrame(cemi, cemi_len, client)) { + ESP_LOGW(kTag, "KNXnet/IP device-configuration cEMI was not consumed by OpenKNX cemiLen=%u", + static_cast(cemi_len)); + } +} + void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { - if (body == nullptr || len < 20) { + if (body == nullptr || len < 18) { + ESP_LOGW(kTag, "invalid KNXnet/IP connect request from %s len=%u", + EndpointString(remote).c_str(), static_cast(len)); return; } + sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 0, remote); + sockaddr_in data_remote = EndpointFromHpaiAt(body, len, 8, remote); + selectOpenKnxNetworkInterface(control_remote); const size_t cri_offset = 16; - if (body[cri_offset] < 4 || body[cri_offset + 1] != kKnxConnectionTypeTunnel || - body[cri_offset + 2] != kKnxTunnelLayerLink) { - sendConnectResponse(0, kKnxErrorConnectionType, remote); + const uint8_t cri_length = body[cri_offset]; + const uint8_t connection_type = body[cri_offset + 1]; + if (cri_length < 2 || cri_offset + cri_length > len) { + ESP_LOGW(kTag, "invalid KNXnet/IP connect CRI from %s len=%u criLen=%u", + EndpointString(remote).c_str(), static_cast(len), + static_cast(cri_length)); + sendConnectResponse(0, kKnxErrorConnectionType, control_remote, kKnxConnectionTypeTunnel, 0); return; } - if (tunnel_connected_) { - sendConnectResponse(0, kKnxErrorNoMoreConnections, remote); + if (connection_type != kKnxConnectionTypeTunnel && + connection_type != kKnxConnectionTypeDeviceManagement) { + ESP_LOGW(kTag, "reject KNXnet/IP connect from %s unsupported type=0x%02x", + EndpointString(remote).c_str(), static_cast(connection_type)); + sendConnectResponse(0, kKnxErrorConnectionType, control_remote, connection_type, 0); return; } - tunnel_connected_ = true; - expected_tunnel_sequence_ = 0; - tunnel_send_sequence_ = 0; - tunnel_remote_ = remote; - sendConnectResponse(tunnel_channel_id_, kKnxNoError, remote); + if (connection_type == kKnxConnectionTypeTunnel && + (cri_length < 4 || body[cri_offset + 2] != kKnxTunnelLayerLink)) { + ESP_LOGW(kTag, "reject KNXnet/IP tunnel connect from %s unsupported layer=0x%02x", + EndpointString(remote).c_str(), + static_cast(cri_length >= 3 ? body[cri_offset + 2] : 0)); + sendConnectResponse(0, kKnxErrorTunnellingLayer, control_remote, connection_type, 0); + return; + } + TunnelClient* client = allocateTunnelClient(control_remote, data_remote, connection_type); + if (client == nullptr) { + ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: no free tunnel client slots", + EndpointString(remote).c_str()); + sendConnectResponse(0, kKnxErrorNoMoreConnections, control_remote, connection_type, 0); + return; + } + ESP_LOGI(kTag, + "accepted KNXnet/IP connect namespace=%s channel=%u type=0x%02x tunnelPa=0x%04x ctrl=%s data=%s remote=%s active=%u/%u", + openknx_namespace_.c_str(), static_cast(client->channel_id), + static_cast(connection_type), static_cast(client->individual_address), + EndpointString(control_remote).c_str(), EndpointString(data_remote).c_str(), + EndpointString(remote).c_str(), + static_cast(std::count_if(tunnel_clients_.begin(), tunnel_clients_.end(), + [](const TunnelClient& item) { + return item.connected; + })), + static_cast(tunnel_clients_.size())); + sendConnectResponse(client->channel_id, kKnxNoError, control_remote, connection_type, + client->individual_address); } void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, size_t len, @@ -2004,10 +2776,22 @@ void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, siz return; } const uint8_t channel_id = body[0]; + const sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 2, remote); + TunnelClient* client = findTunnelClient(channel_id); + const bool endpoint_matches = client != nullptr && + EndpointEquals(control_remote, client->control_remote); + const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId; + if (client != nullptr) { + if (endpoint_matches) { + client->last_activity_tick = xTaskGetTickCount(); + } + } + ESP_LOGI(kTag, "rx KNXnet/IP connection-state request channel=%u status=0x%02x from %s ctrl=%s expected=%s", + static_cast(channel_id), static_cast(status), + EndpointString(remote).c_str(), EndpointString(control_remote).c_str(), + client == nullptr ? "none" : EndpointString(client->control_remote).c_str()); sendConnectionStateResponse( - channel_id, tunnel_connected_ && channel_id == tunnel_channel_id_ ? kKnxNoError - : kKnxErrorConnectionId, - remote); + channel_id, status, control_remote); } void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t len, @@ -2016,15 +2800,19 @@ void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t l return; } const uint8_t channel_id = body[0]; - const uint8_t status = tunnel_connected_ && channel_id == tunnel_channel_id_ - ? kKnxNoError - : kKnxErrorConnectionId; + const sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 2, remote); + TunnelClient* client = findTunnelClient(channel_id); + const bool endpoint_matches = client != nullptr && + EndpointEquals(control_remote, client->control_remote); + const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId; if (status == kKnxNoError) { - tunnel_connected_ = false; - expected_tunnel_sequence_ = 0; - tunnel_send_sequence_ = 0; + resetTunnelClient(*client); } - sendDisconnectResponse(channel_id, status, remote); + ESP_LOGI(kTag, "rx KNXnet/IP disconnect request channel=%u status=0x%02x from %s ctrl=%s expected=%s", + static_cast(channel_id), static_cast(status), + EndpointString(remote).c_str(), EndpointString(control_remote).c_str(), + client == nullptr ? "none" : EndpointString(client->control_remote).c_str()); + sendDisconnectResponse(channel_id, status, control_remote); } void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* body, @@ -2057,8 +2845,20 @@ void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status, const sockaddr_in& remote) { + sendConnectionHeaderAck(kServiceTunnellingAck, channel_id, sequence, status, remote); +} + +void GatewayKnxTpIpRouter::sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence, + uint8_t status, + const sockaddr_in& remote) { + sendConnectionHeaderAck(kServiceDeviceConfigurationAck, channel_id, sequence, status, remote); +} + +void GatewayKnxTpIpRouter::sendConnectionHeaderAck(uint16_t service, uint8_t channel_id, + uint8_t sequence, uint8_t status, + const sockaddr_in& remote) { const std::vector body{0x04, channel_id, sequence, status}; - const auto packet = KnxNetIpPacket(kServiceTunnellingAck, body); + const auto packet = KnxNetIpPacket(service, body); SendAll(udp_sock_, packet.data(), packet.size(), remote); } @@ -2068,19 +2868,249 @@ void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockadd SendAll(udp_sock_, packet.data(), packet.size(), remote); } -void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) { - if (!tunnel_connected_ || udp_sock_ < 0 || data == nullptr || len == 0) { +std::array GatewayKnxTpIpRouter::localHpaiForRemote( + const sockaddr_in& remote) const { + std::array hpai{}; + hpai[0] = 0x08; + hpai[1] = kKnxHpaiIpv4Udp; + const auto netif = SelectKnxNetifForRemote(remote); + WriteIp(hpai.data() + 2, netif.has_value() ? netif->address : htonl(INADDR_ANY)); + WriteBe16(hpai.data() + 6, config_.udp_port); + return hpai; +} + +std::vector GatewayKnxTpIpRouter::buildDeviceInfoDib( + const sockaddr_in& remote) const { + std::vector dib(54, 0); + dib[0] = static_cast(dib.size()); + dib[1] = kKnxDibDeviceInfo; + dib[2] = GatewayKnxConfigUsesTpUart(config_) ? kKnxMediumTp1 : kKnxMediumIp; + { + SemaphoreGuard guard(openknx_lock_); + dib[3] = ets_device_ != nullptr && ets_device_->programmingMode() ? 1 : 0; + } + WriteBe16(dib.data() + 4, effectiveIndividualAddress()); + WriteBe16(dib.data() + 6, 0); + + uint8_t mac[6]{}; + if (ReadBaseMac(mac)) { + dib[8] = static_cast((kKnxManufacturerId >> 8) & 0xff); + dib[9] = static_cast(kKnxManufacturerId & 0xff); + std::memcpy(dib.data() + 10, mac + 2, 4); + std::memcpy(dib.data() + 18, mac, 6); + } + + WriteIp(dib.data() + 14, inet_addr(config_.multicast_address.c_str())); + char friendly[31]{}; + std::snprintf(friendly, sizeof(friendly), "DALI GW MG%u %s", + static_cast(config_.main_group), openknx_namespace_.c_str()); + std::memcpy(dib.data() + 24, friendly, std::min(30, std::strlen(friendly))); + (void)remote; + return dib; +} + +std::vector GatewayKnxTpIpRouter::buildExtendedDeviceInfoDib() const { + std::vector dib(8, 0); + dib[0] = static_cast(dib.size()); + dib[1] = kKnxDibExtendedDeviceInfo; + dib[2] = 0x01; + dib[3] = 0x00; + WriteBe16(dib.data() + 4, 254); + WriteBe16(dib.data() + 6, kKnxDeviceDescriptor); + return dib; +} + +std::vector GatewayKnxTpIpRouter::buildIpConfigDib(const sockaddr_in& remote, + bool current) const { + const auto netif = SelectKnxNetifForRemote(remote); + const uint32_t address = netif.has_value() ? netif->address : htonl(INADDR_ANY); + const uint32_t netmask = netif.has_value() ? netif->netmask : htonl(INADDR_ANY); + const uint32_t gateway = netif.has_value() ? netif->gateway : htonl(INADDR_ANY); + std::vector dib(current ? 20 : 16, 0); + dib[0] = static_cast(dib.size()); + dib[1] = current ? kKnxDibCurrentIpConfig : kKnxDibIpConfig; + WriteIp(dib.data() + 2, address); + WriteIp(dib.data() + 6, netmask); + WriteIp(dib.data() + 10, gateway); + if (current) { + WriteIp(dib.data() + 14, htonl(INADDR_ANY)); + dib[18] = kKnxIpAssignmentManual; + dib[19] = 0x00; + } else { + dib[14] = kKnxIpCapabilityManual; + dib[15] = kKnxIpAssignmentManual; + } + return dib; +} + +std::vector GatewayKnxTpIpRouter::buildKnxAddressesDib() const { + std::vector dib(4 + kMaxTunnelClients * 2U, 0); + dib[0] = static_cast(dib.size()); + dib[1] = kKnxDibKnxAddresses; + WriteBe16(dib.data() + 2, effectiveIndividualAddress()); + size_t offset = 4; + for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) { + WriteBe16(dib.data() + offset, effectiveTunnelAddressForSlot(slot)); + offset += 2; + } + return dib; +} + +std::vector GatewayKnxTpIpRouter::buildTunnelingInfoDib() const { + std::vector dib(4 + kMaxTunnelClients * 4U, 0); + dib[0] = static_cast(dib.size()); + dib[1] = kKnxDibTunnellingInfo; + WriteBe16(dib.data() + 2, 254); + size_t offset = 4; + for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) { + const uint16_t address = effectiveTunnelAddressForSlot(slot); + bool used = false; + for (const auto& client : tunnel_clients_) { + if (client.connected && client.individual_address == address) { + used = true; + break; + } + } + uint16_t flags = 0xffff; + if (used) { + flags = static_cast(flags & ~0x0001U); + flags = static_cast(flags & ~0x0004U); + } + WriteBe16(dib.data() + offset, address); + WriteBe16(dib.data() + offset + 2, flags); + offset += 4; + } + return dib; +} + +std::vector GatewayKnxTpIpRouter::buildSupportedServiceDib() const { + std::vector> services{ + {kKnxServiceFamilyCore, 2}, + {kKnxServiceFamilyDeviceManagement, 1}, + }; + if (config_.tunnel_enabled) { + services.emplace_back(kKnxServiceFamilyTunnelling, 1); + } + if (config_.multicast_enabled) { + services.emplace_back(kKnxServiceFamilyRouting, 1); + } + std::vector dib(2 + services.size() * 2U, 0); + dib[0] = static_cast(dib.size()); + dib[1] = kKnxDibSupportedServices; + size_t offset = 2; + for (const auto& service : services) { + dib[offset++] = service.first; + dib[offset++] = service.second; + } + return dib; +} + +void GatewayKnxTpIpRouter::sendSearchResponse(uint16_t service, const sockaddr_in& remote, + const std::set& requested_dibs) { + if (udp_sock_ < 0) { return; } + const auto hpai = localHpaiForRemote(remote); + std::vector body; + body.insert(body.end(), hpai.begin(), hpai.end()); + std::set appended_dibs; + auto append_dib = [&body, &appended_dibs](const std::vector& dib) { + if (dib.size() < 2 || !appended_dibs.insert(dib[1]).second) { + return; + } + body.insert(body.end(), dib.begin(), dib.end()); + }; + append_dib(buildDeviceInfoDib(remote)); + append_dib(buildSupportedServiceDib()); + if (service == kServiceSearchResponseExt) { + append_dib(buildExtendedDeviceInfoDib()); + for (const uint8_t dib_type : requested_dibs) { + switch (dib_type) { + case kKnxDibDeviceInfo: + append_dib(buildDeviceInfoDib(remote)); + break; + case kKnxDibSupportedServices: + append_dib(buildSupportedServiceDib()); + break; + case kKnxDibIpConfig: + append_dib(buildIpConfigDib(remote, false)); + break; + case kKnxDibCurrentIpConfig: + append_dib(buildIpConfigDib(remote, true)); + break; + case kKnxDibKnxAddresses: + append_dib(buildKnxAddressesDib()); + break; + case kKnxDibTunnellingInfo: + if (config_.tunnel_enabled) { + append_dib(buildTunnelingInfoDib()); + } + break; + case kKnxDibExtendedDeviceInfo: + append_dib(buildExtendedDeviceInfoDib()); + break; + default: + break; + } + } + } + const auto packet = KnxNetIpPacket(service, body); + SendAll(udp_sock_, packet.data(), packet.size(), remote); + ESP_LOGI(kTag, "sent KNXnet/IP search response namespace=%s mainGroup=%u to %s:%u endpoint=%u.%u.%u.%u:%u", + openknx_namespace_.c_str(), static_cast(config_.main_group), + Ipv4String(remote.sin_addr.s_addr).c_str(), static_cast(ntohs(remote.sin_port)), + static_cast(hpai[2]), static_cast(hpai[3]), + static_cast(hpai[4]), static_cast(hpai[5]), + static_cast(config_.udp_port)); +} + +void GatewayKnxTpIpRouter::sendDescriptionResponse(const sockaddr_in& remote) { + if (udp_sock_ < 0) { + return; + } + auto device = buildDeviceInfoDib(remote); + auto services = buildSupportedServiceDib(); + std::vector body; + body.reserve(device.size() + services.size()); + body.insert(body.end(), device.begin(), device.end()); + body.insert(body.end(), services.begin(), services.end()); + const auto packet = KnxNetIpPacket(kServiceDescriptionResponse, body); + SendAll(udp_sock_, packet.data(), packet.size(), remote); + ESP_LOGI(kTag, "sent KNXnet/IP description response namespace=%s to %s:%u", + openknx_namespace_.c_str(), Ipv4String(remote.sin_addr.s_addr).c_str(), + static_cast(ntohs(remote.sin_port))); +} + +void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) { + if (udp_sock_ < 0 || data == nullptr || len == 0) { + return; + } + for (auto& client : tunnel_clients_) { + if (client.connected) { + sendTunnelIndicationToClient(client, data, len); + } + } +} + +void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, + size_t len) { + if (!client.connected || udp_sock_ < 0 || data == nullptr || len == 0) { + return; + } + const uint16_t service = TunnelServiceForCemi(data, len); std::vector body; body.reserve(4 + len); body.push_back(0x04); - body.push_back(tunnel_channel_id_); - body.push_back(tunnel_send_sequence_++); + body.push_back(client.channel_id); + body.push_back(client.send_sequence++); body.push_back(0x00); body.insert(body.end(), data, data + len); - const auto packet = KnxNetIpPacket(kServiceTunnellingRequest, body); - SendAll(udp_sock_, packet.data(), packet.size(), tunnel_remote_); + const auto packet = KnxNetIpPacket(service, body); + SendAll(udp_sock_, packet.data(), packet.size(), client.data_remote); + ESP_LOGI(kTag, "sent KNXnet/IP cEMI service=0x%04x channel=%u seq=%u cemi=0x%02x len=%u to %s", + static_cast(service), static_cast(client.channel_id), + static_cast(body[2]), static_cast(data[0]), + static_cast(len), EndpointString(client.data_remote).c_str()); } void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status, @@ -2098,19 +3128,36 @@ void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t st } void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status, - const sockaddr_in& remote) { + const sockaddr_in& remote, + uint8_t connection_type, + uint16_t tunnel_address) { std::vector body; body.reserve(16); body.push_back(channel_id); body.push_back(status); - const auto data_endpoint = HpaiForRemote(remote); + if (status != kKnxNoError) { + const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); + SendAll(udp_sock_, packet.data(), packet.size(), remote); + ESP_LOGI(kTag, "sent KNXnet/IP connect error channel=%u status=0x%02x to %s", + static_cast(channel_id), static_cast(status), + EndpointString(remote).c_str()); + return; + } + const auto data_endpoint = localHpaiForRemote(remote); body.insert(body.end(), data_endpoint.begin(), data_endpoint.end()); - body.push_back(0x04); - body.push_back(kKnxConnectionTypeTunnel); - body.push_back(static_cast((effectiveTunnelAddress() >> 8) & 0xff)); - body.push_back(static_cast(effectiveTunnelAddress() & 0xff)); + body.push_back(connection_type == kKnxConnectionTypeTunnel ? 0x04 : 0x02); + body.push_back(connection_type); + if (connection_type == kKnxConnectionTypeTunnel) { + body.push_back(static_cast((tunnel_address >> 8) & 0xff)); + body.push_back(static_cast(tunnel_address & 0xff)); + } const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); SendAll(udp_sock_, packet.data(), packet.size(), remote); + ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%u.%u.%u.%u:%u", + static_cast(channel_id), static_cast(connection_type), + EndpointString(remote).c_str(), static_cast(data_endpoint[2]), + static_cast(data_endpoint[3]), static_cast(data_endpoint[4]), + static_cast(data_endpoint[5]), static_cast(config_.udp_port)); } void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) { @@ -2123,17 +3170,45 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str()); const std::vector body(data, data + len); const auto packet = KnxNetIpPacket(kServiceRoutingIndication, body); - SendAll(udp_sock_, packet.data(), packet.size(), remote); + const auto netifs = ActiveKnxNetifs(); + if (netifs.empty()) { + SendAll(udp_sock_, packet.data(), packet.size(), remote); + return; + } + for (const auto& netif : netifs) { + in_addr multicast_interface{}; + multicast_interface.s_addr = netif.address; + if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_IF, &multicast_interface, + sizeof(multicast_interface)) < 0) { + ESP_LOGW(kTag, "failed to select KNX multicast interface %s %s: errno=%d (%s)", + netif.key, Ipv4String(netif.address).c_str(), errno, std::strerror(errno)); + continue; + } + SendAll(udp_sock_, packet.data(), packet.size(), remote); + } } -bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len) { +void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remote) { + const auto netif = SelectKnxNetifForRemote(remote); + SemaphoreGuard guard(openknx_lock_); + if (ets_device_ != nullptr) { + ets_device_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr); + } +} + +bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len, + TunnelClient* response_client) { SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { return false; } const bool consumed = ets_device_->handleTunnelFrame( - data, len, [this](const uint8_t* response, size_t response_len) { - sendTunnelIndication(response, response_len); + data, len, [this, response_client](const uint8_t* response, size_t response_len) { + if (response_client != nullptr && response_client->connected) { + sendTunnelIndicationToClient(*response_client, response, response_len); + } else { + sendTunnelIndication(response, response_len); + } }); syncOpenKnxConfigFromDevice(); return consumed; @@ -2165,11 +3240,19 @@ bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number, return emitted; } +bool GatewayKnxTpIpRouter::shouldRouteDaliApplicationFrames() const { + if (!commissioning_only_) { + return true; + } + return openknx_configured_.load(); +} + void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() { if (ets_device_ == nullptr) { return; } const auto snapshot = ets_device_->snapshot(); + openknx_configured_.store(snapshot.configured); bool changed = false; GatewayKnxConfig updated = config_; if (snapshot.individual_address != 0 && snapshot.individual_address != 0xffff && diff --git a/components/gateway_modbus/src/gateway_modbus.cpp b/components/gateway_modbus/src/gateway_modbus.cpp index d2bed1a..f13daca 100644 --- a/components/gateway_modbus/src/gateway_modbus.cpp +++ b/components/gateway_modbus/src/gateway_modbus.cpp @@ -560,7 +560,7 @@ std::optional GatewayModbusConfigFromValue(const DaliValue* getObjectInt(json, "unitId").value_or(getObjectInt(json, "unit_id").value_or(1)))); if (const auto* serial_value = getObjectValue(json, "serial")) { if (const auto* serial = serial_value->asObject()) { - config.serial.uart_port = clampedInt(*serial, "uartPort", config.serial.uart_port, 0, 2); + config.serial.uart_port = clampedInt(*serial, "uartPort", config.serial.uart_port, -1, 2); config.serial.tx_pin = clampedInt(*serial, "txPin", config.serial.tx_pin, -1, 48); config.serial.rx_pin = clampedInt(*serial, "rxPin", config.serial.rx_pin, -1, 48); config.serial.baudrate = clampedU32(*serial, "baudrate", config.serial.baudrate, diff --git a/components/gateway_network/include/gateway_network.hpp b/components/gateway_network/include/gateway_network.hpp index 80ec218..5755d26 100644 --- a/components/gateway_network/include/gateway_network.hpp +++ b/components/gateway_network/include/gateway_network.hpp @@ -56,8 +56,10 @@ struct GatewayNetworkServiceConfig { bool status_led_active_high{true}; int boot_button_gpio{-1}; bool boot_button_active_low{true}; + int setup_ap_button_gpio{-1}; + bool setup_ap_button_active_low{true}; uint32_t boot_button_long_press_ms{3000}; - uint32_t boot_button_task_stack_size{2048}; + uint32_t boot_button_task_stack_size{8192}; UBaseType_t boot_button_task_priority{2}; uint32_t udp_task_stack_size{4096}; UBaseType_t udp_task_priority{4}; diff --git a/components/gateway_network/src/gateway_network.cpp b/components/gateway_network/src/gateway_network.cpp index c7e1146..6e76e35 100644 --- a/components/gateway_network/src/gateway_network.cpp +++ b/components/gateway_network/src/gateway_network.cpp @@ -909,22 +909,37 @@ esp_err_t GatewayNetworkService::configureStatusLed() { } esp_err_t GatewayNetworkService::configureBootButton() { - if (config_.boot_button_gpio < 0) { + if (config_.boot_button_gpio < 0 && config_.setup_ap_button_gpio < 0) { return ESP_OK; } - gpio_config_t io_config = {}; - io_config.pin_bit_mask = 1ULL << static_cast(config_.boot_button_gpio); - io_config.mode = GPIO_MODE_INPUT; - io_config.pull_up_en = config_.boot_button_active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; - io_config.pull_down_en = config_.boot_button_active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE; - io_config.intr_type = GPIO_INTR_DISABLE; - const esp_err_t err = gpio_config(&io_config); + const auto configure_input = [](int gpio, bool active_low, const char* name) -> esp_err_t { + if (gpio < 0) { + return ESP_OK; + } + gpio_config_t io_config = {}; + io_config.pin_bit_mask = 1ULL << static_cast(gpio); + io_config.mode = GPIO_MODE_INPUT; + io_config.pull_up_en = active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; + io_config.pull_down_en = active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE; + io_config.intr_type = GPIO_INTR_DISABLE; + const esp_err_t err = gpio_config(&io_config); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to configure %s GPIO%d: %s", name, gpio, esp_err_to_name(err)); + } + return err; + }; + + esp_err_t err = configure_input(config_.boot_button_gpio, config_.boot_button_active_low, + "Wi-Fi reset button"); if (err != ESP_OK) { - ESP_LOGE(kTag, "failed to configure boot button GPIO%d: %s", config_.boot_button_gpio, - esp_err_to_name(err)); + return err; } - return err; + if (config_.setup_ap_button_gpio == config_.boot_button_gpio) { + return ESP_OK; + } + return configure_input(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low, + "setup AP button"); } esp_err_t GatewayNetworkService::startHttpServer() { @@ -985,7 +1000,8 @@ esp_err_t GatewayNetworkService::startUdpTask() { } esp_err_t GatewayNetworkService::startBootButtonTask() { - if (config_.boot_button_gpio < 0 || boot_button_task_handle_ != nullptr) { + if ((config_.boot_button_gpio < 0 && config_.setup_ap_button_gpio < 0) || + boot_button_task_handle_ != nullptr) { return ESP_OK; } @@ -1349,39 +1365,88 @@ void GatewayNetworkService::bootButtonTaskLoop() { const TickType_t poll_ticks = pdMS_TO_TICKS(100); const uint32_t long_press_ms = std::max(config_.boot_button_long_press_ms, 100); - auto is_pressed = [this]() { - const int level = gpio_get_level(static_cast(config_.boot_button_gpio)); - return config_.boot_button_active_low ? level == 0 : level != 0; + auto is_pressed = [](int gpio, bool active_low) { + if (gpio < 0) { + return false; + } + const int level = gpio_get_level(static_cast(gpio)); + return active_low ? level == 0 : level != 0; }; - while (true) { - if (!is_pressed()) { - vTaskDelay(poll_ticks); - continue; - } - + auto wait_release = [&](int gpio, bool active_low) { uint32_t pressed_ms = 0; - while (is_pressed()) { + while (is_pressed(gpio, active_low)) { vTaskDelay(poll_ticks); pressed_ms += 100; } + return pressed_ms; + }; - if (pressed_ms >= long_press_ms) { - ESP_LOGW(kTag, "BOOT long press clears Wi-Fi credentials and restarts"); - runtime_.clearWirelessInfo(); - stopEspNow(); - if (wifi_started_) { - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); + auto enter_setup_ap = [this]() { + ESP_LOGI(kTag, "setup AP button enters setup AP mode"); + const uint32_t stack_size = std::max(config_.boot_button_task_stack_size, 8192); + const BaseType_t created = xTaskCreate( + [](void* arg) { + auto* service = static_cast(arg); + service->handleWifiControl(101); + vTaskDelete(nullptr); + }, + "gateway_setup_ap", stack_size, this, config_.boot_button_task_priority, nullptr); + if (created != pdPASS) { + ESP_LOGE(kTag, "failed to create setup AP task"); + } + }; + + auto clear_wifi_and_restart = [this]() { + ESP_LOGW(kTag, "Wi-Fi reset button clears Wi-Fi credentials and restarts"); + runtime_.clearWirelessInfo(); + stopEspNow(); + if (wifi_started_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); + } + vTaskDelay(pdMS_TO_TICKS(300)); + esp_restart(); + }; + + while (true) { + const bool same_button = config_.boot_button_gpio >= 0 && + config_.boot_button_gpio == config_.setup_ap_button_gpio; + if (same_button && is_pressed(config_.boot_button_gpio, config_.boot_button_active_low)) { + const uint32_t pressed_ms = wait_release(config_.boot_button_gpio, + config_.boot_button_active_low); + if (pressed_ms >= long_press_ms) { + clear_wifi_and_restart(); + } else { + enter_setup_ap(); } vTaskDelay(pdMS_TO_TICKS(300)); - esp_restart(); - } else { - ESP_LOGI(kTag, "BOOT short press enters setup AP mode"); - handleWifiControl(101); + continue; } - vTaskDelay(pdMS_TO_TICKS(300)); + if (is_pressed(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low)) { + wait_release(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low); + enter_setup_ap(); + vTaskDelay(pdMS_TO_TICKS(300)); + continue; + } + + if (is_pressed(config_.boot_button_gpio, config_.boot_button_active_low)) { + const uint32_t pressed_ms = wait_release(config_.boot_button_gpio, + config_.boot_button_active_low); + if (pressed_ms >= long_press_ms) { + clear_wifi_and_restart(); + } + vTaskDelay(pdMS_TO_TICKS(300)); + continue; + } + + if (config_.setup_ap_button_gpio < 0 && config_.boot_button_gpio < 0) { + vTaskDelete(nullptr); + return; + } + + vTaskDelay(poll_ticks); } } diff --git a/components/openknx_idf/include/openknx_idf/ets_device_runtime.h b/components/openknx_idf/include/openknx_idf/ets_device_runtime.h index 301b21d..aa9293c 100644 --- a/components/openknx_idf/include/openknx_idf/ets_device_runtime.h +++ b/components/openknx_idf/include/openknx_idf/ets_device_runtime.h @@ -29,11 +29,15 @@ class EtsDeviceRuntime { uint16_t individualAddress() const; uint16_t tunnelClientAddress() const; bool configured() const; + bool programmingMode() const; + void setProgrammingMode(bool enabled); + void toggleProgrammingMode(); EtsMemorySnapshot snapshot() const; void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, FunctionPropertyHandler state_handler); void setGroupWriteHandler(GroupWriteHandler handler); + void setNetworkInterface(esp_netif_t* netif); bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender); bool handleBusFrame(const uint8_t* data, size_t len); diff --git a/components/openknx_idf/src/esp_idf_platform.cpp b/components/openknx_idf/src/esp_idf_platform.cpp index 1e2afdb..439064e 100644 --- a/components/openknx_idf/src/esp_idf_platform.cpp +++ b/components/openknx_idf/src/esp_idf_platform.cpp @@ -20,12 +20,32 @@ namespace { constexpr const char* kTag = "openknx_idf"; constexpr const char* kEepromKey = "eeprom"; -esp_netif_t* findDefaultNetif() { - if (auto* sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")) { - return sta; +bool readBaseMac(uint8_t* data) { + if (data == nullptr) { + return false; } - if (auto* eth = esp_netif_get_handle_from_ifkey("ETH_DEF")) { - return eth; + if (esp_efuse_mac_get_default(data) == ESP_OK) { + return true; + } + return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK; +} + +esp_netif_t* findDefaultNetif() { + constexpr const char* kPreferredIfKeys[] = {"ETH_DEF", "WIFI_STA_DEF", "WIFI_AP_DEF"}; + for (const char* key : kPreferredIfKeys) { + auto* netif = esp_netif_get_handle_from_ifkey(key); + if (netif == nullptr || !esp_netif_is_netif_up(netif)) { + continue; + } + esp_netif_ip_info_t ip_info{}; + if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) { + return netif; + } + } + for (const char* key : kPreferredIfKeys) { + if (auto* netif = esp_netif_get_handle_from_ifkey(key)) { + return netif; + } } return nullptr; } @@ -103,7 +123,7 @@ void EspIdfPlatform::macAddress(uint8_t* data) { if (data == nullptr) { return; } - if (esp_read_mac(data, ESP_MAC_WIFI_STA) != ESP_OK) { + if (!readBaseMac(data)) { std::memset(data, 0, 6); } } @@ -111,7 +131,7 @@ void EspIdfPlatform::macAddress(uint8_t* data) { uint32_t EspIdfPlatform::uniqueSerialNumber() { uint8_t mac[6]{}; macAddress(mac); - return (static_cast(mac[0]) << 24) | (static_cast(mac[1]) << 16) | + return (static_cast(mac[2]) << 24) | (static_cast(mac[3]) << 16) | (static_cast(mac[4]) << 8) | mac[5]; } diff --git a/components/openknx_idf/src/ets_device_runtime.cpp b/components/openknx_idf/src/ets_device_runtime.cpp index 8b9a4b7..224cd3f 100644 --- a/components/openknx_idf/src/ets_device_runtime.cpp +++ b/components/openknx_idf/src/ets_device_runtime.cpp @@ -37,6 +37,13 @@ bool IsUsableIndividualAddress(uint16_t address) { return address != 0 && address != kInvalidIndividualAddress; } +bool IsErasedMemory(const uint8_t* data, size_t size) { + if (data == nullptr || size == 0) { + return true; + } + return std::all_of(data, data + size, [](uint8_t value) { return value == 0xff; }); +} + void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) { device.deviceObject().manufacturerId(kReg1DaliManufacturerId); device.deviceObject().bauNumber(platform.uniqueSerialNumber()); @@ -58,7 +65,11 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, if (IsUsableIndividualAddress(fallback_individual_address)) { device_.deviceObject().individualAddress(fallback_individual_address); } - device_.readMemory(); + const uint8_t* memory = platform_.getNonVolatileMemoryStart(); + const size_t memory_size = platform_.getNonVolatileMemorySize(); + if (!IsErasedMemory(memory, memory_size)) { + device_.readMemory(); + } if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) && IsUsableIndividualAddress(fallback_individual_address)) { device_.deviceObject().individualAddress(fallback_individual_address); @@ -99,6 +110,16 @@ uint16_t EtsDeviceRuntime::tunnelClientAddress() const { bool EtsDeviceRuntime::configured() const { return const_cast(device_).configured(); } +bool EtsDeviceRuntime::programmingMode() const { + return const_cast(device_).deviceObject().progMode(); +} + +void EtsDeviceRuntime::setProgrammingMode(bool enabled) { + device_.deviceObject().progMode(enabled); +} + +void EtsDeviceRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); } + EtsMemorySnapshot EtsDeviceRuntime::snapshot() const { EtsMemorySnapshot out; auto& device = const_cast(device_); @@ -139,6 +160,10 @@ void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) { group_write_handler_ = std::move(handler); } +void EtsDeviceRuntime::setNetworkInterface(esp_netif_t* netif) { + platform_.networkInterface(netif); +} + bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender) { auto* server = device_.getCemiServer(); @@ -289,6 +314,9 @@ bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const { case M_FuncPropStateRead_req: return true; case L_data_req: + if (!const_cast(device_).configured() || programmingMode()) { + return true; + } if (frame.addressType() == IndividualAddress && frame.destinationAddress() == individualAddress()) { return true; diff --git a/components/openknx_idf/src/security_storage.cpp b/components/openknx_idf/src/security_storage.cpp index 4a5165f..205705d 100644 --- a/components/openknx_idf/src/security_storage.cpp +++ b/components/openknx_idf/src/security_storage.cpp @@ -2,8 +2,8 @@ #include "esp_log.h" #include "esp_mac.h" -#include "esp_random.h" #include "esp_timer.h" +#include "mbedtls/sha256.h" #include "nvs.h" #include "nvs_flash.h" @@ -22,11 +22,13 @@ constexpr const char* kFactoryFdskKey = "factory_fdsk"; constexpr size_t kFdskSize = 16; constexpr size_t kSerialSize = 6; constexpr size_t kFdskQrSize = 36; +constexpr uint16_t kKnxManufacturerId = 0x00A4; constexpr const char* kProductIdentity = "REG1-Dali"; constexpr const char* kManufacturerId = "00A4"; constexpr const char* kApplicationNumber = "01"; constexpr const char* kApplicationVersion = "05"; -constexpr const char* kDevelopmentStorage = "plain_nvs_development"; +constexpr const char* kDevelopmentStorage = "base_mac_derived_plain_nvs_development"; +constexpr char kFdskDerivationLabel[] = "DaliMaster REG1-Dali deterministic FDSK v1"; constexpr uint8_t kCrc4Tab[16] = { 0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9, 0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2, @@ -57,10 +59,14 @@ bool plausibleKey(const uint8_t* data) { return !all_zero && !all_ff; } -void generateKey(uint8_t* data) { - do { - esp_fill_random(data, kFdskSize); - } while (!plausibleKey(data)); +bool readBaseMac(uint8_t* data) { + if (data == nullptr) { + return false; + } + if (esp_efuse_mac_get_default(data) == ESP_OK) { + return true; + } + return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK; } void clearOpenKnxFdskCache() { @@ -108,16 +114,60 @@ bool parseHexKey(const std::string& value, uint8_t* out) { return plausibleKey(out); } -bool storeFactoryFdsk(const uint8_t* data) { - if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) { +bool loadKnxSerialNumber(uint8_t* serial) { + if (serial == nullptr) { return false; } + std::array mac{}; + if (!readBaseMac(mac.data())) { + return false; + } + + serial[0] = static_cast((kKnxManufacturerId >> 8) & 0xff); + serial[1] = static_cast(kKnxManufacturerId & 0xff); + std::copy(mac.begin() + 2, mac.end(), serial + 2); + return true; +} + +bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) { + if (serial == nullptr || key == nullptr) { + return false; + } + std::array material{}; + std::copy(kFdskDerivationLabel, kFdskDerivationLabel + sizeof(kFdskDerivationLabel) - 1, + material.begin()); + std::copy(serial, serial + kSerialSize, material.begin() + sizeof(kFdskDerivationLabel) - 1); + + std::array digest{}; + if (mbedtls_sha256(material.data(), material.size(), digest.data(), 0) != 0) { + return false; + } + std::copy(digest.begin(), digest.begin() + kFdskSize, key); + if (!plausibleKey(key)) { + key[kFdskSize - 1] ^= 0xA5; + } + return plausibleKey(key); +} + +void syncFactoryFdskToNvs(const uint8_t* data) { + if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) { + return; + } + + std::array stored{}; + size_t stored_size = stored.size(); nvs_handle_t handle = 0; esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err)); - return false; + return; + } + err = nvs_get_blob(handle, kFactoryFdskKey, stored.data(), &stored_size); + if (err == ESP_OK && stored_size == stored.size() && + std::equal(stored.begin(), stored.end(), data)) { + nvs_close(handle); + return; } err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize); if (err == ESP_OK) { @@ -125,11 +175,10 @@ bool storeFactoryFdsk(const uint8_t* data) { } nvs_close(handle); if (err != ESP_OK) { - ESP_LOGW(kTag, "failed to store KNX factory FDSK: %s", esp_err_to_name(err)); - return false; + ESP_LOGW(kTag, "failed to mirror deterministic KNX factory FDSK: %s", esp_err_to_name(err)); + return; } clearOpenKnxFdskCache(); - return true; } uint8_t crc4Array(const uint8_t* data, size_t len) { @@ -219,35 +268,18 @@ std::string fnv1aHex(const std::string& value) { namespace gateway::openknx { bool LoadFactoryFdsk(uint8_t* data, size_t len) { - if (data == nullptr || len < kFdskSize || !ensureNvsReady()) { + if (data == nullptr || len < kFdskSize) { return false; } - nvs_handle_t handle = 0; - esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle); - if (err != ESP_OK) { - ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err)); - return false; - } - - size_t stored_size = kFdskSize; - err = nvs_get_blob(handle, kFactoryFdskKey, data, &stored_size); - if (err == ESP_OK && stored_size == kFdskSize && plausibleKey(data)) { - nvs_close(handle); - return true; - } - - generateKey(data); - err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize); - if (err == ESP_OK) { - err = nvs_commit(handle); - } - nvs_close(handle); - - if (err != ESP_OK) { - ESP_LOGW(kTag, "failed to store generated KNX factory FDSK: %s", esp_err_to_name(err)); + std::array serial{}; + std::array key{}; + if (!loadKnxSerialNumber(serial.data()) || + !deriveFactoryFdskFromSerial(serial.data(), key.data())) { return false; } + std::memcpy(data, key.data(), kFdskSize); + syncFactoryFdskToNvs(key.data()); return true; } @@ -255,8 +287,7 @@ FactoryFdskInfo LoadFactoryFdskInfo() { FactoryFdskInfo info; std::array key{}; std::array serial{}; - if (!LoadFactoryFdsk(key.data(), key.size()) || - esp_read_mac(serial.data(), ESP_MAC_WIFI_STA) != ESP_OK) { + if (!loadKnxSerialNumber(serial.data()) || !LoadFactoryFdsk(key.data(), key.size())) { return info; } @@ -269,8 +300,7 @@ FactoryFdskInfo LoadFactoryFdskInfo() { bool GenerateFactoryFdsk(FactoryFdskInfo* info) { std::array key{}; - generateKey(key.data()); - const bool stored = storeFactoryFdsk(key.data()); + const bool stored = LoadFactoryFdsk(key.data(), key.size()); std::fill(key.begin(), key.end(), 0); if (!stored) { return false; @@ -286,8 +316,16 @@ bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) { if (!parseHexKey(hex_key, key.data())) { return false; } - const bool stored = storeFactoryFdsk(key.data()); + std::array serial{}; + std::array derived{}; + const bool stored = loadKnxSerialNumber(serial.data()) && + deriveFactoryFdskFromSerial(serial.data(), derived.data()) && + std::equal(key.begin(), key.end(), derived.begin()); + if (stored) { + syncFactoryFdskToNvs(derived.data()); + } std::fill(key.begin(), key.end(), 0); + std::fill(derived.begin(), derived.end(), 0); if (!stored) { return false; }