Refactor GatewayModbus and GatewayNetwork components

- Updated GatewayModbusConfig to allow uart_port and pin values to be -1, indicating an unconfigured state.
- Enhanced GatewayNetworkService to support an additional setup AP button with configurable GPIO and active low settings.
- Refactored boot button configuration logic to reduce redundancy and improve clarity.
- Introduced a new method for handling GPIO input configuration.
- Improved boot button task loop to handle both boot and setup AP buttons more effectively.
- Added programming mode functionality to EtsDeviceRuntime, allowing toggling and querying of the programming state.
- Implemented memory checks to avoid unnecessary reads in EtsDeviceRuntime.
- Enhanced security storage to derive factory FDSK from the device's serial number and store it in NVS.
- Updated factory FDSK loading logic to ensure proper key generation and storage.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-13 12:36:16 +08:00
parent df1dd472cc
commit b74367e5a0
18 changed files with 2244 additions and 609 deletions
+3 -2
View File
@@ -290,8 +290,9 @@ esp_err_t DaliDomainService::bindSerialBus(const DaliSerialBusConfig& config) {
ESP_LOGE(kTag, "failed to configure uart%d: %s", config.uart_port, esp_err_to_name(err));
return err;
}
err = uart_set_pin(uart, config.tx_pin, config.rx_pin, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
err = uart_set_pin(uart, config.tx_pin < 0 ? UART_PIN_NO_CHANGE : config.tx_pin,
config.rx_pin < 0 ? UART_PIN_NO_CHANGE : config.rx_pin,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to set uart%d pins tx=%d rx=%d: %s", config.uart_port,
config.tx_pin, config.rx_pin, esp_err_to_name(err));
@@ -1,5 +1,6 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
@@ -37,7 +38,7 @@ struct GatewayBridgeServiceConfig {
std::vector<int> reserved_uart_ports;
uint32_t bacnet_task_stack_size{8192};
UBaseType_t bacnet_task_priority{5};
uint32_t knx_task_stack_size{8192};
uint32_t knx_task_stack_size{12288};
UBaseType_t knx_task_priority{5};
std::optional<GatewayKnxConfig> default_knx_config;
};
@@ -66,6 +67,14 @@ class GatewayBridgeService {
ChannelRuntime* findRuntime(uint8_t gateway_id);
const ChannelRuntime* findRuntime(uint8_t gateway_id) const;
ChannelRuntime* selectKnxEndpointRuntime();
bool isKnxEndpointRuntime(const ChannelRuntime* runtime) const;
esp_err_t startKnxEndpoint(ChannelRuntime* requested_runtime,
std::set<int>* used_uarts = nullptr);
esp_err_t stopKnxEndpoint(ChannelRuntime* requested_runtime);
DaliBridgeResult routeKnxCemiFrame(const uint8_t* data, size_t len);
DaliBridgeResult routeKnxGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len);
void handleDaliRawFrame(const DaliRawFrame& frame);
void collectUsedRuntimeResources(uint8_t except_gateway_id,
std::set<uint16_t>* modbus_tcp_ports,
@@ -76,6 +85,7 @@ class GatewayBridgeService {
GatewayCache& cache_;
GatewayBridgeServiceConfig config_;
std::vector<std::unique_ptr<ChannelRuntime>> runtimes_;
ChannelRuntime* knx_endpoint_runtime_{nullptr};
};
} // namespace gateway
+275 -93
View File
@@ -15,7 +15,6 @@
#include "gateway_knx.hpp"
#include "gateway_modbus.hpp"
#include "gateway_provisioning.hpp"
#include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/security_storage.h"
#include "cJSON.h"
@@ -1296,12 +1295,20 @@ uart_stop_bits_t UartStopBits(int bits) {
} // namespace
struct GatewayBridgeService::ChannelRuntime {
explicit ChannelRuntime(DaliDomainService& domain, GatewayCache& cache, DaliChannelInfo channel,
explicit ChannelRuntime(GatewayBridgeService& service, DaliDomainService& domain,
GatewayCache& cache, DaliChannelInfo channel,
GatewayBridgeServiceConfig service_config)
: domain(domain), cache(cache), channel(std::move(channel)), service_config(service_config),
: service(service),
domain(domain),
cache(cache),
channel(std::move(channel)),
service_config(service_config),
lock(xSemaphoreCreateRecursiveMutex()) {}
~ChannelRuntime() {
if (knx_router != nullptr) {
knx_router->stop();
}
if (cloud != nullptr) {
cloud->stop();
}
@@ -1311,6 +1318,7 @@ struct GatewayBridgeService::ChannelRuntime {
}
}
GatewayBridgeService& service;
DaliDomainService& domain;
GatewayCache& cache;
DaliChannelInfo channel;
@@ -1427,18 +1435,17 @@ struct GatewayBridgeService::ChannelRuntime {
knx = std::make_unique<GatewayKnxBridge>(*engine);
knx_router = std::make_unique<GatewayKnxTpIpRouter>(
*knx, [this](const uint8_t* data, size_t len) {
LockGuard guard(lock);
if (knx == nullptr) {
DaliBridgeResult result;
result.error = "KNX bridge is not ready";
return result;
}
return knx->handleCemiFrame(data, len);
return service.routeKnxCemiFrame(data, len);
},
openKnxNamespace());
if (knx_config.has_value()) {
knx->setConfig(knx_config.value());
knx_router->setConfig(knx_config.value());
knx_router->setGroupWriteHandler(
[this](uint16_t group_address, const uint8_t* data, size_t len) {
return service.routeKnxGroupWrite(group_address, data, len);
});
if (const auto active_knx = activeKnxConfigLocked(); active_knx.has_value()) {
knx->setConfig(active_knx.value());
knx_router->setConfig(active_knx.value());
knx_router->setCommissioningOnly(!knx_config.has_value());
}
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
@@ -1459,33 +1466,8 @@ struct GatewayBridgeService::ChannelRuntime {
}
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());
const bool has_downloaded_address = snapshot.individual_address != 0 &&
snapshot.individual_address != 0xffff;
if (!snapshot.configured && !has_downloaded_address && snapshot.associations.empty()) {
return;
}
GatewayKnxConfig updated = active_config.value();
if (has_downloaded_address) {
updated.individual_address = snapshot.individual_address;
}
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 OpenKNX ETS address=0x%04x associations=%u from NVS namespace %s",
channel.gateway_id, snapshot.individual_address,
static_cast<unsigned>(snapshot.associations.size()), openKnxNamespace().c_str());
// Bau07B0/OpenKNX memory restore is stack-heavy and owns TP-UART internals;
// the live KNX task restores and syncs ETS associations after startup.
}
std::optional<DaliDomainSnapshot> diagnosticSnapshotLocked(int short_address,
@@ -2056,25 +2038,41 @@ struct GatewayBridgeService::ChannelRuntime {
knx_last_error = validation_error;
return validation_err;
}
if (config->ip_router_enabled && used_ports != nullptr) {
if (used_ports->find(config->udp_port) != used_ports->end()) {
knx_last_error = "duplicate KNXnet/IP UDP port " + std::to_string(config->udp_port);
GatewayKnxConfig runtime_config = config.value();
if (runtime_config.ip_router_enabled && used_ports != nullptr) {
if (used_ports->find(runtime_config.udp_port) != used_ports->end()) {
knx_last_error = "KNXnet/IP UDP port " + std::to_string(runtime_config.udp_port) +
" is already owned by another runtime";
ESP_LOGW(kTag, "gateway=%u skips duplicate KNXnet/IP UDP port %u",
channel.gateway_id, static_cast<unsigned>(runtime_config.udp_port));
return ESP_ERR_INVALID_STATE;
}
used_ports->insert(config->udp_port);
used_ports->insert(runtime_config.udp_port);
}
if (config->ip_router_enabled && used_uarts != nullptr) {
const int uart_port = config->tp_uart.uart_port;
if (GatewayKnxConfigUsesTpUart(runtime_config) && used_uarts != nullptr) {
const int uart_port = runtime_config.tp_uart.uart_port;
if (used_uarts->find(uart_port) != used_uarts->end()) {
knx_last_error = "KNX TP-UART UART" + std::to_string(uart_port) +
" is already used by another runtime";
return ESP_ERR_INVALID_STATE;
ESP_LOGW(kTag,
"gateway=%u KNX TP-UART UART%d is already owned by another runtime; "
"starting this KNX/IP runtime without opening a second UART driver",
channel.gateway_id, uart_port);
runtime_config.tp_uart.uart_port = -1;
}
used_uarts->insert(uart_port);
}
knx->setConfig(config.value());
knx_router->setConfig(config.value());
if (!config->ip_router_enabled) {
const bool commissioning_only = !knx_config.has_value();
ESP_LOGI(kTag,
"gateway=%u KNX/IP start config namespace=%s storedConfig=%d udp=%u tunnel=%d "
"multicast=%d multicastGroup=%s mainGroup=%u tpUart=%d tx=%d rx=%d individual=0x%04x",
channel.gateway_id, openKnxNamespace().c_str(), !commissioning_only,
static_cast<unsigned>(runtime_config.udp_port), runtime_config.tunnel_enabled,
runtime_config.multicast_enabled, runtime_config.multicast_address.c_str(),
static_cast<unsigned>(runtime_config.main_group), runtime_config.tp_uart.uart_port,
runtime_config.tp_uart.tx_pin, runtime_config.tp_uart.rx_pin,
runtime_config.individual_address);
knx->setConfig(runtime_config);
knx_router->setConfig(runtime_config);
knx_router->setCommissioningOnly(commissioning_only);
if (!runtime_config.ip_router_enabled) {
knx_started = false;
return ESP_ERR_NOT_SUPPORTED;
}
@@ -2084,8 +2082,18 @@ struct GatewayBridgeService::ChannelRuntime {
knx_started = err == ESP_OK;
if (err != ESP_OK) {
knx_last_error = knx_router->lastError().empty()
? "failed to start KNX TP-UART router"
? "failed to start KNXnet/IP router"
: knx_router->lastError();
ESP_LOGW(kTag, "gateway=%u KNX/IP start failed err=%s(%d) detail=%s",
channel.gateway_id, esp_err_to_name(err), static_cast<int>(err),
knx_last_error.c_str());
} else {
if (knx_router->tpUartOnline() && used_uarts != nullptr) {
used_uarts->insert(runtime_config.tp_uart.uart_port);
}
ESP_LOGI(kTag, "gateway=%u KNX/IP started namespace=%s udp=%u detail=%s",
channel.gateway_id, openKnxNamespace().c_str(),
static_cast<unsigned>(runtime_config.udp_port), knx_router->lastError().c_str());
}
return err;
}
@@ -3014,12 +3022,15 @@ struct GatewayBridgeService::ChannelRuntime {
const std::optional<GatewayKnxConfig>& candidate_knx,
std::string* error_message = nullptr) const {
const int uart_port = config.serial.uart_port;
if (uart_port < 0 || uart_port > 2) {
if (uart_port < -1 || uart_port > 2) {
if (error_message != nullptr) {
*error_message = "Modbus serial UART port must be 0, 1, or 2";
*error_message = "Modbus serial UART port must be -1, 0, 1, or 2";
}
return ESP_ERR_INVALID_ARG;
}
if (uart_port < 0) {
return ESP_OK;
}
if (uart_port == 0 && !service_config.allow_modbus_uart0) {
if (error_message != nullptr) {
*error_message =
@@ -3035,7 +3046,8 @@ struct GatewayBridgeService::ChannelRuntime {
return ESP_ERR_INVALID_STATE;
}
if (service_config.knx_enabled && candidate_knx.has_value() &&
candidate_knx->ip_router_enabled && candidate_knx->tp_uart.uart_port == uart_port) {
GatewayKnxConfigUsesTpUart(candidate_knx.value()) &&
candidate_knx->tp_uart.uart_port == uart_port) {
if (error_message != nullptr) {
*error_message = "Modbus serial UART" + std::to_string(uart_port) +
" conflicts with KNX TP-UART; choose another free UART for RS485";
@@ -3048,16 +3060,16 @@ struct GatewayBridgeService::ChannelRuntime {
esp_err_t validateKnxConfigLocked(const GatewayKnxConfig& config,
const std::optional<GatewayModbusConfig>& candidate_modbus,
std::string* error_message = nullptr) const {
if (!config.ip_router_enabled) {
return ESP_OK;
}
const int uart_port = config.tp_uart.uart_port;
if (uart_port < 0 || uart_port > 2) {
if (config.tp_uart.uart_port < -1 || config.tp_uart.uart_port > 2) {
if (error_message != nullptr) {
*error_message = "KNX TP-UART port must be 0, 1, or 2";
*error_message = "KNX TP-UART port must be -1, 0, 1, or 2";
}
return ESP_ERR_INVALID_ARG;
}
if (!config.ip_router_enabled || !GatewayKnxConfigUsesTpUart(config)) {
return ESP_OK;
}
const int uart_port = config.tp_uart.uart_port;
if (uart_port == 0 && !service_config.allow_knx_uart0) {
if (error_message != nullptr) {
*error_message =
@@ -3144,7 +3156,18 @@ struct GatewayBridgeService::ChannelRuntime {
if (knx_config.has_value()) {
return knx_config;
}
return service_config.default_knx_config;
if (!service_config.default_knx_config.has_value()) {
return std::nullopt;
}
GatewayKnxConfig config = service_config.default_knx_config.value();
const uint8_t channel_index = channel.channel_index;
config.main_group = static_cast<uint8_t>(std::min<int>(31, config.main_group + channel_index));
const uint16_t device = config.individual_address & 0x00ff;
if (device > 0 && device + channel_index <= 0x00ff) {
config.individual_address = static_cast<uint16_t>((config.individual_address & 0xff00) |
(device + channel_index));
}
return config;
}
esp_err_t saveKnxConfig(const GatewayKnxConfig& config,
@@ -3165,12 +3188,7 @@ struct GatewayBridgeService::ChannelRuntime {
return validation_err;
}
const bool restart_router = knx_started || (knx_router != nullptr && knx_router->started());
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 && merged_config.ip_router_enabled && used_uarts != nullptr &&
if (restart_router && GatewayKnxConfigUsesTpUart(merged_config) && 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";
@@ -3195,6 +3213,7 @@ struct GatewayBridgeService::ChannelRuntime {
}
if (knx_router != nullptr) {
knx_router->setConfig(merged_config);
knx_router->setCommissioningOnly(false);
}
if (restart_router) {
return startKnx(used_ports, used_uarts);
@@ -3350,6 +3369,10 @@ struct GatewayBridgeService::ChannelRuntime {
return ESP_ERR_NOT_FOUND;
}
if (GatewayModbusTransportIsSerial(config->transport)) {
if (config->serial.uart_port < 0) {
modbus_last_error = "Modbus serial UART disabled";
return ESP_ERR_NOT_SUPPORTED;
}
std::string validation_error;
const esp_err_t serial_err = validateSerialModbusConfigLocked(
config.value(), activeKnxConfigLocked(), &validation_error);
@@ -3532,8 +3555,10 @@ struct GatewayBridgeService::ChannelRuntime {
}
const int rts_pin = config.serial.rs485.enabled ? config.serial.rs485.de_pin
: UART_PIN_NO_CHANGE;
err = uart_set_pin(uart_port, config.serial.tx_pin, config.serial.rx_pin, rts_pin,
UART_PIN_NO_CHANGE);
err = uart_set_pin(uart_port,
config.serial.tx_pin < 0 ? UART_PIN_NO_CHANGE : config.serial.tx_pin,
config.serial.rx_pin < 0 ? UART_PIN_NO_CHANGE : config.serial.rx_pin,
rts_pin < 0 ? UART_PIN_NO_CHANGE : rts_pin, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
return err;
}
@@ -3794,7 +3819,7 @@ esp_err_t GatewayBridgeService::start() {
const auto channels = dali_domain_.channelInfo();
runtimes_.reserve(channels.size());
for (const auto& channel : channels) {
auto runtime = std::make_unique<ChannelRuntime>(dali_domain_, cache_, channel, config_);
auto runtime = std::make_unique<ChannelRuntime>(*this, dali_domain_, cache_, channel, config_);
const esp_err_t err = runtime->start();
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to start bridge runtime gateway=%u: %s", channel.gateway_id,
@@ -3820,13 +3845,18 @@ esp_err_t GatewayBridgeService::start() {
}
if (config_.knx_enabled && config_.knx_startup_enabled) {
std::set<uint16_t> used_knx_ports;
for (const auto& runtime : runtimes_) {
const esp_err_t err = runtime->startKnx(&used_knx_ports, &used_serial_uarts);
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(kTag, "gateway=%u KNX/IP startup skipped: %s", runtime->channel.gateway_id,
esp_err_to_name(err));
}
ChannelRuntime* owner = selectKnxEndpointRuntime();
const esp_err_t err = startKnxEndpoint(owner, &used_serial_uarts);
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
const char* detail = owner == nullptr
? "no KNX endpoint owner"
: (!owner->knx_last_error.empty()
? owner->knx_last_error.c_str()
: (owner->knx_router == nullptr
? "router unavailable"
: owner->knx_router->lastError().c_str()));
ESP_LOGW(kTag, "KNX/IP startup skipped: %s(%d) detail=%s",
esp_err_to_name(err), static_cast<int>(err), detail);
}
}
@@ -3863,20 +3893,170 @@ const GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(
return nullptr;
}
GatewayBridgeService::ChannelRuntime* GatewayBridgeService::selectKnxEndpointRuntime() {
auto eligible = [](ChannelRuntime* runtime) {
if (runtime == nullptr) {
return false;
}
LockGuard guard(runtime->lock);
const auto config = runtime->activeKnxConfigLocked();
return config.has_value() && config->ip_router_enabled;
};
if (eligible(knx_endpoint_runtime_)) {
return knx_endpoint_runtime_;
}
ChannelRuntime* selected = nullptr;
for (const auto& runtime : runtimes_) {
if (!eligible(runtime.get())) {
continue;
}
if (selected == nullptr || runtime->channel.channel_index < selected->channel.channel_index) {
selected = runtime.get();
}
}
knx_endpoint_runtime_ = selected;
if (selected != nullptr) {
ESP_LOGI(kTag, "gateway=%u owns shared KNXnet/IP endpoint", selected->channel.gateway_id);
}
return selected;
}
bool GatewayBridgeService::isKnxEndpointRuntime(const ChannelRuntime* runtime) const {
return runtime != nullptr && runtime == knx_endpoint_runtime_;
}
esp_err_t GatewayBridgeService::startKnxEndpoint(ChannelRuntime* requested_runtime,
std::set<int>* used_uarts) {
if (!config_.knx_enabled) {
return ESP_ERR_NOT_SUPPORTED;
}
ChannelRuntime* owner = selectKnxEndpointRuntime();
if (owner == nullptr) {
return ESP_ERR_NOT_FOUND;
}
if (requested_runtime != nullptr && requested_runtime != owner) {
ESP_LOGI(kTag, "gateway=%u requested KNX start; shared endpoint remains owned by gateway=%u",
requested_runtime->channel.gateway_id, owner->channel.gateway_id);
}
if (used_uarts != nullptr) {
LockGuard guard(owner->lock);
const auto owner_config = owner->activeKnxConfigLocked();
if (owner_config.has_value() && GatewayKnxConfigUsesTpUart(owner_config.value())) {
used_uarts->erase(owner_config->tp_uart.uart_port);
}
}
std::set<uint16_t> used_knx_ports;
for (const auto& runtime : runtimes_) {
if (runtime.get() == owner) {
continue;
}
LockGuard guard(runtime->lock);
if (runtime->knx_started ||
(runtime->knx_router != nullptr && runtime->knx_router->started())) {
const auto config = runtime->activeKnxConfigLocked();
if (config.has_value()) {
used_knx_ports.insert(config->udp_port);
}
}
}
return owner->startKnx(&used_knx_ports, used_uarts);
}
esp_err_t GatewayBridgeService::stopKnxEndpoint(ChannelRuntime* requested_runtime) {
ChannelRuntime* owner = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_
: selectKnxEndpointRuntime();
if (owner == nullptr) {
return ESP_ERR_NOT_FOUND;
}
if (requested_runtime != nullptr && requested_runtime != owner) {
ESP_LOGI(kTag, "gateway=%u requested KNX stop; stopping shared endpoint owned by gateway=%u",
requested_runtime->channel.gateway_id, owner->channel.gateway_id);
}
return owner->stopKnx();
}
DaliBridgeResult GatewayBridgeService::routeKnxCemiFrame(const uint8_t* data, size_t len) {
std::vector<ChannelRuntime*> matches;
for (const auto& runtime : runtimes_) {
LockGuard guard(runtime->lock);
if (runtime->knx != nullptr && runtime->knx->matchesCemiFrame(data, len)) {
matches.push_back(runtime.get());
}
}
if (matches.empty()) {
DaliBridgeResult result;
result.error = "No DALI bridge mapping matched KNX cEMI group write";
return result;
}
if (matches.size() > 1) {
DaliBridgeResult result;
result.error = "KNX cEMI group write matched multiple DALI bridge channels";
ESP_LOGW(kTag, "%s", result.error.c_str());
return result;
}
ChannelRuntime* runtime = matches.front();
LockGuard guard(runtime->lock);
if (runtime->knx == nullptr || !runtime->knx->matchesCemiFrame(data, len)) {
DaliBridgeResult result;
result.error = "DALI bridge mapping changed before KNX cEMI dispatch";
return result;
}
return runtime->knx->handleCemiFrame(data, len);
}
DaliBridgeResult GatewayBridgeService::routeKnxGroupWrite(uint16_t group_address,
const uint8_t* data, size_t len) {
std::vector<ChannelRuntime*> matches;
for (const auto& runtime : runtimes_) {
LockGuard guard(runtime->lock);
if (runtime->knx != nullptr && runtime->knx->matchesGroupAddress(group_address)) {
matches.push_back(runtime.get());
}
}
if (matches.empty()) {
DaliBridgeResult result;
result.error = "No DALI bridge mapping matched KNX group " +
GatewayKnxGroupAddressString(group_address);
return result;
}
if (matches.size() > 1) {
DaliBridgeResult result;
result.error = "KNX group " + GatewayKnxGroupAddressString(group_address) +
" matched multiple DALI bridge channels";
ESP_LOGW(kTag, "%s", result.error.c_str());
return result;
}
ChannelRuntime* runtime = matches.front();
LockGuard guard(runtime->lock);
if (runtime->knx == nullptr || !runtime->knx->matchesGroupAddress(group_address)) {
DaliBridgeResult result;
result.error = "DALI bridge mapping changed before KNX group dispatch";
return result;
}
return runtime->knx->handleGroupWrite(group_address, data, len);
}
void GatewayBridgeService::handleDaliRawFrame(const DaliRawFrame& frame) {
const auto update = DecodeDaliKnxStatusUpdate(frame);
if (!update.has_value()) {
return;
}
auto* runtime = findRuntime(frame.gateway_id);
if (runtime == nullptr) {
auto* owner = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_
: selectKnxEndpointRuntime();
if (owner == nullptr || owner->channel.gateway_id != frame.gateway_id) {
return;
}
LockGuard guard(runtime->lock);
if (!runtime->knx_started || runtime->knx_router == nullptr) {
LockGuard guard(owner->lock);
if (!owner->knx_started || owner->knx_router == nullptr) {
return;
}
runtime->knx_router->publishDaliStatus(update->target, update->actual_level);
owner->knx_router->publishDaliStatus(update->target, update->actual_level);
}
void GatewayBridgeService::collectUsedRuntimeResources(
@@ -3909,7 +4089,8 @@ void GatewayBridgeService::collectUsedRuntimeResources(
if (knx_udp_ports != nullptr) {
knx_udp_ports->insert(knx_config->udp_port);
}
if (serial_uarts != nullptr) {
if (serial_uarts != nullptr && runtime->knx_router != nullptr &&
runtime->knx_router->tpUartOnline()) {
serial_uarts->insert(knx_config->tp_uart.uart_port);
}
}
@@ -4214,20 +4395,21 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
return handleGet("knx", gateway_id.value());
}
if (action == "knx_start") {
std::set<uint16_t> used_knx_ports;
ChannelRuntime* owner = selectKnxEndpointRuntime();
std::set<int> used_serial_uarts;
collectUsedRuntimeResources(gateway_id.value(), nullptr, &used_knx_ports,
&used_serial_uarts);
const esp_err_t err = runtime->startKnx(&used_knx_ports, &used_serial_uarts);
collectUsedRuntimeResources(owner == nullptr ? gateway_id.value() : owner->channel.gateway_id,
nullptr, nullptr, &used_serial_uarts);
const esp_err_t err = startKnxEndpoint(runtime, &used_serial_uarts);
if (err != ESP_OK) {
return ErrorResponse(err, runtime->knx_last_error.empty()
auto* owner = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_ : runtime;
return ErrorResponse(err, owner->knx_last_error.empty()
? "failed to start KNX/IP bridge"
: runtime->knx_last_error.c_str());
: owner->knx_last_error.c_str());
}
return handleGet("knx", gateway_id.value());
}
if (action == "knx_stop") {
const esp_err_t err = runtime->stopKnx();
const esp_err_t err = stopKnxEndpoint(runtime);
if (err != ESP_OK) {
return ErrorResponse(err, "failed to stop KNX/IP bridge");
}
+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 openknx_idf
REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip openknx_idf
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
+83 -7
View File
@@ -10,12 +10,14 @@
#include "lwip/sockets.h"
#include <atomic>
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
@@ -62,6 +64,10 @@ struct GatewayKnxConfig {
uint16_t udp_port{kGatewayKnxDefaultUdpPort};
std::string multicast_address{kGatewayKnxDefaultMulticastAddress};
uint16_t individual_address{0x1101};
int programming_button_gpio{-1};
bool programming_button_active_low{true};
int programming_led_gpio{-1};
bool programming_led_active_high{true};
std::vector<GatewayKnxEtsAssociation> ets_associations;
GatewayKnxTpUartConfig tp_uart;
};
@@ -111,6 +117,7 @@ struct GatewayKnxCommissioningBallast {
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config);
const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode);
GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value);
@@ -132,6 +139,8 @@ class GatewayKnxBridge {
size_t etsBindingCount() const;
std::vector<GatewayKnxDaliBinding> describeDaliBindings() const;
bool matchesCemiFrame(const uint8_t* data, size_t len) const;
bool matchesGroupAddress(uint16_t group_address) const;
DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len);
DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len);
@@ -186,13 +195,19 @@ class GatewayKnxBridge {
class GatewayKnxTpIpRouter {
public:
using CemiFrameHandler = std::function<DaliBridgeResult(const uint8_t* data, size_t len)>;
using GroupWriteHandler = std::function<DaliBridgeResult(uint16_t group_address,
const uint8_t* data,
size_t len)>;
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler,
std::string openknx_namespace = "openknx");
~GatewayKnxTpIpRouter();
void setConfig(const GatewayKnxConfig& config);
void setCommissioningOnly(bool enabled);
void setGroupWriteHandler(GroupWriteHandler handler);
const GatewayKnxConfig& config() const;
bool tpUartOnline() const;
esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority);
esp_err_t stop();
@@ -201,17 +216,40 @@ class GatewayKnxTpIpRouter {
bool publishDaliStatus(const GatewayKnxDaliTarget& target, uint8_t actual_level);
private:
static constexpr size_t kMaxTunnelClients = 16;
struct TunnelClient {
bool connected{false};
uint8_t channel_id{0};
uint8_t connection_type{0};
uint8_t received_sequence{255};
uint8_t send_sequence{0};
uint16_t individual_address{0};
TickType_t last_activity_tick{0};
::sockaddr_in control_remote{};
::sockaddr_in data_remote{};
};
static void TaskEntry(void* arg);
esp_err_t initializeRuntime();
void taskLoop();
void finishTask();
void closeSockets();
bool configureSocket();
bool configureTpUart();
bool initializeTpUart();
bool configureProgrammingGpio();
void refreshNetworkInterfaces(bool force_log = false);
void handleUdpDatagram(const uint8_t* data, size_t len, const ::sockaddr_in& remote);
void handleSearchRequest(uint16_t service, const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleDescriptionRequest(const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleRoutingIndication(const uint8_t* body, size_t len);
void handleTunnellingRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
void handleDeviceConfigurationRequest(const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleConnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
void handleConnectionStateRequest(const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
@@ -220,47 +258,85 @@ class GatewayKnxTpIpRouter {
const ::sockaddr_in& remote);
void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
const ::sockaddr_in& remote);
void sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
const ::sockaddr_in& remote);
void sendConnectionHeaderAck(uint16_t service, uint8_t channel_id, uint8_t sequence,
uint8_t status, const ::sockaddr_in& remote);
void sendSecureSessionStatus(uint8_t status, const ::sockaddr_in& remote);
void sendTunnelIndication(const uint8_t* data, size_t len);
void sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, size_t len);
void sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
void sendDisconnectResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
void sendConnectResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
const ::sockaddr_in& remote, uint8_t connection_type,
uint16_t tunnel_address);
void sendRoutingIndication(const uint8_t* data, size_t len);
bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len);
void sendSearchResponse(uint16_t service, const ::sockaddr_in& remote,
const std::set<uint8_t>& requested_dibs = {});
void sendDescriptionResponse(const ::sockaddr_in& remote);
std::array<uint8_t, 8> localHpaiForRemote(const ::sockaddr_in& remote) const;
std::vector<uint8_t> buildDeviceInfoDib(const ::sockaddr_in& remote) const;
std::vector<uint8_t> buildSupportedServiceDib() const;
std::vector<uint8_t> buildExtendedDeviceInfoDib() const;
std::vector<uint8_t> buildIpConfigDib(const ::sockaddr_in& remote, bool current) const;
std::vector<uint8_t> buildKnxAddressesDib() const;
std::vector<uint8_t> buildTunnelingInfoDib() const;
TunnelClient* findTunnelClient(uint8_t channel_id);
const TunnelClient* findTunnelClient(uint8_t channel_id) const;
TunnelClient* allocateTunnelClient(const ::sockaddr_in& control_remote,
const ::sockaddr_in& data_remote,
uint8_t connection_type);
void resetTunnelClient(TunnelClient& client);
uint8_t nextTunnelChannelId() const;
uint16_t effectiveTunnelAddressForSlot(size_t slot) const;
void pruneStaleTunnelClients();
bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len,
TunnelClient* response_client);
bool handleOpenKnxBusFrame(const uint8_t* data, size_t len);
void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote);
bool emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len);
bool shouldRouteDaliApplicationFrames() const;
void syncOpenKnxConfigFromDevice();
uint16_t effectiveIndividualAddress() const;
uint16_t effectiveTunnelAddress() const;
void pollTpUart();
void pollProgrammingButton();
void updateProgrammingLed();
void setProgrammingLed(bool on);
void handleTpUartControlByte(uint8_t byte);
void handleTpTelegram(const uint8_t* data, size_t len);
void forwardCemiToTp(const uint8_t* data, size_t len);
GatewayKnxBridge& bridge_;
CemiFrameHandler handler_;
GroupWriteHandler group_write_handler_;
std::string openknx_namespace_;
GatewayKnxConfig config_;
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
TaskHandle_t task_handle_{nullptr};
SemaphoreHandle_t openknx_lock_{nullptr};
SemaphoreHandle_t startup_semaphore_{nullptr};
esp_err_t startup_result_{ESP_OK};
std::atomic_bool stop_requested_{false};
std::atomic_bool started_{false};
int udp_sock_{-1};
int tp_uart_port_{-1};
uint8_t tunnel_channel_id_{1};
uint8_t expected_tunnel_sequence_{0};
uint8_t tunnel_send_sequence_{0};
bool tunnel_connected_{false};
::sockaddr_in tunnel_remote_{};
std::vector<uint32_t> multicast_joined_interfaces_;
TickType_t network_refresh_tick_{0};
std::array<TunnelClient, kMaxTunnelClients> tunnel_clients_{};
uint8_t last_tunnel_channel_id_{0};
std::vector<uint8_t> tp_rx_frame_;
std::vector<uint8_t> tp_last_sent_telegram_;
TickType_t tp_uart_last_byte_tick_{0};
bool tp_uart_extended_frame_{false};
bool tp_uart_online_{false};
bool commissioning_only_{false};
std::atomic_bool openknx_configured_{false};
bool programming_button_last_pressed_{false};
bool programming_led_state_{false};
TickType_t programming_button_last_toggle_tick_{0};
std::string last_error_;
};
File diff suppressed because it is too large Load Diff
@@ -560,7 +560,7 @@ std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue*
getObjectInt(json, "unitId").value_or(getObjectInt(json, "unit_id").value_or(1))));
if (const auto* serial_value = getObjectValue(json, "serial")) {
if (const auto* serial = serial_value->asObject()) {
config.serial.uart_port = clampedInt(*serial, "uartPort", config.serial.uart_port, 0, 2);
config.serial.uart_port = clampedInt(*serial, "uartPort", config.serial.uart_port, -1, 2);
config.serial.tx_pin = clampedInt(*serial, "txPin", config.serial.tx_pin, -1, 48);
config.serial.rx_pin = clampedInt(*serial, "rxPin", config.serial.rx_pin, -1, 48);
config.serial.baudrate = clampedU32(*serial, "baudrate", config.serial.baudrate,
@@ -56,8 +56,10 @@ struct GatewayNetworkServiceConfig {
bool status_led_active_high{true};
int boot_button_gpio{-1};
bool boot_button_active_low{true};
int setup_ap_button_gpio{-1};
bool setup_ap_button_active_low{true};
uint32_t boot_button_long_press_ms{3000};
uint32_t boot_button_task_stack_size{2048};
uint32_t boot_button_task_stack_size{8192};
UBaseType_t boot_button_task_priority{2};
uint32_t udp_task_stack_size{4096};
UBaseType_t udp_task_priority{4};
@@ -909,22 +909,37 @@ esp_err_t GatewayNetworkService::configureStatusLed() {
}
esp_err_t GatewayNetworkService::configureBootButton() {
if (config_.boot_button_gpio < 0) {
if (config_.boot_button_gpio < 0 && config_.setup_ap_button_gpio < 0) {
return ESP_OK;
}
gpio_config_t io_config = {};
io_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(config_.boot_button_gpio);
io_config.mode = GPIO_MODE_INPUT;
io_config.pull_up_en = config_.boot_button_active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE;
io_config.pull_down_en = config_.boot_button_active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE;
io_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&io_config);
const auto configure_input = [](int gpio, bool active_low, const char* name) -> esp_err_t {
if (gpio < 0) {
return ESP_OK;
}
gpio_config_t io_config = {};
io_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(gpio);
io_config.mode = GPIO_MODE_INPUT;
io_config.pull_up_en = active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE;
io_config.pull_down_en = active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE;
io_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&io_config);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to configure %s GPIO%d: %s", name, gpio, esp_err_to_name(err));
}
return err;
};
esp_err_t err = configure_input(config_.boot_button_gpio, config_.boot_button_active_low,
"Wi-Fi reset button");
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to configure boot button GPIO%d: %s", config_.boot_button_gpio,
esp_err_to_name(err));
return err;
}
return err;
if (config_.setup_ap_button_gpio == config_.boot_button_gpio) {
return ESP_OK;
}
return configure_input(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low,
"setup AP button");
}
esp_err_t GatewayNetworkService::startHttpServer() {
@@ -985,7 +1000,8 @@ esp_err_t GatewayNetworkService::startUdpTask() {
}
esp_err_t GatewayNetworkService::startBootButtonTask() {
if (config_.boot_button_gpio < 0 || boot_button_task_handle_ != nullptr) {
if ((config_.boot_button_gpio < 0 && config_.setup_ap_button_gpio < 0) ||
boot_button_task_handle_ != nullptr) {
return ESP_OK;
}
@@ -1349,39 +1365,88 @@ void GatewayNetworkService::bootButtonTaskLoop() {
const TickType_t poll_ticks = pdMS_TO_TICKS(100);
const uint32_t long_press_ms = std::max<uint32_t>(config_.boot_button_long_press_ms, 100);
auto is_pressed = [this]() {
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.boot_button_gpio));
return config_.boot_button_active_low ? level == 0 : level != 0;
auto is_pressed = [](int gpio, bool active_low) {
if (gpio < 0) {
return false;
}
const int level = gpio_get_level(static_cast<gpio_num_t>(gpio));
return active_low ? level == 0 : level != 0;
};
while (true) {
if (!is_pressed()) {
vTaskDelay(poll_ticks);
continue;
}
auto wait_release = [&](int gpio, bool active_low) {
uint32_t pressed_ms = 0;
while (is_pressed()) {
while (is_pressed(gpio, active_low)) {
vTaskDelay(poll_ticks);
pressed_ms += 100;
}
return pressed_ms;
};
if (pressed_ms >= long_press_ms) {
ESP_LOGW(kTag, "BOOT long press clears Wi-Fi credentials and restarts");
runtime_.clearWirelessInfo();
stopEspNow();
if (wifi_started_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
auto enter_setup_ap = [this]() {
ESP_LOGI(kTag, "setup AP button enters setup AP mode");
const uint32_t stack_size = std::max<uint32_t>(config_.boot_button_task_stack_size, 8192);
const BaseType_t created = xTaskCreate(
[](void* arg) {
auto* service = static_cast<GatewayNetworkService*>(arg);
service->handleWifiControl(101);
vTaskDelete(nullptr);
},
"gateway_setup_ap", stack_size, this, config_.boot_button_task_priority, nullptr);
if (created != pdPASS) {
ESP_LOGE(kTag, "failed to create setup AP task");
}
};
auto clear_wifi_and_restart = [this]() {
ESP_LOGW(kTag, "Wi-Fi reset button clears Wi-Fi credentials and restarts");
runtime_.clearWirelessInfo();
stopEspNow();
if (wifi_started_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
}
vTaskDelay(pdMS_TO_TICKS(300));
esp_restart();
};
while (true) {
const bool same_button = config_.boot_button_gpio >= 0 &&
config_.boot_button_gpio == config_.setup_ap_button_gpio;
if (same_button && is_pressed(config_.boot_button_gpio, config_.boot_button_active_low)) {
const uint32_t pressed_ms = wait_release(config_.boot_button_gpio,
config_.boot_button_active_low);
if (pressed_ms >= long_press_ms) {
clear_wifi_and_restart();
} else {
enter_setup_ap();
}
vTaskDelay(pdMS_TO_TICKS(300));
esp_restart();
} else {
ESP_LOGI(kTag, "BOOT short press enters setup AP mode");
handleWifiControl(101);
continue;
}
vTaskDelay(pdMS_TO_TICKS(300));
if (is_pressed(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low)) {
wait_release(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low);
enter_setup_ap();
vTaskDelay(pdMS_TO_TICKS(300));
continue;
}
if (is_pressed(config_.boot_button_gpio, config_.boot_button_active_low)) {
const uint32_t pressed_ms = wait_release(config_.boot_button_gpio,
config_.boot_button_active_low);
if (pressed_ms >= long_press_ms) {
clear_wifi_and_restart();
}
vTaskDelay(pdMS_TO_TICKS(300));
continue;
}
if (config_.setup_ap_button_gpio < 0 && config_.boot_button_gpio < 0) {
vTaskDelete(nullptr);
return;
}
vTaskDelay(poll_ticks);
}
}
@@ -29,11 +29,15 @@ class EtsDeviceRuntime {
uint16_t individualAddress() const;
uint16_t tunnelClientAddress() const;
bool configured() const;
bool programmingMode() const;
void setProgrammingMode(bool enabled);
void toggleProgrammingMode();
EtsMemorySnapshot snapshot() const;
void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler);
void setGroupWriteHandler(GroupWriteHandler handler);
void setNetworkInterface(esp_netif_t* netif);
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
bool handleBusFrame(const uint8_t* data, size_t len);
@@ -20,12 +20,32 @@ 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;
bool readBaseMac(uint8_t* data) {
if (data == nullptr) {
return false;
}
if (auto* eth = esp_netif_get_handle_from_ifkey("ETH_DEF")) {
return eth;
if (esp_efuse_mac_get_default(data) == ESP_OK) {
return true;
}
return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK;
}
esp_netif_t* findDefaultNetif() {
constexpr const char* kPreferredIfKeys[] = {"ETH_DEF", "WIFI_STA_DEF", "WIFI_AP_DEF"};
for (const char* key : kPreferredIfKeys) {
auto* netif = esp_netif_get_handle_from_ifkey(key);
if (netif == nullptr || !esp_netif_is_netif_up(netif)) {
continue;
}
esp_netif_ip_info_t ip_info{};
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) {
return netif;
}
}
for (const char* key : kPreferredIfKeys) {
if (auto* netif = esp_netif_get_handle_from_ifkey(key)) {
return netif;
}
}
return nullptr;
}
@@ -103,7 +123,7 @@ void EspIdfPlatform::macAddress(uint8_t* data) {
if (data == nullptr) {
return;
}
if (esp_read_mac(data, ESP_MAC_WIFI_STA) != ESP_OK) {
if (!readBaseMac(data)) {
std::memset(data, 0, 6);
}
}
@@ -111,7 +131,7 @@ void EspIdfPlatform::macAddress(uint8_t* data) {
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) |
return (static_cast<uint32_t>(mac[2]) << 24) | (static_cast<uint32_t>(mac[3]) << 16) |
(static_cast<uint32_t>(mac[4]) << 8) | mac[5];
}
@@ -37,6 +37,13 @@ bool IsUsableIndividualAddress(uint16_t address) {
return address != 0 && address != kInvalidIndividualAddress;
}
bool IsErasedMemory(const uint8_t* data, size_t size) {
if (data == nullptr || size == 0) {
return true;
}
return std::all_of(data, data + size, [](uint8_t value) { return value == 0xff; });
}
void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) {
device.deviceObject().manufacturerId(kReg1DaliManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
@@ -58,7 +65,11 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
if (IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
device_.readMemory();
const uint8_t* memory = platform_.getNonVolatileMemoryStart();
const size_t memory_size = platform_.getNonVolatileMemorySize();
if (!IsErasedMemory(memory, memory_size)) {
device_.readMemory();
}
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
@@ -99,6 +110,16 @@ uint16_t EtsDeviceRuntime::tunnelClientAddress() const {
bool EtsDeviceRuntime::configured() const { return const_cast<Bau07B0&>(device_).configured(); }
bool EtsDeviceRuntime::programmingMode() const {
return const_cast<Bau07B0&>(device_).deviceObject().progMode();
}
void EtsDeviceRuntime::setProgrammingMode(bool enabled) {
device_.deviceObject().progMode(enabled);
}
void EtsDeviceRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); }
EtsMemorySnapshot EtsDeviceRuntime::snapshot() const {
EtsMemorySnapshot out;
auto& device = const_cast<Bau07B0&>(device_);
@@ -139,6 +160,10 @@ void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) {
group_write_handler_ = std::move(handler);
}
void EtsDeviceRuntime::setNetworkInterface(esp_netif_t* netif) {
platform_.networkInterface(netif);
}
bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
CemiFrameSender sender) {
auto* server = device_.getCemiServer();
@@ -289,6 +314,9 @@ bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
case M_FuncPropStateRead_req:
return true;
case L_data_req:
if (!const_cast<Bau07B0&>(device_).configured() || programmingMode()) {
return true;
}
if (frame.addressType() == IndividualAddress &&
frame.destinationAddress() == individualAddress()) {
return true;
+79 -41
View File
@@ -2,8 +2,8 @@
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_random.h"
#include "esp_timer.h"
#include "mbedtls/sha256.h"
#include "nvs.h"
#include "nvs_flash.h"
@@ -22,11 +22,13 @@ constexpr const char* kFactoryFdskKey = "factory_fdsk";
constexpr size_t kFdskSize = 16;
constexpr size_t kSerialSize = 6;
constexpr size_t kFdskQrSize = 36;
constexpr uint16_t kKnxManufacturerId = 0x00A4;
constexpr const char* kProductIdentity = "REG1-Dali";
constexpr const char* kManufacturerId = "00A4";
constexpr const char* kApplicationNumber = "01";
constexpr const char* kApplicationVersion = "05";
constexpr const char* kDevelopmentStorage = "plain_nvs_development";
constexpr const char* kDevelopmentStorage = "base_mac_derived_plain_nvs_development";
constexpr char kFdskDerivationLabel[] = "DaliMaster REG1-Dali deterministic FDSK v1";
constexpr uint8_t kCrc4Tab[16] = {
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
@@ -57,10 +59,14 @@ bool plausibleKey(const uint8_t* data) {
return !all_zero && !all_ff;
}
void generateKey(uint8_t* data) {
do {
esp_fill_random(data, kFdskSize);
} while (!plausibleKey(data));
bool readBaseMac(uint8_t* data) {
if (data == nullptr) {
return false;
}
if (esp_efuse_mac_get_default(data) == ESP_OK) {
return true;
}
return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK;
}
void clearOpenKnxFdskCache() {
@@ -108,16 +114,60 @@ bool parseHexKey(const std::string& value, uint8_t* out) {
return plausibleKey(out);
}
bool storeFactoryFdsk(const uint8_t* data) {
if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) {
bool loadKnxSerialNumber(uint8_t* serial) {
if (serial == nullptr) {
return false;
}
std::array<uint8_t, kSerialSize> mac{};
if (!readBaseMac(mac.data())) {
return false;
}
serial[0] = static_cast<uint8_t>((kKnxManufacturerId >> 8) & 0xff);
serial[1] = static_cast<uint8_t>(kKnxManufacturerId & 0xff);
std::copy(mac.begin() + 2, mac.end(), serial + 2);
return true;
}
bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) {
if (serial == nullptr || key == nullptr) {
return false;
}
std::array<uint8_t, sizeof(kFdskDerivationLabel) - 1 + kSerialSize> material{};
std::copy(kFdskDerivationLabel, kFdskDerivationLabel + sizeof(kFdskDerivationLabel) - 1,
material.begin());
std::copy(serial, serial + kSerialSize, material.begin() + sizeof(kFdskDerivationLabel) - 1);
std::array<uint8_t, 32> digest{};
if (mbedtls_sha256(material.data(), material.size(), digest.data(), 0) != 0) {
return false;
}
std::copy(digest.begin(), digest.begin() + kFdskSize, key);
if (!plausibleKey(key)) {
key[kFdskSize - 1] ^= 0xA5;
}
return plausibleKey(key);
}
void syncFactoryFdskToNvs(const uint8_t* data) {
if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) {
return;
}
std::array<uint8_t, kFdskSize> stored{};
size_t stored_size = stored.size();
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
return false;
return;
}
err = nvs_get_blob(handle, kFactoryFdskKey, stored.data(), &stored_size);
if (err == ESP_OK && stored_size == stored.size() &&
std::equal(stored.begin(), stored.end(), data)) {
nvs_close(handle);
return;
}
err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize);
if (err == ESP_OK) {
@@ -125,11 +175,10 @@ bool storeFactoryFdsk(const uint8_t* data) {
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to store KNX factory FDSK: %s", esp_err_to_name(err));
return false;
ESP_LOGW(kTag, "failed to mirror deterministic KNX factory FDSK: %s", esp_err_to_name(err));
return;
}
clearOpenKnxFdskCache();
return true;
}
uint8_t crc4Array(const uint8_t* data, size_t len) {
@@ -219,35 +268,18 @@ std::string fnv1aHex(const std::string& value) {
namespace gateway::openknx {
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
if (data == nullptr || len < kFdskSize || !ensureNvsReady()) {
if (data == nullptr || len < kFdskSize) {
return false;
}
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
return false;
}
size_t stored_size = kFdskSize;
err = nvs_get_blob(handle, kFactoryFdskKey, data, &stored_size);
if (err == ESP_OK && stored_size == kFdskSize && plausibleKey(data)) {
nvs_close(handle);
return true;
}
generateKey(data);
err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to store generated KNX factory FDSK: %s", esp_err_to_name(err));
std::array<uint8_t, kSerialSize> serial{};
std::array<uint8_t, kFdskSize> key{};
if (!loadKnxSerialNumber(serial.data()) ||
!deriveFactoryFdskFromSerial(serial.data(), key.data())) {
return false;
}
std::memcpy(data, key.data(), kFdskSize);
syncFactoryFdskToNvs(key.data());
return true;
}
@@ -255,8 +287,7 @@ FactoryFdskInfo LoadFactoryFdskInfo() {
FactoryFdskInfo info;
std::array<uint8_t, kFdskSize> key{};
std::array<uint8_t, kSerialSize> serial{};
if (!LoadFactoryFdsk(key.data(), key.size()) ||
esp_read_mac(serial.data(), ESP_MAC_WIFI_STA) != ESP_OK) {
if (!loadKnxSerialNumber(serial.data()) || !LoadFactoryFdsk(key.data(), key.size())) {
return info;
}
@@ -269,8 +300,7 @@ FactoryFdskInfo LoadFactoryFdskInfo() {
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
std::array<uint8_t, kFdskSize> key{};
generateKey(key.data());
const bool stored = storeFactoryFdsk(key.data());
const bool stored = LoadFactoryFdsk(key.data(), key.size());
std::fill(key.begin(), key.end(), 0);
if (!stored) {
return false;
@@ -286,8 +316,16 @@ bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
if (!parseHexKey(hex_key, key.data())) {
return false;
}
const bool stored = storeFactoryFdsk(key.data());
std::array<uint8_t, kSerialSize> serial{};
std::array<uint8_t, kFdskSize> derived{};
const bool stored = loadKnxSerialNumber(serial.data()) &&
deriveFactoryFdskFromSerial(serial.data(), derived.data()) &&
std::equal(key.begin(), key.end(), derived.begin());
if (stored) {
syncFactoryFdskToNvs(derived.data());
}
std::fill(key.begin(), key.end(), 0);
std::fill(derived.begin(), derived.end(), 0);
if (!stored) {
return false;
}