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(
|
idf_component_register(
|
||||||
SRCS "app_main.cpp"
|
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)
|
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
|
||||||
@@ -501,6 +501,98 @@ config GATEWAY_START_BACNET_BRIDGE_ENABLED
|
|||||||
help
|
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.
|
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
|
config GATEWAY_CLOUD_BRIDGE_SUPPORTED
|
||||||
bool "MQTT cloud bridge is supported"
|
bool "MQTT cloud bridge is supported"
|
||||||
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
|
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
|
||||||
|
|||||||
@@ -144,6 +144,46 @@
|
|||||||
#define CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY 5
|
#define CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY 5
|
||||||
#endif
|
#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
|
#ifndef CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS
|
||||||
#define CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS 5000
|
#define CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS 5000
|
||||||
#endif
|
#endif
|
||||||
@@ -237,6 +277,30 @@ constexpr bool kBacnetBridgeStartupEnabled = true;
|
|||||||
constexpr bool kBacnetBridgeStartupEnabled = false;
|
constexpr bool kBacnetBridgeStartupEnabled = false;
|
||||||
#endif
|
#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
|
#ifdef CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED
|
||||||
constexpr bool kCloudBridgeSupported = true;
|
constexpr bool kCloudBridgeSupported = true;
|
||||||
#else
|
#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) {
|
if (!any_enabled) {
|
||||||
ESP_LOGE(kTag, "no DALI PHY is configured; enable at least one native or serial channel");
|
ESP_LOGE(kTag, "no DALI PHY is configured; enable at least one native or serial channel");
|
||||||
return false;
|
return false;
|
||||||
@@ -626,6 +710,9 @@ extern "C" void app_main(void) {
|
|||||||
bridge_config.bacnet_enabled = profile.enable_wifi && kBacnetBridgeSupported;
|
bridge_config.bacnet_enabled = profile.enable_wifi && kBacnetBridgeSupported;
|
||||||
bridge_config.bacnet_startup_enabled = profile.enable_wifi && kBacnetBridgeSupported &&
|
bridge_config.bacnet_startup_enabled = profile.enable_wifi && kBacnetBridgeSupported &&
|
||||||
kBacnetBridgeStartupEnabled;
|
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_enabled = profile.enable_wifi && kCloudBridgeSupported;
|
||||||
bridge_config.cloud_startup_enabled = profile.enable_wifi && kCloudBridgeSupported &&
|
bridge_config.cloud_startup_enabled = profile.enable_wifi && kCloudBridgeSupported &&
|
||||||
kCloudBridgeStartupEnabled;
|
kCloudBridgeStartupEnabled;
|
||||||
@@ -649,6 +736,9 @@ extern "C" void app_main(void) {
|
|||||||
bridge_config.reserved_uart_ports.push_back(2);
|
bridge_config.reserved_uart_ports.push_back(2);
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
if (kKnxBridgeSupported) {
|
||||||
|
bridge_config.reserved_uart_ports.push_back(CONFIG_GATEWAY_KNX_TP_UART_PORT);
|
||||||
|
}
|
||||||
if (kModbusBridgeSupported) {
|
if (kModbusBridgeSupported) {
|
||||||
gateway::GatewayModbusConfig default_modbus;
|
gateway::GatewayModbusConfig default_modbus;
|
||||||
#if defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU)
|
#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);
|
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE);
|
||||||
bridge_config.bacnet_task_priority =
|
bridge_config.bacnet_task_priority =
|
||||||
static_cast<UBaseType_t>(CONFIG_GATEWAY_BRIDGE_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,
|
s_bridge = std::make_unique<gateway::GatewayBridgeService>(*s_dali_domain, *s_cache,
|
||||||
bridge_config);
|
bridge_config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -666,6 +666,7 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502
|
|||||||
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
|
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
|
||||||
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
|
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
|
||||||
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
|
# 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_CLOUD_BRIDGE_SUPPORTED=y
|
||||||
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
|
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
|
||||||
CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144
|
CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ set(GATEWAY_BRIDGE_REQUIRES
|
|||||||
esp_driver_uart
|
esp_driver_uart
|
||||||
freertos
|
freertos
|
||||||
gateway_cache
|
gateway_cache
|
||||||
|
gateway_knx
|
||||||
gateway_modbus
|
gateway_modbus
|
||||||
log
|
log
|
||||||
lwip
|
lwip
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
#include "gateway_knx.hpp"
|
||||||
#include "gateway_modbus.hpp"
|
#include "gateway_modbus.hpp"
|
||||||
|
|
||||||
namespace gateway {
|
namespace gateway {
|
||||||
@@ -22,6 +23,8 @@ struct GatewayBridgeServiceConfig {
|
|||||||
bool modbus_startup_enabled{false};
|
bool modbus_startup_enabled{false};
|
||||||
bool bacnet_enabled{false};
|
bool bacnet_enabled{false};
|
||||||
bool bacnet_startup_enabled{false};
|
bool bacnet_startup_enabled{false};
|
||||||
|
bool knx_enabled{false};
|
||||||
|
bool knx_startup_enabled{false};
|
||||||
bool cloud_enabled{true};
|
bool cloud_enabled{true};
|
||||||
bool cloud_startup_enabled{false};
|
bool cloud_startup_enabled{false};
|
||||||
uint32_t modbus_task_stack_size{6144};
|
uint32_t modbus_task_stack_size{6144};
|
||||||
@@ -31,6 +34,9 @@ struct GatewayBridgeServiceConfig {
|
|||||||
std::vector<int> reserved_uart_ports;
|
std::vector<int> reserved_uart_ports;
|
||||||
uint32_t bacnet_task_stack_size{8192};
|
uint32_t bacnet_task_stack_size{8192};
|
||||||
UBaseType_t bacnet_task_priority{5};
|
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 {
|
struct GatewayBridgeHttpResponse {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "dali_domain.hpp"
|
#include "dali_domain.hpp"
|
||||||
#include "gateway_cache.hpp"
|
#include "gateway_cache.hpp"
|
||||||
#include "gateway_cloud.hpp"
|
#include "gateway_cloud.hpp"
|
||||||
|
#include "gateway_knx.hpp"
|
||||||
#include "gateway_modbus.hpp"
|
#include "gateway_modbus.hpp"
|
||||||
#include "gateway_provisioning.hpp"
|
#include "gateway_provisioning.hpp"
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ constexpr const char* kModbusManagementPrefix = "@DALIGW";
|
|||||||
struct GatewayBridgeStoredConfig {
|
struct GatewayBridgeStoredConfig {
|
||||||
BridgeRuntimeConfig bridge;
|
BridgeRuntimeConfig bridge;
|
||||||
std::optional<GatewayModbusConfig> modbus;
|
std::optional<GatewayModbusConfig> modbus;
|
||||||
|
std::optional<GatewayKnxConfig> knx;
|
||||||
std::optional<GatewayBacnetBridgeConfig> bacnet_server;
|
std::optional<GatewayBacnetBridgeConfig> bacnet_server;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -799,11 +801,15 @@ cJSON* ToCjson(const DaliValue& value) {
|
|||||||
DaliValue::Object GatewayBridgeStoredConfigToValue(
|
DaliValue::Object GatewayBridgeStoredConfigToValue(
|
||||||
const BridgeRuntimeConfig& bridge_config,
|
const BridgeRuntimeConfig& bridge_config,
|
||||||
const std::optional<GatewayModbusConfig>& modbus_config,
|
const std::optional<GatewayModbusConfig>& modbus_config,
|
||||||
|
const std::optional<GatewayKnxConfig>& knx_config,
|
||||||
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
|
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
|
||||||
DaliValue::Object out = bridge_config.toJson();
|
DaliValue::Object out = bridge_config.toJson();
|
||||||
if (modbus_config.has_value()) {
|
if (modbus_config.has_value()) {
|
||||||
out["modbus"] = GatewayModbusConfigToValue(modbus_config.value());
|
out["modbus"] = GatewayModbusConfigToValue(modbus_config.value());
|
||||||
}
|
}
|
||||||
|
if (knx_config.has_value()) {
|
||||||
|
out["knx"] = GatewayKnxConfigToValue(knx_config.value());
|
||||||
|
}
|
||||||
if (bacnet_server_config.has_value()) {
|
if (bacnet_server_config.has_value()) {
|
||||||
DaliValue::Object bacnet;
|
DaliValue::Object bacnet;
|
||||||
bacnet["deviceInstance"] = static_cast<int64_t>(bacnet_server_config->deviceInstance);
|
bacnet["deviceInstance"] = static_cast<int64_t>(bacnet_server_config->deviceInstance);
|
||||||
@@ -817,9 +823,10 @@ DaliValue::Object GatewayBridgeStoredConfigToValue(
|
|||||||
std::string GatewayBridgeStoredConfigToJson(
|
std::string GatewayBridgeStoredConfigToJson(
|
||||||
const BridgeRuntimeConfig& bridge_config,
|
const BridgeRuntimeConfig& bridge_config,
|
||||||
const std::optional<GatewayModbusConfig>& modbus_config,
|
const std::optional<GatewayModbusConfig>& modbus_config,
|
||||||
|
const std::optional<GatewayKnxConfig>& knx_config,
|
||||||
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
|
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
|
||||||
cJSON* root = ToCjson(DaliValue(GatewayBridgeStoredConfigToValue(
|
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);
|
const std::string body = PrintJson(root);
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return body;
|
return body;
|
||||||
@@ -843,6 +850,7 @@ GatewayBridgeStoredConfig GatewayBridgeStoredConfigFromValue(const DaliValue::Ob
|
|||||||
GatewayBridgeStoredConfig config;
|
GatewayBridgeStoredConfig config;
|
||||||
config.bridge = BridgeRuntimeConfig::fromJson(object);
|
config.bridge = BridgeRuntimeConfig::fromJson(object);
|
||||||
config.modbus = GatewayModbusConfigFromValue(getObjectValue(object, "modbus"));
|
config.modbus = GatewayModbusConfigFromValue(getObjectValue(object, "modbus"));
|
||||||
|
config.knx = GatewayKnxConfigFromValue(getObjectValue(object, "knx"));
|
||||||
config.bacnet_server = GatewayBacnetBridgeConfigFromValue(
|
config.bacnet_server = GatewayBacnetBridgeConfigFromValue(
|
||||||
getObjectValue(object, "bacnetServer"));
|
getObjectValue(object, "bacnetServer"));
|
||||||
return config;
|
return config;
|
||||||
@@ -1152,12 +1160,15 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
std::unique_ptr<DaliComm> comm;
|
std::unique_ptr<DaliComm> comm;
|
||||||
std::unique_ptr<DaliBridgeEngine> engine;
|
std::unique_ptr<DaliBridgeEngine> engine;
|
||||||
std::unique_ptr<GatewayModbusBridge> modbus;
|
std::unique_ptr<GatewayModbusBridge> modbus;
|
||||||
|
std::unique_ptr<GatewayKnxBridge> knx;
|
||||||
|
std::unique_ptr<GatewayKnxTpIpRouter> knx_router;
|
||||||
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||||
std::unique_ptr<GatewayBacnetBridgeAdapter> bacnet;
|
std::unique_ptr<GatewayBacnetBridgeAdapter> bacnet;
|
||||||
#endif
|
#endif
|
||||||
std::unique_ptr<DaliCloudBridge> cloud;
|
std::unique_ptr<DaliCloudBridge> cloud;
|
||||||
BridgeRuntimeConfig bridge_config;
|
BridgeRuntimeConfig bridge_config;
|
||||||
std::optional<GatewayModbusConfig> modbus_config;
|
std::optional<GatewayModbusConfig> modbus_config;
|
||||||
|
std::optional<GatewayKnxConfig> knx_config;
|
||||||
std::optional<GatewayBacnetBridgeConfig> bacnet_server_config;
|
std::optional<GatewayBacnetBridgeConfig> bacnet_server_config;
|
||||||
BridgeDiscoveryInventory discovery_inventory;
|
BridgeDiscoveryInventory discovery_inventory;
|
||||||
std::optional<GatewayCloudConfig> cloud_config;
|
std::optional<GatewayCloudConfig> cloud_config;
|
||||||
@@ -1166,6 +1177,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
bool cloud_config_loaded{false};
|
bool cloud_config_loaded{false};
|
||||||
bool cloud_started{false};
|
bool cloud_started{false};
|
||||||
bool modbus_started{false};
|
bool modbus_started{false};
|
||||||
|
bool knx_started{false};
|
||||||
bool bacnet_started{false};
|
bool bacnet_started{false};
|
||||||
TaskHandle_t modbus_task_handle{nullptr};
|
TaskHandle_t modbus_task_handle{nullptr};
|
||||||
std::atomic_bool modbus_stop_requested{false};
|
std::atomic_bool modbus_stop_requested{false};
|
||||||
@@ -1210,6 +1222,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
const auto stored_config = GatewayBridgeStoredConfigFromValue(bridge_object);
|
const auto stored_config = GatewayBridgeStoredConfigFromValue(bridge_object);
|
||||||
bridge_config = stored_config.bridge;
|
bridge_config = stored_config.bridge;
|
||||||
modbus_config = stored_config.modbus;
|
modbus_config = stored_config.modbus;
|
||||||
|
knx_config = stored_config.knx;
|
||||||
bacnet_server_config = stored_config.bacnet_server;
|
bacnet_server_config = stored_config.bacnet_server;
|
||||||
bridge_config_loaded = true;
|
bridge_config_loaded = true;
|
||||||
}
|
}
|
||||||
@@ -1245,6 +1258,22 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
modbus->setConfig(modbus_config.value());
|
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 defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||||
if (service_config.bacnet_enabled) {
|
if (service_config.bacnet_enabled) {
|
||||||
bacnet = std::make_unique<GatewayBacnetBridgeAdapter>(*engine);
|
bacnet = std::make_unique<GatewayBacnetBridgeAdapter>(*engine);
|
||||||
@@ -1257,6 +1286,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
applyCloudModelsLocked();
|
applyCloudModelsLocked();
|
||||||
|
knx_started = false;
|
||||||
bacnet_started = false;
|
bacnet_started = false;
|
||||||
diagnostic_snapshot_cache.clear();
|
diagnostic_snapshot_cache.clear();
|
||||||
}
|
}
|
||||||
@@ -1433,14 +1463,15 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
BridgeProvisioningStore store(bridgeNamespace());
|
BridgeProvisioningStore store(bridgeNamespace());
|
||||||
const esp_err_t err = store.saveObject(
|
const esp_err_t err = store.saveObject(
|
||||||
kBridgeConfigKey,
|
kBridgeConfigKey,
|
||||||
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus,
|
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus, parsed->knx,
|
||||||
parsed->bacnet_server));
|
parsed->bacnet_server));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
LockGuard guard(lock);
|
LockGuard guard(lock);
|
||||||
bridge_config = parsed->bridge;
|
bridge_config = parsed->bridge;
|
||||||
modbus_config = parsed->modbus;
|
modbus_config = parsed->modbus;
|
||||||
|
knx_config = parsed->knx;
|
||||||
bacnet_server_config = parsed->bacnet_server;
|
bacnet_server_config = parsed->bacnet_server;
|
||||||
bridge_config_loaded = true;
|
bridge_config_loaded = true;
|
||||||
applyBridgeConfigLocked();
|
applyBridgeConfigLocked();
|
||||||
@@ -1456,6 +1487,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
LockGuard guard(lock);
|
LockGuard guard(lock);
|
||||||
bridge_config = BridgeRuntimeConfig{};
|
bridge_config = BridgeRuntimeConfig{};
|
||||||
modbus_config.reset();
|
modbus_config.reset();
|
||||||
|
knx_config.reset();
|
||||||
bacnet_server_config.reset();
|
bacnet_server_config.reset();
|
||||||
bridge_config_loaded = false;
|
bridge_config_loaded = false;
|
||||||
applyBridgeConfigLocked();
|
applyBridgeConfigLocked();
|
||||||
@@ -1800,6 +1832,41 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
}
|
}
|
||||||
#endif
|
#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) {
|
GatewayBridgeHttpResponse execute(std::string_view json) {
|
||||||
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
|
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
|
||||||
if (root == nullptr) {
|
if (root == nullptr) {
|
||||||
@@ -1887,6 +1954,42 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
cJSON_AddItemToObject(root, "bacnet", bacnet_json);
|
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();
|
cJSON* cloud_json = cJSON_CreateObject();
|
||||||
if (cloud_json != nullptr) {
|
if (cloud_json != nullptr) {
|
||||||
cJSON_AddBoolToObject(cloud_json, "enabled", service_config.cloud_enabled);
|
cJSON_AddBoolToObject(cloud_json, "enabled", service_config.cloud_enabled);
|
||||||
@@ -1906,6 +2009,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
GatewayBridgeHttpResponse configJson() const {
|
GatewayBridgeHttpResponse configJson() const {
|
||||||
return GatewayBridgeHttpResponse{ESP_OK,
|
return GatewayBridgeHttpResponse{ESP_OK,
|
||||||
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config,
|
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config,
|
||||||
|
knx_config,
|
||||||
bacnet_server_config)};
|
bacnet_server_config)};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2104,6 +2208,37 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
return JsonOk(root);
|
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() {
|
GatewayBridgeHttpResponse bacnetBindingsJson() {
|
||||||
cJSON* root = cJSON_CreateObject();
|
cJSON* root = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
||||||
@@ -2597,7 +2732,8 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
BridgeProvisioningStore store(bridgeNamespace());
|
BridgeProvisioningStore store(bridgeNamespace());
|
||||||
const esp_err_t err = store.saveObject(
|
const esp_err_t err = store.saveObject(
|
||||||
kBridgeConfigKey,
|
kBridgeConfigKey,
|
||||||
GatewayBridgeStoredConfigToValue(bridge_config, config, bacnet_server_config));
|
GatewayBridgeStoredConfigToValue(bridge_config, config, knx_config,
|
||||||
|
bacnet_server_config));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -2609,6 +2745,34 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
return ESP_OK;
|
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,
|
std::vector<uint8_t> processModbusPdu(const GatewayModbusConfig& config,
|
||||||
uint8_t unit_id,
|
uint8_t unit_id,
|
||||||
const std::vector<uint8_t>& pdu) {
|
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) {
|
if (config_.bacnet_enabled && config_.bacnet_startup_enabled) {
|
||||||
for (const auto& runtime : runtimes_) {
|
for (const auto& runtime : runtimes_) {
|
||||||
const esp_err_t err = runtime->startBacnet();
|
const esp_err_t err = runtime->startBacnet();
|
||||||
@@ -3294,6 +3468,9 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|||||||
if (action == "modbus") {
|
if (action == "modbus") {
|
||||||
return runtime->modbusBindingsJson();
|
return runtime->modbusBindingsJson();
|
||||||
}
|
}
|
||||||
|
if (action == "knx") {
|
||||||
|
return runtime->knxBindingsJson();
|
||||||
|
}
|
||||||
if (action == "bacnet") {
|
if (action == "bacnet") {
|
||||||
return runtime->bacnetBindingsJson();
|
return runtime->bacnetBindingsJson();
|
||||||
}
|
}
|
||||||
@@ -3502,6 +3679,41 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
|||||||
}
|
}
|
||||||
return handleGet("modbus", gateway_id.value());
|
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") {
|
if (action == "bacnet_start") {
|
||||||
const esp_err_t err = runtime->startBacnet();
|
const esp_err_t err = runtime->startBacnet();
|
||||||
if (err != ESP_OK) {
|
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