Compare commits

...

2 Commits

Author SHA1 Message Date
Tony de0edd5ad9 feat(gateway): enhance KNX support with DALI integration and configuration updates
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-12 20:34:33 +08:00
Tony e58115d303 feat(gateway): add KNX Data Secure support and related configurations
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-12 12:48:18 +08:00
17 changed files with 734 additions and 31 deletions
+35
View File
@@ -621,6 +621,41 @@ config GATEWAY_START_KNX_BRIDGE_ENABLED
Starts the KNXnet/IP tunneling/multicast listener at boot. Disabled by Starts the KNXnet/IP tunneling/multicast listener at boot. Disabled by
default so UDP port 3671 is opened only after provisioning or explicit start. default so UDP port 3671 is opened only after provisioning or explicit start.
config GATEWAY_KNX_DATA_SECURE_SUPPORTED
bool "Enable KNX Data Secure support"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default n
help
Compiles the OpenKNX SecurityInterfaceObject and SecureApplicationLayer
into the ETS runtime. This is the application-layer security path used
for secure KNX group-object and ETS tool traffic.
config GATEWAY_KNX_IP_SECURE_SUPPORTED
bool "Enable KNXnet/IP Secure support"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default n
help
Builds gateway support for KNXnet/IP Secure tunneling and routing. The
secure session transport is implemented by the gateway-owned KNX/IP
router and is separate from KNX Data Secure APDU handling.
config GATEWAY_KNX_SECURITY_DEV_ENDPOINTS
bool "Enable KNX security development HTTP endpoints"
depends on GATEWAY_KNX_DATA_SECURE_SUPPORTED || GATEWAY_KNX_IP_SECURE_SUPPORTED
default n
help
Exposes development-only HTTP actions for reading, writing, generating,
and resetting KNX security material. Disable this for production builds.
config GATEWAY_KNX_SECURITY_PLAIN_NVS
bool "Store KNX security material in plain NVS"
depends on GATEWAY_KNX_DATA_SECURE_SUPPORTED || GATEWAY_KNX_IP_SECURE_SUPPORTED
default y
help
Stores development KNX security material in normal NVS. This is useful
during bring-up, but production builds should replace it with encrypted
NVS, flash encryption, and secure boot before exposing real keys.
config GATEWAY_KNX_MAIN_GROUP config GATEWAY_KNX_MAIN_GROUP
int "KNX DALI main group" int "KNX DALI main group"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED depends on GATEWAY_KNX_BRIDGE_SUPPORTED
+7 -3
View File
@@ -688,6 +688,10 @@ 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=y CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y
CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
# CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set
# CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set
CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y
CONFIG_GATEWAY_KNX_MAIN_GROUP=0 CONFIG_GATEWAY_KNX_MAIN_GROUP=0
CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y
CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y
@@ -1071,12 +1075,12 @@ CONFIG_BT_CTRL_RX_ANTENNA_INDEX_EFF=0
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N0 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N0 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P3 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P3 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P6 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P6 is not set
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9=y # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P12 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P12 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P15 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P15 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P18 is not set # CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P18 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P20 is not set CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P20=y
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF=11 CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF=15
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100 CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100
CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20
+1
View File
@@ -16,3 +16,4 @@ CONFIG_ETH_SPI_ETHERNET_W5500=y
CONFIG_GATEWAY_ETHERNET_SUPPORTED=y CONFIG_GATEWAY_ETHERNET_SUPPORTED=y
CONFIG_GATEWAY_START_ETHERNET_ENABLED=y CONFIG_GATEWAY_START_ETHERNET_ENABLED=y
CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y
CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
+39 -11
View File
@@ -656,6 +656,26 @@ CONFIG_GATEWAY_SMARTCONFIG_SUPPORTED=y
# CONFIG_GATEWAY_START_ESPNOW_SETUP_ENABLED is not set # CONFIG_GATEWAY_START_ESPNOW_SETUP_ENABLED is not set
# CONFIG_GATEWAY_START_SMARTCONFIG_ENABLED is not set # CONFIG_GATEWAY_START_SMARTCONFIG_ENABLED is not set
CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC=60 CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC=60
CONFIG_GATEWAY_ETHERNET_SUPPORTED=y
CONFIG_GATEWAY_START_ETHERNET_ENABLED=y
CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y
#
# Gateway Wired Ethernet
#
CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST=1
CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO=14
CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO=13
CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=12
CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=15
CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=4
CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS=0
CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=36
CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=5
CONFIG_GATEWAY_ETHERNET_PHY_ADDR=1
CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=3072
# end of Gateway Wired Ethernet
CONFIG_GATEWAY_BRIDGE_SUPPORTED=y CONFIG_GATEWAY_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_MODBUS_BRIDGE_SUPPORTED=y CONFIG_GATEWAY_MODBUS_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_MODBUS_BRIDGE_ENABLED is not set # CONFIG_GATEWAY_START_MODBUS_BRIDGE_ENABLED is not set
@@ -666,7 +686,24 @@ 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_KNX_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y
CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
# CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set
# CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set
CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y
CONFIG_GATEWAY_KNX_MAIN_GROUP=0
CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y
CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y
CONFIG_GATEWAY_KNX_UDP_PORT=3671
CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12"
CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=4353
CONFIG_GATEWAY_KNX_TP_UART_PORT=0
CONFIG_GATEWAY_KNX_TP_TX_PIN=-1
CONFIG_GATEWAY_KNX_TP_RX_PIN=-1
CONFIG_GATEWAY_KNX_TP_BAUDRATE=19200
CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=8192
CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5
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
@@ -675,16 +712,7 @@ CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE=8192
CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY=5 CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY=5
CONFIG_GATEWAY_USB_STARTUP_DEBUG_JTAG=y CONFIG_GATEWAY_USB_STARTUP_DEBUG_JTAG=y
# CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set # CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set
CONFIG_GATEWAY_485_CONTROL_ENABLED=y # CONFIG_GATEWAY_485_CONTROL_ENABLED is not set
CONFIG_GATEWAY_485_CONTROL_BAUDRATE=9600
CONFIG_GATEWAY_485_CONTROL_TX_PIN=-1
CONFIG_GATEWAY_485_CONTROL_RX_PIN=-1
CONFIG_GATEWAY_485_CONTROL_RX_BUFFER=256
CONFIG_GATEWAY_485_CONTROL_TX_BUFFER=256
CONFIG_GATEWAY_485_CONTROL_READ_TIMEOUT_MS=20
CONFIG_GATEWAY_485_CONTROL_WRITE_TIMEOUT_MS=20
CONFIG_GATEWAY_485_CONTROL_TASK_STACK_SIZE=4096
CONFIG_GATEWAY_485_CONTROL_TASK_PRIORITY=4
# end of Gateway Startup Services # end of Gateway Startup Services
# #
@@ -16,6 +16,7 @@
namespace gateway { namespace gateway {
class DaliDomainService; class DaliDomainService;
struct DaliRawFrame;
class GatewayCache; class GatewayCache;
struct GatewayBridgeServiceConfig { struct GatewayBridgeServiceConfig {
@@ -65,6 +66,7 @@ class GatewayBridgeService {
ChannelRuntime* findRuntime(uint8_t gateway_id); ChannelRuntime* findRuntime(uint8_t gateway_id);
const ChannelRuntime* findRuntime(uint8_t gateway_id) const; const ChannelRuntime* findRuntime(uint8_t gateway_id) const;
void handleDaliRawFrame(const DaliRawFrame& frame);
void collectUsedRuntimeResources(uint8_t except_gateway_id, void collectUsedRuntimeResources(uint8_t except_gateway_id,
std::set<uint16_t>* modbus_tcp_ports, std::set<uint16_t>* modbus_tcp_ports,
std::set<uint16_t>* knx_udp_ports, std::set<uint16_t>* knx_udp_ports,
@@ -16,6 +16,7 @@
#include "gateway_modbus.hpp" #include "gateway_modbus.hpp"
#include "gateway_provisioning.hpp" #include "gateway_provisioning.hpp"
#include "openknx_idf/ets_memory_loader.h" #include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/security_storage.h"
#include "cJSON.h" #include "cJSON.h"
#include "driver/uart.h" #include "driver/uart.h"
@@ -56,6 +57,10 @@ constexpr uint32_t kBacnetMaxObjectInstance = 4194303;
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0; constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12; constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
constexpr const char* kModbusManagementPrefix = "@DALIGW"; constexpr const char* kModbusManagementPrefix = "@DALIGW";
constexpr uint8_t kDaliGroupRawMin = 0x80;
constexpr uint8_t kDaliGroupRawMax = 0x9F;
constexpr uint8_t kDaliCmdOff = 0x00;
constexpr uint8_t kDaliCmdRecallMax = 0x05;
struct GatewayBridgeStoredConfig { struct GatewayBridgeStoredConfig {
BridgeRuntimeConfig bridge; BridgeRuntimeConfig bridge;
@@ -70,6 +75,11 @@ struct BridgeDiscoveryEntry {
DaliDomainSnapshot discovery; DaliDomainSnapshot discovery;
}; };
struct DaliKnxStatusUpdate {
GatewayKnxDaliTarget target;
uint8_t actual_level{0};
};
using BridgeDiscoveryInventory = std::map<int, BridgeDiscoveryEntry>; using BridgeDiscoveryInventory = std::map<int, BridgeDiscoveryEntry>;
class LockGuard { class LockGuard {
@@ -121,6 +131,25 @@ GatewayBridgeHttpResponse ErrorResponse(esp_err_t err, const char* message) {
return GatewayBridgeHttpResponse{err, body}; return GatewayBridgeHttpResponse{err, body};
} }
cJSON* FactoryFdskInfoToCjson(const openknx::FactoryFdskInfo& fdsk_info,
bool include_secret_strings) {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON_AddBoolToObject(root, "available", fdsk_info.available);
if (fdsk_info.available) {
cJSON_AddStringToObject(root, "serialNumber", fdsk_info.serialNumber.c_str());
cJSON_AddNumberToObject(root, "labelLength", static_cast<double>(fdsk_info.label.size()));
cJSON_AddNumberToObject(root, "qrCodeLength", static_cast<double>(fdsk_info.qrCode.size()));
if (include_secret_strings) {
cJSON_AddStringToObject(root, "label", fdsk_info.label.c_str());
cJSON_AddStringToObject(root, "qrCode", fdsk_info.qrCode.c_str());
}
}
return root;
}
const char* JsonString(const cJSON* parent, const char* name) { const char* JsonString(const cJSON* parent, const char* name) {
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name); const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
return cJSON_IsString(item) && item->valuestring != nullptr ? item->valuestring : nullptr; return cJSON_IsString(item) && item->valuestring != nullptr ? item->valuestring : nullptr;
@@ -200,6 +229,51 @@ bool ValidDaliAddress(int address) {
return address >= 0 && address <= 127; return address >= 0 && address <= 127;
} }
std::optional<GatewayKnxDaliTarget> DecodeKnxDaliTarget(uint8_t raw_addr) {
if (raw_addr <= 0x7F) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
static_cast<int>(raw_addr >> 1)};
}
if (raw_addr >= kDaliGroupRawMin && raw_addr <= kDaliGroupRawMax) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
static_cast<int>((raw_addr - kDaliGroupRawMin) >> 1)};
}
return std::nullopt;
}
std::optional<DaliKnxStatusUpdate> DecodeDaliKnxStatusUpdate(const DaliRawFrame& frame) {
if (frame.data.size() != 2 && frame.data.size() != 3) {
return std::nullopt;
}
uint8_t raw_addr = 0;
uint8_t command = 0;
if (frame.data.size() == 2) {
raw_addr = frame.data[0];
command = frame.data[1];
if (raw_addr == 0xBE) {
return std::nullopt;
}
} else {
raw_addr = frame.data[1];
command = frame.data[2];
}
auto target = DecodeKnxDaliTarget(raw_addr);
if (!target.has_value()) {
return std::nullopt;
}
if ((raw_addr & 0x01U) == 0) {
if (command > 254) {
return std::nullopt;
}
return DaliKnxStatusUpdate{*target, command};
}
if (command == kDaliCmdOff || command == kDaliCmdRecallMax) {
return DaliKnxStatusUpdate{*target,
static_cast<uint8_t>(command == kDaliCmdOff ? 0 : 254)};
}
return std::nullopt;
}
bool ValidShortAddress(int address) { bool ValidShortAddress(int address) {
return address >= 0 && address <= kMaxDaliShortAddress; return address >= 0 && address <= kMaxDaliShortAddress;
} }
@@ -2041,6 +2115,40 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddStringToObject(knx_json, "lastError", cJSON_AddStringToObject(knx_json, "lastError",
knx_last_error.empty() ? router_error.c_str() knx_last_error.empty() ? router_error.c_str()
: knx_last_error.c_str()); : knx_last_error.c_str());
cJSON* security_json = cJSON_CreateObject();
if (security_json != nullptr) {
#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
cJSON_AddBoolToObject(security_json, "dataSecureCompiled", true);
#else
cJSON_AddBoolToObject(security_json, "dataSecureCompiled", false);
#endif
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", true);
#else
cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", false);
#endif
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false);
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true);
#else
cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", false);
#endif
#if defined(CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS)
cJSON_AddBoolToObject(security_json, "plainNvsStorage", true);
cJSON_AddStringToObject(security_json, "storage", "plain_nvs_development");
#else
cJSON_AddBoolToObject(security_json, "plainNvsStorage", false);
cJSON_AddStringToObject(security_json, "storage", "none");
#endif
#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
const auto fdsk_info = openknx::LoadFactoryFdskInfo();
cJSON* fdsk_json = FactoryFdskInfoToCjson(fdsk_info, false);
if (fdsk_json != nullptr) {
cJSON_AddItemToObject(security_json, "factorySetupKey", fdsk_json);
}
#endif
cJSON_AddItemToObject(knx_json, "security", security_json);
}
if (effective_knx.has_value()) { if (effective_knx.has_value()) {
cJSON_AddBoolToObject(knx_json, "daliRouterEnabled", effective_knx->dali_router_enabled); cJSON_AddBoolToObject(knx_json, "daliRouterEnabled", effective_knx->dali_router_enabled);
cJSON_AddBoolToObject(knx_json, "ipRouterEnabled", effective_knx->ip_router_enabled); cJSON_AddBoolToObject(knx_json, "ipRouterEnabled", effective_knx->ip_router_enabled);
@@ -3598,6 +3706,9 @@ esp_err_t GatewayBridgeService::start() {
runtimes_.push_back(std::move(runtime)); runtimes_.push_back(std::move(runtime));
} }
dali_domain_.addRawFrameSink(
[this](const DaliRawFrame& frame) { handleDaliRawFrame(frame); });
std::set<int> used_serial_uarts; std::set<int> used_serial_uarts;
if (config_.modbus_enabled && config_.modbus_startup_enabled) { if (config_.modbus_enabled && config_.modbus_startup_enabled) {
std::set<uint16_t> used_modbus_ports; std::set<uint16_t> used_modbus_ports;
@@ -3654,6 +3765,22 @@ const GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(
return nullptr; return nullptr;
} }
void GatewayBridgeService::handleDaliRawFrame(const DaliRawFrame& frame) {
const auto update = DecodeDaliKnxStatusUpdate(frame);
if (!update.has_value()) {
return;
}
auto* runtime = findRuntime(frame.gateway_id);
if (runtime == nullptr) {
return;
}
LockGuard guard(runtime->lock);
if (!runtime->knx_started || runtime->knx_router == nullptr) {
return;
}
runtime->knx_router->publishDaliStatus(update->target, update->actual_level);
}
void GatewayBridgeService::collectUsedRuntimeResources( void GatewayBridgeService::collectUsedRuntimeResources(
uint8_t except_gateway_id, uint8_t except_gateway_id,
std::set<uint16_t>* modbus_tcp_ports, std::set<uint16_t>* modbus_tcp_ports,
@@ -4008,6 +4135,36 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
} }
return handleGet("knx", gateway_id.value()); return handleGet("knx", gateway_id.value());
} }
if (action == "knx_security_read_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "read-factory-setup-key";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory setup key read confirmation is required");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate security response");
}
cJSON* fdsk_json = FactoryFdskInfoToCjson(openknx::LoadFactoryFdskInfo(), true);
if (fdsk_json == nullptr) {
cJSON_Delete(response);
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate factory setup key response");
}
cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json);
return JsonOk(response);
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNX security development endpoints are disabled");
#endif
}
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) {
@@ -5,6 +5,7 @@
#include "esp_err.h" #include "esp_err.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "lwip/sockets.h" #include "lwip/sockets.h"
@@ -197,6 +198,7 @@ class GatewayKnxTpIpRouter {
esp_err_t stop(); esp_err_t stop();
bool started() const; bool started() const;
const std::string& lastError() const; const std::string& lastError() const;
bool publishDaliStatus(const GatewayKnxDaliTarget& target, uint8_t actual_level);
private: private:
static void TaskEntry(void* arg); static void TaskEntry(void* arg);
@@ -225,6 +227,8 @@ class GatewayKnxTpIpRouter {
const ::sockaddr_in& remote); const ::sockaddr_in& remote);
void sendRoutingIndication(const uint8_t* data, size_t len); void sendRoutingIndication(const uint8_t* data, size_t len);
bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len); bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len);
bool handleOpenKnxBusFrame(const uint8_t* data, size_t len);
bool emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len);
void syncOpenKnxConfigFromDevice(); void syncOpenKnxConfigFromDevice();
uint16_t effectiveIndividualAddress() const; uint16_t effectiveIndividualAddress() const;
uint16_t effectiveTunnelAddress() const; uint16_t effectiveTunnelAddress() const;
@@ -239,6 +243,7 @@ class GatewayKnxTpIpRouter {
GatewayKnxConfig config_; GatewayKnxConfig config_;
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_; std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
TaskHandle_t task_handle_{nullptr}; TaskHandle_t task_handle_{nullptr};
SemaphoreHandle_t openknx_lock_{nullptr};
std::atomic_bool stop_requested_{false}; std::atomic_bool stop_requested_{false};
std::atomic_bool started_{false}; std::atomic_bool started_{false};
int udp_sock_{-1}; int udp_sock_{-1};
+151 -13
View File
@@ -64,6 +64,8 @@ constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2;
constexpr uint8_t kGwReg1KoSwitch = 0; constexpr uint8_t kGwReg1KoSwitch = 0;
constexpr uint8_t kGwReg1KoDimmAbsolute = 3; constexpr uint8_t kGwReg1KoDimmAbsolute = 3;
constexpr uint8_t kGwReg1KoColor = 6; constexpr uint8_t kGwReg1KoColor = 6;
constexpr uint8_t kGwReg1KoSwitchState = 1;
constexpr uint8_t kGwReg1KoDimmState = 4;
constexpr uint8_t kReg1DaliFunctionObjectIndex = 160; constexpr uint8_t kReg1DaliFunctionObjectIndex = 160;
constexpr uint8_t kReg1DaliFunctionPropertyId = 1; constexpr uint8_t kReg1DaliFunctionPropertyId = 1;
constexpr uint8_t kReg1FunctionType = 2; constexpr uint8_t kReg1FunctionType = 2;
@@ -84,6 +86,31 @@ struct DecodedGroupWrite {
std::vector<uint8_t> data; std::vector<uint8_t> data;
}; };
class SemaphoreGuard {
public:
explicit SemaphoreGuard(SemaphoreHandle_t semaphore) : semaphore_(semaphore) {
if (semaphore_ != nullptr) {
xSemaphoreTake(semaphore_, portMAX_DELAY);
locked_ = true;
}
}
~SemaphoreGuard() {
if (locked_) {
xSemaphoreGive(semaphore_);
}
}
private:
SemaphoreHandle_t semaphore_{nullptr};
bool locked_{false};
};
uint8_t DaliArcLevelToDpt5(uint8_t actual_level) {
return static_cast<uint8_t>(
std::clamp<int>(static_cast<int>(std::lround(actual_level * 255.0 / 254.0)), 0, 255));
}
uint16_t ReadBe16(const uint8_t* data) { uint16_t ReadBe16(const uint8_t* data) {
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]); return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
} }
@@ -309,6 +336,22 @@ std::optional<DecodedGroupWrite> DecodeCemiGroupWrite(const uint8_t* data, size_
return out; return out;
} }
bool IsCemiGroupFrame(const uint8_t* data, size_t len) {
if (data == nullptr || len < 10) {
return false;
}
const uint8_t message_code = data[0];
if (message_code != kCemiLDataReq && message_code != kCemiLDataInd &&
message_code != kCemiLDataCon) {
return false;
}
const size_t base = 2U + data[1];
if (len < base + 8U) {
return false;
}
return (data[base + 1] & 0x80) != 0;
}
uint8_t Reg1PercentToArc(uint8_t value) { uint8_t Reg1PercentToArc(uint8_t value) {
if (value == 0 || value == 0xff) { if (value == 0 || value == 0xff) {
return value; return value;
@@ -1528,9 +1571,17 @@ GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHa
std::string openknx_namespace) std::string openknx_namespace)
: bridge_(bridge), : bridge_(bridge),
handler_(std::move(handler)), handler_(std::move(handler)),
openknx_namespace_(std::move(openknx_namespace)) {} openknx_namespace_(std::move(openknx_namespace)) {
openknx_lock_ = xSemaphoreCreateMutex();
}
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); } GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() {
stop();
if (openknx_lock_ != nullptr) {
vSemaphoreDelete(openknx_lock_);
openknx_lock_ = nullptr;
}
}
void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; } void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; }
@@ -1559,6 +1610,13 @@ esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task
std::vector<uint8_t>* response) { std::vector<uint8_t>* response) {
return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response); return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response);
}); });
ets_device_->setGroupWriteHandler(
[this](uint16_t group_address, const uint8_t* data, size_t len) {
const DaliBridgeResult result = bridge_.handleGroupWrite(group_address, data, len);
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str());
}
});
if (!configureTpUart()) { if (!configureTpUart()) {
ets_device_.reset(); ets_device_.reset();
closeSockets(); closeSockets();
@@ -1590,6 +1648,40 @@ bool GatewayKnxTpIpRouter::started() const { return started_; }
const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; } const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; }
bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target,
uint8_t actual_level) {
if (!started_ || !config_.ip_router_enabled) {
return false;
}
uint16_t switch_object = 0;
uint16_t dimm_object = 0;
if (target.kind == GatewayKnxDaliTargetKind::kShortAddress) {
if (target.address < 0 || target.address > 63) {
return false;
}
const uint16_t base = kGwReg1AdrKoOffset +
kGwReg1AdrKoBlockSize * static_cast<uint16_t>(target.address);
switch_object = base + kGwReg1KoSwitchState;
dimm_object = base + kGwReg1KoDimmState;
} else if (target.kind == GatewayKnxDaliTargetKind::kGroup) {
if (target.address < 0 || target.address > 15) {
return false;
}
const uint16_t base = kGwReg1GrpKoOffset +
kGwReg1GrpKoBlockSize * static_cast<uint16_t>(target.address);
switch_object = base + kGwReg1KoSwitchState;
dimm_object = base + kGwReg1KoDimmState;
} else {
return false;
}
const uint8_t switch_value = actual_level > 0 ? 1 : 0;
const uint8_t dimm_value = DaliArcLevelToDpt5(actual_level);
bool emitted = emitOpenKnxGroupValue(switch_object, &switch_value, 1);
emitted = emitOpenKnxGroupValue(dimm_object, &dimm_value, 1) || emitted;
return emitted;
}
void GatewayKnxTpIpRouter::TaskEntry(void* arg) { void GatewayKnxTpIpRouter::TaskEntry(void* arg) {
static_cast<GatewayKnxTpIpRouter*>(arg)->taskLoop(); static_cast<GatewayKnxTpIpRouter*>(arg)->taskLoop();
} }
@@ -1603,8 +1695,11 @@ void GatewayKnxTpIpRouter::taskLoop() {
reinterpret_cast<sockaddr*>(&remote), &remote_len); reinterpret_cast<sockaddr*>(&remote), &remote_len);
if (received <= 0) { if (received <= 0) {
pollTpUart(); pollTpUart();
if (ets_device_ != nullptr) { {
ets_device_->loop(); SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->loop();
}
} }
if (!stop_requested_) { if (!stop_requested_) {
vTaskDelay(pdMS_TO_TICKS(10)); vTaskDelay(pdMS_TO_TICKS(10));
@@ -1613,8 +1708,11 @@ void GatewayKnxTpIpRouter::taskLoop() {
} }
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote); handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
pollTpUart(); pollTpUart();
if (ets_device_ != nullptr) { {
ets_device_->loop(); SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->loop();
}
} }
} }
finishTask(); finishTask();
@@ -1622,7 +1720,10 @@ void GatewayKnxTpIpRouter::taskLoop() {
void GatewayKnxTpIpRouter::finishTask() { void GatewayKnxTpIpRouter::finishTask() {
closeSockets(); closeSockets();
ets_device_.reset(); {
SemaphoreGuard guard(openknx_lock_);
ets_device_.reset();
}
started_ = false; started_ = false;
task_handle_ = nullptr; task_handle_ = nullptr;
vTaskDelete(nullptr); vTaskDelete(nullptr);
@@ -1805,9 +1906,12 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t l
if (body == nullptr || len == 0) { if (body == nullptr || len == 0) {
return; return;
} }
const DaliBridgeResult result = handler_(body, len); const bool consumed_by_openknx = handleOpenKnxBusFrame(body, len);
if (!result.ok && !result.error.empty()) { if (!consumed_by_openknx) {
ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str()); 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); forwardCemiToTp(body, len);
} }
@@ -1831,8 +1935,12 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l
sendTunnellingAck(channel_id, sequence, kKnxNoError, remote); sendTunnellingAck(channel_id, sequence, kKnxNoError, remote);
const uint8_t* cemi = body + 4; const uint8_t* cemi = body + 4;
const size_t cemi_len = len - 4; const size_t cemi_len = len - 4;
const bool group_frame = IsCemiGroupFrame(cemi, cemi_len);
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len); const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len);
if (consumed_by_openknx) { if (consumed_by_openknx) {
if (group_frame) {
forwardCemiToTp(cemi, cemi_len);
}
return; return;
} }
const DaliBridgeResult result = handler_(cemi, cemi_len); const DaliBridgeResult result = handler_(cemi, cemi_len);
@@ -1959,6 +2067,7 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len
} }
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len) { bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) { if (ets_device_ == nullptr) {
return false; return false;
} }
@@ -1970,6 +2079,32 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
return consumed; return consumed;
} }
bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool consumed = ets_device_->handleBusFrame(data, len);
syncOpenKnxConfigFromDevice();
return consumed;
}
bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number,
const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool emitted = ets_device_->emitGroupValue(
group_object_number, data, len, [this](const uint8_t* frame_data, size_t frame_len) {
sendRoutingIndication(frame_data, frame_len);
sendTunnelIndication(frame_data, frame_len);
forwardCemiToTp(frame_data, frame_len);
});
syncOpenKnxConfigFromDevice();
return emitted;
}
void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() { void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
if (ets_device_ == nullptr) { if (ets_device_ == nullptr) {
return; return;
@@ -2122,9 +2257,12 @@ void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) {
if (!cemi.has_value()) { if (!cemi.has_value()) {
return; return;
} }
const DaliBridgeResult result = handler_(cemi->data(), cemi->size()); const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi->data(), cemi->size());
if (!result.ok && !result.error.empty()) { if (!consumed_by_openknx) {
ESP_LOGD(kTag, "KNX TP frame not routed to DALI: %s", result.error.c_str()); 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()); sendTunnelIndication(cemi->data(), cemi->size());
sendRoutingIndication(cemi->data(), cemi->size()); sendRoutingIndication(cemi->data(), cemi->size());
+13
View File
@@ -13,6 +13,12 @@ file(GLOB OPENKNX_SRCS
"${OPENKNX_ROOT}/src/knx/*.cpp" "${OPENKNX_ROOT}/src/knx/*.cpp"
) )
if(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
list(APPEND OPENKNX_SRCS
"${OPENKNX_ROOT}/src/knx/aes.c"
)
endif()
set(TPUART_SRCS set(TPUART_SRCS
"${TPUART_ROOT}/src/TPUart/DataLinkLayer.cpp" "${TPUART_ROOT}/src/TPUart/DataLinkLayer.cpp"
"${TPUART_ROOT}/src/TPUart/Receiver.cpp" "${TPUART_ROOT}/src/TPUart/Receiver.cpp"
@@ -31,6 +37,7 @@ idf_component_register(
"src/esp_idf_platform.cpp" "src/esp_idf_platform.cpp"
"src/ets_device_runtime.cpp" "src/ets_device_runtime.cpp"
"src/ets_memory_loader.cpp" "src/ets_memory_loader.cpp"
"src/security_storage.cpp"
"src/tpuart_uart_interface.cpp" "src/tpuart_uart_interface.cpp"
${OPENKNX_SRCS} ${OPENKNX_SRCS}
${TPUART_SRCS} ${TPUART_SRCS}
@@ -42,11 +49,13 @@ idf_component_register(
esp_driver_gpio esp_driver_gpio
esp_driver_uart esp_driver_uart
esp_netif esp_netif
esp_system
esp_timer esp_timer
esp_wifi esp_wifi
freertos freertos
log log
lwip lwip
mbedtls
nvs_flash nvs_flash
) )
@@ -58,6 +67,10 @@ target_compile_definitions(${COMPONENT_LIB} PUBLIC
USE_CEMI_SERVER USE_CEMI_SERVER
) )
if(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
target_compile_definitions(${COMPONENT_LIB} PUBLIC USE_DATASECURE)
endif()
target_compile_options(${COMPONENT_LIB} PRIVATE target_compile_options(${COMPONENT_LIB} PRIVATE
-Wno-unused-parameter -Wno-unused-parameter
) )
@@ -14,10 +14,15 @@ namespace gateway::openknx {
class EspIdfPlatform : public Platform { class EspIdfPlatform : public Platform {
public: public:
using OutboundCemiFrameCallback = bool (*)(CemiFrame& frame, void* context);
explicit EspIdfPlatform(TPUart::Interface::Abstract* interface = nullptr, explicit EspIdfPlatform(TPUart::Interface::Abstract* interface = nullptr,
const char* nvs_namespace = "openknx"); const char* nvs_namespace = "openknx");
~EspIdfPlatform() override; ~EspIdfPlatform() override;
void outboundCemiFrameCallback(OutboundCemiFrameCallback callback, void* context);
bool handleOutboundCemiFrame(CemiFrame& frame) override;
void networkInterface(esp_netif_t* netif); void networkInterface(esp_netif_t* netif);
esp_netif_t* networkInterface() const; esp_netif_t* networkInterface() const;
@@ -54,6 +59,8 @@ class EspIdfPlatform : public Platform {
std::vector<uint8_t> eeprom_; std::vector<uint8_t> eeprom_;
std::string nvs_namespace_; std::string nvs_namespace_;
bool eeprom_loaded_{false}; bool eeprom_loaded_{false};
OutboundCemiFrameCallback outbound_cemi_frame_callback_{nullptr};
void* outbound_cemi_frame_context_{nullptr};
}; };
} // namespace gateway::openknx } // namespace gateway::openknx
@@ -17,6 +17,8 @@ namespace gateway::openknx {
class EtsDeviceRuntime { class EtsDeviceRuntime {
public: public:
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>; using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
using GroupWriteHandler = std::function<void(uint16_t group_address, const uint8_t* data,
size_t len)>;
using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id, using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len, const uint8_t* data, size_t len,
std::vector<uint8_t>* response)>; std::vector<uint8_t>* response)>;
@@ -31,12 +33,19 @@ class EtsDeviceRuntime {
void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler); FunctionPropertyHandler state_handler);
void setGroupWriteHandler(GroupWriteHandler handler);
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender); bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
bool handleBusFrame(const uint8_t* data, size_t len);
bool emitGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len,
CemiFrameSender sender);
void loop(); void loop();
private: private:
static bool HandleOutboundCemiFrame(CemiFrame& frame, void* context);
static void EmitTunnelFrame(CemiFrame& frame, void* context); static void EmitTunnelFrame(CemiFrame& frame, void* context);
static void HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
uint8_t data_length, void* context);
static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data, uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length); uint8_t* result_data, uint8_t& result_length);
@@ -48,11 +57,13 @@ class EtsDeviceRuntime {
uint8_t property_id, uint8_t length, uint8_t* data, uint8_t property_id, uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length); uint8_t* result_data, uint8_t& result_length);
bool shouldConsumeTunnelFrame(CemiFrame& frame) const; bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
bool shouldConsumeBusFrame(CemiFrame& frame) const;
std::string nvs_namespace_; std::string nvs_namespace_;
EspIdfPlatform platform_; EspIdfPlatform platform_;
Bau07B0 device_; Bau07B0 device_;
CemiFrameSender sender_; CemiFrameSender sender_;
GroupWriteHandler group_write_handler_;
FunctionPropertyHandler command_handler_; FunctionPropertyHandler command_handler_;
FunctionPropertyHandler state_handler_; FunctionPropertyHandler state_handler_;
}; };
@@ -3,6 +3,7 @@
#include "openknx_idf/ets_memory_loader.h" #include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/ets_device_runtime.h" #include "openknx_idf/ets_device_runtime.h"
#include "openknx_idf/esp_idf_platform.h" #include "openknx_idf/esp_idf_platform.h"
#include "openknx_idf/security_storage.h"
#include "openknx_idf/tpuart_uart_interface.h" #include "openknx_idf/tpuart_uart_interface.h"
#include "knx/bau07B0.h" #include "knx/bau07B0.h"
@@ -0,0 +1,19 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace gateway::openknx {
struct FactoryFdskInfo {
bool available{false};
std::string serialNumber;
std::string label;
std::string qrCode;
};
bool LoadFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadFactoryFdskInfo();
} // namespace gateway::openknx
@@ -51,6 +51,19 @@ EspIdfPlatform::EspIdfPlatform(TPUart::Interface::Abstract* interface,
EspIdfPlatform::~EspIdfPlatform() { closeMultiCast(); } EspIdfPlatform::~EspIdfPlatform() { closeMultiCast(); }
void EspIdfPlatform::outboundCemiFrameCallback(OutboundCemiFrameCallback callback,
void* context) {
outbound_cemi_frame_callback_ = callback;
outbound_cemi_frame_context_ = context;
}
bool EspIdfPlatform::handleOutboundCemiFrame(CemiFrame& frame) {
if (outbound_cemi_frame_callback_ == nullptr) {
return false;
}
return outbound_cemi_frame_callback_(frame, outbound_cemi_frame_context_);
}
void EspIdfPlatform::networkInterface(esp_netif_t* netif) { netif_ = netif; } void EspIdfPlatform::networkInterface(esp_netif_t* netif) { netif_ = netif; }
esp_netif_t* EspIdfPlatform::networkInterface() const { return netif_; } esp_netif_t* EspIdfPlatform::networkInterface() const { return netif_; }
@@ -1,6 +1,7 @@
#include "openknx_idf/ets_device_runtime.h" #include "openknx_idf/ets_device_runtime.h"
#include "knx/cemi_server.h" #include "knx/cemi_server.h"
#include "knx/secure_application_layer.h"
#include "knx/property.h" #include "knx/property.h"
#include <algorithm> #include <algorithm>
@@ -52,6 +53,7 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
: nvs_namespace_(std::move(nvs_namespace)), : nvs_namespace_(std::move(nvs_namespace)),
platform_(nullptr, nvs_namespace_.c_str()), platform_(nullptr, nvs_namespace_.c_str()),
device_(platform_) { device_(platform_) {
platform_.outboundCemiFrameCallback(&EtsDeviceRuntime::HandleOutboundCemiFrame, this);
ApplyReg1DaliIdentity(device_, platform_); ApplyReg1DaliIdentity(device_, platform_);
if (IsUsableIndividualAddress(fallback_individual_address)) { if (IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address); device_.deviceObject().individualAddress(fallback_individual_address);
@@ -67,9 +69,16 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
} }
device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand); device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand);
device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState); device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState);
#ifdef USE_DATASECURE
device_.secureGroupWriteCallback(&EtsDeviceRuntime::HandleSecureGroupWrite, this);
#endif
} }
EtsDeviceRuntime::~EtsDeviceRuntime() { EtsDeviceRuntime::~EtsDeviceRuntime() {
platform_.outboundCemiFrameCallback(nullptr, nullptr);
#ifdef USE_DATASECURE
device_.secureGroupWriteCallback(nullptr, nullptr);
#endif
device_.functionPropertyCallback(nullptr); device_.functionPropertyCallback(nullptr);
device_.functionPropertyStateCallback(nullptr); device_.functionPropertyStateCallback(nullptr);
if (auto* server = device_.getCemiServer()) { if (auto* server = device_.getCemiServer()) {
@@ -126,6 +135,10 @@ void EtsDeviceRuntime::setFunctionPropertyHandlers(FunctionPropertyHandler comma
state_handler_ = std::move(state_handler); state_handler_ = std::move(state_handler);
} }
void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) {
group_write_handler_ = std::move(handler);
}
bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len, bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
CemiFrameSender sender) { CemiFrameSender sender) {
auto* server = device_.getCemiServer(); auto* server = device_.getCemiServer();
@@ -146,8 +159,58 @@ bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
return consumed; return consumed;
} }
bool EtsDeviceRuntime::handleBusFrame(const uint8_t* data, size_t len) {
auto* data_link_layer = device_.getDataLinkLayer();
if (data_link_layer == nullptr || data == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
const bool consumed = shouldConsumeBusFrame(frame);
if (!consumed) {
return false;
}
data_link_layer->externalFrameReceived(frame);
loop();
return consumed;
}
bool EtsDeviceRuntime::emitGroupValue(uint16_t group_object_number, const uint8_t* data,
size_t len, CemiFrameSender sender) {
if (group_object_number == 0 || data == nullptr || !sender || !device_.configured()) {
return false;
}
auto& table = device_.groupObjectTable();
if (group_object_number > table.entryCount()) {
return false;
}
auto& group_object = table.get(group_object_number);
if (len != group_object.valueSize() || group_object.valueRef() == nullptr) {
return false;
}
if (group_object.sizeInTelegram() == 0) {
group_object.valueRef()[0] = data[0] & 0x01;
} else {
std::copy_n(data, len, group_object.valueRef());
}
sender_ = std::move(sender);
group_object.objectWritten();
loop();
sender_ = nullptr;
return true;
}
void EtsDeviceRuntime::loop() { device_.loop(); } void EtsDeviceRuntime::loop() { device_.loop(); }
bool EtsDeviceRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->sender_) {
return false;
}
self->sender_(frame.data(), frame.dataLength());
return true;
}
void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) { void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context); auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->sender_) { if (self == nullptr || !self->sender_) {
@@ -156,6 +219,15 @@ void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
self->sender_(frame.data(), frame.dataLength()); self->sender_(frame.data(), frame.dataLength());
} }
void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
uint8_t data_length, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->group_write_handler_) {
return;
}
self->group_write_handler_(group_address, data, data_length);
}
bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data, uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t* result_data,
@@ -217,11 +289,27 @@ bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
case M_FuncPropStateRead_req: case M_FuncPropStateRead_req:
return true; return true;
case L_data_req: case L_data_req:
return frame.addressType() == IndividualAddress && if (frame.addressType() == IndividualAddress &&
frame.destinationAddress() == individualAddress(); frame.destinationAddress() == individualAddress()) {
return true;
}
#ifdef USE_DATASECURE
return frame.addressType() == GroupAddress && frame.apdu().type() == SecureService;
#else
return false;
#endif
default: default:
return false; return false;
} }
} }
bool EtsDeviceRuntime::shouldConsumeBusFrame(CemiFrame& frame) const {
#ifdef USE_DATASECURE
return frame.messageCode() == L_data_ind && frame.addressType() == GroupAddress &&
frame.apdu().type() == SecureService;
#else
return false;
#endif
}
} // namespace gateway::openknx } // namespace gateway::openknx
@@ -0,0 +1,181 @@
#include "openknx_idf/security_storage.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_random.h"
#include "nvs.h"
#include "nvs_flash.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
namespace {
constexpr const char* kTag = "openknx_sec";
constexpr const char* kNamespace = "knx_sec";
constexpr const char* kFactoryFdskKey = "factory_fdsk";
constexpr size_t kFdskSize = 16;
constexpr size_t kSerialSize = 6;
constexpr size_t kFdskQrSize = 36;
constexpr uint8_t kCrc4Tab[16] = {
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
};
constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
bool ensureNvsReady() {
const esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
if (nvs_flash_erase() != ESP_OK) {
return false;
}
return nvs_flash_init() == ESP_OK;
}
return err == ESP_OK || err == ESP_ERR_INVALID_STATE;
}
bool plausibleKey(const uint8_t* data) {
const bool all_zero = std::all_of(data, data + kFdskSize, [](uint8_t value) {
return value == 0x00;
});
const bool all_ff = std::all_of(data, data + kFdskSize, [](uint8_t value) {
return value == 0xff;
});
return !all_zero && !all_ff;
}
void generateKey(uint8_t* data) {
do {
esp_fill_random(data, kFdskSize);
} while (!plausibleKey(data));
}
uint8_t crc4Array(const uint8_t* data, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len; ++i) {
crc = kCrc4Tab[crc ^ (data[i] >> 4)];
crc = kCrc4Tab[crc ^ (data[i] & 0x0f)];
}
return crc;
}
std::string toBase32NoPadding(const uint8_t* data, size_t len) {
std::string result;
result.reserve(((len * 8) + 4) / 5);
uint32_t buffer = 0;
int bits_left = 0;
for (size_t i = 0; i < len; ++i) {
buffer = (buffer << 8) | data[i];
bits_left += 8;
while (bits_left >= 5) {
const uint8_t index = static_cast<uint8_t>((buffer >> (bits_left - 5)) & 0x1f);
result.push_back(kBase32Alphabet[index]);
bits_left -= 5;
}
}
if (bits_left > 0) {
const uint8_t index = static_cast<uint8_t>((buffer << (5 - bits_left)) & 0x1f);
result.push_back(kBase32Alphabet[index]);
}
return result;
}
std::string toHex(const uint8_t* data, size_t len) {
std::string result;
result.reserve(len * 2);
for (size_t i = 0; i < len; ++i) {
result.push_back(kHexAlphabet[(data[i] >> 4) & 0x0f]);
result.push_back(kHexAlphabet[data[i] & 0x0f]);
}
return result;
}
std::string generateFdskQrCode(const uint8_t* serial, const uint8_t* key) {
std::array<uint8_t, kSerialSize + kFdskSize + 1> buffer{};
std::copy(serial, serial + kSerialSize, buffer.begin());
std::copy(key, key + kFdskSize, buffer.begin() + kSerialSize);
buffer[kSerialSize + kFdskSize] = static_cast<uint8_t>((crc4Array(buffer.data(), buffer.size() - 1) << 4) & 0xff);
std::string encoded = toBase32NoPadding(buffer.data(), buffer.size());
if (encoded.size() > kFdskQrSize) {
encoded.resize(kFdskQrSize);
}
return encoded;
}
std::string formatFdskLabel(const std::string& qr_code) {
std::string label;
label.reserve(qr_code.size() + (qr_code.size() / 6));
for (size_t i = 0; i < qr_code.size(); ++i) {
if (i != 0 && (i % 6) == 0) {
label.push_back('-');
}
label.push_back(qr_code[i]);
}
return label;
}
} // namespace
namespace gateway::openknx {
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
if (data == nullptr || len < kFdskSize || !ensureNvsReady()) {
return false;
}
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
return false;
}
size_t stored_size = kFdskSize;
err = nvs_get_blob(handle, kFactoryFdskKey, data, &stored_size);
if (err == ESP_OK && stored_size == kFdskSize && plausibleKey(data)) {
nvs_close(handle);
return true;
}
generateKey(data);
err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to store generated KNX factory FDSK: %s", esp_err_to_name(err));
return false;
}
return true;
}
FactoryFdskInfo LoadFactoryFdskInfo() {
FactoryFdskInfo info;
std::array<uint8_t, kFdskSize> key{};
std::array<uint8_t, kSerialSize> serial{};
if (!LoadFactoryFdsk(key.data(), key.size()) ||
esp_read_mac(serial.data(), ESP_MAC_WIFI_STA) != ESP_OK) {
return info;
}
info.available = true;
info.serialNumber = toHex(serial.data(), serial.size());
info.qrCode = generateFdskQrCode(serial.data(), key.data());
info.label = formatFdskLabel(info.qrCode);
return info;
}
} // namespace gateway::openknx
extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) {
return gateway::openknx::LoadFactoryFdsk(data, len);
}
+1 -1
Submodule knx updated: 339d8472e7...1549366447