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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user