d16c289626
- 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>
1451 lines
50 KiB
C++
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
|