#include "gateway_bacnet.hpp" #include "gateway_bacnet_stack_port.h" #include "esp_log.h" #include "freertos/semphr.h" #include #include #include #include #include #include namespace gateway { namespace { constexpr const char* kTag = "gateway_bacnet"; constexpr TickType_t kPollDelayTicks = pdMS_TO_TICKS(10); constexpr TickType_t kValueRefreshTicks = pdMS_TO_TICKS(2000); constexpr uint32_t kReliabilityCommunicationFailure = 12; 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::analogInput: return GW_BACNET_OBJECT_ANALOG_INPUT; case BridgeObjectType::analogValue: return GW_BACNET_OBJECT_ANALOG_VALUE; case BridgeObjectType::analogOutput: return GW_BACNET_OBJECT_ANALOG_OUTPUT; case BridgeObjectType::binaryInput: return GW_BACNET_OBJECT_BINARY_INPUT; case BridgeObjectType::binaryValue: return GW_BACNET_OBJECT_BINARY_VALUE; case BridgeObjectType::binaryOutput: return GW_BACNET_OBJECT_BINARY_OUTPUT; case BridgeObjectType::multiStateInput: return GW_BACNET_OBJECT_MULTI_STATE_INPUT; case BridgeObjectType::multiStateValue: return GW_BACNET_OBJECT_MULTI_STATE_VALUE; case BridgeObjectType::multiStateOutput: return GW_BACNET_OBJECT_MULTI_STATE_OUTPUT; case BridgeObjectType::holdingRegister: return GW_BACNET_OBJECT_ANALOG_VALUE; case BridgeObjectType::inputRegister: return GW_BACNET_OBJECT_ANALOG_INPUT; case BridgeObjectType::coil: return GW_BACNET_OBJECT_BINARY_OUTPUT; case BridgeObjectType::discreteInput: return GW_BACNET_OBJECT_BINARY_INPUT; default: return GW_BACNET_OBJECT_UNKNOWN; } } BridgeObjectType FromBacnetKind(gateway_bacnet_object_kind_t kind) { switch (kind) { case GW_BACNET_OBJECT_ANALOG_INPUT: return BridgeObjectType::analogInput; case GW_BACNET_OBJECT_ANALOG_VALUE: return BridgeObjectType::analogValue; case GW_BACNET_OBJECT_ANALOG_OUTPUT: return BridgeObjectType::analogOutput; case GW_BACNET_OBJECT_BINARY_INPUT: return BridgeObjectType::binaryInput; case GW_BACNET_OBJECT_BINARY_VALUE: return BridgeObjectType::binaryValue; case GW_BACNET_OBJECT_BINARY_OUTPUT: return BridgeObjectType::binaryOutput; case GW_BACNET_OBJECT_MULTI_STATE_INPUT: return BridgeObjectType::multiStateInput; case GW_BACNET_OBJECT_MULTI_STATE_VALUE: return BridgeObjectType::multiStateValue; case GW_BACNET_OBJECT_MULTI_STATE_OUTPUT: return BridgeObjectType::multiStateOutput; 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(value.unsigned_value)); default: return DaliValue(); } } bool DaliValueToStackPresentValue(BridgeObjectType object_type, const DaliValue& value, gateway_bacnet_write_value_t* out) { if (out == nullptr) { return false; } switch (ToBacnetKind(object_type)) { case GW_BACNET_OBJECT_ANALOG_INPUT: case GW_BACNET_OBJECT_ANALOG_VALUE: case GW_BACNET_OBJECT_ANALOG_OUTPUT: { const auto parsed = value.asDouble(); if (!parsed.has_value()) { return false; } *out = gateway_bacnet_write_value_t{GW_BACNET_WRITE_VALUE_REAL, parsed.value(), false, 0}; return true; } case GW_BACNET_OBJECT_BINARY_INPUT: case GW_BACNET_OBJECT_BINARY_VALUE: case GW_BACNET_OBJECT_BINARY_OUTPUT: { const auto parsed = value.asBool(); if (!parsed.has_value()) { return false; } *out = gateway_bacnet_write_value_t{GW_BACNET_WRITE_VALUE_BOOLEAN, 0.0, parsed.value(), 0}; return true; } case GW_BACNET_OBJECT_MULTI_STATE_INPUT: case GW_BACNET_OBJECT_MULTI_STATE_VALUE: case GW_BACNET_OBJECT_MULTI_STATE_OUTPUT: { const auto parsed = value.asInt(); if (!parsed.has_value()) { return false; } const uint32_t unsigned_value = parsed.value() <= 0 ? 1 : static_cast(parsed.value()); *out = gateway_bacnet_write_value_t{GW_BACNET_WRITE_VALUE_UNSIGNED, 0.0, false, unsigned_value}; return true; } default: return false; } } 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 bindings; GatewayBacnetWriteCallback write_callback; GatewayBacnetReadCallback read_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"}; bool out_of_service{false}; uint32_t reliability{0}; bool readable{false}; GatewayBacnetWriteCallback write_callback; GatewayBacnetReadCallback read_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 bindings, GatewayBacnetWriteCallback write_callback, GatewayBacnetReadCallback read_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()); 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; }); if (bindings.empty() && !started_ && channel == channels_.end()) { return ESP_ERR_NOT_FOUND; } ChannelRegistration registration{gateway_id, config, std::move(bindings), std::move(write_callback), std::move(read_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(active_config_.device_instance), active_config_.udp_port); return ESP_OK; } esp_err_t GatewayBacnetServer::rebuildObjectsLocked() { runtime_bindings_.clear(); std::set> used_objects; if (!gateway_bacnet_stack_clear_objects()) { return ESP_FAIL; } 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(binding.object_type), static_cast(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(), binding.out_of_service, binding.reliability)) { 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, binding.out_of_service, binding.reliability, binding.readable, channel.write_callback, channel.read_callback}); } } ESP_LOGI(kTag, "BACnet/IP object table updated objects=%u", static_cast(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(object_type), static_cast(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(object_instance)); } return ok; } void GatewayBacnetServer::refreshPresentValues() { std::vector bindings; { LockGuard guard(lock_); bindings = runtime_bindings_; } for (const auto& binding : bindings) { const auto object_kind = ToBacnetKind(binding.object_type); if (object_kind == GW_BACNET_OBJECT_UNKNOWN) { continue; } if (!binding.readable || binding.read_callback == nullptr) { LockGuard guard(lock_); gateway_bacnet_stack_set_object_state(object_kind, binding.object_instance, binding.out_of_service, binding.reliability); continue; } gateway_bacnet_write_value_t stack_value = {}; const auto value = binding.read_callback(binding.object_type, binding.object_instance, binding.property); const bool converted = value.has_value() && DaliValueToStackPresentValue(binding.object_type, value.value(), &stack_value); LockGuard guard(lock_); const bool ok = converted && gateway_bacnet_stack_set_present_value( object_kind, binding.object_instance, &stack_value); gateway_bacnet_stack_set_object_state( object_kind, binding.object_instance, binding.out_of_service || !ok, ok ? binding.reliability : kReliabilityCommunicationFailure); } } void GatewayBacnetServer::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } void GatewayBacnetServer::taskLoop() { TickType_t last_timer = xTaskGetTickCount(); TickType_t last_refresh = last_timer; 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(elapsed * portTICK_PERIOD_MS); last_timer = now; } bool refresh_due = false; { LockGuard guard(lock_); gateway_bacnet_stack_poll(elapsed_ms); if ((now - last_refresh) >= kValueRefreshTicks) { refresh_due = true; } } if (refresh_due) { refreshPresentValues(); last_refresh = now; } vTaskDelay(kPollDelayTicks); } } } // namespace gateway