feat(gateway): enhance OpenKNX integration with new DIB construction methods and improve BLE configuration

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-16 01:50:26 +08:00
parent 449a3a801a
commit 2a3808c1e4
5 changed files with 193 additions and 169 deletions
+2 -2
View File
@@ -1130,8 +1130,8 @@ CONFIG_BT_CTRL_BLE_ADV=y
# Common Options
#
CONFIG_BT_ALARM_MAX_NUM=50
# CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT is not set
CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS=y
CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT=y
# CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS is not set
#
# BLE Log
@@ -36,6 +36,10 @@ class EtsDeviceRuntime {
void toggleProgrammingMode();
EtsMemorySnapshot snapshot() const;
// Accessors for OpenKNX integration (DIB construction, IP parameter object).
DeviceObject& deviceObject();
Platform& platform();
void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler);
void setGroupWriteHandler(GroupWriteHandler handler);
+11 -6
View File
@@ -3,6 +3,8 @@
#include "bridge.hpp"
#include "model_value.hpp"
#include "knx/ip_parameter_object.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
@@ -259,9 +261,8 @@ class GatewayKnxTpIpRouter {
bool configureProgrammingGpio();
void refreshNetworkInterfaces(bool force_log = false);
void handleUdpDatagram(const uint8_t* data, size_t len, const ::sockaddr_in& remote);
void handleSearchRequest(uint16_t service, const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleDescriptionRequest(const uint8_t* body, size_t len,
void handleSearchRequest(const uint8_t* data, size_t len, const ::sockaddr_in& remote);
void handleDescriptionRequest(const uint8_t* data, size_t len,
const ::sockaddr_in& remote);
void handleRoutingIndication(const uint8_t* body, size_t len);
void handleTunnellingRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
@@ -290,15 +291,18 @@ class GatewayKnxTpIpRouter {
const ::sockaddr_in& remote, uint8_t connection_type,
uint16_t tunnel_address);
void sendRoutingIndication(const uint8_t* data, size_t len);
void sendSearchResponse(uint16_t service, const ::sockaddr_in& remote,
const std::set<uint8_t>& requested_dibs = {});
void sendDescriptionResponse(const ::sockaddr_in& remote);
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;
// --- OpenKNX-backed DIB construction (uses KnxIpSearchResponse / KnxIpDescriptionResponse) ---
std::vector<uint8_t> buildOpenKnxSearchResponse(const ::sockaddr_in& remote) const;
std::vector<uint8_t> buildOpenKnxDescriptionResponse(const ::sockaddr_in& remote) const;
// --- Hand-rolled DIB builders (fallback when OpenKNX is unavailable) ---
std::vector<uint8_t> buildDeviceInfoDib(const ::sockaddr_in& remote) const;
std::vector<uint8_t> buildSupportedServiceDib() const;
std::vector<uint8_t> buildExtendedDeviceInfoDib() const;
@@ -355,6 +359,7 @@ class GatewayKnxTpIpRouter {
TickType_t network_refresh_tick_{0};
std::array<TcpClient, kMaxTcpClients> tcp_clients_{};
std::array<TunnelClient, kMaxTunnelClients> tunnel_clients_{};
std::unique_ptr<IpParameterObject> knx_ip_parameters_;
uint8_t last_tunnel_channel_id_{0};
std::vector<uint8_t> tp_rx_frame_;
std::vector<uint8_t> tp_last_sent_telegram_;
@@ -28,6 +28,7 @@ class ActiveFunctionPropertyRuntimeScope {
};
constexpr uint16_t kInvalidIndividualAddress = 0xffff;
constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff; // KNX broadcast IA for unconfigured devices
constexpr uint16_t kReg1DaliManufacturerId = 0x00a4;
constexpr uint8_t kReg1DaliApplicationNumber = 0x01;
constexpr uint8_t kReg1DaliApplicationVersion = 0x05;
@@ -125,6 +126,10 @@ void EtsDeviceRuntime::setProgrammingMode(bool enabled) {
void EtsDeviceRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); }
DeviceObject& EtsDeviceRuntime::deviceObject() { return device_.deviceObject(); }
Platform& EtsDeviceRuntime::platform() { return platform_; }
EtsMemorySnapshot EtsDeviceRuntime::snapshot() const {
EtsMemorySnapshot out;
auto& device = const_cast<Bau07B0&>(device_);
@@ -318,19 +323,26 @@ bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
case M_FuncPropCommand_req:
case M_FuncPropStateRead_req:
return true;
case L_data_req:
if (!const_cast<Bau07B0&>(device_).configured() || programmingMode()) {
return true;
case L_data_req: {
// In commissioning / programming mode ETS may address the device via its
// individual address, the cEMI-client tunnel address (device+1), or the
// unconfigured broadcast address 0xFFFF. Consume only those; let all
// other individual-addressed frames (bus-scan, DeviceDescriptorRead, …)
// pass through to the physical TP-UART so real KNX devices on the bus
// can reply.
const uint16_t dest = frame.destinationAddress();
const uint16_t own_address = individualAddress();
const uint16_t client_address = tunnelClientAddress();
const bool commissioning = !const_cast<Bau07B0&>(device_).configured() || programmingMode();
if (frame.addressType() == IndividualAddress) {
if (dest == own_address || dest == client_address ||
(commissioning && dest == kKnxUnconfiguredBroadcastAddress)) {
return true;
}
}
if (frame.addressType() == IndividualAddress &&
frame.destinationAddress() == individualAddress()) {
return true;
}
#ifdef USE_DATASECURE
return frame.addressType() == GroupAddress && frame.apdu().type() == SecureService;
#else
return false;
#endif
}
default:
return false;
}
+153 -150
View File
@@ -11,6 +11,10 @@
#include "ets_device_runtime.h"
#include "soc/uart_periph.h"
#include "knx/cemi_frame.h"
#include "knx/knx_ip_search_response.h"
#include "knx/knx_ip_description_response.h"
#include <algorithm>
#include <array>
#include <cerrno>
@@ -741,17 +745,34 @@ bool IsKnxNetIpSecureService(uint16_t service) {
}
bool IsExtendedTpFrame(const uint8_t* data, size_t len) {
return len > 0 && (data[0] & 0xD3) == 0x10;
if (data == nullptr || len == 0) {
return false;
}
// TP-UART non-monitor mode: L_DATA_EXTENDED_IND indicator byte (0x10 with mask 0xD3)
if ((data[0] & 0xD3U) == 0x10U) {
return true;
}
// Raw bus frame (transparent / bus-monitor mode): bits 7-6 = 01, bit 4 = 1
return (data[0] & 0xD0U) == 0x40U && (data[0] & 0x10U) != 0;
}
size_t ExpectedTpFrameSize(const uint8_t* data, size_t len) {
if (data == nullptr || len < 6) {
return 0;
}
// TP-UART non-monitor mode prepends L_DATA_STANDARD_IND (0x90) or
// L_DATA_EXTENDED_IND (0x10) indicator bytes before each frame.
const bool has_indicator = (data[0] & 0xD3U) == 0x90U || (data[0] & 0xD3U) == 0x10U;
const size_t off = has_indicator ? 1U : 0U;
// Standard frame: ctrl(1)+src(2)+dst(2)+npci(1)=6, data(L), crc(1)
// Extended frame: ctrl(1)+src(2)+dst(2)+npci(1)+ext_len(1)=7, data(L), crc(1)
// With indicator byte prepended, add 1 to each base.
if (IsExtendedTpFrame(data, len)) {
return 9U + data[6];
const size_t base = has_indicator ? 9U : 8U;
return base + data[6U + off];
}
return 8U + (data[5] & 0x0F);
const size_t base = has_indicator ? 8U : 7U;
return base + (data[5U + off] & 0x0FU);
}
bool ValidateTpChecksum(const uint8_t* data, size_t len) {
@@ -766,18 +787,44 @@ bool ValidateTpChecksum(const uint8_t* data, size_t len) {
}
bool IsTpUartControlByte(uint8_t byte) {
// L_Data.con: lower 7 bits must equal 0x0B (matches both positive 0x8B and negative 0x0B).
// Using the reference mask-based check to be robust across different TP-UART implementations.
return byte == kTpUartResetIndication ||
byte == kTpUartLDataConfirmPositive ||
byte == kTpUartLDataConfirmNegative || byte == kTpUartBusy ||
(byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask;
(byte & 0x7fU) == kTpUartLDataConfirmNegative ||
byte == kTpUartBusy ||
(byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask ||
(byte & 0x33U) == 0x00U; // L_ACKN_IND
}
bool IsTpUartFrameStart(uint8_t byte, bool* extended) {
if (extended == nullptr) {
return false;
}
*extended = (byte & 0x80) == 0;
return (byte & 0x50) == 0x10;
// TP-UART non-monitor mode: controller sends L_DATA_STANDARD_IND (0x90) or
// L_DATA_EXTENDED_IND (0x10) indicator bytes before each frame.
// TP-UART bus-monitor (transparent) mode: raw bus control bytes are passed through.
// Standard raw: bit7=1, bit6=0, bit4=1. Extended raw: bit7=0, bit6=1, bit4=1.
// Detect both indicator and raw formats.
const bool is_standard_indicator = (byte & 0xD3U) == 0x90U; // L_DATA_STANDARD_IND
const bool is_extended_indicator = (byte & 0xD3U) == 0x10U; // L_DATA_EXTENDED_IND
if (is_standard_indicator || is_extended_indicator) {
*extended = is_extended_indicator;
return true;
}
// Raw bus frame format (transparent / bus-monitor mode)
if ((byte & 0x10U) == 0) {
return false; // bit 4 must be set for valid KNX frame control bytes
}
const uint8_t top = byte & 0xC0U;
if (top == 0x80U) {
*extended = false; // standard frame
return true;
}
if (top == 0x40U) {
*extended = true; // extended frame
return true;
}
return false;
}
std::vector<uint8_t> WrapTpUartTelegram(const std::vector<uint8_t>& telegram) {
@@ -2047,6 +2094,8 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
ets_device_ = std::make_unique<openknx::EtsDeviceRuntime>(openknx_namespace_,
config_.individual_address,
effectiveTunnelAddress());
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 "
@@ -2191,6 +2240,7 @@ void GatewayKnxTpIpRouter::finishTask() {
{
SemaphoreGuard guard(openknx_lock_);
setProgrammingLed(false);
knx_ip_parameters_.reset();
ets_device_.reset();
openknx_configured_.store(false);
}
@@ -2322,7 +2372,11 @@ bool GatewayKnxTpIpRouter::configureSocket() {
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) {
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),
@@ -2686,7 +2740,7 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
switch (service) {
case kServiceSearchRequest:
case kServiceSearchRequestExt:
handleSearchRequest(service, body, body_len, remote);
handleSearchRequest(body, body_len, remote);
break;
case kServiceDescriptionRequest:
handleDescriptionRequest(body, body_len, remote);
@@ -2732,7 +2786,7 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
}
}
void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* body,
void GatewayKnxTpIpRouter::handleSearchRequest(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",
@@ -2741,48 +2795,27 @@ void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t*
}
sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote);
selectOpenKnxNetworkInterface(response_remote);
std::set<uint8_t> requested_dibs;
if (service == kServiceSearchRequestExt && body != nullptr && len > 8) {
size_t offset = 8;
while (offset + 2 <= len) {
const uint8_t srp_len = body[offset];
if (srp_len < 2 || offset + srp_len > len) {
break;
}
const uint8_t srp_type = body[offset + 1];
if (srp_type == 0x01) {
// 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) {
return;
}
} else if (srp_type == 0x03) {
for (size_t service_offset = offset + 2; service_offset + 1 < offset + srp_len;
service_offset += 2) {
const uint8_t family = body[service_offset];
const uint8_t version = body[service_offset + 1];
if ((family == kKnxServiceFamilyCore && version > 2) ||
(family == kKnxServiceFamilyDeviceManagement && version > 1) ||
(family == kKnxServiceFamilyTunnelling &&
(!config_.tunnel_enabled || version > 1)) ||
(family == kKnxServiceFamilyRouting &&
(!config_.multicast_enabled || version > 1))) {
return;
}
}
} else if (srp_type == 0x04) {
for (size_t dib_offset = offset + 2; dib_offset < offset + srp_len; ++dib_offset) {
requested_dibs.insert(body[dib_offset]);
}
}
offset += srp_len;
}
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;
}
sendSearchResponse(service == kServiceSearchRequestExt ? kServiceSearchResponseExt
: kServiceSearchResponse,
response_remote, requested_dibs);
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());
const auto packet = KnxNetIpPacket(kServiceSearchResponse, body_resp);
sendPacket(packet, response_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(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* body, size_t len,
@@ -2794,7 +2827,18 @@ void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* body, size_t
}
const sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote);
selectOpenKnxNetworkInterface(response_remote);
sendDescriptionResponse(response_remote);
auto device = buildDeviceInfoDib(response_remote);
auto services = buildSupportedServiceDib();
std::vector<uint8_t> body_resp;
body_resp.reserve(device.size() + services.size());
body_resp.insert(body_resp.end(), device.begin(), device.end());
body_resp.insert(body_resp.end(), services.begin(), services.end());
const auto packet = KnxNetIpPacket(kServiceDescriptionResponse, body_resp);
sendPacket(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* body, size_t len) {
@@ -2978,12 +3022,19 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l
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 group_frame = IsCemiGroupFrame(cemi, cemi_len);
// Forward ALL tunnel cEMI frames to the TP bus first, so that bus-check,
// property-read and other KNX bus frames reach the physical TP-UART
// (NCN5120) and real KNX devices on the bus can reply. The TP-UART
// response/confirmation will come back through pollTpUart() and be
// forwarded to ETS via sendTunnelIndication().
forwardCemiToTp(cemi, cemi_len);
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len, client);
if (consumed_by_openknx) {
if (group_frame) {
forwardCemiToTp(cemi, cemi_len);
}
// OpenKNX (CemiServer / Bau07B0) handled the frame (ETS memory-model
// queries, function-property commands, etc.). It may have emitted
// a response already via EmitTunnelFrame.
return;
}
if (shouldRouteDaliApplicationFrames()) {
@@ -2992,7 +3043,7 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l
ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str());
}
}
forwardCemiToTp(cemi, cemi_len);
// Frame was already forwarded to TP-UART above; no need to forward again.
}
void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* body, size_t len,
@@ -3255,6 +3306,28 @@ std::optional<std::array<uint8_t, 8>> GatewayKnxTpIpRouter::localHpaiForRemote(
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);
@@ -3380,88 +3453,6 @@ std::vector<uint8_t> GatewayKnxTpIpRouter::buildSupportedServiceDib() const {
return dib;
}
void GatewayKnxTpIpRouter::sendSearchResponse(uint16_t service, const sockaddr_in& remote,
const std::set<uint8_t>& requested_dibs) {
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;
}
std::vector<uint8_t> body;
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) {
return;
}
body.insert(body.end(), dib.begin(), dib.end());
};
append_dib(buildDeviceInfoDib(remote));
append_dib(buildSupportedServiceDib());
if (service == kServiceSearchResponseExt) {
append_dib(buildExtendedDeviceInfoDib());
for (const uint8_t dib_type : requested_dibs) {
switch (dib_type) {
case kKnxDibDeviceInfo:
append_dib(buildDeviceInfoDib(remote));
break;
case kKnxDibSupportedServices:
append_dib(buildSupportedServiceDib());
break;
case kKnxDibIpConfig:
append_dib(buildIpConfigDib(remote, false));
break;
case kKnxDibCurrentIpConfig:
append_dib(buildIpConfigDib(remote, true));
break;
case kKnxDibKnxAddresses:
append_dib(buildKnxAddressesDib());
break;
case kKnxDibTunnellingInfo:
if (config_.tunnel_enabled) {
append_dib(buildTunnelingInfoDib());
}
break;
case kKnxDibExtendedDeviceInfo:
append_dib(buildExtendedDeviceInfoDib());
break;
default:
break;
}
}
}
const auto packet = KnxNetIpPacket(service, body);
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>(config_.udp_port));
}
void GatewayKnxTpIpRouter::sendDescriptionResponse(const sockaddr_in& remote) {
if (udp_sock_ < 0 && active_tcp_sock_ < 0) {
return;
}
auto device = buildDeviceInfoDib(remote);
auto services = buildSupportedServiceDib();
std::vector<uint8_t> body;
body.reserve(device.size() + services.size());
body.insert(body.end(), device.begin(), device.end());
body.insert(body.end(), services.begin(), services.end());
const auto packet = KnxNetIpPacket(kServiceDescriptionResponse, body);
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 (data == nullptr || len == 0) {
return;
@@ -3769,34 +3760,46 @@ void GatewayKnxTpIpRouter::handleTpUartControlByte(uint8_t byte) {
return;
}
if (byte == kTpUartBusy) {
last_error_ = "KNX TP-UART bus busy";
ESP_LOGW(kTag, "%s", last_error_.c_str());
ESP_LOGW(kTag, "KNX TP-UART bus busy");
return;
}
if (byte == kTpUartLDataConfirmNegative) {
last_error_ = "KNX TP-UART negative confirmation";
ESP_LOGW(kTag, "%s", last_error_.c_str());
return;
}
if (byte == kTpUartLDataConfirmPositive) {
// L_Data.con: use masked comparison consistent with IsTpUartControlByte
if ((byte & 0x7fU) == kTpUartLDataConfirmNegative) {
const bool positive = (byte & 0x80U) != 0;
if (!positive) {
ESP_LOGD(kTag, "KNX TP-UART negative confirmation 0x%02x", byte);
}
return;
}
if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) {
tp_uart_online_ = true;
}
// L_ACKN_IND: acknowledge indication (busy / nack status embedded in byte)
if ((byte & 0x33U) == 0x00U) {
return;
}
}
void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0) {
return;
}
const std::vector<uint8_t> telegram(data, data + len);
// In non-monitor mode the TP-UART prepends L_DATA_STANDARD_IND (0x90) or
// L_DATA_EXTENDED_IND (0x10) indicator bytes. Strip them so downstream
// helpers always receive a raw KNX TP telegram starting with the control byte.
const uint8_t* frame_data = data;
size_t frame_len = len;
if (len > 1U && ((data[0] & 0xD3U) == 0x90U || (data[0] & 0xD3U) == 0x10U)) {
frame_data = data + 1;
frame_len = len - 1U;
}
const std::vector<uint8_t> telegram(frame_data, frame_data + frame_len);
if (!tp_last_sent_telegram_.empty() &&
TpTelegramEqualsIgnoringRepeatBit(telegram, tp_last_sent_telegram_)) {
tp_last_sent_telegram_.clear();
return;
}
const auto cemi = TpTelegramToCemi(data, len);
const auto cemi = TpTelegramToCemi(frame_data, frame_len);
if (!cemi.has_value()) {
return;
}