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:
@@ -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
|
||||
Reference in New Issue
Block a user