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:
@@ -451,7 +451,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::discoverDeviceTypes(
|
|||||||
if (channel == nullptr || channel->dali == nullptr) {
|
if (channel == nullptr || channel->dali == nullptr) {
|
||||||
return std::nullopt;
|
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;
|
: fallback_types;
|
||||||
auto discovery = channel->dali->base.discoverDeviceTypes(short_address, fallback,
|
auto discovery = channel->dali->base.discoverDeviceTypes(short_address, fallback,
|
||||||
max_next_types);
|
max_next_types);
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ struct GatewayBacnetObjectBinding {
|
|||||||
BridgeObjectType object_type{BridgeObjectType::unknown};
|
BridgeObjectType object_type{BridgeObjectType::unknown};
|
||||||
uint32_t object_instance{0};
|
uint32_t object_instance{0};
|
||||||
std::string property{"presentValue"};
|
std::string property{"presentValue"};
|
||||||
|
bool out_of_service{false};
|
||||||
|
uint32_t reliability{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GatewayBacnetServerStatus {
|
struct GatewayBacnetServerStatus {
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ bool gateway_bacnet_stack_upsert_object(
|
|||||||
gateway_bacnet_object_kind_t object_kind,
|
gateway_bacnet_object_kind_t object_kind,
|
||||||
uint32_t object_instance,
|
uint32_t object_instance,
|
||||||
const char* object_name,
|
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_send_i_am(void);
|
||||||
void gateway_bacnet_stack_poll(uint16_t elapsed_ms);
|
void gateway_bacnet_stack_poll(uint16_t elapsed_ms);
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ struct GatewayBacnetServer::RuntimeBinding {
|
|||||||
uint32_t object_instance{0};
|
uint32_t object_instance{0};
|
||||||
std::string model_id;
|
std::string model_id;
|
||||||
std::string property{"presentValue"};
|
std::string property{"presentValue"};
|
||||||
|
bool out_of_service{false};
|
||||||
|
uint32_t reliability{0};
|
||||||
GatewayBacnetWriteCallback write_callback;
|
GatewayBacnetWriteCallback write_callback;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,12 +170,9 @@ esp_err_t GatewayBacnetServer::registerChannel(
|
|||||||
|
|
||||||
bindings.erase(std::remove_if(bindings.begin(), bindings.end(), [](const auto& binding) {
|
bindings.erase(std::remove_if(bindings.begin(), bindings.end(), [](const auto& binding) {
|
||||||
return !IsSupportedObjectType(binding.object_type) ||
|
return !IsSupportedObjectType(binding.object_type) ||
|
||||||
binding.object_instance > kMaxBacnetInstance;
|
binding.object_instance > kMaxBacnetInstance;
|
||||||
}),
|
}),
|
||||||
bindings.end());
|
bindings.end());
|
||||||
if (bindings.empty()) {
|
|
||||||
return ESP_ERR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
LockGuard guard(lock_);
|
LockGuard guard(lock_);
|
||||||
if (started_ && !configCompatible(config)) {
|
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) {
|
auto channel = std::find_if(channels_.begin(), channels_.end(), [gateway_id](const auto& item) {
|
||||||
return item.gateway_id == gateway_id;
|
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),
|
ChannelRegistration registration{gateway_id, config, std::move(bindings),
|
||||||
std::move(write_callback)};
|
std::move(write_callback)};
|
||||||
if (channel == channels_.end()) {
|
if (channel == channels_.end()) {
|
||||||
@@ -240,6 +242,10 @@ esp_err_t GatewayBacnetServer::rebuildObjectsLocked() {
|
|||||||
runtime_bindings_.clear();
|
runtime_bindings_.clear();
|
||||||
std::set<std::pair<BridgeObjectType, uint32_t>> used_objects;
|
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& channel : channels_) {
|
||||||
for (const auto& binding : channel.bindings) {
|
for (const auto& binding : channel.bindings) {
|
||||||
const auto key = std::make_pair(binding.object_type, binding.object_instance);
|
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);
|
const std::string name = ObjectName(binding);
|
||||||
if (!gateway_bacnet_stack_upsert_object(ToBacnetKind(binding.object_type),
|
if (!gateway_bacnet_stack_upsert_object(ToBacnetKind(binding.object_type),
|
||||||
binding.object_instance, name.c_str(),
|
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;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
runtime_bindings_.push_back(RuntimeBinding{channel.gateway_id,
|
runtime_bindings_.push_back(RuntimeBinding{channel.gateway_id,
|
||||||
@@ -263,6 +271,8 @@ esp_err_t GatewayBacnetServer::rebuildObjectsLocked() {
|
|||||||
binding.model_id,
|
binding.model_id,
|
||||||
binding.property.empty() ? "presentValue"
|
binding.property.empty() ? "presentValue"
|
||||||
: binding.property,
|
: binding.property,
|
||||||
|
binding.out_of_service,
|
||||||
|
binding.reliability,
|
||||||
channel.write_callback});
|
channel.write_callback});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,91 @@ static const char Multistate_Value_States[] =
|
|||||||
"State 15\0"
|
"State 15\0"
|
||||||
"State 16\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(
|
static void notify_write_real(
|
||||||
gateway_bacnet_object_kind_t object_kind, uint32_t object_instance, double value)
|
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,
|
gateway_bacnet_object_kind_t object_kind,
|
||||||
uint32_t object_instance,
|
uint32_t object_instance,
|
||||||
const char* object_name,
|
const char* object_name,
|
||||||
const char* description)
|
const char* description,
|
||||||
|
bool out_of_service,
|
||||||
|
uint32_t reliability)
|
||||||
{
|
{
|
||||||
if (!object_name || object_name[0] == '\0') {
|
if (!object_name || object_name[0] == '\0') {
|
||||||
object_name = "DALI BACnet Object";
|
object_name = "DALI BACnet Object";
|
||||||
@@ -218,6 +305,8 @@ bool gateway_bacnet_stack_upsert_object(
|
|||||||
description = "";
|
description = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BACNET_RELIABILITY object_reliability = (BACNET_RELIABILITY)reliability;
|
||||||
|
|
||||||
switch (object_kind) {
|
switch (object_kind) {
|
||||||
case GW_BACNET_OBJECT_ANALOG_VALUE:
|
case GW_BACNET_OBJECT_ANALOG_VALUE:
|
||||||
if (!Analog_Value_Valid_Instance(object_instance)) {
|
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_Description_Set(object_instance, description);
|
||||||
Analog_Value_Units_Set(object_instance, UNITS_PERCENT);
|
Analog_Value_Units_Set(object_instance, UNITS_PERCENT);
|
||||||
Analog_Value_Present_Value_Set(object_instance, 0.0f, BACNET_NO_PRIORITY);
|
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;
|
return true;
|
||||||
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
|
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
|
||||||
if (!Analog_Output_Valid_Instance(object_instance)) {
|
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_Description_Set(object_instance, description);
|
||||||
Analog_Output_Units_Set(object_instance, UNITS_PERCENT);
|
Analog_Output_Units_Set(object_instance, UNITS_PERCENT);
|
||||||
Analog_Output_Present_Value_Set(object_instance, 0.0f, BACNET_MAX_PRIORITY);
|
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;
|
return true;
|
||||||
case GW_BACNET_OBJECT_BINARY_VALUE:
|
case GW_BACNET_OBJECT_BINARY_VALUE:
|
||||||
if (!Binary_Value_Valid_Instance(object_instance)) {
|
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_Description_Set(object_instance, description);
|
||||||
Binary_Value_Write_Enable(object_instance);
|
Binary_Value_Write_Enable(object_instance);
|
||||||
Binary_Value_Present_Value_Set(object_instance, BINARY_INACTIVE);
|
Binary_Value_Present_Value_Set(object_instance, BINARY_INACTIVE);
|
||||||
|
set_binary_value_state(object_instance, out_of_service, object_reliability);
|
||||||
return true;
|
return true;
|
||||||
case GW_BACNET_OBJECT_BINARY_OUTPUT:
|
case GW_BACNET_OBJECT_BINARY_OUTPUT:
|
||||||
if (!Binary_Output_Valid_Instance(object_instance)) {
|
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_Name_Set(object_instance, object_name);
|
||||||
Binary_Output_Description_Set(object_instance, description);
|
Binary_Output_Description_Set(object_instance, description);
|
||||||
Binary_Output_Present_Value_Set(object_instance, BINARY_INACTIVE, BACNET_MAX_PRIORITY);
|
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;
|
return true;
|
||||||
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
|
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
|
||||||
if (!Multistate_Value_Valid_Instance(object_instance)) {
|
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_State_Text_List_Set(object_instance, Multistate_Value_States);
|
||||||
Multistate_Value_Write_Enable(object_instance);
|
Multistate_Value_Write_Enable(object_instance);
|
||||||
Multistate_Value_Present_Value_Set(object_instance, 1);
|
Multistate_Value_Present_Value_Set(object_instance, 1);
|
||||||
|
set_multistate_value_state(object_instance, out_of_service, object_reliability);
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
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)
|
void gateway_bacnet_stack_send_i_am(void)
|
||||||
{
|
{
|
||||||
Send_I_Am(&Handler_Transmit_Buffer[0]);
|
Send_I_Am(&Handler_Transmit_Buffer[0]);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class GatewayBridgeService {
|
|||||||
esp_err_t start();
|
esp_err_t start();
|
||||||
|
|
||||||
GatewayBridgeHttpResponse handleGet(const std::string& action, int gateway_id = -1,
|
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,
|
GatewayBridgeHttpResponse handlePost(const std::string& action, int gateway_id,
|
||||||
const std::string& body);
|
const std::string& body);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@@ -35,6 +36,18 @@ namespace {
|
|||||||
constexpr const char* kTag = "gateway_bridge";
|
constexpr const char* kTag = "gateway_bridge";
|
||||||
constexpr int kDefaultModbusPort = 1502;
|
constexpr int kDefaultModbusPort = 1502;
|
||||||
constexpr size_t kModbusMaxPduBytes = 252;
|
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 {
|
class LockGuard {
|
||||||
public:
|
public:
|
||||||
@@ -164,6 +177,69 @@ bool ValidDaliAddress(int address) {
|
|||||||
return address >= 0 && address <= 127;
|
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* IntArrayToCjson(const std::vector<int>& values) {
|
||||||
cJSON* array = cJSON_CreateArray();
|
cJSON* array = cJSON_CreateArray();
|
||||||
if (array == nullptr) {
|
if (array == nullptr) {
|
||||||
@@ -212,6 +288,248 @@ cJSON* SnapshotToCjson(const DaliDomainSnapshot& snapshot) {
|
|||||||
return root;
|
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,
|
GatewayBridgeHttpResponse SnapshotResponse(const std::optional<DaliDomainSnapshot>& snapshot,
|
||||||
const char* missing_message) {
|
const char* missing_message) {
|
||||||
if (!snapshot.has_value()) {
|
if (!snapshot.has_value()) {
|
||||||
@@ -568,8 +886,10 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
#endif
|
#endif
|
||||||
std::unique_ptr<DaliCloudBridge> cloud;
|
std::unique_ptr<DaliCloudBridge> cloud;
|
||||||
BridgeRuntimeConfig bridge_config;
|
BridgeRuntimeConfig bridge_config;
|
||||||
|
BridgeDiscoveryInventory discovery_inventory;
|
||||||
std::optional<GatewayCloudConfig> cloud_config;
|
std::optional<GatewayCloudConfig> cloud_config;
|
||||||
bool bridge_config_loaded{false};
|
bool bridge_config_loaded{false};
|
||||||
|
bool discovery_inventory_loaded{false};
|
||||||
bool cloud_config_loaded{false};
|
bool cloud_config_loaded{false};
|
||||||
bool cloud_started{false};
|
bool cloud_started{false};
|
||||||
bool modbus_started{false};
|
bool modbus_started{false};
|
||||||
@@ -601,6 +921,11 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
|
|
||||||
BridgeProvisioningStore bridge_store(bridgeNamespace());
|
BridgeProvisioningStore bridge_store(bridgeNamespace());
|
||||||
bridge_config_loaded = bridge_store.load(&bridge_config) == ESP_OK;
|
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();
|
applyBridgeConfigLocked();
|
||||||
|
|
||||||
GatewayProvisioningStore cloud_store(cloudNamespace());
|
GatewayProvisioningStore cloud_store(cloudNamespace());
|
||||||
@@ -643,6 +968,98 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
bacnet_started = false;
|
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() {
|
void applyCloudModelsLocked() {
|
||||||
if (cloud_started && cloud != nullptr) {
|
if (cloud_started && cloud != nullptr) {
|
||||||
cloud->stop();
|
cloud->stop();
|
||||||
@@ -760,6 +1177,45 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
return model == bridge_config.models.end() ? model_id : model->displayName();
|
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)
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||||
GatewayBacnetServerConfig bacnetServerConfigLocked() const {
|
GatewayBacnetServerConfig bacnetServerConfigLocked() const {
|
||||||
GatewayBacnetServerConfig config;
|
GatewayBacnetServerConfig config;
|
||||||
@@ -776,22 +1232,32 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() const {
|
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() {
|
||||||
std::vector<GatewayBacnetObjectBinding> bindings;
|
std::vector<GatewayBacnetObjectBinding> bindings;
|
||||||
if (bacnet == nullptr) {
|
for (const auto& binding : effectiveBacnetObjectsLocked()) {
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
for (const auto& binding : bacnet->describeObjects()) {
|
|
||||||
if (binding.objectInstance < 0) {
|
if (binding.objectInstance < 0) {
|
||||||
continue;
|
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,
|
bindings.push_back(GatewayBacnetObjectBinding{channel.gateway_id,
|
||||||
binding.modelID,
|
binding.modelID,
|
||||||
modelName(binding.modelID),
|
modelName(binding.modelID),
|
||||||
binding.objectType,
|
binding.objectType,
|
||||||
static_cast<uint32_t>(binding.objectInstance),
|
static_cast<uint32_t>(binding.objectInstance),
|
||||||
binding.property.empty() ? "presentValue"
|
binding.property.empty() ? "presentValue"
|
||||||
: binding.property});
|
: binding.property,
|
||||||
|
out_of_service,
|
||||||
|
reliability});
|
||||||
}
|
}
|
||||||
return bindings;
|
return bindings;
|
||||||
}
|
}
|
||||||
@@ -819,17 +1285,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
if (bacnet == nullptr) {
|
if (bacnet == nullptr) {
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
const auto bindings = bacnetObjectBindingsLocked();
|
const esp_err_t err = syncBacnetServerLocked();
|
||||||
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;
|
bacnet_started = err == ESP_OK;
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -864,6 +1320,9 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
cJSON_AddNumberToObject(root, "channel", channel.channel_index);
|
cJSON_AddNumberToObject(root, "channel", channel.channel_index);
|
||||||
cJSON_AddStringToObject(root, "name", channel.name.c_str());
|
cJSON_AddStringToObject(root, "name", channel.name.c_str());
|
||||||
cJSON_AddBoolToObject(root, "bridgeConfigLoaded", bridge_config_loaded);
|
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_AddNumberToObject(root, "modelCount", static_cast<double>(bridge_config.models.size()));
|
||||||
|
|
||||||
cJSON* modbus_json = cJSON_CreateObject();
|
cJSON* modbus_json = cJSON_CreateObject();
|
||||||
@@ -922,6 +1381,154 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
return GatewayBridgeHttpResponse{ESP_OK, BridgeRuntimeConfigToJson(bridge_config)};
|
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 {
|
GatewayBridgeHttpResponse modbusBindingsJson() const {
|
||||||
cJSON* root = cJSON_CreateObject();
|
cJSON* root = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
||||||
@@ -941,13 +1548,13 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
return JsonOk(root);
|
return JsonOk(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
GatewayBridgeHttpResponse bacnetBindingsJson() const {
|
GatewayBridgeHttpResponse bacnetBindingsJson() {
|
||||||
cJSON* root = cJSON_CreateObject();
|
cJSON* root = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
||||||
cJSON* bindings = cJSON_CreateArray();
|
cJSON* bindings = cJSON_CreateArray();
|
||||||
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||||
if (bindings != nullptr && bacnet != nullptr) {
|
if (bindings != nullptr && bacnet != nullptr) {
|
||||||
for (const auto& binding : bacnet->describeObjects()) {
|
for (const auto& binding : effectiveBacnetObjectsLocked()) {
|
||||||
cJSON* item = cJSON_CreateObject();
|
cJSON* item = cJSON_CreateObject();
|
||||||
if (item == nullptr) {
|
if (item == nullptr) {
|
||||||
continue;
|
continue;
|
||||||
@@ -956,6 +1563,36 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
cJSON_AddStringToObject(item, "objectType", bridgeObjectTypeToString(binding.objectType));
|
cJSON_AddStringToObject(item, "objectType", bridgeObjectTypeToString(binding.objectType));
|
||||||
cJSON_AddNumberToObject(item, "objectInstance", binding.objectInstance);
|
cJSON_AddNumberToObject(item, "objectInstance", binding.objectInstance);
|
||||||
cJSON_AddStringToObject(item, "property", binding.property.c_str());
|
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);
|
cJSON_AddItemToArray(bindings, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1226,7 +1863,7 @@ const GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
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) {
|
if (!config_.bridge_enabled) {
|
||||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "bridge service is disabled");
|
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "bridge service is disabled");
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1894,7 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|||||||
if (!gateway_id.has_value()) {
|
if (!gateway_id.has_value()) {
|
||||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required");
|
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) {
|
if (runtime == nullptr) {
|
||||||
return ErrorResponse(ESP_ERR_NOT_FOUND, "unknown gateway id");
|
return ErrorResponse(ESP_ERR_NOT_FOUND, "unknown gateway id");
|
||||||
}
|
}
|
||||||
@@ -1274,6 +1911,12 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|||||||
if (action == "bacnet") {
|
if (action == "bacnet") {
|
||||||
return runtime->bacnetBindingsJson();
|
return runtime->bacnetBindingsJson();
|
||||||
}
|
}
|
||||||
|
if (action == "inventory") {
|
||||||
|
return runtime->inventoryJson();
|
||||||
|
}
|
||||||
|
if (action == "effective_model") {
|
||||||
|
return runtime->effectiveModelJson();
|
||||||
|
}
|
||||||
if (action == "cloud") {
|
if (action == "cloud") {
|
||||||
return runtime->cloudJson();
|
return runtime->cloudJson();
|
||||||
}
|
}
|
||||||
@@ -1282,8 +1925,21 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|||||||
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
||||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
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") {
|
if (action == "dt4" || action == "dt5" || action == "dt6") {
|
||||||
const auto address = QueryInt(query, "addr", "address");
|
const auto address = QueryInt(query, "addr", "address");
|
||||||
@@ -1443,6 +2099,9 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
|||||||
}
|
}
|
||||||
return handleGet("bacnet", gateway_id.value());
|
return handleGet("bacnet", gateway_id.value());
|
||||||
}
|
}
|
||||||
|
if (action == "scan") {
|
||||||
|
return runtime->scanInventory(body);
|
||||||
|
}
|
||||||
|
|
||||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge POST action");
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge POST action");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user