3 Commits

Author SHA1 Message Date
Tony 626f86ec4e feat: add support for W5500 SPI Ethernet in gateway
- Introduced configuration options for wired Ethernet support in Kconfig and sdkconfig.
- Implemented Ethernet initialization and event handling in GatewayNetworkService.
- Enhanced app_main to manage Ethernet alongside Wi-Fi.
- Updated GatewayRuntime to store Ethernet information.
- Modified CMakeLists and include files to accommodate new Ethernet dependencies.
- Ensured backward compatibility by allowing Ethernet initialization failures to be ignored.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-12 08:42:10 +08:00
Tony 36d10702da Add EtsDeviceRuntime class for handling KNX device runtime operations
- Introduced EtsDeviceRuntime class to manage device runtime functionalities including handling tunnel frames and function property commands.
- Added support for individual address management and memory snapshot retrieval.
- Updated EtsMemorySnapshot structure to include individual address.
- Implemented identity application for DALI devices in the memory loader.
- Enhanced CMakeLists.txt to include new source files and compile definitions.
- Updated header files to include new dependencies and declarations.
- Refactored existing memory loading logic to accommodate new device runtime features.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-12 05:19:14 +08:00
Tony d231460612 fix(git): update .gitignore to include .DS_Store and change knx submodule branch to tonycloud-dev
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-11 12:16:48 +08:00
24 changed files with 1636 additions and 59 deletions
+1
View File
@@ -1,3 +1,4 @@
**/build/
build/
**/managed_components/
.DS_Store
+1 -1
View File
@@ -8,7 +8,7 @@
[submodule "knx_dali_gw"]
path = knx_dali_gw
url = https://git.tonycloud.org/knx/GW-REG1-Dali.git
branch = v1
branch = tonycloud-dev
[submodule "tpuart"]
path = tpuart
url = https://git.tonycloud.org/knx/tpuart.git
+3 -3
View File
@@ -12,19 +12,19 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
- `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks.
- `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges.
- `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions.
- `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework.
- `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, ETS cEMI programming support, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework.
- `gateway_modbus/`: gateway-owned Modbus TCP/RTU/ASCII config, generated DALI point tables, and provisioned Modbus model override dispatch.
- `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, 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, and BOOT-button Wi-Fi reset 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.
## 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`, 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 is enabled by default, 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, 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.
## Modbus
+107 -4
View File
@@ -380,6 +380,109 @@ config GATEWAY_SMARTCONFIG_TIMEOUT_SEC
help
Timeout passed to ESP-IDF smartconfig before provisioning restarts internally.
config GATEWAY_ETHERNET_SUPPORTED
bool "Wired Ethernet gateway transport is supported"
default y
select ETH_USE_SPI_ETHERNET
select ETH_SPI_ETHERNET_W5500
help
Enables the ESP-IDF Ethernet driver path for wired gateway networking. The
native gateway currently provisions a W5500 SPI Ethernet controller and
exposes the same HTTP, UDP, KNXnet/IP, BACnet/IP, Modbus TCP, and cloud
services over the wired netif.
config GATEWAY_START_ETHERNET_ENABLED
bool "Start wired Ethernet at startup"
depends on GATEWAY_ETHERNET_SUPPORTED
default y
help
Starts the configured W5500 Ethernet netif during boot and uses DHCP for
address assignment. Disable this when the board is built without Ethernet
hardware even though the firmware keeps Ethernet support compiled in.
config GATEWAY_ETHERNET_IGNORE_INIT_FAILURE
bool "Ignore wired Ethernet init failures"
depends on GATEWAY_START_ETHERNET_ENABLED
default y
help
Continues booting if the W5500 Ethernet controller is missing, held in
reset, miswired, or otherwise fails ESP-IDF Ethernet driver startup.
Disable this for strict hardware bring-up where Ethernet failure should
abort application startup.
menu "Gateway Wired Ethernet"
depends on GATEWAY_ETHERNET_SUPPORTED
config GATEWAY_ETHERNET_W5500_SPI_HOST
int "W5500 SPI host number"
range 1 2
default 1
help
SPI host used for the W5500 Ethernet controller. On ESP32-S3, host 1 maps
to SPI2 and host 2 maps to SPI3; do not use host 0 because it is reserved
by flash/PSRAM.
config GATEWAY_ETHERNET_W5500_SCLK_GPIO
int "W5500 SPI SCLK GPIO"
range 0 48
default 14
config GATEWAY_ETHERNET_W5500_MOSI_GPIO
int "W5500 SPI MOSI GPIO"
range 0 48
default 13
config GATEWAY_ETHERNET_W5500_MISO_GPIO
int "W5500 SPI MISO GPIO"
range 0 48
default 12
config GATEWAY_ETHERNET_W5500_CS_GPIO
int "W5500 SPI CS GPIO"
range 0 48
default 15
config GATEWAY_ETHERNET_W5500_INT_GPIO
int "W5500 interrupt GPIO"
range -1 48
default 4
help
W5500 interrupt pin. Set to -1 to disable interrupt mode and poll RX
status periodically.
config GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS
int "W5500 polling period ms"
range 0 1000
default 0
help
Polling interval used when the W5500 interrupt pin is disabled. A value of
0 keeps interrupt mode when an interrupt GPIO is configured; if the
interrupt GPIO is -1, the gateway falls back to 100 ms polling.
config GATEWAY_ETHERNET_W5500_CLOCK_MHZ
int "W5500 SPI clock MHz"
range 5 80
default 36
config GATEWAY_ETHERNET_PHY_RESET_GPIO
int "Ethernet PHY reset GPIO"
range -1 48
default 5
help
GPIO used to reset the W5500 PHY. Set to -1 to disable hardware reset.
config GATEWAY_ETHERNET_PHY_ADDR
int "Ethernet PHY address"
range 0 31
default 1
config GATEWAY_ETHERNET_RX_TASK_STACK_SIZE
int "Ethernet RX task stack bytes"
range 2048 8192
default 3072
endmenu
config GATEWAY_BRIDGE_SUPPORTED
bool "dali_cpp bridge runtime is supported"
default y
@@ -411,7 +514,7 @@ choice GATEWAY_MODBUS_DEFAULT_TRANSPORT
config GATEWAY_MODBUS_DEFAULT_TRANSPORT_TCP
bool "TCP server"
depends on GATEWAY_WIFI_SUPPORTED
depends on GATEWAY_WIFI_SUPPORTED || GATEWAY_ETHERNET_SUPPORTED
config GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU
bool "RTU server on UART"
@@ -488,7 +591,7 @@ config GATEWAY_MODBUS_SERIAL_RS485_DE_PIN
config GATEWAY_BACNET_BRIDGE_SUPPORTED
bool "BACnet/IP bridge is supported"
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
depends on GATEWAY_BRIDGE_SUPPORTED && (GATEWAY_WIFI_SUPPORTED || GATEWAY_ETHERNET_SUPPORTED)
default n
help
Enables BACnet bridge configuration, binding discovery, and the bacnet-stack BACnet/IP server adapter.
@@ -503,7 +606,7 @@ config GATEWAY_START_BACNET_BRIDGE_ENABLED
config GATEWAY_KNX_BRIDGE_SUPPORTED
bool "KNX to DALI bridge is supported"
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
depends on GATEWAY_BRIDGE_SUPPORTED && (GATEWAY_WIFI_SUPPORTED || GATEWAY_ETHERNET_SUPPORTED)
default n
help
Enables the gateway-owned KNX group-address router and KNXnet/IP TP/IP
@@ -595,7 +698,7 @@ config GATEWAY_BRIDGE_KNX_TASK_PRIORITY
config GATEWAY_CLOUD_BRIDGE_SUPPORTED
bool "MQTT cloud bridge is supported"
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
depends on GATEWAY_BRIDGE_SUPPORTED && (GATEWAY_WIFI_SUPPORTED || GATEWAY_ETHERNET_SUPPORTED)
default y
help
Enables per-channel DaliCloudBridge provisioning and MQTT downlink execution.
+89 -8
View File
@@ -96,6 +96,54 @@
#define CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC 60
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE
#define CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE 0
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST
#define CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST 1
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO
#define CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO 14
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO
#define CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO 13
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO
#define CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO 12
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO
#define CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO 15
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO
#define CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO 4
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS
#define CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS 0
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ
#define CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ 36
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO
#define CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO 5
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_PHY_ADDR
#define CONFIG_GATEWAY_ETHERNET_PHY_ADDR 1
#endif
#ifndef CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE
#define CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE 3072
#endif
#ifndef CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE
#define CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE 6144
#endif
@@ -199,12 +247,24 @@ constexpr bool kWifiSupported = true;
constexpr bool kWifiSupported = false;
#endif
#ifdef CONFIG_GATEWAY_ETHERNET_SUPPORTED
constexpr bool kEthernetSupported = true;
#else
constexpr bool kEthernetSupported = false;
#endif
#ifdef CONFIG_GATEWAY_START_WIFI_STA_ENABLED
constexpr bool kWifiStartupEnabled = true;
#else
constexpr bool kWifiStartupEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_START_ETHERNET_ENABLED
constexpr bool kEthernetStartupEnabled = true;
#else
constexpr bool kEthernetStartupEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_BLE_SUPPORTED
constexpr bool kBleSupported = true;
#else
@@ -642,7 +702,7 @@ extern "C" void app_main(void) {
"gateway",
kWifiSupported,
kBleSupported,
true,
kEthernetSupported,
kEspnowSetupSupported,
kUsbSetupStartupEnabled,
};
@@ -678,10 +738,11 @@ extern "C" void app_main(void) {
ESP_ERROR_CHECK(s_cache->start());
gateway::GatewayControllerConfig controller_config;
const bool network_transport_supported = profile.enable_wifi || profile.enable_eth;
controller_config.setup_supported = true;
controller_config.ble_supported = profile.enable_ble;
controller_config.wifi_supported = profile.enable_wifi;
controller_config.ip_router_supported = profile.enable_wifi || profile.enable_eth;
controller_config.ip_router_supported = network_transport_supported;
controller_config.internal_scene_supported = true;
controller_config.internal_group_supported = true;
@@ -715,14 +776,14 @@ extern "C" void app_main(void) {
bridge_config.bridge_enabled = true;
bridge_config.modbus_enabled = kModbusBridgeSupported;
bridge_config.modbus_startup_enabled = kModbusBridgeSupported && kModbusBridgeStartupEnabled;
bridge_config.bacnet_enabled = profile.enable_wifi && kBacnetBridgeSupported;
bridge_config.bacnet_startup_enabled = profile.enable_wifi && kBacnetBridgeSupported &&
bridge_config.bacnet_enabled = network_transport_supported && kBacnetBridgeSupported;
bridge_config.bacnet_startup_enabled = network_transport_supported && kBacnetBridgeSupported &&
kBacnetBridgeStartupEnabled;
bridge_config.knx_enabled = profile.enable_wifi && kKnxBridgeSupported;
bridge_config.knx_startup_enabled = profile.enable_wifi && kKnxBridgeSupported &&
bridge_config.knx_enabled = network_transport_supported && kKnxBridgeSupported;
bridge_config.knx_startup_enabled = network_transport_supported && kKnxBridgeSupported &&
kKnxBridgeStartupEnabled;
bridge_config.cloud_enabled = profile.enable_wifi && kCloudBridgeSupported;
bridge_config.cloud_startup_enabled = profile.enable_wifi && kCloudBridgeSupported &&
bridge_config.cloud_enabled = network_transport_supported && kCloudBridgeSupported;
bridge_config.cloud_startup_enabled = network_transport_supported && kCloudBridgeSupported &&
kCloudBridgeStartupEnabled;
bridge_config.modbus_task_stack_size =
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE);
@@ -796,6 +857,12 @@ extern "C" void app_main(void) {
if (profile.enable_wifi || profile.enable_eth) {
gateway::GatewayNetworkServiceConfig network_config;
network_config.wifi_enabled = profile.enable_wifi && kWifiStartupEnabled;
network_config.ethernet_enabled = profile.enable_eth && kEthernetStartupEnabled;
#if CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE
network_config.ethernet_ignore_init_failure = true;
#else
network_config.ethernet_ignore_init_failure = false;
#endif
network_config.espnow_setup_enabled = profile.enable_espnow;
network_config.espnow_setup_startup_enabled =
profile.enable_espnow && kEspnowSetupStartupEnabled;
@@ -815,6 +882,20 @@ extern "C" void app_main(void) {
#endif
network_config.http_port = static_cast<uint16_t>(CONFIG_GATEWAY_NETWORK_HTTP_PORT);
network_config.udp_port = static_cast<uint16_t>(CONFIG_GATEWAY_NETWORK_UDP_PORT);
network_config.ethernet_spi_host = CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST;
network_config.ethernet_spi_sclk_gpio = CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO;
network_config.ethernet_spi_mosi_gpio = CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO;
network_config.ethernet_spi_miso_gpio = CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO;
network_config.ethernet_spi_cs_gpio = CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO;
network_config.ethernet_spi_int_gpio = CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO;
network_config.ethernet_poll_period_ms =
static_cast<uint32_t>(CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS);
network_config.ethernet_spi_clock_mhz =
static_cast<uint8_t>(CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ);
network_config.ethernet_phy_reset_gpio = CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO;
network_config.ethernet_phy_addr = CONFIG_GATEWAY_ETHERNET_PHY_ADDR;
network_config.ethernet_rx_task_stack_size =
static_cast<uint32_t>(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.boot_button_long_press_ms = CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS;
+35 -11
View File
@@ -656,6 +656,26 @@ CONFIG_GATEWAY_SMARTCONFIG_SUPPORTED=y
# CONFIG_GATEWAY_START_ESPNOW_SETUP_ENABLED is not set
# CONFIG_GATEWAY_START_SMARTCONFIG_ENABLED is not set
CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC=60
CONFIG_GATEWAY_ETHERNET_SUPPORTED=y
CONFIG_GATEWAY_START_ETHERNET_ENABLED=y
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_POLL_PERIOD_MS=0
CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=36
CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=5
CONFIG_GATEWAY_ETHERNET_PHY_ADDR=1
CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=3072
# end of Gateway Wired Ethernet
CONFIG_GATEWAY_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_MODBUS_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_MODBUS_BRIDGE_ENABLED is not set
@@ -666,7 +686,20 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
# CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED is not set
CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y
CONFIG_GATEWAY_KNX_MAIN_GROUP=0
CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y
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_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_PRIORITY=5
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144
@@ -675,16 +708,7 @@ CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE=8192
CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY=5
CONFIG_GATEWAY_USB_STARTUP_DEBUG_JTAG=y
# CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set
CONFIG_GATEWAY_485_CONTROL_ENABLED=y
CONFIG_GATEWAY_485_CONTROL_BAUDRATE=9600
CONFIG_GATEWAY_485_CONTROL_TX_PIN=-1
CONFIG_GATEWAY_485_CONTROL_RX_PIN=-1
CONFIG_GATEWAY_485_CONTROL_RX_BUFFER=256
CONFIG_GATEWAY_485_CONTROL_TX_BUFFER=256
CONFIG_GATEWAY_485_CONTROL_READ_TIMEOUT_MS=20
CONFIG_GATEWAY_485_CONTROL_WRITE_TIMEOUT_MS=20
CONFIG_GATEWAY_485_CONTROL_TASK_STACK_SIZE=4096
CONFIG_GATEWAY_485_CONTROL_TASK_PRIORITY=4
# CONFIG_GATEWAY_485_CONTROL_ENABLED is not set
# end of Gateway Startup Services
#
+4 -1
View File
@@ -12,4 +12,7 @@ CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_ETH_ENABLED=y
CONFIG_ETH_USE_SPI_ETHERNET=y
CONFIG_ETH_SPI_ETHERNET_W5500=y
CONFIG_ETH_SPI_ETHERNET_W5500=y
CONFIG_GATEWAY_ETHERNET_SUPPORTED=y
CONFIG_GATEWAY_START_ETHERNET_ENABLED=y
CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y
+7 -8
View File
@@ -666,6 +666,7 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
# CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED is not set
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144
@@ -744,6 +745,7 @@ CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y
#
# CONFIG_APPTRACE_DEST_JTAG is not set
CONFIG_APPTRACE_DEST_NONE=y
# CONFIG_APPTRACE_DEST_UART0 is not set
# CONFIG_APPTRACE_DEST_UART1 is not set
# CONFIG_APPTRACE_DEST_UART2 is not set
# CONFIG_APPTRACE_DEST_USB_CDC is not set
@@ -1787,18 +1789,15 @@ CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y
# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set
CONFIG_ESP_MAIN_TASK_AFFINITY=0x0
CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
# CONFIG_ESP_CONSOLE_USB_CDC is not set
# CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG is not set
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set
# CONFIG_ESP_CONSOLE_NONE is not set
# CONFIG_ESP_CONSOLE_SECONDARY_NONE is not set
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y
CONFIG_ESP_CONSOLE_UART=y
CONFIG_ESP_CONSOLE_UART_NUM=0
CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
CONFIG_ESP_CONSOLE_UART_NUM=-1
CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=4
CONFIG_ESP_INT_WDT=y
CONFIG_ESP_INT_WDT_TIMEOUT_MS=300
CONFIG_ESP_INT_WDT_CHECK_CPU1=y
@@ -1276,7 +1276,8 @@ struct GatewayBridgeService::ChannelRuntime {
return result;
}
return knx->handleCemiFrame(data, len);
});
},
openKnxNamespace());
if (knx_config.has_value()) {
knx->setConfig(knx_config.value());
knx_router->setConfig(knx_config.value());
@@ -1308,10 +1309,15 @@ struct GatewayBridgeService::ChannelRuntime {
return;
}
const auto snapshot = openknx::LoadEtsMemorySnapshot(openKnxNamespace());
if (snapshot.associations.empty()) {
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) {
@@ -1319,9 +1325,9 @@ struct GatewayBridgeService::ChannelRuntime {
association.group_address, association.group_object_number});
}
knx_config = std::move(updated);
ESP_LOGI(kTag, "gateway=%u loaded %u OpenKNX ETS associations from NVS namespace %s",
channel.gateway_id, static_cast<unsigned>(snapshot.associations.size()),
openKnxNamespace().c_str());
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<unsigned>(snapshot.associations.size()), openKnxNamespace().c_str());
}
std::optional<DaliDomainSnapshot> diagnosticSnapshotLocked(int short_address,
+52 -1
View File
@@ -13,12 +13,17 @@
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace gateway {
namespace openknx {
class EtsDeviceRuntime;
}
constexpr uint16_t kGatewayKnxDefaultUdpPort = 3671;
constexpr const char* kGatewayKnxDefaultMulticastAddress = "224.0.23.12";
constexpr uint32_t kGatewayKnxDefaultTpBaudrate = 19200;
@@ -96,6 +101,13 @@ struct GatewayKnxDaliBinding {
GatewayKnxDaliTarget target;
};
struct GatewayKnxCommissioningBallast {
uint8_t high{0};
uint8_t middle{0};
uint8_t low{0};
uint8_t short_address{0xff};
};
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
@@ -122,6 +134,12 @@ class GatewayKnxBridge {
DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len);
DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len);
bool handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
private:
DaliBridgeResult executeForDecodedWrite(uint16_t group_address,
@@ -133,16 +151,43 @@ class GatewayKnxBridge {
const uint8_t* data, size_t len);
void rebuildEtsBindings();
bool handleReg1TypeCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1ScanCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1AssignCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1EvgWriteCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1EvgReadCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1SetSceneCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1GetSceneCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1IdentifyCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1ScanState(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1AssignState(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleReg1FoundEvgsState(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
DaliBridgeEngine& engine_;
GatewayKnxConfig config_;
std::map<uint16_t, std::vector<GatewayKnxDaliBinding>> ets_bindings_by_group_address_;
bool commissioning_scan_done_{true};
bool commissioning_assign_done_{true};
std::vector<GatewayKnxCommissioningBallast> commissioning_found_ballasts_;
};
class GatewayKnxTpIpRouter {
public:
using CemiFrameHandler = std::function<DaliBridgeResult(const uint8_t* data, size_t len)>;
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler);
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler,
std::string openknx_namespace = "openknx");
~GatewayKnxTpIpRouter();
void setConfig(const GatewayKnxConfig& config);
@@ -179,6 +224,10 @@ class GatewayKnxTpIpRouter {
void sendConnectResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
void sendRoutingIndication(const uint8_t* data, size_t len);
bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len);
void syncOpenKnxConfigFromDevice();
uint16_t effectiveIndividualAddress() const;
uint16_t effectiveTunnelAddress() const;
void pollTpUart();
void handleTpUartControlByte(uint8_t byte);
void handleTpTelegram(const uint8_t* data, size_t len);
@@ -186,7 +235,9 @@ class GatewayKnxTpIpRouter {
GatewayKnxBridge& bridge_;
CemiFrameHandler handler_;
std::string openknx_namespace_;
GatewayKnxConfig config_;
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
TaskHandle_t task_handle_{nullptr};
std::atomic_bool stop_requested_{false};
std::atomic_bool started_{false};
+618 -6
View File
@@ -1,9 +1,11 @@
#include "gateway_knx.hpp"
#include "dali_define.hpp"
#include "driver/uart.h"
#include "esp_log.h"
#include "lwip/inet.h"
#include "lwip/sockets.h"
#include "openknx_idf/ets_device_runtime.h"
#include <algorithm>
#include <array>
@@ -62,6 +64,20 @@ constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2;
constexpr uint8_t kGwReg1KoSwitch = 0;
constexpr uint8_t kGwReg1KoDimmAbsolute = 3;
constexpr uint8_t kGwReg1KoColor = 6;
constexpr uint8_t kReg1DaliFunctionObjectIndex = 160;
constexpr uint8_t kReg1DaliFunctionPropertyId = 1;
constexpr uint8_t kReg1FunctionType = 2;
constexpr uint8_t kReg1FunctionScan = 3;
constexpr uint8_t kReg1FunctionAssign = 4;
constexpr uint8_t kReg1FunctionEvgWrite = 10;
constexpr uint8_t kReg1FunctionEvgRead = 11;
constexpr uint8_t kReg1FunctionSetScene = 12;
constexpr uint8_t kReg1FunctionGetScene = 13;
constexpr uint8_t kReg1FunctionIdentify = 14;
constexpr uint8_t kReg1DeviceTypeDt8 = 8;
constexpr uint8_t kReg1ColorTypeTw = 1;
constexpr uint8_t kDaliDeviceTypeNone = 0xfe;
constexpr uint8_t kDaliDeviceTypeMultiple = 0xff;
struct DecodedGroupWrite {
uint16_t group_address{0};
@@ -293,6 +309,88 @@ std::optional<DecodedGroupWrite> DecodeCemiGroupWrite(const uint8_t* data, size_
return out;
}
uint8_t Reg1PercentToArc(uint8_t value) {
if (value == 0 || value == 0xff) {
return value;
}
const double arc = ((253.0 / 3.0) * (std::log10(static_cast<double>(value)) + 1.0)) + 1.0;
return static_cast<uint8_t>(std::clamp(static_cast<int>(arc + 0.5), 0, 254));
}
uint8_t Reg1ArcToPercent(uint8_t value) {
if (value == 0 || value == 0xff) {
return value;
}
const double percent = std::pow(10.0, ((static_cast<double>(value) - 1.0) / (253.0 / 3.0)) - 1.0);
return static_cast<uint8_t>(std::clamp(static_cast<int>(percent + 0.5), 0, 100));
}
GatewayKnxDaliTarget Reg1SceneTarget(uint8_t encoded_target) {
if ((encoded_target & 0x80) != 0) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
static_cast<int>(encoded_target & 0x0f)};
}
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
static_cast<int>(encoded_target & 0x3f)};
}
DaliBridgeRequest FunctionRequest(const char* sequence, BridgeOperation operation) {
DaliBridgeRequest request;
request.sequence = sequence == nullptr ? "knx-function-property" : sequence;
request.operation = operation;
return request;
}
void ApplyTargetToRequest(const GatewayKnxDaliTarget& target, DaliBridgeRequest* request) {
if (request == nullptr) {
return;
}
switch (target.kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
request->metadata["broadcast"] = true;
break;
case GatewayKnxDaliTargetKind::kShortAddress:
request->shortAddress = target.address;
break;
case GatewayKnxDaliTargetKind::kGroup:
request->metadata["group"] = target.address;
break;
case GatewayKnxDaliTargetKind::kNone:
default:
break;
}
}
DaliBridgeResult ExecuteRaw(DaliBridgeEngine& engine, BridgeOperation operation, uint8_t addr,
uint8_t cmd, const char* sequence) {
DaliBridgeRequest request = FunctionRequest(sequence, operation);
request.rawAddress = addr;
request.rawCommand = cmd;
return engine.execute(request);
}
std::optional<int> QueryShort(DaliBridgeEngine& engine, uint8_t short_address, uint8_t command,
const char* sequence) {
const auto result = ExecuteRaw(engine, BridgeOperation::query, DaliComm::toCmdAddr(short_address),
command, sequence);
if (!result.ok || !result.data.has_value()) {
return std::nullopt;
}
return result.data.value();
}
bool SendRaw(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) {
return ExecuteRaw(engine, BridgeOperation::send, addr, cmd, sequence).ok;
}
bool SendRawExt(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) {
return ExecuteRaw(engine, BridgeOperation::sendExt, addr, cmd, sequence).ok;
}
std::optional<int> MetadataInt(const DaliBridgeResult& result, const std::string& key) {
return getObjectInt(result.metadata, key);
}
DaliBridgeRequest RequestForTarget(uint16_t group_address,
const GatewayKnxDaliTarget& target,
BridgeOperation operation) {
@@ -923,6 +1021,421 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons
return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len);
}
bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId ||
data == nullptr || len == 0 || response == nullptr) {
return false;
}
switch (data[0]) {
case kReg1FunctionType:
return handleReg1TypeCommand(data, len, response);
case kReg1FunctionScan:
return handleReg1ScanCommand(data, len, response);
case kReg1FunctionAssign:
return handleReg1AssignCommand(data, len, response);
case kReg1FunctionEvgWrite:
return handleReg1EvgWriteCommand(data, len, response);
case kReg1FunctionEvgRead:
return handleReg1EvgReadCommand(data, len, response);
case kReg1FunctionSetScene:
return handleReg1SetSceneCommand(data, len, response);
case kReg1FunctionGetScene:
return handleReg1GetSceneCommand(data, len, response);
case kReg1FunctionIdentify:
return handleReg1IdentifyCommand(data, len, response);
default:
return false;
}
}
bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId ||
data == nullptr || len == 0 || response == nullptr) {
return false;
}
switch (data[0]) {
case kReg1FunctionScan:
case 5:
return handleReg1ScanState(data, len, response);
case kReg1FunctionAssign:
return handleReg1AssignState(data, len, response);
case 7:
return handleReg1FoundEvgsState(data, len, response);
default:
return false;
}
}
bool GatewayKnxBridge::handleReg1TypeCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 2 || response == nullptr) {
return false;
}
const uint8_t short_address = data[1];
const auto type_response = QueryShort(engine_, short_address, DALI_CMD_QUERY_DEVICE_TYPE,
"knx-function-type");
if (!type_response.has_value()) {
*response = {0x01};
return true;
}
uint8_t device_type = static_cast<uint8_t>(type_response.value());
if (device_type == kDaliDeviceTypeMultiple) {
for (int index = 0; index < 16; ++index) {
const auto next_type = QueryShort(engine_, short_address, DALI_CMD_QUERY_NEXT_DEVICE_TYPE,
"knx-function-next-device-type");
if (!next_type.has_value()) {
*response = {0x01};
return true;
}
if (next_type.value() == kDaliDeviceTypeNone) {
break;
}
if (next_type.value() < 20) {
device_type = static_cast<uint8_t>(next_type.value());
}
}
}
*response = {0x00, device_type};
if (device_type == kReg1DeviceTypeDt8) {
if (!SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
"knx-function-dt8-select")) {
*response = {0x02};
return true;
}
const auto color_features = QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_TYPE,
"knx-function-color-type");
if (!color_features.has_value()) {
*response = {0x02};
return true;
}
response->push_back(static_cast<uint8_t>(color_features.value()));
}
return true;
}
bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 5 || response == nullptr) {
return false;
}
commissioning_scan_done_ = false;
commissioning_found_ballasts_.clear();
const bool delete_all = data[3] == 1;
const bool assign = data[4] == 1;
if (assign || delete_all) {
DaliBridgeRequest allocate = FunctionRequest(
"knx-function-scan-allocate",
delete_all ? BridgeOperation::resetAndAllocateShortAddresses
: BridgeOperation::allocateAllShortAddresses);
allocate.value = DaliValue::Object{{"start", 0}, {"removeAddrFirst", delete_all}};
engine_.execute(allocate);
}
DaliBridgeRequest search = FunctionRequest("knx-function-scan-search", BridgeOperation::searchAddressRange);
search.value = DaliValue::Object{{"start", 0}, {"end", 63}};
const auto search_result = engine_.execute(search);
if (search_result.ok) {
if (const auto* addresses_value = getObjectValue(search_result.metadata, "addresses")) {
if (const auto* addresses = addresses_value->asArray()) {
for (const auto& address_value : *addresses) {
const auto short_address = address_value.asInt();
if (!short_address.has_value() || short_address.value() < 0 || short_address.value() > 63) {
continue;
}
GatewayKnxCommissioningBallast ballast;
ballast.short_address = static_cast<uint8_t>(short_address.value());
ballast.high = static_cast<uint8_t>(
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_H,
"knx-function-scan-rand-h")
.value_or(0));
ballast.middle = static_cast<uint8_t>(
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_M,
"knx-function-scan-rand-m")
.value_or(0));
ballast.low = static_cast<uint8_t>(
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_L,
"knx-function-scan-rand-l")
.value_or(0));
commissioning_found_ballasts_.push_back(ballast);
}
}
}
}
commissioning_scan_done_ = true;
response->clear();
return true;
}
bool GatewayKnxBridge::handleReg1AssignCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 5 || response == nullptr) {
return false;
}
commissioning_assign_done_ = false;
const uint8_t short_address = data[1] == 99 ? 0xff : data[1];
const bool ok = SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, 0x00,
"knx-function-assign-init") &&
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRH, data[2],
"knx-function-assign-search-h") &&
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRM, data[3],
"knx-function-assign-search-m") &&
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRL, data[4],
"knx-function-assign-search-l") &&
SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS,
short_address == 0xff ? 0xff : DaliComm::toCmdAddr(short_address),
"knx-function-assign-program") &&
SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, 0x00,
"knx-function-assign-terminate");
commissioning_assign_done_ = true;
if (!ok) {
ESP_LOGW(kTag, "REG1-Dali assign command failed while programming short address %u",
short_address);
}
response->clear();
return true;
}
bool GatewayKnxBridge::handleReg1EvgWriteCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 10 || response == nullptr) {
return false;
}
const uint8_t short_address = data[1];
DaliBridgeRequest settings = FunctionRequest("knx-function-evg-write-settings",
BridgeOperation::setAddressSettings);
settings.shortAddress = short_address;
settings.value = DaliValue::Object{
{"minLevel", Reg1PercentToArc(data[2])},
{"maxLevel", Reg1PercentToArc(data[3])},
{"powerOnLevel", Reg1PercentToArc(data[4])},
{"systemFailureLevel", Reg1PercentToArc(data[5])},
{"fadeTime", static_cast<int>((data[6] >> 4) & 0x0f)},
{"fadeRate", static_cast<int>(data[6] & 0x0f)},
};
const bool settings_ok = engine_.execute(settings).ok;
DaliBridgeRequest groups = FunctionRequest("knx-function-evg-write-groups",
BridgeOperation::setGroupMask);
groups.shortAddress = short_address;
groups.value = static_cast<int>(static_cast<uint16_t>(data[8]) |
(static_cast<uint16_t>(data[9]) << 8));
const bool groups_ok = engine_.execute(groups).ok;
if (!settings_ok || !groups_ok) {
ESP_LOGW(kTag, "REG1-Dali EVG write command failed for short address %u", short_address);
}
response->clear();
return true;
}
bool GatewayKnxBridge::handleReg1EvgReadCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 2 || response == nullptr) {
return false;
}
const uint8_t short_address = data[1];
response->assign(12, 0x00);
(*response)[0] = 0x00;
uint8_t error_byte = 0;
DaliBridgeRequest settings = FunctionRequest("knx-function-evg-read-settings",
BridgeOperation::getAddressSettings);
settings.shortAddress = short_address;
const auto settings_result = engine_.execute(settings);
const auto set_level = [&](size_t index, const char* key, uint8_t error_mask) {
const auto value = MetadataInt(settings_result, key);
if (!settings_result.ok || !value.has_value()) {
error_byte |= error_mask;
(*response)[index] = 0xff;
return;
}
(*response)[index] = Reg1ArcToPercent(static_cast<uint8_t>(std::clamp(value.value(), 0, 255)));
};
set_level(1, "minLevel", 0b00000001);
set_level(2, "maxLevel", 0b00000010);
set_level(3, "powerOnLevel", 0b00000100);
set_level(4, "systemFailureLevel", 0b00001000);
const auto fade_time = MetadataInt(settings_result, "fadeTime");
const auto fade_rate = MetadataInt(settings_result, "fadeRate");
if (!settings_result.ok || !fade_time.has_value() || !fade_rate.has_value()) {
error_byte |= 0b00010000;
(*response)[5] = 0xff;
} else {
(*response)[5] = static_cast<uint8_t>(((fade_rate.value() & 0x0f) << 4) |
(fade_time.value() & 0x0f));
}
DaliBridgeRequest groups = FunctionRequest("knx-function-evg-read-groups", BridgeOperation::getGroupMask);
groups.shortAddress = short_address;
const auto groups_result = engine_.execute(groups);
if (!groups_result.ok || !groups_result.data.has_value()) {
error_byte |= 0b11000000;
} else {
const uint16_t mask = static_cast<uint16_t>(groups_result.data.value());
(*response)[7] = static_cast<uint8_t>(mask & 0xff);
(*response)[8] = static_cast<uint8_t>((mask >> 8) & 0xff);
}
(*response)[9] = error_byte;
return true;
}
bool GatewayKnxBridge::handleReg1SetSceneCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 10 || response == nullptr) {
return false;
}
const GatewayKnxDaliTarget target = Reg1SceneTarget(data[1]);
const uint8_t scene = data[2] & 0x0f;
const bool enabled = data[3] != 0;
DaliBridgeRequest request = FunctionRequest(
enabled ? "knx-function-set-scene" : "knx-function-remove-scene",
enabled ? (data[4] == kReg1DeviceTypeDt8 ? BridgeOperation::storeDt8SceneSnapshot
: BridgeOperation::setSceneLevel)
: BridgeOperation::removeSceneLevel);
ApplyTargetToRequest(target, &request);
DaliValue::Object value{{"scene", static_cast<int>(scene)}};
if (enabled) {
value["brightness"] = static_cast<int>(Reg1PercentToArc(data[6]));
if (data[4] == kReg1DeviceTypeDt8) {
if (data[5] == kReg1ColorTypeTw) {
const uint16_t kelvin = ReadBe16(data + 7);
value["colorMode"] = "color_temperature";
value["colorTemperature"] = static_cast<int>(kelvin);
} else {
value["colorMode"] = "rgb";
value["r"] = static_cast<int>(data[7]);
value["g"] = static_cast<int>(data[8]);
value["b"] = static_cast<int>(data[9]);
}
}
}
request.value = std::move(value);
const auto result = engine_.execute(request);
if (!result.ok) {
ESP_LOGW(kTag, "REG1-Dali set scene command failed for scene %u", scene);
}
response->clear();
return true;
}
bool GatewayKnxBridge::handleReg1GetSceneCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 5 || response == nullptr) {
return false;
}
const uint8_t short_address = data[1];
const uint8_t scene = data[2] & 0x0f;
DaliBridgeRequest request = FunctionRequest("knx-function-get-scene", BridgeOperation::getSceneLevel);
request.shortAddress = short_address;
request.value = DaliValue::Object{{"scene", static_cast<int>(scene)}};
const auto result = engine_.execute(request);
if (!result.ok || !result.data.has_value()) {
*response = {0xff};
return true;
}
const uint8_t raw_level = static_cast<uint8_t>(std::clamp(result.data.value(), 0, 255));
*response = {static_cast<uint8_t>(raw_level == 0xff ? 0xff : Reg1ArcToPercent(raw_level))};
if (raw_level != 0xff && data[3] == kReg1DeviceTypeDt8) {
if (data[4] == kReg1ColorTypeTw) {
response->resize(3, 0);
SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xe2, "knx-function-get-scene-ct-selector");
SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
"knx-function-get-scene-ct-dt-select");
const uint16_t mirek = static_cast<uint16_t>(
(QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE,
"knx-function-get-scene-mirek-h")
.value_or(0)
<< 8) |
QueryShort(engine_, short_address, DALI_CMD_QUERY_CONTENT_DTR,
"knx-function-get-scene-mirek-l")
.value_or(0));
const uint16_t kelvin = mirek == 0 ? 0 : static_cast<uint16_t>(1000000U / mirek);
(*response)[1] = static_cast<uint8_t>((kelvin >> 8) & 0xff);
(*response)[2] = static_cast<uint8_t>(kelvin & 0xff);
} else {
response->resize(4, 0);
const std::array<uint8_t, 3> selectors{0xe9, 0xea, 0xeb};
for (size_t index = 0; index < selectors.size(); ++index) {
SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, selectors[index],
"knx-function-get-scene-rgb-selector");
SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
"knx-function-get-scene-rgb-dt-select");
(*response)[index + 1] = static_cast<uint8_t>(
QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE,
"knx-function-get-scene-rgb-value")
.value_or(0));
}
}
}
return true;
}
bool GatewayKnxBridge::handleReg1IdentifyCommand(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 2 || response == nullptr) {
return false;
}
DaliBridgeRequest off = FunctionRequest("knx-function-identify-broadcast-off", BridgeOperation::off);
off.metadata["broadcast"] = true;
engine_.execute(off);
DaliBridgeRequest identify = FunctionRequest("knx-function-identify-recall-max",
BridgeOperation::recallMaxLevel);
identify.shortAddress = data[1];
engine_.execute(identify);
response->clear();
return true;
}
bool GatewayKnxBridge::handleReg1ScanState(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 1 || response == nullptr) {
return false;
}
response->clear();
response->push_back(commissioning_scan_done_ ? 1 : 0);
if (data[0] == kReg1FunctionScan) {
response->push_back(static_cast<uint8_t>(
std::min<size_t>(commissioning_found_ballasts_.size(), 0xff)));
}
return true;
}
bool GatewayKnxBridge::handleReg1AssignState(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 1 || response == nullptr) {
return false;
}
*response = {static_cast<uint8_t>(commissioning_assign_done_ ? 1 : 0)};
return true;
}
bool GatewayKnxBridge::handleReg1FoundEvgsState(const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (len < 2 || response == nullptr) {
return false;
}
if (data[1] == 254) {
commissioning_found_ballasts_.clear();
response->clear();
return true;
}
const size_t index = data[1];
response->clear();
response->push_back(index < commissioning_found_ballasts_.size() ? 1 : 0);
if (index < commissioning_found_ballasts_.size()) {
const auto& ballast = commissioning_found_ballasts_[index];
response->push_back(ballast.high);
response->push_back(ballast.middle);
response->push_back(ballast.low);
response->push_back(ballast.short_address);
}
return true;
}
DaliBridgeResult GatewayKnxBridge::executeEtsBindings(
uint16_t group_address, const std::vector<GatewayKnxDaliBinding>& bindings,
const uint8_t* data, size_t len) {
@@ -1011,8 +1524,11 @@ DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address
}
}
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler)
: bridge_(bridge), handler_(std::move(handler)) {}
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler,
std::string openknx_namespace)
: bridge_(bridge),
handler_(std::move(handler)),
openknx_namespace_(std::move(openknx_namespace)) {}
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); }
@@ -1032,7 +1548,19 @@ esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task
if (!configureSocket()) {
return ESP_FAIL;
}
ets_device_ = std::make_unique<openknx::EtsDeviceRuntime>(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<uint8_t>* 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<uint8_t>* response) {
return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response);
});
if (!configureTpUart()) {
ets_device_.reset();
closeSockets();
return ESP_FAIL;
}
@@ -1075,6 +1603,9 @@ void GatewayKnxTpIpRouter::taskLoop() {
reinterpret_cast<sockaddr*>(&remote), &remote_len);
if (received <= 0) {
pollTpUart();
if (ets_device_ != nullptr) {
ets_device_->loop();
}
if (!stop_requested_) {
vTaskDelay(pdMS_TO_TICKS(10));
}
@@ -1082,12 +1613,16 @@ void GatewayKnxTpIpRouter::taskLoop() {
}
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
pollTpUart();
if (ets_device_ != nullptr) {
ets_device_->loop();
}
}
finishTask();
}
void GatewayKnxTpIpRouter::finishTask() {
closeSockets();
ets_device_.reset();
started_ = false;
task_handle_ = nullptr;
vTaskDelete(nullptr);
@@ -1209,8 +1744,8 @@ bool GatewayKnxTpIpRouter::initializeTpUart() {
saw_reset = true;
const std::array<uint8_t, 3> set_address{
kTpUartSetAddressRequest,
static_cast<uint8_t>((config_.individual_address >> 8) & 0xff),
static_cast<uint8_t>(config_.individual_address & 0xff),
static_cast<uint8_t>((effectiveIndividualAddress() >> 8) & 0xff),
static_cast<uint8_t>(effectiveIndividualAddress() & 0xff),
};
uart_write_bytes(uart_port, set_address.data(), set_address.size());
const uint8_t state_request = kTpUartStateRequest;
@@ -1296,6 +1831,10 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l
sendTunnellingAck(channel_id, sequence, kKnxNoError, remote);
const uint8_t* cemi = body + 4;
const size_t cemi_len = len - 4;
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len);
if (consumed_by_openknx) {
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());
@@ -1400,8 +1939,8 @@ void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t statu
body.insert(body.end(), data_endpoint.begin(), data_endpoint.end());
body.push_back(0x04);
body.push_back(kKnxConnectionTypeTunnel);
body.push_back(static_cast<uint8_t>((config_.individual_address >> 8) & 0xff));
body.push_back(static_cast<uint8_t>(config_.individual_address & 0xff));
body.push_back(static_cast<uint8_t>((effectiveTunnelAddress() >> 8) & 0xff));
body.push_back(static_cast<uint8_t>(effectiveTunnelAddress() & 0xff));
const auto packet = KnxNetIpPacket(kServiceConnectResponse, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
@@ -1419,6 +1958,79 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len) {
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);
});
syncOpenKnxConfigFromDevice();
return consumed;
}
void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
if (ets_device_ == nullptr) {
return;
}
const auto snapshot = ets_device_->snapshot();
bool changed = false;
GatewayKnxConfig updated = config_;
if (snapshot.individual_address != 0 && snapshot.individual_address != 0xffff &&
snapshot.individual_address != updated.individual_address) {
updated.individual_address = snapshot.individual_address;
changed = true;
}
if (snapshot.configured || !snapshot.associations.empty()) {
std::vector<GatewayKnxEtsAssociation> associations;
associations.reserve(snapshot.associations.size());
for (const auto& association : snapshot.associations) {
associations.push_back(GatewayKnxEtsAssociation{association.group_address,
association.group_object_number});
}
if (associations.size() != updated.ets_associations.size() ||
!std::equal(associations.begin(), associations.end(), updated.ets_associations.begin(),
[](const GatewayKnxEtsAssociation& lhs,
const GatewayKnxEtsAssociation& rhs) {
return lhs.group_address == rhs.group_address &&
lhs.group_object_number == rhs.group_object_number;
})) {
updated.ets_associations = std::move(associations);
changed = true;
}
}
if (!changed) {
return;
}
config_ = updated;
bridge_.setConfig(config_);
}
uint16_t GatewayKnxTpIpRouter::effectiveIndividualAddress() const {
if (ets_device_ != nullptr) {
const uint16_t address = ets_device_->individualAddress();
if (address != 0 && address != 0xffff) {
return address;
}
}
return config_.individual_address;
}
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddress() const {
if (ets_device_ != nullptr) {
const uint16_t address = ets_device_->tunnelClientAddress();
if (address != 0 && address != 0xffff) {
return address;
}
}
uint16_t device = static_cast<uint16_t>((config_.individual_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {
device = 1;
}
return static_cast<uint16_t>((config_.individual_address & 0xff00) | device);
}
void GatewayKnxTpIpRouter::pollTpUart() {
if (tp_uart_port_ < 0) {
return;
+1 -1
View File
@@ -1,7 +1,7 @@
idf_component_register(
SRCS "src/gateway_network.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_domain esp_event esp_http_server esp_netif esp_wifi freertos gateway_bridge gateway_controller gateway_runtime log lwip espressif__cjson
REQUIRES dali_domain esp_driver_gpio esp_driver_spi esp_eth esp_event esp_http_server esp_hw_support esp_netif esp_wifi freertos gateway_bridge gateway_controller gateway_runtime log lwip espressif__cjson
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -8,6 +8,10 @@
#include "esp_err.h"
#include "esp_event.h"
#include "esp_eth.h"
#include "esp_eth_mac.h"
#include "esp_eth_netif_glue.h"
#include "esp_eth_phy.h"
#include "esp_http_server.h"
#include "esp_netif.h"
#include "esp_now.h"
@@ -26,6 +30,8 @@ struct DaliRawFrame;
struct GatewayNetworkServiceConfig {
bool wifi_enabled{true};
bool ethernet_enabled{false};
bool ethernet_ignore_init_failure{false};
bool espnow_setup_enabled{true};
bool espnow_setup_startup_enabled{false};
bool smartconfig_enabled{true};
@@ -35,6 +41,17 @@ struct GatewayNetworkServiceConfig {
bool udp_enabled{true};
uint16_t http_port{80};
uint16_t udp_port{2020};
int ethernet_spi_host{1};
int ethernet_spi_sclk_gpio{14};
int ethernet_spi_mosi_gpio{13};
int ethernet_spi_miso_gpio{12};
int ethernet_spi_cs_gpio{15};
int ethernet_spi_int_gpio{4};
uint32_t ethernet_poll_period_ms{0};
uint8_t ethernet_spi_clock_mhz{36};
int ethernet_phy_reset_gpio{5};
int ethernet_phy_addr{1};
uint32_t ethernet_rx_task_stack_size{3072};
int status_led_gpio{-1};
bool status_led_active_high{true};
int boot_button_gpio{-1};
@@ -66,12 +83,17 @@ class GatewayNetworkService {
static esp_err_t HandleLedOnGet(httpd_req_t* req);
static esp_err_t HandleLedOffGet(httpd_req_t* req);
static esp_err_t HandleJqJsGet(httpd_req_t* req);
static void HandleEthernetEvent(void* arg, esp_event_base_t event_base, int32_t event_id,
void* event_data);
static void HandleWifiEvent(void* arg, esp_event_base_t event_base, int32_t event_id,
void* event_data);
static void HandleEspNowReceive(const esp_now_recv_info_t* info, const uint8_t* data,
int data_len);
esp_err_t ensureNetworkStack();
esp_err_t startEthernet();
esp_err_t probeEthernetStartup();
void stopEthernet();
esp_err_t startWifi();
esp_err_t startSetupAp();
esp_err_t startSmartconfig();
@@ -89,6 +111,7 @@ class GatewayNetworkService {
void bootButtonTaskLoop();
void handleGatewayNotification(const std::vector<uint8_t>& frame);
void handleWifiControl(uint8_t mode);
void handleEthernetEvent(esp_event_base_t event_base, int32_t event_id, void* event_data);
void handleWifiEvent(esp_event_base_t event_base, int32_t event_id, void* event_data);
void handleEspNowReceive(const esp_now_recv_info_t* info, const uint8_t* data, int data_len);
void handleSetupUartFrame(int setup_id, const std::vector<uint8_t>& frame);
@@ -106,8 +129,15 @@ class GatewayNetworkService {
GatewayBridgeService* bridge_service_{nullptr};
bool started_{false};
httpd_handle_t http_server_{nullptr};
esp_netif_t* eth_netif_{nullptr};
esp_netif_t* wifi_sta_netif_{nullptr};
esp_netif_t* wifi_ap_netif_{nullptr};
esp_eth_handle_t eth_handle_{nullptr};
esp_eth_mac_t* eth_mac_{nullptr};
esp_eth_phy_t* eth_phy_{nullptr};
esp_eth_netif_glue_handle_t eth_glue_{nullptr};
bool ethernet_started_{false};
bool ethernet_event_handlers_registered_{false};
bool wifi_started_{false};
bool wifi_event_handlers_registered_{false};
bool setup_ap_started_{false};
@@ -7,8 +7,12 @@
#include "cJSON.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_event.h"
#include "esp_eth_driver.h"
#include "esp_eth_mac_spi.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "esp_netif_ip_addr.h"
#include "esp_smartconfig.h"
@@ -268,6 +272,19 @@ esp_err_t GatewayNetworkService::start() {
return err;
}
if (config_.ethernet_enabled) {
err = startEthernet();
if (err != ESP_OK) {
if (config_.ethernet_ignore_init_failure) {
ESP_LOGW(kTag, "Ethernet init failed; Ethernet is disabled for this boot: %s",
esp_err_to_name(err));
config_.ethernet_enabled = false;
} else {
return err;
}
}
}
if (config_.espnow_setup_startup_enabled) {
err = startSetupAp();
if (err != ESP_OK) {
@@ -320,7 +337,8 @@ esp_err_t GatewayNetworkService::start() {
}
started_ = true;
ESP_LOGI(kTag, "network service started http=%d udp=%d", config_.http_enabled,
ESP_LOGI(kTag, "network service started eth=%d wifi=%d http=%d udp=%d",
config_.ethernet_enabled, config_.wifi_enabled, config_.http_enabled,
config_.udp_enabled);
return ESP_OK;
}
@@ -341,6 +359,234 @@ esp_err_t GatewayNetworkService::ensureNetworkStack() {
return ESP_OK;
}
esp_err_t GatewayNetworkService::startEthernet() {
if (ethernet_started_) {
return ESP_OK;
}
#if CONFIG_ETH_SPI_ETHERNET_W5500
if (eth_netif_ == nullptr) {
esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_ETH();
eth_netif_ = esp_netif_new(&netif_config);
if (eth_netif_ == nullptr) {
ESP_LOGE(kTag, "failed to create Ethernet netif");
return ESP_ERR_NO_MEM;
}
}
if (!ethernet_event_handlers_registered_) {
esp_err_t err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID,
&GatewayNetworkService::HandleEthernetEvent, this);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(kTag, "failed to register Ethernet event handler: %s", esp_err_to_name(err));
stopEthernet();
return err;
}
err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP,
&GatewayNetworkService::HandleEthernetEvent, this);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(kTag, "failed to register Ethernet IP event handler: %s", esp_err_to_name(err));
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
ETH_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleEthernetEvent));
stopEthernet();
return err;
}
ethernet_event_handlers_registered_ = true;
}
if (config_.ethernet_spi_int_gpio >= 0) {
esp_err_t err = gpio_install_isr_service(0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(kTag, "failed to install GPIO ISR service for Ethernet: %s", esp_err_to_name(err));
stopEthernet();
return err;
}
}
const auto spi_host = static_cast<spi_host_device_t>(config_.ethernet_spi_host);
spi_bus_config_t bus_config = {};
bus_config.miso_io_num = config_.ethernet_spi_miso_gpio;
bus_config.mosi_io_num = config_.ethernet_spi_mosi_gpio;
bus_config.sclk_io_num = config_.ethernet_spi_sclk_gpio;
bus_config.quadwp_io_num = -1;
bus_config.quadhd_io_num = -1;
esp_err_t err = spi_bus_initialize(spi_host, &bus_config, SPI_DMA_CH_AUTO);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(kTag, "failed to initialize Ethernet SPI host %d: %s", config_.ethernet_spi_host,
esp_err_to_name(err));
stopEthernet();
return err;
}
spi_device_interface_config_t spi_device_config = {};
spi_device_config.mode = 0;
spi_device_config.clock_speed_hz = static_cast<int>(config_.ethernet_spi_clock_mhz) * 1000 * 1000;
spi_device_config.spics_io_num = config_.ethernet_spi_cs_gpio;
spi_device_config.queue_size = 20;
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
mac_config.rx_task_stack_size = config_.ethernet_rx_task_stack_size;
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
phy_config.phy_addr = config_.ethernet_phy_addr;
phy_config.reset_gpio_num = config_.ethernet_phy_reset_gpio;
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_host, &spi_device_config);
w5500_config.int_gpio_num = config_.ethernet_spi_int_gpio;
w5500_config.poll_period_ms = config_.ethernet_poll_period_ms;
if (w5500_config.int_gpio_num < 0 && w5500_config.poll_period_ms == 0) {
w5500_config.poll_period_ms = 100;
}
eth_mac_ = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
if (eth_mac_ == nullptr) {
ESP_LOGE(kTag, "failed to create W5500 Ethernet MAC");
stopEthernet();
return ESP_ERR_NO_MEM;
}
eth_phy_ = esp_eth_phy_new_w5500(&phy_config);
if (eth_phy_ == nullptr) {
ESP_LOGE(kTag, "failed to create W5500 Ethernet PHY");
stopEthernet();
return ESP_ERR_NO_MEM;
}
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(eth_mac_, eth_phy_);
err = esp_eth_driver_install(&eth_config, &eth_handle_);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to install Ethernet driver: %s", esp_err_to_name(err));
stopEthernet();
return err;
}
uint8_t eth_mac[6] = {};
err = esp_read_mac(eth_mac, ESP_MAC_ETH);
if (err == ESP_OK) {
err = esp_eth_ioctl(eth_handle_, ETH_CMD_S_MAC_ADDR, eth_mac);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to set Ethernet MAC address: %s", esp_err_to_name(err));
stopEthernet();
return err;
}
EthernetInfo info;
info.mac = MacToHex(eth_mac);
runtime_.setEthernetInfo(std::move(info));
} else {
ESP_LOGW(kTag, "failed to read Ethernet MAC address: %s", esp_err_to_name(err));
}
eth_glue_ = esp_eth_new_netif_glue(eth_handle_);
if (eth_glue_ == nullptr) {
ESP_LOGE(kTag, "failed to create Ethernet netif glue");
stopEthernet();
return ESP_ERR_NO_MEM;
}
err = esp_netif_attach(eth_netif_, eth_glue_);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to attach Ethernet netif: %s", esp_err_to_name(err));
stopEthernet();
return err;
}
err = esp_eth_start(eth_handle_);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to start Ethernet: %s", esp_err_to_name(err));
stopEthernet();
return err;
}
ethernet_started_ = true;
err = probeEthernetStartup();
if (err != ESP_OK) {
ESP_LOGE(kTag, "Ethernet startup probe failed: %s", esp_err_to_name(err));
stopEthernet();
return err;
}
ESP_LOGI(kTag,
"Ethernet W5500 started spi_host=%d sclk=%d mosi=%d miso=%d cs=%d int=%d reset=%d",
config_.ethernet_spi_host, config_.ethernet_spi_sclk_gpio,
config_.ethernet_spi_mosi_gpio, config_.ethernet_spi_miso_gpio,
config_.ethernet_spi_cs_gpio, config_.ethernet_spi_int_gpio,
config_.ethernet_phy_reset_gpio);
return ESP_OK;
#else
ESP_LOGW(kTag, "Ethernet requested but W5500 support is not enabled in esp-eth");
return ESP_ERR_NOT_SUPPORTED;
#endif
}
esp_err_t GatewayNetworkService::probeEthernetStartup() {
if (eth_handle_ == nullptr || !ethernet_started_) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = esp_eth_stop(eth_handle_);
ethernet_started_ = false;
if (err != ESP_OK) {
return err;
}
err = esp_eth_start(eth_handle_);
if (err == ESP_OK) {
ethernet_started_ = true;
}
return err;
}
void GatewayNetworkService::stopEthernet() {
if (ethernet_event_handlers_registered_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
IP_EVENT, IP_EVENT_ETH_GOT_IP, &GatewayNetworkService::HandleEthernetEvent));
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
ETH_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleEthernetEvent));
ethernet_event_handlers_registered_ = false;
}
if (eth_handle_ != nullptr) {
const esp_err_t stop_err = esp_eth_stop(eth_handle_);
if (stop_err != ESP_OK && stop_err != ESP_ERR_INVALID_STATE) {
ESP_LOGW(kTag, "failed to stop Ethernet during disable: %s", esp_err_to_name(stop_err));
}
ethernet_started_ = false;
}
if (eth_glue_ != nullptr) {
const esp_err_t glue_err = esp_eth_del_netif_glue(eth_glue_);
if (glue_err != ESP_OK) {
ESP_LOGW(kTag, "failed to delete Ethernet netif glue: %s", esp_err_to_name(glue_err));
} else {
eth_glue_ = nullptr;
}
}
if (eth_handle_ != nullptr) {
const esp_err_t uninstall_err = esp_eth_driver_uninstall(eth_handle_);
if (uninstall_err != ESP_OK) {
ESP_LOGW(kTag, "failed to uninstall Ethernet driver: %s", esp_err_to_name(uninstall_err));
} else {
eth_handle_ = nullptr;
}
}
if (eth_phy_ != nullptr && eth_handle_ == nullptr) {
ESP_ERROR_CHECK_WITHOUT_ABORT(eth_phy_->del(eth_phy_));
eth_phy_ = nullptr;
}
if (eth_mac_ != nullptr && eth_handle_ == nullptr) {
ESP_ERROR_CHECK_WITHOUT_ABORT(eth_mac_->del(eth_mac_));
eth_mac_ = nullptr;
}
if (eth_netif_ != nullptr && eth_glue_ == nullptr) {
esp_netif_destroy(eth_netif_);
eth_netif_ = nullptr;
}
runtime_.clearEthernetInfo();
}
esp_err_t GatewayNetworkService::startWifi() {
if (wifi_started_) {
return ESP_OK;
@@ -778,6 +1024,69 @@ void GatewayNetworkService::HandleEspNowReceive(const esp_now_recv_info_t* info,
}
}
void GatewayNetworkService::HandleEthernetEvent(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data) {
auto* service = static_cast<GatewayNetworkService*>(arg);
if (service != nullptr) {
service->handleEthernetEvent(event_base, event_id, event_data);
}
}
void GatewayNetworkService::handleEthernetEvent(esp_event_base_t event_base, int32_t event_id,
void* event_data) {
if (event_base == ETH_EVENT) {
esp_eth_handle_t handle = eth_handle_;
if (event_data != nullptr) {
handle = *static_cast<esp_eth_handle_t*>(event_data);
}
if (event_id == ETHERNET_EVENT_CONNECTED) {
uint8_t mac[6] = {};
if (handle != nullptr && esp_eth_ioctl(handle, ETH_CMD_G_MAC_ADDR, mac) == ESP_OK) {
const std::string mac_hex = MacToHex(mac);
EthernetInfo info = runtime_.deviceInfo().eth.value_or(EthernetInfo{});
info.mac = mac_hex;
runtime_.setEthernetInfo(std::move(info));
ESP_LOGI(kTag, "Ethernet link up mac=%s", mac_hex.c_str());
} else {
ESP_LOGI(kTag, "Ethernet link up");
}
return;
}
if (event_id == ETHERNET_EVENT_DISCONNECTED) {
runtime_.clearEthernetIp();
ESP_LOGI(kTag, "Ethernet link down");
return;
}
if (event_id == ETHERNET_EVENT_START) {
ESP_LOGI(kTag, "Ethernet driver started");
return;
}
if (event_id == ETHERNET_EVENT_STOP) {
runtime_.clearEthernetIp();
ESP_LOGI(kTag, "Ethernet driver stopped");
}
return;
}
if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP && event_data != nullptr) {
auto* event = static_cast<ip_event_got_ip_t*>(event_data);
char ip[16] = {0};
esp_ip4addr_ntoa(&event->ip_info.ip, ip, sizeof(ip));
EthernetInfo info = runtime_.deviceInfo().eth.value_or(EthernetInfo{});
uint8_t mac[6] = {};
if (eth_handle_ != nullptr && esp_eth_ioctl(eth_handle_, ETH_CMD_G_MAC_ADDR, mac) == ESP_OK) {
info.mac = MacToHex(mac);
}
info.ip = ip;
runtime_.setEthernetInfo(std::move(info));
ESP_LOGI(kTag, "Ethernet got IP %s", ip);
}
}
void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t event_id,
void* event_data) {
if (!config_.wifi_enabled) {
@@ -1168,6 +1477,15 @@ std::string GatewayNetworkService::deviceInfoJson() const {
}
}
if (info.eth.has_value()) {
cJSON* eth = cJSON_CreateObject();
if (eth != nullptr) {
cJSON_AddStringToObject(eth, "mac", info.eth->mac.c_str());
cJSON_AddStringToObject(eth, "IP", info.eth->ip.c_str());
cJSON_AddItemToObject(root, "ethInfo", eth);
}
}
const std::string rendered = PrintJson(root);
cJSON_Delete(root);
return rendered;
@@ -30,6 +30,11 @@ struct WirelessInfo {
std::string ip;
};
struct EthernetInfo {
std::string mac;
std::string ip;
};
struct GatewayRuntimeConfig {
std::string_view project_name;
std::string_view version;
@@ -47,6 +52,7 @@ struct GatewayDeviceInfo {
size_t dali_gateway_count{0};
bool ble_enabled{false};
std::optional<WirelessInfo> wlan;
std::optional<EthernetInfo> eth;
};
class GatewaySettingsStore {
@@ -119,6 +125,9 @@ class GatewayRuntime {
void setGatewayCount(size_t gateway_count);
void setWirelessInfo(WirelessInfo info);
bool clearWirelessInfo();
void setEthernetInfo(EthernetInfo info);
void clearEthernetInfo();
void clearEthernetIp();
void setCommandAddressResolver(std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> resolver);
GatewayDeviceInfo deviceInfo() const;
@@ -159,6 +168,7 @@ class GatewayRuntime {
CommandDropReason last_enqueue_drop_reason_{CommandDropReason::kNone};
std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> command_address_resolver_;
std::optional<WirelessInfo> wireless_info_;
std::optional<EthernetInfo> ethernet_info_;
SemaphoreHandle_t command_lock_{nullptr};
};
@@ -450,6 +450,23 @@ bool GatewayRuntime::clearWirelessInfo() {
return settings_.clearWifiCredentials();
}
void GatewayRuntime::setEthernetInfo(EthernetInfo info) {
LockGuard guard(command_lock_);
ethernet_info_ = std::move(info);
}
void GatewayRuntime::clearEthernetInfo() {
LockGuard guard(command_lock_);
ethernet_info_.reset();
}
void GatewayRuntime::clearEthernetIp() {
LockGuard guard(command_lock_);
if (ethernet_info_.has_value()) {
ethernet_info_->ip.clear();
}
}
void GatewayRuntime::setCommandAddressResolver(
std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> resolver) {
LockGuard guard(command_lock_);
@@ -470,6 +487,7 @@ GatewayDeviceInfo GatewayRuntime::deviceInfo() const {
info.dali_gateway_count = gateway_count_;
info.ble_enabled = ble_enabled_;
info.wlan = wireless_info_;
info.eth = ethernet_info_;
return info;
}
+2
View File
@@ -29,6 +29,7 @@ idf_component_register(
SRCS
"src/arduino_compat.cpp"
"src/esp_idf_platform.cpp"
"src/ets_device_runtime.cpp"
"src/ets_memory_loader.cpp"
"src/tpuart_uart_interface.cpp"
${OPENKNX_SRCS}
@@ -54,6 +55,7 @@ target_compile_definitions(${COMPONENT_LIB} PUBLIC
KNX_FLASH_SIZE=4096
KNX_NO_AUTOMATIC_GLOBAL_INSTANCE
KNX_NO_SPI
USE_CEMI_SERVER
)
target_compile_options(${COMPONENT_LIB} PRIVATE
@@ -0,0 +1,60 @@
#pragma once
#include "openknx_idf/esp_idf_platform.h"
#include "openknx_idf/ets_memory_loader.h"
#include "knx/bau07B0.h"
#include "knx/cemi_frame.h"
#include <cstddef>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace gateway::openknx {
class EtsDeviceRuntime {
public:
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response)>;
EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address);
~EtsDeviceRuntime();
uint16_t individualAddress() const;
uint16_t tunnelClientAddress() const;
bool configured() const;
EtsMemorySnapshot snapshot() const;
void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler);
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
void loop();
private:
static void EmitTunnelFrame(CemiFrame& frame, void* context);
static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
static bool HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
static uint16_t DefaultTunnelClientAddress(uint16_t individual_address);
static bool DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index,
uint8_t property_id, uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
std::string nvs_namespace_;
EspIdfPlatform platform_;
Bau07B0 device_;
CemiFrameSender sender_;
FunctionPropertyHandler command_handler_;
FunctionPropertyHandler state_handler_;
};
} // namespace gateway::openknx
@@ -13,6 +13,7 @@ struct EtsAssociation {
struct EtsMemorySnapshot {
bool configured{false};
uint16_t individual_address{0};
std::vector<EtsAssociation> associations;
};
@@ -1,6 +1,7 @@
#pragma once
#include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/ets_device_runtime.h"
#include "openknx_idf/esp_idf_platform.h"
#include "openknx_idf/tpuart_uart_interface.h"
@@ -0,0 +1,227 @@
#include "openknx_idf/ets_device_runtime.h"
#include "knx/cemi_server.h"
#include "knx/property.h"
#include <algorithm>
#include <cstdint>
#include <utility>
#include <vector>
namespace gateway::openknx {
namespace {
thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr;
class ActiveFunctionPropertyRuntimeScope {
public:
explicit ActiveFunctionPropertyRuntimeScope(EtsDeviceRuntime* runtime)
: previous_(active_function_property_runtime) {
active_function_property_runtime = runtime;
}
~ActiveFunctionPropertyRuntimeScope() { active_function_property_runtime = previous_; }
private:
EtsDeviceRuntime* previous_;
};
constexpr uint16_t kInvalidIndividualAddress = 0xffff;
constexpr uint16_t kReg1DaliManufacturerId = 0x00a4;
constexpr uint8_t kReg1DaliApplicationNumber = 0x01;
constexpr uint8_t kReg1DaliApplicationVersion = 0x05;
constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
bool IsUsableIndividualAddress(uint16_t address) {
return address != 0 && address != kInvalidIndividualAddress;
}
void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) {
device.deviceObject().manufacturerId(kReg1DaliManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.deviceObject().orderNumber(kReg1DaliOrderNumber);
const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber,
kReg1DaliApplicationVersion};
device.parameters().property(PID_PROG_VERSION)->write(program_version);
}
} // namespace
EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
uint16_t fallback_individual_address)
: nvs_namespace_(std::move(nvs_namespace)),
platform_(nullptr, nvs_namespace_.c_str()),
device_(platform_) {
ApplyReg1DaliIdentity(device_, platform_);
if (IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
device_.readMemory();
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
if (auto* server = device_.getCemiServer()) {
server->clientAddress(DefaultTunnelClientAddress(device_.deviceObject().individualAddress()));
server->tunnelFrameCallback(&EtsDeviceRuntime::EmitTunnelFrame, this);
}
device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand);
device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState);
}
EtsDeviceRuntime::~EtsDeviceRuntime() {
device_.functionPropertyCallback(nullptr);
device_.functionPropertyStateCallback(nullptr);
if (auto* server = device_.getCemiServer()) {
server->tunnelFrameCallback(nullptr, nullptr);
}
}
uint16_t EtsDeviceRuntime::individualAddress() const {
return const_cast<Bau07B0&>(device_).deviceObject().individualAddress();
}
uint16_t EtsDeviceRuntime::tunnelClientAddress() const {
if (auto* server = const_cast<Bau07B0&>(device_).getCemiServer()) {
return server->clientAddress();
}
return DefaultTunnelClientAddress(individualAddress());
}
bool EtsDeviceRuntime::configured() const { return const_cast<Bau07B0&>(device_).configured(); }
EtsMemorySnapshot EtsDeviceRuntime::snapshot() const {
EtsMemorySnapshot out;
auto& device = const_cast<Bau07B0&>(device_);
out.configured = device.configured();
out.individual_address = device.deviceObject().individualAddress();
device.forEachEtsAssociation(
[](uint16_t group_address, uint16_t group_object_number, void* context) {
auto* associations = static_cast<std::vector<EtsAssociation>*>(context);
if (associations != nullptr) {
associations->push_back(EtsAssociation{group_address, group_object_number});
}
},
&out.associations);
std::sort(out.associations.begin(), out.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
if (lhs.group_address != rhs.group_address) {
return lhs.group_address < rhs.group_address;
}
return lhs.group_object_number < rhs.group_object_number;
});
out.associations.erase(
std::unique(out.associations.begin(), out.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
return lhs.group_address == rhs.group_address &&
lhs.group_object_number == rhs.group_object_number;
}),
out.associations.end());
return out;
}
void EtsDeviceRuntime::setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler) {
command_handler_ = std::move(command_handler);
state_handler_ = std::move(state_handler);
}
bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
CemiFrameSender sender) {
auto* server = device_.getCemiServer();
if (server == nullptr || data == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
const bool consumed = shouldConsumeTunnelFrame(frame);
if (!consumed) {
return false;
}
sender_ = std::move(sender);
ActiveFunctionPropertyRuntimeScope callback_scope(this);
server->frameReceived(frame);
loop();
sender_ = nullptr;
return consumed;
}
void EtsDeviceRuntime::loop() { device_.loop(); }
void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->sender_) {
return;
}
self->sender_(frame.data(), frame.dataLength());
}
bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (active_function_property_runtime == nullptr) {
return false;
}
return DispatchFunctionProperty(&active_function_property_runtime->command_handler_, object_index,
property_id, length, data, result_data, result_length);
}
bool EtsDeviceRuntime::HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (active_function_property_runtime == nullptr) {
return false;
}
return DispatchFunctionProperty(&active_function_property_runtime->state_handler_, object_index,
property_id, length, data, result_data, result_length);
}
bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler,
uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length) {
if (handler == nullptr || !*handler || result_data == nullptr) {
return false;
}
std::vector<uint8_t> response;
if (!(*handler)(object_index, property_id, data, length, &response)) {
return false;
}
result_length = static_cast<uint8_t>(std::min<size_t>(response.size(), result_length));
if (result_length > 0) {
std::copy_n(response.begin(), result_length, result_data);
}
return true;
}
uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) {
if (!IsUsableIndividualAddress(individual_address)) {
return 0x1101;
}
const uint16_t line_base = individual_address & 0xff00;
uint16_t device = static_cast<uint16_t>((individual_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {
device = 1;
}
return static_cast<uint16_t>(line_base | device);
}
bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
switch (frame.messageCode()) {
case M_PropRead_req:
case M_PropWrite_req:
case M_Reset_req:
case M_FuncPropCommand_req:
case M_FuncPropStateRead_req:
return true;
case L_data_req:
return frame.addressType() == IndividualAddress &&
frame.destinationAddress() == individualAddress();
default:
return false;
}
}
} // namespace gateway::openknx
@@ -3,8 +3,11 @@
#include "openknx_idf/esp_idf_platform.h"
#include "knx/bau07B0.h"
#include "knx/property.h"
#include <algorithm>
#include <cstdint>
#include <memory>
namespace gateway::openknx {
namespace {
@@ -18,18 +21,45 @@ void CollectAssociation(uint16_t group_address, uint16_t group_object_number,
associations->push_back(EtsAssociation{group_address, group_object_number});
}
bool IsErasedMemory(const uint8_t* data, size_t size) {
if (data == nullptr) {
return false;
}
return std::all_of(data, data + size, [](uint8_t value) { return value == 0xff; });
}
constexpr uint16_t kReg1DaliManufacturerId = 0x00a4;
constexpr uint8_t kReg1DaliApplicationNumber = 0x01;
constexpr uint8_t kReg1DaliApplicationVersion = 0x05;
constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) {
device.deviceObject().manufacturerId(kReg1DaliManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.deviceObject().orderNumber(kReg1DaliOrderNumber);
const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber,
kReg1DaliApplicationVersion};
device.parameters().property(PID_PROG_VERSION)->write(program_version);
}
} // namespace
EtsMemorySnapshot LoadEtsMemorySnapshot(const std::string& nvs_namespace) {
EspIdfPlatform platform(nullptr, nvs_namespace.c_str());
Bau07B0 device(platform);
device.deviceObject().manufacturerId(0xfa);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.readMemory();
EtsMemorySnapshot snapshot;
snapshot.configured = device.configured();
device.forEachEtsAssociation(CollectAssociation, &snapshot.associations);
const uint8_t* memory = platform.getNonVolatileMemoryStart();
const size_t memory_size = platform.getNonVolatileMemorySize();
if (memory == nullptr || memory_size == 0 || IsErasedMemory(memory, memory_size)) {
return snapshot;
}
auto device = std::make_unique<Bau07B0>(platform);
ApplyReg1DaliIdentity(*device, platform);
device->readMemory();
snapshot.configured = device->configured();
snapshot.individual_address = device->deviceObject().individualAddress();
device->forEachEtsAssociation(CollectAssociation, &snapshot.associations);
std::sort(snapshot.associations.begin(), snapshot.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
if (lhs.group_address != rhs.group_address) {
+1 -1
Submodule knx updated: b747f6284d...339d8472e7