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