Add OpenKNX IDF component with TPUart integration

- Created CMakeLists.txt for the OpenKNX IDF component, ensuring dependencies on OpenKNX and TPUart submodules.
- Implemented Arduino compatibility header for basic functions like millis, delay, pinMode, and digitalRead.
- Developed EspIdfPlatform class for network interface management and multicast communication.
- Added EtsMemoryLoader for loading ETS memory snapshots and managing associations.
- Introduced TpuartUartInterface for UART communication with methods for reading, writing, and managing callbacks.
- Implemented arduino_compat.cpp for Arduino-like functionality on ESP-IDF.
- Created source files for platform and memory loader implementations.
- Updated submodules for knx, knx_dali_gw, and tpuart.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-11 07:05:40 +08:00
parent 1b8753636f
commit 70367f53ca
20 changed files with 1359 additions and 20 deletions
+8
View File
@@ -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
+1
View File
@@ -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.
+1
View File
@@ -10,6 +10,7 @@ set(GATEWAY_BRIDGE_REQUIRES
log
lwip
nvs_flash
openknx_idf
)
idf_component_register(
@@ -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<DaliComm>(
[this](const uint8_t* data, size_t len) {
@@ -1254,6 +1259,8 @@ struct GatewayBridgeService::ChannelRuntime {
engine->upsertModel(model);
}
refreshOpenKnxEtsAssociationsLocked();
modbus = std::make_unique<GatewayModbusBridge>(*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<unsigned>(snapshot.associations.size()),
openKnxNamespace().c_str());
}
std::optional<DaliDomainSnapshot> 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<uint16_t>* used_ports = nullptr,
std::set<int>* 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);
+1 -1
View File
@@ -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)
@@ -12,6 +12,7 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <vector>
@@ -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<GatewayKnxEtsAssociation> 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<GatewayKnxConfig> 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<GatewayKnxDaliDataType> GatewayKnxDaliDataTypeForMiddleGroup(
@@ -94,6 +116,7 @@ class GatewayKnxBridge {
void setConfig(const GatewayKnxConfig& config);
const GatewayKnxConfig& config() const;
size_t etsBindingCount() const;
std::vector<GatewayKnxDaliBinding> 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<GatewayKnxDaliBinding>& bindings,
const uint8_t* data, size_t len);
void rebuildEtsBindings();
DaliBridgeEngine& engine_;
GatewayKnxConfig config_;
std::map<uint16_t, std::vector<GatewayKnxDaliBinding>> ets_bindings_by_group_address_;
};
class GatewayKnxTpIpRouter {
+375 -1
View File
@@ -7,9 +7,13 @@
#include <algorithm>
#include <array>
#include <cerrno>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <initializer_list>
#include <set>
#include <utility>
#include <unistd.h>
@@ -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<std::string> ObjectStringAny(const DaliValue::Object& object,
return std::nullopt;
}
const DaliValue* ObjectValueAny(const DaliValue::Object& object,
std::initializer_list<const char*> 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<char>(std::tolower(ch));
});
return value;
}
std::optional<uint16_t> 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<int>(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<uint16_t>(((parts[0] & 0x1f) << 11) | ((parts[1] & 0x07) << 8) |
(parts[2] & 0xff));
}
std::optional<uint16_t> ObjectGroupAddressAny(const DaliValue::Object& object,
std::initializer_list<const char*> 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<uint16_t>(raw.value());
}
}
if (const auto raw = value->asString()) {
if (const auto parsed = ParseGroupAddressString(raw.value())) {
return parsed.value();
}
}
}
return std::nullopt;
}
std::vector<GatewayKnxEtsAssociation> ParseEtsAssociations(const DaliValue::Object& object) {
std::vector<GatewayKnxEtsAssociation> 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<uint16_t>(object_number.value())});
}
return associations;
}
std::string TargetName(const GatewayKnxDaliTarget& target) {
switch (target.kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
@@ -386,6 +501,12 @@ std::optional<GatewayKnxConfig> 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<uint8_t>(
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<int>(config.main_group);
out["udpPort"] = static_cast<int>(config.udp_port);
out["multicastAddress"] = config.multicast_address;
@@ -444,9 +567,47 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
serial["txBufferSize"] = static_cast<int>(config.tp_uart.tx_buffer_size);
serial["readTimeoutMs"] = static_cast<int>(config.tp_uart.read_timeout_ms);
out["tpUart"] = std::move(serial);
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<int>(association.group_address);
entry["groupObjectNumber"] = static_cast<int>(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<uint8_t>(object_number >> 8),
static_cast<uint8_t>(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<int>(object_number);
binding.channel_index = channel_index;
binding.object_role = object_role;
binding.main_group = main_group;
binding.middle_group = static_cast<uint8_t>((object_number >> 8) & 0x07);
binding.sub_group = static_cast<uint8_t>(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<GatewayKnxDaliBinding> 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<int>(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<int>(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<GatewayKnxDaliBinding> 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<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() const {
std::vector<GatewayKnxDaliBinding> bindings;
std::set<uint16_t> 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<uint16_t>(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<uint16_t>(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<GatewayKnxDaliBinding> 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<GatewayKnxDaliBinding> 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<uint8_t>((group_address >> 11) & 0x1f);
const uint8_t middle = static_cast<uint8_t>((group_address >> 8) & 0x07);
const uint8_t sub = static_cast<uint8_t>(group_address & 0xff);
if (main != config_.main_group) {
return ErrorResult(group_address, "KNX main group does not match gateway config");
}
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
const uint16_t object_number = static_cast<uint16_t>((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<GatewayKnxDaliBinding>& 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<int>(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<int>(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,
+63
View File
@@ -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)
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <stdint.h>
#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);
@@ -0,0 +1,59 @@
#pragma once
#include "knx/platform.h"
#include "esp_netif.h"
#include "lwip/sockets.h"
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
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<uint8_t> eeprom_;
std::string nvs_namespace_;
bool eeprom_loaded_{false};
};
} // namespace gateway::openknx
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace gateway::openknx {
struct EtsAssociation {
uint16_t group_address{0};
uint16_t group_object_number{0};
};
struct EtsMemorySnapshot {
bool configured{false};
std::vector<EtsAssociation> associations;
};
EtsMemorySnapshot LoadEtsMemorySnapshot(const std::string& nvs_namespace);
} // namespace gateway::openknx
@@ -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<EspIdfPlatform, Bau07B0>;
} // namespace gateway::openknx
@@ -0,0 +1,41 @@
#pragma once
#include "TPUart/Interface/Abstract.h"
#include "driver/uart.h"
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <functional>
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<bool()> 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<bool()> callback_;
};
} // namespace gateway::openknx
@@ -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 <array>
#include <cstdio>
namespace {
std::array<voidFuncPtr, GPIO_NUM_MAX> g_gpio_callbacks{};
bool g_isr_service_installed = false;
void IRAM_ATTR gpioIsrThunk(void* arg) {
const auto pin = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(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<unsigned long long>(value));
} else {
std::printf("%lld", value);
}
}
} // namespace
uint32_t millis() { return static_cast<uint32_t>(esp_timer_get_time() / 1000ULL); }
uint32_t micros() { return static_cast<uint32_t>(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<gpio_num_t>(pin), value == LOW ? 0 : 1);
}
}
uint32_t digitalRead(uint32_t pin) {
if (pin >= GPIO_NUM_MAX) {
return LOW;
}
return gpio_get_level(static_cast<gpio_num_t>(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<gpio_num_t>(pin), toGpioInterrupt(mode));
gpio_isr_handler_remove(static_cast<gpio_num_t>(pin));
g_gpio_callbacks[pin] = callback;
if (callback != nullptr) {
gpio_isr_handler_add(static_cast<gpio_num_t>(pin), gpioIsrThunk,
reinterpret_cast<void*>(static_cast<uintptr_t>(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"); }
@@ -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 <algorithm>
#include <cerrno>
#include <cstring>
#include <unistd.h>
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<uint32_t>(mac[0]) << 24) | (static_cast<uint32_t>(mac[1]) << 16) |
(static_cast<uint32_t>(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<sockaddr*>(&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<sockaddr*>(&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<sockaddr*>(&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<sockaddr*>(&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<uint8_t> 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
@@ -0,0 +1,50 @@
#include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/esp_idf_platform.h"
#include "knx/bau07B0.h"
#include <algorithm>
namespace gateway::openknx {
namespace {
void CollectAssociation(uint16_t group_address, uint16_t group_object_number,
void* context) {
auto* associations = static_cast<std::vector<EtsAssociation>*>(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
@@ -0,0 +1,114 @@
#include "openknx_idf/tpuart_uart_interface.h"
#include "esp_log.h"
#include <utility>
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<bool()> callback) {
callback_ = std::move(callback);
}
} // namespace gateway::openknx
+1 -1
Submodule knx updated: 7124a6435d...b747f6284d
Submodule
+1
Submodule knx_dali_gw added at 5cd7e66bf0
Submodule
+1
Submodule tpuart added at f8c01e6a32