Implement KNX Gateway functionality with support for DALI integration

- Added gateway_knx.cpp to handle KNX communication and DALI bridge requests.
- Implemented functions for encoding/decoding KNX telegrams and managing group writes.
- Introduced GatewayKnxBridge and GatewayKnxTpIpRouter classes for managing KNX to DALI routing and IP tunneling.
- Added configuration handling for KNX settings, including UART and multicast options.
- Implemented error handling and logging for various KNX operations.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-08 18:19:37 +08:00
parent 029785ff1d
commit 1a8ee06ec1
10 changed files with 1589 additions and 5 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
idf_component_register(
SRCS "app_main.cpp"
REQUIRES gateway_core gateway_controller gateway_network gateway_bridge gateway_cache dali_domain gateway_runtime gateway_ble gateway_usb_setup gateway_485_control log
REQUIRES gateway_core gateway_controller gateway_network gateway_bridge gateway_cache dali_domain gateway_runtime gateway_ble gateway_usb_setup gateway_485_control gateway_knx log
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
+92
View File
@@ -501,6 +501,98 @@ config GATEWAY_START_BACNET_BRIDGE_ENABLED
help
Starts configured BACnet/IP object bindings at boot. Disabled by default so the UDP BACnet/IP port is opened only after provisioning or explicit runtime start.
config GATEWAY_KNX_BRIDGE_SUPPORTED
bool "KNX to DALI bridge is supported"
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
default n
help
Enables the gateway-owned KNX group-address router and KNXnet/IP TP/IP
router. Group addresses use the configured main group, middle groups as
DALI data types, and subgroups matching DALI short address structure.
config GATEWAY_START_KNX_BRIDGE_ENABLED
bool "Start KNX/IP bridge at startup"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default n
help
Starts the KNXnet/IP tunneling/multicast listener at boot. Disabled by
default so UDP port 3671 is opened only after provisioning or explicit start.
config GATEWAY_KNX_MAIN_GROUP
int "KNX DALI main group"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0 31
default 0
help
Main group used by the built-in KNX to DALI router. Middle groups select
the data type and subgroups select broadcast, short-address, or group targets.
config GATEWAY_KNX_TUNNEL_ENABLED
bool "Enable KNXnet/IP tunneling mode"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default y
config GATEWAY_KNX_MULTICAST_ENABLED
bool "Enable KNXnet/IP multicast routing mode"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default y
config GATEWAY_KNX_UDP_PORT
int "KNXnet/IP UDP port"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 1 65535
default 3671
config GATEWAY_KNX_MULTICAST_ADDRESS
string "KNXnet/IP 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"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0 65535
default 4353
help
Raw 16-bit individual address advertised to KNXnet/IP tunnel clients.
The default 4353 is 1.1.1.
config GATEWAY_KNX_TP_UART_PORT
int "KNX TP UART port"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0 2
default 1
config GATEWAY_KNX_TP_TX_PIN
int "KNX TP UART TX pin"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range -1 48
default -1
config GATEWAY_KNX_TP_RX_PIN
int "KNX TP UART RX pin"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range -1 48
default -1
config GATEWAY_KNX_TP_BAUDRATE
int "KNX TP UART baudrate"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 1200 921600
default 19200
config GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE
int "KNX/IP bridge task stack bytes"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 6144 24576
default 8192
config GATEWAY_BRIDGE_KNX_TASK_PRIORITY
int "KNX/IP bridge task priority"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 1 10
default 5
config GATEWAY_CLOUD_BRIDGE_SUPPORTED
bool "MQTT cloud bridge is supported"
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
+111
View File
@@ -144,6 +144,46 @@
#define CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY 5
#endif
#ifndef CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE
#define CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE 8192
#endif
#ifndef CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY
#define CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY 5
#endif
#ifndef CONFIG_GATEWAY_KNX_MAIN_GROUP
#define CONFIG_GATEWAY_KNX_MAIN_GROUP 0
#endif
#ifndef CONFIG_GATEWAY_KNX_UDP_PORT
#define CONFIG_GATEWAY_KNX_UDP_PORT 3671
#endif
#ifndef CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS
#define CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS "224.0.23.12"
#endif
#ifndef CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS
#define CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS 4353
#endif
#ifndef CONFIG_GATEWAY_KNX_TP_UART_PORT
#define CONFIG_GATEWAY_KNX_TP_UART_PORT 1
#endif
#ifndef CONFIG_GATEWAY_KNX_TP_TX_PIN
#define CONFIG_GATEWAY_KNX_TP_TX_PIN -1
#endif
#ifndef CONFIG_GATEWAY_KNX_TP_RX_PIN
#define CONFIG_GATEWAY_KNX_TP_RX_PIN -1
#endif
#ifndef CONFIG_GATEWAY_KNX_TP_BAUDRATE
#define CONFIG_GATEWAY_KNX_TP_BAUDRATE 19200
#endif
#ifndef CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS
#define CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS 5000
#endif
@@ -237,6 +277,30 @@ constexpr bool kBacnetBridgeStartupEnabled = true;
constexpr bool kBacnetBridgeStartupEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED
constexpr bool kKnxBridgeSupported = true;
#else
constexpr bool kKnxBridgeSupported = false;
#endif
#ifdef CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED
constexpr bool kKnxBridgeStartupEnabled = true;
#else
constexpr bool kKnxBridgeStartupEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_KNX_TUNNEL_ENABLED
constexpr bool kKnxTunnelEnabled = true;
#else
constexpr bool kKnxTunnelEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_KNX_MULTICAST_ENABLED
constexpr bool kKnxMulticastEnabled = true;
#else
constexpr bool kKnxMulticastEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED
constexpr bool kCloudBridgeSupported = true;
#else
@@ -449,6 +513,26 @@ bool ValidateChannelBindings() {
}
}
if (kKnxBridgeSupported) {
const int knx_uart = CONFIG_GATEWAY_KNX_TP_UART_PORT;
if (k485ControlEnabled && knx_uart == 0) {
ESP_LOGE(kTag, "KNX TP UART0 conflicts with the UART0 control bridge");
return false;
}
if (kModbusBridgeSupported && kModbusDefaultSerialTransport &&
knx_uart == CONFIG_GATEWAY_MODBUS_SERIAL_UART_PORT) {
ESP_LOGE(kTag, "KNX TP UART%d conflicts with default Modbus serial UART", knx_uart);
return false;
}
for (int i = 0; i < CONFIG_GATEWAY_CHANNEL_COUNT; ++i) {
if (channels[i].enabled && channels[i].serial_phy && channels[i].uart_port == knx_uart) {
ESP_LOGE(kTag, "KNX TP UART%d conflicts with DALI channel %d serial PHY", knx_uart,
i + 1);
return false;
}
}
}
if (!any_enabled) {
ESP_LOGE(kTag, "no DALI PHY is configured; enable at least one native or serial channel");
return false;
@@ -626,6 +710,9 @@ extern "C" void app_main(void) {
bridge_config.bacnet_enabled = profile.enable_wifi && kBacnetBridgeSupported;
bridge_config.bacnet_startup_enabled = profile.enable_wifi && kBacnetBridgeSupported &&
kBacnetBridgeStartupEnabled;
bridge_config.knx_enabled = profile.enable_wifi && kKnxBridgeSupported;
bridge_config.knx_startup_enabled = profile.enable_wifi && kKnxBridgeSupported &&
kKnxBridgeStartupEnabled;
bridge_config.cloud_enabled = profile.enable_wifi && kCloudBridgeSupported;
bridge_config.cloud_startup_enabled = profile.enable_wifi && kCloudBridgeSupported &&
kCloudBridgeStartupEnabled;
@@ -649,6 +736,9 @@ extern "C" void app_main(void) {
bridge_config.reserved_uart_ports.push_back(2);
#endif
#endif
if (kKnxBridgeSupported) {
bridge_config.reserved_uart_ports.push_back(CONFIG_GATEWAY_KNX_TP_UART_PORT);
}
if (kModbusBridgeSupported) {
gateway::GatewayModbusConfig default_modbus;
#if defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU)
@@ -675,6 +765,27 @@ extern "C" void app_main(void) {
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE);
bridge_config.bacnet_task_priority =
static_cast<UBaseType_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY);
if (kKnxBridgeSupported) {
gateway::GatewayKnxConfig default_knx;
default_knx.dali_router_enabled = true;
default_knx.ip_router_enabled = true;
default_knx.tunnel_enabled = kKnxTunnelEnabled;
default_knx.multicast_enabled = kKnxMulticastEnabled;
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.individual_address =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS);
default_knx.tp_uart.uart_port = CONFIG_GATEWAY_KNX_TP_UART_PORT;
default_knx.tp_uart.tx_pin = CONFIG_GATEWAY_KNX_TP_TX_PIN;
default_knx.tp_uart.rx_pin = CONFIG_GATEWAY_KNX_TP_RX_PIN;
default_knx.tp_uart.baudrate = static_cast<uint32_t>(CONFIG_GATEWAY_KNX_TP_BAUDRATE);
bridge_config.default_knx_config = default_knx;
}
bridge_config.knx_task_stack_size =
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE);
bridge_config.knx_task_priority =
static_cast<UBaseType_t>(CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY);
s_bridge = std::make_unique<gateway::GatewayBridgeService>(*s_dali_domain, *s_cache,
bridge_config);
}
+1
View File
@@ -666,6 +666,7 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
# CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED is not set
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144
+1
View File
@@ -5,6 +5,7 @@ set(GATEWAY_BRIDGE_REQUIRES
esp_driver_uart
freertos
gateway_cache
gateway_knx
gateway_modbus
log
lwip
@@ -9,6 +9,7 @@
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "gateway_knx.hpp"
#include "gateway_modbus.hpp"
namespace gateway {
@@ -22,6 +23,8 @@ struct GatewayBridgeServiceConfig {
bool modbus_startup_enabled{false};
bool bacnet_enabled{false};
bool bacnet_startup_enabled{false};
bool knx_enabled{false};
bool knx_startup_enabled{false};
bool cloud_enabled{true};
bool cloud_startup_enabled{false};
uint32_t modbus_task_stack_size{6144};
@@ -31,6 +34,9 @@ struct GatewayBridgeServiceConfig {
std::vector<int> reserved_uart_ports;
uint32_t bacnet_task_stack_size{8192};
UBaseType_t bacnet_task_priority{5};
uint32_t knx_task_stack_size{8192};
UBaseType_t knx_task_priority{5};
std::optional<GatewayKnxConfig> default_knx_config;
};
struct GatewayBridgeHttpResponse {
@@ -12,6 +12,7 @@
#include "dali_domain.hpp"
#include "gateway_cache.hpp"
#include "gateway_cloud.hpp"
#include "gateway_knx.hpp"
#include "gateway_modbus.hpp"
#include "gateway_provisioning.hpp"
@@ -58,6 +59,7 @@ constexpr const char* kModbusManagementPrefix = "@DALIGW";
struct GatewayBridgeStoredConfig {
BridgeRuntimeConfig bridge;
std::optional<GatewayModbusConfig> modbus;
std::optional<GatewayKnxConfig> knx;
std::optional<GatewayBacnetBridgeConfig> bacnet_server;
};
@@ -799,11 +801,15 @@ cJSON* ToCjson(const DaliValue& value) {
DaliValue::Object GatewayBridgeStoredConfigToValue(
const BridgeRuntimeConfig& bridge_config,
const std::optional<GatewayModbusConfig>& modbus_config,
const std::optional<GatewayKnxConfig>& knx_config,
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
DaliValue::Object out = bridge_config.toJson();
if (modbus_config.has_value()) {
out["modbus"] = GatewayModbusConfigToValue(modbus_config.value());
}
if (knx_config.has_value()) {
out["knx"] = GatewayKnxConfigToValue(knx_config.value());
}
if (bacnet_server_config.has_value()) {
DaliValue::Object bacnet;
bacnet["deviceInstance"] = static_cast<int64_t>(bacnet_server_config->deviceInstance);
@@ -817,9 +823,10 @@ DaliValue::Object GatewayBridgeStoredConfigToValue(
std::string GatewayBridgeStoredConfigToJson(
const BridgeRuntimeConfig& bridge_config,
const std::optional<GatewayModbusConfig>& modbus_config,
const std::optional<GatewayKnxConfig>& knx_config,
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
cJSON* root = ToCjson(DaliValue(GatewayBridgeStoredConfigToValue(
bridge_config, modbus_config, bacnet_server_config)));
bridge_config, modbus_config, knx_config, bacnet_server_config)));
const std::string body = PrintJson(root);
cJSON_Delete(root);
return body;
@@ -843,6 +850,7 @@ GatewayBridgeStoredConfig GatewayBridgeStoredConfigFromValue(const DaliValue::Ob
GatewayBridgeStoredConfig config;
config.bridge = BridgeRuntimeConfig::fromJson(object);
config.modbus = GatewayModbusConfigFromValue(getObjectValue(object, "modbus"));
config.knx = GatewayKnxConfigFromValue(getObjectValue(object, "knx"));
config.bacnet_server = GatewayBacnetBridgeConfigFromValue(
getObjectValue(object, "bacnetServer"));
return config;
@@ -1152,12 +1160,15 @@ struct GatewayBridgeService::ChannelRuntime {
std::unique_ptr<DaliComm> comm;
std::unique_ptr<DaliBridgeEngine> engine;
std::unique_ptr<GatewayModbusBridge> modbus;
std::unique_ptr<GatewayKnxBridge> knx;
std::unique_ptr<GatewayKnxTpIpRouter> knx_router;
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
std::unique_ptr<GatewayBacnetBridgeAdapter> bacnet;
#endif
std::unique_ptr<DaliCloudBridge> cloud;
BridgeRuntimeConfig bridge_config;
std::optional<GatewayModbusConfig> modbus_config;
std::optional<GatewayKnxConfig> knx_config;
std::optional<GatewayBacnetBridgeConfig> bacnet_server_config;
BridgeDiscoveryInventory discovery_inventory;
std::optional<GatewayCloudConfig> cloud_config;
@@ -1166,6 +1177,7 @@ struct GatewayBridgeService::ChannelRuntime {
bool cloud_config_loaded{false};
bool cloud_started{false};
bool modbus_started{false};
bool knx_started{false};
bool bacnet_started{false};
TaskHandle_t modbus_task_handle{nullptr};
std::atomic_bool modbus_stop_requested{false};
@@ -1210,6 +1222,7 @@ struct GatewayBridgeService::ChannelRuntime {
const auto stored_config = GatewayBridgeStoredConfigFromValue(bridge_object);
bridge_config = stored_config.bridge;
modbus_config = stored_config.modbus;
knx_config = stored_config.knx;
bacnet_server_config = stored_config.bacnet_server;
bridge_config_loaded = true;
}
@@ -1245,6 +1258,22 @@ struct GatewayBridgeService::ChannelRuntime {
modbus->setConfig(modbus_config.value());
}
knx = std::make_unique<GatewayKnxBridge>(*engine);
knx_router = std::make_unique<GatewayKnxTpIpRouter>(
*knx, [this](const uint8_t* data, size_t len) {
LockGuard guard(lock);
if (knx == nullptr) {
DaliBridgeResult result;
result.error = "KNX bridge is not ready";
return result;
}
return knx->handleCemiFrame(data, len);
});
if (knx_config.has_value()) {
knx->setConfig(knx_config.value());
knx_router->setConfig(knx_config.value());
}
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
if (service_config.bacnet_enabled) {
bacnet = std::make_unique<GatewayBacnetBridgeAdapter>(*engine);
@@ -1257,6 +1286,7 @@ struct GatewayBridgeService::ChannelRuntime {
#endif
applyCloudModelsLocked();
knx_started = false;
bacnet_started = false;
diagnostic_snapshot_cache.clear();
}
@@ -1433,7 +1463,7 @@ struct GatewayBridgeService::ChannelRuntime {
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.saveObject(
kBridgeConfigKey,
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus,
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus, parsed->knx,
parsed->bacnet_server));
if (err != ESP_OK) {
return err;
@@ -1441,6 +1471,7 @@ struct GatewayBridgeService::ChannelRuntime {
LockGuard guard(lock);
bridge_config = parsed->bridge;
modbus_config = parsed->modbus;
knx_config = parsed->knx;
bacnet_server_config = parsed->bacnet_server;
bridge_config_loaded = true;
applyBridgeConfigLocked();
@@ -1456,6 +1487,7 @@ struct GatewayBridgeService::ChannelRuntime {
LockGuard guard(lock);
bridge_config = BridgeRuntimeConfig{};
modbus_config.reset();
knx_config.reset();
bacnet_server_config.reset();
bridge_config_loaded = false;
applyBridgeConfigLocked();
@@ -1800,6 +1832,41 @@ struct GatewayBridgeService::ChannelRuntime {
}
#endif
esp_err_t startKnx() {
LockGuard guard(lock);
if (!service_config.knx_enabled) {
return ESP_ERR_NOT_SUPPORTED;
}
if (knx == nullptr || knx_router == nullptr) {
return ESP_ERR_INVALID_STATE;
}
const auto config = activeKnxConfigLocked();
if (!config.has_value()) {
return ESP_ERR_NOT_FOUND;
}
knx->setConfig(config.value());
knx_router->setConfig(config.value());
if (!config->ip_router_enabled) {
knx_started = false;
return ESP_ERR_NOT_SUPPORTED;
}
const esp_err_t err = knx_router->start(service_config.knx_task_stack_size,
service_config.knx_task_priority);
knx_started = err == ESP_OK;
return err;
}
esp_err_t stopKnx() {
LockGuard guard(lock);
if (knx_router != nullptr) {
const esp_err_t err = knx_router->stop();
knx_started = false;
return err;
}
knx_started = false;
return ESP_OK;
}
GatewayBridgeHttpResponse execute(std::string_view json) {
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
if (root == nullptr) {
@@ -1887,6 +1954,42 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddItemToObject(root, "bacnet", bacnet_json);
}
cJSON* knx_json = cJSON_CreateObject();
if (knx_json != nullptr) {
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());
if (knx_router != nullptr) {
cJSON_AddStringToObject(knx_json, "lastError", knx_router->lastError().c_str());
}
if (effective_knx.has_value()) {
cJSON_AddBoolToObject(knx_json, "daliRouterEnabled",
effective_knx->dali_router_enabled);
cJSON_AddBoolToObject(knx_json, "ipRouterEnabled",
effective_knx->ip_router_enabled);
cJSON_AddBoolToObject(knx_json, "tunnelEnabled", effective_knx->tunnel_enabled);
cJSON_AddBoolToObject(knx_json, "multicastEnabled",
effective_knx->multicast_enabled);
cJSON_AddNumberToObject(knx_json, "mainGroup", effective_knx->main_group);
cJSON_AddNumberToObject(knx_json, "udpPort", effective_knx->udp_port);
cJSON_AddStringToObject(knx_json, "multicastAddress",
effective_knx->multicast_address.c_str());
cJSON_AddNumberToObject(knx_json, "individualAddress",
effective_knx->individual_address);
cJSON* serial_json = cJSON_CreateObject();
if (serial_json != nullptr) {
cJSON_AddNumberToObject(serial_json, "uartPort", effective_knx->tp_uart.uart_port);
cJSON_AddNumberToObject(serial_json, "txPin", effective_knx->tp_uart.tx_pin);
cJSON_AddNumberToObject(serial_json, "rxPin", effective_knx->tp_uart.rx_pin);
cJSON_AddNumberToObject(serial_json, "baudrate", effective_knx->tp_uart.baudrate);
cJSON_AddItemToObject(knx_json, "tpUart", serial_json);
}
}
cJSON_AddItemToObject(root, "knx", knx_json);
}
cJSON* cloud_json = cJSON_CreateObject();
if (cloud_json != nullptr) {
cJSON_AddBoolToObject(cloud_json, "enabled", service_config.cloud_enabled);
@@ -1906,6 +2009,7 @@ struct GatewayBridgeService::ChannelRuntime {
GatewayBridgeHttpResponse configJson() const {
return GatewayBridgeHttpResponse{ESP_OK,
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config,
knx_config,
bacnet_server_config)};
}
@@ -2104,6 +2208,37 @@ struct GatewayBridgeService::ChannelRuntime {
return JsonOk(root);
}
GatewayBridgeHttpResponse knxBindingsJson() const {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON* bindings = cJSON_CreateArray();
if (bindings != nullptr && knx != nullptr) {
for (const auto& binding : knx->describeDaliBindings()) {
cJSON* item = cJSON_CreateObject();
if (item == nullptr) {
continue;
}
cJSON_AddStringToObject(item, "address", binding.address.c_str());
cJSON_AddNumberToObject(item, "rawAddress", binding.group_address);
cJSON_AddNumberToObject(item, "mainGroup", binding.main_group);
cJSON_AddNumberToObject(item, "middleGroup", binding.middle_group);
cJSON_AddNumberToObject(item, "subGroup", binding.sub_group);
cJSON_AddStringToObject(item, "name", binding.name.c_str());
cJSON_AddStringToObject(item, "datapointType", binding.datapoint_type.c_str());
cJSON_AddStringToObject(item, "dataType",
GatewayKnxDataTypeToString(binding.data_type));
cJSON_AddStringToObject(item, "targetKind",
GatewayKnxTargetKindToString(binding.target.kind));
if (binding.target.address >= 0) {
cJSON_AddNumberToObject(item, "targetAddress", binding.target.address);
}
cJSON_AddItemToArray(bindings, item);
}
}
cJSON_AddItemToObject(root, "bindings", bindings);
return JsonOk(root);
}
GatewayBridgeHttpResponse bacnetBindingsJson() {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
@@ -2597,7 +2732,8 @@ struct GatewayBridgeService::ChannelRuntime {
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.saveObject(
kBridgeConfigKey,
GatewayBridgeStoredConfigToValue(bridge_config, config, bacnet_server_config));
GatewayBridgeStoredConfigToValue(bridge_config, config, knx_config,
bacnet_server_config));
if (err != ESP_OK) {
return err;
}
@@ -2609,6 +2745,34 @@ struct GatewayBridgeService::ChannelRuntime {
return ESP_OK;
}
std::optional<GatewayKnxConfig> activeKnxConfigLocked() const {
if (knx_config.has_value()) {
return knx_config;
}
return service_config.default_knx_config;
}
esp_err_t saveKnxConfig(const GatewayKnxConfig& config) {
LockGuard guard(lock);
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.saveObject(
kBridgeConfigKey,
GatewayBridgeStoredConfigToValue(bridge_config, modbus_config, config,
bacnet_server_config));
if (err != ESP_OK) {
return err;
}
knx_config = config;
bridge_config_loaded = true;
if (knx != nullptr) {
knx->setConfig(config);
}
if (knx_router != nullptr) {
knx_router->setConfig(config);
}
return ESP_OK;
}
std::vector<uint8_t> processModbusPdu(const GatewayModbusConfig& config,
uint8_t unit_id,
const std::vector<uint8_t>& pdu) {
@@ -3215,6 +3379,16 @@ esp_err_t GatewayBridgeService::start() {
}
}
if (config_.knx_enabled && config_.knx_startup_enabled) {
for (const auto& runtime : runtimes_) {
const esp_err_t err = runtime->startKnx();
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(kTag, "gateway=%u KNX/IP startup skipped: %s", runtime->channel.gateway_id,
esp_err_to_name(err));
}
}
}
if (config_.bacnet_enabled && config_.bacnet_startup_enabled) {
for (const auto& runtime : runtimes_) {
const esp_err_t err = runtime->startBacnet();
@@ -3294,6 +3468,9 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
if (action == "modbus") {
return runtime->modbusBindingsJson();
}
if (action == "knx") {
return runtime->knxBindingsJson();
}
if (action == "bacnet") {
return runtime->bacnetBindingsJson();
}
@@ -3502,6 +3679,41 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
}
return handleGet("modbus", gateway_id.value());
}
if (action == "knx_config" || action == "save_knx") {
cJSON* knx_root = cJSON_ParseWithLength(body.data(), body.size());
if (knx_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid KNX config JSON");
}
const cJSON* knx_node = cJSON_GetObjectItemCaseSensitive(knx_root, "knx");
if (knx_node == nullptr) {
knx_node = knx_root;
}
const DaliValue knx_value = FromCjson(knx_node);
cJSON_Delete(knx_root);
const auto parsed = GatewayKnxConfigFromValue(&knx_value);
if (!parsed.has_value()) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid KNX config");
}
const esp_err_t err = runtime->saveKnxConfig(parsed.value());
if (err != ESP_OK) {
return ErrorResponse(err, "failed to save KNX bridge config");
}
return handleGet("knx", gateway_id.value());
}
if (action == "knx_start") {
const esp_err_t err = runtime->startKnx();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to start KNX/IP bridge");
}
return handleGet("knx", gateway_id.value());
}
if (action == "knx_stop") {
const esp_err_t err = runtime->stopKnx();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to stop KNX/IP bridge");
}
return handleGet("knx", gateway_id.value());
}
if (action == "bacnet_start") {
const esp_err_t err = runtime->startBacnet();
if (err != ESP_OK) {
+7
View File
@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/gateway_knx.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_cpp esp_driver_uart freertos log lwip
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,174 @@
#pragma once
#include "bridge.hpp"
#include "model_value.hpp"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lwip/sockets.h"
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <vector>
namespace gateway {
constexpr uint16_t kGatewayKnxDefaultUdpPort = 3671;
constexpr const char* kGatewayKnxDefaultMulticastAddress = "224.0.23.12";
constexpr uint32_t kGatewayKnxDefaultTpBaudrate = 19200;
struct GatewayKnxTpUartConfig {
int uart_port{1};
int tx_pin{-1};
int rx_pin{-1};
uint32_t baudrate{kGatewayKnxDefaultTpBaudrate};
size_t rx_buffer_size{1024};
size_t tx_buffer_size{1024};
uint32_t read_timeout_ms{20};
};
struct GatewayKnxConfig {
bool dali_router_enabled{true};
bool ip_router_enabled{false};
bool tunnel_enabled{true};
bool multicast_enabled{true};
uint8_t main_group{0};
uint16_t udp_port{kGatewayKnxDefaultUdpPort};
std::string multicast_address{kGatewayKnxDefaultMulticastAddress};
uint16_t individual_address{0x1101};
GatewayKnxTpUartConfig tp_uart;
};
enum class GatewayKnxDaliDataType : uint8_t {
kUnknown = 0,
kSwitch = 1,
kBrightness = 2,
kColorTemperature = 3,
kRgb = 4,
};
enum class GatewayKnxDaliTargetKind : uint8_t {
kNone = 0,
kBroadcast = 1,
kShortAddress = 2,
kGroup = 3,
};
struct GatewayKnxDaliTarget {
GatewayKnxDaliTargetKind kind{GatewayKnxDaliTargetKind::kNone};
int address{-1};
};
struct GatewayKnxDaliBinding {
uint16_t group_address{0};
uint8_t main_group{0};
uint8_t middle_group{0};
uint8_t sub_group{0};
std::string address;
std::string name;
std::string datapoint_type;
GatewayKnxDaliDataType data_type{GatewayKnxDaliDataType::kUnknown};
GatewayKnxDaliTarget target;
};
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type);
const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind);
std::optional<GatewayKnxDaliDataType> GatewayKnxDaliDataTypeForMiddleGroup(
uint8_t middle_group);
std::optional<GatewayKnxDaliTarget> GatewayKnxDaliTargetForSubgroup(uint8_t sub_group);
uint16_t GatewayKnxGroupAddress(uint8_t main_group, uint8_t middle_group,
uint8_t sub_group);
std::string GatewayKnxGroupAddressString(uint16_t group_address);
class GatewayKnxBridge {
public:
explicit GatewayKnxBridge(DaliBridgeEngine& engine);
void setConfig(const GatewayKnxConfig& config);
const GatewayKnxConfig& config() const;
std::vector<GatewayKnxDaliBinding> describeDaliBindings() const;
DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len);
DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len);
private:
DaliBridgeResult executeForDecodedWrite(uint16_t group_address,
GatewayKnxDaliDataType data_type,
GatewayKnxDaliTarget target,
const uint8_t* data, size_t len);
DaliBridgeEngine& engine_;
GatewayKnxConfig config_;
};
class GatewayKnxTpIpRouter {
public:
using CemiFrameHandler = std::function<DaliBridgeResult(const uint8_t* data, size_t len)>;
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler);
~GatewayKnxTpIpRouter();
void setConfig(const GatewayKnxConfig& config);
const GatewayKnxConfig& config() const;
esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority);
esp_err_t stop();
bool started() const;
const std::string& lastError() const;
private:
static void TaskEntry(void* arg);
void taskLoop();
void finishTask();
void closeSockets();
bool configureSocket();
bool configureTpUart();
void handleUdpDatagram(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);
void handleConnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
void handleConnectionStateRequest(const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleDisconnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
const ::sockaddr_in& remote);
void sendTunnelIndication(const uint8_t* data, size_t len);
void sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
void sendDisconnectResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
void sendConnectResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
void sendRoutingIndication(const uint8_t* data, size_t len);
void pollTpUart();
void handleTpTelegram(const uint8_t* data, size_t len);
void forwardCemiToTp(const uint8_t* data, size_t len);
GatewayKnxBridge& bridge_;
CemiFrameHandler handler_;
GatewayKnxConfig config_;
TaskHandle_t task_handle_{nullptr};
std::atomic_bool stop_requested_{false};
std::atomic_bool started_{false};
int udp_sock_{-1};
int tp_uart_port_{-1};
uint8_t tunnel_channel_id_{1};
uint8_t expected_tunnel_sequence_{0};
uint8_t tunnel_send_sequence_{0};
bool tunnel_connected_{false};
::sockaddr_in tunnel_remote_{};
std::vector<uint8_t> tp_rx_frame_;
std::string last_error_;
};
} // namespace gateway
+980
View File
@@ -0,0 +1,980 @@
#include "gateway_knx.hpp"
#include "driver/uart.h"
#include "esp_log.h"
#include "lwip/inet.h"
#include "lwip/sockets.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <initializer_list>
#include <utility>
#include <unistd.h>
namespace gateway {
namespace {
constexpr const char* kTag = "gateway_knx";
constexpr uint8_t kCemiLDataReq = 0x11;
constexpr uint8_t kCemiLDataInd = 0x29;
constexpr uint8_t kCemiLDataCon = 0x2e;
constexpr uint16_t kServiceConnectRequest = 0x0205;
constexpr uint16_t kServiceConnectResponse = 0x0206;
constexpr uint16_t kServiceConnectionStateRequest = 0x0207;
constexpr uint16_t kServiceConnectionStateResponse = 0x0208;
constexpr uint16_t kServiceDisconnectRequest = 0x0209;
constexpr uint16_t kServiceDisconnectResponse = 0x020a;
constexpr uint16_t kServiceTunnellingRequest = 0x0420;
constexpr uint16_t kServiceTunnellingAck = 0x0421;
constexpr uint16_t kServiceRoutingIndication = 0x0530;
constexpr uint8_t kKnxNetIpHeaderSize = 0x06;
constexpr uint8_t kKnxNetIpVersion10 = 0x10;
constexpr uint8_t kKnxNoError = 0x00;
constexpr uint8_t kKnxErrorConnectionId = 0x21;
constexpr uint8_t kKnxErrorConnectionType = 0x22;
constexpr uint8_t kKnxErrorNoMoreConnections = 0x24;
constexpr uint8_t kKnxErrorSequenceNumber = 0x04;
constexpr uint8_t kKnxConnectionTypeTunnel = 0x04;
constexpr uint8_t kKnxTunnelLayerLink = 0x02;
struct DecodedGroupWrite {
uint16_t group_address{0};
std::vector<uint8_t> data;
};
uint16_t ReadBe16(const uint8_t* data) {
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
}
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);
}
std::optional<int> ObjectIntAny(const DaliValue::Object& object,
std::initializer_list<const char*> keys) {
for (const char* key : keys) {
if (const auto value = getObjectInt(object, key)) {
return value;
}
}
return std::nullopt;
}
std::optional<bool> ObjectBoolAny(const DaliValue::Object& object,
std::initializer_list<const char*> keys) {
for (const char* key : keys) {
if (const auto value = getObjectBool(object, key)) {
return value;
}
}
return std::nullopt;
}
std::optional<std::string> ObjectStringAny(const DaliValue::Object& object,
std::initializer_list<const char*> keys) {
for (const char* key : keys) {
if (const auto value = getObjectString(object, key)) {
return value;
}
}
return std::nullopt;
}
std::string TargetName(const GatewayKnxDaliTarget& target) {
switch (target.kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
return "Broadcast";
case GatewayKnxDaliTargetKind::kShortAddress:
return "A" + std::to_string(target.address);
case GatewayKnxDaliTargetKind::kGroup:
return "Group " + std::to_string(target.address);
case GatewayKnxDaliTargetKind::kNone:
default:
return "Unmapped";
}
}
std::string DataTypeName(GatewayKnxDaliDataType data_type) {
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch:
return "Switch";
case GatewayKnxDaliDataType::kBrightness:
return "Dimmer";
case GatewayKnxDaliDataType::kColorTemperature:
return "Color Temperature";
case GatewayKnxDaliDataType::kRgb:
return "RGB";
case GatewayKnxDaliDataType::kUnknown:
default:
return "Unknown";
}
}
const char* DataTypeDpt(GatewayKnxDaliDataType data_type) {
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch:
return "DPST-1-1";
case GatewayKnxDaliDataType::kBrightness:
return "DPST-5-1";
case GatewayKnxDaliDataType::kColorTemperature:
return "DPST-7-600";
case GatewayKnxDaliDataType::kRgb:
return "DPST-232-600";
case GatewayKnxDaliDataType::kUnknown:
default:
return "";
}
}
std::optional<DecodedGroupWrite> DecodeCemiGroupWrite(const uint8_t* data, size_t len) {
if (data == nullptr || len < 10) {
return std::nullopt;
}
const uint8_t message_code = data[0];
if (message_code != kCemiLDataReq && message_code != kCemiLDataInd &&
message_code != kCemiLDataCon) {
return std::nullopt;
}
const size_t base = 2U + data[1];
if (len < base + 8U) {
return std::nullopt;
}
const uint8_t control2 = data[base + 1];
if ((control2 & 0x80) == 0) {
return std::nullopt;
}
const uint16_t destination = ReadBe16(data + base + 4);
const size_t tpdu_len = static_cast<size_t>(data[base + 6]) + 1U;
if (tpdu_len < 2U || len < base + 7U + tpdu_len) {
return std::nullopt;
}
const uint8_t* tpdu = data + base + 7;
const uint16_t apci = static_cast<uint16_t>(((tpdu[0] & 0x03) << 8) | (tpdu[1] & 0xc0));
if (apci != 0x80) {
return std::nullopt;
}
DecodedGroupWrite out;
out.group_address = destination;
if (tpdu_len == 2U) {
out.data.push_back(tpdu[1] & 0x3f);
} else {
out.data.assign(tpdu + 2, tpdu + tpdu_len);
}
return out;
}
DaliBridgeRequest RequestForTarget(uint16_t group_address,
const GatewayKnxDaliTarget& target,
BridgeOperation operation) {
DaliBridgeRequest request;
request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
request.operation = operation;
switch (target.kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
request.metadata["broadcast"] = true;
break;
case GatewayKnxDaliTargetKind::kShortAddress:
request.shortAddress = target.address;
break;
case GatewayKnxDaliTargetKind::kGroup:
request.metadata["group"] = target.address;
break;
case GatewayKnxDaliTargetKind::kNone:
default:
break;
}
request.metadata["sourceProtocol"] = "knx";
request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address);
return request;
}
DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) {
DaliBridgeResult result;
result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
result.error = message == nullptr ? "KNX error" : message;
return result;
}
bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) {
return sendto(sock, data, len, 0, reinterpret_cast<const sockaddr*>(&remote),
sizeof(remote)) == static_cast<int>(len);
}
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;
packet[1] = kKnxNetIpVersion10;
WriteBe16(packet.data() + 2, service);
WriteBe16(packet.data() + 4, static_cast<uint16_t>(packet.size()));
if (!body.empty()) {
std::memcpy(packet.data() + 6, body.data(), body.size());
}
return packet;
}
std::array<uint8_t, 8> HpaiForRemote(const sockaddr_in& remote) {
std::array<uint8_t, 8> hpai{};
hpai[0] = 0x08;
hpai[1] = 0x01;
const uint32_t address = ntohl(remote.sin_addr.s_addr);
hpai[2] = static_cast<uint8_t>((address >> 24) & 0xff);
hpai[3] = static_cast<uint8_t>((address >> 16) & 0xff);
hpai[4] = static_cast<uint8_t>((address >> 8) & 0xff);
hpai[5] = static_cast<uint8_t>(address & 0xff);
WriteBe16(hpai.data() + 6, ntohs(remote.sin_port));
return hpai;
}
bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service,
uint16_t* total_len) {
if (data == nullptr || len < 6 || data[0] != kKnxNetIpHeaderSize ||
data[1] != kKnxNetIpVersion10) {
return false;
}
*service = ReadBe16(data + 2);
*total_len = ReadBe16(data + 4);
return *total_len >= 6 && *total_len <= len;
}
bool IsExtendedTpFrame(const uint8_t* data, size_t len) {
return len > 0 && (data[0] & 0xD3) == 0x10;
}
size_t ExpectedTpFrameSize(const uint8_t* data, size_t len) {
if (data == nullptr || len < 6) {
return 0;
}
if (IsExtendedTpFrame(data, len)) {
return 9U + data[6];
}
return 8U + (data[5] & 0x0F);
}
bool ValidateTpChecksum(const uint8_t* data, size_t len) {
if (data == nullptr || len < 2) {
return false;
}
uint8_t crc = 0xFF;
for (size_t index = 0; index + 1 < len; ++index) {
crc ^= data[index];
}
return data[len - 1] == crc;
}
std::optional<std::vector<uint8_t>> CemiToTpTelegram(const uint8_t* data, size_t len) {
if (data == nullptr || len < 10 || data[1] != 0) {
return std::nullopt;
}
const uint8_t* ctrl = data + 2;
const bool standard = (ctrl[0] & 0x80) != 0;
const size_t tp_len = standard ? len - 2U : len - 1U;
if (tp_len < 8) {
return std::nullopt;
}
std::vector<uint8_t> telegram(tp_len, 0);
if (standard) {
telegram[0] = ctrl[0];
std::memcpy(telegram.data() + 1, ctrl + 2, 4);
telegram[5] = static_cast<uint8_t>((ctrl[1] & 0xF0) | (ctrl[6] & 0x0F));
if (tp_len > 7U) {
std::memcpy(telegram.data() + 6, ctrl + 7, tp_len - 7U);
}
} else {
std::memcpy(telegram.data(), ctrl, tp_len - 1U);
}
uint8_t crc = 0xFF;
for (size_t index = 0; index + 1 < telegram.size(); ++index) {
crc ^= telegram[index];
}
telegram.back() = crc;
return telegram;
}
std::optional<std::vector<uint8_t>> TpTelegramToCemi(const uint8_t* data, size_t len) {
if (data == nullptr || len < 8 || !ValidateTpChecksum(data, len)) {
return std::nullopt;
}
const bool extended = IsExtendedTpFrame(data, len);
const size_t cemi_len = len + (extended ? 2U : 3U) - 1U;
std::vector<uint8_t> cemi(cemi_len, 0);
cemi[0] = kCemiLDataInd;
cemi[1] = 0x00;
cemi[2] = data[0];
if (extended) {
std::memcpy(cemi.data() + 2, data, len - 1U);
} else {
cemi[3] = data[5] & 0xF0;
std::memcpy(cemi.data() + 4, data + 1, 4);
cemi[8] = data[5] & 0x0F;
const size_t copy_len = static_cast<size_t>(cemi[8]) + 1U;
if (9U + copy_len > cemi.size() || 6U + copy_len > len) {
return std::nullopt;
}
std::memcpy(cemi.data() + 9, data + 6, copy_len);
}
return cemi;
}
} // namespace
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value) {
if (value == nullptr || value->asObject() == nullptr) {
return std::nullopt;
}
const auto& object = *value->asObject();
GatewayKnxConfig config;
config.dali_router_enabled = ObjectBoolAny(object, {"daliRouterEnabled", "dali_router_enabled"})
.value_or(config.dali_router_enabled);
config.ip_router_enabled = ObjectBoolAny(object, {"ipRouterEnabled", "ip_router_enabled"})
.value_or(config.ip_router_enabled);
config.tunnel_enabled = ObjectBoolAny(object, {"tunnelEnabled", "tunnel_enabled"})
.value_or(config.tunnel_enabled);
config.multicast_enabled = ObjectBoolAny(object, {"multicastEnabled", "multicast_enabled"})
.value_or(config.multicast_enabled);
config.main_group = static_cast<uint8_t>(
std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group),
0, 31));
config.udp_port = static_cast<uint16_t>(std::clamp(
ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1,
65535));
config.multicast_address = ObjectStringAny(object, {"multicastAddress", "multicast_address"})
.value_or(config.multicast_address);
config.individual_address = static_cast<uint16_t>(std::clamp(
ObjectIntAny(object, {"individualAddress", "individual_address"})
.value_or(config.individual_address),
0, 0xffff));
const auto* tp_uart = getObjectValue(object, "tpUart");
if (tp_uart == nullptr) {
tp_uart = getObjectValue(object, "tp_uart");
}
if (tp_uart != nullptr && tp_uart->asObject() != nullptr) {
const auto& serial = *tp_uart->asObject();
config.tp_uart.uart_port = std::clamp(
ObjectIntAny(serial, {"uartPort", "uart_port"}).value_or(config.tp_uart.uart_port), 0,
2);
config.tp_uart.tx_pin = ObjectIntAny(serial, {"txPin", "tx_pin"}).value_or(config.tp_uart.tx_pin);
config.tp_uart.rx_pin = ObjectIntAny(serial, {"rxPin", "rx_pin"}).value_or(config.tp_uart.rx_pin);
config.tp_uart.baudrate = static_cast<uint32_t>(std::max(
1200, ObjectIntAny(serial, {"baudrate", "baud"}).value_or(config.tp_uart.baudrate)));
config.tp_uart.rx_buffer_size = static_cast<size_t>(std::max(
128, ObjectIntAny(serial, {"rxBufferSize", "rx_buffer_size"})
.value_or(static_cast<int>(config.tp_uart.rx_buffer_size))));
config.tp_uart.tx_buffer_size = static_cast<size_t>(std::max(
128, ObjectIntAny(serial, {"txBufferSize", "tx_buffer_size"})
.value_or(static_cast<int>(config.tp_uart.tx_buffer_size))));
config.tp_uart.read_timeout_ms = static_cast<uint32_t>(std::max(
1, ObjectIntAny(serial, {"readTimeoutMs", "read_timeout_ms"})
.value_or(static_cast<int>(config.tp_uart.read_timeout_ms))));
}
return config;
}
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
DaliValue::Object out;
out["daliRouterEnabled"] = config.dali_router_enabled;
out["ipRouterEnabled"] = config.ip_router_enabled;
out["tunnelEnabled"] = config.tunnel_enabled;
out["multicastEnabled"] = config.multicast_enabled;
out["mainGroup"] = static_cast<int>(config.main_group);
out["udpPort"] = static_cast<int>(config.udp_port);
out["multicastAddress"] = config.multicast_address;
out["individualAddress"] = static_cast<int>(config.individual_address);
DaliValue::Object serial;
serial["uartPort"] = config.tp_uart.uart_port;
serial["txPin"] = config.tp_uart.tx_pin;
serial["rxPin"] = config.tp_uart.rx_pin;
serial["baudrate"] = static_cast<int>(config.tp_uart.baudrate);
serial["rxBufferSize"] = static_cast<int>(config.tp_uart.rx_buffer_size);
serial["txBufferSize"] = static_cast<int>(config.tp_uart.tx_buffer_size);
serial["readTimeoutMs"] = static_cast<int>(config.tp_uart.read_timeout_ms);
out["tpUart"] = std::move(serial);
return DaliValue(std::move(out));
}
const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) {
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch:
return "switch";
case GatewayKnxDaliDataType::kBrightness:
return "brightness";
case GatewayKnxDaliDataType::kColorTemperature:
return "color_temperature";
case GatewayKnxDaliDataType::kRgb:
return "rgb";
case GatewayKnxDaliDataType::kUnknown:
default:
return "unknown";
}
}
const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind) {
switch (kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
return "broadcast";
case GatewayKnxDaliTargetKind::kShortAddress:
return "short_address";
case GatewayKnxDaliTargetKind::kGroup:
return "group";
case GatewayKnxDaliTargetKind::kNone:
default:
return "none";
}
}
std::optional<GatewayKnxDaliDataType> GatewayKnxDaliDataTypeForMiddleGroup(
uint8_t middle_group) {
switch (middle_group) {
case 1:
return GatewayKnxDaliDataType::kSwitch;
case 2:
return GatewayKnxDaliDataType::kBrightness;
case 3:
return GatewayKnxDaliDataType::kColorTemperature;
case 4:
return GatewayKnxDaliDataType::kRgb;
default:
return std::nullopt;
}
}
std::optional<GatewayKnxDaliTarget> GatewayKnxDaliTargetForSubgroup(uint8_t sub_group) {
if (sub_group == 0) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127};
}
if (sub_group >= 1 && sub_group <= 64) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
static_cast<int>(sub_group - 1)};
}
if (sub_group >= 65 && sub_group <= 80) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
static_cast<int>(sub_group - 65)};
}
return std::nullopt;
}
uint16_t GatewayKnxGroupAddress(uint8_t main_group, uint8_t middle_group,
uint8_t sub_group) {
return static_cast<uint16_t>(((main_group & 0x1f) << 11) |
((middle_group & 0x07) << 8) | sub_group);
}
std::string GatewayKnxGroupAddressString(uint16_t group_address) {
const int main = (group_address >> 11) & 0x1f;
const int middle = (group_address >> 8) & 0x07;
const int sub = group_address & 0xff;
return std::to_string(main) + "/" + std::to_string(middle) + "/" +
std::to_string(sub);
}
GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {}
void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; }
const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; }
std::vector<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() const {
std::vector<GatewayKnxDaliBinding> bindings;
bindings.reserve(4 * 81);
for (uint8_t middle = 1; middle <= 4; ++middle) {
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
if (!data_type.has_value()) {
continue;
}
for (uint8_t sub = 0; sub <= 80; ++sub) {
const auto target = GatewayKnxDaliTargetForSubgroup(sub);
if (!target.has_value()) {
continue;
}
GatewayKnxDaliBinding binding;
binding.main_group = config_.main_group;
binding.middle_group = middle;
binding.sub_group = sub;
binding.group_address = GatewayKnxGroupAddress(config_.main_group, middle, sub);
binding.address = GatewayKnxGroupAddressString(binding.group_address);
binding.data_type = data_type.value();
binding.target = target.value();
binding.datapoint_type = DataTypeDpt(data_type.value());
binding.name = TargetName(target.value()) + " - " + DataTypeName(data_type.value());
bindings.push_back(std::move(binding));
}
}
return bindings;
}
DaliBridgeResult GatewayKnxBridge::handleCemiFrame(const uint8_t* data, size_t len) {
const auto decoded = DecodeCemiGroupWrite(data, len);
if (!decoded.has_value()) {
return ErrorResult(0, "unsupported or non group-write cEMI frame");
}
return handleGroupWrite(decoded->group_address, decoded->data.data(), decoded->data.size());
}
DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len) {
if (!config_.dali_router_enabled) {
return ErrorResult(group_address, "KNX to DALI router disabled");
}
const uint8_t main = static_cast<uint8_t>((group_address >> 11) & 0x1f);
const uint8_t middle = static_cast<uint8_t>((group_address >> 8) & 0x07);
const uint8_t sub = static_cast<uint8_t>(group_address & 0xff);
if (main != config_.main_group) {
return ErrorResult(group_address, "KNX main group does not match gateway config");
}
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
const auto target = GatewayKnxDaliTargetForSubgroup(sub);
if (!data_type.has_value() || !target.has_value()) {
return ErrorResult(group_address, "unmapped KNX group address");
}
return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len);
}
DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address,
GatewayKnxDaliDataType data_type,
GatewayKnxDaliTarget target,
const uint8_t* data, size_t len) {
if (target.kind == GatewayKnxDaliTargetKind::kNone) {
return ErrorResult(group_address, "missing DALI target");
}
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch: {
if (data == nullptr || len < 1) {
return ErrorResult(group_address, "missing DPT1 switch payload");
}
DaliBridgeRequest request = RequestForTarget(
group_address, target, (data[0] & 0x01) != 0 ? BridgeOperation::on : BridgeOperation::off);
return engine_.execute(request);
}
case GatewayKnxDaliDataType::kBrightness: {
if (data == nullptr || len < 1) {
return ErrorResult(group_address, "missing DPT5 brightness payload");
}
DaliBridgeRequest request = RequestForTarget(group_address, target,
BridgeOperation::setBrightnessPercent);
request.value = (static_cast<double>(data[0]) * 100.0) / 255.0;
return engine_.execute(request);
}
case GatewayKnxDaliDataType::kColorTemperature: {
if (data == nullptr || len < 2) {
return ErrorResult(group_address, "missing DPT7 color temperature payload");
}
DaliBridgeRequest request = RequestForTarget(group_address, target,
BridgeOperation::setColorTemperature);
request.value = static_cast<int>(ReadBe16(data));
return engine_.execute(request);
}
case GatewayKnxDaliDataType::kRgb: {
if (data == nullptr || len < 3) {
return ErrorResult(group_address, "missing DPT232 RGB payload");
}
DaliBridgeRequest request = RequestForTarget(group_address, target,
BridgeOperation::setColourRGB);
DaliValue::Object rgb;
rgb["r"] = static_cast<int>(data[0]);
rgb["g"] = static_cast<int>(data[1]);
rgb["b"] = static_cast<int>(data[2]);
request.value = std::move(rgb);
return engine_.execute(request);
}
case GatewayKnxDaliDataType::kUnknown:
default:
return ErrorResult(group_address, "unsupported KNX data type");
}
}
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler)
: bridge_(bridge), handler_(std::move(handler)) {}
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); }
void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; }
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) {
if (started_ || task_handle_ != nullptr) {
return ESP_OK;
}
if (!config_.ip_router_enabled) {
return ESP_ERR_NOT_SUPPORTED;
}
stop_requested_ = false;
last_error_.clear();
const BaseType_t created = xTaskCreate(&GatewayKnxTpIpRouter::TaskEntry, "gw_knx_ip",
task_stack_size, this, task_priority, &task_handle_);
if (created != pdPASS) {
task_handle_ = nullptr;
return ESP_ERR_NO_MEM;
}
started_ = true;
return ESP_OK;
}
esp_err_t GatewayKnxTpIpRouter::stop() {
stop_requested_ = true;
closeSockets();
return ESP_OK;
}
bool GatewayKnxTpIpRouter::started() const { return started_; }
const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; }
void GatewayKnxTpIpRouter::TaskEntry(void* arg) {
static_cast<GatewayKnxTpIpRouter*>(arg)->taskLoop();
}
void GatewayKnxTpIpRouter::taskLoop() {
if (!configureSocket()) {
finishTask();
return;
}
configureTpUart();
std::array<uint8_t, 768> buffer{};
while (!stop_requested_) {
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();
if (!stop_requested_) {
vTaskDelay(pdMS_TO_TICKS(10));
}
continue;
}
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
pollTpUart();
}
finishTask();
}
void GatewayKnxTpIpRouter::finishTask() {
closeSockets();
started_ = false;
task_handle_ = nullptr;
vTaskDelete(nullptr);
}
void GatewayKnxTpIpRouter::closeSockets() {
if (udp_sock_ >= 0) {
shutdown(udp_sock_, SHUT_RDWR);
close(udp_sock_);
udp_sock_ = -1;
}
if (tp_uart_port_ >= 0) {
uart_driver_delete(static_cast<uart_port_t>(tp_uart_port_));
tp_uart_port_ = -1;
}
}
bool GatewayKnxTpIpRouter::configureSocket() {
udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (udp_sock_ < 0) {
last_error_ = "failed to create KNXnet/IP UDP socket";
return false;
}
int reuse = 1;
setsockopt(udp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in bind_addr{};
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(config_.udp_port);
if (bind(udp_sock_, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) {
last_error_ = "failed to bind KNXnet/IP UDP socket";
closeSockets();
return false;
}
timeval timeout{};
timeout.tv_sec = 0;
timeout.tv_usec = 20000;
setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
if (config_.multicast_enabled) {
uint8_t multicast_loop = 0;
setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop,
sizeof(multicast_loop));
ip_mreq mreq{};
mreq.imr_multiaddr.s_addr = inet_addr(config_.multicast_address.c_str());
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
ESP_LOGW(kTag, "failed to join KNX multicast group %s", config_.multicast_address.c_str());
}
}
return true;
}
bool GatewayKnxTpIpRouter::configureTpUart() {
const auto& serial = config_.tp_uart;
if (serial.uart_port < 0 || serial.uart_port > 2) {
return false;
}
uart_config_t uart_config{};
uart_config.baud_rate = static_cast<int>(serial.baudrate);
uart_config.data_bits = UART_DATA_8_BITS;
uart_config.parity = UART_PARITY_EVEN;
uart_config.stop_bits = UART_STOP_BITS_1;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uart_config.source_clk = UART_SCLK_DEFAULT;
const uart_port_t uart_port = static_cast<uart_port_t>(serial.uart_port);
if (uart_param_config(uart_port, &uart_config) != ESP_OK) {
return false;
}
if (uart_set_pin(uart_port, serial.tx_pin, serial.rx_pin, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE) != ESP_OK) {
return false;
}
if (uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr,
0) != ESP_OK) {
return false;
}
tp_uart_port_ = serial.uart_port;
return true;
}
void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
const sockaddr_in& remote) {
uint16_t service = 0;
uint16_t total_len = 0;
if (!ParseKnxNetIpHeader(data, len, &service, &total_len)) {
return;
}
const uint8_t* body = data + 6;
const size_t body_len = total_len - 6;
switch (service) {
case kServiceRoutingIndication:
if (config_.multicast_enabled) {
handleRoutingIndication(body, body_len);
}
break;
case kServiceTunnellingRequest:
if (config_.tunnel_enabled) {
handleTunnellingRequest(body, body_len, remote);
}
break;
case kServiceConnectRequest:
if (config_.tunnel_enabled) {
handleConnectRequest(body, body_len, remote);
}
break;
case kServiceConnectionStateRequest:
handleConnectionStateRequest(body, body_len, remote);
break;
case kServiceDisconnectRequest:
handleDisconnectRequest(body, body_len, remote);
break;
default:
break;
}
}
void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t len) {
if (body == nullptr || len == 0) {
return;
}
const DaliBridgeResult result = handler_(body, len);
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str());
}
forwardCemiToTp(body, len);
}
void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
if (body == nullptr || len < 5 || body[0] != 0x04) {
return;
}
const uint8_t channel_id = body[1];
const uint8_t sequence = body[2];
if (!tunnel_connected_ || channel_id != tunnel_channel_id_) {
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
if (sequence != expected_tunnel_sequence_) {
sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote);
return;
}
expected_tunnel_sequence_ = static_cast<uint8_t>((expected_tunnel_sequence_ + 1) & 0xff);
sendTunnellingAck(channel_id, sequence, kKnxNoError, remote);
const uint8_t* cemi = body + 4;
const size_t cemi_len = len - 4;
const DaliBridgeResult result = handler_(cemi, cemi_len);
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str());
}
forwardCemiToTp(cemi, cemi_len);
}
void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
if (body == nullptr || len < 20) {
return;
}
const size_t cri_offset = 16;
if (body[cri_offset] < 4 || body[cri_offset + 1] != kKnxConnectionTypeTunnel ||
body[cri_offset + 2] != kKnxTunnelLayerLink) {
sendConnectResponse(0, kKnxErrorConnectionType, remote);
return;
}
if (tunnel_connected_) {
sendConnectResponse(0, kKnxErrorNoMoreConnections, remote);
return;
}
tunnel_connected_ = true;
expected_tunnel_sequence_ = 0;
tunnel_send_sequence_ = 0;
tunnel_remote_ = remote;
sendConnectResponse(tunnel_channel_id_, kKnxNoError, remote);
}
void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
if (body == nullptr || len < 2) {
return;
}
const uint8_t channel_id = body[0];
sendConnectionStateResponse(
channel_id, tunnel_connected_ && channel_id == tunnel_channel_id_ ? kKnxNoError
: kKnxErrorConnectionId,
remote);
}
void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
if (body == nullptr || len < 2) {
return;
}
const uint8_t channel_id = body[0];
const uint8_t status = tunnel_connected_ && channel_id == tunnel_channel_id_
? kKnxNoError
: kKnxErrorConnectionId;
if (status == kKnxNoError) {
tunnel_connected_ = false;
expected_tunnel_sequence_ = 0;
tunnel_send_sequence_ = 0;
}
sendDisconnectResponse(channel_id, status, remote);
}
void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence,
uint8_t status, const sockaddr_in& remote) {
const std::vector<uint8_t> body{0x04, channel_id, sequence, status};
const auto packet = KnxNetIpPacket(kServiceTunnellingAck, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) {
if (!tunnel_connected_ || udp_sock_ < 0 || data == nullptr || len == 0) {
return;
}
std::vector<uint8_t> body;
body.reserve(4 + len);
body.push_back(0x04);
body.push_back(tunnel_channel_id_);
body.push_back(tunnel_send_sequence_++);
body.push_back(0x00);
body.insert(body.end(), data, data + len);
const auto packet = KnxNetIpPacket(kServiceTunnellingRequest, body);
SendAll(udp_sock_, packet.data(), packet.size(), tunnel_remote_);
}
void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
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);
}
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);
}
void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote) {
std::vector<uint8_t> body;
body.reserve(16);
body.push_back(channel_id);
body.push_back(status);
const auto data_endpoint = HpaiForRemote(remote);
body.insert(body.end(), data_endpoint.begin(), data_endpoint.end());
body.push_back(0x04);
body.push_back(kKnxConnectionTypeTunnel);
body.push_back(static_cast<uint8_t>((config_.individual_address >> 8) & 0xff));
body.push_back(static_cast<uint8_t>(config_.individual_address & 0xff));
const auto packet = KnxNetIpPacket(kServiceConnectResponse, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) {
if (!config_.multicast_enabled || udp_sock_ < 0 || data == nullptr || len == 0) {
return;
}
sockaddr_in remote{};
remote.sin_family = AF_INET;
remote.sin_port = htons(config_.udp_port);
remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str());
const std::vector<uint8_t> body(data, data + len);
const auto packet = KnxNetIpPacket(kServiceRoutingIndication, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::pollTpUart() {
if (tp_uart_port_ < 0) {
return;
}
std::array<uint8_t, 128> buffer{};
const int read = uart_read_bytes(static_cast<uart_port_t>(tp_uart_port_), buffer.data(),
buffer.size(), 0);
if (read <= 0) {
return;
}
for (int index = 0; index < read; ++index) {
tp_rx_frame_.push_back(buffer[index]);
const size_t expected = ExpectedTpFrameSize(tp_rx_frame_.data(), tp_rx_frame_.size());
if (expected == 0) {
continue;
}
if (tp_rx_frame_.size() == expected) {
handleTpTelegram(tp_rx_frame_.data(), tp_rx_frame_.size());
tp_rx_frame_.clear();
} else if (tp_rx_frame_.size() > expected || tp_rx_frame_.size() > 263U) {
tp_rx_frame_.clear();
}
}
}
void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) {
const auto cemi = TpTelegramToCemi(data, len);
if (!cemi.has_value()) {
return;
}
const DaliBridgeResult result = handler_(cemi->data(), cemi->size());
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "KNX TP frame not routed to DALI: %s", result.error.c_str());
}
sendTunnelIndication(cemi->data(), cemi->size());
sendRoutingIndication(cemi->data(), cemi->size());
}
void GatewayKnxTpIpRouter::forwardCemiToTp(const uint8_t* data, size_t len) {
if (tp_uart_port_ < 0 || data == nullptr || len == 0) {
return;
}
const auto telegram = CemiToTpTelegram(data, len);
if (!telegram.has_value()) {
return;
}
uart_write_bytes(static_cast<uart_port_t>(tp_uart_port_), telegram->data(), telegram->size());
}
} // namespace gateway