feat(gateway_bacnet): enhance BACnet object binding with out_of_service and reliability fields

feat(gateway_bacnet): add functions to clear BACnet objects and set their states

feat(gateway_bridge): implement discovery inventory management and scanning functionality

fix(gateway_bridge): update handleGet to support new inventory and effective model actions

refactor(gateway_bridge): improve BACnet binding handling and reliability reporting

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Tony
2026-05-02 21:16:32 +08:00
parent fa2eae87cf
commit 30a96c5125
7 changed files with 810 additions and 32 deletions
+1 -1
View File
@@ -451,7 +451,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::discoverDeviceTypes(
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
const std::vector<int> fallback = fallback_types.empty() ? std::vector<int>{4, 5, 6, 8}
const std::vector<int> fallback = fallback_types.empty() ? std::vector<int>{1, 4, 5, 6, 8}
: fallback_types;
auto discovery = channel->dali->base.discoverDeviceTypes(short_address, fallback,
max_next_types);
@@ -30,6 +30,8 @@ struct GatewayBacnetObjectBinding {
BridgeObjectType object_type{BridgeObjectType::unknown};
uint32_t object_instance{0};
std::string property{"presentValue"};
bool out_of_service{false};
uint32_t reliability{0};
};
struct GatewayBacnetServerStatus {
@@ -48,7 +48,11 @@ bool gateway_bacnet_stack_upsert_object(
gateway_bacnet_object_kind_t object_kind,
uint32_t object_instance,
const char* object_name,
const char* description);
const char* description,
bool out_of_service,
uint32_t reliability);
bool gateway_bacnet_stack_clear_objects(void);
void gateway_bacnet_stack_send_i_am(void);
void gateway_bacnet_stack_poll(uint16_t elapsed_ms);
@@ -124,6 +124,8 @@ struct GatewayBacnetServer::RuntimeBinding {
uint32_t object_instance{0};
std::string model_id;
std::string property{"presentValue"};
bool out_of_service{false};
uint32_t reliability{0};
GatewayBacnetWriteCallback write_callback;
};
@@ -171,9 +173,6 @@ esp_err_t GatewayBacnetServer::registerChannel(
binding.object_instance > kMaxBacnetInstance;
}),
bindings.end());
if (bindings.empty()) {
return ESP_ERR_NOT_FOUND;
}
LockGuard guard(lock_);
if (started_ && !configCompatible(config)) {
@@ -183,6 +182,9 @@ esp_err_t GatewayBacnetServer::registerChannel(
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)};
if (channel == channels_.end()) {
@@ -240,6 +242,10 @@ 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);
@@ -254,7 +260,9 @@ esp_err_t GatewayBacnetServer::rebuildObjectsLocked() {
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.model_id.c_str(),
binding.out_of_service,
binding.reliability)) {
return ESP_FAIL;
}
runtime_bindings_.push_back(RuntimeBinding{channel.gateway_id,
@@ -263,6 +271,8 @@ esp_err_t GatewayBacnetServer::rebuildObjectsLocked() {
binding.model_id,
binding.property.empty() ? "presentValue"
: binding.property,
binding.out_of_service,
binding.reliability,
channel.write_callback});
}
}
@@ -39,6 +39,91 @@ static const char Multistate_Value_States[] =
"State 15\0"
"State 16\0";
static bool clear_analog_value_objects(void)
{
unsigned count = Analog_Value_Count();
while (count > 0) {
count--;
Analog_Value_Delete(Analog_Value_Index_To_Instance(count));
}
return true;
}
static bool clear_analog_output_objects(void)
{
unsigned count = Analog_Output_Count();
while (count > 0) {
count--;
Analog_Output_Delete(Analog_Output_Index_To_Instance(count));
}
return true;
}
static bool clear_binary_value_objects(void)
{
unsigned count = Binary_Value_Count();
while (count > 0) {
count--;
Binary_Value_Delete(Binary_Value_Index_To_Instance(count));
}
return true;
}
static bool clear_binary_output_objects(void)
{
unsigned count = Binary_Output_Count();
while (count > 0) {
count--;
Binary_Output_Delete(Binary_Output_Index_To_Instance(count));
}
return true;
}
static bool clear_multistate_value_objects(void)
{
unsigned count = Multistate_Value_Count();
while (count > 0) {
count--;
Multistate_Value_Delete(Multistate_Value_Index_To_Instance(count));
}
return true;
}
static void set_analog_value_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Analog_Value_Out_Of_Service_Set(object_instance, out_of_service);
Analog_Value_Reliability_Set(object_instance, reliability);
}
static void set_analog_output_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Analog_Output_Out_Of_Service_Set(object_instance, out_of_service);
Analog_Output_Reliability_Set(object_instance, reliability);
}
static void set_binary_value_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Binary_Value_Out_Of_Service_Set(object_instance, out_of_service);
Binary_Value_Reliability_Set(object_instance, reliability);
}
static void set_binary_output_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Binary_Output_Out_Of_Service_Set(object_instance, out_of_service);
Binary_Output_Reliability_Set(object_instance, reliability);
}
static void set_multistate_value_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Multistate_Value_Out_Of_Service_Set(object_instance, out_of_service);
Multistate_Value_Reliability_Set(object_instance, reliability);
}
static void notify_write_real(
gateway_bacnet_object_kind_t object_kind, uint32_t object_instance, double value)
{
@@ -209,7 +294,9 @@ bool gateway_bacnet_stack_upsert_object(
gateway_bacnet_object_kind_t object_kind,
uint32_t object_instance,
const char* object_name,
const char* description)
const char* description,
bool out_of_service,
uint32_t reliability)
{
if (!object_name || object_name[0] == '\0') {
object_name = "DALI BACnet Object";
@@ -218,6 +305,8 @@ bool gateway_bacnet_stack_upsert_object(
description = "";
}
BACNET_RELIABILITY object_reliability = (BACNET_RELIABILITY)reliability;
switch (object_kind) {
case GW_BACNET_OBJECT_ANALOG_VALUE:
if (!Analog_Value_Valid_Instance(object_instance)) {
@@ -227,6 +316,7 @@ bool gateway_bacnet_stack_upsert_object(
Analog_Value_Description_Set(object_instance, description);
Analog_Value_Units_Set(object_instance, UNITS_PERCENT);
Analog_Value_Present_Value_Set(object_instance, 0.0f, BACNET_NO_PRIORITY);
set_analog_value_state(object_instance, out_of_service, object_reliability);
return true;
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
if (!Analog_Output_Valid_Instance(object_instance)) {
@@ -236,6 +326,7 @@ bool gateway_bacnet_stack_upsert_object(
Analog_Output_Description_Set(object_instance, description);
Analog_Output_Units_Set(object_instance, UNITS_PERCENT);
Analog_Output_Present_Value_Set(object_instance, 0.0f, BACNET_MAX_PRIORITY);
set_analog_output_state(object_instance, out_of_service, object_reliability);
return true;
case GW_BACNET_OBJECT_BINARY_VALUE:
if (!Binary_Value_Valid_Instance(object_instance)) {
@@ -245,6 +336,7 @@ bool gateway_bacnet_stack_upsert_object(
Binary_Value_Description_Set(object_instance, description);
Binary_Value_Write_Enable(object_instance);
Binary_Value_Present_Value_Set(object_instance, BINARY_INACTIVE);
set_binary_value_state(object_instance, out_of_service, object_reliability);
return true;
case GW_BACNET_OBJECT_BINARY_OUTPUT:
if (!Binary_Output_Valid_Instance(object_instance)) {
@@ -253,6 +345,7 @@ bool gateway_bacnet_stack_upsert_object(
Binary_Output_Name_Set(object_instance, object_name);
Binary_Output_Description_Set(object_instance, description);
Binary_Output_Present_Value_Set(object_instance, BINARY_INACTIVE, BACNET_MAX_PRIORITY);
set_binary_output_state(object_instance, out_of_service, object_reliability);
return true;
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
if (!Multistate_Value_Valid_Instance(object_instance)) {
@@ -263,12 +356,22 @@ bool gateway_bacnet_stack_upsert_object(
Multistate_Value_State_Text_List_Set(object_instance, Multistate_Value_States);
Multistate_Value_Write_Enable(object_instance);
Multistate_Value_Present_Value_Set(object_instance, 1);
set_multistate_value_state(object_instance, out_of_service, object_reliability);
return true;
default:
return false;
}
}
bool gateway_bacnet_stack_clear_objects(void)
{
return clear_analog_value_objects() &&
clear_analog_output_objects() &&
clear_binary_value_objects() &&
clear_binary_output_objects() &&
clear_multistate_value_objects();
}
void gateway_bacnet_stack_send_i_am(void)
{
Send_I_Am(&Handler_Transmit_Buffer[0]);
@@ -41,7 +41,7 @@ class GatewayBridgeService {
esp_err_t start();
GatewayBridgeHttpResponse handleGet(const std::string& action, int gateway_id = -1,
const std::string& query = {}) const;
const std::string& query = {});
GatewayBridgeHttpResponse handlePost(const std::string& action, int gateway_id,
const std::string& body);
+682 -23
View File
@@ -22,6 +22,7 @@
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <map>
#include <optional>
#include <set>
#include <string_view>
@@ -35,6 +36,18 @@ namespace {
constexpr const char* kTag = "gateway_bridge";
constexpr int kDefaultModbusPort = 1502;
constexpr size_t kModbusMaxPduBytes = 252;
constexpr const char* kDiscoveryInventoryKey = "bridge_disc";
constexpr int kMaxDaliShortAddress = 63;
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
struct BridgeDiscoveryEntry {
int short_address{0};
bool online{true};
DaliDomainSnapshot discovery;
};
using BridgeDiscoveryInventory = std::map<int, BridgeDiscoveryEntry>;
class LockGuard {
public:
@@ -164,6 +177,69 @@ bool ValidDaliAddress(int address) {
return address >= 0 && address <= 127;
}
bool ValidShortAddress(int address) {
return address >= 0 && address <= kMaxDaliShortAddress;
}
bool IsRawBridgeOperation(BridgeOperation operation) {
switch (operation) {
case BridgeOperation::send:
case BridgeOperation::sendExt:
case BridgeOperation::query:
return true;
default:
return false;
}
}
bool SnapshotHasDeviceType(const DaliDomainSnapshot& snapshot, int device_type) {
const auto primary = snapshot.ints.find("primaryType");
if (primary != snapshot.ints.end() && primary->second == device_type) {
return true;
}
const auto types = snapshot.int_arrays.find("types");
return types != snapshot.int_arrays.end() &&
std::find(types->second.begin(), types->second.end(), device_type) !=
types->second.end();
}
bool OperationRequiresDt1(BridgeOperation operation) {
switch (operation) {
case BridgeOperation::getEmergencyLevel:
case BridgeOperation::getEmergencyStatus:
case BridgeOperation::getEmergencyFailureStatus:
case BridgeOperation::startEmergencyFunctionTest:
case BridgeOperation::stopEmergencyTest:
case BridgeOperation::startEmergencyDurationTest:
return true;
default:
return false;
}
}
bool OperationRequiresDt8(BridgeOperation operation) {
switch (operation) {
case BridgeOperation::setColorTemperature:
case BridgeOperation::getColorTemperature:
case BridgeOperation::getColorStatus:
return true;
default:
return false;
}
}
std::optional<int> BridgeTargetValue(const BridgeDaliTarget& target) {
switch (target.kind) {
case BridgeDaliTargetKind::shortAddress:
return target.shortAddress;
case BridgeDaliTargetKind::group:
return target.groupAddress;
case BridgeDaliTargetKind::broadcast:
return std::nullopt;
}
return std::nullopt;
}
cJSON* IntArrayToCjson(const std::vector<int>& values) {
cJSON* array = cJSON_CreateArray();
if (array == nullptr) {
@@ -212,6 +288,248 @@ cJSON* SnapshotToCjson(const DaliDomainSnapshot& snapshot) {
return root;
}
DaliValue::Object SnapshotToValue(const DaliDomainSnapshot& snapshot) {
DaliValue::Object out;
out["gatewayId"] = snapshot.gateway_id;
out["address"] = snapshot.address;
out["kind"] = snapshot.kind;
if (!snapshot.bools.empty()) {
DaliValue::Object bools;
for (const auto& entry : snapshot.bools) {
bools[entry.first] = entry.second;
}
out["bools"] = std::move(bools);
}
if (!snapshot.ints.empty()) {
DaliValue::Object ints;
for (const auto& entry : snapshot.ints) {
ints[entry.first] = entry.second;
}
out["ints"] = std::move(ints);
}
if (!snapshot.numbers.empty()) {
DaliValue::Object numbers;
for (const auto& entry : snapshot.numbers) {
numbers[entry.first] = entry.second;
}
out["numbers"] = std::move(numbers);
}
if (!snapshot.int_arrays.empty()) {
DaliValue::Object arrays;
for (const auto& entry : snapshot.int_arrays) {
DaliValue::Array values;
values.reserve(entry.second.size());
for (const int value : entry.second) {
values.emplace_back(value);
}
arrays[entry.first] = std::move(values);
}
out["intArrays"] = std::move(arrays);
}
if (!snapshot.number_arrays.empty()) {
DaliValue::Object arrays;
for (const auto& entry : snapshot.number_arrays) {
DaliValue::Array values;
values.reserve(entry.second.size());
for (const double value : entry.second) {
values.emplace_back(value);
}
arrays[entry.first] = std::move(values);
}
out["numberArrays"] = std::move(arrays);
}
return out;
}
std::optional<DaliDomainSnapshot> SnapshotFromValue(const DaliValue* value) {
if (value == nullptr) {
return std::nullopt;
}
const auto* object = value->asObject();
if (object == nullptr) {
return std::nullopt;
}
DaliDomainSnapshot snapshot;
snapshot.gateway_id = static_cast<uint8_t>(getObjectInt(*object, "gatewayId").value_or(0));
snapshot.address = getObjectInt(*object, "address").value_or(0);
snapshot.kind = getObjectString(*object, "kind").value_or("device");
if (const auto* bools = getObjectValue(*object, "bools")) {
if (const auto* bool_object = bools->asObject()) {
for (const auto& entry : *bool_object) {
if (const auto parsed = entry.second.asBool()) {
snapshot.bools[entry.first] = parsed.value();
}
}
}
}
if (const auto* ints = getObjectValue(*object, "ints")) {
if (const auto* int_object = ints->asObject()) {
for (const auto& entry : *int_object) {
if (const auto parsed = entry.second.asInt()) {
snapshot.ints[entry.first] = parsed.value();
}
}
}
}
if (const auto* numbers = getObjectValue(*object, "numbers")) {
if (const auto* number_object = numbers->asObject()) {
for (const auto& entry : *number_object) {
if (const auto parsed = entry.second.asDouble()) {
snapshot.numbers[entry.first] = parsed.value();
}
}
}
}
if (const auto* int_arrays = getObjectValue(*object, "intArrays")) {
if (const auto* array_object = int_arrays->asObject()) {
for (const auto& entry : *array_object) {
const auto* array = entry.second.asArray();
if (array == nullptr) {
continue;
}
std::vector<int> values;
values.reserve(array->size());
for (const auto& item : *array) {
values.push_back(item.asInt().value_or(0));
}
snapshot.int_arrays[entry.first] = std::move(values);
}
}
}
if (const auto* number_arrays = getObjectValue(*object, "numberArrays")) {
if (const auto* array_object = number_arrays->asObject()) {
for (const auto& entry : *array_object) {
const auto* array = entry.second.asArray();
if (array == nullptr) {
continue;
}
std::vector<double> values;
values.reserve(array->size());
for (const auto& item : *array) {
values.push_back(item.asDouble().value_or(0.0));
}
snapshot.number_arrays[entry.first] = std::move(values);
}
}
}
return snapshot;
}
const char* DiscoveryStateString(bool online) {
return online ? "online" : "offline";
}
const char* BacnetReliabilityToString(uint32_t reliability) {
switch (reliability) {
case kBacnetReliabilityCommunicationFailure:
return "communication_failure";
case kBacnetReliabilityNoFaultDetected:
default:
return "no_fault_detected";
}
}
bool SnapshotSupportsDeviceType(const DaliDomainSnapshot& snapshot, int device_type) {
return SnapshotHasDeviceType(snapshot, device_type);
}
void AddDiscoveryCapabilities(cJSON* root, const DaliDomainSnapshot& snapshot) {
if (root == nullptr) {
return;
}
cJSON_AddBoolToObject(root, "supportsDt1", SnapshotSupportsDeviceType(snapshot, 1));
cJSON_AddBoolToObject(root, "supportsDt4", SnapshotSupportsDeviceType(snapshot, 4));
cJSON_AddBoolToObject(root, "supportsDt5", SnapshotSupportsDeviceType(snapshot, 5));
cJSON_AddBoolToObject(root, "supportsDt6", SnapshotSupportsDeviceType(snapshot, 6));
cJSON_AddBoolToObject(root, "supportsDt8", SnapshotSupportsDeviceType(snapshot, 8));
}
cJSON* DiscoveryEntryToCjson(const BridgeDiscoveryEntry& entry) {
cJSON* root = SnapshotToCjson(entry.discovery);
if (root == nullptr) {
return nullptr;
}
cJSON_AddBoolToObject(root, "discovered", true);
cJSON_AddBoolToObject(root, "online", entry.online);
cJSON_AddStringToObject(root, "inventoryState", DiscoveryStateString(entry.online));
AddDiscoveryCapabilities(root, entry.discovery);
return root;
}
cJSON* MissingDiscoveryEntryToCjson(uint8_t gateway_id, int short_address) {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON_AddNumberToObject(root, "gatewayId", gateway_id);
cJSON_AddNumberToObject(root, "address", short_address);
cJSON_AddStringToObject(root, "kind", "device");
cJSON_AddBoolToObject(root, "discovered", false);
cJSON_AddBoolToObject(root, "online", false);
cJSON_AddStringToObject(root, "inventoryState", "never_seen");
return root;
}
DaliValue::Object DiscoveryEntryToValue(const BridgeDiscoveryEntry& entry) {
DaliValue::Object out;
out["shortAddress"] = entry.short_address;
out["state"] = DiscoveryStateString(entry.online);
out["snapshot"] = SnapshotToValue(entry.discovery);
return out;
}
std::optional<BridgeDiscoveryEntry> DiscoveryEntryFromValue(const DaliValue& value) {
const auto* object = value.asObject();
if (object == nullptr) {
return std::nullopt;
}
const auto short_address = getObjectInt(*object, "shortAddress");
const auto snapshot = SnapshotFromValue(getObjectValue(*object, "snapshot"));
if (!short_address.has_value() || !snapshot.has_value() ||
!ValidShortAddress(short_address.value())) {
return std::nullopt;
}
const std::string state = getObjectString(*object, "state").value_or("online");
BridgeDiscoveryEntry entry;
entry.short_address = short_address.value();
entry.online = state != "offline";
entry.discovery = snapshot.value();
entry.discovery.address = short_address.value();
return entry;
}
DaliValue::Object DiscoveryInventoryToValue(const BridgeDiscoveryInventory& inventory) {
DaliValue::Object out;
DaliValue::Array entries;
entries.reserve(inventory.size());
for (const auto& item : inventory) {
entries.emplace_back(DiscoveryEntryToValue(item.second));
}
out["entries"] = std::move(entries);
return out;
}
BridgeDiscoveryInventory DiscoveryInventoryFromValue(const DaliValue::Object& object) {
BridgeDiscoveryInventory inventory;
const auto* entries = getObjectValue(object, "entries");
if (entries == nullptr || entries->asArray() == nullptr) {
return inventory;
}
for (const auto& item : *entries->asArray()) {
const auto entry = DiscoveryEntryFromValue(item);
if (!entry.has_value()) {
continue;
}
inventory[entry->short_address] = entry.value();
}
return inventory;
}
GatewayBridgeHttpResponse SnapshotResponse(const std::optional<DaliDomainSnapshot>& snapshot,
const char* missing_message) {
if (!snapshot.has_value()) {
@@ -568,8 +886,10 @@ struct GatewayBridgeService::ChannelRuntime {
#endif
std::unique_ptr<DaliCloudBridge> cloud;
BridgeRuntimeConfig bridge_config;
BridgeDiscoveryInventory discovery_inventory;
std::optional<GatewayCloudConfig> cloud_config;
bool bridge_config_loaded{false};
bool discovery_inventory_loaded{false};
bool cloud_config_loaded{false};
bool cloud_started{false};
bool modbus_started{false};
@@ -601,6 +921,11 @@ struct GatewayBridgeService::ChannelRuntime {
BridgeProvisioningStore bridge_store(bridgeNamespace());
bridge_config_loaded = bridge_store.load(&bridge_config) == ESP_OK;
DaliValue::Object discovery_object;
if (bridge_store.loadObject(kDiscoveryInventoryKey, &discovery_object) == ESP_OK) {
discovery_inventory = DiscoveryInventoryFromValue(discovery_object);
discovery_inventory_loaded = true;
}
applyBridgeConfigLocked();
GatewayProvisioningStore cloud_store(cloudNamespace());
@@ -643,6 +968,98 @@ struct GatewayBridgeService::ChannelRuntime {
bacnet_started = false;
}
esp_err_t saveDiscoveryInventoryLocked() const {
BridgeProvisioningStore store(bridgeNamespace());
if (discovery_inventory.empty()) {
return store.clearKey(kDiscoveryInventoryKey);
}
return store.saveObject(kDiscoveryInventoryKey, DiscoveryInventoryToValue(discovery_inventory));
}
const BridgeDiscoveryEntry* findDiscoveryEntryLocked(int short_address) const {
const auto entry = discovery_inventory.find(short_address);
return entry == discovery_inventory.end() ? nullptr : &entry->second;
}
BridgeDiscoveryEntry* findDiscoveryEntryLocked(int short_address) {
const auto entry = discovery_inventory.find(short_address);
return entry == discovery_inventory.end() ? nullptr : &entry->second;
}
const BridgeDiscoveryEntry* updateDiscoveryEntryLocked(int short_address, bool persist) {
if (!ValidShortAddress(short_address)) {
return nullptr;
}
const auto snapshot = domain.discoverDeviceTypes(channel.gateway_id, short_address);
if (snapshot.has_value()) {
auto& entry = discovery_inventory[short_address];
entry.short_address = short_address;
entry.online = true;
entry.discovery = snapshot.value();
entry.discovery.address = short_address;
entry.discovery.gateway_id = channel.gateway_id;
if (persist) {
saveDiscoveryInventoryLocked();
}
return &entry;
}
auto* existing = findDiscoveryEntryLocked(short_address);
if (existing != nullptr) {
existing->online = false;
if (persist) {
saveDiscoveryInventoryLocked();
}
}
return existing;
}
const BridgeDiscoveryEntry* ensureDiscoveryEntryLocked(int short_address) {
const auto* existing = findDiscoveryEntryLocked(short_address);
if (existing != nullptr) {
return existing;
}
return updateDiscoveryEntryLocked(short_address, true);
}
std::vector<int> referencedShortAddressesLocked() const {
std::set<int> addresses;
for (const auto& model : bridge_config.models) {
if (model.dali.kind != BridgeDaliTargetKind::shortAddress ||
!model.dali.shortAddress.has_value() ||
!ValidShortAddress(model.dali.shortAddress.value())) {
continue;
}
addresses.insert(model.dali.shortAddress.value());
}
return std::vector<int>(addresses.begin(), addresses.end());
}
esp_err_t syncBacnetServerLocked() {
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
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() && !bacnet_started) {
return ESP_ERR_NOT_FOUND;
}
const auto server_config = bacnetServerConfigLocked();
return 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);
});
#else
return ESP_ERR_NOT_SUPPORTED;
#endif
}
void applyCloudModelsLocked() {
if (cloud_started && cloud != nullptr) {
cloud->stop();
@@ -760,6 +1177,45 @@ struct GatewayBridgeService::ChannelRuntime {
return model == bridge_config.models.end() ? model_id : model->displayName();
}
bool shouldPublishBacnetBindingLocked(const BacnetObjectBinding& binding) {
if (binding.objectInstance < 0) {
return false;
}
if (IsRawBridgeOperation(binding.operation) ||
binding.target.kind != BridgeDaliTargetKind::shortAddress) {
return true;
}
if (!binding.target.shortAddress.has_value()) {
return false;
}
const int short_address = binding.target.shortAddress.value();
const auto* discovered = ensureDiscoveryEntryLocked(short_address);
if (discovered == nullptr) {
return false;
}
if (OperationRequiresDt1(binding.operation)) {
return SnapshotHasDeviceType(discovered->discovery, 1);
}
if (OperationRequiresDt8(binding.operation)) {
return SnapshotHasDeviceType(discovered->discovery, 8);
}
return true;
}
std::vector<BacnetObjectBinding> effectiveBacnetObjectsLocked() {
std::vector<BacnetObjectBinding> bindings;
if (bacnet == nullptr) {
return bindings;
}
for (const auto& binding : bacnet->describeObjects()) {
if (shouldPublishBacnetBindingLocked(binding)) {
bindings.push_back(binding);
}
}
return bindings;
}
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
GatewayBacnetServerConfig bacnetServerConfigLocked() const {
GatewayBacnetServerConfig config;
@@ -776,22 +1232,32 @@ struct GatewayBridgeService::ChannelRuntime {
return config;
}
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() const {
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() {
std::vector<GatewayBacnetObjectBinding> bindings;
if (bacnet == nullptr) {
return bindings;
}
for (const auto& binding : bacnet->describeObjects()) {
for (const auto& binding : effectiveBacnetObjectsLocked()) {
if (binding.objectInstance < 0) {
continue;
}
bool out_of_service = false;
uint32_t reliability = kBacnetReliabilityNoFaultDetected;
if (binding.target.kind == BridgeDaliTargetKind::shortAddress &&
binding.target.shortAddress.has_value()) {
if (const auto* discovery = findDiscoveryEntryLocked(binding.target.shortAddress.value())) {
if (!discovery->online) {
out_of_service = true;
reliability = kBacnetReliabilityCommunicationFailure;
}
}
}
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});
: binding.property,
out_of_service,
reliability});
}
return bindings;
}
@@ -819,17 +1285,7 @@ struct GatewayBridgeService::ChannelRuntime {
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);
});
const esp_err_t err = syncBacnetServerLocked();
bacnet_started = err == ESP_OK;
return err;
}
@@ -864,6 +1320,9 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddNumberToObject(root, "channel", channel.channel_index);
cJSON_AddStringToObject(root, "name", channel.name.c_str());
cJSON_AddBoolToObject(root, "bridgeConfigLoaded", bridge_config_loaded);
cJSON_AddBoolToObject(root, "discoveryInventoryLoaded", discovery_inventory_loaded);
cJSON_AddNumberToObject(root, "inventoryCount",
static_cast<double>(discovery_inventory.size()));
cJSON_AddNumberToObject(root, "modelCount", static_cast<double>(bridge_config.models.size()));
cJSON* modbus_json = cJSON_CreateObject();
@@ -922,6 +1381,154 @@ struct GatewayBridgeService::ChannelRuntime {
return GatewayBridgeHttpResponse{ESP_OK, BridgeRuntimeConfigToJson(bridge_config)};
}
GatewayBridgeHttpResponse inventoryJson() const {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON_AddBoolToObject(root, "loaded", discovery_inventory_loaded);
cJSON_AddNumberToObject(root, "count", static_cast<double>(discovery_inventory.size()));
cJSON* inventory = cJSON_CreateArray();
if (inventory != nullptr) {
for (const auto& entry : discovery_inventory) {
cJSON_AddItemToArray(inventory, DiscoveryEntryToCjson(entry.second));
}
cJSON_AddItemToObject(root, "inventory", inventory);
}
return JsonOk(root);
}
GatewayBridgeHttpResponse effectiveModelJsonLocked() {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON_AddStringToObject(root, "name", channel.name.c_str());
cJSON* models = cJSON_CreateArray();
const auto effective_bacnet = effectiveBacnetObjectsLocked();
if (models != nullptr) {
for (const auto& model : bridge_config.models) {
cJSON* item = ToCjson(DaliValue(model.toJson()));
if (item == nullptr) {
continue;
}
cJSON_AddStringToObject(item, "displayName", model.displayName().c_str());
if (model.dali.kind == BridgeDaliTargetKind::shortAddress &&
model.dali.shortAddress.has_value() &&
ValidShortAddress(model.dali.shortAddress.value())) {
const int short_address = model.dali.shortAddress.value();
if (const auto* entry = findDiscoveryEntryLocked(short_address)) {
cJSON_AddBoolToObject(item, "discovered", true);
cJSON_AddBoolToObject(item, "online", entry->online);
cJSON_AddStringToObject(item, "inventoryState", DiscoveryStateString(entry->online));
cJSON_AddItemToObject(item, "discovery", DiscoveryEntryToCjson(*entry));
} else {
cJSON_AddBoolToObject(item, "discovered", false);
cJSON_AddBoolToObject(item, "online", false);
cJSON_AddStringToObject(item, "inventoryState", "never_seen");
}
}
if (model.protocol == BridgeProtocolKind::bacnet &&
model.external.objectInstance.has_value()) {
const auto binding = std::find_if(
effective_bacnet.begin(), effective_bacnet.end(),
[&model](const auto& item) { return item.modelID == model.id; });
const bool published = binding != effective_bacnet.end();
cJSON_AddBoolToObject(item, "bacnetPublished", published);
if (published && model.dali.kind == BridgeDaliTargetKind::shortAddress &&
model.dali.shortAddress.has_value()) {
const auto* entry = findDiscoveryEntryLocked(model.dali.shortAddress.value());
const bool out_of_service = entry != nullptr && !entry->online;
cJSON_AddBoolToObject(item, "bacnetOutOfService", out_of_service);
cJSON_AddStringToObject(item, "bacnetReliability",
BacnetReliabilityToString(out_of_service
? kBacnetReliabilityCommunicationFailure
: kBacnetReliabilityNoFaultDetected));
}
}
cJSON_AddItemToArray(models, item);
}
cJSON_AddItemToObject(root, "models", models);
}
return JsonOk(root);
}
GatewayBridgeHttpResponse effectiveModelJson() {
LockGuard guard(lock);
return effectiveModelJsonLocked();
}
GatewayBridgeHttpResponse scanInventory(std::string_view body) {
std::string mode = "referenced";
if (!body.empty()) {
cJSON* root = cJSON_ParseWithLength(body.data(), body.size());
if (root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid scan JSON");
}
if (const char* mode_value = JsonString(root, "mode")) {
mode = mode_value;
} else if (const char* mode_value = JsonString(root, "scanMode")) {
mode = mode_value;
}
cJSON_Delete(root);
}
LockGuard guard(lock);
std::vector<int> addresses;
if (mode == "all" || mode == "full") {
addresses.reserve(kMaxDaliShortAddress + 1);
for (int address = 0; address <= kMaxDaliShortAddress; ++address) {
addresses.push_back(address);
}
mode = "all";
} else {
addresses = referencedShortAddressesLocked();
mode = "referenced";
}
size_t online_count = 0;
size_t offline_count = 0;
size_t unseen_count = 0;
cJSON* results = cJSON_CreateArray();
for (const int address : addresses) {
const auto* entry = updateDiscoveryEntryLocked(address, false);
if (entry != nullptr) {
if (entry->online) {
++online_count;
} else {
++offline_count;
}
cJSON_AddItemToArray(results, DiscoveryEntryToCjson(*entry));
} else {
++unseen_count;
cJSON_AddItemToArray(results, MissingDiscoveryEntryToCjson(channel.gateway_id, address));
}
}
const esp_err_t persist_err = saveDiscoveryInventoryLocked();
if (persist_err != ESP_OK) {
cJSON_Delete(results);
return ErrorResponse(persist_err, "failed to persist discovery inventory");
}
discovery_inventory_loaded = discovery_inventory_loaded || !discovery_inventory.empty();
if (bacnet_started) {
const esp_err_t err = syncBacnetServerLocked();
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND) {
cJSON_Delete(results);
return ErrorResponse(err, "failed to refresh BACnet bridge after scan");
}
bacnet_started = err == ESP_OK;
}
cJSON* root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", true);
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON_AddStringToObject(root, "mode", mode.c_str());
cJSON_AddNumberToObject(root, "scanned", static_cast<double>(addresses.size()));
cJSON_AddNumberToObject(root, "onlineCount", static_cast<double>(online_count));
cJSON_AddNumberToObject(root, "offlineCount", static_cast<double>(offline_count));
cJSON_AddNumberToObject(root, "neverSeenCount", static_cast<double>(unseen_count));
cJSON_AddItemToObject(root, "inventory", results);
return JsonOk(root);
}
GatewayBridgeHttpResponse modbusBindingsJson() const {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
@@ -941,13 +1548,13 @@ struct GatewayBridgeService::ChannelRuntime {
return JsonOk(root);
}
GatewayBridgeHttpResponse bacnetBindingsJson() const {
GatewayBridgeHttpResponse bacnetBindingsJson() {
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()) {
for (const auto& binding : effectiveBacnetObjectsLocked()) {
cJSON* item = cJSON_CreateObject();
if (item == nullptr) {
continue;
@@ -956,6 +1563,36 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddStringToObject(item, "objectType", bridgeObjectTypeToString(binding.objectType));
cJSON_AddNumberToObject(item, "objectInstance", binding.objectInstance);
cJSON_AddStringToObject(item, "property", binding.property.c_str());
cJSON_AddStringToObject(item, "operation", bridgeOperationToString(binding.operation));
cJSON_AddStringToObject(item, "targetKind",
bridgeDaliTargetKindToString(binding.target.kind));
if (const auto target_value = BridgeTargetValue(binding.target)) {
cJSON_AddNumberToObject(item, "targetAddress", target_value.value());
}
if (binding.target.rawAddress.has_value()) {
cJSON_AddNumberToObject(item, "rawAddress", binding.target.rawAddress.value());
}
if (binding.target.rawCommand.has_value()) {
cJSON_AddNumberToObject(item, "rawCommand", binding.target.rawCommand.value());
}
if (binding.target.kind == BridgeDaliTargetKind::shortAddress &&
binding.target.shortAddress.has_value()) {
if (const auto* discovery = findDiscoveryEntryLocked(binding.target.shortAddress.value())) {
const bool out_of_service = !discovery->online;
cJSON_AddBoolToObject(item, "outOfService", out_of_service);
cJSON_AddStringToObject(item, "reliability",
BacnetReliabilityToString(out_of_service
? kBacnetReliabilityCommunicationFailure
: kBacnetReliabilityNoFaultDetected));
cJSON_AddStringToObject(item, "inventoryState",
DiscoveryStateString(discovery->online));
} else {
cJSON_AddBoolToObject(item, "outOfService", false);
cJSON_AddStringToObject(item, "reliability",
BacnetReliabilityToString(kBacnetReliabilityNoFaultDetected));
cJSON_AddStringToObject(item, "inventoryState", "never_seen");
}
}
cJSON_AddItemToArray(bindings, item);
}
}
@@ -1226,7 +1863,7 @@ const GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(
}
GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
const std::string& action_arg, int gateway_id_arg, const std::string& query_arg) const {
const std::string& action_arg, int gateway_id_arg, const std::string& query_arg) {
if (!config_.bridge_enabled) {
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "bridge service is disabled");
}
@@ -1257,7 +1894,7 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
if (!gateway_id.has_value()) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required");
}
const auto* runtime = findRuntime(gateway_id.value());
auto* runtime = findRuntime(gateway_id.value());
if (runtime == nullptr) {
return ErrorResponse(ESP_ERR_NOT_FOUND, "unknown gateway id");
}
@@ -1274,6 +1911,12 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
if (action == "bacnet") {
return runtime->bacnetBindingsJson();
}
if (action == "inventory") {
return runtime->inventoryJson();
}
if (action == "effective_model") {
return runtime->effectiveModelJson();
}
if (action == "cloud") {
return runtime->cloudJson();
}
@@ -1282,8 +1925,21 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
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");
{
LockGuard guard(runtime->lock);
const auto* entry = runtime->updateDiscoveryEntryLocked(address.value(), true);
if (entry != nullptr && runtime->bacnet_started) {
const esp_err_t err = runtime->syncBacnetServerLocked();
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND) {
return ErrorResponse(err, "failed to refresh BACnet bridge after discovery");
}
runtime->bacnet_started = err == ESP_OK;
}
if (entry != nullptr) {
return JsonOk(DiscoveryEntryToCjson(*entry));
}
}
return ErrorResponse(ESP_ERR_NOT_FOUND, "device did not respond to type discovery");
}
if (action == "dt4" || action == "dt5" || action == "dt6") {
const auto address = QueryInt(query, "addr", "address");
@@ -1443,6 +2099,9 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
}
return handleGet("bacnet", gateway_id.value());
}
if (action == "scan") {
return runtime->scanInventory(body);
}
return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge POST action");
}