Files
gateway/components/gateway_knx/src/gateway_knx_router_lifecycle.cpp
T

970 lines
37 KiB
C++

#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<uart_port_t>(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<unsigned>(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<uint16_t>(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<uint16_t>(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<GatewayKnxTpIpRouter*>(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::EtsDeviceRuntime>(openknx_namespace_,
config_.individual_address,
effectiveTunnelAddress(),
std::move(tp_uart_interface));
bridge_.setRuntimeContext(ets_device_.get());
knx_ip_parameters_ = std::make_unique<IpParameterObject>(
ets_device_->deviceObject(), ets_device_->platform());
if (config_.oam_router.enabled) {
oam_router_ = std::make_unique<openknx::OamRouterRuntime>(
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<uint8_t>* 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<uint8_t>* 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<uint8_t>* 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<uint8_t>* 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<unsigned>(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<unsigned>(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<uint8_t, 768> 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<sockaddr*>(&remote), &remote_len);
if (received > 0) {
handleUdpDatagram(buffer.data(), static_cast<size_t>(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<gpio_num_t>(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<gpio_num_t>(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<gpio_num_t>(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<gpio_num_t>(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<unsigned>(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<sockaddr*>(&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<unsigned>(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<unsigned>(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<sockaddr*>(&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<int>(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<unsigned>(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<sockaddr*>(&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<uint8_t, 512> 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<uint8_t> 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<unsigned>(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<unsigned>(config_.udp_port));
}
}
std::unique_ptr<openknx::TpuartUartInterface>
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<uart_port_t>(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<openknx::TpuartUartInterface>(
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<TickType_t>(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<unsigned long>(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<unsigned>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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