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
+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() {