Files
gateway/components/gateway_bridge/src/gateway_bridge.cpp
T
Tony d16c289626 feat(gateway_network): integrate GatewayBridgeService and add bridge handling
- Updated CMakeLists.txt to require gateway_bridge component.
- Modified GatewayNetworkService to include a pointer to GatewayBridgeService.
- Added new HTTP handlers for bridge GET and POST requests.
- Implemented query utility functions for handling request parameters.
- Enhanced response handling for bridge actions with JSON responses.

Co-authored-by: Copilot <copilot@github.com>
2026-05-01 03:54:02 +08:00

1451 lines
50 KiB
C++

#include "gateway_bridge.hpp"
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
#include "bacnet_bridge.hpp"
#include "gateway_bacnet.hpp"
#endif
#include "bridge.hpp"
#include "bridge_model.hpp"
#include "bridge_provisioning.hpp"
#include "dali_comm.hpp"
#include "dali_domain.hpp"
#include "gateway_cloud.hpp"
#include "gateway_provisioning.hpp"
#include "modbus_bridge.hpp"
#include "cJSON.h"
#include "esp_log.h"
#include "freertos/semphr.h"
#include "lwip/inet.h"
#include "lwip/sockets.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <optional>
#include <set>
#include <string_view>
#include <utility>
#include <vector>
namespace gateway {
namespace {
constexpr const char* kTag = "gateway_bridge";
constexpr int kDefaultModbusPort = 1502;
constexpr size_t kModbusMaxPduBytes = 252;
class LockGuard {
public:
explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) {
if (lock_ != nullptr) {
xSemaphoreTakeRecursive(lock_, portMAX_DELAY);
}
}
~LockGuard() {
if (lock_ != nullptr) {
xSemaphoreGiveRecursive(lock_);
}
}
private:
SemaphoreHandle_t lock_;
};
std::string PrintJson(cJSON* node) {
if (node == nullptr) {
return "{}";
}
char* rendered = cJSON_PrintUnformatted(node);
if (rendered == nullptr) {
return "{}";
}
std::string out(rendered);
cJSON_free(rendered);
return out;
}
GatewayBridgeHttpResponse JsonOk(cJSON* node) {
const std::string body = PrintJson(node);
cJSON_Delete(node);
return GatewayBridgeHttpResponse{ESP_OK, body};
}
GatewayBridgeHttpResponse ErrorResponse(esp_err_t err, const char* message) {
cJSON* root = cJSON_CreateObject();
if (root != nullptr) {
cJSON_AddBoolToObject(root, "ok", false);
cJSON_AddStringToObject(root, "error", message == nullptr ? "error" : message);
cJSON_AddNumberToObject(root, "espErr", static_cast<double>(err));
}
const std::string body = PrintJson(root);
cJSON_Delete(root);
return GatewayBridgeHttpResponse{err, body};
}
const char* JsonString(const cJSON* parent, const char* name) {
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
return cJSON_IsString(item) && item->valuestring != nullptr ? item->valuestring : nullptr;
}
std::optional<int> JsonInt(const cJSON* parent, const char* name) {
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
if (!cJSON_IsNumber(item)) {
return std::nullopt;
}
return item->valueint;
}
std::optional<uint8_t> JsonGatewayId(const cJSON* root) {
if (root == nullptr) {
return std::nullopt;
}
const auto gateway = JsonInt(root, "gw").value_or(JsonInt(root, "gatewayId").value_or(-1));
if (gateway < 0 || gateway > 255) {
return std::nullopt;
}
return static_cast<uint8_t>(gateway);
}
std::string QueryValue(std::string_view query, std::string_view key) {
if (query.empty() || key.empty()) {
return {};
}
size_t start = 0;
while (start < query.size()) {
const size_t end = query.find('&', start);
const size_t segment_end = end == std::string_view::npos ? query.size() : end;
const std::string_view segment = query.substr(start, segment_end - start);
const size_t equals = segment.find('=');
if (equals != std::string_view::npos && segment.substr(0, equals) == key) {
return std::string(segment.substr(equals + 1));
}
if (end == std::string_view::npos) {
break;
}
start = end + 1;
}
return {};
}
std::optional<int> ParseInt(std::string_view raw) {
if (raw.empty()) {
return std::nullopt;
}
std::string text(raw);
char* end = nullptr;
const long parsed = std::strtol(text.c_str(), &end, 10);
if (end == text.c_str() || *end != '\0') {
return std::nullopt;
}
return static_cast<int>(parsed);
}
std::optional<int> QueryInt(std::string_view query, std::string_view primary,
std::string_view fallback = {}) {
auto value = ParseInt(QueryValue(query, primary));
if (value.has_value() || fallback.empty()) {
return value;
}
return ParseInt(QueryValue(query, fallback));
}
std::optional<int> JsonIntAny(const cJSON* parent, const char* primary, const char* fallback) {
auto value = JsonInt(parent, primary);
if (value.has_value() || fallback == nullptr) {
return value;
}
return JsonInt(parent, fallback);
}
bool ValidDaliAddress(int address) {
return address >= 0 && address <= 127;
}
cJSON* IntArrayToCjson(const std::vector<int>& values) {
cJSON* array = cJSON_CreateArray();
if (array == nullptr) {
return nullptr;
}
for (const int value : values) {
cJSON_AddItemToArray(array, cJSON_CreateNumber(value));
}
return array;
}
cJSON* NumberArrayToCjson(const std::vector<double>& values) {
cJSON* array = cJSON_CreateArray();
if (array == nullptr) {
return nullptr;
}
for (const double value : values) {
cJSON_AddItemToArray(array, cJSON_CreateNumber(value));
}
return array;
}
cJSON* SnapshotToCjson(const DaliDomainSnapshot& snapshot) {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON_AddNumberToObject(root, "gatewayId", snapshot.gateway_id);
cJSON_AddNumberToObject(root, "address", snapshot.address);
cJSON_AddStringToObject(root, "kind", snapshot.kind.c_str());
for (const auto& entry : snapshot.bools) {
cJSON_AddBoolToObject(root, entry.first.c_str(), entry.second);
}
for (const auto& entry : snapshot.ints) {
cJSON_AddNumberToObject(root, entry.first.c_str(), entry.second);
}
for (const auto& entry : snapshot.numbers) {
cJSON_AddNumberToObject(root, entry.first.c_str(), entry.second);
}
for (const auto& entry : snapshot.int_arrays) {
cJSON_AddItemToObject(root, entry.first.c_str(), IntArrayToCjson(entry.second));
}
for (const auto& entry : snapshot.number_arrays) {
cJSON_AddItemToObject(root, entry.first.c_str(), NumberArrayToCjson(entry.second));
}
return root;
}
GatewayBridgeHttpResponse SnapshotResponse(const std::optional<DaliDomainSnapshot>& snapshot,
const char* missing_message) {
if (!snapshot.has_value()) {
return ErrorResponse(ESP_ERR_NOT_FOUND, missing_message);
}
return JsonOk(SnapshotToCjson(snapshot.value()));
}
GatewayBridgeHttpResponse StoredSnapshotResponse(
const std::optional<DaliDomainSnapshot>& snapshot, uint8_t gateway_id, int address,
const char* kind) {
if (snapshot.has_value()) {
return JsonOk(SnapshotToCjson(snapshot.value()));
}
cJSON* root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddBoolToObject(root, "stored", true);
cJSON_AddBoolToObject(root, "reportAvailable", false);
cJSON_AddNumberToObject(root, "gatewayId", gateway_id);
cJSON_AddNumberToObject(root, "address", address);
cJSON_AddStringToObject(root, "kind", kind == nullptr ? "dt8_snapshot" : kind);
return JsonOk(root);
}
DaliDt8SceneColorMode JsonColorMode(const cJSON* root) {
const cJSON* item = cJSON_GetObjectItemCaseSensitive(root, "colorMode");
if (item == nullptr) {
item = cJSON_GetObjectItemCaseSensitive(root, "color_mode");
}
if (cJSON_IsNumber(item)) {
if (item->valueint == 1) {
return DaliDt8SceneColorMode::kColorTemperature;
}
if (item->valueint == 2) {
return DaliDt8SceneColorMode::kRgb;
}
return DaliDt8SceneColorMode::kDisabled;
}
if (!cJSON_IsString(item) || item->valuestring == nullptr) {
return DaliDt8SceneColorMode::kDisabled;
}
const std::string_view mode(item->valuestring);
if (mode == "colorTemperature" || mode == "color_temperature" || mode == "ct") {
return DaliDt8SceneColorMode::kColorTemperature;
}
if (mode == "rgb") {
return DaliDt8SceneColorMode::kRgb;
}
return DaliDt8SceneColorMode::kDisabled;
}
GatewayBridgeHttpResponse StoreDt8SceneSnapshot(DaliDomainService& domain, uint8_t gateway_id,
std::string_view body) {
cJSON* root = cJSON_ParseWithLength(body.data(), body.size());
if (root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid DT8 scene snapshot JSON");
}
const auto address = JsonIntAny(root, "addr", "address");
const auto scene = JsonInt(root, "scene");
const auto brightness = JsonInt(root, "brightness");
const auto color_mode = JsonColorMode(root);
const int color_temperature = JsonIntAny(root, "colorTemperature", "color_temperature").value_or(0);
const int red = JsonIntAny(root, "red", "r").value_or(0);
const int green = JsonIntAny(root, "green", "g").value_or(0);
const int blue = JsonIntAny(root, "blue", "b").value_or(0);
cJSON_Delete(root);
if (!address.has_value() || !scene.has_value() || !brightness.has_value() ||
!ValidDaliAddress(address.value()) || scene.value() < 0 || scene.value() > 15) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "addr, scene, and brightness are required");
}
if (!domain.storeDt8SceneSnapshot(gateway_id, address.value(), scene.value(),
brightness.value(), color_mode, color_temperature, red,
green, blue)) {
return ErrorResponse(ESP_FAIL, "failed to store DT8 scene snapshot");
}
return StoredSnapshotResponse(domain.dt8SceneColorReport(gateway_id, address.value(),
scene.value()),
gateway_id, address.value(), "dt8_scene");
}
GatewayBridgeHttpResponse StoreDt8LevelSnapshot(DaliDomainService& domain, uint8_t gateway_id,
std::string_view body, bool power_on) {
cJSON* root = cJSON_ParseWithLength(body.data(), body.size());
if (root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid DT8 level snapshot JSON");
}
const auto address = JsonIntAny(root, "addr", "address");
const auto level = JsonInt(root, "level");
cJSON_Delete(root);
if (!address.has_value() || !level.has_value() || !ValidDaliAddress(address.value())) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "addr and level are required");
}
const bool stored = power_on
? domain.storeDt8PowerOnLevelSnapshot(gateway_id, address.value(),
level.value())
: domain.storeDt8SystemFailureLevelSnapshot(gateway_id, address.value(),
level.value());
if (!stored) {
return ErrorResponse(ESP_FAIL, power_on ? "failed to store DT8 power-on snapshot"
: "failed to store DT8 system-failure snapshot");
}
return StoredSnapshotResponse(power_on ? domain.dt8PowerOnLevelColorReport(gateway_id,
address.value())
: domain.dt8SystemFailureLevelColorReport(
gateway_id, address.value()),
gateway_id, address.value(),
power_on ? "dt8_power_on" : "dt8_system_failure");
}
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)) {
const double value = item->valuedouble;
if (value == static_cast<double>(item->valueint)) {
return DaliValue(item->valueint);
}
return DaliValue(value);
}
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();
}
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();
}
std::string BridgeRuntimeConfigToJson(const BridgeRuntimeConfig& config) {
cJSON* root = ToCjson(DaliValue(config.toJson()));
const std::string body = PrintJson(root);
cJSON_Delete(root);
return body;
}
std::optional<BridgeRuntimeConfig> BridgeRuntimeConfigFromJson(std::string_view json) {
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
if (root == nullptr) {
return std::nullopt;
}
const DaliValue value = FromCjson(root);
cJSON_Delete(root);
const auto* object = value.asObject();
if (object == nullptr) {
return std::nullopt;
}
return BridgeRuntimeConfig::fromJson(*object);
}
GatewayCloudConfig GatewayCloudConfigFromJson(cJSON* root) {
GatewayCloudConfig config;
if (const char* value = JsonString(root, "brokerURI")) {
config.brokerURI = value;
}
if (const char* value = JsonString(root, "deviceID")) {
config.deviceID = value;
}
if (const char* value = JsonString(root, "username")) {
config.username = value;
}
if (const char* value = JsonString(root, "password")) {
config.password = value;
}
if (const char* value = JsonString(root, "topicPrefix")) {
config.topicPrefix = value;
}
if (const auto qos = JsonInt(root, "qos")) {
config.qos = qos.value();
}
return config;
}
cJSON* GatewayCloudConfigToCjson(const GatewayCloudConfig& config) {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON_AddStringToObject(root, "brokerURI", config.brokerURI.c_str());
cJSON_AddStringToObject(root, "deviceID", config.deviceID.c_str());
cJSON_AddStringToObject(root, "username", config.username.c_str());
cJSON_AddStringToObject(root, "password", config.password.c_str());
cJSON_AddStringToObject(root, "topicPrefix", config.topicPrefix.c_str());
cJSON_AddNumberToObject(root, "qos", config.qos);
return root;
}
DaliBridgeRequest BridgeRequestFromJson(cJSON* root) {
DaliBridgeRequest request;
if (const char* seq = JsonString(root, "seq")) {
request.sequence = seq;
}
if (const char* model = JsonString(root, "model")) {
request.modelID = model;
}
if (const char* op = JsonString(root, "op")) {
request.operation = bridgeOperationFromString(op);
}
if (const auto addr = JsonInt(root, "addr")) {
request.rawAddress = addr.value();
}
if (const auto cmd = JsonInt(root, "cmd")) {
request.rawCommand = cmd.value();
}
if (const auto short_address = JsonInt(root, "shortAddress")) {
request.shortAddress = short_address.value();
}
if (const cJSON* value = cJSON_GetObjectItemCaseSensitive(root, "value")) {
request.value = FromCjson(value);
}
if (const cJSON* meta = cJSON_GetObjectItemCaseSensitive(root, "meta")) {
const auto meta_value = FromCjson(meta);
if (const auto* object = meta_value.asObject()) {
request.metadata = *object;
}
}
return request;
}
cJSON* BridgeResultToCjson(const DaliBridgeResult& result) {
return ToCjson(DaliValue(result.toJson()));
}
uint16_t ReadBe16(const uint8_t* data) {
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
}
void WriteBe16(uint8_t* data, uint16_t value) {
data[0] = static_cast<uint8_t>((value >> 8) & 0xFF);
data[1] = static_cast<uint8_t>(value & 0xFF);
}
bool RecvAll(int sock, uint8_t* buffer, size_t len) {
size_t received = 0;
while (received < len) {
const int ret = recv(sock, buffer + received, len - received, 0);
if (ret <= 0) {
return false;
}
received += static_cast<size_t>(ret);
}
return true;
}
bool SendAll(int sock, const uint8_t* buffer, size_t len) {
size_t sent = 0;
while (sent < len) {
const int ret = send(sock, buffer + sent, len - sent, 0);
if (ret <= 0) {
return false;
}
sent += static_cast<size_t>(ret);
}
return true;
}
bool SendModbusFrame(int sock, const uint8_t* mbap, const std::vector<uint8_t>& pdu) {
std::vector<uint8_t> frame(7 + pdu.size());
std::memcpy(frame.data(), mbap, 7);
WriteBe16(&frame[4], static_cast<uint16_t>(pdu.size() + 1));
std::memcpy(frame.data() + 7, pdu.data(), pdu.size());
return SendAll(sock, frame.data(), frame.size());
}
bool SendModbusException(int sock, const uint8_t* mbap, uint8_t function_code,
uint8_t exception_code) {
const std::vector<uint8_t> pdu{static_cast<uint8_t>(function_code | 0x80), exception_code};
return SendModbusFrame(sock, mbap, pdu);
}
int HoldingRegisterFromWireAddress(uint16_t zero_based_address) {
return 40001 + static_cast<int>(zero_based_address);
}
} // namespace
struct GatewayBridgeService::ChannelRuntime {
explicit ChannelRuntime(DaliDomainService& domain, DaliChannelInfo channel,
GatewayBridgeServiceConfig service_config)
: domain(domain), channel(std::move(channel)), service_config(service_config),
lock(xSemaphoreCreateRecursiveMutex()) {}
~ChannelRuntime() {
if (cloud != nullptr) {
cloud->stop();
}
if (lock != nullptr) {
vSemaphoreDelete(lock);
lock = nullptr;
}
}
DaliDomainService& domain;
DaliChannelInfo channel;
GatewayBridgeServiceConfig service_config;
SemaphoreHandle_t lock{nullptr};
std::unique_ptr<DaliComm> comm;
std::unique_ptr<DaliBridgeEngine> engine;
std::unique_ptr<DaliModbusBridge> modbus;
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
std::unique_ptr<DaliBacnetBridge> bacnet;
#endif
std::unique_ptr<DaliCloudBridge> cloud;
BridgeRuntimeConfig bridge_config;
std::optional<GatewayCloudConfig> cloud_config;
bool bridge_config_loaded{false};
bool cloud_config_loaded{false};
bool cloud_started{false};
bool modbus_started{false};
bool bacnet_started{false};
TaskHandle_t modbus_task_handle{nullptr};
static void ModbusTaskEntry(void* arg) {
static_cast<ChannelRuntime*>(arg)->modbusTaskLoop();
}
std::string bridgeNamespace() const {
return "dali_bridge_" + std::to_string(channel.gateway_id);
}
std::string cloudNamespace() const {
return "dali_cloud_" + std::to_string(channel.gateway_id);
}
esp_err_t start() {
comm = std::make_unique<DaliComm>(
[this](const uint8_t* data, size_t len) {
return domain.writeBridgeFrame(channel.gateway_id, data, len);
},
nullptr,
[this](const uint8_t* data, size_t len) {
return domain.transactBridgeFrame(channel.gateway_id, data, len);
},
[](uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); });
BridgeProvisioningStore bridge_store(bridgeNamespace());
bridge_config_loaded = bridge_store.load(&bridge_config) == ESP_OK;
applyBridgeConfigLocked();
GatewayProvisioningStore cloud_store(cloudNamespace());
GatewayCloudConfig loaded_cloud;
if (cloud_store.load(&loaded_cloud) == ESP_OK) {
cloud_config = loaded_cloud;
cloud_config_loaded = true;
}
applyCloudModelsLocked();
if (service_config.cloud_enabled && service_config.cloud_startup_enabled) {
startCloudLocked();
}
return ESP_OK;
}
void applyBridgeConfigLocked() {
engine = std::make_unique<DaliBridgeEngine>(*comm);
for (const auto& model : bridge_config.models) {
engine->upsertModel(model);
}
modbus = std::make_unique<DaliModbusBridge>(*engine);
if (bridge_config.modbus.has_value()) {
modbus->setConfig(bridge_config.modbus.value());
}
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
if (service_config.bacnet_enabled) {
bacnet = std::make_unique<DaliBacnetBridge>(*engine);
if (bridge_config.bacnet.has_value()) {
bacnet->setConfig(bridge_config.bacnet.value());
}
} else {
bacnet.reset();
}
#endif
applyCloudModelsLocked();
bacnet_started = false;
}
void applyCloudModelsLocked() {
if (cloud_started && cloud != nullptr) {
cloud->stop();
cloud_started = false;
}
cloud = std::make_unique<DaliCloudBridge>(*comm);
for (const auto& model : bridge_config.models) {
cloud->bridge().upsertModel(model);
}
}
esp_err_t saveBridgeConfig(std::string_view json) {
auto parsed = BridgeRuntimeConfigFromJson(json);
if (!parsed.has_value()) {
return ESP_ERR_INVALID_ARG;
}
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.save(parsed.value());
if (err != ESP_OK) {
return err;
}
LockGuard guard(lock);
bridge_config = parsed.value();
bridge_config_loaded = true;
applyBridgeConfigLocked();
return ESP_OK;
}
esp_err_t clearBridgeConfig() {
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.clear();
if (err != ESP_OK) {
return err;
}
LockGuard guard(lock);
bridge_config = BridgeRuntimeConfig{};
bridge_config_loaded = false;
applyBridgeConfigLocked();
return ESP_OK;
}
esp_err_t saveCloudConfig(std::string_view json) {
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
if (root == nullptr) {
return ESP_ERR_INVALID_ARG;
}
const GatewayCloudConfig parsed = GatewayCloudConfigFromJson(root);
cJSON_Delete(root);
GatewayProvisioningStore store(cloudNamespace());
const esp_err_t err = store.save(parsed);
if (err != ESP_OK) {
return err;
}
LockGuard guard(lock);
cloud_config = parsed;
cloud_config_loaded = true;
if (cloud_started) {
startCloudLocked();
}
return ESP_OK;
}
esp_err_t clearCloudConfig() {
GatewayProvisioningStore store(cloudNamespace());
const esp_err_t err = store.clear();
if (err != ESP_OK) {
return err;
}
LockGuard guard(lock);
if (cloud != nullptr) {
cloud->stop();
}
cloud_config.reset();
cloud_config_loaded = false;
cloud_started = false;
applyCloudModelsLocked();
return ESP_OK;
}
esp_err_t startCloud() {
LockGuard guard(lock);
return startCloudLocked();
}
esp_err_t startCloudLocked() {
if (!service_config.cloud_enabled) {
return ESP_ERR_NOT_SUPPORTED;
}
if (!cloud_config.has_value()) {
return ESP_ERR_NOT_FOUND;
}
if (cloud == nullptr) {
applyCloudModelsLocked();
}
if (cloud->start(cloud_config.value())) {
cloud_started = true;
return ESP_OK;
}
cloud_started = false;
return ESP_FAIL;
}
esp_err_t stopCloud() {
LockGuard guard(lock);
if (cloud != nullptr) {
cloud->stop();
}
cloud_started = false;
return ESP_OK;
}
std::string modelName(const std::string& model_id) const {
const auto model = std::find_if(bridge_config.models.begin(), bridge_config.models.end(),
[&model_id](const auto& item) { return item.id == model_id; });
return model == bridge_config.models.end() ? model_id : model->displayName();
}
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
GatewayBacnetServerConfig bacnetServerConfigLocked() const {
GatewayBacnetServerConfig config;
config.device_name = channel.name.empty() ? "DALI Gateway " + std::to_string(channel.gateway_id)
: channel.name;
config.task_stack_size = service_config.bacnet_task_stack_size;
config.task_priority = service_config.bacnet_task_priority;
if (bacnet != nullptr) {
const auto& bridge_config = bacnet->config();
config.device_instance = bridge_config.deviceInstance;
config.local_address = bridge_config.localAddress;
config.udp_port = bridge_config.udpPort;
}
return config;
}
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() const {
std::vector<GatewayBacnetObjectBinding> bindings;
if (bacnet == nullptr) {
return bindings;
}
for (const auto& binding : bacnet->describeObjects()) {
if (binding.objectInstance < 0) {
continue;
}
bindings.push_back(GatewayBacnetObjectBinding{channel.gateway_id,
binding.modelID,
modelName(binding.modelID),
binding.objectType,
static_cast<uint32_t>(binding.objectInstance),
binding.property.empty() ? "presentValue"
: binding.property});
}
return bindings;
}
bool handleBacnetWrite(BridgeObjectType object_type, uint32_t object_instance,
const std::string& property, const DaliValue& value) {
LockGuard guard(lock);
if (bacnet == nullptr) {
return false;
}
const DaliBridgeResult result = bacnet->handlePropertyWrite(
object_type, static_cast<int>(object_instance), property, value);
if (!result.ok) {
ESP_LOGW(kTag, "gateway=%u BACnet write rejected: %s", channel.gateway_id,
result.error.c_str());
}
return result.ok;
}
esp_err_t startBacnet() {
LockGuard guard(lock);
if (!service_config.bacnet_enabled) {
return ESP_ERR_NOT_SUPPORTED;
}
if (bacnet == nullptr) {
return ESP_ERR_INVALID_STATE;
}
const auto bindings = bacnetObjectBindingsLocked();
if (bindings.empty()) {
return ESP_ERR_NOT_FOUND;
}
const auto server_config = bacnetServerConfigLocked();
const esp_err_t err = GatewayBacnetServer::instance().registerChannel(
channel.gateway_id, server_config, bindings,
[this](BridgeObjectType object_type, uint32_t object_instance,
const std::string& property, const DaliValue& value) {
return handleBacnetWrite(object_type, object_instance, property, value);
});
bacnet_started = err == ESP_OK;
return err;
}
#else
esp_err_t startBacnet() {
return ESP_ERR_NOT_SUPPORTED;
}
#endif
GatewayBridgeHttpResponse execute(std::string_view json) {
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
if (root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid execute JSON");
}
const DaliBridgeRequest request = BridgeRequestFromJson(root);
cJSON_Delete(root);
LockGuard guard(lock);
if (engine == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_STATE, "bridge engine is not ready");
}
const DaliBridgeResult result = engine->execute(request);
return JsonOk(BridgeResultToCjson(result));
}
cJSON* statusCjson() const {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON_AddNumberToObject(root, "channel", channel.channel_index);
cJSON_AddStringToObject(root, "name", channel.name.c_str());
cJSON_AddBoolToObject(root, "bridgeConfigLoaded", bridge_config_loaded);
cJSON_AddNumberToObject(root, "modelCount", static_cast<double>(bridge_config.models.size()));
cJSON* modbus_json = cJSON_CreateObject();
if (modbus_json != nullptr) {
cJSON_AddBoolToObject(modbus_json, "enabled", service_config.modbus_enabled);
cJSON_AddBoolToObject(modbus_json, "started", modbus_started);
if (bridge_config.modbus.has_value()) {
cJSON_AddStringToObject(modbus_json, "transport", bridge_config.modbus->transport.c_str());
cJSON_AddNumberToObject(modbus_json, "port", bridge_config.modbus->port);
cJSON_AddNumberToObject(modbus_json, "unitID", bridge_config.modbus->unitID);
}
cJSON_AddItemToObject(root, "modbus", modbus_json);
}
cJSON* bacnet_json = cJSON_CreateObject();
if (bacnet_json != nullptr) {
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
const auto server_status = GatewayBacnetServer::instance().status();
#endif
cJSON_AddBoolToObject(bacnet_json, "enabled", service_config.bacnet_enabled);
cJSON_AddBoolToObject(bacnet_json, "startupEnabled", service_config.bacnet_startup_enabled);
cJSON_AddBoolToObject(bacnet_json, "started", bacnet_started);
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
cJSON_AddBoolToObject(bacnet_json, "serverStarted", server_status.started);
cJSON_AddNumberToObject(bacnet_json, "serverObjectCount",
static_cast<double>(server_status.object_count));
#else
cJSON_AddBoolToObject(bacnet_json, "serverStarted", false);
cJSON_AddNumberToObject(bacnet_json, "serverObjectCount", 0);
#endif
if (bridge_config.bacnet.has_value()) {
cJSON_AddNumberToObject(bacnet_json, "deviceInstance", bridge_config.bacnet->deviceInstance);
cJSON_AddStringToObject(bacnet_json, "localAddress", bridge_config.bacnet->localAddress.c_str());
cJSON_AddNumberToObject(bacnet_json, "udpPort", bridge_config.bacnet->udpPort);
}
cJSON_AddItemToObject(root, "bacnet", bacnet_json);
}
cJSON* cloud_json = cJSON_CreateObject();
if (cloud_json != nullptr) {
cJSON_AddBoolToObject(cloud_json, "enabled", service_config.cloud_enabled);
cJSON_AddBoolToObject(cloud_json, "configured", cloud_config_loaded);
cJSON_AddBoolToObject(cloud_json, "started", cloud_started);
cJSON_AddBoolToObject(cloud_json, "connected", cloud != nullptr && cloud->isConnected());
if (cloud_config.has_value()) {
cJSON_AddStringToObject(cloud_json, "deviceID", cloud_config->deviceID.c_str());
cJSON_AddStringToObject(cloud_json, "topicPrefix", cloud_config->topicPrefix.c_str());
}
cJSON_AddItemToObject(root, "cloud", cloud_json);
}
return root;
}
GatewayBridgeHttpResponse configJson() const {
return GatewayBridgeHttpResponse{ESP_OK, BridgeRuntimeConfigToJson(bridge_config)};
}
GatewayBridgeHttpResponse modbusBindingsJson() const {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON* bindings = cJSON_CreateArray();
if (bindings != nullptr && modbus != nullptr) {
for (const auto& binding : modbus->describeHoldingRegisters()) {
cJSON* item = cJSON_CreateObject();
if (item == nullptr) {
continue;
}
cJSON_AddStringToObject(item, "model", binding.modelID.c_str());
cJSON_AddNumberToObject(item, "registerAddress", binding.registerAddress);
cJSON_AddItemToArray(bindings, item);
}
}
cJSON_AddItemToObject(root, "bindings", bindings);
return JsonOk(root);
}
GatewayBridgeHttpResponse bacnetBindingsJson() const {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON* bindings = cJSON_CreateArray();
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
if (bindings != nullptr && bacnet != nullptr) {
for (const auto& binding : bacnet->describeObjects()) {
cJSON* item = cJSON_CreateObject();
if (item == nullptr) {
continue;
}
cJSON_AddStringToObject(item, "model", binding.modelID.c_str());
cJSON_AddStringToObject(item, "objectType", bridgeObjectTypeToString(binding.objectType));
cJSON_AddNumberToObject(item, "objectInstance", binding.objectInstance);
cJSON_AddStringToObject(item, "property", binding.property.c_str());
cJSON_AddItemToArray(bindings, item);
}
}
#endif
cJSON_AddItemToObject(root, "bindings", bindings);
cJSON* server_json = cJSON_CreateObject();
if (server_json != nullptr) {
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
const auto server_status = GatewayBacnetServer::instance().status();
cJSON_AddBoolToObject(server_json, "started", server_status.started);
cJSON_AddNumberToObject(server_json, "deviceInstance", server_status.device_instance);
cJSON_AddNumberToObject(server_json, "udpPort", server_status.udp_port);
cJSON_AddNumberToObject(server_json, "channelCount",
static_cast<double>(server_status.channel_count));
cJSON_AddNumberToObject(server_json, "objectCount",
static_cast<double>(server_status.object_count));
#else
cJSON_AddBoolToObject(server_json, "started", false);
cJSON_AddNumberToObject(server_json, "deviceInstance", 0);
cJSON_AddNumberToObject(server_json, "udpPort", 0);
cJSON_AddNumberToObject(server_json, "channelCount", 0);
cJSON_AddNumberToObject(server_json, "objectCount", 0);
#endif
cJSON_AddItemToObject(root, "server", server_json);
}
return JsonOk(root);
}
GatewayBridgeHttpResponse cloudJson() const {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON_AddBoolToObject(root, "configured", cloud_config_loaded);
cJSON_AddBoolToObject(root, "started", cloud_started);
cJSON_AddBoolToObject(root, "connected", cloud != nullptr && cloud->isConnected());
if (cloud_config.has_value()) {
cJSON_AddItemToObject(root, "config", GatewayCloudConfigToCjson(cloud_config.value()));
}
return JsonOk(root);
}
esp_err_t startModbus(std::set<uint16_t>* used_ports = nullptr) {
LockGuard guard(lock);
if (!service_config.modbus_enabled) {
return ESP_ERR_NOT_SUPPORTED;
}
if (modbus_started || modbus_task_handle != nullptr) {
return ESP_OK;
}
if (!bridge_config.modbus.has_value()) {
return ESP_ERR_NOT_FOUND;
}
const uint16_t port = bridge_config.modbus->port == 0 ? kDefaultModbusPort
: bridge_config.modbus->port;
if (used_ports != nullptr) {
if (used_ports->find(port) != used_ports->end()) {
ESP_LOGW(kTag, "gateway=%u skips duplicate Modbus TCP port %u", channel.gateway_id, port);
return ESP_ERR_INVALID_STATE;
}
used_ports->insert(port);
}
const BaseType_t created = xTaskCreate(&ChannelRuntime::ModbusTaskEntry, "gw_modbus_tcp",
service_config.modbus_task_stack_size, this,
service_config.modbus_task_priority,
&modbus_task_handle);
if (created != pdPASS) {
modbus_task_handle = nullptr;
return ESP_ERR_NO_MEM;
}
modbus_started = true;
return ESP_OK;
}
void modbusTaskLoop() {
const uint16_t port = bridge_config.modbus.has_value() && bridge_config.modbus->port != 0
? bridge_config.modbus->port
: kDefaultModbusPort;
const int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (listen_sock < 0) {
ESP_LOGE(kTag, "gateway=%u failed to create Modbus socket", channel.gateway_id);
modbus_started = false;
modbus_task_handle = nullptr;
vTaskDelete(nullptr);
return;
}
int reuse = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(port);
if (bind(listen_sock, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0 ||
listen(listen_sock, 2) != 0) {
ESP_LOGE(kTag, "gateway=%u failed to bind Modbus TCP port %u", channel.gateway_id, port);
close(listen_sock);
modbus_started = false;
modbus_task_handle = nullptr;
vTaskDelete(nullptr);
return;
}
ESP_LOGI(kTag, "gateway=%u Modbus TCP listening on port %u", channel.gateway_id, port);
while (true) {
sockaddr_in client_address = {};
socklen_t client_len = sizeof(client_address);
const int client_sock = accept(listen_sock, reinterpret_cast<sockaddr*>(&client_address),
&client_len);
if (client_sock < 0) {
continue;
}
handleModbusClient(client_sock);
close(client_sock);
}
}
void handleModbusClient(int client_sock) {
uint8_t header[7] = {};
while (RecvAll(client_sock, header, sizeof(header))) {
const uint16_t protocol_id = ReadBe16(&header[2]);
const uint16_t length = ReadBe16(&header[4]);
if (protocol_id != 0 || length < 2 || length > kModbusMaxPduBytes) {
break;
}
std::vector<uint8_t> pdu(length - 1);
if (!RecvAll(client_sock, pdu.data(), pdu.size()) || pdu.empty()) {
break;
}
if (bridge_config.modbus.has_value() && bridge_config.modbus->unitID != 0 &&
header[6] != bridge_config.modbus->unitID) {
SendModbusException(client_sock, header, pdu[0], 0x0B);
continue;
}
if (pdu[0] == 0x06 && pdu.size() == 5) {
const uint16_t wire_register = ReadBe16(&pdu[1]);
const uint16_t value = ReadBe16(&pdu[3]);
const int holding_register = HoldingRegisterFromWireAddress(wire_register);
const auto result = handleHoldingRegisterWrite(holding_register, value);
if (!result.ok) {
SendModbusException(client_sock, header, pdu[0], 0x04);
continue;
}
SendModbusFrame(client_sock, header, pdu);
continue;
}
if (pdu[0] == 0x10 && pdu.size() >= 6) {
const uint16_t start_register = ReadBe16(&pdu[1]);
const uint16_t quantity = ReadBe16(&pdu[3]);
const uint8_t byte_count = pdu[5];
if (pdu.size() != static_cast<size_t>(6 + byte_count) || byte_count != quantity * 2) {
SendModbusException(client_sock, header, pdu[0], 0x03);
continue;
}
bool ok = true;
for (uint16_t index = 0; index < quantity; ++index) {
const size_t offset = 6 + (index * 2);
const uint16_t value = ReadBe16(&pdu[offset]);
const int holding_register = HoldingRegisterFromWireAddress(start_register + index);
const auto result = handleHoldingRegisterWrite(holding_register, value);
if (!result.ok) {
ok = false;
break;
}
}
if (!ok) {
SendModbusException(client_sock, header, pdu[0], 0x04);
continue;
}
std::vector<uint8_t> response(5);
response[0] = pdu[0];
WriteBe16(&response[1], start_register);
WriteBe16(&response[3], quantity);
SendModbusFrame(client_sock, header, response);
continue;
}
SendModbusException(client_sock, header, pdu[0], 0x01);
}
}
DaliBridgeResult handleHoldingRegisterWrite(int holding_register, int value) {
LockGuard guard(lock);
if (modbus == nullptr) {
DaliBridgeResult result;
result.sequence = "modbus-" + std::to_string(holding_register);
result.error = "modbus bridge not ready";
return result;
}
return modbus->handleHoldingRegisterWrite(holding_register, value);
}
};
GatewayBridgeService::GatewayBridgeService(DaliDomainService& dali_domain,
GatewayBridgeServiceConfig config)
: dali_domain_(dali_domain), config_(config) {}
GatewayBridgeService::~GatewayBridgeService() = default;
esp_err_t GatewayBridgeService::start() {
if (!config_.bridge_enabled) {
ESP_LOGI(kTag, "bridge service disabled");
return ESP_OK;
}
if (!runtimes_.empty()) {
return ESP_OK;
}
const auto channels = dali_domain_.channelInfo();
runtimes_.reserve(channels.size());
for (const auto& channel : channels) {
auto runtime = std::make_unique<ChannelRuntime>(dali_domain_, channel, config_);
const esp_err_t err = runtime->start();
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to start bridge runtime gateway=%u: %s", channel.gateway_id,
esp_err_to_name(err));
return err;
}
runtimes_.push_back(std::move(runtime));
}
if (config_.modbus_enabled && config_.modbus_startup_enabled) {
std::set<uint16_t> used_ports;
for (const auto& runtime : runtimes_) {
const esp_err_t err = runtime->startModbus(&used_ports);
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(kTag, "gateway=%u Modbus startup skipped: %s", runtime->channel.gateway_id,
esp_err_to_name(err));
}
}
}
if (config_.bacnet_enabled && config_.bacnet_startup_enabled) {
for (const auto& runtime : runtimes_) {
const esp_err_t err = runtime->startBacnet();
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(kTag, "gateway=%u BACnet startup skipped: %s", runtime->channel.gateway_id,
esp_err_to_name(err));
}
}
}
ESP_LOGI(kTag, "bridge service started channels=%u", static_cast<unsigned>(runtimes_.size()));
return ESP_OK;
}
GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(uint8_t gateway_id) {
for (const auto& runtime : runtimes_) {
if (runtime->channel.gateway_id == gateway_id) {
return runtime.get();
}
}
return nullptr;
}
const GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(
uint8_t gateway_id) const {
for (const auto& runtime : runtimes_) {
if (runtime->channel.gateway_id == gateway_id) {
return runtime.get();
}
}
return nullptr;
}
GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
const std::string& action_arg, int gateway_id_arg, const std::string& query_arg) const {
if (!config_.bridge_enabled) {
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "bridge service is disabled");
}
std::string_view action(action_arg);
std::string_view query(query_arg);
std::optional<uint8_t> gateway_id;
if (gateway_id_arg >= 0 && gateway_id_arg <= 255) {
gateway_id = static_cast<uint8_t>(gateway_id_arg);
}
if (action.empty()) {
action = "status";
}
if (action == "status" && !gateway_id.has_value()) {
cJSON* root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "enabled", true);
cJSON_AddNumberToObject(root, "count", static_cast<double>(runtimes_.size()));
cJSON* channels = cJSON_CreateArray();
if (channels != nullptr) {
for (const auto& runtime : runtimes_) {
cJSON_AddItemToArray(channels, runtime->statusCjson());
}
cJSON_AddItemToObject(root, "channels", channels);
}
return JsonOk(root);
}
if (!gateway_id.has_value()) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required");
}
const auto* runtime = findRuntime(gateway_id.value());
if (runtime == nullptr) {
return ErrorResponse(ESP_ERR_NOT_FOUND, "unknown gateway id");
}
if (action == "status") {
return JsonOk(runtime->statusCjson());
}
if (action == "config") {
return runtime->configJson();
}
if (action == "modbus") {
return runtime->modbusBindingsJson();
}
if (action == "bacnet") {
return runtime->bacnetBindingsJson();
}
if (action == "cloud") {
return runtime->cloudJson();
}
if (action == "device") {
const auto address = QueryInt(query, "addr", "address");
if (!address.has_value() || !ValidDaliAddress(address.value())) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
}
return SnapshotResponse(dali_domain_.discoverDeviceTypes(gateway_id.value(), address.value()),
"device did not respond to type discovery");
}
if (action == "dt4" || action == "dt5" || action == "dt6") {
const auto address = QueryInt(query, "addr", "address");
if (!address.has_value() || !ValidDaliAddress(address.value())) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
}
if (action == "dt4") {
return SnapshotResponse(dali_domain_.dt4Snapshot(gateway_id.value(), address.value()),
"DT4 snapshot is unavailable");
}
if (action == "dt5") {
return SnapshotResponse(dali_domain_.dt5Snapshot(gateway_id.value(), address.value()),
"DT5 snapshot is unavailable");
}
return SnapshotResponse(dali_domain_.dt6Snapshot(gateway_id.value(), address.value()),
"DT6 snapshot is unavailable");
}
if (action == "dt8_scene") {
const auto address = QueryInt(query, "addr", "address");
const auto scene = QueryInt(query, "scene");
if (!address.has_value() || !scene.has_value() || !ValidDaliAddress(address.value()) ||
scene.value() < 0 || scene.value() > 15) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr and scene are required");
}
return SnapshotResponse(dali_domain_.dt8SceneColorReport(gateway_id.value(), address.value(),
scene.value()),
"DT8 scene color report is unavailable");
}
if (action == "dt8_power_on") {
const auto address = QueryInt(query, "addr", "address");
if (!address.has_value() || !ValidDaliAddress(address.value())) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
}
return SnapshotResponse(dali_domain_.dt8PowerOnLevelColorReport(gateway_id.value(),
address.value()),
"DT8 power-on color report is unavailable");
}
if (action == "dt8_system_failure") {
const auto address = QueryInt(query, "addr", "address");
if (!address.has_value() || !ValidDaliAddress(address.value())) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
}
return SnapshotResponse(dali_domain_.dt8SystemFailureLevelColorReport(gateway_id.value(),
address.value()),
"DT8 system-failure color report is unavailable");
}
return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge GET action");
}
GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
const std::string& action_arg, int gateway_id_arg, const std::string& body_arg) {
if (!config_.bridge_enabled) {
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "bridge service is disabled");
}
std::string_view action(action_arg);
std::string_view body(body_arg);
std::optional<uint8_t> gateway_id;
if (gateway_id_arg >= 0 && gateway_id_arg <= 255) {
gateway_id = static_cast<uint8_t>(gateway_id_arg);
}
cJSON* root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (!body.empty() && root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid JSON body");
}
if (action.empty() && root != nullptr) {
if (const char* body_action = JsonString(root, "action")) {
action = body_action;
}
}
if (!gateway_id.has_value()) {
gateway_id = JsonGatewayId(root);
}
cJSON_Delete(root);
if (action.empty()) {
action = "execute";
}
if (!gateway_id.has_value()) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required");
}
auto* runtime = findRuntime(gateway_id.value());
if (runtime == nullptr) {
return ErrorResponse(ESP_ERR_NOT_FOUND, "unknown gateway id");
}
if (action == "dt8_scene_snapshot" || action == "store_dt8_scene_snapshot") {
return StoreDt8SceneSnapshot(dali_domain_, gateway_id.value(), body);
}
if (action == "dt8_power_on_snapshot" || action == "store_dt8_power_on_snapshot") {
return StoreDt8LevelSnapshot(dali_domain_, gateway_id.value(), body, true);
}
if (action == "dt8_system_failure_snapshot" ||
action == "store_dt8_system_failure_snapshot") {
return StoreDt8LevelSnapshot(dali_domain_, gateway_id.value(), body, false);
}
if (action == "execute") {
return runtime->execute(body);
}
if (action == "config" || action == "save_config") {
const esp_err_t err = runtime->saveBridgeConfig(body);
if (err != ESP_OK) {
return ErrorResponse(err, "failed to save bridge config");
}
return handleGet("config", gateway_id.value());
}
if (action == "clear_config") {
const esp_err_t err = runtime->clearBridgeConfig();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to clear bridge config");
}
return handleGet("config", gateway_id.value());
}
if (action == "cloud" || action == "save_cloud") {
const esp_err_t err = runtime->saveCloudConfig(body);
if (err != ESP_OK) {
return ErrorResponse(err, "failed to save cloud config");
}
return handleGet("cloud", gateway_id.value());
}
if (action == "cloud_start") {
const esp_err_t err = runtime->startCloud();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to start cloud bridge");
}
return handleGet("cloud", gateway_id.value());
}
if (action == "cloud_stop") {
const esp_err_t err = runtime->stopCloud();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to stop cloud bridge");
}
return handleGet("cloud", gateway_id.value());
}
if (action == "cloud_clear") {
const esp_err_t err = runtime->clearCloudConfig();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to clear cloud config");
}
return handleGet("cloud", gateway_id.value());
}
if (action == "modbus_start") {
const esp_err_t err = runtime->startModbus();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to start Modbus TCP bridge");
}
return handleGet("modbus", gateway_id.value());
}
if (action == "bacnet_start") {
const esp_err_t err = runtime->startBacnet();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to start BACnet/IP bridge");
}
return handleGet("bacnet", gateway_id.value());
}
return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge POST action");
}
} // namespace gateway