#include "gateway_knx_private.hpp" namespace gateway { GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, std::string openknx_namespace) : bridge_(bridge), openknx_namespace_(std::move(openknx_namespace)) { openknx_lock_ = xSemaphoreCreateMutex(); startup_semaphore_ = xSemaphoreCreateBinary(); } GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); if (startup_semaphore_ != nullptr) { vSemaphoreDelete(startup_semaphore_); startup_semaphore_ = nullptr; } if (openknx_lock_ != nullptr) { vSemaphoreDelete(openknx_lock_); openknx_lock_ = nullptr; } } void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; } void GatewayKnxTpIpRouter::setCommissioningOnly(bool enabled) { commissioning_only_ = enabled; } void GatewayKnxTpIpRouter::setGroupWriteHandler(GroupWriteHandler handler) { group_write_handler_ = std::move(handler); } void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) { group_object_write_handler_ = std::move(handler); } void GatewayKnxTpIpRouter::setOamIpSecureCredentials( const GatewayKnxIpSecureCredentialMaterial& credentials) { oam_ip_secure_credentials_ = credentials; } void GatewayKnxTpIpRouter::setOamIpSecureRoutingSequenceStoreHandler( RoutingSequenceStoreHandler handler) { routing_sequence_store_handler_ = std::move(handler); } void GatewayKnxTpIpRouter::setCloudCemiPublisher(CloudCemiPublisher publisher) { cloud_cemi_publisher_ = std::move(publisher); } const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; } bool GatewayKnxTpIpRouter::injectCloudCemiFrame(const uint8_t* data, size_t len) { if (data == nullptr || len == 0 || !config_.oam_router.cloud_remote.enabled) { return false; } cloud_cemi_downlink_frames_.fetch_add(1, std::memory_order_relaxed); return handleOpenKnxTunnelFrame(data, len, nullptr, kServiceTunnellingRequest); } GatewayKnxTpIpRouter::CloudCemiStats GatewayKnxTpIpRouter::cloudCemiStats() const { return CloudCemiStats{ config_.oam_router.cloud_remote.enabled, cloud_cemi_uplink_frames_.load(std::memory_order_relaxed), cloud_cemi_downlink_frames_.load(std::memory_order_relaxed)}; } 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()); } bool GatewayKnxTpIpRouter::oamProgrammingMode() { if (openknx_lock_ == nullptr) { return false; } SemaphoreGuard guard(openknx_lock_); return oam_router_ != nullptr ? oam_router_->programmingMode() : oam_programming_mode_; } esp_err_t GatewayKnxTpIpRouter::setOamProgrammingMode(bool enabled) { if (openknx_lock_ == nullptr) { last_error_ = "KNX runtime lock is unavailable"; return ESP_ERR_INVALID_STATE; } if (!config_.oam_router.enabled) { last_error_ = "OAM KNX/IP router persona is disabled"; return ESP_ERR_NOT_SUPPORTED; } SemaphoreGuard guard(openknx_lock_); oam_programming_mode_ = enabled; if (oam_router_ != nullptr) { oam_router_->setProgrammingMode(enabled); } setOamProgrammingLed(enabled); ESP_LOGI(kTag, "OAM KNX/IP router programming mode %s namespace=%s", enabled ? "enabled" : "disabled", openknx_namespace_.c_str()); return ESP_OK; } esp_err_t GatewayKnxTpIpRouter::toggleOamProgrammingMode() { return setOamProgrammingMode(!oamProgrammingMode()); } esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) { if (started_ || task_handle_ != nullptr) { return ESP_OK; } if (openknx_lock_ == nullptr || startup_semaphore_ == nullptr) { last_error_ = "failed to allocate KNX runtime synchronization primitives"; return ESP_ERR_NO_MEM; } if (!config_.ip_router_enabled) { last_error_ = "KNXnet/IP router is disabled in config"; return ESP_ERR_NOT_SUPPORTED; } stop_requested_ = false; last_error_.clear(); int log_tp_uart_tx_pin = -1; int log_tp_uart_rx_pin = -1; if (config_.tp_uart.uart_port >= 0 && config_.tp_uart.uart_port < SOC_UART_NUM) { const uart_port_t log_uart_port = static_cast(config_.tp_uart.uart_port); ResolveUartIoPin(log_uart_port, config_.tp_uart.tx_pin, SOC_UART_TX_PIN_IDX, &log_tp_uart_tx_pin); ResolveUartIoPin(log_uart_port, config_.tp_uart.rx_pin, SOC_UART_RX_PIN_IDX, &log_tp_uart_rx_pin); } ESP_LOGI(kTag, "starting KNXnet/IP router namespace=%s udp=%u tunnel=%d multicast=%d group=%s " "tpUart=%d tx=%s rx=%s nineBit=%d commissioningOnly=%d", openknx_namespace_.c_str(), static_cast(config_.udp_port), config_.tunnel_enabled, config_.multicast_enabled, config_.multicast_address.c_str(), config_.tp_uart.uart_port, UartPinDescription(config_.tp_uart.tx_pin, log_tp_uart_tx_pin).c_str(), UartPinDescription(config_.tp_uart.rx_pin, log_tp_uart_rx_pin).c_str(), config_.tp_uart.nine_bit_mode, commissioning_only_); if (!configureSocket()) { return ESP_FAIL; } while (xSemaphoreTake(startup_semaphore_, 0) == pdTRUE) { } startup_result_ = ESP_ERR_TIMEOUT; const BaseType_t created = xTaskCreate(&GatewayKnxTpIpRouter::TaskEntry, "gw_knx_ip", task_stack_size, this, task_priority, &task_handle_); if (created != pdPASS) { task_handle_ = nullptr; closeSockets(); return ESP_ERR_NO_MEM; } if (xSemaphoreTake(startup_semaphore_, pdMS_TO_TICKS(10000)) != pdTRUE) { last_error_ = "timed out starting KNXnet/IP OpenKNX runtime"; stop_requested_ = true; closeSockets(); return ESP_ERR_TIMEOUT; } return startup_result_; } esp_err_t GatewayKnxTpIpRouter::stop() { stop_requested_ = true; closeSockets(); const TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); for (int attempt = 0; task_handle_ != nullptr && task_handle_ != current_task && attempt < 50; ++attempt) { vTaskDelay(pdMS_TO_TICKS(10)); } return ESP_OK; } bool GatewayKnxTpIpRouter::started() const { return started_; } const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; } bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target, uint8_t actual_level) { if (!started_ || !config_.ip_router_enabled || !shouldRouteDaliApplicationFrames()) { return false; } uint16_t switch_object = 0; uint16_t dimm_object = 0; if (target.kind == GatewayKnxDaliTargetKind::kShortAddress) { if (target.address < 0 || target.address > 63) { return false; } const uint16_t base = kGwReg1AdrKoOffset + kGwReg1AdrKoBlockSize * static_cast(target.address); switch_object = base + kGwReg1KoSwitchState; dimm_object = base + kGwReg1KoDimmState; } else if (target.kind == GatewayKnxDaliTargetKind::kGroup) { if (target.address < 0 || target.address > 15) { return false; } const uint16_t base = kGwReg1GrpKoOffset + kGwReg1GrpKoBlockSize * static_cast(target.address); switch_object = base + kGwReg1KoSwitchState; dimm_object = base + kGwReg1KoDimmState; } else if (target.kind == GatewayKnxDaliTargetKind::kBroadcast) { switch_object = kGwReg1AppKoBroadcastSwitch; dimm_object = kGwReg1AppKoBroadcastDimm; } else { return false; } const uint8_t switch_value = actual_level > 0 ? 1 : 0; const uint8_t dimm_value = DaliArcLevelToDpt5(actual_level); bool emitted = emitOpenKnxGroupValue(switch_object, &switch_value, 1); emitted = emitOpenKnxGroupValue(dimm_object, &dimm_value, 1) || emitted; return emitted; } void GatewayKnxTpIpRouter::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { { SemaphoreGuard guard(openknx_lock_); auto tp_uart_interface = createOpenKnxTpUartInterface(); if (GatewayKnxConfigUsesTpUart(config_) && tp_uart_interface == nullptr && !last_error_.empty()) { return ESP_FAIL; } ets_device_ = std::make_unique(openknx_namespace_, config_.individual_address, effectiveTunnelAddress(), std::move(tp_uart_interface)); bridge_.setRuntimeContext(ets_device_.get()); knx_ip_parameters_ = std::make_unique( ets_device_->deviceObject(), ets_device_->platform()); if (config_.oam_router.enabled) { oam_router_ = std::make_unique( openknx_namespace_ + "_oam", config_.oam_router.individual_address, config_.oam_router.tunnel_address_base); if (oam_router_->available()) { oam_router_->setProgrammingMode(oam_programming_mode_); } else { ESP_LOGW(kTag, "OAM router persona requested but BAU091A support is not compiled in"); oam_router_.reset(); } } else { oam_router_.reset(); } openknx_configured_.store(ets_device_->configured()); ESP_LOGI(kTag, "OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x " "device=0x%04x tunnelClient=0x%04x commissioningOnly=%d", openknx_namespace_.c_str(), ets_device_->configured(), effectiveIpInterfaceIndividualAddress(), ets_device_->individualAddress(), ets_device_->tunnelClientAddress(), commissioning_only_); if (oam_router_ != nullptr) { ESP_LOGI(kTag, "OAM router persona namespace=%s_oam configured=%d device=0x%04x tunnelClient=0x%04x secureTunnel=%d secureRouting=%d", openknx_namespace_.c_str(), oam_router_->configured(), oam_router_->individualAddress(), oam_router_->tunnelClientAddress(), config_.oam_router.secure_tunnel_enabled, config_.oam_router.secure_routing_enabled); } ets_device_->setFunctionPropertyHandlers( [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (!shouldRouteDaliApplicationFrames()) { return false; } return bridge_.handleFunctionPropertyCommand(object_index, property_id, data, len, response); }, [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (!shouldRouteDaliApplicationFrames()) { return false; } return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response); }); ets_device_->setFunctionPropertyExtHandlers( [this](uint16_t object_type, uint8_t object_instance, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { return handleFunctionPropertyExtCommand(object_type, object_instance, property_id, data, len, response); }, [this](uint16_t object_type, uint8_t object_instance, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { return handleFunctionPropertyExtState(object_type, object_instance, property_id, data, len, response); }); ets_device_->setGroupWriteHandler( [this](uint16_t group_address, const uint8_t* data, size_t len) { if (!shouldRouteDaliApplicationFrames()) { return; } const DaliBridgeResult result = group_write_handler_ ? group_write_handler_(group_address, data, len) : bridge_.handleGroupWrite(group_address, data, len); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str()); } }); ets_device_->setGroupObjectWriteHandler( [this](uint16_t group_object_number, const uint8_t* data, size_t len) { if (!shouldRouteDaliApplicationFrames()) { return IgnoredResult( GwReg1GroupAddressForObject(config_.main_group, group_object_number), group_object_number, "routing blocked by commissioning-only state"); } const DaliBridgeResult result = group_object_write_handler_ ? group_object_write_handler_(group_object_number, data, len) : bridge_.handleGroupObjectWrite(group_object_number, data, len); const bool ignored = getObjectBool(result.metadata, "ignored").value_or(false); if (ignored) { const auto reason = getObjectString(result.metadata, "reason").value_or("ignored"); ESP_LOGW(kTag, "OpenKNX group object %u accepted by ETS but ignored: %s", static_cast(group_object_number), reason.c_str()); } else if (!result.ok && !result.error.empty()) { ESP_LOGW(kTag, "OpenKNX group object %u not routed to DALI: %s", static_cast(group_object_number), result.error.c_str()); } return result; }); ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) { publishCloudCemiFrame(data, len); sendTunnelIndication(data, len); sendRoutingIndication(data, len); }); syncOpenKnxConfigFromDevice(); } if (!configureTpUart()) { last_error_ = last_error_.empty() ? "failed to configure KNX TP-UART" : last_error_; return ESP_FAIL; } if (!configureProgrammingGpio()) { last_error_ = last_error_.empty() ? "failed to configure KNX programming GPIO" : last_error_; return ESP_FAIL; } return ESP_OK; } void GatewayKnxTpIpRouter::taskLoop() { startup_result_ = initializeRuntime(); if (startup_result_ == ESP_OK) { started_ = true; } if (startup_semaphore_ != nullptr) { xSemaphoreGive(startup_semaphore_); } if (startup_result_ != ESP_OK || stop_requested_) { finishTask(); return; } std::array buffer{}; auto run_maintenance = [this]() { { SemaphoreGuard guard(openknx_lock_); if (ets_device_ != nullptr) { pollProgrammingButton(); ets_device_->loop(); if (oam_router_ != nullptr) { oam_router_->loop(); oam_programming_mode_ = oam_router_->programmingMode(); } tp_uart_online_ = ets_device_->tpUartOnline(); 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(); } void GatewayKnxTpIpRouter::finishTask() { closeSockets(); { SemaphoreGuard guard(openknx_lock_); setProgrammingLed(false); setOamProgrammingLed(false); oam_programming_mode_ = false; knx_ip_parameters_.reset(); bridge_.setRuntimeContext(nullptr); oam_router_.reset(); ets_device_.reset(); openknx_configured_.store(false); } started_ = false; task_handle_ = nullptr; vTaskDelete(nullptr); } void GatewayKnxTpIpRouter::pollProgrammingButton() { const TickType_t now = xTaskGetTickCount(); if (config_.programming_button_gpio >= 0 && ets_device_ != nullptr) { const int level = gpio_get_level(static_cast(config_.programming_button_gpio)); const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0; if (pressed && !programming_button_last_pressed_ && now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) { ets_device_->toggleProgrammingMode(); ESP_LOGI(kTag, "KNX programming mode %s namespace=%s", ets_device_->programmingMode() ? "enabled" : "disabled", openknx_namespace_.c_str()); programming_button_last_toggle_tick_ = now; } programming_button_last_pressed_ = pressed; } if (!config_.oam_router.enabled || config_.oam_router.programming_button_gpio < 0) { return; } const int oam_level = gpio_get_level( static_cast(config_.oam_router.programming_button_gpio)); const bool oam_pressed = config_.oam_router.programming_button_active_low ? oam_level == 0 : oam_level != 0; if (oam_pressed && !oam_programming_button_last_pressed_ && now - oam_programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) { oam_programming_mode_ = !oam_programming_mode_; if (oam_router_ != nullptr) { oam_router_->setProgrammingMode(oam_programming_mode_); } setOamProgrammingLed(oam_programming_mode_); ESP_LOGI(kTag, "OAM KNX/IP router programming mode %s namespace=%s", oam_programming_mode_ ? "enabled" : "disabled", openknx_namespace_.c_str()); oam_programming_button_last_toggle_tick_ = now; } oam_programming_button_last_pressed_ = oam_pressed; } void GatewayKnxTpIpRouter::updateProgrammingLed() { if (config_.programming_led_gpio >= 0 && ets_device_ != nullptr) { const bool programming_mode = ets_device_->programmingMode(); if (programming_mode != programming_led_state_) { setProgrammingLed(programming_mode); } } if (config_.oam_router.enabled && config_.oam_router.programming_led_gpio >= 0 && oam_programming_mode_ != oam_programming_led_state_) { setOamProgrammingLed(oam_programming_mode_); } } void GatewayKnxTpIpRouter::setProgrammingLed(bool on) { if (config_.programming_led_gpio < 0) { programming_led_state_ = on; return; } const bool level = config_.programming_led_active_high ? on : !on; gpio_set_level(static_cast(config_.programming_led_gpio), level ? 1 : 0); programming_led_state_ = on; } void GatewayKnxTpIpRouter::setOamProgrammingLed(bool on) { if (config_.oam_router.programming_led_gpio < 0) { oam_programming_led_state_ = on; return; } const bool level = config_.oam_router.programming_led_active_high ? on : !on; gpio_set_level(static_cast(config_.oam_router.programming_led_gpio), level ? 1 : 0); oam_programming_led_state_ = on; } void GatewayKnxTpIpRouter::closeSockets() { if (udp_sock_ >= 0) { shutdown(udp_sock_, SHUT_RDWR); 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_) { resetTunnelClient(client); } last_tunnel_channel_id_ = 0; } bool GatewayKnxTpIpRouter::configureSocket() { 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()); return false; } int broadcast = 1; if (setsockopt(udp_sock_, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { ESP_LOGW(kTag, "failed to enable broadcast for KNX UDP port %u: errno=%d (%s)", static_cast(config_.udp_port), errno, std::strerror(errno)); } sockaddr_in bind_addr{}; bind_addr.sin_family = AF_INET; bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); bind_addr.sin_port = htons(config_.udp_port); if (bind(udp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { const int saved_errno = errno; last_error_ = ErrnoDetail("failed to bind KNXnet/IP UDP socket on port " + std::to_string(config_.udp_port), saved_errno); ESP_LOGE(kTag, "%s", last_error_.c_str()); closeSockets(); return false; } timeval timeout{}; timeout.tv_sec = 0; timeout.tv_usec = 20000; if (setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { ESP_LOGW(kTag, "failed to set KNX UDP receive timeout on port %u: errno=%d (%s)", static_cast(config_.udp_port), errno, std::strerror(errno)); } if (config_.multicast_enabled) { uint8_t multicast_loop = 0; if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop, sizeof(multicast_loop)) < 0) { ESP_LOGW(kTag, "failed to disable KNX multicast loopback for %s: errno=%d (%s)", config_.multicast_address.c_str(), errno, std::strerror(errno)); } refreshNetworkInterfaces(true); } 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)); } sockaddr_in tcp_bind_addr{}; tcp_bind_addr.sin_family = AF_INET; tcp_bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); tcp_bind_addr.sin_port = htons(config_.udp_port); if (bind(tcp_sock_, reinterpret_cast(&tcp_bind_addr), sizeof(tcp_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); } } closeSecureSessionsForTcp(sock); 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()); } 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(), netif.address) != multicast_joined_interfaces_.end()) { continue; } ip_mreq mreq{}; mreq.imr_multiaddr.s_addr = multicast_address; mreq.imr_interface.s_addr = netif.address; if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { ESP_LOGW(kTag, "failed to join KNX multicast group %s on %s %s UDP port %u: errno=%d (%s)", config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(), static_cast(config_.udp_port), errno, std::strerror(errno)); continue; } multicast_joined_interfaces_.push_back(netif.address); ESP_LOGI(kTag, "joined KNX multicast group %s on %s %s UDP port %u", config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(), static_cast(config_.udp_port)); } } std::unique_ptr GatewayKnxTpIpRouter::createOpenKnxTpUartInterface() { if (!GatewayKnxConfigUsesTpUart(config_)) { tp_uart_port_ = -1; tp_uart_tx_pin_ = -1; tp_uart_rx_pin_ = -1; tp_uart_online_ = false; ESP_LOGI(kTag, "KNX TP-UART disabled by UART port; KNXnet/IP uses IP-only runtime"); return nullptr; } const auto& serial = config_.tp_uart; if (serial.uart_port < 0 || serial.uart_port > 2) { last_error_ = "invalid KNX TP-UART port " + std::to_string(serial.uart_port); ESP_LOGE(kTag, "%s", last_error_.c_str()); return nullptr; } const uart_port_t uart_port = static_cast(serial.uart_port); int tx_pin = UART_PIN_NO_CHANGE; int rx_pin = UART_PIN_NO_CHANGE; const bool tx_pin_ok = ResolveUartIoPin(uart_port, serial.tx_pin, SOC_UART_TX_PIN_IDX, &tx_pin); const bool rx_pin_ok = ResolveUartIoPin(uart_port, serial.rx_pin, SOC_UART_RX_PIN_IDX, &rx_pin); if (!tx_pin_ok || !rx_pin_ok) { last_error_ = "KNX TP-UART UART" + std::to_string(serial.uart_port) + " has no ESP-IDF default " + (!tx_pin_ok ? std::string("TX") : std::string("")) + (!tx_pin_ok && !rx_pin_ok ? "/" : "") + (!rx_pin_ok ? std::string("RX") : std::string("")) + " pin; configure explicit txPin/rxPin values"; ESP_LOGE(kTag, "%s", last_error_.c_str()); return nullptr; } tp_uart_port_ = serial.uart_port; tp_uart_tx_pin_ = tx_pin; tp_uart_rx_pin_ = rx_pin; return std::make_unique( uart_port, serial.tx_pin, serial.rx_pin, serial.rx_buffer_size, serial.tx_buffer_size, serial.nine_bit_mode); } bool GatewayKnxTpIpRouter::configureTpUart() { if (tp_uart_port_ < 0) { return true; } if (ets_device_ == nullptr || !ets_device_->hasTpUart()) { last_error_ = "KNX TP-UART interface is unavailable in OpenKNX runtime"; ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } const TickType_t startup_timeout_ticks = pdMS_TO_TICKS(config_.tp_uart.startup_timeout_ms); const TickType_t retry_poll_ticks = std::max(1, pdMS_TO_TICKS(20)); const TickType_t startup_begin_tick = xTaskGetTickCount(); tp_uart_online_ = ets_device_->enableTpUart(true); while (!tp_uart_online_ && startup_timeout_ticks > 0 && (xTaskGetTickCount() - startup_begin_tick) < startup_timeout_ticks) { vTaskDelay(retry_poll_ticks); ets_device_->loop(); tp_uart_online_ = ets_device_->tpUartOnline(); } if (!tp_uart_online_) { last_error_ = "OpenKNX failed to initialize KNX TP-UART uart=" + std::to_string(config_.tp_uart.uart_port) + " tx=" + UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_) + " rx=" + UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_); const bool configured = ets_device_ != nullptr && ets_device_->configured(); ESP_LOGW(kTag, "%s; continuing KNXnet/IP in %s IP mode while TP-UART stays offline", last_error_.c_str(), configured ? "configured" : "commissioning-only"); tp_uart_port_ = -1; tp_uart_online_ = false; return true; } const TickType_t startup_elapsed_ticks = xTaskGetTickCount() - startup_begin_tick; if (startup_elapsed_ticks > 0) { ESP_LOGI(kTag, "KNX TP-UART startup settled after %lu ms", static_cast(pdTICKS_TO_MS(startup_elapsed_ticks))); } ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%s rx=%s baud=%u nineBit=%d", config_.tp_uart.uart_port, UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_).c_str(), UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_).c_str(), static_cast(config_.tp_uart.baudrate), config_.tp_uart.nine_bit_mode); return true; } bool GatewayKnxTpIpRouter::configureProgrammingGpio() { programming_button_last_pressed_ = false; programming_button_last_toggle_tick_ = 0; programming_led_state_ = false; oam_programming_button_last_pressed_ = false; oam_programming_button_last_toggle_tick_ = 0; oam_programming_led_state_ = false; if (config_.programming_button_gpio >= 0) { gpio_config_t button_config{}; button_config.pin_bit_mask = 1ULL << static_cast(config_.programming_button_gpio); button_config.mode = GPIO_MODE_INPUT; button_config.pull_up_en = config_.programming_button_active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; button_config.pull_down_en = config_.programming_button_active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE; button_config.intr_type = GPIO_INTR_DISABLE; const esp_err_t err = gpio_config(&button_config); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure KNX programming button GPIO" + std::to_string(config_.programming_button_gpio), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } } if (config_.programming_led_gpio >= 0) { gpio_config_t led_config{}; led_config.pin_bit_mask = 1ULL << static_cast(config_.programming_led_gpio); led_config.mode = GPIO_MODE_OUTPUT; led_config.pull_up_en = GPIO_PULLUP_DISABLE; led_config.pull_down_en = GPIO_PULLDOWN_DISABLE; led_config.intr_type = GPIO_INTR_DISABLE; const esp_err_t err = gpio_config(&led_config); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure KNX programming LED GPIO" + std::to_string(config_.programming_led_gpio), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } setProgrammingLed(false); } if (config_.oam_router.enabled && config_.oam_router.programming_button_gpio >= 0) { gpio_config_t button_config{}; button_config.pin_bit_mask = 1ULL << static_cast(config_.oam_router.programming_button_gpio); button_config.mode = GPIO_MODE_INPUT; button_config.pull_up_en = config_.oam_router.programming_button_active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; button_config.pull_down_en = config_.oam_router.programming_button_active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE; button_config.intr_type = GPIO_INTR_DISABLE; const esp_err_t err = gpio_config(&button_config); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure OAM KNX programming button GPIO" + std::to_string(config_.oam_router.programming_button_gpio), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } } if (config_.oam_router.enabled && config_.oam_router.programming_led_gpio >= 0) { gpio_config_t led_config{}; led_config.pin_bit_mask = 1ULL << static_cast(config_.oam_router.programming_led_gpio); led_config.mode = GPIO_MODE_OUTPUT; led_config.pull_up_en = GPIO_PULLUP_DISABLE; led_config.pull_down_en = GPIO_PULLDOWN_DISABLE; led_config.intr_type = GPIO_INTR_DISABLE; const esp_err_t err = gpio_config(&led_config); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure OAM KNX programming LED GPIO" + std::to_string(config_.oam_router.programming_led_gpio), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } setOamProgrammingLed(false); } return true; } } // namespace gateway