diff --git a/.gitmodules b/.gitmodules index 449e10b..513fff1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,11 @@ path = knx url = https://git.tonycloud.org/knx/knx.git branch = v1 +[submodule "knx_dali_gw"] + path = knx_dali_gw + url = https://git.tonycloud.org/knx/GW-REG1-Dali.git + branch = v1 +[submodule "tpuart"] + path = tpuart + url = https://git.tonycloud.org/knx/tpuart.git + branch = main diff --git a/README.md b/README.md index a8ecd38..f3562bd 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway. - `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks. - `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges. - `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions. + - `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework. - `gateway_modbus/`: gateway-owned Modbus TCP/RTU/ASCII config, generated DALI point tables, and provisioned Modbus model override dispatch. - `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack, including the gateway-owned BACnet bridge model adapter. - `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications. diff --git a/components/gateway_bridge/CMakeLists.txt b/components/gateway_bridge/CMakeLists.txt index bd6f366..4443b23 100644 --- a/components/gateway_bridge/CMakeLists.txt +++ b/components/gateway_bridge/CMakeLists.txt @@ -10,6 +10,7 @@ set(GATEWAY_BRIDGE_REQUIRES log lwip nvs_flash + openknx_idf ) idf_component_register( diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 5b835d2..fb248a7 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -15,6 +15,7 @@ #include "gateway_knx.hpp" #include "gateway_modbus.hpp" #include "gateway_provisioning.hpp" +#include "openknx_idf/ets_memory_loader.h" #include "cJSON.h" #include "driver/uart.h" @@ -1206,6 +1207,10 @@ struct GatewayBridgeService::ChannelRuntime { return "dali_cloud_" + std::to_string(channel.gateway_id); } + std::string openKnxNamespace() const { + return "openknx_" + std::to_string(channel.gateway_id); + } + esp_err_t start() { comm = std::make_unique( [this](const uint8_t* data, size_t len) { @@ -1254,6 +1259,8 @@ struct GatewayBridgeService::ChannelRuntime { engine->upsertModel(model); } + refreshOpenKnxEtsAssociationsLocked(); + modbus = std::make_unique(*engine); if (modbus_config.has_value()) { modbus->setConfig(modbus_config.value()); @@ -1292,6 +1299,31 @@ struct GatewayBridgeService::ChannelRuntime { diagnostic_snapshot_cache.clear(); } + void refreshOpenKnxEtsAssociationsLocked() { + if (!service_config.knx_enabled) { + return; + } + const auto active_config = activeKnxConfigLocked(); + if (!active_config.has_value()) { + return; + } + const auto snapshot = openknx::LoadEtsMemorySnapshot(openKnxNamespace()); + if (snapshot.associations.empty()) { + return; + } + GatewayKnxConfig updated = active_config.value(); + updated.ets_associations.clear(); + updated.ets_associations.reserve(snapshot.associations.size()); + for (const auto& association : snapshot.associations) { + updated.ets_associations.push_back(GatewayKnxEtsAssociation{ + association.group_address, association.group_object_number}); + } + knx_config = std::move(updated); + ESP_LOGI(kTag, "gateway=%u loaded %u OpenKNX ETS associations from NVS namespace %s", + channel.gateway_id, static_cast(snapshot.associations.size()), + openKnxNamespace().c_str()); + } + std::optional diagnosticSnapshotLocked(int short_address, std::string_view kind) { if (!ValidShortAddress(short_address) || kind.empty()) { @@ -2004,13 +2036,15 @@ struct GatewayBridgeService::ChannelRuntime { knx_last_error.empty() ? router_error.c_str() : knx_last_error.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, "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_AddBoolToObject(knx_json, "multicastEnabled", effective_knx->multicast_enabled); + cJSON_AddBoolToObject(knx_json, "etsDatabaseEnabled", effective_knx->ets_database_enabled); + cJSON_AddNumberToObject(knx_json, "etsBindingCount", + knx == nullptr ? 0 : knx->etsBindingCount()); + cJSON_AddStringToObject(knx_json, "mappingMode", + GatewayKnxMappingModeToString(effective_knx->mapping_mode)); cJSON_AddNumberToObject(knx_json, "mainGroup", effective_knx->main_group); cJSON_AddNumberToObject(knx_json, "udpPort", effective_knx->udp_port); cJSON_AddStringToObject(knx_json, "multicastAddress", @@ -2262,6 +2296,17 @@ struct GatewayBridgeService::ChannelRuntime { cJSON_AddNumberToObject(item, "mainGroup", binding.main_group); cJSON_AddNumberToObject(item, "middleGroup", binding.middle_group); cJSON_AddNumberToObject(item, "subGroup", binding.sub_group); + cJSON_AddStringToObject(item, "mappingMode", + GatewayKnxMappingModeToString(binding.mapping_mode)); + if (binding.group_object_number >= 0) { + cJSON_AddNumberToObject(item, "objectNumber", binding.group_object_number); + } + if (binding.channel_index >= 0) { + cJSON_AddNumberToObject(item, "channelIndex", binding.channel_index); + } + if (!binding.object_role.empty()) { + cJSON_AddStringToObject(item, "objectRole", binding.object_role.c_str()); + } cJSON_AddStringToObject(item, "name", binding.name.c_str()); cJSON_AddStringToObject(item, "datapointType", binding.datapoint_type.c_str()); cJSON_AddStringToObject(item, "dataType", @@ -2894,22 +2939,28 @@ struct GatewayBridgeService::ChannelRuntime { std::set* used_ports = nullptr, std::set* used_uarts = nullptr) { LockGuard guard(lock); + GatewayKnxConfig merged_config = config; + const auto previous_knx = activeKnxConfigLocked(); + if (merged_config.ets_associations.empty() && previous_knx.has_value() && + !previous_knx->ets_associations.empty()) { + merged_config.ets_associations = previous_knx->ets_associations; + } std::string validation_error; const esp_err_t validation_err = validateKnxConfigLocked( - config, activeModbusConfigLocked(), &validation_error); + merged_config, activeModbusConfigLocked(), &validation_error); if (validation_err != ESP_OK) { knx_last_error = validation_error; return validation_err; } const bool restart_router = knx_started || (knx_router != nullptr && knx_router->started()); - if (restart_router && config.ip_router_enabled && used_ports != nullptr && - used_ports->find(config.udp_port) != used_ports->end()) { - knx_last_error = "duplicate KNXnet/IP UDP port " + std::to_string(config.udp_port); + if (restart_router && merged_config.ip_router_enabled && used_ports != nullptr && + used_ports->find(merged_config.udp_port) != used_ports->end()) { + knx_last_error = "duplicate KNXnet/IP UDP port " + std::to_string(merged_config.udp_port); return ESP_ERR_INVALID_STATE; } - if (restart_router && config.ip_router_enabled && used_uarts != nullptr && - used_uarts->find(config.tp_uart.uart_port) != used_uarts->end()) { - knx_last_error = "KNX TP-UART UART" + std::to_string(config.tp_uart.uart_port) + + if (restart_router && merged_config.ip_router_enabled && used_uarts != nullptr && + used_uarts->find(merged_config.tp_uart.uart_port) != used_uarts->end()) { + knx_last_error = "KNX TP-UART UART" + std::to_string(merged_config.tp_uart.uart_port) + " is already used by another runtime"; return ESP_ERR_INVALID_STATE; } @@ -2920,18 +2971,18 @@ struct GatewayBridgeService::ChannelRuntime { BridgeProvisioningStore store(bridgeNamespace()); const esp_err_t err = store.saveObject( kBridgeConfigKey, - GatewayBridgeStoredConfigToValue(bridge_config, modbus_config, config, + GatewayBridgeStoredConfigToValue(bridge_config, modbus_config, merged_config, bacnet_server_config)); if (err != ESP_OK) { return err; } - knx_config = config; + knx_config = merged_config; bridge_config_loaded = true; if (knx != nullptr) { - knx->setConfig(config); + knx->setConfig(merged_config); } if (knx_router != nullptr) { - knx_router->setConfig(config); + knx_router->setConfig(merged_config); } if (restart_router) { return startKnx(used_ports, used_uarts); diff --git a/components/gateway_knx/CMakeLists.txt b/components/gateway_knx/CMakeLists.txt index 4ec1972..a33ad38 100644 --- a/components/gateway_knx/CMakeLists.txt +++ b/components/gateway_knx/CMakeLists.txt @@ -1,7 +1,7 @@ idf_component_register( SRCS "src/gateway_knx.cpp" INCLUDE_DIRS "include" - REQUIRES dali_cpp esp_driver_uart freertos log lwip + REQUIRES dali_cpp esp_driver_uart freertos log lwip openknx_idf ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index dedd60a..d7bcf69 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -32,15 +33,30 @@ struct GatewayKnxTpUartConfig { uint32_t read_timeout_ms{20}; }; +enum class GatewayKnxMappingMode : uint8_t { + kFormula = 0, + kGwReg1Direct = 1, + kManual = 2, + kEtsDatabase = 3, +}; + +struct GatewayKnxEtsAssociation { + uint16_t group_address{0}; + uint16_t group_object_number{0}; +}; + struct GatewayKnxConfig { bool dali_router_enabled{true}; bool ip_router_enabled{false}; bool tunnel_enabled{true}; bool multicast_enabled{true}; + bool ets_database_enabled{true}; + GatewayKnxMappingMode mapping_mode{GatewayKnxMappingMode::kFormula}; uint8_t main_group{0}; uint16_t udp_port{kGatewayKnxDefaultUdpPort}; std::string multicast_address{kGatewayKnxDefaultMulticastAddress}; uint16_t individual_address{0x1101}; + std::vector ets_associations; GatewayKnxTpUartConfig tp_uart; }; @@ -69,8 +85,12 @@ struct GatewayKnxDaliBinding { uint8_t main_group{0}; uint8_t middle_group{0}; uint8_t sub_group{0}; + GatewayKnxMappingMode mapping_mode{GatewayKnxMappingMode::kFormula}; + int group_object_number{-1}; + int channel_index{-1}; std::string address; std::string name; + std::string object_role; std::string datapoint_type; GatewayKnxDaliDataType data_type{GatewayKnxDaliDataType::kUnknown}; GatewayKnxDaliTarget target; @@ -79,6 +99,8 @@ struct GatewayKnxDaliBinding { std::optional GatewayKnxConfigFromValue(const DaliValue* value); DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config); +const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode); +GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value); const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type); const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind); std::optional GatewayKnxDaliDataTypeForMiddleGroup( @@ -94,6 +116,7 @@ class GatewayKnxBridge { void setConfig(const GatewayKnxConfig& config); const GatewayKnxConfig& config() const; + size_t etsBindingCount() const; std::vector describeDaliBindings() const; DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len); @@ -105,9 +128,14 @@ class GatewayKnxBridge { GatewayKnxDaliDataType data_type, GatewayKnxDaliTarget target, const uint8_t* data, size_t len); + DaliBridgeResult executeEtsBindings(uint16_t group_address, + const std::vector& bindings, + const uint8_t* data, size_t len); + void rebuildEtsBindings(); DaliBridgeEngine& engine_; GatewayKnxConfig config_; + std::map> ets_bindings_by_group_address_; }; class GatewayKnxTpIpRouter { diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 96feda0..70b7f1a 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -7,9 +7,13 @@ #include #include +#include +#include #include +#include #include #include +#include #include #include @@ -49,6 +53,15 @@ constexpr uint8_t kTpUartLDataConfirmNegative = 0x0b; constexpr uint8_t kTpUartLDataStart = 0x80; constexpr uint8_t kTpUartLDataEnd = 0x40; constexpr uint8_t kTpUartBusy = 0xc0; +constexpr uint16_t kGwReg1AdrKoOffset = 12; +constexpr uint16_t kGwReg1AdrKoBlockSize = 18; +constexpr uint16_t kGwReg1GrpKoOffset = 1164; +constexpr uint16_t kGwReg1GrpKoBlockSize = 17; +constexpr uint16_t kGwReg1AppKoBroadcastSwitch = 1; +constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2; +constexpr uint8_t kGwReg1KoSwitch = 0; +constexpr uint8_t kGwReg1KoDimmAbsolute = 3; +constexpr uint8_t kGwReg1KoColor = 6; struct DecodedGroupWrite { uint16_t group_address{0}; @@ -94,6 +107,108 @@ std::optional ObjectStringAny(const DaliValue::Object& object, return std::nullopt; } +const DaliValue* ObjectValueAny(const DaliValue::Object& object, + std::initializer_list keys) { + for (const char* key : keys) { + if (const auto* value = getObjectValue(object, key)) { + return value; + } + } + return nullptr; +} + +std::string NormalizeModeString(std::string value) { + value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) { + return ch == '_' || ch == '-' || std::isspace(ch) != 0; + }), + value.end()); + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +std::optional ParseGroupAddressString(const std::string& value) { + int parts[3] = {-1, -1, -1}; + size_t start = 0; + for (int index = 0; index < 3; ++index) { + const size_t slash = value.find('/', start); + const bool last = index == 2; + if ((slash == std::string::npos) != last) { + return std::nullopt; + } + const std::string token = value.substr(start, last ? std::string::npos : slash - start); + if (token.empty()) { + return std::nullopt; + } + char* end = nullptr; + errno = 0; + const long parsed = std::strtol(token.c_str(), &end, 10); + if (errno != 0 || end == token.c_str() || *end != '\0') { + return std::nullopt; + } + parts[index] = static_cast(parsed); + start = slash + 1; + } + if (parts[0] < 0 || parts[0] > 31 || parts[1] < 0 || parts[1] > 7 || parts[2] < 0 || + parts[2] > 255) { + return std::nullopt; + } + return static_cast(((parts[0] & 0x1f) << 11) | ((parts[1] & 0x07) << 8) | + (parts[2] & 0xff)); +} + +std::optional ObjectGroupAddressAny(const DaliValue::Object& object, + std::initializer_list keys) { + for (const char* key : keys) { + const auto* value = getObjectValue(object, key); + if (value == nullptr) { + continue; + } + if (const auto raw = value->asInt()) { + if (raw.value() >= 0 && raw.value() <= 0xffff) { + return static_cast(raw.value()); + } + } + if (const auto raw = value->asString()) { + if (const auto parsed = ParseGroupAddressString(raw.value())) { + return parsed.value(); + } + } + } + return std::nullopt; +} + +std::vector ParseEtsAssociations(const DaliValue::Object& object) { + std::vector associations; + const auto* raw_associations = ObjectValueAny( + object, {"etsAssociations", "ets_associations", "etsBindings", "ets_bindings", + "associationTable", "association_table"}); + const auto* array = raw_associations == nullptr ? nullptr : raw_associations->asArray(); + if (array == nullptr) { + return associations; + } + associations.reserve(array->size()); + for (const auto& item : *array) { + const auto* entry = item.asObject(); + if (entry == nullptr) { + continue; + } + const auto group_address = ObjectGroupAddressAny( + *entry, {"groupAddress", "group_address", "address", "rawAddress", "raw_address"}); + const auto object_number = ObjectIntAny( + *entry, {"objectNumber", "object_number", "groupObjectNumber", "group_object_number", + "ko", "asap"}); + if (!group_address.has_value() || !object_number.has_value() || object_number.value() < 0 || + object_number.value() > kGwReg1GrpKoOffset + (kGwReg1GrpKoBlockSize * 16)) { + continue; + } + associations.push_back(GatewayKnxEtsAssociation{ + group_address.value(), static_cast(object_number.value())}); + } + return associations; +} + std::string TargetName(const GatewayKnxDaliTarget& target) { switch (target.kind) { case GatewayKnxDaliTargetKind::kBroadcast: @@ -386,6 +501,12 @@ std::optional GatewayKnxConfigFromValue(const DaliValue* value .value_or(config.tunnel_enabled); config.multicast_enabled = ObjectBoolAny(object, {"multicastEnabled", "multicast_enabled"}) .value_or(config.multicast_enabled); + if (const auto mode = ObjectStringAny(object, {"mappingMode", "mapping_mode"})) { + config.mapping_mode = GatewayKnxMappingModeFromString(mode.value()); + } + config.ets_database_enabled = ObjectBoolAny(object, {"etsDatabaseEnabled", "ets_database_enabled"}) + .value_or(config.ets_database_enabled); + config.ets_associations = ParseEtsAssociations(object); config.main_group = static_cast( std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group), 0, 31)); @@ -431,6 +552,8 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { out["ipRouterEnabled"] = config.ip_router_enabled; out["tunnelEnabled"] = config.tunnel_enabled; out["multicastEnabled"] = config.multicast_enabled; + out["etsDatabaseEnabled"] = config.ets_database_enabled; + out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode); out["mainGroup"] = static_cast(config.main_group); out["udpPort"] = static_cast(config.udp_port); out["multicastAddress"] = config.multicast_address; @@ -444,9 +567,47 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { serial["txBufferSize"] = static_cast(config.tp_uart.tx_buffer_size); serial["readTimeoutMs"] = static_cast(config.tp_uart.read_timeout_ms); out["tpUart"] = std::move(serial); + DaliValue::Array ets_associations; + ets_associations.reserve(config.ets_associations.size()); + for (const auto& association : config.ets_associations) { + DaliValue::Object entry; + entry["groupAddress"] = static_cast(association.group_address); + entry["groupObjectNumber"] = static_cast(association.group_object_number); + ets_associations.emplace_back(std::move(entry)); + } + out["etsAssociations"] = std::move(ets_associations); return DaliValue(std::move(out)); } +const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode) { + switch (mode) { + case GatewayKnxMappingMode::kEtsDatabase: + return "ets_database"; + case GatewayKnxMappingMode::kGwReg1Direct: + return "gw_reg1_direct"; + case GatewayKnxMappingMode::kManual: + return "manual"; + case GatewayKnxMappingMode::kFormula: + default: + return "formula"; + } +} + +GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value) { + const std::string normalized = NormalizeModeString(value); + if (normalized == "gwreg1direct" || normalized == "gwreg1" || + normalized == "gwreg1channel" || normalized == "channelindex") { + return GatewayKnxMappingMode::kGwReg1Direct; + } + if (normalized == "manual" || normalized == "database" || normalized == "db") { + return GatewayKnxMappingMode::kManual; + } + if (normalized == "etsdatabase" || normalized == "ets" || normalized == "openknx") { + return GatewayKnxMappingMode::kEtsDatabase; + } + return GatewayKnxMappingMode::kFormula; +} + const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) { switch (data_type) { case GatewayKnxDaliDataType::kSwitch: @@ -522,14 +683,170 @@ std::string GatewayKnxGroupAddressString(uint16_t group_address) { std::to_string(sub); } +namespace { + +uint16_t GwReg1GroupAddressForObject(uint8_t main_group, uint16_t object_number) { + return GatewayKnxGroupAddress(main_group, static_cast(object_number >> 8), + static_cast(object_number & 0xff)); +} + +GatewayKnxDaliBinding MakeGwReg1Binding(uint8_t main_group, uint16_t object_number, + int channel_index, const char* object_role, + GatewayKnxDaliDataType data_type, + GatewayKnxDaliTarget target) { + GatewayKnxDaliBinding binding; + binding.mapping_mode = GatewayKnxMappingMode::kGwReg1Direct; + binding.group_object_number = static_cast(object_number); + binding.channel_index = channel_index; + binding.object_role = object_role; + binding.main_group = main_group; + binding.middle_group = static_cast((object_number >> 8) & 0x07); + binding.sub_group = static_cast(object_number & 0xff); + binding.group_address = GwReg1GroupAddressForObject(main_group, object_number); + binding.address = GatewayKnxGroupAddressString(binding.group_address); + binding.data_type = data_type; + binding.target = target; + binding.datapoint_type = DataTypeDpt(data_type); + binding.name = std::string("GW-REG1 ") + TargetName(target) + " - " + + DataTypeName(data_type); + return binding; +} + +std::optional GwReg1BindingForObject(uint8_t main_group, + uint16_t object_number) { + if (object_number == kGwReg1AppKoBroadcastSwitch) { + return MakeGwReg1Binding( + main_group, object_number, -1, "broadcast_switch", GatewayKnxDaliDataType::kSwitch, + GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}); + } + if (object_number == kGwReg1AppKoBroadcastDimm) { + return MakeGwReg1Binding( + main_group, object_number, -1, "broadcast_dimm_absolute", + GatewayKnxDaliDataType::kBrightness, + GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}); + } + + const int adr_relative = static_cast(object_number) - kGwReg1AdrKoOffset; + if (adr_relative >= 0 && adr_relative < kGwReg1AdrKoBlockSize * 64) { + const int channel = adr_relative / kGwReg1AdrKoBlockSize; + const int slot = adr_relative % kGwReg1AdrKoBlockSize; + const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kShortAddress, channel}; + if (slot == kGwReg1KoSwitch) { + return MakeGwReg1Binding(main_group, object_number, channel, "switch", + GatewayKnxDaliDataType::kSwitch, target); + } + if (slot == kGwReg1KoDimmAbsolute) { + return MakeGwReg1Binding(main_group, object_number, channel, "dimm_absolute", + GatewayKnxDaliDataType::kBrightness, target); + } + if (slot == kGwReg1KoColor) { + return MakeGwReg1Binding(main_group, object_number, channel, "color", + GatewayKnxDaliDataType::kRgb, target); + } + } + + const int group_relative = static_cast(object_number) - kGwReg1GrpKoOffset; + if (group_relative >= 0 && group_relative < kGwReg1GrpKoBlockSize * 16) { + const int group = group_relative / kGwReg1GrpKoBlockSize; + const int slot = group_relative % kGwReg1GrpKoBlockSize; + const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kGroup, group}; + if (slot == kGwReg1KoSwitch) { + return MakeGwReg1Binding(main_group, object_number, group, "switch", + GatewayKnxDaliDataType::kSwitch, target); + } + if (slot == kGwReg1KoDimmAbsolute) { + return MakeGwReg1Binding(main_group, object_number, group, "dimm_absolute", + GatewayKnxDaliDataType::kBrightness, target); + } + if (slot == kGwReg1KoColor) { + return MakeGwReg1Binding(main_group, object_number, group, "color", + GatewayKnxDaliDataType::kRgb, target); + } + } + + return std::nullopt; +} + +std::optional EtsBindingForAssociation(uint8_t main_group, + const GatewayKnxEtsAssociation& association) { + auto binding = GwReg1BindingForObject(main_group, association.group_object_number); + if (!binding.has_value()) { + return std::nullopt; + } + binding->mapping_mode = GatewayKnxMappingMode::kEtsDatabase; + binding->group_address = association.group_address; + binding->address = GatewayKnxGroupAddressString(association.group_address); + binding->name = std::string("ETS ") + binding->name; + return binding; +} + +} // namespace + GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {} -void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; } +void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { + config_ = config; + rebuildEtsBindings(); +} const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; } +size_t GatewayKnxBridge::etsBindingCount() const { + size_t count = 0; + for (const auto& entry : ets_bindings_by_group_address_) { + count += entry.second.size(); + } + return count; +} + std::vector GatewayKnxBridge::describeDaliBindings() const { std::vector bindings; + std::set ets_group_addresses; + if (config_.ets_database_enabled) { + for (const auto& entry : ets_bindings_by_group_address_) { + ets_group_addresses.insert(entry.first); + bindings.insert(bindings.end(), entry.second.begin(), entry.second.end()); + } + } + if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { + bindings.reserve(2 + (64 * 3) + (16 * 3)); + if (const auto binding = GwReg1BindingForObject(config_.main_group, + kGwReg1AppKoBroadcastSwitch)) { + if (ets_group_addresses.count(binding->group_address) == 0) { + bindings.push_back(binding.value()); + } + } + if (const auto binding = GwReg1BindingForObject(config_.main_group, + kGwReg1AppKoBroadcastDimm)) { + if (ets_group_addresses.count(binding->group_address) == 0) { + bindings.push_back(binding.value()); + } + } + for (int address = 0; address < 64; ++address) { + const uint16_t base = static_cast(kGwReg1AdrKoOffset + + (address * kGwReg1AdrKoBlockSize)); + for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) { + if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) { + if (ets_group_addresses.count(binding->group_address) == 0) { + bindings.push_back(binding.value()); + } + } + } + } + for (int group = 0; group < 16; ++group) { + const uint16_t base = static_cast(kGwReg1GrpKoOffset + + (group * kGwReg1GrpKoBlockSize)); + for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) { + if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) { + if (ets_group_addresses.count(binding->group_address) == 0) { + bindings.push_back(binding.value()); + } + } + } + } + return bindings; + } + bindings.reserve(4 * 81); for (uint8_t middle = 1; middle <= 4; ++middle) { const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle); @@ -542,6 +859,7 @@ std::vector GatewayKnxBridge::describeDaliBindings() cons continue; } GatewayKnxDaliBinding binding; + binding.mapping_mode = GatewayKnxMappingMode::kFormula; binding.main_group = config_.main_group; binding.middle_group = middle; binding.sub_group = sub; @@ -549,6 +867,10 @@ std::vector GatewayKnxBridge::describeDaliBindings() cons binding.address = GatewayKnxGroupAddressString(binding.group_address); binding.data_type = data_type.value(); binding.target = target.value(); + if (ets_group_addresses.count(binding.group_address) != 0) { + continue; + } + binding.object_role = GatewayKnxDataTypeToString(data_type.value()); binding.datapoint_type = DataTypeDpt(data_type.value()); binding.name = TargetName(target.value()) + " - " + DataTypeName(data_type.value()); bindings.push_back(std::move(binding)); @@ -570,12 +892,29 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons if (!config_.dali_router_enabled) { return ErrorResult(group_address, "KNX to DALI router disabled"); } + if (config_.ets_database_enabled) { + const auto ets_bindings = ets_bindings_by_group_address_.find(group_address); + if (ets_bindings != ets_bindings_by_group_address_.end()) { + return executeEtsBindings(group_address, ets_bindings->second, data, len); + } + } const uint8_t main = static_cast((group_address >> 11) & 0x1f); const uint8_t middle = static_cast((group_address >> 8) & 0x07); const uint8_t sub = static_cast(group_address & 0xff); if (main != config_.main_group) { return ErrorResult(group_address, "KNX main group does not match gateway config"); } + if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { + const uint16_t object_number = static_cast((middle << 8) | sub); + const auto binding = GwReg1BindingForObject(config_.main_group, object_number); + if (!binding.has_value()) { + return ErrorResult(group_address, "unmapped GW-REG1 KNX object address"); + } + return executeForDecodedWrite(group_address, binding->data_type, binding->target, data, len); + } + if (config_.mapping_mode == GatewayKnxMappingMode::kManual) { + return ErrorResult(group_address, "manual KNX mapping dataset is not configured"); + } const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle); const auto target = GatewayKnxDaliTargetForSubgroup(sub); if (!data_type.has_value() || !target.has_value()) { @@ -584,6 +923,41 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len); } +DaliBridgeResult GatewayKnxBridge::executeEtsBindings( + uint16_t group_address, const std::vector& bindings, + const uint8_t* data, size_t len) { + if (bindings.empty()) { + return ErrorResult(group_address, "unmapped ETS KNX group address"); + } + DaliBridgeResult result; + result.ok = true; + result.metadata["source"] = "ets_database"; + result.metadata["groupAddress"] = GatewayKnxGroupAddressString(group_address); + result.metadata["bindingCount"] = static_cast(bindings.size()); + for (const auto& binding : bindings) { + DaliBridgeResult child = executeForDecodedWrite(group_address, binding.data_type, + binding.target, data, len); + result.ok = result.ok && child.ok; + result.results.emplace_back(child.toJson()); + } + result.data = static_cast(result.results.size()); + if (!result.ok) { + result.error = "one or more ETS KNX bindings failed"; + } + return result; +} + +void GatewayKnxBridge::rebuildEtsBindings() { + ets_bindings_by_group_address_.clear(); + for (const auto& association : config_.ets_associations) { + const auto binding = EtsBindingForAssociation(config_.main_group, association); + if (!binding.has_value()) { + continue; + } + ets_bindings_by_group_address_[association.group_address].push_back(binding.value()); + } +} + DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address, GatewayKnxDaliDataType data_type, GatewayKnxDaliTarget target, diff --git a/components/openknx_idf/CMakeLists.txt b/components/openknx_idf/CMakeLists.txt new file mode 100644 index 0000000..633538c --- /dev/null +++ b/components/openknx_idf/CMakeLists.txt @@ -0,0 +1,63 @@ +set(OPENKNX_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../knx") +set(TPUART_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../tpuart") + +if(NOT EXISTS "${OPENKNX_ROOT}/src/knx/platform.h") + message(FATAL_ERROR "OpenKNX submodule is missing at ${OPENKNX_ROOT}") +endif() + +if(NOT EXISTS "${TPUART_ROOT}/src/TPUart/DataLinkLayer.h") + message(FATAL_ERROR "TPUart submodule is missing at ${TPUART_ROOT}") +endif() + +file(GLOB OPENKNX_SRCS + "${OPENKNX_ROOT}/src/knx/*.cpp" +) + +set(TPUART_SRCS + "${TPUART_ROOT}/src/TPUart/DataLinkLayer.cpp" + "${TPUART_ROOT}/src/TPUart/Receiver.cpp" + "${TPUART_ROOT}/src/TPUart/RepetitionFilter.cpp" + "${TPUART_ROOT}/src/TPUart/RingBuffer.cpp" + "${TPUART_ROOT}/src/TPUart/SearchBuffer.cpp" + "${TPUART_ROOT}/src/TPUart/Statistics.cpp" + "${TPUART_ROOT}/src/TPUart/SystemState.cpp" + "${TPUART_ROOT}/src/TPUart/Transmitter.cpp" + "${TPUART_ROOT}/src/TPUart.cpp" +) + +idf_component_register( + SRCS + "src/arduino_compat.cpp" + "src/esp_idf_platform.cpp" + "src/ets_memory_loader.cpp" + "src/tpuart_uart_interface.cpp" + ${OPENKNX_SRCS} + ${TPUART_SRCS} + INCLUDE_DIRS + "include" + "${OPENKNX_ROOT}/src" + "${TPUART_ROOT}/src" + REQUIRES + esp_driver_gpio + esp_driver_uart + esp_netif + esp_timer + esp_wifi + freertos + log + lwip + nvs_flash +) + +target_compile_definitions(${COMPONENT_LIB} PUBLIC + MASK_VERSION=0x07B0 + KNX_FLASH_SIZE=4096 + KNX_NO_AUTOMATIC_GLOBAL_INSTANCE + KNX_NO_SPI +) + +target_compile_options(${COMPONENT_LIB} PRIVATE + -Wno-unused-parameter +) + +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/openknx_idf/include/Arduino.h b/components/openknx_idf/include/Arduino.h new file mode 100644 index 0000000..546da3d --- /dev/null +++ b/components/openknx_idf/include/Arduino.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +#ifndef DEC +#define DEC 10 +#endif + +#ifndef HEX +#define HEX 16 +#endif + +#ifndef INPUT +#define INPUT 0x0 +#endif + +#ifndef OUTPUT +#define OUTPUT 0x1 +#endif + +#ifndef INPUT_PULLUP +#define INPUT_PULLUP 0x2 +#endif + +#ifndef INPUT_PULLDOWN +#define INPUT_PULLDOWN 0x3 +#endif + +#ifndef LOW +#define LOW 0x0 +#endif + +#ifndef HIGH +#define HIGH 0x1 +#endif + +#ifndef CHANGE +#define CHANGE 2 +#endif + +#ifndef FALLING +#define FALLING 3 +#endif + +#ifndef RISING +#define RISING 4 +#endif + +using uint = unsigned int; + +uint32_t millis(); +uint32_t micros(); +void delay(uint32_t millis); +void delayMicroseconds(unsigned int howLong); +void pinMode(uint32_t pin, uint32_t mode); +void digitalWrite(uint32_t pin, uint32_t value); +uint32_t digitalRead(uint32_t pin); +typedef void (*voidFuncPtr)(void); +void attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode); \ No newline at end of file diff --git a/components/openknx_idf/include/openknx_idf/esp_idf_platform.h b/components/openknx_idf/include/openknx_idf/esp_idf_platform.h new file mode 100644 index 0000000..e97e2dd --- /dev/null +++ b/components/openknx_idf/include/openknx_idf/esp_idf_platform.h @@ -0,0 +1,59 @@ +#pragma once + +#include "knx/platform.h" + +#include "esp_netif.h" +#include "lwip/sockets.h" + +#include +#include +#include +#include + +namespace gateway::openknx { + +class EspIdfPlatform : public Platform { + public: + explicit EspIdfPlatform(TPUart::Interface::Abstract* interface = nullptr, + const char* nvs_namespace = "openknx"); + ~EspIdfPlatform() override; + + void networkInterface(esp_netif_t* netif); + esp_netif_t* networkInterface() const; + + uint32_t currentIpAddress() override; + uint32_t currentSubnetMask() override; + uint32_t currentDefaultGateway() override; + void macAddress(uint8_t* data) override; + uint32_t uniqueSerialNumber() override; + + void restart() override; + void fatalError() override; + + void setupMultiCast(uint32_t addr, uint16_t port) override; + void closeMultiCast() override; + bool sendBytesMultiCast(uint8_t* buffer, uint16_t len) override; + int readBytesMultiCast(uint8_t* buffer, uint16_t maxLen) override; + int readBytesMultiCast(uint8_t* buffer, uint16_t maxLen, uint32_t& src_addr, + uint16_t& src_port) override; + bool sendBytesUniCast(uint32_t addr, uint16_t port, uint8_t* buffer, + uint16_t len) override; + + uint8_t* getEepromBuffer(uint32_t size) override; + void commitToEeprom() override; + + private: + esp_netif_t* effectiveNetif() const; + void loadEeprom(size_t size); + + esp_netif_t* netif_{nullptr}; + int udp_sock_{-1}; + sockaddr_in multicast_remote_{}; + sockaddr_in last_remote_{}; + bool has_last_remote_{false}; + std::vector eeprom_; + std::string nvs_namespace_; + bool eeprom_loaded_{false}; +}; + +} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/include/openknx_idf/ets_memory_loader.h b/components/openknx_idf/include/openknx_idf/ets_memory_loader.h new file mode 100644 index 0000000..e838545 --- /dev/null +++ b/components/openknx_idf/include/openknx_idf/ets_memory_loader.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +namespace gateway::openknx { + +struct EtsAssociation { + uint16_t group_address{0}; + uint16_t group_object_number{0}; +}; + +struct EtsMemorySnapshot { + bool configured{false}; + std::vector associations; +}; + +EtsMemorySnapshot LoadEtsMemorySnapshot(const std::string& nvs_namespace); + +} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/include/openknx_idf/openknx_idf.h b/components/openknx_idf/include/openknx_idf/openknx_idf.h new file mode 100644 index 0000000..d036c53 --- /dev/null +++ b/components/openknx_idf/include/openknx_idf/openknx_idf.h @@ -0,0 +1,14 @@ +#pragma once + +#include "openknx_idf/ets_memory_loader.h" +#include "openknx_idf/esp_idf_platform.h" +#include "openknx_idf/tpuart_uart_interface.h" + +#include "knx/bau07B0.h" +#include "knx_facade.h" + +namespace gateway::openknx { + +using DaliGatewayDevice = KnxFacade; + +} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h b/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h new file mode 100644 index 0000000..788683a --- /dev/null +++ b/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h @@ -0,0 +1,41 @@ +#pragma once + +#include "TPUart/Interface/Abstract.h" + +#include "driver/uart.h" + +#include +#include +#include +#include + +namespace gateway::openknx { + +class TpuartUartInterface : public TPUart::Interface::Abstract { + public: + TpuartUartInterface(uart_port_t uart_port, int tx_pin, int rx_pin, + size_t rx_buffer_size = 512, size_t tx_buffer_size = 512); + ~TpuartUartInterface(); + + void begin(int baud) override; + void end() override; + bool available() override; + bool availableForWrite() override; + bool write(char value) override; + int read() override; + bool overflow() override; + void flush() override; + bool hasCallback() override; + void registerCallback(std::function callback) override; + + private: + uart_port_t uart_port_; + int tx_pin_; + int rx_pin_; + size_t rx_buffer_size_; + size_t tx_buffer_size_; + std::atomic_bool overflow_{false}; + std::function callback_; +}; + +} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/src/arduino_compat.cpp b/components/openknx_idf/src/arduino_compat.cpp new file mode 100644 index 0000000..dd9c98b --- /dev/null +++ b/components/openknx_idf/src/arduino_compat.cpp @@ -0,0 +1,180 @@ +#include "Arduino.h" + +#include "driver/gpio.h" +#include "esp_err.h" +#include "esp_rom_sys.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include +#include + +namespace { + +std::array g_gpio_callbacks{}; +bool g_isr_service_installed = false; + +void IRAM_ATTR gpioIsrThunk(void* arg) { + const auto pin = static_cast(reinterpret_cast(arg)); + if (pin < g_gpio_callbacks.size() && g_gpio_callbacks[pin] != nullptr) { + g_gpio_callbacks[pin](); + } +} + +gpio_int_type_t toGpioInterrupt(uint32_t mode) { + switch (mode) { + case RISING: + return GPIO_INTR_POSEDGE; + case FALLING: + return GPIO_INTR_NEGEDGE; + case CHANGE: + return GPIO_INTR_ANYEDGE; + default: + return GPIO_INTR_DISABLE; + } +} + +void printUnsigned(unsigned long long value, int base) { + if (base == HEX) { + std::printf("%llX", value); + } else { + std::printf("%llu", value); + } +} + +void printSigned(long long value, int base) { + if (base == HEX) { + std::printf("%llX", static_cast(value)); + } else { + std::printf("%lld", value); + } +} + +} // namespace + +uint32_t millis() { return static_cast(esp_timer_get_time() / 1000ULL); } + +uint32_t micros() { return static_cast(esp_timer_get_time()); } + +void delay(uint32_t millis) { vTaskDelay(pdMS_TO_TICKS(millis)); } + +void delayMicroseconds(unsigned int howLong) { esp_rom_delay_us(howLong); } + +void pinMode(uint32_t pin, uint32_t mode) { + if (pin >= GPIO_NUM_MAX) { + return; + } + gpio_config_t config{}; + config.pin_bit_mask = 1ULL << pin; + config.mode = mode == OUTPUT ? GPIO_MODE_OUTPUT : GPIO_MODE_INPUT; + config.pull_up_en = mode == INPUT_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; + config.pull_down_en = mode == INPUT_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; + config.intr_type = GPIO_INTR_DISABLE; + gpio_config(&config); +} + +void digitalWrite(uint32_t pin, uint32_t value) { + if (pin < GPIO_NUM_MAX) { + gpio_set_level(static_cast(pin), value == LOW ? 0 : 1); + } +} + +uint32_t digitalRead(uint32_t pin) { + if (pin >= GPIO_NUM_MAX) { + return LOW; + } + return gpio_get_level(static_cast(pin)) == 0 ? LOW : HIGH; +} + +void attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode) { + if (pin >= GPIO_NUM_MAX) { + return; + } + if (!g_isr_service_installed) { + const esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM); + g_isr_service_installed = err == ESP_OK || err == ESP_ERR_INVALID_STATE; + } + if (!g_isr_service_installed) { + return; + } + gpio_set_intr_type(static_cast(pin), toGpioInterrupt(mode)); + gpio_isr_handler_remove(static_cast(pin)); + g_gpio_callbacks[pin] = callback; + if (callback != nullptr) { + gpio_isr_handler_add(static_cast(pin), gpioIsrThunk, + reinterpret_cast(static_cast(pin))); + } +} + +void print(const char value[]) { std::printf("%s", value == nullptr ? "" : value); } + +void print(char value) { std::printf("%c", value); } + +void print(unsigned char value, int base) { printUnsigned(value, base); } + +void print(int value, int base) { printSigned(value, base); } + +void print(unsigned int value, int base) { printUnsigned(value, base); } + +void print(long value, int base) { printSigned(value, base); } + +void print(unsigned long value, int base) { printUnsigned(value, base); } + +void print(long long value, int base) { printSigned(value, base); } + +void print(unsigned long long value, int base) { printUnsigned(value, base); } + +void print(double value) { std::printf("%f", value); } + +void println(const char value[]) { + print(value); + std::printf("\n"); +} + +void println(char value) { + print(value); + std::printf("\n"); +} + +void println(unsigned char value, int base) { + print(value, base); + std::printf("\n"); +} + +void println(int value, int base) { + print(value, base); + std::printf("\n"); +} + +void println(unsigned int value, int base) { + print(value, base); + std::printf("\n"); +} + +void println(long value, int base) { + print(value, base); + std::printf("\n"); +} + +void println(unsigned long value, int base) { + print(value, base); + std::printf("\n"); +} + +void println(long long value, int base) { + print(value, base); + std::printf("\n"); +} + +void println(unsigned long long value, int base) { + print(value, base); + std::printf("\n"); +} + +void println(double value) { + print(value); + std::printf("\n"); +} + +void println(void) { std::printf("\n"); } \ No newline at end of file diff --git a/components/openknx_idf/src/esp_idf_platform.cpp b/components/openknx_idf/src/esp_idf_platform.cpp new file mode 100644 index 0000000..ad92fb5 --- /dev/null +++ b/components/openknx_idf/src/esp_idf_platform.cpp @@ -0,0 +1,273 @@ +#include "openknx_idf/esp_idf_platform.h" + +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "lwip/inet.h" +#include "nvs.h" +#include "nvs_flash.h" + +#include +#include +#include +#include + +namespace gateway::openknx { +namespace { + +constexpr const char* kTag = "openknx_idf"; +constexpr const char* kEepromKey = "eeprom"; + +esp_netif_t* findDefaultNetif() { + if (auto* sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")) { + return sta; + } + if (auto* eth = esp_netif_get_handle_from_ifkey("ETH_DEF")) { + return eth; + } + return nullptr; +} + +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; +} + +} // namespace + +EspIdfPlatform::EspIdfPlatform(TPUart::Interface::Abstract* interface, + const char* nvs_namespace) + : nvs_namespace_(nvs_namespace == nullptr ? "openknx" : nvs_namespace) { + this->interface(interface); +} + +EspIdfPlatform::~EspIdfPlatform() { closeMultiCast(); } + +void EspIdfPlatform::networkInterface(esp_netif_t* netif) { netif_ = netif; } + +esp_netif_t* EspIdfPlatform::networkInterface() const { return netif_; } + +esp_netif_t* EspIdfPlatform::effectiveNetif() const { + return netif_ == nullptr ? findDefaultNetif() : netif_; +} + +uint32_t EspIdfPlatform::currentIpAddress() { + esp_netif_ip_info_t ip_info{}; + esp_netif_t* netif = effectiveNetif(); + if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) { + return 0; + } + return ip_info.ip.addr; +} + +uint32_t EspIdfPlatform::currentSubnetMask() { + esp_netif_ip_info_t ip_info{}; + esp_netif_t* netif = effectiveNetif(); + if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) { + return 0; + } + return ip_info.netmask.addr; +} + +uint32_t EspIdfPlatform::currentDefaultGateway() { + esp_netif_ip_info_t ip_info{}; + esp_netif_t* netif = effectiveNetif(); + if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) { + return 0; + } + return ip_info.gw.addr; +} + +void EspIdfPlatform::macAddress(uint8_t* data) { + if (data == nullptr) { + return; + } + if (esp_read_mac(data, ESP_MAC_WIFI_STA) != ESP_OK) { + std::memset(data, 0, 6); + } +} + +uint32_t EspIdfPlatform::uniqueSerialNumber() { + uint8_t mac[6]{}; + macAddress(mac); + return (static_cast(mac[0]) << 24) | (static_cast(mac[1]) << 16) | + (static_cast(mac[4]) << 8) | mac[5]; +} + +void EspIdfPlatform::restart() { esp_restart(); } + +void EspIdfPlatform::fatalError() { + ESP_LOGE(kTag, "OpenKNX fatal error"); + while (true) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +void EspIdfPlatform::setupMultiCast(uint32_t addr, uint16_t port) { + closeMultiCast(); + udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (udp_sock_ < 0) { + ESP_LOGE(kTag, "failed to create UDP socket: errno=%d", errno); + return; + } + + 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(port); + if (bind(udp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { + ESP_LOGE(kTag, "failed to bind UDP socket: errno=%d", errno); + closeMultiCast(); + return; + } + + timeval timeout{}; + timeout.tv_sec = 0; + timeout.tv_usec = 1000; + setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + ip_mreq mreq{}; + mreq.imr_multiaddr.s_addr = htonl(addr); + mreq.imr_interface.s_addr = currentIpAddress(); + if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { + ESP_LOGW(kTag, "failed to join KNX multicast group: errno=%d", errno); + } + + uint8_t loop = 0; + setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)); + + multicast_remote_ = {}; + multicast_remote_.sin_family = AF_INET; + multicast_remote_.sin_addr.s_addr = htonl(addr); + multicast_remote_.sin_port = htons(port); +} + +void EspIdfPlatform::closeMultiCast() { + if (udp_sock_ >= 0) { + shutdown(udp_sock_, SHUT_RDWR); + close(udp_sock_); + udp_sock_ = -1; + } + has_last_remote_ = false; +} + +bool EspIdfPlatform::sendBytesMultiCast(uint8_t* buffer, uint16_t len) { + if (udp_sock_ < 0 || buffer == nullptr || len == 0) { + return false; + } + const int sent = sendto(udp_sock_, buffer, len, 0, reinterpret_cast(&multicast_remote_), + sizeof(multicast_remote_)); + return sent == len; +} + +int EspIdfPlatform::readBytesMultiCast(uint8_t* buffer, uint16_t maxLen) { + uint32_t src_addr = 0; + uint16_t src_port = 0; + return readBytesMultiCast(buffer, maxLen, src_addr, src_port); +} + +int EspIdfPlatform::readBytesMultiCast(uint8_t* buffer, uint16_t maxLen, uint32_t& src_addr, + uint16_t& src_port) { + if (udp_sock_ < 0 || buffer == nullptr || maxLen == 0) { + return 0; + } + sockaddr_in remote{}; + socklen_t remote_len = sizeof(remote); + const int len = recvfrom(udp_sock_, buffer, maxLen, 0, reinterpret_cast(&remote), + &remote_len); + if (len <= 0) { + return 0; + } + last_remote_ = remote; + has_last_remote_ = true; + src_addr = ntohl(remote.sin_addr.s_addr); + src_port = ntohs(remote.sin_port); + return len; +} + +bool EspIdfPlatform::sendBytesUniCast(uint32_t addr, uint16_t port, uint8_t* buffer, + uint16_t len) { + if (udp_sock_ < 0 || buffer == nullptr || len == 0) { + return false; + } + sockaddr_in remote{}; + if (addr == 0 && port == 0 && has_last_remote_) { + remote = last_remote_; + } else { + remote.sin_family = AF_INET; + remote.sin_addr.s_addr = htonl(addr); + remote.sin_port = htons(port); + } + const int sent = sendto(udp_sock_, buffer, len, 0, reinterpret_cast(&remote), + sizeof(remote)); + return sent == len; +} + +void EspIdfPlatform::loadEeprom(size_t size) { + if (eeprom_loaded_ && eeprom_.size() == size) { + return; + } + eeprom_.assign(size, 0xff); + eeprom_loaded_ = true; + + if (!ensureNvsReady()) { + ESP_LOGW(kTag, "NVS is not ready for OpenKNX EEPROM load"); + return; + } + + nvs_handle_t handle = 0; + if (nvs_open(nvs_namespace_.c_str(), NVS_READONLY, &handle) != ESP_OK) { + return; + } + size_t stored_size = 0; + if (nvs_get_blob(handle, kEepromKey, nullptr, &stored_size) == ESP_OK && stored_size > 0) { + std::vector stored(stored_size); + if (nvs_get_blob(handle, kEepromKey, stored.data(), &stored_size) == ESP_OK) { + std::memcpy(eeprom_.data(), stored.data(), std::min(eeprom_.size(), stored.size())); + } + } + nvs_close(handle); +} + +uint8_t* EspIdfPlatform::getEepromBuffer(uint32_t size) { + loadEeprom(size); + return eeprom_.data(); +} + +void EspIdfPlatform::commitToEeprom() { + if (eeprom_.empty()) { + return; + } + if (!ensureNvsReady()) { + ESP_LOGW(kTag, "NVS is not ready for OpenKNX EEPROM commit"); + return; + } + + nvs_handle_t handle = 0; + esp_err_t err = nvs_open(nvs_namespace_.c_str(), NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGW(kTag, "failed to open OpenKNX NVS namespace: %s", esp_err_to_name(err)); + return; + } + err = nvs_set_blob(handle, kEepromKey, eeprom_.data(), eeprom_.size()); + if (err == ESP_OK) { + err = nvs_commit(handle); + } + if (err != ESP_OK) { + ESP_LOGW(kTag, "failed to commit OpenKNX EEPROM: %s", esp_err_to_name(err)); + } + nvs_close(handle); +} + +} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/src/ets_memory_loader.cpp b/components/openknx_idf/src/ets_memory_loader.cpp new file mode 100644 index 0000000..be3e19c --- /dev/null +++ b/components/openknx_idf/src/ets_memory_loader.cpp @@ -0,0 +1,50 @@ +#include "openknx_idf/ets_memory_loader.h" + +#include "openknx_idf/esp_idf_platform.h" + +#include "knx/bau07B0.h" + +#include + +namespace gateway::openknx { +namespace { + +void CollectAssociation(uint16_t group_address, uint16_t group_object_number, + void* context) { + auto* associations = static_cast*>(context); + if (associations == nullptr) { + return; + } + associations->push_back(EtsAssociation{group_address, group_object_number}); +} + +} // namespace + +EtsMemorySnapshot LoadEtsMemorySnapshot(const std::string& nvs_namespace) { + EspIdfPlatform platform(nullptr, nvs_namespace.c_str()); + Bau07B0 device(platform); + device.deviceObject().manufacturerId(0xfa); + device.deviceObject().bauNumber(platform.uniqueSerialNumber()); + device.readMemory(); + + EtsMemorySnapshot snapshot; + snapshot.configured = device.configured(); + device.forEachEtsAssociation(CollectAssociation, &snapshot.associations); + std::sort(snapshot.associations.begin(), snapshot.associations.end(), + [](const EtsAssociation& lhs, const EtsAssociation& rhs) { + if (lhs.group_address != rhs.group_address) { + return lhs.group_address < rhs.group_address; + } + return lhs.group_object_number < rhs.group_object_number; + }); + snapshot.associations.erase( + std::unique(snapshot.associations.begin(), snapshot.associations.end(), + [](const EtsAssociation& lhs, const EtsAssociation& rhs) { + return lhs.group_address == rhs.group_address && + lhs.group_object_number == rhs.group_object_number; + }), + snapshot.associations.end()); + return snapshot; +} + +} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/src/tpuart_uart_interface.cpp b/components/openknx_idf/src/tpuart_uart_interface.cpp new file mode 100644 index 0000000..4920a53 --- /dev/null +++ b/components/openknx_idf/src/tpuart_uart_interface.cpp @@ -0,0 +1,114 @@ +#include "openknx_idf/tpuart_uart_interface.h" + +#include "esp_log.h" + +#include + +namespace gateway::openknx { +namespace { + +constexpr const char* kTag = "openknx_tpuart"; + +} // namespace + +TpuartUartInterface::TpuartUartInterface(uart_port_t uart_port, int tx_pin, int rx_pin, + size_t rx_buffer_size, size_t tx_buffer_size) + : uart_port_(uart_port), + tx_pin_(tx_pin), + rx_pin_(rx_pin), + rx_buffer_size_(rx_buffer_size), + tx_buffer_size_(tx_buffer_size) {} + +TpuartUartInterface::~TpuartUartInterface() { end(); } + +void TpuartUartInterface::begin(int baud) { + if (_running) { + end(); + } + + uart_config_t config{}; + config.baud_rate = baud; + config.data_bits = UART_DATA_8_BITS; + config.parity = UART_PARITY_EVEN; + config.stop_bits = UART_STOP_BITS_1; + config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; + config.source_clk = UART_SCLK_DEFAULT; + + esp_err_t err = uart_param_config(uart_port_, &config); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to configure UART%d: %s", uart_port_, esp_err_to_name(err)); + return; + } + + err = uart_set_pin(uart_port_, tx_pin_ < 0 ? UART_PIN_NO_CHANGE : tx_pin_, + rx_pin_ < 0 ? UART_PIN_NO_CHANGE : rx_pin_, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to route UART%d pins: %s", uart_port_, esp_err_to_name(err)); + return; + } + + err = uart_driver_install(uart_port_, rx_buffer_size_, tx_buffer_size_, 0, nullptr, 0); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to install UART%d driver: %s", uart_port_, esp_err_to_name(err)); + return; + } + + uart_set_rx_full_threshold(uart_port_, 1); + _running = true; +} + +void TpuartUartInterface::end() { + if (!_running) { + return; + } + _running = false; + uart_driver_delete(uart_port_); +} + +bool TpuartUartInterface::available() { + if (!_running) { + return false; + } + size_t len = 0; + return uart_get_buffered_data_len(uart_port_, &len) == ESP_OK && len > 0; +} + +bool TpuartUartInterface::availableForWrite() { + if (!_running) { + return false; + } + size_t len = 0; + return uart_get_tx_buffer_free_size(uart_port_, &len) == ESP_OK && len > 0; +} + +bool TpuartUartInterface::write(char value) { + if (!_running) { + return false; + } + return uart_write_bytes(uart_port_, &value, 1) == 1; +} + +int TpuartUartInterface::read() { + if (!_running) { + return -1; + } + uint8_t value = 0; + return uart_read_bytes(uart_port_, &value, 1, 0) == 1 ? value : -1; +} + +bool TpuartUartInterface::overflow() { return overflow_.exchange(false); } + +void TpuartUartInterface::flush() { + if (_running) { + uart_flush(uart_port_); + } +} + +bool TpuartUartInterface::hasCallback() { return false; } + +void TpuartUartInterface::registerCallback(std::function callback) { + callback_ = std::move(callback); +} + +} // namespace gateway::openknx \ No newline at end of file diff --git a/knx b/knx index 7124a64..b747f62 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit 7124a6435ddca51c36279c748da2df1ea9b877b9 +Subproject commit b747f6284d528a4a4aeb65c17e07b57bca647da7 diff --git a/knx_dali_gw b/knx_dali_gw new file mode 160000 index 0000000..5cd7e66 --- /dev/null +++ b/knx_dali_gw @@ -0,0 +1 @@ +Subproject commit 5cd7e66bf0e0aa692973e32a2acd6e941a05e8be diff --git a/tpuart b/tpuart new file mode 160000 index 0000000..f8c01e6 --- /dev/null +++ b/tpuart @@ -0,0 +1 @@ +Subproject commit f8c01e6a32b19f338b0b56ddb58a5d7a4135aece