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