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:
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user