Add GatewayKnxTpIpRouter implementation for handling KNXnet/IP services

- Implemented handleUdpDatagram to process incoming UDP datagrams and route them to appropriate handlers based on service type.
- Added methods for handling various KNXnet/IP requests including search, description, tunneling, device configuration, connection state, and disconnect requests.
- Introduced TunnelClient management for handling multiple tunnel connections, including allocation, resetting, and pruning stale clients.
- Implemented secure service handling with appropriate logging for unsupported secure sessions.
- Enhanced logging for better traceability of incoming requests and responses.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-21 14:12:46 +08:00
parent 8e80fd05b4
commit 2b8ef31263
8 changed files with 4513 additions and 4467 deletions
+5
View File
@@ -1,6 +1,11 @@
idf_component_register(
SRCS
"src/gateway_knx.cpp"
"src/gateway_knx_bridge.cpp"
"src/gateway_knx_router_lifecycle.cpp"
"src/gateway_knx_router_openknx.cpp"
"src/gateway_knx_router_packets.cpp"
"src/gateway_knx_router_services.cpp"
"src/ets_device_runtime.cpp"
"src/ets_memory_loader.cpp"
INCLUDE_DIRS "include"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,798 @@
#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);
}
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;
}
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());
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_);
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) {
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();
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);
knx_ip_parameters_.reset();
bridge_.setRuntimeContext(nullptr);
ets_device_.reset();
openknx_configured_.store(false);
}
started_ = false;
task_handle_ = nullptr;
vTaskDelete(nullptr);
}
void GatewayKnxTpIpRouter::pollProgrammingButton() {
if (config_.programming_button_gpio < 0 || ets_device_ == nullptr) {
return;
}
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.programming_button_gpio));
const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0;
const TickType_t now = xTaskGetTickCount();
if (pressed && !programming_button_last_pressed_ &&
now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
ets_device_->toggleProgrammingMode();
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
ets_device_->programmingMode() ? "enabled" : "disabled",
openknx_namespace_.c_str());
programming_button_last_toggle_tick_ = now;
}
programming_button_last_pressed_ = pressed;
}
void GatewayKnxTpIpRouter::updateProgrammingLed() {
if (config_.programming_led_gpio < 0 || ets_device_ == nullptr) {
return;
}
const bool programming_mode = ets_device_->programmingMode();
if (programming_mode == programming_led_state_) {
return;
}
setProgrammingLed(programming_mode);
}
void GatewayKnxTpIpRouter::setProgrammingLed(bool on) {
if (config_.programming_led_gpio < 0) {
programming_led_state_ = on;
return;
}
const bool level = config_.programming_led_active_high ? on : !on;
gpio_set_level(static_cast<gpio_num_t>(config_.programming_led_gpio), level ? 1 : 0);
programming_led_state_ = on;
}
void GatewayKnxTpIpRouter::closeSockets() {
if (udp_sock_ >= 0) {
shutdown(udp_sock_, SHUT_RDWR);
close(udp_sock_);
udp_sock_ = -1;
}
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);
}
}
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;
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);
}
return true;
}
} // namespace gateway
@@ -0,0 +1,319 @@
#include "gateway_knx_private.hpp"
namespace gateway {
void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remote) {
const auto netif = SelectKnxNetifForRemote(remote);
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
}
}
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len,
TunnelClient* response_client,
uint16_t response_service,
const uint8_t* suppress_routing_echo,
size_t suppress_routing_echo_len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
std::vector<uint8_t> tunnel_confirmation;
const bool needs_tunnel_confirmation =
response_client != nullptr && response_client->connected &&
response_service == kServiceTunnellingRequest &&
BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation);
bool sent_tunnel_confirmation = false;
const bool consumed = ets_device_->handleTunnelFrame(
data, len,
[this, response_client, response_service, needs_tunnel_confirmation,
&tunnel_confirmation, &sent_tunnel_confirmation,
suppress_routing_echo, suppress_routing_echo_len](const uint8_t* response,
size_t response_len) {
if (response == nullptr || response_len == 0) {
return;
}
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
if (routing_context && suppress_routing_echo != nullptr &&
IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo,
suppress_routing_echo_len)) {
return;
}
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
message_code.has_value() && message_code.value() != L_data_con) {
sent_tunnel_confirmation = sendCemiFrameToClient(
*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
if (service == kServiceDeviceConfigurationRequest) {
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
} else if (routing_context) {
sendRoutingIndication(response, response_len);
}
return;
}
if (message_code.has_value() && message_code.value() == L_data_con) {
if (routing_context) {
return;
}
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response, response_len) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response, response_len);
return;
}
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
return;
}
sendTunnelIndication(response, response_len);
});
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
syncOpenKnxConfigFromDevice();
return consumed;
}
bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool sent = ets_device_->transmitTpFrame(data, len);
tp_uart_online_ = ets_device_->tpUartOnline();
return sent;
}
bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool consumed = ets_device_->handleBusFrame(data, len);
syncOpenKnxConfigFromDevice();
return consumed;
}
bool GatewayKnxTpIpRouter::routeOpenKnxGroupWrite(const uint8_t* data, size_t len,
const char* context) {
const auto decoded = DecodeOpenKnxGroupWrite(data, len);
if (!decoded.has_value()) {
return false;
}
if (!shouldRouteDaliApplicationFrames()) {
return true;
}
const DaliBridgeResult result = group_write_handler_
? group_write_handler_(decoded->group_address,
decoded->data.data(),
decoded->data.size())
: bridge_.handleGroupWrite(decoded->group_address,
decoded->data.data(),
decoded->data.size());
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "%s not routed to DALI: %s", context == nullptr ? "KNX group write" : context,
result.error.c_str());
}
return true;
}
bool GatewayKnxTpIpRouter::handleFunctionPropertyExtCommand(
uint16_t object_type, uint8_t object_instance, uint8_t property_id,
const uint8_t* data, size_t len, std::vector<uint8_t>* response) {
if (response == nullptr || object_type != kGroupObjectTableObjectType ||
property_id != kPidGoDiagnostics) {
return false;
}
const auto decoded = DecodeGoDiagnosticsGroupWrite(data, len);
if (!decoded.has_value()) {
const std::string payload = HexBytes(data, len);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics write malformed objType=0x%04X objInst=%u property=0x%02X len=%u payload=%s",
static_cast<unsigned>(object_type), static_cast<unsigned>(object_instance),
static_cast<unsigned>(property_id), static_cast<unsigned>(len), payload.c_str());
*response = {ReturnCodes::DataVoid};
return true;
}
const std::string group_address_text =
GatewayKnxGroupAddressString(decoded->group_address);
const std::string payload = HexBytes(decoded->payload, decoded->payload_len);
ESP_LOGI(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) len=%u payload=%s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
static_cast<unsigned>(decoded->payload_len), payload.c_str());
if (!shouldRouteDaliApplicationFrames()) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) blocked by commissioning-only routing state",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str());
*response = {ReturnCodes::TemporarilyNotAvailable};
return true;
}
const DaliBridgeResult result =
group_write_handler_ ? group_write_handler_(decoded->group_address, decoded->payload,
decoded->payload_len)
: bridge_.handleGroupWrite(decoded->group_address,
decoded->payload,
decoded->payload_len);
const uint8_t return_code = GoDiagnosticsReturnCode(result);
if (return_code == ReturnCodes::AddressVoid) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) returning AddressVoid: %s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
result.error.empty() ? "unmapped KNX group address"
: result.error.c_str());
} else if (!result.ok) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) failed rc=0x%02X: %s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
static_cast<unsigned>(return_code),
result.error.empty() ? "command routing failed" : result.error.c_str());
}
response->assign(1, return_code);
return true;
}
bool GatewayKnxTpIpRouter::handleFunctionPropertyExtState(
uint16_t object_type, uint8_t object_instance, uint8_t property_id,
const uint8_t* data, size_t len, std::vector<uint8_t>* response) {
if (response == nullptr || object_type != kGroupObjectTableObjectType ||
property_id != kPidGoDiagnostics) {
return false;
}
const auto decoded = DecodeGoDiagnosticsGroupWrite(data, len);
if (!decoded.has_value()) {
const std::string payload = HexBytes(data, len);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics state request malformed objType=0x%04X objInst=%u property=0x%02X len=%u payload=%s",
static_cast<unsigned>(object_type), static_cast<unsigned>(object_instance),
static_cast<unsigned>(property_id), static_cast<unsigned>(len), payload.c_str());
*response = {ReturnCodes::DataVoid};
return true;
}
const std::string group_address_text =
GatewayKnxGroupAddressString(decoded->group_address);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics state request unsupported ga=0x%04X (%s)",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str());
*response = {ReturnCodes::InvalidCommand};
return true;
}
bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number,
const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool emitted = ets_device_->emitGroupValue(
group_object_number, data, len, [this](const uint8_t* frame_data, size_t frame_len) {
sendRoutingIndication(frame_data, frame_len);
sendTunnelIndication(frame_data, frame_len);
if (ets_device_ != nullptr) {
const bool sent_to_tp = ets_device_->transmitTpFrame(frame_data, frame_len);
tp_uart_online_ = sent_to_tp || ets_device_->tpUartOnline();
}
});
syncOpenKnxConfigFromDevice();
return emitted;
}
bool GatewayKnxTpIpRouter::shouldRouteDaliApplicationFrames() const {
if (!commissioning_only_) {
return true;
}
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;
}
const auto snapshot = ets_device_->snapshot();
openknx_configured_.store(snapshot.configured);
bool changed = false;
GatewayKnxConfig updated = config_;
if (snapshot.individual_address != 0 && snapshot.individual_address != 0xffff &&
snapshot.individual_address != updated.individual_address) {
updated.individual_address = snapshot.individual_address;
changed = true;
}
if (snapshot.configured || !snapshot.associations.empty()) {
std::vector<GatewayKnxEtsAssociation> associations;
associations.reserve(snapshot.associations.size());
for (const auto& association : snapshot.associations) {
associations.push_back(GatewayKnxEtsAssociation{association.group_address,
association.group_object_number});
}
if (associations.size() != updated.ets_associations.size() ||
!std::equal(associations.begin(), associations.end(), updated.ets_associations.begin(),
[](const GatewayKnxEtsAssociation& lhs,
const GatewayKnxEtsAssociation& rhs) {
return lhs.group_address == rhs.group_address &&
lhs.group_object_number == rhs.group_object_number;
})) {
updated.ets_associations = std::move(associations);
changed = true;
}
}
if (!changed) {
return;
}
config_ = updated;
bridge_.setConfig(config_);
}
uint16_t GatewayKnxTpIpRouter::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) {
return address;
}
}
return config_.individual_address;
}
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddress() const {
const uint16_t interface_address = effectiveIpInterfaceIndividualAddress();
uint16_t device = static_cast<uint16_t>((interface_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {
device = 1;
}
uint16_t address = static_cast<uint16_t>((interface_address & 0xff00) | device);
if (address == 0xffff) {
address = static_cast<uint16_t>((interface_address & 0xff00) | 0x0001);
}
return address;
}
} // namespace gateway
@@ -0,0 +1,421 @@
#include "gateway_knx_private.hpp"
namespace gateway {
void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence,
uint8_t status, const sockaddr_in& remote) {
sendConnectionHeaderAck(kServiceTunnellingAck, channel_id, sequence, status, remote);
}
void GatewayKnxTpIpRouter::sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence,
uint8_t status,
const sockaddr_in& remote) {
sendConnectionHeaderAck(kServiceDeviceConfigurationAck, channel_id, sequence, status, remote);
}
void GatewayKnxTpIpRouter::sendConnectionHeaderAck(uint16_t service, uint8_t channel_id,
uint8_t sequence, uint8_t status,
const sockaddr_in& remote) {
KnxIpTunnelingAck ack;
ack.serviceTypeIdentifier(service);
ack.connectionHeader().length(LEN_CH);
ack.connectionHeader().channelId(channel_id);
ack.connectionHeader().sequenceCounter(sequence);
ack.connectionHeader().status(status);
const std::vector<uint8_t> packet(ack.data(), ack.data() + ack.totalLength());
sendPacket(packet, remote);
}
void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) {
const std::vector<uint8_t> body{status, 0x00};
const auto packet = OpenKnxIpPacket(kServiceSecureSessionStatus, body);
sendPacket(packet, remote);
}
bool GatewayKnxTpIpRouter::sendPacket(const std::vector<uint8_t>& 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<uint8_t>& 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<std::array<uint8_t, 8>> GatewayKnxTpIpRouter::localHpaiForRemote(
const sockaddr_in& remote, bool tcp) const {
std::array<uint8_t, 8> hpai{};
hpai[0] = 0x08;
hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp;
if (tcp) {
return hpai;
}
const auto netif = SelectKnxNetifForRemote(remote);
if (!netif.has_value()) {
return std::nullopt;
}
WriteIp(hpai.data() + 2, netif->address);
WriteBe16(hpai.data() + 6, config_.udp_port);
return hpai;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildOpenKnxSearchResponse(
const sockaddr_in& remote) const {
// Use OpenKNX's proven DIB construction via KnxIpSearchResponse.
// Requires ets_device_ to be initialized (DeviceObject + Platform).
if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) {
ESP_LOGW(kTag, "OpenKNX search response unavailable; falling back to hand-rolled DIBs");
return {};
}
KnxIpSearchResponse response(*knx_ip_parameters_, ets_device_->deviceObject());
return std::vector<uint8_t>(response.data(), response.data() + response.totalLength());
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildOpenKnxDescriptionResponse(
const sockaddr_in& remote) const {
if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) {
ESP_LOGW(kTag, "OpenKNX description response unavailable; falling back to hand-rolled DIBs");
return {};
}
KnxIpDescriptionResponse response(*knx_ip_parameters_, ets_device_->deviceObject());
return std::vector<uint8_t>(response.data(), response.data() + response.totalLength());
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildDeviceInfoDib(
const sockaddr_in& remote) const {
std::vector<uint8_t> dib(54, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibDeviceInfo;
dib[2] = advertisedMedium();
dib[3] = 0;
WriteBe16(dib.data() + 4, effectiveIpInterfaceIndividualAddress());
WriteBe16(dib.data() + 6, 0);
uint8_t mac[6]{};
if (ReadBaseMac(mac)) {
dib[8] = static_cast<uint8_t>((knx_internal::kReg1DaliManufacturerId >> 8) & 0xff);
dib[9] = static_cast<uint8_t>(knx_internal::kReg1DaliManufacturerId & 0xff);
std::memcpy(dib.data() + 10, mac + 2, 4);
std::memcpy(dib.data() + 18, mac, 6);
}
WriteIp(dib.data() + 14, inet_addr(config_.multicast_address.c_str()));
char friendly[31]{};
std::snprintf(friendly, sizeof(friendly), "DALI GW MG%u %s",
static_cast<unsigned>(config_.main_group), openknx_namespace_.c_str());
std::memcpy(dib.data() + 24, friendly, std::min<size_t>(30, std::strlen(friendly)));
(void)remote;
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildExtendedDeviceInfoDib() const {
std::vector<uint8_t> dib(8, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibExtendedDeviceInfo;
dib[2] = 0x01;
dib[3] = 0x00;
WriteBe16(dib.data() + 4, 254);
WriteBe16(dib.data() + 6,
advertisedMedium() == kKnxMediumIp ? kKnxIpOnlyDeviceDescriptor
: kKnxTpIpInterfaceDeviceDescriptor);
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildIpConfigDib(const sockaddr_in& remote,
bool current) const {
const auto netif = SelectKnxNetifForRemote(remote);
const uint32_t address = netif.has_value() ? netif->address : htonl(INADDR_ANY);
const uint32_t netmask = netif.has_value() ? netif->netmask : htonl(INADDR_ANY);
const uint32_t gateway = netif.has_value() ? netif->gateway : htonl(INADDR_ANY);
std::vector<uint8_t> dib(current ? 20 : 16, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = current ? kKnxDibCurrentIpConfig : kKnxDibIpConfig;
WriteIp(dib.data() + 2, address);
WriteIp(dib.data() + 6, netmask);
WriteIp(dib.data() + 10, gateway);
if (current) {
WriteIp(dib.data() + 14, htonl(INADDR_ANY));
dib[18] = kKnxIpAssignmentManual;
dib[19] = 0x00;
} else {
dib[14] = kKnxIpCapabilityManual;
dib[15] = kKnxIpAssignmentManual;
}
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildKnxAddressesDib() const {
std::vector<uint8_t> dib(4 + kMaxTunnelClients * 2U, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibKnxAddresses;
WriteBe16(dib.data() + 2, effectiveIpInterfaceIndividualAddress());
size_t offset = 4;
for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) {
WriteBe16(dib.data() + offset, effectiveTunnelAddressForSlot(slot));
offset += 2;
}
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildTunnelingInfoDib() const {
std::vector<uint8_t> dib(4 + kMaxTunnelClients * 4U, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibTunnellingInfo;
WriteBe16(dib.data() + 2, 254);
size_t offset = 4;
for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) {
const uint16_t address = effectiveTunnelAddressForSlot(slot);
bool used = false;
for (const auto& client : tunnel_clients_) {
if (client.connected && client.individual_address == address) {
used = true;
break;
}
}
uint16_t flags = 0xffff;
if (used) {
flags = static_cast<uint16_t>(flags & ~0x0001U);
flags = static_cast<uint16_t>(flags & ~0x0004U);
}
WriteBe16(dib.data() + offset, address);
WriteBe16(dib.data() + offset + 2, flags);
offset += 4;
}
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildSupportedServiceDib() const {
std::vector<std::pair<uint8_t, uint8_t>> services{
{kKnxServiceFamilyCore, 2},
{kKnxServiceFamilyDeviceManagement, 1},
};
if (config_.tunnel_enabled) {
services.emplace_back(kKnxServiceFamilyTunnelling, 1);
}
if (config_.multicast_enabled) {
services.emplace_back(kKnxServiceFamilyRouting, 1);
}
std::vector<uint8_t> dib(2 + services.size() * 2U, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibSupportedServices;
size_t offset = 2;
for (const auto& service : services) {
dib[offset++] = service.first;
dib[offset++] = service.second;
}
return dib;
}
void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0) {
return;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
ESP_LOGW(kTag, "not sending invalid OpenKNX tunnel indication len=%u",
static_cast<unsigned>(len));
return;
}
auto is_tunnel_recipient = [](const TunnelClient& client) {
return client.connected && client.connection_type == kKnxConnectionTypeTunnel;
};
auto send_to_client = [this, data, len](TunnelClient& client) {
sendTunnelIndicationToClient(client, data, len);
};
const bool suppress_source_echo =
frame.addressType() == GroupAddress || frame.addressType() == IndividualAddress;
const uint16_t source_address = suppress_source_echo ? frame.sourceAddress() : 0;
if (frame.addressType() == IndividualAddress) {
for (auto& client : tunnel_clients_) {
if (!is_tunnel_recipient(client)) {
continue;
}
if (client.individual_address == source_address) {
continue;
}
if (client.individual_address == frame.destinationAddress()) {
send_to_client(client);
return;
}
}
}
for (auto& client : tunnel_clients_) {
if (!is_tunnel_recipient(client)) {
continue;
}
if (suppress_source_echo && client.individual_address == source_address) {
continue;
}
send_to_client(client);
}
}
void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data,
size_t len) {
sendCemiFrameToClient(client, kServiceTunnellingRequest, data, len);
}
bool GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service,
const uint8_t* data, size_t len) {
if (!client.connected || data == nullptr || len == 0) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
ESP_LOGW(kTag, "not sending invalid OpenKNX cEMI service=0x%04x len=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(len),
EndpointString(client.data_remote).c_str());
return false;
}
KnxIpTunnelingRequest request(frame);
request.serviceTypeIdentifier(service);
request.connectionHeader().length(LEN_CH);
request.connectionHeader().channelId(client.channel_id);
const auto message_code = CemiMessageCode(data, len);
const uint8_t send_sequence = client.send_sequence++;
request.connectionHeader().sequenceCounter(send_sequence);
request.connectionHeader().status(kKnxNoError);
const std::vector<uint8_t> packet(request.data(), request.data() + request.totalLength());
if (!sendPacketToTunnelClient(client, packet)) {
ESP_LOGW(kTag, "failed to send KNXnet/IP cEMI service=0x%04x channel=%u seq=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(request.connectionHeader().sequenceCounter()),
EndpointString(client.data_remote).c_str());
return false;
}
if (service == kServiceTunnellingRequest && message_code.has_value() &&
message_code.value() == L_data_con) {
if (IsOpenKnxGroupValueWrite(data, len)) {
client.last_tunnel_confirmation_sequence = 0;
client.last_tunnel_confirmation_packet.clear();
} else {
client.last_tunnel_confirmation_sequence = send_sequence;
client.last_tunnel_confirmation_packet = packet;
}
}
ESP_LOGI(kTag, "sent KNXnet/IP cEMI service=0x%04x channel=%u seq=%u cemi=0x%02x len=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(request.connectionHeader().sequenceCounter()), static_cast<unsigned>(data[0]),
static_cast<unsigned>(len), EndpointString(client.data_remote).c_str());
return true;
}
void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote) {
KnxIpStateResponse response(channel_id, status);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
}
void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote) {
KnxIpDisconnectResponse response(channel_id, status);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
}
void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote,
uint8_t connection_type,
uint16_t tunnel_address) {
if (status != kKnxNoError) {
KnxIpConnectResponse response(channel_id, status);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
ESP_LOGI(kTag, "sent KNXnet/IP connect error channel=%u status=0x%02x to %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(status),
EndpointString(remote).c_str());
return;
}
const auto netif = SelectKnxNetifForRemote(remote);
if (!netif.has_value() || knx_ip_parameters_ == nullptr) {
ESP_LOGW(kTag, "cannot accept KNXnet/IP connect from %s: no active IPv4 interface",
EndpointString(remote).c_str());
KnxIpConnectResponse response(channel_id, kKnxErrorConnectionType);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
return;
}
KnxIpConnectResponse response(*knx_ip_parameters_, tunnel_address, config_.udp_port,
channel_id, connection_type);
const bool tcp = currentTransportAllowsTcpHpai();
const uint32_t endpoint_address = ntohl(netif->address);
response.controlEndpoint().code(tcp ? IPV4_TCP : IPV4_UDP);
response.controlEndpoint().ipAddress(tcp ? 0 : endpoint_address);
response.controlEndpoint().ipPortNumber(tcp ? 0 : config_.udp_port);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
std::string endpoint_string;
if (tcp) {
endpoint_string = "0.0.0.0:0 (TCP HPAI)";
} else {
sockaddr_in local_endpoint{};
local_endpoint.sin_family = AF_INET;
local_endpoint.sin_port = htons(config_.udp_port);
local_endpoint.sin_addr.s_addr = netif->address;
endpoint_string = EndpointString(local_endpoint);
}
ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(connection_type),
EndpointString(remote).c_str(), endpoint_string.c_str());
}
void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) {
if (!config_.multicast_enabled || udp_sock_ < 0 || data == nullptr || len == 0) {
return;
}
sockaddr_in remote{};
remote.sin_family = AF_INET;
remote.sin_port = htons(config_.udp_port);
remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str());
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
ESP_LOGW(kTag, "not sending invalid OpenKNX routing cEMI len=%u",
static_cast<unsigned>(len));
return;
}
KnxIpRoutingIndication routing(frame);
const std::vector<uint8_t> packet(routing.data(), routing.data() + routing.totalLength());
const auto netifs = ActiveKnxNetifs();
if (netifs.empty()) {
SendAll(udp_sock_, packet.data(), packet.size(), remote);
return;
}
for (const auto& netif : netifs) {
in_addr multicast_interface{};
multicast_interface.s_addr = netif.address;
if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_IF, &multicast_interface,
sizeof(multicast_interface)) < 0) {
ESP_LOGW(kTag, "failed to select KNX multicast interface %s %s: errno=%d (%s)",
netif.key, Ipv4String(netif.address).c_str(), errno, std::strerror(errno));
continue;
}
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
}
} // namespace gateway
@@ -0,0 +1,662 @@
#include "gateway_knx_private.hpp"
namespace gateway {
void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
const sockaddr_in& remote) {
uint16_t service = 0;
uint16_t total_len = 0;
if (!ParseKnxNetIpHeader(data, len, &service, &total_len)) {
return;
}
const uint8_t* body = data + 6;
const size_t body_len = total_len - 6;
if (IsKnxNetIpSecureService(service)) {
handleSecureService(service, body, body_len, remote);
return;
}
switch (service) {
case kServiceSearchRequest:
case kServiceSearchRequestExt:
handleSearchRequest(service, data, total_len, remote);
break;
case kServiceDescriptionRequest:
handleDescriptionRequest(data, total_len, remote);
break;
case kServiceDeviceConfigurationRequest:
handleDeviceConfigurationRequest(data, total_len, remote);
break;
case kServiceDeviceConfigurationAck:
case kServiceTunnellingAck:
if (body_len >= 4) {
ESP_LOGD(kTag, "rx KNXnet/IP ack service=0x%04x channel=%u seq=%u status=0x%02x from %s",
static_cast<unsigned>(service), static_cast<unsigned>(body[1]),
static_cast<unsigned>(body[2]), static_cast<unsigned>(body[3]),
EndpointString(remote).c_str());
TunnelClient* client = findTunnelClient(body[1]);
if (client != nullptr) {
client->last_activity_tick = xTaskGetTickCount();
if (service == kServiceTunnellingAck && body[3] == kKnxNoError &&
!client->last_tunnel_confirmation_packet.empty() &&
client->last_tunnel_confirmation_sequence == body[2]) {
client->last_tunnel_confirmation_packet.clear();
}
}
}
break;
case kServiceRoutingIndication:
if (config_.multicast_enabled) {
handleRoutingIndication(data, total_len);
}
break;
case kServiceTunnellingRequest:
if (config_.tunnel_enabled) {
handleTunnellingRequest(data, total_len, remote);
}
break;
case kServiceConnectRequest:
if (config_.tunnel_enabled) {
handleConnectRequest(data, total_len, remote);
}
break;
case kServiceConnectionStateRequest:
handleConnectionStateRequest(data, total_len, remote);
break;
case kServiceDisconnectRequest:
handleDisconnectRequest(data, total_len, remote);
break;
default:
ESP_LOGD(kTag, "ignore KNXnet/IP service=0x%04x len=%u from %s",
static_cast<unsigned>(service), static_cast<unsigned>(body_len),
EndpointString(remote).c_str());
break;
}
}
void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* packet_data,
size_t len, const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) {
ESP_LOGW(kTag, "invalid KNXnet/IP search request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> request_packet(packet_data, packet_data + len);
KnxIpSearchRequest request(request_packet.data(), static_cast<uint16_t>(request_packet.size()));
auto& request_hpai = request.hpai();
if (OpenKnxHpaiUsesUnsupportedProtocol(request_hpai, currentTransportAllowsTcpHpai())) {
ESP_LOGW(kTag, "ignore KNXnet/IP search request from %s: unsupported HPAI protocol",
EndpointString(remote).c_str());
return;
}
sockaddr_in response_remote = EndpointFromOpenKnxHpai(request_hpai, remote);
selectOpenKnxNetworkInterface(response_remote);
const auto hpai = localHpaiForRemote(response_remote, currentTransportAllowsTcpHpai());
if (!hpai.has_value()) {
ESP_LOGW(kTag, "cannot send KNXnet/IP search response to %s: no active IPv4 interface",
EndpointString(response_remote).c_str());
return;
}
std::vector<uint8_t> body_resp;
body_resp.insert(body_resp.end(), hpai->begin(), hpai->end());
auto dev_dib = buildDeviceInfoDib(response_remote);
body_resp.insert(body_resp.end(), dev_dib.begin(), dev_dib.end());
auto svc_dib = buildSupportedServiceDib();
body_resp.insert(body_resp.end(), svc_dib.begin(), svc_dib.end());
if (service == kServiceSearchRequestExt) {
auto ext_dib = buildExtendedDeviceInfoDib();
body_resp.insert(body_resp.end(), ext_dib.begin(), ext_dib.end());
auto ip_dib = buildIpConfigDib(response_remote, false);
body_resp.insert(body_resp.end(), ip_dib.begin(), ip_dib.end());
auto current_ip_dib = buildIpConfigDib(response_remote, true);
body_resp.insert(body_resp.end(), current_ip_dib.begin(), current_ip_dib.end());
auto addresses_dib = buildKnxAddressesDib();
body_resp.insert(body_resp.end(), addresses_dib.begin(), addresses_dib.end());
auto tunneling_dib = buildTunnelingInfoDib();
body_resp.insert(body_resp.end(), tunneling_dib.begin(), tunneling_dib.end());
}
const auto response_packet = OpenKnxIpPacket(
service == kServiceSearchRequestExt ? kServiceSearchResponseExt : kServiceSearchResponse,
body_resp);
sendPacket(response_packet, response_remote);
ESP_LOGI(kTag, "sent KNXnet/IP search response service=0x%04x namespace=%s mainGroup=%u to %s:%u endpoint=%u.%u.%u.%u:%u",
static_cast<unsigned>(service), openknx_namespace_.c_str(),
static_cast<unsigned>(config_.main_group),
Ipv4String(response_remote.sin_addr.s_addr).c_str(), static_cast<unsigned>(ntohs(response_remote.sin_port)),
static_cast<unsigned>((*hpai)[2]), static_cast<unsigned>((*hpai)[3]),
static_cast<unsigned>((*hpai)[4]), static_cast<unsigned>((*hpai)[5]),
static_cast<unsigned>(config_.udp_port));
}
void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) {
ESP_LOGW(kTag, "invalid KNXnet/IP description request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> request_packet(packet_data, packet_data + len);
KnxIpDescriptionRequest request(request_packet.data(), static_cast<uint16_t>(request_packet.size()));
auto& hpai = request.hpaiCtrl();
if (OpenKnxHpaiUsesUnsupportedProtocol(hpai, currentTransportAllowsTcpHpai())) {
ESP_LOGW(kTag, "ignore KNXnet/IP description request from %s: unsupported HPAI protocol",
EndpointString(remote).c_str());
return;
}
const sockaddr_in response_remote = EndpointFromOpenKnxHpai(hpai, remote);
selectOpenKnxNetworkInterface(response_remote);
auto device = buildDeviceInfoDib(response_remote);
auto services = buildSupportedServiceDib();
std::vector<uint8_t> body_resp;
auto extended = buildExtendedDeviceInfoDib();
auto ip_config = buildIpConfigDib(response_remote, false);
auto current_ip_config = buildIpConfigDib(response_remote, true);
auto addresses = buildKnxAddressesDib();
auto tunneling = buildTunnelingInfoDib();
body_resp.reserve(device.size() + services.size() + extended.size() + ip_config.size() +
current_ip_config.size() + addresses.size() + tunneling.size());
body_resp.insert(body_resp.end(), device.begin(), device.end());
body_resp.insert(body_resp.end(), services.begin(), services.end());
body_resp.insert(body_resp.end(), extended.begin(), extended.end());
body_resp.insert(body_resp.end(), ip_config.begin(), ip_config.end());
body_resp.insert(body_resp.end(), current_ip_config.begin(), current_ip_config.end());
body_resp.insert(body_resp.end(), addresses.begin(), addresses.end());
body_resp.insert(body_resp.end(), tunneling.begin(), tunneling.end());
const auto response_packet = OpenKnxIpPacket(kServiceDescriptionResponse, body_resp);
sendPacket(response_packet, response_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<unsigned>(advertisedMedium()),
tp_uart_online_, Ipv4String(response_remote.sin_addr.s_addr).c_str(),
static_cast<unsigned>(ntohs(response_remote.sin_port)));
}
void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, size_t len) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2) {
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpRoutingIndication routing(packet.data(), static_cast<uint16_t>(packet.size()));
CemiFrame& frame = routing.frame();
if (!frame.valid()) {
ESP_LOGW(kTag, "invalid OpenKNX routing cEMI len=%u", static_cast<unsigned>(len));
return;
}
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
bool consumed_by_local_application = false;
if (ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_)) {
std::vector<uint8_t> local_tunnel_frame;
if (BuildLocalRoutingTunnelFrame(cemi, cemi_len, &local_tunnel_frame)) {
consumed_by_local_application = handleOpenKnxTunnelFrame(
local_tunnel_frame.data(), local_tunnel_frame.size(), nullptr,
kServiceRoutingIndication,
frame.messageCode() == L_data_ind ? cemi : nullptr,
frame.messageCode() == L_data_ind ? cemi_len : 0);
}
}
if (consumed_by_local_application) {
return;
}
const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi, cemi_len);
const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX routing indication");
const bool sent_to_tp = transmitOpenKnxTpFrame(cemi, cemi_len);
if (!consumed_by_openknx && !routed_to_dali && !sent_to_tp) {
ESP_LOGD(kTag, "KNX routing indication ignored: no OpenKNX/DALI handler matched");
}
}
GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient(
uint8_t channel_id) {
if (channel_id == 0) {
return nullptr;
}
for (auto& client : tunnel_clients_) {
if (client.connected && client.channel_id == channel_id) {
return &client;
}
}
return nullptr;
}
const GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient(
uint8_t channel_id) const {
if (channel_id == 0) {
return nullptr;
}
for (const auto& client : tunnel_clients_) {
if (client.connected && client.channel_id == channel_id) {
return &client;
}
}
return nullptr;
}
void GatewayKnxTpIpRouter::resetTunnelClient(TunnelClient& client) {
if (client.connected) {
ESP_LOGI(kTag, "closed KNXnet/IP tunnel channel=%u type=0x%02x data=%s",
static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(client.connection_type),
EndpointString(client.data_remote).c_str());
}
client = TunnelClient{};
}
uint8_t GatewayKnxTpIpRouter::nextTunnelChannelId() const {
uint8_t candidate = last_tunnel_channel_id_;
for (int attempts = 0; attempts < 255; ++attempts) {
candidate = static_cast<uint8_t>(candidate + 1);
if (candidate == 0) {
candidate = 1;
}
if (findTunnelClient(candidate) == nullptr) {
return candidate;
}
}
return 0;
}
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddressForSlot(size_t slot) const {
const uint16_t first = effectiveTunnelAddress();
const uint16_t line = first & 0xff00;
uint16_t device = static_cast<uint16_t>((first & 0x00ff) + slot);
if (device == 0 || device > 0xff) {
device = static_cast<uint16_t>(1 + slot);
}
return static_cast<uint16_t>(line | (device & 0x00ff));
}
GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient(
const sockaddr_in& control_remote, const sockaddr_in& data_remote, uint8_t connection_type) {
TunnelClient* free_client = nullptr;
size_t free_index = 0;
for (size_t index = 0; index < tunnel_clients_.size(); ++index) {
auto& client = tunnel_clients_[index];
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 &&
(same_tcp_stream || same_udp_endpoints)) {
ESP_LOGW(kTag, "replacing existing KNXnet/IP tunnel channel=%u for endpoint %s",
static_cast<unsigned>(client.channel_id), EndpointString(data_remote).c_str());
resetTunnelClient(client);
free_client = &client;
free_index = index;
break;
}
if (!client.connected && free_client == nullptr) {
free_client = &client;
free_index = index;
}
}
if (free_client == nullptr) {
return nullptr;
}
const uint8_t channel_id = nextTunnelChannelId();
if (channel_id == 0) {
return nullptr;
}
free_client->connected = true;
free_client->channel_id = channel_id;
free_client->connection_type = connection_type;
free_client->received_sequence = 255;
free_client->send_sequence = 0;
free_client->individual_address = effectiveTunnelAddressForSlot(free_index);
free_client->last_activity_tick = xTaskGetTickCount();
free_client->control_remote = control_remote;
free_client->data_remote = data_remote;
free_client->tcp_sock = active_tcp_sock_;
last_tunnel_channel_id_ = channel_id;
return free_client;
}
void GatewayKnxTpIpRouter::pruneStaleTunnelClients() {
const TickType_t now = xTaskGetTickCount();
const TickType_t timeout = pdMS_TO_TICKS(120000);
for (auto& client : tunnel_clients_) {
if (!client.connected || client.last_activity_tick == 0) {
continue;
}
if (now - client.last_activity_tick > timeout) {
ESP_LOGW(kTag, "closing stale KNXnet/IP tunnel channel=%u after heartbeat timeout",
static_cast<unsigned>(client.channel_id));
resetTunnelClient(client);
}
}
}
void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) {
ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpTunnelingRequest tunneling(packet.data(), static_cast<uint16_t>(packet.size()));
auto& header = tunneling.connectionHeader();
if (header.length() != LEN_CH) {
ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling header from %s len=%u chLen=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len),
static_cast<unsigned>(header.length()));
return;
}
const uint8_t channel_id = header.channelId();
const uint8_t sequence = header.sequenceCounter();
TunnelClient* client = findTunnelClient(channel_id);
if (client == nullptr) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: no connection",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str());
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
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<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str());
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
CemiFrame& frame = tunneling.frame();
if (!frame.valid()) {
ESP_LOGW(kTag, "invalid OpenKNX tunnel cEMI channel=%u seq=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str());
return;
}
if (frame.messageCode() == L_data_req && frame.sourceAddress() == 0) {
frame.sourceAddress(client->individual_address);
}
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
const std::vector<uint8_t> current_cemi(cemi, cemi + cemi_len);
const bool is_group_value_write = IsOpenKnxGroupValueWrite(cemi, cemi_len);
const bool duplicate_sequence = sequence == client->received_sequence;
const bool duplicate_payload = duplicate_sequence && client->last_received_cemi == current_cemi;
if (duplicate_payload && !is_group_value_write) {
ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
if (!client->last_tunnel_confirmation_packet.empty()) {
if (sendPacketToTunnelClient(*client, client->last_tunnel_confirmation_packet)) {
ESP_LOGI(kTag,
"resent cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u after duplicate req seq=%u to %s",
static_cast<unsigned>(channel_id),
static_cast<unsigned>(client->last_tunnel_confirmation_sequence),
static_cast<unsigned>(sequence), EndpointString(client->data_remote).c_str());
} else {
ESP_LOGW(kTag,
"failed to resend cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u to %s",
static_cast<unsigned>(channel_id),
static_cast<unsigned>(client->last_tunnel_confirmation_sequence),
EndpointString(client->data_remote).c_str());
}
}
return;
}
if (duplicate_payload) {
ESP_LOGI(kTag,
"reprocessing duplicate KNXnet/IP GroupValueWrite channel=%u seq=%u without cached confirmation replay",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
} else if (duplicate_sequence) {
ESP_LOGW(kTag,
"accept KNXnet/IP tunnelling request channel=%u with repeated seq=%u because cEMI payload changed",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
} else if (static_cast<uint8_t>(sequence - 1) != client->received_sequence) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(static_cast<uint8_t>(client->received_sequence + 1)),
EndpointString(remote).c_str());
sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote);
return;
}
client->received_sequence = sequence;
client->last_received_cemi = current_cemi;
client->last_activity_tick = xTaskGetTickCount();
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
ESP_LOGI(kTag, "rx KNXnet/IP tunnelling request channel=%u seq=%u cemiLen=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(cemi_len), EndpointString(remote).c_str());
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(
cemi, cemi_len, client, kServiceTunnellingRequest);
const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX tunnel frame");
const bool sent_to_tp = !consumed_by_openknx && !routed_to_dali &&
transmitOpenKnxTpFrame(cemi, cemi_len);
if ((!consumed_by_openknx && routed_to_dali) || sent_to_tp) {
std::vector<uint8_t> tunnel_confirmation;
if (BuildTunnelConfirmationFrame(cemi, cemi_len, &tunnel_confirmation)) {
sendCemiFrameToClient(*client, kServiceTunnellingRequest, tunnel_confirmation.data(),
tunnel_confirmation.size());
}
}
if (consumed_by_openknx || routed_to_dali || sent_to_tp) {
return;
}
ESP_LOGD(kTag, "KNX tunnel frame ignored: no OpenKNX/DALI/TP handler matched");
}
void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) {
ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpConfigRequest config_request(packet.data(), static_cast<uint16_t>(packet.size()));
auto& header = config_request.connectionHeader();
if (header.length() != LEN_CH) {
ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration header from %s len=%u chLen=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len),
static_cast<unsigned>(header.length()));
return;
}
const uint8_t channel_id = header.channelId();
const uint8_t sequence = header.sequenceCounter();
TunnelClient* client = findTunnelClient(channel_id);
if (client == nullptr) {
ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: no connection",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str());
sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
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<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str());
sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
client->last_activity_tick = xTaskGetTickCount();
sendDeviceConfigurationAck(channel_id, sequence, kKnxNoError, client->data_remote);
CemiFrame& frame = config_request.frame();
if (!frame.valid()) {
ESP_LOGW(kTag, "invalid OpenKNX device-configuration cEMI channel=%u seq=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str());
return;
}
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
ESP_LOGI(kTag, "rx KNXnet/IP device-configuration request channel=%u seq=%u cemiLen=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(cemi_len), EndpointString(remote).c_str());
if (!handleOpenKnxTunnelFrame(cemi, cemi_len, client, kServiceDeviceConfigurationRequest)) {
ESP_LOGW(kTag, "KNXnet/IP device-configuration cEMI was not consumed by OpenKNX cemiLen=%u",
static_cast<unsigned>(cemi_len));
}
}
void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + 2) {
ESP_LOGW(kTag, "invalid KNXnet/IP connect request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpConnectRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
auto& control_hpai = request.hpaiCtrl();
auto& data_hpai = request.hpaiData();
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai()) ||
OpenKnxHpaiUsesUnsupportedProtocol(data_hpai, 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 = EndpointFromOpenKnxHpai(control_hpai, remote);
sockaddr_in data_remote = EndpointFromOpenKnxHpai(data_hpai, remote);
selectOpenKnxNetworkInterface(control_remote);
auto& cri = request.cri();
const uint8_t cri_length = cri.length();
const uint8_t connection_type = static_cast<uint8_t>(cri.type());
if (cri_length < 2 || kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + cri_length > len) {
ESP_LOGW(kTag, "invalid KNXnet/IP connect CRI from %s len=%u criLen=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len),
static_cast<unsigned>(cri_length));
sendConnectResponse(0, kKnxErrorConnectionType, control_remote, kKnxConnectionTypeTunnel, 0);
return;
}
if (connection_type != kKnxConnectionTypeTunnel &&
connection_type != kKnxConnectionTypeDeviceManagement) {
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s unsupported type=0x%02x",
EndpointString(remote).c_str(), static_cast<unsigned>(connection_type));
sendConnectResponse(0, kKnxErrorConnectionType, control_remote, connection_type, 0);
return;
}
if (connection_type == kKnxConnectionTypeTunnel &&
(cri_length < 4 || cri.layer() != kKnxTunnelLayerLink)) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnel connect from %s unsupported layer=0x%02x",
EndpointString(remote).c_str(),
static_cast<unsigned>(cri_length >= 3 ? cri.layer() : 0));
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",
EndpointString(remote).c_str());
sendConnectResponse(0, kKnxErrorNoMoreConnections, control_remote, connection_type, 0);
return;
}
ESP_LOGI(kTag,
"accepted KNXnet/IP connect namespace=%s channel=%u type=0x%02x tunnelPa=0x%04x ctrl=%s data=%s remote=%s active=%u/%u",
openknx_namespace_.c_str(), static_cast<unsigned>(client->channel_id),
static_cast<unsigned>(connection_type), static_cast<unsigned>(client->individual_address),
EndpointString(control_remote).c_str(), EndpointString(data_remote).c_str(),
EndpointString(remote).c_str(),
static_cast<unsigned>(std::count_if(tunnel_clients_.begin(), tunnel_clients_.end(),
[](const TunnelClient& item) {
return item.connected;
})),
static_cast<unsigned>(tunnel_clients_.size()));
sendConnectResponse(client->channel_id, kKnxNoError, control_remote, connection_type,
client->individual_address);
}
void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) {
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpStateRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
auto& control_hpai = request.hpaiCtrl();
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, 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 = request.channelId();
const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote);
TunnelClient* client = findTunnelClient(channel_id);
const bool endpoint_matches = client != nullptr &&
((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) {
client->last_activity_tick = xTaskGetTickCount();
}
}
ESP_LOGI(kTag, "rx KNXnet/IP connection-state request channel=%u status=0x%02x from %s ctrl=%s expected=%s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(status),
EndpointString(remote).c_str(), EndpointString(control_remote).c_str(),
client == nullptr ? "none" : EndpointString(client->control_remote).c_str());
sendConnectionStateResponse(
channel_id, status, control_remote);
}
void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) {
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpDisconnectRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
auto& control_hpai = request.hpaiCtrl();
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) {
ESP_LOGW(kTag, "reject KNXnet/IP disconnect request from %s: unsupported HPAI protocol",
EndpointString(remote).c_str());
return;
}
const uint8_t channel_id = request.channelId();
const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote);
TunnelClient* client = findTunnelClient(channel_id);
const bool endpoint_matches = client != nullptr &&
((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<unsigned>(channel_id), static_cast<unsigned>(status),
EndpointString(remote).c_str(), EndpointString(control_remote).c_str(),
expected.c_str());
sendDisconnectResponse(channel_id, status, control_remote);
}
void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* body,
size_t len, const sockaddr_in& remote) {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
switch (service) {
case kServiceSecureSessionRequest:
case kServiceSecureSessionAuth:
ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service);
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
break;
case kServiceSecureWrapper:
ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session");
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
break;
case kServiceSecureGroupSync:
ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned");
break;
default:
ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service);
break;
}
#else
(void)service;
(void)body;
(void)len;
(void)remote;
#endif
}
} // namespace gateway