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>
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
#include "gateway_bacnet.hpp"
|
||||
|
||||
#include "gateway_bacnet_stack_port.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace gateway {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kTag = "gateway_bacnet";
|
||||
constexpr TickType_t kPollDelayTicks = pdMS_TO_TICKS(10);
|
||||
|
||||
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_;
|
||||
};
|
||||
|
||||
constexpr uint32_t kMaxBacnetInstance = 4194303;
|
||||
GatewayBacnetServer* g_server = nullptr;
|
||||
|
||||
gateway_bacnet_object_kind_t ToBacnetKind(BridgeObjectType type) {
|
||||
switch (type) {
|
||||
case BridgeObjectType::analogValue:
|
||||
return GW_BACNET_OBJECT_ANALOG_VALUE;
|
||||
case BridgeObjectType::analogOutput:
|
||||
return GW_BACNET_OBJECT_ANALOG_OUTPUT;
|
||||
case BridgeObjectType::binaryValue:
|
||||
return GW_BACNET_OBJECT_BINARY_VALUE;
|
||||
case BridgeObjectType::binaryOutput:
|
||||
return GW_BACNET_OBJECT_BINARY_OUTPUT;
|
||||
case BridgeObjectType::multiStateValue:
|
||||
return GW_BACNET_OBJECT_MULTI_STATE_VALUE;
|
||||
default:
|
||||
return GW_BACNET_OBJECT_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
BridgeObjectType FromBacnetKind(gateway_bacnet_object_kind_t kind) {
|
||||
switch (kind) {
|
||||
case GW_BACNET_OBJECT_ANALOG_VALUE:
|
||||
return BridgeObjectType::analogValue;
|
||||
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
|
||||
return BridgeObjectType::analogOutput;
|
||||
case GW_BACNET_OBJECT_BINARY_VALUE:
|
||||
return BridgeObjectType::binaryValue;
|
||||
case GW_BACNET_OBJECT_BINARY_OUTPUT:
|
||||
return BridgeObjectType::binaryOutput;
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
|
||||
return BridgeObjectType::multiStateValue;
|
||||
default:
|
||||
return BridgeObjectType::unknown;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsSupportedObjectType(BridgeObjectType type) {
|
||||
return ToBacnetKind(type) != GW_BACNET_OBJECT_UNKNOWN;
|
||||
}
|
||||
|
||||
std::string ObjectName(const GatewayBacnetObjectBinding& binding) {
|
||||
if (!binding.name.empty()) {
|
||||
return binding.name;
|
||||
}
|
||||
if (!binding.model_id.empty()) {
|
||||
return "DALI " + binding.model_id;
|
||||
}
|
||||
return "DALI BACnet " + std::to_string(binding.object_instance);
|
||||
}
|
||||
|
||||
DaliValue StackWriteValueToDali(const gateway_bacnet_write_value_t& value) {
|
||||
switch (value.kind) {
|
||||
case GW_BACNET_WRITE_VALUE_REAL:
|
||||
return DaliValue(value.real_value);
|
||||
case GW_BACNET_WRITE_VALUE_BOOLEAN:
|
||||
return DaliValue(value.boolean_value);
|
||||
case GW_BACNET_WRITE_VALUE_UNSIGNED:
|
||||
return DaliValue(static_cast<int>(value.unsigned_value));
|
||||
default:
|
||||
return DaliValue();
|
||||
}
|
||||
}
|
||||
|
||||
void HandleStackWrite(gateway_bacnet_object_kind_t object_kind, uint32_t object_instance,
|
||||
const gateway_bacnet_write_value_t* value, void*) {
|
||||
if (g_server == nullptr || value == nullptr) {
|
||||
return;
|
||||
}
|
||||
g_server->handleWrite(FromBacnetKind(object_kind), object_instance, StackWriteValueToDali(*value));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct GatewayBacnetServer::ChannelRegistration {
|
||||
uint8_t gateway_id{0};
|
||||
GatewayBacnetServerConfig config;
|
||||
std::vector<GatewayBacnetObjectBinding> bindings;
|
||||
GatewayBacnetWriteCallback write_callback;
|
||||
};
|
||||
|
||||
struct GatewayBacnetServer::RuntimeBinding {
|
||||
uint8_t gateway_id{0};
|
||||
BridgeObjectType object_type{BridgeObjectType::unknown};
|
||||
uint32_t object_instance{0};
|
||||
std::string model_id;
|
||||
std::string property{"presentValue"};
|
||||
GatewayBacnetWriteCallback write_callback;
|
||||
};
|
||||
|
||||
GatewayBacnetServer& GatewayBacnetServer::instance() {
|
||||
static GatewayBacnetServer server;
|
||||
return server;
|
||||
}
|
||||
|
||||
GatewayBacnetServer::GatewayBacnetServer() : lock_(xSemaphoreCreateRecursiveMutex()) {
|
||||
g_server = this;
|
||||
}
|
||||
|
||||
GatewayBacnetServer::~GatewayBacnetServer() {
|
||||
if (lock_ != nullptr) {
|
||||
vSemaphoreDelete(lock_);
|
||||
lock_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool GatewayBacnetServer::configCompatible(const GatewayBacnetServerConfig& config) const {
|
||||
LockGuard guard(lock_);
|
||||
return !started_ || (active_config_.udp_port == config.udp_port &&
|
||||
active_config_.device_instance == config.device_instance);
|
||||
}
|
||||
|
||||
GatewayBacnetServerStatus GatewayBacnetServer::status() const {
|
||||
LockGuard guard(lock_);
|
||||
return GatewayBacnetServerStatus{started_,
|
||||
active_config_.device_instance,
|
||||
active_config_.udp_port,
|
||||
channels_.size(),
|
||||
runtime_bindings_.size()};
|
||||
}
|
||||
|
||||
esp_err_t GatewayBacnetServer::registerChannel(
|
||||
uint8_t gateway_id, const GatewayBacnetServerConfig& config,
|
||||
std::vector<GatewayBacnetObjectBinding> bindings,
|
||||
GatewayBacnetWriteCallback write_callback) {
|
||||
if (write_callback == nullptr) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
bindings.erase(std::remove_if(bindings.begin(), bindings.end(), [](const auto& binding) {
|
||||
return !IsSupportedObjectType(binding.object_type) ||
|
||||
binding.object_instance > kMaxBacnetInstance;
|
||||
}),
|
||||
bindings.end());
|
||||
if (bindings.empty()) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
LockGuard guard(lock_);
|
||||
if (started_ && !configCompatible(config)) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
auto channel = std::find_if(channels_.begin(), channels_.end(), [gateway_id](const auto& item) {
|
||||
return item.gateway_id == gateway_id;
|
||||
});
|
||||
ChannelRegistration registration{gateway_id, config, std::move(bindings),
|
||||
std::move(write_callback)};
|
||||
if (channel == channels_.end()) {
|
||||
channels_.push_back(std::move(registration));
|
||||
} else {
|
||||
*channel = std::move(registration);
|
||||
}
|
||||
|
||||
esp_err_t err = startStackLocked(config);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
return rebuildObjectsLocked();
|
||||
}
|
||||
|
||||
esp_err_t GatewayBacnetServer::startStackLocked(const GatewayBacnetServerConfig& config) {
|
||||
if (started_) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
active_config_ = config;
|
||||
if (active_config_.device_name.empty()) {
|
||||
active_config_.device_name = "DALI Gateway";
|
||||
}
|
||||
if (active_config_.udp_port == 0) {
|
||||
active_config_.udp_port = 47808;
|
||||
}
|
||||
if (active_config_.task_stack_size < 6144) {
|
||||
active_config_.task_stack_size = 6144;
|
||||
}
|
||||
|
||||
if (!gateway_bacnet_stack_start(active_config_.device_instance, active_config_.device_name.c_str(),
|
||||
active_config_.udp_port, HandleStackWrite, this)) {
|
||||
ESP_LOGE(kTag, "failed to initialize BACnet/IP port %u", active_config_.udp_port);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
const BaseType_t created = xTaskCreate(&GatewayBacnetServer::TaskEntry, "gw_bacnet_ip",
|
||||
active_config_.task_stack_size, this,
|
||||
active_config_.task_priority, &task_handle_);
|
||||
if (created != pdPASS) {
|
||||
task_handle_ = nullptr;
|
||||
gateway_bacnet_stack_cleanup();
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
started_ = true;
|
||||
gateway_bacnet_stack_send_i_am();
|
||||
ESP_LOGI(kTag, "BACnet/IP server started device=%lu port=%u",
|
||||
static_cast<unsigned long>(active_config_.device_instance), active_config_.udp_port);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t GatewayBacnetServer::rebuildObjectsLocked() {
|
||||
runtime_bindings_.clear();
|
||||
std::set<std::pair<BridgeObjectType, uint32_t>> used_objects;
|
||||
|
||||
for (const auto& channel : channels_) {
|
||||
for (const auto& binding : channel.bindings) {
|
||||
const auto key = std::make_pair(binding.object_type, binding.object_instance);
|
||||
if (used_objects.find(key) != used_objects.end()) {
|
||||
ESP_LOGE(kTag, "duplicate BACnet object type=%d instance=%lu",
|
||||
static_cast<int>(binding.object_type),
|
||||
static_cast<unsigned long>(binding.object_instance));
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
used_objects.insert(key);
|
||||
|
||||
const std::string name = ObjectName(binding);
|
||||
if (!gateway_bacnet_stack_upsert_object(ToBacnetKind(binding.object_type),
|
||||
binding.object_instance, name.c_str(),
|
||||
binding.model_id.c_str())) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
runtime_bindings_.push_back(RuntimeBinding{channel.gateway_id,
|
||||
binding.object_type,
|
||||
binding.object_instance,
|
||||
binding.model_id,
|
||||
binding.property.empty() ? "presentValue"
|
||||
: binding.property,
|
||||
channel.write_callback});
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(kTag, "BACnet/IP object table updated objects=%u",
|
||||
static_cast<unsigned>(runtime_bindings_.size()));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool GatewayBacnetServer::handleWrite(BridgeObjectType object_type, uint32_t object_instance,
|
||||
const DaliValue& value) {
|
||||
GatewayBacnetWriteCallback callback;
|
||||
std::string property;
|
||||
std::string model_id;
|
||||
uint8_t gateway_id = 0;
|
||||
{
|
||||
LockGuard guard(lock_);
|
||||
const auto binding = std::find_if(runtime_bindings_.begin(), runtime_bindings_.end(),
|
||||
[object_type, object_instance](const auto& item) {
|
||||
return item.object_type == object_type &&
|
||||
item.object_instance == object_instance;
|
||||
});
|
||||
if (binding == runtime_bindings_.end()) {
|
||||
ESP_LOGW(kTag, "write for unmapped BACnet object type=%d instance=%lu",
|
||||
static_cast<int>(object_type), static_cast<unsigned long>(object_instance));
|
||||
return false;
|
||||
}
|
||||
callback = binding->write_callback;
|
||||
property = binding->property;
|
||||
model_id = binding->model_id;
|
||||
gateway_id = binding->gateway_id;
|
||||
}
|
||||
|
||||
const bool ok = callback != nullptr && callback(object_type, object_instance, property, value);
|
||||
if (!ok) {
|
||||
ESP_LOGW(kTag, "gateway=%u BACnet write failed model=%s object=%lu",
|
||||
gateway_id, model_id.c_str(), static_cast<unsigned long>(object_instance));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void GatewayBacnetServer::TaskEntry(void* arg) {
|
||||
static_cast<GatewayBacnetServer*>(arg)->taskLoop();
|
||||
}
|
||||
|
||||
void GatewayBacnetServer::taskLoop() {
|
||||
TickType_t last_timer = xTaskGetTickCount();
|
||||
|
||||
while (true) {
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
const TickType_t elapsed = now - last_timer;
|
||||
uint16_t elapsed_ms = 0;
|
||||
if (elapsed >= pdMS_TO_TICKS(1000)) {
|
||||
elapsed_ms = static_cast<uint16_t>(elapsed * portTICK_PERIOD_MS);
|
||||
last_timer = now;
|
||||
}
|
||||
{
|
||||
LockGuard guard(lock_);
|
||||
gateway_bacnet_stack_poll(elapsed_ms);
|
||||
}
|
||||
vTaskDelay(kPollDelayTicks);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gateway
|
||||
Reference in New Issue
Block a user