feat(gateway): enhance DALI and KNX settings management with instance support

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-26 12:57:13 +08:00
parent 2b779d5532
commit f922993d2f
5 changed files with 217 additions and 42 deletions
+27 -10
View File
@@ -1,5 +1,7 @@
menu "Gateway App"
menu "DALI Settings"
config GATEWAY_CHANNEL_COUNT
int "Gateway channel count"
range 1 2
@@ -329,6 +331,8 @@ config GATEWAY_DALI_BAUDRATE
help
Runtime baudrate used when initializing the local DALI bus.
endmenu
menu "Gateway Startup Services"
config GATEWAY_BLE_SUPPORTED
@@ -620,6 +624,17 @@ config GATEWAY_START_BACNET_BRIDGE_ENABLED
help
Starts configured BACnet/IP object bindings at boot. Disabled by default so the UDP BACnet/IP port is opened only after provisioning or explicit runtime start.
menu "KNX Settings"
config GATEWAY_KNX_INSTANCE_COUNT
int "Enabled KNX instance count"
range 1 16
default 1
help
Number of enabled KNX logical instances exposed by management APIs.
Instance 0 preserves the legacy serial/FDSK identity; each additional
instance derives its serial/FDSK from the base identity plus instance id.
config GATEWAY_KNX_BRIDGE_SUPPORTED
bool "KNX to DALI bridge is supported"
depends on GATEWAY_BRIDGE_SUPPORTED && (GATEWAY_WIFI_SUPPORTED || GATEWAY_ETHERNET_SUPPORTED)
@@ -956,16 +971,16 @@ config GATEWAY_KNX_TP_UART_9BIT_MODE
mode commonly described as 19200 baud 9-bit UART. Disable only for
hardware wired for 8N1 host UART mode.
config GATEWAY_KNX_TP_FULL_IP_FORWARD
bool "Mirror all physical KNX TP telegrams to KNXnet/IP"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED && GATEWAY_KNX_TP_UART_PORT >= 0
default n
help
Mirrors physical KNX TP telegrams received from the TP-UART line back
out through KNXnet/IP tunnelling and multicast even when the gateway
runs the single-interface ETS device runtime. Enable this when ETS must
monitor or download other TP devices through the gateway's IP endpoint.
Leave it disabled to preserve the narrower default forwarding behavior.
config GATEWAY_KNX_TP_FULL_IP_FORWARD
bool "Mirror all physical KNX TP telegrams to KNXnet/IP"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED && GATEWAY_KNX_TP_UART_PORT >= 0
default n
help
Mirrors physical KNX TP telegrams received from the TP-UART line back
out through KNXnet/IP tunnelling and multicast even when the gateway
runs the single-interface ETS device runtime. Enable this when ETS must
monitor or download other TP devices through the gateway's IP endpoint.
Leave it disabled to preserve the narrower default forwarding behavior.
config GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE
int "KNX/IP bridge task stack bytes"
@@ -979,6 +994,8 @@ config GATEWAY_BRIDGE_KNX_TASK_PRIORITY
range 1 10
default 5
endmenu
config GATEWAY_CLOUD_BRIDGE_SUPPORTED
bool "MQTT cloud bridge is supported"
depends on GATEWAY_BRIDGE_SUPPORTED && (GATEWAY_WIFI_SUPPORTED || GATEWAY_ETHERNET_SUPPORTED)
+11
View File
@@ -596,6 +596,10 @@ CONFIG_PARTITION_TABLE_MD5=y
#
# Gateway App
#
#
# DALI Settings
#
CONFIG_GATEWAY_CHANNEL_COUNT=2
#
@@ -640,6 +644,7 @@ CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y
# end of Gateway Cache
# CONFIG_GATEWAY_ENABLE_DALI_BUS is not set
# end of DALI Settings
#
# Gateway Startup Services
@@ -683,6 +688,10 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
#
# KNX Settings
#
CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y
CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
@@ -716,6 +725,8 @@ CONFIG_GATEWAY_KNX_TP_UART_9BIT_MODE=y
CONFIG_GATEWAY_KNX_TP_FULL_IP_FORWARD=y
CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=12288
CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5
# end of KNX Settings
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144
@@ -40,10 +40,14 @@ struct IpSecureCredentialStatus {
bool LoadFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadFactoryFdskInfo();
bool LoadFactoryFdskForInstance(uint32_t instance_id, uint8_t* data, size_t len);
FactoryFdskInfo LoadFactoryFdskInfoForInstance(uint32_t instance_id);
bool GenerateFactoryFdsk(FactoryFdskInfo* info = nullptr);
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info = nullptr);
bool ResetFactoryFdskCache(FactoryFdskInfo* info = nullptr);
bool ResetFactorySecurityForInstance(uint32_t instance_id, FactoryFdskInfo* info = nullptr);
FactoryCertificatePayload BuildFactoryCertificatePayload();
FactoryCertificatePayload BuildFactoryCertificatePayloadForInstance(uint32_t instance_id);
bool LoadOamFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadOamFactoryFdskInfo();
+133 -28
View File
@@ -24,6 +24,8 @@
#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#endif
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "sdkconfig.h"
#include "freertos/semphr.h"
#include "lwip/inet.h"
@@ -64,6 +66,10 @@ constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
constexpr const char* kModbusManagementPrefix = "@DALIGW";
constexpr uint8_t kDaliGroupRawMin = 0x80;
constexpr uint8_t kDaliGroupRawMax = 0x9F;
#ifndef CONFIG_GATEWAY_KNX_INSTANCE_COUNT
#define CONFIG_GATEWAY_KNX_INSTANCE_COUNT 1
#endif
constexpr uint32_t kKnxEnabledInstanceCount = CONFIG_GATEWAY_KNX_INSTANCE_COUNT;
void ConfigureDaliCppLogging() {
static bool configured = false;
@@ -199,7 +205,7 @@ cJSON* FactoryCertificateToCjson(const openknx::FactoryCertificatePayload& certi
return root;
}
cJSON* IpSecureCredentialStatusToCjson(
[[maybe_unused]] cJSON* IpSecureCredentialStatusToCjson(
const openknx::IpSecureCredentialStatus& status) {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
@@ -393,6 +399,50 @@ std::optional<int> QueryInt(std::string_view query, std::string_view primary,
return ParseInt(QueryValue(query, fallback));
}
std::optional<uint32_t> ValidKnxInstanceId(int value) {
if (value < 0 || static_cast<uint32_t>(value) >= kKnxEnabledInstanceCount) {
return std::nullopt;
}
return static_cast<uint32_t>(value);
}
std::string KnxInstanceIdRangeMessage() {
return "KNX instanceId must be in range 0.." +
std::to_string(kKnxEnabledInstanceCount - 1);
}
std::optional<uint32_t> QueryKnxInstanceId(std::string_view query) {
return ValidKnxInstanceId(QueryInt(query, "instanceId", "instance").value_or(0));
}
bool EnsureBridgeNvsReady() {
const esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
if (nvs_flash_erase() != ESP_OK) {
return false;
}
return nvs_flash_init() == ESP_OK;
}
return err == ESP_OK || err == ESP_ERR_INVALID_STATE;
}
bool EraseOpenKnxEeprom(const std::string& nvs_namespace) {
if (nvs_namespace.empty() || !EnsureBridgeNvsReady()) {
return false;
}
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(nvs_namespace.c_str(), NVS_READWRITE, &handle);
if (err != ESP_OK) {
return err == ESP_ERR_NVS_NOT_FOUND;
}
err = nvs_erase_key(handle, "eeprom");
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
return err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND;
}
std::optional<int> JsonIntAny(const cJSON* parent, const char* primary, const char* fallback) {
auto value = JsonInt(parent, primary);
if (value.has_value() || fallback == nullptr) {
@@ -401,6 +451,10 @@ std::optional<int> JsonIntAny(const cJSON* parent, const char* primary, const ch
return JsonInt(parent, fallback);
}
std::optional<uint32_t> JsonKnxInstanceId(const cJSON* parent) {
return ValidKnxInstanceId(JsonIntAny(parent, "instanceId", "instance").value_or(0));
}
bool ValidDaliAddress(int address) {
return address >= 0 && address <= 127;
}
@@ -2229,7 +2283,7 @@ struct GatewayBridgeService::ChannelRuntime {
return JsonOk(BridgeResultToCjson(result));
}
cJSON* knxStatusCjson() const {
cJSON* knxStatusCjson(uint32_t instance_id = 0) const {
cJSON* knx_json = cJSON_CreateObject();
if (knx_json == nullptr) {
return nullptr;
@@ -2254,6 +2308,9 @@ struct GatewayBridgeService::ChannelRuntime {
}
const auto effective_knx =
knx_config.has_value() ? knx_config : service_config.default_knx_config;
cJSON_AddNumberToObject(knx_json, "instanceId", static_cast<double>(instance_id));
cJSON_AddNumberToObject(knx_json, "enabledInstanceCount",
static_cast<double>(kKnxEnabledInstanceCount));
cJSON_AddBoolToObject(knx_json, "enabled", service_config.knx_enabled);
cJSON_AddBoolToObject(knx_json, "startupEnabled", service_config.knx_startup_enabled);
cJSON_AddBoolToObject(knx_json, "started", knx_started);
@@ -2307,13 +2364,14 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddStringToObject(security_json, "storage", "none");
#endif
#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
const auto fdsk_info = openknx::LoadFactoryFdskInfo();
const auto fdsk_info = openknx::LoadFactoryFdskInfoForInstance(instance_id);
cJSON* fdsk_json = FactoryFdskInfoToCjson(fdsk_info, true);
if (fdsk_json != nullptr) {
cJSON_AddItemToObject(security_json, "factorySetupKey", fdsk_json);
}
cJSON* certificate_json =
FactoryCertificateToCjson(openknx::BuildFactoryCertificatePayload(), false);
FactoryCertificateToCjson(
openknx::BuildFactoryCertificatePayloadForInstance(instance_id), false);
if (certificate_json != nullptr) {
cJSON_AddItemToObject(security_json, "factoryCertificate", certificate_json);
}
@@ -2322,27 +2380,6 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddItemToObject(security_json, "failures", failures_json);
}
#endif
cJSON* oam_security_json = cJSON_CreateObject();
if (oam_security_json != nullptr) {
cJSON* oam_fdsk_json = FactoryFdskInfoToCjson(
openknx::LoadOamFactoryFdskInfo(), false);
if (oam_fdsk_json != nullptr) {
cJSON_AddItemToObject(oam_security_json, "factorySetupKey", oam_fdsk_json);
}
cJSON* oam_certificate_json = FactoryCertificateToCjson(
openknx::BuildOamFactoryCertificatePayload(), false);
if (oam_certificate_json != nullptr) {
cJSON_AddItemToObject(oam_security_json, "factoryCertificate",
oam_certificate_json);
}
cJSON* oam_credentials_json = IpSecureCredentialStatusToCjson(
openknx::LoadOamIpSecureCredentialStatus());
if (oam_credentials_json != nullptr) {
cJSON_AddItemToObject(oam_security_json, "ipSecureCredentials",
oam_credentials_json);
}
cJSON_AddItemToObject(security_json, "oamRouter", oam_security_json);
}
cJSON_AddItemToObject(knx_json, "security", security_json);
}
if (effective_knx.has_value()) {
@@ -2491,13 +2528,13 @@ struct GatewayBridgeService::ChannelRuntime {
return root;
}
GatewayBridgeHttpResponse knxStatusJson() const {
GatewayBridgeHttpResponse knxStatusJson(uint32_t instance_id = 0) const {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate KNX status JSON");
}
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON* knx_json = knxStatusCjson();
cJSON* knx_json = knxStatusCjson(instance_id);
if (knx_json == nullptr) {
cJSON_Delete(root);
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate KNX status JSON");
@@ -3035,14 +3072,17 @@ struct GatewayBridgeService::ChannelRuntime {
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortOn:
case GatewayModbusGeneratedKind::kShortRecallMax:
domain.markHostCommandFrame(channel.gateway_id, raw_command_address, DALI_CMD_RECALL_MAX);
sent = domain.on(channel.gateway_id, point.short_address);
mirrored_command = DALI_CMD_RECALL_MAX;
break;
case GatewayModbusGeneratedKind::kShortOff:
domain.markHostCommandFrame(channel.gateway_id, raw_command_address, DALI_CMD_OFF);
sent = domain.off(channel.gateway_id, point.short_address);
mirrored_command = DALI_CMD_OFF;
break;
case GatewayModbusGeneratedKind::kShortRecallMin:
domain.markHostCommandFrame(channel.gateway_id, raw_command_address, DALI_CMD_RECALL_MIN);
sent = domain.sendRaw(channel.gateway_id, raw_command_address, DALI_CMD_RECALL_MIN);
mirrored_command = DALI_CMD_RECALL_MIN;
break;
@@ -3065,6 +3105,8 @@ struct GatewayBridgeService::ChannelRuntime {
if (value > 254) {
return false;
}
domain.markHostCommandFrame(channel.gateway_id, RawArcAddressFromDec(point.short_address),
static_cast<uint8_t>(value));
if (domain.setBright(channel.gateway_id, point.short_address, value)) {
cache.mirrorDaliCommand(channel.gateway_id, RawArcAddressFromDec(point.short_address),
static_cast<uint8_t>(value));
@@ -3072,8 +3114,10 @@ struct GatewayBridgeService::ChannelRuntime {
}
return false;
case GatewayModbusGeneratedKind::kShortColorTemperature:
domain.markHostActivity(channel.gateway_id);
return domain.setColTemp(channel.gateway_id, point.short_address, value);
case GatewayModbusGeneratedKind::kShortGroupMask:
domain.markHostActivity(channel.gateway_id);
if (domain.applyGroupMask(channel.gateway_id, point.short_address, value)) {
cache.setDaliGroupMask(channel.gateway_id, static_cast<uint8_t>(point.short_address),
value);
@@ -3120,6 +3164,7 @@ struct GatewayBridgeService::ChannelRuntime {
domain_settings.max_level = current.max_level;
domain_settings.fade_time = current.fade_time;
domain_settings.fade_rate = current.fade_rate;
domain.markHostActivity(channel.gateway_id);
if (domain.applyAddressSettings(channel.gateway_id, point.short_address, domain_settings)) {
cache.setDaliSettings(channel.gateway_id, static_cast<uint8_t>(point.short_address),
current);
@@ -4415,7 +4460,12 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
return JsonOk(runtime->statusCjson());
}
if (action == "knx_status") {
return runtime->knxStatusJson();
const auto instance_id = QueryKnxInstanceId(query);
if (!instance_id.has_value()) {
const std::string message = KnxInstanceIdRangeMessage();
return ErrorResponse(ESP_ERR_INVALID_ARG, message.c_str());
}
return runtime->knxStatusJson(instance_id.value());
}
if (action == "config") {
return runtime->configJson();
@@ -4762,6 +4812,61 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
}
return handleGet("status", gateway_id.value());
}
if (action == "knx_factory_reset") {
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "KNX factory reset confirmation JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "reset-knx-factory-defaults";
const auto instance_id = JsonKnxInstanceId(body_root);
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "KNX factory reset confirmation is required");
}
if (!instance_id.has_value()) {
const std::string message = KnxInstanceIdRangeMessage();
return ErrorResponse(ESP_ERR_INVALID_ARG, message.c_str());
}
const bool reset_runtime = instance_id.value() == 0;
const bool restart_router = reset_runtime &&
(runtime->knx_started ||
(runtime->knx_router != nullptr && runtime->knx_router->started()));
if (restart_router) {
const esp_err_t stop_err = stopKnxEndpoint(runtime);
if (stop_err != ESP_OK) {
return ErrorResponse(stop_err, "failed to stop KNX/IP bridge before factory reset");
}
}
std::string nvs_namespace = runtime->openKnxNamespace();
if (instance_id.value() != 0) {
nvs_namespace += "_" + std::to_string(instance_id.value());
}
if (!EraseOpenKnxEeprom(nvs_namespace)) {
return ErrorResponse(ESP_FAIL, "failed to erase KNX runtime data");
}
openknx::FactoryFdskInfo info;
if (!openknx::ResetFactorySecurityForInstance(instance_id.value(), &info)) {
return ErrorResponse(ESP_FAIL, "failed to restore KNX factory security key");
}
if (restart_router) {
std::set<int> used_serial_uarts;
collectUsedRuntimeResources(runtime->channel.gateway_id, nullptr, nullptr, &used_serial_uarts);
const esp_err_t start_err = startKnxEndpoint(runtime, &used_serial_uarts);
if (start_err != ESP_OK) {
return ErrorResponse(start_err, runtime->knx_last_error.empty()
? "failed to restart KNX/IP bridge after factory reset"
: runtime->knx_last_error.c_str());
}
}
return runtime->knxStatusJson(instance_id.value());
}
if (action == "knx_security_read_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
@@ -45,7 +45,7 @@ constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
struct FactoryFdskContext {
const char* nvsNamespace;
std::string nvsNamespace;
const char* productIdentity;
const char* derivationLabel;
uint16_t manufacturerId;
@@ -55,7 +55,7 @@ struct FactoryFdskContext {
bool clearOpenKnxCache;
};
constexpr FactoryFdskContext kReg1Context{
const FactoryFdskContext kReg1Context{
kNamespace,
kProductIdentity,
kFdskDerivationLabel,
@@ -65,7 +65,7 @@ constexpr FactoryFdskContext kReg1Context{
gateway::knx_internal::kReg1DaliSerialMacIncrement,
true};
constexpr FactoryFdskContext kOamContext{
const FactoryFdskContext kOamContext{
kOamNamespace,
kOamProductIdentity,
kOamFdskDerivationLabel,
@@ -77,6 +77,16 @@ constexpr FactoryFdskContext kOamContext{
extern "C" void knx_platform_clear_cached_fdsk() __attribute__((weak));
FactoryFdskContext reg1ContextForInstance(uint32_t instance_id) {
FactoryFdskContext context = kReg1Context;
context.serialMacIncrement = gateway::knx_internal::kReg1DaliSerialMacIncrement + instance_id;
context.clearOpenKnxCache = instance_id == 0;
if (instance_id != 0) {
context.nvsNamespace = std::string(kNamespace) + "_" + std::to_string(instance_id);
}
return context;
}
std::string hexValue(uint32_t value, int width) {
std::array<char, 9> buffer{};
std::snprintf(buffer.data(), buffer.size(), "%0*" PRIX32, width, value);
@@ -213,7 +223,7 @@ void syncFactoryFdskToNvs(const FactoryFdskContext& context, const uint8_t* data
size_t stored_size = stored.size();
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(context.nvsNamespace, NVS_READWRITE, &handle);
esp_err_t err = nvs_open(context.nvsNamespace.c_str(), NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
return;
@@ -483,6 +493,16 @@ FactoryFdskInfo LoadFactoryFdskInfo() {
return LoadFactoryFdskInfoForContext(kReg1Context);
}
bool LoadFactoryFdskForInstance(uint32_t instance_id, uint8_t* data, size_t len) {
const auto context = reg1ContextForInstance(instance_id);
return LoadFactoryFdskForContext(context, data, len);
}
FactoryFdskInfo LoadFactoryFdskInfoForInstance(uint32_t instance_id) {
const auto context = reg1ContextForInstance(instance_id);
return LoadFactoryFdskInfoForContext(context);
}
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
return GenerateFactoryFdskForContext(kReg1Context, info);
}
@@ -495,10 +515,28 @@ bool ResetFactoryFdskCache(FactoryFdskInfo* info) {
return ResetFactoryFdskCacheForContext(kReg1Context, info);
}
bool ResetFactorySecurityForInstance(uint32_t instance_id, FactoryFdskInfo* info) {
const auto context = reg1ContextForInstance(instance_id);
if (ensureNvsReady()) {
nvs_handle_t handle = 0;
if (nvs_open(context.nvsNamespace.c_str(), NVS_READWRITE, &handle) == ESP_OK) {
nvs_erase_key(handle, kFactoryFdskKey);
nvs_commit(handle);
nvs_close(handle);
}
}
return ResetFactoryFdskCacheForContext(context, info);
}
FactoryCertificatePayload BuildFactoryCertificatePayload() {
return BuildFactoryCertificatePayloadForContext(kReg1Context);
}
FactoryCertificatePayload BuildFactoryCertificatePayloadForInstance(uint32_t instance_id) {
const auto context = reg1ContextForInstance(instance_id);
return BuildFactoryCertificatePayloadForContext(context);
}
bool LoadOamFactoryFdsk(uint8_t* data, size_t len) {
return LoadFactoryFdskForContext(kOamContext, data, len);
}