diff --git a/include/bacnet_bridge.hpp b/include/bacnet_bridge.hpp index a414668..ba491d9 100644 --- a/include/bacnet_bridge.hpp +++ b/include/bacnet_bridge.hpp @@ -18,6 +18,8 @@ struct BacnetObjectBinding { BridgeObjectType objectType = BridgeObjectType::unknown; int objectInstance = -1; std::string property; + BridgeOperation operation = BridgeOperation::unknown; + BridgeDaliTarget target; }; class DaliBacnetBridge { diff --git a/include/bridge.hpp b/include/bridge.hpp index 8749a2c..b7c9c69 100644 --- a/include/bridge.hpp +++ b/include/bridge.hpp @@ -55,8 +55,8 @@ class DaliBridgeEngine { DaliBridgeResult executeResolved(const DaliBridgeRequest& request, const BridgeModel* model, BridgeOperation operation); - std::optional resolveShortAddress(const DaliBridgeRequest& request, - const BridgeModel* model) const; + std::optional resolveTargetAddress(const DaliBridgeRequest& request, + const BridgeModel* model) const; std::optional resolveRawAddress(const DaliBridgeRequest& request, const BridgeModel* model) const; std::optional resolveRawCommand(const DaliBridgeRequest& request, diff --git a/include/bridge_model.hpp b/include/bridge_model.hpp index 1b62a0c..810d22e 100644 --- a/include/bridge_model.hpp +++ b/include/bridge_model.hpp @@ -25,6 +25,12 @@ enum class BridgeObjectType { multiStateValue = 9, }; +enum class BridgeDaliTargetKind { + shortAddress = 0, + group = 1, + broadcast = 2, +}; + enum class BridgeOperation { unknown = 0, send = 1, @@ -46,6 +52,7 @@ enum class BridgeOperation { getEmergencyFailureStatus = 17, startEmergencyFunctionTest = 18, stopEmergencyTest = 19, + startEmergencyDurationTest = 20, }; enum class BridgeValueEncoding { @@ -80,12 +87,15 @@ struct BridgeExternalPoint { }; struct BridgeDaliTarget { + BridgeDaliTargetKind kind = BridgeDaliTargetKind::shortAddress; std::optional shortAddress; + std::optional groupAddress; std::optional rawAddress; std::optional rawCommand; static BridgeDaliTarget fromJson(const DaliValue::Object* json); DaliValue::Object toJson() const; + std::optional logicalAddress() const; }; struct BridgeModel { @@ -110,6 +120,9 @@ BridgeProtocolKind bridgeProtocolKindFromString(const std::string& value); const char* bridgeObjectTypeToString(BridgeObjectType type); BridgeObjectType bridgeObjectTypeFromString(const std::string& value); +const char* bridgeDaliTargetKindToString(BridgeDaliTargetKind kind); +BridgeDaliTargetKind bridgeDaliTargetKindFromString(const std::string& value); + const char* bridgeOperationToString(BridgeOperation operation); BridgeOperation bridgeOperationFromString(const std::string& value); diff --git a/include/bridge_provisioning.hpp b/include/bridge_provisioning.hpp index cddaf72..686307b 100644 --- a/include/bridge_provisioning.hpp +++ b/include/bridge_provisioning.hpp @@ -34,6 +34,9 @@ class BridgeProvisioningStore { esp_err_t save(const BridgeRuntimeConfig& config) const; esp_err_t load(BridgeRuntimeConfig* config) const; esp_err_t clear() const; + esp_err_t saveObject(const char* key, const DaliValue::Object& object) const; + esp_err_t loadObject(const char* key, DaliValue::Object* object) const; + esp_err_t clearKey(const char* key) const; private: std::string nvsNamespace_; diff --git a/src/bacnet_bridge.cpp b/src/bacnet_bridge.cpp index 1a135f1..ed23dc4 100644 --- a/src/bacnet_bridge.cpp +++ b/src/bacnet_bridge.cpp @@ -44,7 +44,8 @@ std::optional DaliBacnetBridge::findObject(BridgeObjectType if (!model.external.property.empty() && model.external.property != property) { continue; } - return BacnetObjectBinding{model.id, objectType, objectInstance, property}; + return BacnetObjectBinding{model.id, objectType, objectInstance, property, + model.operation, model.dali}; } return std::nullopt; } @@ -57,7 +58,8 @@ std::vector DaliBacnetBridge::describeObjects() const { } bindings.push_back(BacnetObjectBinding{model.id, model.external.objectType, model.external.objectInstance.value(), - model.external.property}); + model.external.property, model.operation, + model.dali}); } return bindings; } \ No newline at end of file diff --git a/src/bridge.cpp b/src/bridge.cpp index b7029a5..a1256ba 100644 --- a/src/bridge.cpp +++ b/src/bridge.cpp @@ -135,7 +135,7 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ return result; } case BridgeOperation::setBrightness: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); const auto value = resolveIntValue(request, model); if (!address.has_value() || !value.has_value()) { result.error = "missing address/value"; @@ -146,7 +146,7 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::setBrightnessPercent: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); const auto value = resolveDoubleValue(request); if (!address.has_value() || !value.has_value()) { result.error = "missing address/value"; @@ -157,43 +157,43 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::on: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } result.ok = base_.on(address.value()); break; } case BridgeOperation::off: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } result.ok = base_.off(address.value()); break; } case BridgeOperation::recallMaxLevel: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } result.ok = base_.recallMaxLevel(address.value()); break; } case BridgeOperation::recallMinLevel: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } result.ok = base_.recallMinLevel(address.value()); break; } case BridgeOperation::setColorTemperature: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); const auto value = resolveIntValue(request, model); if (!address.has_value() || !value.has_value()) { result.error = "missing address/value"; @@ -204,9 +204,9 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::getBrightness: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } const auto value = base_.getBright(address.value()); @@ -219,9 +219,9 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::getStatus: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } const auto value = base_.getStatus(address.value()); @@ -235,9 +235,9 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::getColorTemperature: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } const auto value = dt8_.getColorTemperature(address.value()); @@ -250,9 +250,9 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::getColorStatus: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } const auto value = dt8_.getColorStatus(address.value()); @@ -273,9 +273,9 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::getEmergencyLevel: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } const auto value = dt1_.getEmergencyLevel(address.value()); @@ -288,9 +288,9 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::getEmergencyStatus: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } const auto status = dt1_.getEmergencyStatusDecoded(address.value()); @@ -311,9 +311,9 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::getEmergencyFailureStatus: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } const auto status = dt1_.getDT1TestStatusDetailed(address.value()); @@ -337,18 +337,27 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ break; } case BridgeOperation::startEmergencyFunctionTest: { - const auto address = resolveShortAddress(request, model); + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; return result; } result.ok = dt1_.startFunctionTestCmd(address.value()); break; } - case BridgeOperation::stopEmergencyTest: { - const auto address = resolveShortAddress(request, model); + case BridgeOperation::startEmergencyDurationTest: { + const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { - result.error = "missing short address"; + result.error = "missing target address"; + return result; + } + result.ok = dt1_.startDurationTestCmd(address.value()); + break; + } + case BridgeOperation::stopEmergencyTest: { + const auto address = resolveTargetAddress(request, model); + if (!address.has_value()) { + result.error = "missing target address"; return result; } result.ok = dt1_.stopTest(address.value()); @@ -366,13 +375,13 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ return result; } -std::optional DaliBridgeEngine::resolveShortAddress(const DaliBridgeRequest& request, - const BridgeModel* model) const { +std::optional DaliBridgeEngine::resolveTargetAddress(const DaliBridgeRequest& request, + const BridgeModel* model) const { if (request.shortAddress.has_value()) { return request.shortAddress; } if (model != nullptr) { - return model->dali.shortAddress; + return model->dali.logicalAddress(); } return std::nullopt; } diff --git a/src/bridge_model.cpp b/src/bridge_model.cpp index ad5ee11..37ced9a 100644 --- a/src/bridge_model.cpp +++ b/src/bridge_model.cpp @@ -7,6 +7,9 @@ namespace { +constexpr int kDaliGroupBaseAddress = 64; +constexpr int kDaliBroadcastAddress = 127; + std::string normalize(const std::string& value) { std::string out = value; std::transform(out.begin(), out.end(), out.begin(), [](unsigned char ch) { @@ -88,7 +91,37 @@ BridgeDaliTarget BridgeDaliTarget::fromJson(const DaliValue::Object* json) { if (json == nullptr) { return target; } - target.shortAddress = getObjectInt(*json, "shortAddress"); + const auto explicit_kind = bridgeDaliTargetKindFromString( + getObjectString(*json, "kind") + .value_or(getObjectString(*json, "targetKind").value_or(""))); + const auto shared_address = getObjectInt(*json, "address"); + const auto short_address = getObjectInt(*json, "shortAddress"); + const auto group_address = getObjectInt(*json, "groupAddress").has_value() + ? getObjectInt(*json, "groupAddress") + : getObjectInt(*json, "group"); + const bool broadcast = getObjectBool(*json, "broadcast").value_or(false); + + target.kind = explicit_kind; + if (explicit_kind == BridgeDaliTargetKind::shortAddress) { + target.shortAddress = shared_address; + } else if (explicit_kind == BridgeDaliTargetKind::group) { + target.groupAddress = shared_address; + } + if (short_address.has_value()) { + target.shortAddress = short_address; + if (explicit_kind != BridgeDaliTargetKind::broadcast) { + target.kind = BridgeDaliTargetKind::shortAddress; + } + } + if (group_address.has_value()) { + target.groupAddress = group_address; + if (explicit_kind != BridgeDaliTargetKind::broadcast) { + target.kind = BridgeDaliTargetKind::group; + } + } + if (broadcast) { + target.kind = BridgeDaliTargetKind::broadcast; + } target.rawAddress = getObjectInt(*json, "rawAddress"); target.rawCommand = getObjectInt(*json, "rawCommand"); return target; @@ -96,12 +129,44 @@ BridgeDaliTarget BridgeDaliTarget::fromJson(const DaliValue::Object* json) { DaliValue::Object BridgeDaliTarget::toJson() const { DaliValue::Object out; - if (shortAddress.has_value()) out["shortAddress"] = shortAddress.value(); + out["kind"] = bridgeDaliTargetKindToString(kind); + switch (kind) { + case BridgeDaliTargetKind::shortAddress: + if (shortAddress.has_value()) { + out["address"] = shortAddress.value(); + out["shortAddress"] = shortAddress.value(); + } + break; + case BridgeDaliTargetKind::group: + if (groupAddress.has_value()) { + out["address"] = groupAddress.value(); + out["groupAddress"] = groupAddress.value(); + } + break; + case BridgeDaliTargetKind::broadcast: + out["broadcast"] = true; + break; + } if (rawAddress.has_value()) out["rawAddress"] = rawAddress.value(); if (rawCommand.has_value()) out["rawCommand"] = rawCommand.value(); return out; } +std::optional BridgeDaliTarget::logicalAddress() const { + switch (kind) { + case BridgeDaliTargetKind::shortAddress: + return shortAddress; + case BridgeDaliTargetKind::group: + if (!groupAddress.has_value()) { + return std::nullopt; + } + return kDaliGroupBaseAddress + groupAddress.value(); + case BridgeDaliTargetKind::broadcast: + return kDaliBroadcastAddress; + } + return std::nullopt; +} + BridgeModel BridgeModel::fromJson(const DaliValue::Object& json) { BridgeModel model; model.id = getObjectString(json, "id").value_or(""); @@ -210,6 +275,26 @@ BridgeObjectType bridgeObjectTypeFromString(const std::string& value) { return BridgeObjectType::unknown; } +const char* bridgeDaliTargetKindToString(BridgeDaliTargetKind kind) { + switch (kind) { + case BridgeDaliTargetKind::shortAddress: + return "short_address"; + case BridgeDaliTargetKind::group: + return "group"; + case BridgeDaliTargetKind::broadcast: + return "broadcast"; + default: + return "short_address"; + } +} + +BridgeDaliTargetKind bridgeDaliTargetKindFromString(const std::string& value) { + const std::string normalized = normalize(value); + if (normalized == "group") return BridgeDaliTargetKind::group; + if (normalized == "broadcast") return BridgeDaliTargetKind::broadcast; + return BridgeDaliTargetKind::shortAddress; +} + const char* bridgeOperationToString(BridgeOperation operation) { switch (operation) { case BridgeOperation::send: @@ -250,6 +335,8 @@ const char* bridgeOperationToString(BridgeOperation operation) { return "start_emergency_function_test"; case BridgeOperation::stopEmergencyTest: return "stop_emergency_test"; + case BridgeOperation::startEmergencyDurationTest: + return "start_emergency_duration_test"; case BridgeOperation::unknown: default: return "unknown"; @@ -279,6 +366,9 @@ BridgeOperation bridgeOperationFromString(const std::string& value) { return BridgeOperation::startEmergencyFunctionTest; } if (normalized == "stop_emergency_test") return BridgeOperation::stopEmergencyTest; + if (normalized == "start_emergency_duration_test") { + return BridgeOperation::startEmergencyDurationTest; + } return BridgeOperation::unknown; } diff --git a/src/bridge_provisioning.cpp b/src/bridge_provisioning.cpp index 692ee0d..43140be 100644 --- a/src/bridge_provisioning.cpp +++ b/src/bridge_provisioning.cpp @@ -143,6 +143,98 @@ DaliValue bacnetToJson(const BacnetBridgeConfig& config) { return DaliValue(std::move(out)); } +esp_err_t saveJsonObject(const std::string& nvs_namespace, const char* key, + const DaliValue::Object& object) { + if (key == nullptr || key[0] == '\0') { + return ESP_ERR_INVALID_ARG; + } + + nvs_handle_t handle; + esp_err_t err = nvs_open(nvs_namespace.c_str(), NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(kTag, "nvs_open(save %s) failed: %s", key, esp_err_to_name(err)); + return err; + } + + cJSON* root = toCjson(DaliValue(object)); + char* raw = cJSON_PrintUnformatted(root); + if (raw == nullptr) { + cJSON_Delete(root); + nvs_close(handle); + return ESP_ERR_NO_MEM; + } + + err = nvs_set_str(handle, key, raw); + if (err == ESP_OK) { + err = nvs_commit(handle); + } + + cJSON_Delete(root); + cJSON_free(raw); + nvs_close(handle); + if (err != ESP_OK) { + ESP_LOGE(kTag, "save %s failed: %s", key, esp_err_to_name(err)); + } + return err; +} + +esp_err_t loadJsonObject(const std::string& nvs_namespace, const char* key, + DaliValue::Object* object) { + if (object == nullptr || key == nullptr || key[0] == '\0') { + return ESP_ERR_INVALID_ARG; + } + + nvs_handle_t handle; + esp_err_t err = nvs_open(nvs_namespace.c_str(), NVS_READONLY, &handle); + if (err != ESP_OK) { + return err; + } + + std::string payload; + err = readString(handle, key, &payload); + nvs_close(handle); + if (err != ESP_OK) { + return err; + } + + cJSON* root = cJSON_Parse(payload.c_str()); + if (root == nullptr) { + return ESP_ERR_INVALID_RESPONSE; + } + + const DaliValue value = fromCjson(root); + cJSON_Delete(root); + const auto* parsed = value.asObject(); + if (parsed == nullptr) { + return ESP_ERR_INVALID_RESPONSE; + } + + *object = *parsed; + return ESP_OK; +} + +esp_err_t clearStoredKey(const std::string& nvs_namespace, const char* key) { + if (key == nullptr || key[0] == '\0') { + return ESP_ERR_INVALID_ARG; + } + + nvs_handle_t handle; + esp_err_t err = nvs_open(nvs_namespace.c_str(), NVS_READWRITE, &handle); + if (err != ESP_OK) { + return err; + } + + err = nvs_erase_key(handle, key); + if (err == ESP_ERR_NVS_NOT_FOUND) { + err = ESP_OK; + } + if (err == ESP_OK) { + err = nvs_commit(handle); + } + nvs_close(handle); + return err; +} + } // namespace BridgeRuntimeConfig BridgeRuntimeConfig::fromJson(const DaliValue::Object& json) { @@ -182,33 +274,7 @@ DaliValue::Object BridgeRuntimeConfig::toJson() const { } esp_err_t BridgeProvisioningStore::save(const BridgeRuntimeConfig& config) const { - nvs_handle_t handle; - esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READWRITE, &handle); - if (err != ESP_OK) { - ESP_LOGE(kTag, "nvs_open(save) failed: %s", esp_err_to_name(err)); - return err; - } - - cJSON* root = toCjson(DaliValue(config.toJson())); - char* raw = cJSON_PrintUnformatted(root); - if (raw == nullptr) { - cJSON_Delete(root); - nvs_close(handle); - return ESP_ERR_NO_MEM; - } - - err = nvs_set_str(handle, kKeyConfig, raw); - if (err == ESP_OK) { - err = nvs_commit(handle); - } - - cJSON_Delete(root); - cJSON_free(raw); - nvs_close(handle); - if (err != ESP_OK) { - ESP_LOGE(kTag, "save failed: %s", esp_err_to_name(err)); - } - return err; + return saveJsonObject(nvsNamespace_, kKeyConfig, config.toJson()); } esp_err_t BridgeProvisioningStore::load(BridgeRuntimeConfig* config) const { @@ -216,51 +282,31 @@ esp_err_t BridgeProvisioningStore::load(BridgeRuntimeConfig* config) const { return ESP_ERR_INVALID_ARG; } - nvs_handle_t handle; - esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READONLY, &handle); + DaliValue::Object object; + const esp_err_t err = loadJsonObject(nvsNamespace_, kKeyConfig, &object); if (err != ESP_OK) { return err; } - std::string payload; - err = readString(handle, kKeyConfig, &payload); - nvs_close(handle); - if (err != ESP_OK) { - return err; - } - - cJSON* root = cJSON_Parse(payload.c_str()); - if (root == nullptr) { - return ESP_ERR_INVALID_RESPONSE; - } - - const DaliValue value = fromCjson(root); - cJSON_Delete(root); - const auto* object = value.asObject(); - if (object == nullptr) { - return ESP_ERR_INVALID_RESPONSE; - } - - *config = BridgeRuntimeConfig::fromJson(*object); + *config = BridgeRuntimeConfig::fromJson(object); return ESP_OK; } esp_err_t BridgeProvisioningStore::clear() const { - nvs_handle_t handle; - esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READWRITE, &handle); - if (err != ESP_OK) { - return err; - } + return clearStoredKey(nvsNamespace_, kKeyConfig); +} - err = nvs_erase_key(handle, kKeyConfig); - if (err == ESP_ERR_NVS_NOT_FOUND) { - err = ESP_OK; - } - if (err == ESP_OK) { - err = nvs_commit(handle); - } - nvs_close(handle); - return err; +esp_err_t BridgeProvisioningStore::saveObject(const char* key, + const DaliValue::Object& object) const { + return saveJsonObject(nvsNamespace_, key, object); +} + +esp_err_t BridgeProvisioningStore::loadObject(const char* key, DaliValue::Object* object) const { + return loadJsonObject(nvsNamespace_, key, object); +} + +esp_err_t BridgeProvisioningStore::clearKey(const char* key) const { + return clearStoredKey(nvsNamespace_, key); } #else @@ -284,4 +330,22 @@ esp_err_t BridgeProvisioningStore::load(BridgeRuntimeConfig* config) const { esp_err_t BridgeProvisioningStore::clear() const { return -1; } +esp_err_t BridgeProvisioningStore::saveObject(const char* key, + const DaliValue::Object& object) const { + (void)key; + (void)object; + return -1; +} + +esp_err_t BridgeProvisioningStore::loadObject(const char* key, DaliValue::Object* object) const { + (void)key; + (void)object; + return -1; +} + +esp_err_t BridgeProvisioningStore::clearKey(const char* key) const { + (void)key; + return -1; +} + #endif \ No newline at end of file