Files
gateway/components/gateway_bacnet/src/gateway_bacnet.cpp
T
Tony 7424b43bdd Add diagnostic bit support to Gateway Modbus
- Introduced new enum value `kShortDiagnosticBit` to `GatewayModbusGeneratedKind`.
- Enhanced `GatewayModbusPoint` and `GatewayModbusPointBinding` structures to include diagnostic snapshot, boolean key, and device type.
- Added new diagnostic bit specifications and updated the corresponding arrays for generated discrete inputs and holding registers.
- Implemented `addGeneratedDiagnosticPoint` function to handle the creation of diagnostic points.
- Updated `rebuildMap` method to include generated diagnostic points during the map rebuilding process.

Co-authored-by: Copilot <copilot@github.com>
2026-05-04 02:26:09 +08:00

468 lines
16 KiB
C++

#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);
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<int>(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<uint32_t>(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<GatewayBacnetObjectBinding> 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<GatewayBacnetObjectBinding> 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<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;
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<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(),
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<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::refreshPresentValues() {
std::vector<RuntimeBinding> 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<GatewayBacnetServer*>(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<uint16_t>(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