feat: Enhance EtsDeviceRuntime constructor and multicast handling

- Updated EtsDeviceRuntime constructor to accept an optional tunnel_client_address parameter with a default value of 0.
- Modified EspIdfPlatform::setupMultiCast to use IPPROTO_UDP for socket creation and improved multicast interface selection based on the current IP address.
- Ensured that the multicast interface is set only if a valid local address is available.
- Adjusted the client address assignment in EtsDeviceRuntime to use the provided tunnel_client_address if valid, falling back to the default otherwise.

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