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
@@ -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);
}