Implement DALI Bridge Engine and Model Management

- Added `bridge.cpp` to handle DALI bridge operations including model management, command execution, and response formatting.
- Introduced `bridge_model.cpp` for defining bridge models, value transformations, and JSON serialization/deserialization.
- Created `bridge_provisioning.cpp` for managing bridge configuration storage and retrieval using NVS on ESP platform.
- Enhanced `gateway_cloud.cpp` to integrate DALI bridge requests and responses with cloud communication.
- Introduced `modbus_bridge.cpp` to handle Modbus-specific operations and register management.
- Implemented utility functions for converting between DaliValue and cJSON formats.
- Added error handling and metadata management in bridge responses.
This commit is contained in:
Tony
2026-04-22 10:14:42 +08:00
parent ec5f36b581
commit 21afc6b942
22 changed files with 2045 additions and 35 deletions
+63
View File
@@ -0,0 +1,63 @@
#include "bacnet_bridge.hpp"
#include <utility>
DaliBacnetBridge::DaliBacnetBridge(DaliBridgeEngine& engine) : engine_(engine) {}
void DaliBacnetBridge::setConfig(const BacnetBridgeConfig& config) { config_ = config; }
const BacnetBridgeConfig& DaliBacnetBridge::config() const { return config_; }
DaliBridgeResult DaliBacnetBridge::handlePropertyWrite(BridgeObjectType objectType,
int objectInstance,
const std::string& property,
const DaliValue& value) const {
const auto binding = findObject(objectType, objectInstance, property);
DaliBridgeRequest request;
request.sequence = "bacnet-" + std::to_string(objectInstance);
request.value = value;
if (!binding.has_value()) {
DaliBridgeResult result;
result.sequence = request.sequence;
result.error = "unmapped bacnet object";
return result;
}
request.modelID = binding->modelID;
return engine_.execute(request);
}
std::optional<BacnetObjectBinding> DaliBacnetBridge::findObject(BridgeObjectType objectType,
int objectInstance,
const std::string& property) const {
for (const auto& model : engine_.listModels()) {
if (model.protocol != BridgeProtocolKind::bacnet) {
continue;
}
if (model.external.objectType != objectType) {
continue;
}
if (model.external.objectInstance.value_or(-1) != objectInstance) {
continue;
}
if (!model.external.property.empty() && model.external.property != property) {
continue;
}
return BacnetObjectBinding{model.id, objectType, objectInstance, property};
}
return std::nullopt;
}
std::vector<BacnetObjectBinding> DaliBacnetBridge::describeObjects() const {
std::vector<BacnetObjectBinding> bindings;
for (const auto& model : engine_.listModels()) {
if (model.protocol != BridgeProtocolKind::bacnet || !model.external.objectInstance.has_value()) {
continue;
}
bindings.push_back(BacnetObjectBinding{model.id, model.external.objectType,
model.external.objectInstance.value(),
model.external.property});
}
return bindings;
}
+419
View File
@@ -0,0 +1,419 @@
#include "bridge.hpp"
#include <cmath>
#include <utility>
namespace {
void addStatusMetadata(DaliBridgeResult* result, int rawStatus) {
const DaliStatus status = DaliStatus::fromByte(static_cast<uint8_t>(rawStatus));
result->metadata["controlGearPresent"] = status.controlGearPresent;
result->metadata["lampFailure"] = status.lampFailure;
result->metadata["lampPowerOn"] = status.lampPowerOn;
result->metadata["limitError"] = status.limitError;
result->metadata["fadingCompleted"] = status.fadingCompleted;
result->metadata["resetState"] = status.resetState;
result->metadata["missingShortAddress"] = status.missingShortAddress;
result->metadata["psFault"] = status.psFault;
}
} // namespace
DaliValue::Object DaliBridgeResult::toJson() const {
DaliValue::Object out;
out["type"] = "dali_resp";
out["seq"] = sequence;
if (!modelID.empty()) out["model"] = modelID;
out["op"] = bridgeOperationToString(operation);
out["ok"] = ok;
if (data.has_value()) out["data"] = data.value();
if (!error.empty()) out["error"] = error;
if (!metadata.empty()) out["meta"] = metadata;
return out;
}
DaliBridgeEngine::DaliBridgeEngine(DaliComm& comm) : comm_(comm), base_(comm), dt1_(base_), dt8_(base_) {}
bool DaliBridgeEngine::upsertModel(const BridgeModel& model) {
if (model.id.empty()) {
return false;
}
models_[model.id] = model;
return true;
}
bool DaliBridgeEngine::removeModel(const std::string& modelID) {
return models_.erase(modelID) > 0;
}
const BridgeModel* DaliBridgeEngine::findModel(const std::string& modelID) const {
const auto it = models_.find(modelID);
if (it == models_.end()) {
return nullptr;
}
return &it->second;
}
std::vector<BridgeModel> DaliBridgeEngine::listModels() const {
std::vector<BridgeModel> out;
out.reserve(models_.size());
for (const auto& entry : models_) {
out.push_back(entry.second);
}
return out;
}
DaliBridgeResult DaliBridgeEngine::execute(const DaliBridgeRequest& request) {
const BridgeModel* model = nullptr;
if (!request.modelID.empty()) {
model = findModel(request.modelID);
if (model == nullptr) {
DaliBridgeResult result;
result.sequence = request.sequence;
result.modelID = request.modelID;
result.error = "unknown model";
return result;
}
}
const BridgeOperation operation =
request.operation.value_or(model != nullptr ? model->operation : BridgeOperation::unknown);
if (operation == BridgeOperation::unknown) {
DaliBridgeResult result;
result.sequence = request.sequence;
result.modelID = request.modelID;
result.error = "missing operation";
return result;
}
return executeResolved(request, model, operation);
}
DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& request,
const BridgeModel* model,
BridgeOperation operation) {
DaliBridgeResult result;
result.sequence = request.sequence;
result.modelID = model != nullptr ? model->id : request.modelID;
result.operation = operation;
if (model != nullptr) {
result.metadata["protocol"] = bridgeProtocolKindToString(model->protocol);
result.metadata["modelName"] = model->displayName();
}
switch (operation) {
case BridgeOperation::send:
case BridgeOperation::sendExt:
case BridgeOperation::query: {
const auto addr = resolveRawAddress(request, model);
const auto cmd = resolveRawCommand(request, model);
if (!addr.has_value() || !cmd.has_value() || addr.value() < 0 || addr.value() > 255 ||
cmd.value() < 0 || cmd.value() > 255) {
result.error = "invalid addr/cmd";
return result;
}
if (operation == BridgeOperation::send) {
result.ok = comm_.sendRaw(static_cast<uint8_t>(addr.value()), static_cast<uint8_t>(cmd.value()));
} else if (operation == BridgeOperation::sendExt) {
result.ok =
comm_.sendExtRaw(static_cast<uint8_t>(addr.value()), static_cast<uint8_t>(cmd.value()));
} else {
const auto response =
comm_.queryRaw(static_cast<uint8_t>(addr.value()), static_cast<uint8_t>(cmd.value()));
if (!response.has_value()) {
result.error = "no response";
return result;
}
result.ok = true;
result.data = static_cast<int>(response.value());
}
if (!result.ok && result.error.empty()) {
result.error = "dispatch failed";
}
return result;
}
case BridgeOperation::setBrightness: {
const auto address = resolveShortAddress(request, model);
const auto value = resolveIntValue(request, model);
if (!address.has_value() || !value.has_value()) {
result.error = "missing address/value";
return result;
}
result.ok = base_.setBright(address.value(), value.value());
result.data = value;
break;
}
case BridgeOperation::setBrightnessPercent: {
const auto address = resolveShortAddress(request, model);
const auto value = resolveDoubleValue(request);
if (!address.has_value() || !value.has_value()) {
result.error = "missing address/value";
return result;
}
result.ok = base_.setBrightPercentage(address.value(), value.value());
result.data = static_cast<int>(std::lround(value.value()));
break;
}
case BridgeOperation::on: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
result.ok = base_.on(address.value());
break;
}
case BridgeOperation::off: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
result.ok = base_.off(address.value());
break;
}
case BridgeOperation::recallMaxLevel: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
result.ok = base_.recallMaxLevel(address.value());
break;
}
case BridgeOperation::recallMinLevel: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
result.ok = base_.recallMinLevel(address.value());
break;
}
case BridgeOperation::setColorTemperature: {
const auto address = resolveShortAddress(request, model);
const auto value = resolveIntValue(request, model);
if (!address.has_value() || !value.has_value()) {
result.error = "missing address/value";
return result;
}
result.ok = dt8_.setColorTemperature(address.value(), value.value());
result.data = value;
break;
}
case BridgeOperation::getBrightness: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
const auto value = base_.getBright(address.value());
if (!value.has_value()) {
result.error = "no response";
return result;
}
result.ok = true;
result.data = value;
break;
}
case BridgeOperation::getStatus: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
const auto value = base_.getStatus(address.value());
if (!value.has_value()) {
result.error = "no response";
return result;
}
result.ok = true;
result.data = value;
addStatusMetadata(&result, value.value());
break;
}
case BridgeOperation::getColorTemperature: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
const auto value = dt8_.getColorTemperature(address.value());
if (!value.has_value()) {
result.error = "no response";
return result;
}
result.ok = true;
result.data = value;
break;
}
case BridgeOperation::getColorStatus: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
const auto value = dt8_.getColorStatus(address.value());
if (!value.has_value()) {
result.error = "no response";
return result;
}
result.ok = true;
result.data = value->raw();
result.metadata["xyOutOfRange"] = value->xyOutOfRange();
result.metadata["ctOutOfRange"] = value->ctOutOfRange();
result.metadata["autoCalibrationActive"] = value->autoCalibrationActive();
result.metadata["autoCalibrationSuccess"] = value->autoCalibrationSuccess();
result.metadata["xyActive"] = value->xyActive();
result.metadata["ctActive"] = value->ctActive();
result.metadata["primaryNActive"] = value->primaryNActive();
result.metadata["rgbwafActive"] = value->rgbwafActive();
break;
}
case BridgeOperation::getEmergencyLevel: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
const auto value = dt1_.getEmergencyLevel(address.value());
if (!value.has_value()) {
result.error = "no response";
return result;
}
result.ok = true;
result.data = value;
break;
}
case BridgeOperation::getEmergencyStatus: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
const auto status = dt1_.getEmergencyStatusDecoded(address.value());
if (!status.has_value()) {
result.error = "no response";
return result;
}
result.ok = true;
result.data = status->raw();
result.metadata["inhibitMode"] = status->inhibitMode();
result.metadata["functionTestResultValid"] = status->functionTestResultValid();
result.metadata["durationTestResultValid"] = status->durationTestResultValid();
result.metadata["batteryFullyCharged"] = status->batteryFullyCharged();
result.metadata["functionTestRequestPending"] = status->functionTestRequestPending();
result.metadata["durationTestRequestPending"] = status->durationTestRequestPending();
result.metadata["identificationActive"] = status->identificationActive();
result.metadata["physicallySelected"] = status->physicallySelected();
break;
}
case BridgeOperation::getEmergencyFailureStatus: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
const auto status = dt1_.getDT1TestStatusDetailed(address.value());
if (!status.has_value()) {
result.error = "no response";
return result;
}
result.ok = true;
result.data = status->failureStatus;
if (status->emergencyStatus.has_value()) result.metadata["emergencyStatus"] = status->emergencyStatus.value();
if (status->emergencyMode.has_value()) result.metadata["emergencyMode"] = status->emergencyMode.value();
if (status->feature.has_value()) result.metadata["feature"] = status->feature.value();
result.metadata["testInProgress"] = status->testInProgress;
result.metadata["lampFailure"] = status->lampFailure;
result.metadata["batteryFailure"] = status->batteryFailure;
result.metadata["functionTestActive"] = status->functionTestActive;
result.metadata["durationTestActive"] = status->durationTestActive;
result.metadata["testDone"] = status->testDone;
result.metadata["identifyActive"] = status->identifyActive;
result.metadata["physicalSelectionActive"] = status->physicalSelectionActive;
break;
}
case BridgeOperation::startEmergencyFunctionTest: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
result.ok = dt1_.startFunctionTestCmd(address.value());
break;
}
case BridgeOperation::stopEmergencyTest: {
const auto address = resolveShortAddress(request, model);
if (!address.has_value()) {
result.error = "missing short address";
return result;
}
result.ok = dt1_.stopTest(address.value());
break;
}
case BridgeOperation::unknown:
default:
result.error = "unsupported op";
return result;
}
if (!result.ok && result.error.empty()) {
result.error = "dispatch failed";
}
return result;
}
std::optional<int> DaliBridgeEngine::resolveShortAddress(const DaliBridgeRequest& request,
const BridgeModel* model) const {
if (request.shortAddress.has_value()) {
return request.shortAddress;
}
if (model != nullptr) {
return model->dali.shortAddress;
}
return std::nullopt;
}
std::optional<int> DaliBridgeEngine::resolveRawAddress(const DaliBridgeRequest& request,
const BridgeModel* model) const {
if (request.rawAddress.has_value()) {
return request.rawAddress;
}
if (model != nullptr) {
return model->dali.rawAddress;
}
return std::nullopt;
}
std::optional<int> DaliBridgeEngine::resolveRawCommand(const DaliBridgeRequest& request,
const BridgeModel* model) const {
if (request.rawCommand.has_value()) {
return request.rawCommand;
}
if (model != nullptr) {
return model->dali.rawCommand;
}
return std::nullopt;
}
std::optional<int> DaliBridgeEngine::resolveIntValue(const DaliBridgeRequest& request,
const BridgeModel* model) const {
if (request.value.isNull()) {
return std::nullopt;
}
if (model != nullptr) {
const auto number = request.value.asDouble();
if (!number.has_value()) {
return std::nullopt;
}
return model->valueTransform.apply(number.value());
}
return request.value.asInt();
}
std::optional<double> DaliBridgeEngine::resolveDoubleValue(const DaliBridgeRequest& request) const {
return request.value.asDouble();
}
+305
View File
@@ -0,0 +1,305 @@
#include "bridge_model.hpp"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <utility>
namespace {
std::string normalize(const std::string& value) {
std::string out = value;
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char ch) {
if (ch == '-' || ch == ' ') {
return static_cast<char>('_');
}
return static_cast<char>(std::tolower(ch));
});
return out;
}
} // namespace
BridgeValueTransform BridgeValueTransform::fromJson(const DaliValue::Object* json) {
BridgeValueTransform transform;
if (json == nullptr) {
return transform;
}
if (const auto scale = getObjectValue(*json, "scale")) {
transform.scale = scale->asDouble().value_or(1.0);
}
if (const auto offset = getObjectValue(*json, "offset")) {
transform.offset = offset->asDouble().value_or(0.0);
}
transform.clampMin = getObjectInt(*json, "clampMin");
transform.clampMax = getObjectInt(*json, "clampMax");
return transform;
}
DaliValue::Object BridgeValueTransform::toJson() const {
DaliValue::Object out;
out["scale"] = scale;
out["offset"] = offset;
if (clampMin.has_value()) out["clampMin"] = clampMin.value();
if (clampMax.has_value()) out["clampMax"] = clampMax.value();
return out;
}
int BridgeValueTransform::apply(double raw) const {
int value = static_cast<int>(std::lround((raw * scale) + offset));
if (clampMin.has_value() && value < clampMin.value()) {
value = clampMin.value();
}
if (clampMax.has_value() && value > clampMax.value()) {
value = clampMax.value();
}
return value;
}
BridgeExternalPoint BridgeExternalPoint::fromJson(const DaliValue::Object* json) {
BridgeExternalPoint point;
if (json == nullptr) {
return point;
}
point.network = getObjectString(*json, "network").value_or("");
point.device = getObjectString(*json, "device").value_or("");
point.objectType = bridgeObjectTypeFromString(getObjectString(*json, "objectType").value_or(""));
point.objectInstance = getObjectInt(*json, "objectInstance");
point.registerAddress = getObjectInt(*json, "registerAddress");
point.bitIndex = getObjectInt(*json, "bitIndex");
point.property = getObjectString(*json, "property").value_or("");
return point;
}
DaliValue::Object BridgeExternalPoint::toJson() const {
DaliValue::Object out;
if (!network.empty()) out["network"] = network;
if (!device.empty()) out["device"] = device;
out["objectType"] = bridgeObjectTypeToString(objectType);
if (objectInstance.has_value()) out["objectInstance"] = objectInstance.value();
if (registerAddress.has_value()) out["registerAddress"] = registerAddress.value();
if (bitIndex.has_value()) out["bitIndex"] = bitIndex.value();
if (!property.empty()) out["property"] = property;
return out;
}
BridgeDaliTarget BridgeDaliTarget::fromJson(const DaliValue::Object* json) {
BridgeDaliTarget target;
if (json == nullptr) {
return target;
}
target.shortAddress = getObjectInt(*json, "shortAddress");
target.rawAddress = getObjectInt(*json, "rawAddress");
target.rawCommand = getObjectInt(*json, "rawCommand");
return target;
}
DaliValue::Object BridgeDaliTarget::toJson() const {
DaliValue::Object out;
if (shortAddress.has_value()) out["shortAddress"] = shortAddress.value();
if (rawAddress.has_value()) out["rawAddress"] = rawAddress.value();
if (rawCommand.has_value()) out["rawCommand"] = rawCommand.value();
return out;
}
BridgeModel BridgeModel::fromJson(const DaliValue::Object& json) {
BridgeModel model;
model.id = getObjectString(json, "id").value_or("");
model.name = getObjectString(json, "name").value_or(model.id);
model.protocol = bridgeProtocolKindFromString(getObjectString(json, "protocol").value_or(""));
model.operation = bridgeOperationFromString(getObjectString(json, "operation").value_or(""));
model.valueEncoding =
bridgeValueEncodingFromString(getObjectString(json, "valueEncoding").value_or(""));
if (const auto* external = getObjectValue(json, "external")) {
model.external = BridgeExternalPoint::fromJson(external->asObject());
}
if (const auto* dali = getObjectValue(json, "dali")) {
model.dali = BridgeDaliTarget::fromJson(dali->asObject());
}
if (const auto* transform = getObjectValue(json, "valueTransform")) {
model.valueTransform = BridgeValueTransform::fromJson(transform->asObject());
}
if (const auto* metadata = getObjectValue(json, "meta")) {
if (const auto* object = metadata->asObject()) {
model.metadata = *object;
}
}
if (model.name.empty()) {
model.name = model.id;
}
return model;
}
DaliValue::Object BridgeModel::toJson() const {
DaliValue::Object out;
out["id"] = id;
out["name"] = name;
out["protocol"] = bridgeProtocolKindToString(protocol);
out["external"] = external.toJson();
out["dali"] = dali.toJson();
out["operation"] = bridgeOperationToString(operation);
out["valueEncoding"] = bridgeValueEncodingToString(valueEncoding);
out["valueTransform"] = valueTransform.toJson();
if (!metadata.empty()) out["meta"] = metadata;
return out;
}
std::string BridgeModel::displayName() const {
return name.empty() ? id : name;
}
const char* bridgeProtocolKindToString(BridgeProtocolKind kind) {
switch (kind) {
case BridgeProtocolKind::mqtt:
return "mqtt";
case BridgeProtocolKind::modbus:
return "modbus";
case BridgeProtocolKind::bacnet:
return "bacnet";
case BridgeProtocolKind::unknown:
default:
return "unknown";
}
}
BridgeProtocolKind bridgeProtocolKindFromString(const std::string& value) {
const std::string normalized = normalize(value);
if (normalized == "mqtt") return BridgeProtocolKind::mqtt;
if (normalized == "modbus") return BridgeProtocolKind::modbus;
if (normalized == "bacnet") return BridgeProtocolKind::bacnet;
return BridgeProtocolKind::unknown;
}
const char* bridgeObjectTypeToString(BridgeObjectType type) {
switch (type) {
case BridgeObjectType::holdingRegister:
return "holding_register";
case BridgeObjectType::inputRegister:
return "input_register";
case BridgeObjectType::coil:
return "coil";
case BridgeObjectType::discreteInput:
return "discrete_input";
case BridgeObjectType::analogValue:
return "analog_value";
case BridgeObjectType::analogOutput:
return "analog_output";
case BridgeObjectType::binaryValue:
return "binary_value";
case BridgeObjectType::binaryOutput:
return "binary_output";
case BridgeObjectType::multiStateValue:
return "multi_state_value";
case BridgeObjectType::unknown:
default:
return "unknown";
}
}
BridgeObjectType bridgeObjectTypeFromString(const std::string& value) {
const std::string normalized = normalize(value);
if (normalized == "holding_register") return BridgeObjectType::holdingRegister;
if (normalized == "input_register") return BridgeObjectType::inputRegister;
if (normalized == "coil") return BridgeObjectType::coil;
if (normalized == "discrete_input") return BridgeObjectType::discreteInput;
if (normalized == "analog_value") return BridgeObjectType::analogValue;
if (normalized == "analog_output") return BridgeObjectType::analogOutput;
if (normalized == "binary_value") return BridgeObjectType::binaryValue;
if (normalized == "binary_output") return BridgeObjectType::binaryOutput;
if (normalized == "multi_state_value") return BridgeObjectType::multiStateValue;
return BridgeObjectType::unknown;
}
const char* bridgeOperationToString(BridgeOperation operation) {
switch (operation) {
case BridgeOperation::send:
return "send";
case BridgeOperation::sendExt:
return "send_ext";
case BridgeOperation::query:
return "query";
case BridgeOperation::setBrightness:
return "set_brightness";
case BridgeOperation::setBrightnessPercent:
return "set_brightness_percent";
case BridgeOperation::on:
return "on";
case BridgeOperation::off:
return "off";
case BridgeOperation::recallMaxLevel:
return "recall_max_level";
case BridgeOperation::recallMinLevel:
return "recall_min_level";
case BridgeOperation::setColorTemperature:
return "set_color_temperature";
case BridgeOperation::getBrightness:
return "get_brightness";
case BridgeOperation::getStatus:
return "get_status";
case BridgeOperation::getColorTemperature:
return "get_color_temperature";
case BridgeOperation::getColorStatus:
return "get_color_status";
case BridgeOperation::getEmergencyLevel:
return "get_emergency_level";
case BridgeOperation::getEmergencyStatus:
return "get_emergency_status";
case BridgeOperation::getEmergencyFailureStatus:
return "get_emergency_failure_status";
case BridgeOperation::startEmergencyFunctionTest:
return "start_emergency_function_test";
case BridgeOperation::stopEmergencyTest:
return "stop_emergency_test";
case BridgeOperation::unknown:
default:
return "unknown";
}
}
BridgeOperation bridgeOperationFromString(const std::string& value) {
const std::string normalized = normalize(value);
if (normalized == "send") return BridgeOperation::send;
if (normalized == "send_ext") return BridgeOperation::sendExt;
if (normalized == "query") return BridgeOperation::query;
if (normalized == "set_brightness") return BridgeOperation::setBrightness;
if (normalized == "set_brightness_percent") return BridgeOperation::setBrightnessPercent;
if (normalized == "on") return BridgeOperation::on;
if (normalized == "off") return BridgeOperation::off;
if (normalized == "recall_max_level") return BridgeOperation::recallMaxLevel;
if (normalized == "recall_min_level") return BridgeOperation::recallMinLevel;
if (normalized == "set_color_temperature") return BridgeOperation::setColorTemperature;
if (normalized == "get_brightness") return BridgeOperation::getBrightness;
if (normalized == "get_status") return BridgeOperation::getStatus;
if (normalized == "get_color_temperature") return BridgeOperation::getColorTemperature;
if (normalized == "get_color_status") return BridgeOperation::getColorStatus;
if (normalized == "get_emergency_level") return BridgeOperation::getEmergencyLevel;
if (normalized == "get_emergency_status") return BridgeOperation::getEmergencyStatus;
if (normalized == "get_emergency_failure_status") return BridgeOperation::getEmergencyFailureStatus;
if (normalized == "start_emergency_function_test") {
return BridgeOperation::startEmergencyFunctionTest;
}
if (normalized == "stop_emergency_test") return BridgeOperation::stopEmergencyTest;
return BridgeOperation::unknown;
}
const char* bridgeValueEncodingToString(BridgeValueEncoding encoding) {
switch (encoding) {
case BridgeValueEncoding::integer:
return "integer";
case BridgeValueEncoding::percentage:
return "percentage";
case BridgeValueEncoding::kelvin:
return "kelvin";
case BridgeValueEncoding::none:
default:
return "none";
}
}
BridgeValueEncoding bridgeValueEncodingFromString(const std::string& value) {
const std::string normalized = normalize(value);
if (normalized == "integer") return BridgeValueEncoding::integer;
if (normalized == "percentage") return BridgeValueEncoding::percentage;
if (normalized == "kelvin") return BridgeValueEncoding::kelvin;
return BridgeValueEncoding::none;
}
+287
View File
@@ -0,0 +1,287 @@
#include "bridge_provisioning.hpp"
#include <utility>
#ifdef ESP_PLATFORM
extern "C" {
#include "cJSON.h"
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
}
namespace {
constexpr const char* kTag = "bridge_provision";
constexpr const char* kKeyConfig = "bridge_cfg";
cJSON* toCjson(const DaliValue& value) {
if (value.isNull()) {
return cJSON_CreateNull();
}
if (value.isBool()) {
return cJSON_CreateBool(value.asBool().value_or(false));
}
if (value.isInt()) {
return cJSON_CreateNumber(value.asInt().value_or(0));
}
if (value.isDouble()) {
return cJSON_CreateNumber(value.asDouble().value_or(0.0));
}
if (value.isString()) {
return cJSON_CreateString(value.asString().value_or("").c_str());
}
if (const auto* array = value.asArray()) {
cJSON* out = cJSON_CreateArray();
for (const auto& item : *array) {
cJSON_AddItemToArray(out, toCjson(item));
}
return out;
}
if (const auto* object = value.asObject()) {
cJSON* out = cJSON_CreateObject();
for (const auto& entry : *object) {
cJSON_AddItemToObject(out, entry.first.c_str(), toCjson(entry.second));
}
return out;
}
return cJSON_CreateNull();
}
DaliValue fromCjson(const cJSON* item) {
if (item == nullptr || cJSON_IsNull(item)) {
return DaliValue();
}
if (cJSON_IsBool(item)) {
return DaliValue(cJSON_IsTrue(item));
}
if (cJSON_IsNumber(item)) {
if (item->valuedouble == static_cast<double>(item->valueint)) {
return DaliValue(item->valueint);
}
return DaliValue(item->valuedouble);
}
if (cJSON_IsString(item) && item->valuestring != nullptr) {
return DaliValue(std::string(item->valuestring));
}
if (cJSON_IsArray(item)) {
DaliValue::Array out;
for (const cJSON* child = item->child; child != nullptr; child = child->next) {
out.push_back(fromCjson(child));
}
return DaliValue(std::move(out));
}
if (cJSON_IsObject(item)) {
DaliValue::Object out;
for (const cJSON* child = item->child; child != nullptr; child = child->next) {
if (child->string != nullptr) {
out[child->string] = fromCjson(child);
}
}
return DaliValue(std::move(out));
}
return DaliValue();
}
esp_err_t readString(nvs_handle_t handle, const char* key, std::string* value) {
size_t required = 0;
esp_err_t err = nvs_get_str(handle, key, nullptr, &required);
if (err != ESP_OK) {
return err;
}
std::string buffer(required, '\0');
err = nvs_get_str(handle, key, buffer.data(), &required);
if (err != ESP_OK) {
return err;
}
if (!buffer.empty() && buffer.back() == '\0') {
buffer.pop_back();
}
*value = buffer;
return ESP_OK;
}
std::optional<ModbusBridgeConfig> modbusFromJson(const DaliValue* value) {
if (value == nullptr || value->asObject() == nullptr) {
return std::nullopt;
}
const auto& json = *value->asObject();
ModbusBridgeConfig config;
config.transport = getObjectString(json, "transport").value_or("tcp");
config.host = getObjectString(json, "host").value_or("");
config.port = static_cast<uint16_t>(getObjectInt(json, "port").value_or(502));
config.unitID = static_cast<uint8_t>(getObjectInt(json, "unitID").value_or(1));
return config;
}
DaliValue modbusToJson(const ModbusBridgeConfig& config) {
DaliValue::Object out;
out["transport"] = config.transport;
out["host"] = config.host;
out["port"] = static_cast<int>(config.port);
out["unitID"] = static_cast<int>(config.unitID);
return DaliValue(std::move(out));
}
std::optional<BacnetBridgeConfig> bacnetFromJson(const DaliValue* value) {
if (value == nullptr || value->asObject() == nullptr) {
return std::nullopt;
}
const auto& json = *value->asObject();
BacnetBridgeConfig config;
config.deviceInstance = static_cast<uint32_t>(getObjectInt(json, "deviceInstance").value_or(4194303));
config.localAddress = getObjectString(json, "localAddress").value_or("");
config.udpPort = static_cast<uint16_t>(getObjectInt(json, "udpPort").value_or(47808));
return config;
}
DaliValue bacnetToJson(const BacnetBridgeConfig& config) {
DaliValue::Object out;
out["deviceInstance"] = static_cast<int64_t>(config.deviceInstance);
out["localAddress"] = config.localAddress;
out["udpPort"] = static_cast<int>(config.udpPort);
return DaliValue(std::move(out));
}
} // namespace
BridgeRuntimeConfig BridgeRuntimeConfig::fromJson(const DaliValue::Object& json) {
BridgeRuntimeConfig config;
if (const auto* modelsValue = getObjectValue(json, "models")) {
if (const auto* models = modelsValue->asArray()) {
config.models.reserve(models->size());
for (const auto& modelValue : *models) {
if (const auto* object = modelValue.asObject()) {
config.models.push_back(BridgeModel::fromJson(*object));
}
}
}
}
config.modbus = modbusFromJson(getObjectValue(json, "modbus"));
config.bacnet = bacnetFromJson(getObjectValue(json, "bacnet"));
if (const auto* metadata = getObjectValue(json, "meta")) {
if (const auto* object = metadata->asObject()) {
config.metadata = *object;
}
}
return config;
}
DaliValue::Object BridgeRuntimeConfig::toJson() const {
DaliValue::Object out;
DaliValue::Array modelsValue;
modelsValue.reserve(models.size());
for (const auto& model : models) {
modelsValue.emplace_back(model.toJson());
}
out["models"] = std::move(modelsValue);
if (modbus.has_value()) out["modbus"] = modbusToJson(modbus.value());
if (bacnet.has_value()) out["bacnet"] = bacnetToJson(bacnet.value());
if (!metadata.empty()) out["meta"] = metadata;
return out;
}
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;
}
esp_err_t BridgeProvisioningStore::load(BridgeRuntimeConfig* config) const {
if (config == nullptr) {
return ESP_ERR_INVALID_ARG;
}
nvs_handle_t handle;
esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READONLY, &handle);
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);
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;
}
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;
}
#else
BridgeRuntimeConfig BridgeRuntimeConfig::fromJson(const DaliValue::Object& json) {
(void)json;
return BridgeRuntimeConfig{};
}
DaliValue::Object BridgeRuntimeConfig::toJson() const { return DaliValue::Object{}; }
esp_err_t BridgeProvisioningStore::save(const BridgeRuntimeConfig& config) const {
(void)config;
return -1;
}
esp_err_t BridgeProvisioningStore::load(BridgeRuntimeConfig* config) const {
(void)config;
return -1;
}
esp_err_t BridgeProvisioningStore::clear() const { return -1; }
#endif
+87 -34
View File
@@ -33,10 +33,43 @@ int toInt(const cJSON* item, int fallback) {
return item->valueint;
}
cJSON* toCjson(const DaliValue& value) {
if (value.isNull()) {
return cJSON_CreateNull();
}
if (value.isBool()) {
return cJSON_CreateBool(value.asBool().value_or(false));
}
if (value.isInt()) {
return cJSON_CreateNumber(value.asInt().value_or(0));
}
if (value.isDouble()) {
return cJSON_CreateNumber(value.asDouble().value_or(0.0));
}
if (value.isString()) {
return cJSON_CreateString(value.asString().value_or("").c_str());
}
if (const auto* array = value.asArray()) {
cJSON* out = cJSON_CreateArray();
for (const auto& item : *array) {
cJSON_AddItemToArray(out, toCjson(item));
}
return out;
}
if (const auto* object = value.asObject()) {
cJSON* out = cJSON_CreateObject();
for (const auto& entry : *object) {
cJSON_AddItemToObject(out, entry.first.c_str(), toCjson(entry.second));
}
return out;
}
return cJSON_CreateNull();
}
} // namespace
#endif
DaliCloudBridge::DaliCloudBridge(DaliComm& comm) : comm_(comm) {}
DaliCloudBridge::DaliCloudBridge(DaliComm& comm) : comm_(comm), bridge_(comm) {}
bool DaliCloudBridge::start(const GatewayCloudConfig& config) {
config_ = config;
@@ -137,49 +170,69 @@ bool DaliCloudBridge::handleDownlink(const std::string& payload) {
}
const cJSON* seqItem = cJSON_GetObjectItemCaseSensitive(root, "seq");
const cJSON* modelItem = cJSON_GetObjectItemCaseSensitive(root, "model");
const cJSON* opItem = cJSON_GetObjectItemCaseSensitive(root, "op");
const cJSON* addrItem = cJSON_GetObjectItemCaseSensitive(root, "addr");
const cJSON* cmdItem = cJSON_GetObjectItemCaseSensitive(root, "cmd");
const cJSON* shortAddrItem = cJSON_GetObjectItemCaseSensitive(root, "shortAddress");
const cJSON* valueItem = cJSON_GetObjectItemCaseSensitive(root, "value");
const std::string seq = toString(seqItem);
const std::string op = toString(opItem).empty() ? "send" : toString(opItem);
const int addr = toInt(addrItem, -1);
const int cmd = toInt(cmdItem, -1);
DaliBridgeRequest request;
request.sequence = toString(seqItem);
request.modelID = toString(modelItem);
bool ok = false;
bool hasData = false;
int data = -1;
std::string error = "";
if (addr < 0 || addr > 255 || cmd < 0 || cmd > 255) {
error = "invalid addr/cmd";
} else if (op == "send") {
ok = comm_.sendRaw(static_cast<uint8_t>(addr), static_cast<uint8_t>(cmd));
} else if (op == "send_ext") {
ok = comm_.sendExtRaw(static_cast<uint8_t>(addr), static_cast<uint8_t>(cmd));
} else if (op == "query") {
auto response = comm_.queryRaw(static_cast<uint8_t>(addr), static_cast<uint8_t>(cmd));
if (response.has_value()) {
ok = true;
hasData = true;
data = static_cast<int>(response.value());
} else {
error = "no response";
}
} else {
error = "unsupported op";
const std::string op = toString(opItem);
if (!op.empty()) {
request.operation = bridgeOperationFromString(op);
}
const int addr = toInt(addrItem, -1);
if (addr >= 0) {
request.rawAddress = addr;
}
const int cmd = toInt(cmdItem, -1);
if (cmd >= 0) {
request.rawCommand = cmd;
}
const int shortAddr = toInt(shortAddrItem, -1);
if (shortAddr >= 0) {
request.shortAddress = shortAddr;
}
if (valueItem != nullptr) {
if (cJSON_IsNumber(valueItem)) {
request.value = valueItem->valuedouble;
} else if (cJSON_IsString(valueItem) && valueItem->valuestring != nullptr) {
request.value = std::string(valueItem->valuestring);
} else if (cJSON_IsBool(valueItem)) {
request.value = cJSON_IsTrue(valueItem);
}
}
if (!request.operation.has_value()) {
request.operation = BridgeOperation::send;
}
const DaliBridgeResult result = bridge_.execute(request);
cJSON* resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "type", "dali_resp");
cJSON_AddStringToObject(resp, "seq", seq.c_str());
cJSON_AddStringToObject(resp, "op", op.c_str());
cJSON_AddBoolToObject(resp, "ok", ok);
if (hasData) {
cJSON_AddNumberToObject(resp, "data", data);
cJSON_AddStringToObject(resp, "seq", result.sequence.c_str());
if (!result.modelID.empty()) {
cJSON_AddStringToObject(resp, "model", result.modelID.c_str());
}
if (!ok) {
cJSON_AddStringToObject(resp, "error", error.c_str());
cJSON_AddStringToObject(resp, "op", bridgeOperationToString(result.operation));
cJSON_AddBoolToObject(resp, "ok", result.ok);
if (result.data.has_value()) {
cJSON_AddNumberToObject(resp, "data", result.data.value());
}
if (!result.error.empty()) {
cJSON_AddStringToObject(resp, "error", result.error.c_str());
}
if (!result.metadata.empty()) {
cJSON_AddItemToObject(resp, "meta", toCjson(DaliValue(result.metadata)));
}
char* raw = cJSON_PrintUnformatted(resp);
+55
View File
@@ -0,0 +1,55 @@
#include "modbus_bridge.hpp"
#include <utility>
DaliModbusBridge::DaliModbusBridge(DaliBridgeEngine& engine) : engine_(engine) {}
void DaliModbusBridge::setConfig(const ModbusBridgeConfig& config) { config_ = config; }
const ModbusBridgeConfig& DaliModbusBridge::config() const { return config_; }
DaliBridgeResult DaliModbusBridge::handleHoldingRegisterWrite(int registerAddress, int value) const {
const auto binding = findHoldingRegister(registerAddress);
DaliBridgeRequest request;
request.sequence = "modbus-" + std::to_string(registerAddress);
request.value = value;
if (!binding.has_value()) {
DaliBridgeResult result;
result.sequence = request.sequence;
result.error = "unmapped holding register";
return result;
}
request.modelID = binding->modelID;
return engine_.execute(request);
}
std::optional<ModbusRegisterBinding> DaliModbusBridge::findHoldingRegister(int registerAddress) const {
for (const auto& model : engine_.listModels()) {
if (model.protocol != BridgeProtocolKind::modbus) {
continue;
}
if (model.external.objectType != BridgeObjectType::holdingRegister) {
continue;
}
if (model.external.registerAddress.value_or(-1) != registerAddress) {
continue;
}
return ModbusRegisterBinding{model.id, registerAddress};
}
return std::nullopt;
}
std::vector<ModbusRegisterBinding> DaliModbusBridge::describeHoldingRegisters() const {
std::vector<ModbusRegisterBinding> bindings;
for (const auto& model : engine_.listModels()) {
if (model.protocol != BridgeProtocolKind::modbus ||
model.external.objectType != BridgeObjectType::holdingRegister ||
!model.external.registerAddress.has_value()) {
continue;
}
bindings.push_back(ModbusRegisterBinding{model.id, model.external.registerAddress.value()});
}
return bindings;
}