diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index 4dc3897..8f3605d 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -693,14 +693,23 @@ config GATEWAY_KNX_MULTICAST_ADDRESS depends on GATEWAY_KNX_BRIDGE_SUPPORTED && GATEWAY_KNX_MULTICAST_ENABLED default "224.0.23.12" -config GATEWAY_KNX_INDIVIDUAL_ADDRESS - int "KNX individual address raw value" +config GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS + int "KNXnet/IP interface individual address raw value" depends on GATEWAY_KNX_BRIDGE_SUPPORTED range 0 65535 - default 4353 + default 65281 help - Raw 16-bit individual address advertised to KNXnet/IP tunnel clients. - The default 4353 is 1.1.1. + Raw 16-bit individual address advertised by the KNXnet/IP interface. + The default 65281 is 15.15.1. + +config GATEWAY_KNX_INDIVIDUAL_ADDRESS + int "Logical KNX-DALI gateway individual address raw value" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0 65535 + default 65534 + help + Raw 16-bit individual address used by the ETS-programmable KNX-DALI gateway device. + The default 65534 is 15.15.254, used as the unprogrammed logical device address. config GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO int "KNX programming button GPIO" diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 5ae5502..d299e93 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -228,7 +228,11 @@ #endif #ifndef CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS -#define CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS 4353 +#define CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS 65534 +#endif + +#ifndef CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS +#define CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS 65281 #endif #ifndef CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO @@ -862,6 +866,8 @@ extern "C" void app_main(void) { default_knx.main_group = static_cast(CONFIG_GATEWAY_KNX_MAIN_GROUP); default_knx.udp_port = static_cast(CONFIG_GATEWAY_KNX_UDP_PORT); default_knx.multicast_address = CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS; + default_knx.ip_interface_individual_address = + static_cast(CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS); default_knx.individual_address = static_cast(CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS); default_knx.programming_button_gpio = CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO; diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 5d72a29..37b3c35 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -686,7 +686,8 @@ 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_IP_INTERFACE_INDIVIDUAL_ADDRESS=65281 +CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=65534 CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO=0 CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW=y CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO=10 @@ -1806,7 +1807,7 @@ CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=y CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 -CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y # CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set # CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set @@ -2940,7 +2941,7 @@ CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160=y 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_MAIN_TASK_STACK_SIZE=8192 # CONFIG_CONSOLE_UART_DEFAULT is not set # CONFIG_CONSOLE_UART_CUSTOM is not set # CONFIG_CONSOLE_UART_NONE is not set diff --git a/apps/gateway/sdkconfig.defaults b/apps/gateway/sdkconfig.defaults index 84ea107..677ae1c 100644 --- a/apps/gateway/sdkconfig.defaults +++ b/apps/gateway/sdkconfig.defaults @@ -7,6 +7,8 @@ CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_OFFSET=0x8000 CONFIG_PARTITION_TABLE_MD5=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + CONFIG_BT_ENABLED=y CONFIG_BT_NIMBLE_ENABLED=y diff --git a/apps/gateway/sdkconfig.old b/apps/gateway/sdkconfig.old index 10d5050..99cb301 100644 --- a/apps/gateway/sdkconfig.old +++ b/apps/gateway/sdkconfig.old @@ -697,7 +697,8 @@ 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_IP_INTERFACE_INDIVIDUAL_ADDRESS=65281 +CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=65534 CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO=0 CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW=y CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO=10 diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 6c3df31..44bb17d 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -2198,11 +2198,36 @@ struct GatewayBridgeService::ChannelRuntime { cJSON* knx_json = cJSON_CreateObject(); if (knx_json != nullptr) { + auto* endpoint_runtime = service.knx_endpoint_runtime_; + if (endpoint_runtime == nullptr) { + endpoint_runtime = const_cast(service).selectKnxEndpointRuntime(); + } + bool programming_mode = false; + bool programming_control_available = false; + int endpoint_owner_gateway_id = -1; + if (endpoint_runtime != nullptr) { + LockGuard owner_guard(endpoint_runtime->lock); + endpoint_owner_gateway_id = endpoint_runtime->channel.gateway_id; + programming_control_available = endpoint_runtime->knx_router != nullptr && + endpoint_runtime->knx_router->started(); + if (programming_control_available) { + programming_mode = endpoint_runtime->knx_router->programmingMode(); + } + } const auto effective_knx = knx_config.has_value() ? knx_config : service_config.default_knx_config; cJSON_AddBoolToObject(knx_json, "enabled", service_config.knx_enabled); cJSON_AddBoolToObject(knx_json, "startupEnabled", service_config.knx_startup_enabled); cJSON_AddBoolToObject(knx_json, "started", knx_started); cJSON_AddBoolToObject(knx_json, "routerReady", knx_router != nullptr && knx_router->started()); + cJSON_AddBoolToObject(knx_json, "programmingMode", programming_mode); + cJSON_AddBoolToObject(knx_json, "programmingControlAvailable", + programming_control_available); + cJSON_AddBoolToObject(knx_json, "endpointOwner", + endpoint_owner_gateway_id == channel.gateway_id); + if (endpoint_owner_gateway_id >= 0) { + cJSON_AddNumberToObject(knx_json, "endpointOwnerGatewayId", + endpoint_owner_gateway_id); + } const std::string router_error = knx_router == nullptr ? "" : knx_router->lastError(); cJSON_AddStringToObject(knx_json, "lastError", knx_last_error.empty() ? router_error.c_str() @@ -2269,6 +2294,8 @@ struct GatewayBridgeService::ChannelRuntime { cJSON_AddNumberToObject(knx_json, "udpPort", effective_knx->udp_port); cJSON_AddStringToObject(knx_json, "multicastAddress", effective_knx->multicast_address.c_str()); + cJSON_AddNumberToObject(knx_json, "ipInterfaceIndividualAddress", + effective_knx->ip_interface_individual_address); cJSON_AddNumberToObject(knx_json, "individualAddress", effective_knx->individual_address); cJSON* serial_json = cJSON_CreateObject(); @@ -3066,6 +3093,25 @@ struct GatewayBridgeService::ChannelRuntime { } return ESP_ERR_INVALID_ARG; } + if (config.ip_interface_individual_address == 0 || + config.ip_interface_individual_address == 0xffff) { + if (error_message != nullptr) { + *error_message = "KNX IP interface individual address must be a configured address"; + } + return ESP_ERR_INVALID_ARG; + } + if (config.individual_address == 0 || config.individual_address == 0xffff) { + if (error_message != nullptr) { + *error_message = "KNX-DALI gateway individual address must be a configured address"; + } + return ESP_ERR_INVALID_ARG; + } + if (config.ip_interface_individual_address == config.individual_address) { + if (error_message != nullptr) { + *error_message = "KNX IP interface and KNX-DALI gateway addresses must differ"; + } + return ESP_ERR_INVALID_ARG; + } if (!config.ip_router_enabled || !GatewayKnxConfigUsesTpUart(config)) { return ESP_OK; } @@ -3162,10 +3208,10 @@ struct GatewayBridgeService::ChannelRuntime { 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; + const uint16_t device = config.ip_interface_individual_address & 0x00ff; if (device > 0 && device + channel_index <= 0x00ff) { - config.individual_address = static_cast((config.individual_address & 0xff00) | - (device + channel_index)); + config.ip_interface_individual_address = static_cast( + (config.ip_interface_individual_address & 0xff00) | (device + channel_index)); } return config; } @@ -4415,6 +4461,40 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost( } return handleGet("knx", gateway_id.value()); } + if (action == "knx_programming_mode") { + cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size()); + if (body_root == nullptr) { + return ErrorResponse(ESP_ERR_INVALID_ARG, "KNX programming mode JSON is required"); + } + const cJSON* enabled_item = cJSON_GetObjectItemCaseSensitive(body_root, "enabled"); + if (!cJSON_IsBool(enabled_item)) { + cJSON_Delete(body_root); + return ErrorResponse(ESP_ERR_INVALID_ARG, "boolean enabled field is required"); + } + const bool enabled = cJSON_IsTrue(enabled_item); + cJSON_Delete(body_root); + + ChannelRuntime* owner = selectKnxEndpointRuntime(); + if (owner == nullptr) { + return ErrorResponse(ESP_ERR_NOT_FOUND, "no KNX/IP endpoint owner is configured"); + } + + esp_err_t err = ESP_ERR_INVALID_STATE; + std::string detail = "KNX/IP router is unavailable"; + { + LockGuard guard(owner->lock); + if (owner->knx_router != nullptr) { + err = owner->knx_router->setProgrammingMode(enabled); + detail = owner->knx_router->lastError(); + } + owner->knx_last_error = err == ESP_OK ? std::string() : detail; + } + if (err != ESP_OK) { + return ErrorResponse(err, detail.empty() ? "failed to change KNX programming mode" + : detail.c_str()); + } + return handleGet("status", gateway_id.value()); + } if (action == "knx_security_read_factory_key") { #if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \ defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED) diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index f091f76..dad35e9 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -63,7 +63,8 @@ struct GatewayKnxConfig { uint8_t main_group{0}; uint16_t udp_port{kGatewayKnxDefaultUdpPort}; std::string multicast_address{kGatewayKnxDefaultMulticastAddress}; - uint16_t individual_address{0x1101}; + uint16_t ip_interface_individual_address{0xff01}; + uint16_t individual_address{0xfffe}; int programming_button_gpio{-1}; bool programming_button_active_low{true}; int programming_led_gpio{-1}; @@ -208,6 +209,9 @@ class GatewayKnxTpIpRouter { void setGroupWriteHandler(GroupWriteHandler handler); const GatewayKnxConfig& config() const; bool tpUartOnline() const; + bool programmingMode(); + esp_err_t setProgrammingMode(bool enabled); + esp_err_t toggleProgrammingMode(); esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority); esp_err_t stop(); @@ -217,6 +221,14 @@ class GatewayKnxTpIpRouter { private: static constexpr size_t kMaxTunnelClients = 16; + static constexpr size_t kMaxTcpClients = 4; + + struct TcpClient { + int sock{-1}; + ::sockaddr_in remote{}; + std::vector rx_buffer; + TickType_t last_activity_tick{0}; + }; struct TunnelClient { bool connected{false}; @@ -225,6 +237,7 @@ class GatewayKnxTpIpRouter { uint8_t received_sequence{255}; uint8_t send_sequence{0}; uint16_t individual_address{0}; + int tcp_sock{-1}; TickType_t last_activity_tick{0}; ::sockaddr_in control_remote{}; ::sockaddr_in data_remote{}; @@ -237,6 +250,9 @@ class GatewayKnxTpIpRouter { void finishTask(); void closeSockets(); bool configureSocket(); + void handleTcpAccept(); + void handleTcpClient(TcpClient& client); + void closeTcpClient(TcpClient& client); bool configureTpUart(); bool initializeTpUart(); bool configureProgrammingGpio(); @@ -276,7 +292,12 @@ class GatewayKnxTpIpRouter { 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; + bool sendPacket(const std::vector& packet, const ::sockaddr_in& remote) const; + bool sendPacketToTunnelClient(const TunnelClient& client, + const std::vector& packet) const; + bool currentTransportAllowsTcpHpai() const; + std::optional> localHpaiForRemote(const ::sockaddr_in& remote, + bool tcp = false) const; std::vector buildDeviceInfoDib(const ::sockaddr_in& remote) const; std::vector buildSupportedServiceDib() const; std::vector buildExtendedDeviceInfoDib() const; @@ -298,8 +319,10 @@ class GatewayKnxTpIpRouter { void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote); bool emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len); bool shouldRouteDaliApplicationFrames() const; + uint8_t advertisedMedium() const; void syncOpenKnxConfigFromDevice(); - uint16_t effectiveIndividualAddress() const; + uint16_t effectiveIpInterfaceIndividualAddress() const; + uint16_t effectiveKnxDeviceIndividualAddress() const; uint16_t effectiveTunnelAddress() const; void pollTpUart(); void pollProgrammingButton(); @@ -322,9 +345,12 @@ class GatewayKnxTpIpRouter { std::atomic_bool stop_requested_{false}; std::atomic_bool started_{false}; int udp_sock_{-1}; + int tcp_sock_{-1}; + int active_tcp_sock_{-1}; int tp_uart_port_{-1}; std::vector multicast_joined_interfaces_; TickType_t network_refresh_tick_{0}; + std::array tcp_clients_{}; std::array tunnel_clients_{}; uint8_t last_tunnel_channel_id_{0}; std::vector tp_rx_frame_; diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index e311127..c84578d 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -67,6 +67,7 @@ constexpr uint8_t kKnxConnectionTypeDeviceManagement = 0x03; constexpr uint8_t kKnxConnectionTypeTunnel = 0x04; constexpr uint8_t kKnxTunnelLayerLink = 0x02; constexpr uint8_t kKnxHpaiIpv4Udp = 0x01; +constexpr uint8_t kKnxHpaiIpv4Tcp = 0x02; constexpr uint8_t kKnxDibDeviceInfo = 0x01; constexpr uint8_t kKnxDibSupportedServices = 0x02; constexpr uint8_t kKnxDibIpConfig = 0x03; @@ -81,7 +82,8 @@ 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 uint16_t kKnxIpOnlyDeviceDescriptor = 0x57b0; +constexpr uint16_t kKnxTpIpInterfaceDeviceDescriptor = 0x091a; constexpr uint8_t kKnxIpAssignmentManual = 0x01; constexpr uint8_t kKnxIpCapabilityManual = 0x01; constexpr uint8_t kTpUartResetRequest = 0x01; @@ -245,7 +247,7 @@ 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) { + (body[offset + 1] != kKnxHpaiIpv4Udp && body[offset + 1] != kKnxHpaiIpv4Tcp)) { return out; } uint32_t address = 0; @@ -265,6 +267,15 @@ sockaddr_in ResponseEndpointFromHpai(const uint8_t* body, size_t len, return EndpointFromHpaiAt(body, len, 0, fallback); } +bool HasUnsupportedHpaiProtocolAt(const uint8_t* body, size_t len, size_t offset, + bool allow_tcp) { + if (body == nullptr || offset + 2 > len || body[offset] != 0x08) { + return false; + } + const uint8_t protocol = body[offset + 1]; + return protocol != kKnxHpaiIpv4Udp && !(allow_tcp && protocol == kKnxHpaiIpv4Tcp); +} + void WriteBe16(uint8_t* data, uint16_t value) { data[0] = static_cast((value >> 8) & 0xff); data[1] = static_cast(value & 0xff); @@ -647,6 +658,18 @@ bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remot sizeof(remote)) == static_cast(len); } +bool SendStream(int sock, const uint8_t* data, size_t len) { + size_t sent = 0; + while (sent < len) { + const int written = send(sock, data + sent, len - sent, 0); + if (written <= 0) { + return false; + } + sent += static_cast(written); + } + return true; +} + std::vector KnxNetIpPacket(uint16_t service, const std::vector& body) { std::vector packet(6 + body.size()); packet[0] = kKnxNetIpHeaderSize; @@ -833,8 +856,20 @@ std::optional GatewayKnxConfigFromValue(const DaliValue* value 65535)); config.multicast_address = ObjectStringAny(object, {"multicastAddress", "multicast_address"}) .value_or(config.multicast_address); + config.ip_interface_individual_address = static_cast(std::clamp( + ObjectIntAny(object, {"ipInterfaceIndividualAddress", + "ip_interface_individual_address", + "ipInterfaceAddress", + "ip_interface_address"}) + .value_or(config.ip_interface_individual_address), + 0, 0xffff)); config.individual_address = static_cast(std::clamp( - ObjectIntAny(object, {"individualAddress", "individual_address"}) + ObjectIntAny(object, {"individualAddress", + "individual_address", + "knxDaliGatewayIndividualAddress", + "knx_dali_gateway_individual_address", + "deviceIndividualAddress", + "device_individual_address"}) .value_or(config.individual_address), 0, 0xffff)); config.programming_button_gpio = std::clamp( @@ -889,6 +924,8 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { out["mainGroup"] = static_cast(config.main_group); out["udpPort"] = static_cast(config.udp_port); out["multicastAddress"] = config.multicast_address; + out["ipInterfaceIndividualAddress"] = + static_cast(config.ip_interface_individual_address); out["individualAddress"] = static_cast(config.individual_address); out["programmingButtonGpio"] = config.programming_button_gpio; out["programmingButtonActiveLow"] = config.programming_button_active_low; @@ -1831,6 +1868,35 @@ const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; } bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; } +bool GatewayKnxTpIpRouter::programmingMode() { + if (openknx_lock_ == nullptr) { + return false; + } + SemaphoreGuard guard(openknx_lock_); + return ets_device_ != nullptr && ets_device_->programmingMode(); +} + +esp_err_t GatewayKnxTpIpRouter::setProgrammingMode(bool enabled) { + if (openknx_lock_ == nullptr) { + last_error_ = "KNX runtime lock is unavailable"; + return ESP_ERR_INVALID_STATE; + } + SemaphoreGuard guard(openknx_lock_); + if (ets_device_ == nullptr) { + last_error_ = "KNX OpenKNX runtime is unavailable"; + return ESP_ERR_INVALID_STATE; + } + ets_device_->setProgrammingMode(enabled); + setProgrammingLed(enabled); + ESP_LOGI(kTag, "KNX programming mode %s namespace=%s", + enabled ? "enabled" : "disabled", openknx_namespace_.c_str()); + return ESP_OK; +} + +esp_err_t GatewayKnxTpIpRouter::toggleProgrammingMode() { + return setProgrammingMode(!programmingMode()); +} + esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) { if (started_ || task_handle_ != nullptr) { return ESP_OK; @@ -1931,14 +1997,15 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { { SemaphoreGuard guard(openknx_lock_); ets_device_ = std::make_unique(openknx_namespace_, - config_.individual_address); + config_.individual_address, + effectiveTunnelAddress()); openknx_configured_.store(ets_device_->configured()); ESP_LOGI(kTag, - "OpenKNX runtime namespace=%s configured=%d individual=0x%04x tunnelClient=0x%04x " - "commissioningOnly=%d", + "OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x " + "device=0x%04x tunnelClient=0x%04x commissioningOnly=%d", openknx_namespace_.c_str(), ets_device_->configured(), - ets_device_->individualAddress(), ets_device_->tunnelClientAddress(), - commissioning_only_); + effectiveIpInterfaceIndividualAddress(), 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) { @@ -1994,34 +2061,7 @@ void GatewayKnxTpIpRouter::taskLoop() { 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, - reinterpret_cast(&remote), &remote_len); - if (received <= 0) { - pollTpUart(); - { - SemaphoreGuard guard(openknx_lock_); - if (ets_device_ != nullptr) { - pollProgrammingButton(); - ets_device_->loop(); - updateProgrammingLed(); - } - } - if (!stop_requested_) { - vTaskDelay(pdMS_TO_TICKS(10)); - } - continue; - } - handleUdpDatagram(buffer.data(), static_cast(received), remote); + auto run_maintenance = [this]() { pollTpUart(); { SemaphoreGuard guard(openknx_lock_); @@ -2031,6 +2071,69 @@ void GatewayKnxTpIpRouter::taskLoop() { updateProgrammingLed(); } } + }; + 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; + } + + fd_set read_fds; + FD_ZERO(&read_fds); + int max_fd = -1; + if (udp_sock_ >= 0) { + FD_SET(udp_sock_, &read_fds); + max_fd = std::max(max_fd, udp_sock_); + } + if (tcp_sock_ >= 0) { + FD_SET(tcp_sock_, &read_fds); + max_fd = std::max(max_fd, tcp_sock_); + } + for (const auto& client : tcp_clients_) { + if (client.sock >= 0) { + FD_SET(client.sock, &read_fds); + max_fd = std::max(max_fd, client.sock); + } + } + + timeval timeout{}; + timeout.tv_sec = 0; + timeout.tv_usec = 20000; + const int selected = max_fd >= 0 ? select(max_fd + 1, &read_fds, nullptr, nullptr, &timeout) + : 0; + if (selected < 0) { + ESP_LOGW(kTag, "KNXnet/IP socket select failed: errno=%d (%s)", errno, + std::strerror(errno)); + run_maintenance(); + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + if (selected == 0) { + run_maintenance(); + continue; + } + + if (tcp_sock_ >= 0 && FD_ISSET(tcp_sock_, &read_fds)) { + handleTcpAccept(); + } + for (auto& client : tcp_clients_) { + if (client.sock >= 0 && FD_ISSET(client.sock, &read_fds)) { + handleTcpClient(client); + } + } + sockaddr_in remote{}; + socklen_t remote_len = sizeof(remote); + if (udp_sock_ >= 0 && FD_ISSET(udp_sock_, &read_fds)) { + const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0, + reinterpret_cast(&remote), &remote_len); + if (received > 0) { + handleUdpDatagram(buffer.data(), static_cast(received), remote); + } + } + run_maintenance(); } finishTask(); } @@ -2093,6 +2196,15 @@ void GatewayKnxTpIpRouter::closeSockets() { close(udp_sock_); udp_sock_ = -1; } + if (tcp_sock_ >= 0) { + shutdown(tcp_sock_, SHUT_RDWR); + close(tcp_sock_); + tcp_sock_ = -1; + } + for (auto& client : tcp_clients_) { + closeTcpClient(client); + } + active_tcp_sock_ = -1; multicast_joined_interfaces_.clear(); network_refresh_tick_ = 0; for (auto& client : tunnel_clients_) { @@ -2106,7 +2218,7 @@ void GatewayKnxTpIpRouter::closeSockets() { } bool GatewayKnxTpIpRouter::configureSocket() { - udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_sock_ < 0) { last_error_ = ErrnoDetail("failed to create KNXnet/IP UDP socket", errno); ESP_LOGE(kTag, "%s", last_error_.c_str()); @@ -2149,15 +2261,139 @@ bool GatewayKnxTpIpRouter::configureSocket() { } refreshNetworkInterfaces(true); } + + tcp_sock_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (tcp_sock_ < 0) { + last_error_ = ErrnoDetail("failed to create KNXnet/IP TCP socket", errno); + ESP_LOGE(kTag, "%s", last_error_.c_str()); + closeSockets(); + return false; + } + int reuse = 1; + if (setsockopt(tcp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { + ESP_LOGW(kTag, "failed to enable TCP reuse for KNX port %u: errno=%d (%s)", + static_cast(config_.udp_port), errno, std::strerror(errno)); + } + if (bind(tcp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { + const int saved_errno = errno; + last_error_ = ErrnoDetail("failed to bind KNXnet/IP TCP socket on port " + + std::to_string(config_.udp_port), + saved_errno); + ESP_LOGE(kTag, "%s", last_error_.c_str()); + closeSockets(); + return false; + } + if (listen(tcp_sock_, static_cast(kMaxTcpClients)) < 0) { + const int saved_errno = errno; + last_error_ = ErrnoDetail("failed to listen on KNXnet/IP TCP port " + + std::to_string(config_.udp_port), + saved_errno); + ESP_LOGE(kTag, "%s", last_error_.c_str()); + closeSockets(); + return false; + } + ESP_LOGI(kTag, "KNXnet/IP listening on UDP/TCP port %u", + static_cast(config_.udp_port)); return true; } +void GatewayKnxTpIpRouter::handleTcpAccept() { + sockaddr_in remote{}; + socklen_t remote_len = sizeof(remote); + const int client_sock = accept(tcp_sock_, reinterpret_cast(&remote), &remote_len); + if (client_sock < 0) { + ESP_LOGW(kTag, "failed to accept KNXnet/IP TCP client: errno=%d (%s)", errno, + std::strerror(errno)); + return; + } + TcpClient* slot = nullptr; + for (auto& client : tcp_clients_) { + if (client.sock < 0) { + slot = &client; + break; + } + } + if (slot == nullptr) { + ESP_LOGW(kTag, "reject KNXnet/IP TCP client from %s: no free TCP slots", + EndpointString(remote).c_str()); + close(client_sock); + return; + } + slot->sock = client_sock; + slot->remote = remote; + slot->rx_buffer.clear(); + slot->last_activity_tick = xTaskGetTickCount(); + ESP_LOGI(kTag, "accepted KNXnet/IP TCP client from %s", EndpointString(remote).c_str()); +} + +void GatewayKnxTpIpRouter::handleTcpClient(TcpClient& client) { + if (client.sock < 0) { + return; + } + std::array buffer{}; + const int received = recv(client.sock, buffer.data(), buffer.size(), 0); + if (received <= 0) { + ESP_LOGI(kTag, "closed KNXnet/IP TCP client from %s", EndpointString(client.remote).c_str()); + closeTcpClient(client); + return; + } + client.last_activity_tick = xTaskGetTickCount(); + client.rx_buffer.insert(client.rx_buffer.end(), buffer.begin(), buffer.begin() + received); + while (client.rx_buffer.size() >= 6) { + uint16_t service = 0; + uint16_t total_len = 0; + if (!ParseKnxNetIpHeader(client.rx_buffer.data(), client.rx_buffer.size(), &service, + &total_len)) { + ESP_LOGW(kTag, "invalid KNXnet/IP TCP packet from %s; closing stream", + EndpointString(client.remote).c_str()); + closeTcpClient(client); + return; + } + if (client.rx_buffer.size() < total_len) { + return; + } + std::vector packet(client.rx_buffer.begin(), client.rx_buffer.begin() + total_len); + client.rx_buffer.erase(client.rx_buffer.begin(), client.rx_buffer.begin() + total_len); + active_tcp_sock_ = client.sock; + handleUdpDatagram(packet.data(), packet.size(), client.remote); + active_tcp_sock_ = -1; + if (client.sock < 0) { + return; + } + } +} + +void GatewayKnxTpIpRouter::closeTcpClient(TcpClient& client) { + if (client.sock < 0) { + client.rx_buffer.clear(); + return; + } + const int sock = client.sock; + for (auto& tunnel : tunnel_clients_) { + if (tunnel.connected && tunnel.tcp_sock == sock) { + resetTunnelClient(tunnel); + } + } + if (active_tcp_sock_ == sock) { + active_tcp_sock_ = -1; + } + shutdown(sock, SHUT_RDWR); + close(sock); + client.sock = -1; + client.rx_buffer.clear(); + client.last_activity_tick = 0; +} + void GatewayKnxTpIpRouter::refreshNetworkInterfaces(bool force_log) { if (!config_.multicast_enabled || udp_sock_ < 0) { return; } const auto netifs = ActiveKnxNetifs(); if (netifs.empty()) { + SemaphoreGuard guard(openknx_lock_); + if (ets_device_ != nullptr) { + ets_device_->setNetworkInterface(nullptr); + } if (force_log) { ESP_LOGW(kTag, "KNX multicast group %s not joined yet: no IPv4 interface is up", config_.multicast_address.c_str()); @@ -2165,6 +2401,13 @@ void GatewayKnxTpIpRouter::refreshNetworkInterfaces(bool force_log) { return; } + { + SemaphoreGuard guard(openknx_lock_); + if (ets_device_ != nullptr) { + ets_device_->setNetworkInterface(netifs.front().netif); + } + } + 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(), @@ -2338,8 +2581,8 @@ bool GatewayKnxTpIpRouter::initializeTpUart() { saw_reset = true; const std::array set_address{ kTpUartSetAddressRequest, - static_cast((effectiveIndividualAddress() >> 8) & 0xff), - static_cast(effectiveIndividualAddress() & 0xff), + static_cast((effectiveIpInterfaceIndividualAddress() >> 8) & 0xff), + static_cast(effectiveIpInterfaceIndividualAddress() & 0xff), }; uart_write_bytes(uart_port, set_address.data(), set_address.size()); const uint8_t state_request = kTpUartStateRequest; @@ -2427,6 +2670,11 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* body, size_t len, const sockaddr_in& remote) { + if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai())) { + ESP_LOGW(kTag, "ignore KNXnet/IP search request from %s: unsupported HPAI protocol", + EndpointString(remote).c_str()); + return; + } sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); selectOpenKnxNetworkInterface(response_remote); std::set requested_dibs; @@ -2439,10 +2687,8 @@ void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* } const uint8_t srp_type = body[offset + 1]; if (srp_type == 0x01) { - SemaphoreGuard guard(openknx_lock_); - if (ets_device_ == nullptr || !ets_device_->programmingMode()) { - return; - } + // The programming button belongs to the logical KNX-DALI device behind the tunnel. + return; } else if (srp_type == 0x02 && srp_len >= 8) { uint8_t mac[6]{}; if (!ReadBaseMac(mac) || std::memcmp(mac, body + offset + 2, 6) != 0) { @@ -2477,6 +2723,11 @@ void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { + if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai())) { + ESP_LOGW(kTag, "ignore KNXnet/IP description request from %s: unsupported HPAI protocol", + EndpointString(remote).c_str()); + return; + } const sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); selectOpenKnxNetworkInterface(response_remote); sendDescriptionResponse(response_remote); @@ -2562,9 +2813,11 @@ GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient( size_t free_index = 0; for (size_t index = 0; index < tunnel_clients_.size(); ++index) { auto& client = tunnel_clients_[index]; + const bool same_tcp_stream = active_tcp_sock_ >= 0 && client.tcp_sock == active_tcp_sock_; + const bool same_udp_endpoints = EndpointEquals(client.control_remote, control_remote) && + EndpointEquals(client.data_remote, data_remote); if (client.connected && client.connection_type == connection_type && - EndpointEquals(client.control_remote, control_remote) && - EndpointEquals(client.data_remote, data_remote)) { + (same_tcp_stream || same_udp_endpoints)) { 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); @@ -2593,6 +2846,7 @@ GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient( free_client->last_activity_tick = xTaskGetTickCount(); free_client->control_remote = control_remote; free_client->data_remote = data_remote; + free_client->tcp_sock = active_tcp_sock_; last_tunnel_channel_id_ = channel_id; return free_client; } @@ -2629,7 +2883,8 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } - if (!EndpointEquals(remote, client->data_remote)) { + const bool same_tcp_stream = client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock; + if (!same_tcp_stream && !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()); @@ -2693,7 +2948,8 @@ void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* body, sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } - if (!EndpointEquals(remote, client->data_remote)) { + const bool same_tcp_stream = client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock; + if (!same_tcp_stream && !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()); @@ -2720,6 +2976,13 @@ void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len, EndpointString(remote).c_str(), static_cast(len)); return; } + if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai()) || + HasUnsupportedHpaiProtocolAt(body, len, 8, currentTransportAllowsTcpHpai())) { + ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: unsupported HPAI protocol", + EndpointString(remote).c_str()); + sendConnectResponse(0, kKnxErrorConnectionType, remote, kKnxConnectionTypeTunnel, 0); + return; + } sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 0, remote); sockaddr_in data_remote = EndpointFromHpaiAt(body, len, 8, remote); selectOpenKnxNetworkInterface(control_remote); @@ -2748,6 +3011,12 @@ void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len, sendConnectResponse(0, kKnxErrorTunnellingLayer, control_remote, connection_type, 0); return; } + if (!SelectKnxNetifForRemote(control_remote).has_value()) { + ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: no active IPv4 interface for response", + EndpointString(remote).c_str()); + sendConnectResponse(0, kKnxErrorConnectionType, 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", @@ -2775,11 +3044,18 @@ void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, siz if (body == nullptr || len < 2) { return; } + if (HasUnsupportedHpaiProtocolAt(body, len, 2, currentTransportAllowsTcpHpai())) { + ESP_LOGW(kTag, + "reject KNXnet/IP connection-state request from %s: unsupported HPAI protocol", + EndpointString(remote).c_str()); + 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); + ((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) || + EndpointEquals(control_remote, client->control_remote)); const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId; if (client != nullptr) { if (endpoint_matches) { @@ -2799,19 +3075,26 @@ void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t l if (body == nullptr || len < 2) { return; } + if (HasUnsupportedHpaiProtocolAt(body, len, 2, currentTransportAllowsTcpHpai())) { + ESP_LOGW(kTag, "reject KNXnet/IP disconnect request from %s: unsupported HPAI protocol", + EndpointString(remote).c_str()); + 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); + ((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) || + EndpointEquals(control_remote, client->control_remote)); const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId; + const std::string expected = client == nullptr ? "none" : EndpointString(client->control_remote); if (status == kKnxNoError) { resetTunnelClient(*client); } 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()); + expected.c_str()); sendDisconnectResponse(channel_id, status, control_remote); } @@ -2859,22 +3142,51 @@ void GatewayKnxTpIpRouter::sendConnectionHeaderAck(uint16_t service, uint8_t cha const sockaddr_in& remote) { const std::vector body{0x04, channel_id, sequence, status}; const auto packet = KnxNetIpPacket(service, body); - SendAll(udp_sock_, packet.data(), packet.size(), remote); + sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) { const std::vector body{status, 0x00}; const auto packet = KnxNetIpPacket(kServiceSecureSessionStatus, body); - SendAll(udp_sock_, packet.data(), packet.size(), remote); + sendPacket(packet, remote); } -std::array GatewayKnxTpIpRouter::localHpaiForRemote( - const sockaddr_in& remote) const { +bool GatewayKnxTpIpRouter::sendPacket(const std::vector& packet, + const sockaddr_in& remote) const { + if (packet.empty()) { + return false; + } + if (active_tcp_sock_ >= 0) { + return SendStream(active_tcp_sock_, packet.data(), packet.size()); + } + return udp_sock_ >= 0 && SendAll(udp_sock_, packet.data(), packet.size(), remote); +} + +bool GatewayKnxTpIpRouter::sendPacketToTunnelClient( + const TunnelClient& client, const std::vector& packet) const { + if (packet.empty()) { + return false; + } + if (client.tcp_sock >= 0) { + return SendStream(client.tcp_sock, packet.data(), packet.size()); + } + return udp_sock_ >= 0 && SendAll(udp_sock_, packet.data(), packet.size(), client.data_remote); +} + +bool GatewayKnxTpIpRouter::currentTransportAllowsTcpHpai() const { + return active_tcp_sock_ >= 0; +} + +std::optional> GatewayKnxTpIpRouter::localHpaiForRemote( + const sockaddr_in& remote, bool tcp) const { + const auto netif = SelectKnxNetifForRemote(remote); + if (!netif.has_value()) { + return std::nullopt; + } 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)); + hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp; + WriteIp(hpai.data() + 2, netif->address); WriteBe16(hpai.data() + 6, config_.udp_port); return hpai; } @@ -2884,12 +3196,9 @@ std::vector GatewayKnxTpIpRouter::buildDeviceInfoDib( 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()); + dib[2] = advertisedMedium(); + dib[3] = 0; + WriteBe16(dib.data() + 4, effectiveIpInterfaceIndividualAddress()); WriteBe16(dib.data() + 6, 0); uint8_t mac[6]{}; @@ -2916,7 +3225,9 @@ std::vector GatewayKnxTpIpRouter::buildExtendedDeviceInfoDib() const { dib[2] = 0x01; dib[3] = 0x00; WriteBe16(dib.data() + 4, 254); - WriteBe16(dib.data() + 6, kKnxDeviceDescriptor); + WriteBe16(dib.data() + 6, + advertisedMedium() == kKnxMediumIp ? kKnxIpOnlyDeviceDescriptor + : kKnxTpIpInterfaceDeviceDescriptor); return dib; } @@ -2947,7 +3258,7 @@ 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()); + WriteBe16(dib.data() + 2, effectiveIpInterfaceIndividualAddress()); size_t offset = 4; for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) { WriteBe16(dib.data() + offset, effectiveTunnelAddressForSlot(slot)); @@ -3007,12 +3318,17 @@ std::vector GatewayKnxTpIpRouter::buildSupportedServiceDib() const { void GatewayKnxTpIpRouter::sendSearchResponse(uint16_t service, const sockaddr_in& remote, const std::set& requested_dibs) { - if (udp_sock_ < 0) { + if (udp_sock_ < 0 && active_tcp_sock_ < 0) { + return; + } + const auto hpai = localHpaiForRemote(remote, currentTransportAllowsTcpHpai()); + if (!hpai.has_value()) { + ESP_LOGW(kTag, "cannot send KNXnet/IP search response to %s: no active IPv4 interface", + EndpointString(remote).c_str()); return; } - const auto hpai = localHpaiForRemote(remote); std::vector body; - body.insert(body.end(), hpai.begin(), hpai.end()); + 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) { @@ -3055,17 +3371,17 @@ void GatewayKnxTpIpRouter::sendSearchResponse(uint16_t service, const sockaddr_i } } const auto packet = KnxNetIpPacket(service, body); - SendAll(udp_sock_, packet.data(), packet.size(), remote); + sendPacket(packet, 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((*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) { + if (udp_sock_ < 0 && active_tcp_sock_ < 0) { return; } auto device = buildDeviceInfoDib(remote); @@ -3075,14 +3391,15 @@ void GatewayKnxTpIpRouter::sendDescriptionResponse(const sockaddr_in& remote) { 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(), + sendPacket(packet, remote); + ESP_LOGI(kTag, "sent KNXnet/IP description response namespace=%s medium=0x%02x tpOnline=%d to %s:%u", + openknx_namespace_.c_str(), static_cast(advertisedMedium()), + tp_uart_online_, 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) { + if (data == nullptr || len == 0) { return; } for (auto& client : tunnel_clients_) { @@ -3094,7 +3411,7 @@ void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, size_t len) { - if (!client.connected || udp_sock_ < 0 || data == nullptr || len == 0) { + if (!client.connected || data == nullptr || len == 0) { return; } const uint16_t service = TunnelServiceForCemi(data, len); @@ -3106,7 +3423,7 @@ void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, co body.push_back(0x00); body.insert(body.end(), data, data + len); const auto packet = KnxNetIpPacket(service, body); - SendAll(udp_sock_, packet.data(), packet.size(), client.data_remote); + sendPacketToTunnelClient(client, packet); 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]), @@ -3117,14 +3434,14 @@ void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8 const sockaddr_in& remote) { const std::vector body{channel_id, status}; const auto packet = KnxNetIpPacket(kServiceConnectionStateResponse, body); - SendAll(udp_sock_, packet.data(), packet.size(), remote); + sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote) { const std::vector body{channel_id, status}; const auto packet = KnxNetIpPacket(kServiceDisconnectResponse, body); - SendAll(udp_sock_, packet.data(), packet.size(), remote); + sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status, @@ -3137,14 +3454,22 @@ void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t statu body.push_back(status); if (status != kKnxNoError) { const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); - SendAll(udp_sock_, packet.data(), packet.size(), remote); + sendPacket(packet, 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()); + const auto data_endpoint = localHpaiForRemote(remote, currentTransportAllowsTcpHpai()); + if (!data_endpoint.has_value()) { + ESP_LOGW(kTag, "cannot accept KNXnet/IP connect from %s: no active IPv4 interface", + EndpointString(remote).c_str()); + const auto packet = KnxNetIpPacket(kServiceConnectResponse, + std::vector{channel_id, kKnxErrorConnectionType}); + sendPacket(packet, remote); + return; + } + body.insert(body.end(), data_endpoint->begin(), data_endpoint->end()); body.push_back(connection_type == kKnxConnectionTypeTunnel ? 0x04 : 0x02); body.push_back(connection_type); if (connection_type == kKnxConnectionTypeTunnel) { @@ -3152,12 +3477,12 @@ void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t statu body.push_back(static_cast(tunnel_address & 0xff)); } const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); - SendAll(udp_sock_, packet.data(), packet.size(), remote); + sendPacket(packet, 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)); + 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) { @@ -3247,6 +3572,10 @@ bool GatewayKnxTpIpRouter::shouldRouteDaliApplicationFrames() const { return openknx_configured_.load(); } +uint8_t GatewayKnxTpIpRouter::advertisedMedium() const { + return (config_.tunnel_enabled || tp_uart_online_) ? kKnxMediumTp1 : kKnxMediumIp; +} + void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() { if (ets_device_ == nullptr) { return; @@ -3285,7 +3614,15 @@ void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() { bridge_.setConfig(config_); } -uint16_t GatewayKnxTpIpRouter::effectiveIndividualAddress() const { +uint16_t GatewayKnxTpIpRouter::effectiveIpInterfaceIndividualAddress() const { + if (config_.ip_interface_individual_address != 0 && + config_.ip_interface_individual_address != 0xffff) { + return config_.ip_interface_individual_address; + } + return 0xff01; +} + +uint16_t GatewayKnxTpIpRouter::effectiveKnxDeviceIndividualAddress() const { if (ets_device_ != nullptr) { const uint16_t address = ets_device_->individualAddress(); if (address != 0 && address != 0xffff) { @@ -3296,17 +3633,16 @@ uint16_t GatewayKnxTpIpRouter::effectiveIndividualAddress() const { } 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((config_.individual_address & 0x00ff) + 1); + const uint16_t interface_address = effectiveIpInterfaceIndividualAddress(); + uint16_t device = static_cast((interface_address & 0x00ff) + 1); if (device == 0 || device > 0xff) { device = 1; } - return static_cast((config_.individual_address & 0xff00) | device); + uint16_t address = static_cast((interface_address & 0xff00) | device); + if (address == 0xffff) { + address = static_cast((interface_address & 0xff00) | 0x0001); + } + return address; } void GatewayKnxTpIpRouter::pollTpUart() { 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 aa9293c..4e44ed5 100644 --- a/components/openknx_idf/include/openknx_idf/ets_device_runtime.h +++ b/components/openknx_idf/include/openknx_idf/ets_device_runtime.h @@ -23,7 +23,9 @@ class EtsDeviceRuntime { const uint8_t* data, size_t len, std::vector* response)>; - EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address); + EtsDeviceRuntime(std::string nvs_namespace, + uint16_t fallback_individual_address, + uint16_t tunnel_client_address = 0); ~EtsDeviceRuntime(); uint16_t individualAddress() const; diff --git a/components/openknx_idf/src/esp_idf_platform.cpp b/components/openknx_idf/src/esp_idf_platform.cpp index 439064e..91d776a 100644 --- a/components/openknx_idf/src/esp_idf_platform.cpp +++ b/components/openknx_idf/src/esp_idf_platform.cpp @@ -146,7 +146,7 @@ void EspIdfPlatform::fatalError() { void EspIdfPlatform::setupMultiCast(uint32_t addr, uint16_t port) { closeMultiCast(); - udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_sock_ < 0) { ESP_LOGE(kTag, "failed to create UDP socket: errno=%d", errno); return; @@ -157,7 +157,8 @@ void EspIdfPlatform::setupMultiCast(uint32_t addr, uint16_t port) { sockaddr_in bind_addr{}; bind_addr.sin_family = AF_INET; - bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); + const uint32_t local_address = currentIpAddress(); + bind_addr.sin_addr.s_addr = local_address == 0 ? htonl(INADDR_ANY) : local_address; bind_addr.sin_port = htons(port); if (bind(udp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { ESP_LOGE(kTag, "failed to bind UDP socket: errno=%d", errno); @@ -172,11 +173,20 @@ void EspIdfPlatform::setupMultiCast(uint32_t addr, uint16_t port) { ip_mreq mreq{}; mreq.imr_multiaddr.s_addr = htonl(addr); - mreq.imr_interface.s_addr = currentIpAddress(); + mreq.imr_interface.s_addr = local_address == 0 ? htonl(INADDR_ANY) : local_address; if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { ESP_LOGW(kTag, "failed to join KNX multicast group: errno=%d", errno); } + if (local_address != 0) { + in_addr multicast_interface{}; + multicast_interface.s_addr = local_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: errno=%d", errno); + } + } + uint8_t loop = 0; setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)); diff --git a/components/openknx_idf/src/ets_device_runtime.cpp b/components/openknx_idf/src/ets_device_runtime.cpp index 224cd3f..3668698 100644 --- a/components/openknx_idf/src/ets_device_runtime.cpp +++ b/components/openknx_idf/src/ets_device_runtime.cpp @@ -56,7 +56,8 @@ void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) { } // namespace EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, - uint16_t fallback_individual_address) + uint16_t fallback_individual_address, + uint16_t tunnel_client_address) : nvs_namespace_(std::move(nvs_namespace)), platform_(nullptr, nvs_namespace_.c_str()), device_(platform_) { @@ -75,7 +76,10 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, device_.deviceObject().individualAddress(fallback_individual_address); } if (auto* server = device_.getCemiServer()) { - server->clientAddress(DefaultTunnelClientAddress(device_.deviceObject().individualAddress())); + server->clientAddress(IsUsableIndividualAddress(tunnel_client_address) + ? tunnel_client_address + : DefaultTunnelClientAddress( + device_.deviceObject().individualAddress())); server->tunnelFrameCallback(&EtsDeviceRuntime::EmitTunnelFrame, this); } device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand);