e94945fc0f
Signed-off-by: Tony <tonylu@tony-cloud.com>
3969 lines
150 KiB
C++
3969 lines
150 KiB
C++
#include "gateway_bridge.hpp"
|
|
|
|
#include "gateway_bacnet_bridge.hpp"
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
#include "gateway_bacnet.hpp"
|
|
#endif
|
|
#include "bridge.hpp"
|
|
#include "bridge_model.hpp"
|
|
#include "bridge_provisioning.hpp"
|
|
#include "dali_comm.hpp"
|
|
#include "dali_define.hpp"
|
|
#include "dali_domain.hpp"
|
|
#include "gateway_cache.hpp"
|
|
#include "gateway_cloud.hpp"
|
|
#include "gateway_knx.hpp"
|
|
#include "gateway_modbus.hpp"
|
|
#include "gateway_provisioning.hpp"
|
|
|
|
#include "cJSON.h"
|
|
#include "driver/uart.h"
|
|
#include "esp_log.h"
|
|
#include "freertos/semphr.h"
|
|
#include "lwip/inet.h"
|
|
#include "lwip/sockets.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cctype>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string_view>
|
|
#include <sys/time.h>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace gateway {
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kTag = "gateway_bridge";
|
|
constexpr const char* kBridgeConfigKey = "bridge_cfg";
|
|
constexpr const char* kDiscoveryInventoryKey = "bridge_disc";
|
|
constexpr int kMaxDaliShortAddress = 63;
|
|
constexpr uint16_t kModbusUnknownRegister = 0xFFFF;
|
|
constexpr uint16_t kModbusDiscreteInputBase = 10001;
|
|
constexpr uint32_t kDiagnosticSnapshotCacheTtlMs = 500;
|
|
constexpr uint32_t kBacnetGeneratedBinaryInputBase = 1000000;
|
|
constexpr uint32_t kBacnetGeneratedBinaryInputChannelStride = 32768;
|
|
constexpr uint32_t kBacnetMaxObjectInstance = 4194303;
|
|
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
|
|
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
|
|
constexpr const char* kModbusManagementPrefix = "@DALIGW";
|
|
|
|
struct GatewayBridgeStoredConfig {
|
|
BridgeRuntimeConfig bridge;
|
|
std::optional<GatewayModbusConfig> modbus;
|
|
std::optional<GatewayKnxConfig> knx;
|
|
std::optional<GatewayBacnetBridgeConfig> bacnet_server;
|
|
};
|
|
|
|
struct BridgeDiscoveryEntry {
|
|
int short_address{0};
|
|
bool online{true};
|
|
DaliDomainSnapshot discovery;
|
|
};
|
|
|
|
using BridgeDiscoveryInventory = std::map<int, BridgeDiscoveryEntry>;
|
|
|
|
class LockGuard {
|
|
public:
|
|
explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) {
|
|
if (lock_ != nullptr) {
|
|
xSemaphoreTakeRecursive(lock_, portMAX_DELAY);
|
|
}
|
|
}
|
|
|
|
~LockGuard() {
|
|
if (lock_ != nullptr) {
|
|
xSemaphoreGiveRecursive(lock_);
|
|
}
|
|
}
|
|
|
|
private:
|
|
SemaphoreHandle_t lock_;
|
|
};
|
|
|
|
std::string PrintJson(cJSON* node) {
|
|
if (node == nullptr) {
|
|
return "{}";
|
|
}
|
|
char* rendered = cJSON_PrintUnformatted(node);
|
|
if (rendered == nullptr) {
|
|
return "{}";
|
|
}
|
|
std::string out(rendered);
|
|
cJSON_free(rendered);
|
|
return out;
|
|
}
|
|
|
|
GatewayBridgeHttpResponse JsonOk(cJSON* node) {
|
|
const std::string body = PrintJson(node);
|
|
cJSON_Delete(node);
|
|
return GatewayBridgeHttpResponse{ESP_OK, body};
|
|
}
|
|
|
|
GatewayBridgeHttpResponse ErrorResponse(esp_err_t err, const char* message) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (root != nullptr) {
|
|
cJSON_AddBoolToObject(root, "ok", false);
|
|
cJSON_AddStringToObject(root, "error", message == nullptr ? "error" : message);
|
|
cJSON_AddNumberToObject(root, "espErr", static_cast<double>(err));
|
|
}
|
|
const std::string body = PrintJson(root);
|
|
cJSON_Delete(root);
|
|
return GatewayBridgeHttpResponse{err, body};
|
|
}
|
|
|
|
const char* JsonString(const cJSON* parent, const char* name) {
|
|
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
|
|
return cJSON_IsString(item) && item->valuestring != nullptr ? item->valuestring : nullptr;
|
|
}
|
|
|
|
std::optional<int> JsonInt(const cJSON* parent, const char* name) {
|
|
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
|
|
if (!cJSON_IsNumber(item)) {
|
|
return std::nullopt;
|
|
}
|
|
return item->valueint;
|
|
}
|
|
|
|
std::optional<uint8_t> JsonGatewayId(const cJSON* root) {
|
|
if (root == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
const auto gateway = JsonInt(root, "gw").value_or(JsonInt(root, "gatewayId").value_or(-1));
|
|
if (gateway < 0 || gateway > 255) {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<uint8_t>(gateway);
|
|
}
|
|
|
|
std::string QueryValue(std::string_view query, std::string_view key) {
|
|
if (query.empty() || key.empty()) {
|
|
return {};
|
|
}
|
|
size_t start = 0;
|
|
while (start < query.size()) {
|
|
const size_t end = query.find('&', start);
|
|
const size_t segment_end = end == std::string_view::npos ? query.size() : end;
|
|
const std::string_view segment = query.substr(start, segment_end - start);
|
|
const size_t equals = segment.find('=');
|
|
if (equals != std::string_view::npos && segment.substr(0, equals) == key) {
|
|
return std::string(segment.substr(equals + 1));
|
|
}
|
|
if (end == std::string_view::npos) {
|
|
break;
|
|
}
|
|
start = end + 1;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::optional<int> ParseInt(std::string_view raw) {
|
|
if (raw.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
std::string text(raw);
|
|
char* end = nullptr;
|
|
const long parsed = std::strtol(text.c_str(), &end, 10);
|
|
if (end == text.c_str() || *end != '\0') {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<int>(parsed);
|
|
}
|
|
|
|
std::optional<int> QueryInt(std::string_view query, std::string_view primary,
|
|
std::string_view fallback = {}) {
|
|
auto value = ParseInt(QueryValue(query, primary));
|
|
if (value.has_value() || fallback.empty()) {
|
|
return value;
|
|
}
|
|
return ParseInt(QueryValue(query, fallback));
|
|
}
|
|
|
|
std::optional<int> JsonIntAny(const cJSON* parent, const char* primary, const char* fallback) {
|
|
auto value = JsonInt(parent, primary);
|
|
if (value.has_value() || fallback == nullptr) {
|
|
return value;
|
|
}
|
|
return JsonInt(parent, fallback);
|
|
}
|
|
|
|
bool ValidDaliAddress(int address) {
|
|
return address >= 0 && address <= 127;
|
|
}
|
|
|
|
bool ValidShortAddress(int address) {
|
|
return address >= 0 && address <= kMaxDaliShortAddress;
|
|
}
|
|
|
|
uint8_t RawArcAddressFromDec(int dec_address) {
|
|
if (dec_address >= 0 && dec_address < 64) {
|
|
return static_cast<uint8_t>(dec_address * 2);
|
|
}
|
|
if (dec_address >= 64 && dec_address < 80) {
|
|
return static_cast<uint8_t>(0x80 + (dec_address - 64) * 2);
|
|
}
|
|
return 0xfe;
|
|
}
|
|
|
|
uint8_t RawCommandAddressFromDec(int dec_address) {
|
|
if (dec_address >= 0 && dec_address < 64) {
|
|
return static_cast<uint8_t>(dec_address * 2 + 1);
|
|
}
|
|
if (dec_address >= 64 && dec_address < 80) {
|
|
return static_cast<uint8_t>(0x80 + (dec_address - 64) * 2 + 1);
|
|
}
|
|
return 0xff;
|
|
}
|
|
|
|
uint16_t DeviceTypeMask(const DaliDomainSnapshot& snapshot) {
|
|
uint16_t mask = 0;
|
|
const auto types = snapshot.int_arrays.find("types");
|
|
if (types != snapshot.int_arrays.end()) {
|
|
for (const int type : types->second) {
|
|
if (type >= 0 && type < 16) {
|
|
mask |= static_cast<uint16_t>(1U << type);
|
|
}
|
|
}
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
std::optional<bool> SnapshotBoolValue(const DaliDomainSnapshot& snapshot,
|
|
std::string_view key) {
|
|
for (const auto& entry : snapshot.bools) {
|
|
if (std::string_view(entry.first) == key) {
|
|
return entry.second;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<int> SnapshotIntValue(const DaliDomainSnapshot& snapshot,
|
|
const std::string& key) {
|
|
const auto found = snapshot.ints.find(key);
|
|
return found == snapshot.ints.end() ? std::nullopt : std::optional<int>(found->second);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
bool BridgeOperationReadable(BridgeOperation operation) {
|
|
switch (operation) {
|
|
case BridgeOperation::query:
|
|
case BridgeOperation::getBrightness:
|
|
case BridgeOperation::getStatus:
|
|
case BridgeOperation::getColorTemperature:
|
|
case BridgeOperation::getColorStatus:
|
|
case BridgeOperation::getEmergencyLevel:
|
|
case BridgeOperation::getEmergencyStatus:
|
|
case BridgeOperation::getEmergencyFailureStatus:
|
|
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) {
|
|
return nullptr;
|
|
}
|
|
for (const int value : values) {
|
|
cJSON_AddItemToArray(array, cJSON_CreateNumber(value));
|
|
}
|
|
return array;
|
|
}
|
|
|
|
cJSON* NumberArrayToCjson(const std::vector<double>& values) {
|
|
cJSON* array = cJSON_CreateArray();
|
|
if (array == nullptr) {
|
|
return nullptr;
|
|
}
|
|
for (const double value : values) {
|
|
cJSON_AddItemToArray(array, cJSON_CreateNumber(value));
|
|
}
|
|
return array;
|
|
}
|
|
|
|
cJSON* SnapshotToCjson(const DaliDomainSnapshot& snapshot) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (root == nullptr) {
|
|
return nullptr;
|
|
}
|
|
cJSON_AddNumberToObject(root, "gatewayId", snapshot.gateway_id);
|
|
cJSON_AddNumberToObject(root, "address", snapshot.address);
|
|
cJSON_AddStringToObject(root, "kind", snapshot.kind.c_str());
|
|
for (const auto& entry : snapshot.bools) {
|
|
cJSON_AddBoolToObject(root, entry.first.c_str(), entry.second);
|
|
}
|
|
for (const auto& entry : snapshot.ints) {
|
|
cJSON_AddNumberToObject(root, entry.first.c_str(), entry.second);
|
|
}
|
|
for (const auto& entry : snapshot.numbers) {
|
|
cJSON_AddNumberToObject(root, entry.first.c_str(), entry.second);
|
|
}
|
|
for (const auto& entry : snapshot.int_arrays) {
|
|
cJSON_AddItemToObject(root, entry.first.c_str(), IntArrayToCjson(entry.second));
|
|
}
|
|
for (const auto& entry : snapshot.number_arrays) {
|
|
cJSON_AddItemToObject(root, entry.first.c_str(), NumberArrayToCjson(entry.second));
|
|
}
|
|
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()) {
|
|
return ErrorResponse(ESP_ERR_NOT_FOUND, missing_message);
|
|
}
|
|
return JsonOk(SnapshotToCjson(snapshot.value()));
|
|
}
|
|
|
|
GatewayBridgeHttpResponse StoredSnapshotResponse(
|
|
const std::optional<DaliDomainSnapshot>& snapshot, uint8_t gateway_id, int address,
|
|
const char* kind) {
|
|
if (snapshot.has_value()) {
|
|
return JsonOk(SnapshotToCjson(snapshot.value()));
|
|
}
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(root, "ok", true);
|
|
cJSON_AddBoolToObject(root, "stored", true);
|
|
cJSON_AddBoolToObject(root, "reportAvailable", false);
|
|
cJSON_AddNumberToObject(root, "gatewayId", gateway_id);
|
|
cJSON_AddNumberToObject(root, "address", address);
|
|
cJSON_AddStringToObject(root, "kind", kind == nullptr ? "dt8_snapshot" : kind);
|
|
return JsonOk(root);
|
|
}
|
|
|
|
DaliDt8SceneColorMode JsonColorMode(const cJSON* root) {
|
|
const cJSON* item = cJSON_GetObjectItemCaseSensitive(root, "colorMode");
|
|
if (item == nullptr) {
|
|
item = cJSON_GetObjectItemCaseSensitive(root, "color_mode");
|
|
}
|
|
if (cJSON_IsNumber(item)) {
|
|
if (item->valueint == 1) {
|
|
return DaliDt8SceneColorMode::kColorTemperature;
|
|
}
|
|
if (item->valueint == 2) {
|
|
return DaliDt8SceneColorMode::kRgb;
|
|
}
|
|
return DaliDt8SceneColorMode::kDisabled;
|
|
}
|
|
if (!cJSON_IsString(item) || item->valuestring == nullptr) {
|
|
return DaliDt8SceneColorMode::kDisabled;
|
|
}
|
|
const std::string_view mode(item->valuestring);
|
|
if (mode == "colorTemperature" || mode == "color_temperature" || mode == "ct") {
|
|
return DaliDt8SceneColorMode::kColorTemperature;
|
|
}
|
|
if (mode == "rgb") {
|
|
return DaliDt8SceneColorMode::kRgb;
|
|
}
|
|
return DaliDt8SceneColorMode::kDisabled;
|
|
}
|
|
|
|
GatewayBridgeHttpResponse StoreDt8SceneSnapshot(DaliDomainService& domain, uint8_t gateway_id,
|
|
std::string_view body) {
|
|
cJSON* root = cJSON_ParseWithLength(body.data(), body.size());
|
|
if (root == nullptr) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid DT8 scene snapshot JSON");
|
|
}
|
|
const auto address = JsonIntAny(root, "addr", "address");
|
|
const auto scene = JsonInt(root, "scene");
|
|
const auto brightness = JsonInt(root, "brightness");
|
|
const auto color_mode = JsonColorMode(root);
|
|
const int color_temperature = JsonIntAny(root, "colorTemperature", "color_temperature").value_or(0);
|
|
const int red = JsonIntAny(root, "red", "r").value_or(0);
|
|
const int green = JsonIntAny(root, "green", "g").value_or(0);
|
|
const int blue = JsonIntAny(root, "blue", "b").value_or(0);
|
|
cJSON_Delete(root);
|
|
|
|
if (!address.has_value() || !scene.has_value() || !brightness.has_value() ||
|
|
!ValidDaliAddress(address.value()) || scene.value() < 0 || scene.value() > 15) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "addr, scene, and brightness are required");
|
|
}
|
|
if (!domain.storeDt8SceneSnapshot(gateway_id, address.value(), scene.value(),
|
|
brightness.value(), color_mode, color_temperature, red,
|
|
green, blue)) {
|
|
return ErrorResponse(ESP_FAIL, "failed to store DT8 scene snapshot");
|
|
}
|
|
return StoredSnapshotResponse(domain.dt8SceneColorReport(gateway_id, address.value(),
|
|
scene.value()),
|
|
gateway_id, address.value(), "dt8_scene");
|
|
}
|
|
|
|
GatewayBridgeHttpResponse StoreDt8LevelSnapshot(DaliDomainService& domain, uint8_t gateway_id,
|
|
std::string_view body, bool power_on) {
|
|
cJSON* root = cJSON_ParseWithLength(body.data(), body.size());
|
|
if (root == nullptr) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid DT8 level snapshot JSON");
|
|
}
|
|
const auto address = JsonIntAny(root, "addr", "address");
|
|
const auto level = JsonInt(root, "level");
|
|
cJSON_Delete(root);
|
|
|
|
if (!address.has_value() || !level.has_value() || !ValidDaliAddress(address.value())) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "addr and level are required");
|
|
}
|
|
|
|
const bool stored = power_on
|
|
? domain.storeDt8PowerOnLevelSnapshot(gateway_id, address.value(),
|
|
level.value())
|
|
: domain.storeDt8SystemFailureLevelSnapshot(gateway_id, address.value(),
|
|
level.value());
|
|
if (!stored) {
|
|
return ErrorResponse(ESP_FAIL, power_on ? "failed to store DT8 power-on snapshot"
|
|
: "failed to store DT8 system-failure snapshot");
|
|
}
|
|
return StoredSnapshotResponse(power_on ? domain.dt8PowerOnLevelColorReport(gateway_id,
|
|
address.value())
|
|
: domain.dt8SystemFailureLevelColorReport(
|
|
gateway_id, address.value()),
|
|
gateway_id, address.value(),
|
|
power_on ? "dt8_power_on" : "dt8_system_failure");
|
|
}
|
|
|
|
DaliValue FromCjson(const cJSON* item) {
|
|
if (item == nullptr || cJSON_IsNull(item)) {
|
|
return DaliValue();
|
|
}
|
|
if (cJSON_IsBool(item)) {
|
|
return DaliValue(cJSON_IsTrue(item));
|
|
}
|
|
if (cJSON_IsNumber(item)) {
|
|
const double value = item->valuedouble;
|
|
if (value == static_cast<double>(item->valueint)) {
|
|
return DaliValue(item->valueint);
|
|
}
|
|
return DaliValue(value);
|
|
}
|
|
if (cJSON_IsString(item) && item->valuestring != nullptr) {
|
|
return DaliValue(std::string(item->valuestring));
|
|
}
|
|
if (cJSON_IsArray(item)) {
|
|
DaliValue::Array out;
|
|
for (const cJSON* child = item->child; child != nullptr; child = child->next) {
|
|
out.push_back(FromCjson(child));
|
|
}
|
|
return DaliValue(std::move(out));
|
|
}
|
|
if (cJSON_IsObject(item)) {
|
|
DaliValue::Object out;
|
|
for (const cJSON* child = item->child; child != nullptr; child = child->next) {
|
|
if (child->string != nullptr) {
|
|
out[child->string] = FromCjson(child);
|
|
}
|
|
}
|
|
return DaliValue(std::move(out));
|
|
}
|
|
return DaliValue();
|
|
}
|
|
|
|
cJSON* ToCjson(const DaliValue& value) {
|
|
if (value.isNull()) {
|
|
return cJSON_CreateNull();
|
|
}
|
|
if (value.isBool()) {
|
|
return cJSON_CreateBool(value.asBool().value_or(false));
|
|
}
|
|
if (value.isInt()) {
|
|
return cJSON_CreateNumber(value.asInt().value_or(0));
|
|
}
|
|
if (value.isDouble()) {
|
|
return cJSON_CreateNumber(value.asDouble().value_or(0.0));
|
|
}
|
|
if (value.isString()) {
|
|
return cJSON_CreateString(value.asString().value_or("").c_str());
|
|
}
|
|
if (const auto* array = value.asArray()) {
|
|
cJSON* out = cJSON_CreateArray();
|
|
for (const auto& item : *array) {
|
|
cJSON_AddItemToArray(out, ToCjson(item));
|
|
}
|
|
return out;
|
|
}
|
|
if (const auto* object = value.asObject()) {
|
|
cJSON* out = cJSON_CreateObject();
|
|
for (const auto& entry : *object) {
|
|
cJSON_AddItemToObject(out, entry.first.c_str(), ToCjson(entry.second));
|
|
}
|
|
return out;
|
|
}
|
|
return cJSON_CreateNull();
|
|
}
|
|
|
|
DaliValue::Object GatewayBridgeStoredConfigToValue(
|
|
const BridgeRuntimeConfig& bridge_config,
|
|
const std::optional<GatewayModbusConfig>& modbus_config,
|
|
const std::optional<GatewayKnxConfig>& knx_config,
|
|
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
|
|
DaliValue::Object out = bridge_config.toJson();
|
|
if (modbus_config.has_value()) {
|
|
out["modbus"] = GatewayModbusConfigToValue(modbus_config.value());
|
|
}
|
|
if (knx_config.has_value()) {
|
|
out["knx"] = GatewayKnxConfigToValue(knx_config.value());
|
|
}
|
|
if (bacnet_server_config.has_value()) {
|
|
DaliValue::Object bacnet;
|
|
bacnet["deviceInstance"] = static_cast<int64_t>(bacnet_server_config->deviceInstance);
|
|
bacnet["localAddress"] = bacnet_server_config->localAddress;
|
|
bacnet["udpPort"] = static_cast<int>(bacnet_server_config->udpPort);
|
|
out["bacnetServer"] = std::move(bacnet);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string GatewayBridgeStoredConfigToJson(
|
|
const BridgeRuntimeConfig& bridge_config,
|
|
const std::optional<GatewayModbusConfig>& modbus_config,
|
|
const std::optional<GatewayKnxConfig>& knx_config,
|
|
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
|
|
cJSON* root = ToCjson(DaliValue(GatewayBridgeStoredConfigToValue(
|
|
bridge_config, modbus_config, knx_config, bacnet_server_config)));
|
|
const std::string body = PrintJson(root);
|
|
cJSON_Delete(root);
|
|
return body;
|
|
}
|
|
|
|
std::optional<GatewayBacnetBridgeConfig> GatewayBacnetBridgeConfigFromValue(
|
|
const DaliValue* value) {
|
|
if (value == nullptr || value->asObject() == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
const auto& json = *value->asObject();
|
|
GatewayBacnetBridgeConfig config;
|
|
config.deviceInstance = static_cast<uint32_t>(
|
|
getObjectInt(json, "deviceInstance").value_or(4194303));
|
|
config.localAddress = getObjectString(json, "localAddress").value_or("");
|
|
config.udpPort = static_cast<uint16_t>(getObjectInt(json, "udpPort").value_or(47808));
|
|
return config;
|
|
}
|
|
|
|
GatewayBridgeStoredConfig GatewayBridgeStoredConfigFromValue(const DaliValue::Object& object) {
|
|
GatewayBridgeStoredConfig config;
|
|
config.bridge = BridgeRuntimeConfig::fromJson(object);
|
|
config.modbus = GatewayModbusConfigFromValue(getObjectValue(object, "modbus"));
|
|
config.knx = GatewayKnxConfigFromValue(getObjectValue(object, "knx"));
|
|
config.bacnet_server = GatewayBacnetBridgeConfigFromValue(
|
|
getObjectValue(object, "bacnetServer"));
|
|
return config;
|
|
}
|
|
|
|
std::optional<GatewayBridgeStoredConfig> GatewayBridgeStoredConfigFromJson(std::string_view json) {
|
|
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
|
|
if (root == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
const DaliValue value = FromCjson(root);
|
|
cJSON_Delete(root);
|
|
const auto* object = value.asObject();
|
|
if (object == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
return GatewayBridgeStoredConfigFromValue(*object);
|
|
}
|
|
|
|
GatewayCloudConfig GatewayCloudConfigFromJson(cJSON* root) {
|
|
GatewayCloudConfig config;
|
|
if (const char* value = JsonString(root, "brokerURI")) {
|
|
config.brokerURI = value;
|
|
}
|
|
if (const char* value = JsonString(root, "deviceID")) {
|
|
config.deviceID = value;
|
|
}
|
|
if (const char* value = JsonString(root, "username")) {
|
|
config.username = value;
|
|
}
|
|
if (const char* value = JsonString(root, "password")) {
|
|
config.password = value;
|
|
}
|
|
if (const char* value = JsonString(root, "topicPrefix")) {
|
|
config.topicPrefix = value;
|
|
}
|
|
if (const auto qos = JsonInt(root, "qos")) {
|
|
config.qos = qos.value();
|
|
}
|
|
return config;
|
|
}
|
|
|
|
cJSON* GatewayCloudConfigToCjson(const GatewayCloudConfig& config) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (root == nullptr) {
|
|
return nullptr;
|
|
}
|
|
cJSON_AddStringToObject(root, "brokerURI", config.brokerURI.c_str());
|
|
cJSON_AddStringToObject(root, "deviceID", config.deviceID.c_str());
|
|
cJSON_AddStringToObject(root, "username", config.username.c_str());
|
|
cJSON_AddStringToObject(root, "password", config.password.c_str());
|
|
cJSON_AddStringToObject(root, "topicPrefix", config.topicPrefix.c_str());
|
|
cJSON_AddNumberToObject(root, "qos", config.qos);
|
|
return root;
|
|
}
|
|
|
|
bool IsKnownBridgeRequestKey(const char* key) {
|
|
if (key == nullptr) return true;
|
|
static const char* known[] = {"type", "seq", "sequence", "model",
|
|
"modelID", "modelId", "op", "operation",
|
|
"addr", "rawAddress", "cmd", "rawCommand",
|
|
"shortAddress", "short_address", "value", "meta"};
|
|
for (const char* item : known) {
|
|
if (std::strcmp(key, item) == 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
DaliBridgeRequest BridgeRequestFromJson(cJSON* root) {
|
|
DaliBridgeRequest request;
|
|
if (const char* seq = JsonString(root, "seq")) {
|
|
request.sequence = seq;
|
|
}
|
|
if (request.sequence.empty()) {
|
|
if (const char* seq = JsonString(root, "sequence")) {
|
|
request.sequence = seq;
|
|
}
|
|
}
|
|
if (const char* model = JsonString(root, "model")) {
|
|
request.modelID = model;
|
|
}
|
|
if (request.modelID.empty()) {
|
|
if (const char* model = JsonString(root, "modelID")) {
|
|
request.modelID = model;
|
|
}
|
|
}
|
|
if (const char* op = JsonString(root, "op")) {
|
|
request.operation = bridgeOperationFromString(op);
|
|
}
|
|
if (!request.operation.has_value()) {
|
|
if (const char* op = JsonString(root, "operation")) {
|
|
request.operation = bridgeOperationFromString(op);
|
|
}
|
|
}
|
|
if (const auto addr = JsonInt(root, "addr")) {
|
|
request.rawAddress = addr.value();
|
|
}
|
|
if (!request.rawAddress.has_value()) {
|
|
if (const auto addr = JsonInt(root, "rawAddress")) {
|
|
request.rawAddress = addr.value();
|
|
}
|
|
}
|
|
if (const auto cmd = JsonInt(root, "cmd")) {
|
|
request.rawCommand = cmd.value();
|
|
}
|
|
if (!request.rawCommand.has_value()) {
|
|
if (const auto cmd = JsonInt(root, "rawCommand")) {
|
|
request.rawCommand = cmd.value();
|
|
}
|
|
}
|
|
if (const auto short_address = JsonInt(root, "shortAddress")) {
|
|
request.shortAddress = short_address.value();
|
|
}
|
|
if (!request.shortAddress.has_value()) {
|
|
if (const auto short_address = JsonInt(root, "short_address")) {
|
|
request.shortAddress = short_address.value();
|
|
}
|
|
}
|
|
if (const cJSON* value = cJSON_GetObjectItemCaseSensitive(root, "value")) {
|
|
request.value = FromCjson(value);
|
|
}
|
|
if (const cJSON* meta = cJSON_GetObjectItemCaseSensitive(root, "meta")) {
|
|
const auto meta_value = FromCjson(meta);
|
|
if (const auto* object = meta_value.asObject()) {
|
|
request.metadata = *object;
|
|
}
|
|
}
|
|
for (const cJSON* child = root != nullptr ? root->child : nullptr; child != nullptr;
|
|
child = child->next) {
|
|
if (!IsKnownBridgeRequestKey(child->string)) {
|
|
request.metadata[child->string] = FromCjson(child);
|
|
}
|
|
}
|
|
return request;
|
|
}
|
|
|
|
cJSON* BridgeResultToCjson(const DaliBridgeResult& result) {
|
|
return ToCjson(DaliValue(result.toJson()));
|
|
}
|
|
|
|
uint16_t ReadBe16(const uint8_t* data) {
|
|
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
|
|
}
|
|
|
|
void WriteBe16(uint8_t* data, uint16_t value) {
|
|
data[0] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
|
data[1] = static_cast<uint8_t>(value & 0xFF);
|
|
}
|
|
|
|
bool RecvAll(int sock, uint8_t* buffer, size_t len) {
|
|
size_t received = 0;
|
|
while (received < len) {
|
|
const int ret = recv(sock, buffer + received, len - received, 0);
|
|
if (ret <= 0) {
|
|
return false;
|
|
}
|
|
received += static_cast<size_t>(ret);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SendAll(int sock, const uint8_t* buffer, size_t len) {
|
|
size_t sent = 0;
|
|
while (sent < len) {
|
|
const int ret = send(sock, buffer + sent, len - sent, 0);
|
|
if (ret <= 0) {
|
|
return false;
|
|
}
|
|
sent += static_cast<size_t>(ret);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SendModbusFrame(int sock, const uint8_t* mbap, const std::vector<uint8_t>& pdu) {
|
|
std::vector<uint8_t> frame(7 + pdu.size());
|
|
std::memcpy(frame.data(), mbap, 7);
|
|
WriteBe16(&frame[4], static_cast<uint16_t>(pdu.size() + 1));
|
|
std::memcpy(frame.data() + 7, pdu.data(), pdu.size());
|
|
return SendAll(sock, frame.data(), frame.size());
|
|
}
|
|
|
|
std::vector<uint8_t> ModbusExceptionPdu(uint8_t function_code, uint8_t exception_code) {
|
|
return {static_cast<uint8_t>(function_code | 0x80), exception_code};
|
|
}
|
|
|
|
uint16_t ModbusCrc16(const uint8_t* data, size_t len) {
|
|
uint16_t crc = 0xFFFF;
|
|
for (size_t i = 0; i < len; ++i) {
|
|
crc ^= data[i];
|
|
for (int bit = 0; bit < 8; ++bit) {
|
|
if ((crc & 0x0001) != 0) {
|
|
crc = static_cast<uint16_t>((crc >> 1) ^ 0xA001);
|
|
} else {
|
|
crc = static_cast<uint16_t>(crc >> 1);
|
|
}
|
|
}
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
uint8_t ModbusAsciiLrc(const uint8_t* data, size_t len) {
|
|
uint8_t sum = 0;
|
|
for (size_t i = 0; i < len; ++i) {
|
|
sum = static_cast<uint8_t>(sum + data[i]);
|
|
}
|
|
return static_cast<uint8_t>(-sum);
|
|
}
|
|
|
|
std::optional<uint8_t> HexNibble(char ch) {
|
|
if (ch >= '0' && ch <= '9') {
|
|
return static_cast<uint8_t>(ch - '0');
|
|
}
|
|
if (ch >= 'A' && ch <= 'F') {
|
|
return static_cast<uint8_t>(ch - 'A' + 10);
|
|
}
|
|
if (ch >= 'a' && ch <= 'f') {
|
|
return static_cast<uint8_t>(ch - 'a' + 10);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<std::vector<uint8_t>> DecodeModbusAsciiLine(std::string_view line) {
|
|
while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) {
|
|
line.remove_suffix(1);
|
|
}
|
|
if (line.size() < 7 || line.front() != ':' || ((line.size() - 1) % 2) != 0) {
|
|
return std::nullopt;
|
|
}
|
|
std::vector<uint8_t> bytes;
|
|
bytes.reserve((line.size() - 1) / 2);
|
|
for (size_t i = 1; i + 1 < line.size(); i += 2) {
|
|
const auto high = HexNibble(line[i]);
|
|
const auto low = HexNibble(line[i + 1]);
|
|
if (!high.has_value() || !low.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
bytes.push_back(static_cast<uint8_t>((high.value() << 4) | low.value()));
|
|
}
|
|
uint8_t sum = 0;
|
|
for (const auto byte : bytes) {
|
|
sum = static_cast<uint8_t>(sum + byte);
|
|
}
|
|
if (sum != 0) {
|
|
return std::nullopt;
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
std::string EncodeModbusAsciiLine(const std::vector<uint8_t>& bytes) {
|
|
constexpr char kHex[] = "0123456789ABCDEF";
|
|
std::string out;
|
|
out.reserve(1 + bytes.size() * 2 + 2);
|
|
out.push_back(':');
|
|
for (const auto byte : bytes) {
|
|
out.push_back(kHex[(byte >> 4) & 0x0F]);
|
|
out.push_back(kHex[byte & 0x0F]);
|
|
}
|
|
out.append("\r\n");
|
|
return out;
|
|
}
|
|
|
|
bool LineStartsWith(std::string_view line, std::string_view prefix) {
|
|
return line.size() >= prefix.size() && line.substr(0, prefix.size()) == prefix;
|
|
}
|
|
|
|
uart_word_length_t UartWordLength(int bits) {
|
|
return bits <= 7 ? UART_DATA_7_BITS : UART_DATA_8_BITS;
|
|
}
|
|
|
|
uart_parity_t UartParity(const std::string& parity) {
|
|
if (parity == "even") {
|
|
return UART_PARITY_EVEN;
|
|
}
|
|
if (parity == "odd") {
|
|
return UART_PARITY_ODD;
|
|
}
|
|
return UART_PARITY_DISABLE;
|
|
}
|
|
|
|
uart_stop_bits_t UartStopBits(int bits) {
|
|
return bits >= 2 ? UART_STOP_BITS_2 : UART_STOP_BITS_1;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct GatewayBridgeService::ChannelRuntime {
|
|
explicit ChannelRuntime(DaliDomainService& domain, GatewayCache& cache, DaliChannelInfo channel,
|
|
GatewayBridgeServiceConfig service_config)
|
|
: domain(domain), cache(cache), channel(std::move(channel)), service_config(service_config),
|
|
lock(xSemaphoreCreateRecursiveMutex()) {}
|
|
|
|
~ChannelRuntime() {
|
|
if (cloud != nullptr) {
|
|
cloud->stop();
|
|
}
|
|
if (lock != nullptr) {
|
|
vSemaphoreDelete(lock);
|
|
lock = nullptr;
|
|
}
|
|
}
|
|
|
|
DaliDomainService& domain;
|
|
GatewayCache& cache;
|
|
DaliChannelInfo channel;
|
|
GatewayBridgeServiceConfig service_config;
|
|
SemaphoreHandle_t lock{nullptr};
|
|
std::unique_ptr<DaliComm> comm;
|
|
std::unique_ptr<DaliBridgeEngine> engine;
|
|
std::unique_ptr<GatewayModbusBridge> modbus;
|
|
std::unique_ptr<GatewayKnxBridge> knx;
|
|
std::unique_ptr<GatewayKnxTpIpRouter> knx_router;
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
std::unique_ptr<GatewayBacnetBridgeAdapter> bacnet;
|
|
#endif
|
|
std::unique_ptr<DaliCloudBridge> cloud;
|
|
BridgeRuntimeConfig bridge_config;
|
|
std::optional<GatewayModbusConfig> modbus_config;
|
|
std::optional<GatewayKnxConfig> knx_config;
|
|
std::optional<GatewayBacnetBridgeConfig> bacnet_server_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};
|
|
bool knx_started{false};
|
|
bool bacnet_started{false};
|
|
TaskHandle_t modbus_task_handle{nullptr};
|
|
std::atomic_bool modbus_stop_requested{false};
|
|
std::atomic_bool modbus_restart_requested{false};
|
|
int modbus_listen_sock{-1};
|
|
int modbus_client_sock{-1};
|
|
int modbus_uart_port{-1};
|
|
std::string modbus_last_error;
|
|
std::string knx_last_error;
|
|
|
|
struct DiagnosticSnapshotCacheEntry {
|
|
DaliDomainSnapshot snapshot;
|
|
TickType_t captured_ticks{0};
|
|
};
|
|
std::map<std::string, DiagnosticSnapshotCacheEntry> diagnostic_snapshot_cache;
|
|
|
|
static void ModbusTaskEntry(void* arg) {
|
|
static_cast<ChannelRuntime*>(arg)->modbusTaskLoop();
|
|
}
|
|
|
|
std::string bridgeNamespace() const {
|
|
return "dali_bridge_" + std::to_string(channel.gateway_id);
|
|
}
|
|
|
|
std::string cloudNamespace() const {
|
|
return "dali_cloud_" + std::to_string(channel.gateway_id);
|
|
}
|
|
|
|
esp_err_t start() {
|
|
comm = std::make_unique<DaliComm>(
|
|
[this](const uint8_t* data, size_t len) {
|
|
return domain.writeBridgeFrame(channel.gateway_id, data, len);
|
|
},
|
|
nullptr,
|
|
[this](const uint8_t* data, size_t len) {
|
|
return domain.transactBridgeFrame(channel.gateway_id, data, len);
|
|
},
|
|
[](uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); });
|
|
|
|
BridgeProvisioningStore bridge_store(bridgeNamespace());
|
|
DaliValue::Object bridge_object;
|
|
if (bridge_store.loadObject(kBridgeConfigKey, &bridge_object) == ESP_OK) {
|
|
const auto stored_config = GatewayBridgeStoredConfigFromValue(bridge_object);
|
|
bridge_config = stored_config.bridge;
|
|
modbus_config = stored_config.modbus;
|
|
knx_config = stored_config.knx;
|
|
bacnet_server_config = stored_config.bacnet_server;
|
|
bridge_config_loaded = true;
|
|
}
|
|
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());
|
|
GatewayCloudConfig loaded_cloud;
|
|
if (cloud_store.load(&loaded_cloud) == ESP_OK) {
|
|
cloud_config = loaded_cloud;
|
|
cloud_config_loaded = true;
|
|
}
|
|
applyCloudModelsLocked();
|
|
|
|
if (service_config.cloud_enabled && service_config.cloud_startup_enabled) {
|
|
startCloudLocked();
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
void applyBridgeConfigLocked() {
|
|
engine = std::make_unique<DaliBridgeEngine>(*comm);
|
|
for (const auto& model : bridge_config.models) {
|
|
engine->upsertModel(model);
|
|
}
|
|
|
|
modbus = std::make_unique<GatewayModbusBridge>(*engine);
|
|
if (modbus_config.has_value()) {
|
|
modbus->setConfig(modbus_config.value());
|
|
}
|
|
|
|
knx = std::make_unique<GatewayKnxBridge>(*engine);
|
|
knx_router = std::make_unique<GatewayKnxTpIpRouter>(
|
|
*knx, [this](const uint8_t* data, size_t len) {
|
|
LockGuard guard(lock);
|
|
if (knx == nullptr) {
|
|
DaliBridgeResult result;
|
|
result.error = "KNX bridge is not ready";
|
|
return result;
|
|
}
|
|
return knx->handleCemiFrame(data, len);
|
|
});
|
|
if (knx_config.has_value()) {
|
|
knx->setConfig(knx_config.value());
|
|
knx_router->setConfig(knx_config.value());
|
|
}
|
|
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
if (service_config.bacnet_enabled) {
|
|
bacnet = std::make_unique<GatewayBacnetBridgeAdapter>(*engine);
|
|
if (bacnet_server_config.has_value()) {
|
|
bacnet->setConfig(bacnet_server_config.value());
|
|
}
|
|
} else {
|
|
bacnet.reset();
|
|
}
|
|
#endif
|
|
|
|
applyCloudModelsLocked();
|
|
knx_started = false;
|
|
bacnet_started = false;
|
|
diagnostic_snapshot_cache.clear();
|
|
}
|
|
|
|
std::optional<DaliDomainSnapshot> diagnosticSnapshotLocked(int short_address,
|
|
std::string_view kind) {
|
|
if (!ValidShortAddress(short_address) || kind.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
std::string key(kind.data(), kind.size());
|
|
key += ":";
|
|
key += std::to_string(short_address);
|
|
const TickType_t now = xTaskGetTickCount();
|
|
const auto cached = diagnostic_snapshot_cache.find(key);
|
|
if (cached != diagnostic_snapshot_cache.end() &&
|
|
(now - cached->second.captured_ticks) <= pdMS_TO_TICKS(kDiagnosticSnapshotCacheTtlMs)) {
|
|
return cached->second.snapshot;
|
|
}
|
|
|
|
std::optional<DaliDomainSnapshot> snapshot;
|
|
if (kind == "base_status") {
|
|
snapshot = domain.baseStatusSnapshot(channel.gateway_id, short_address);
|
|
} else if (kind == "dt1") {
|
|
snapshot = domain.dt1Snapshot(channel.gateway_id, short_address);
|
|
} else if (kind == "dt4") {
|
|
snapshot = domain.dt4Snapshot(channel.gateway_id, short_address);
|
|
} else if (kind == "dt5") {
|
|
snapshot = domain.dt5Snapshot(channel.gateway_id, short_address);
|
|
} else if (kind == "dt6") {
|
|
snapshot = domain.dt6Snapshot(channel.gateway_id, short_address);
|
|
} else if (kind == "dt8_status") {
|
|
snapshot = domain.dt8StatusSnapshot(channel.gateway_id, short_address);
|
|
}
|
|
|
|
if (snapshot.has_value()) {
|
|
diagnostic_snapshot_cache[key] = DiagnosticSnapshotCacheEntry{snapshot.value(), now};
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
std::optional<bool> readSnapshotBoolLocked(int short_address, std::string_view kind,
|
|
std::string_view bool_key) {
|
|
const auto snapshot = diagnosticSnapshotLocked(short_address, kind);
|
|
if (!snapshot.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
return SnapshotBoolValue(snapshot.value(), bool_key);
|
|
}
|
|
|
|
std::optional<bool> readDiagnosticBoolPointLocked(const GatewayModbusPoint& point) {
|
|
if (point.diagnostic_device_type > 0) {
|
|
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
|
if (discovery == nullptr ||
|
|
!SnapshotHasDeviceType(discovery->discovery, point.diagnostic_device_type)) {
|
|
return false;
|
|
}
|
|
}
|
|
return readSnapshotBoolLocked(point.short_address, point.diagnostic_snapshot,
|
|
point.diagnostic_bool);
|
|
}
|
|
|
|
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);
|
|
},
|
|
[this](BridgeObjectType object_type, uint32_t object_instance,
|
|
const std::string& property) -> std::optional<DaliValue> {
|
|
return readBacnetValue(object_type, object_instance, property);
|
|
});
|
|
#else
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
#endif
|
|
}
|
|
|
|
void applyCloudModelsLocked() {
|
|
if (cloud_started && cloud != nullptr) {
|
|
cloud->stop();
|
|
cloud_started = false;
|
|
}
|
|
cloud = std::make_unique<DaliCloudBridge>(*comm);
|
|
for (const auto& model : bridge_config.models) {
|
|
cloud->bridge().upsertModel(model);
|
|
}
|
|
}
|
|
|
|
esp_err_t saveBridgeConfig(std::string_view json) {
|
|
auto parsed = GatewayBridgeStoredConfigFromJson(json);
|
|
if (!parsed.has_value()) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
{
|
|
LockGuard guard(lock);
|
|
const esp_err_t validation_err = validateStoredBridgeConfigLocked(parsed.value());
|
|
if (validation_err != ESP_OK) {
|
|
return validation_err;
|
|
}
|
|
}
|
|
BridgeProvisioningStore store(bridgeNamespace());
|
|
const esp_err_t err = store.saveObject(
|
|
kBridgeConfigKey,
|
|
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus, parsed->knx,
|
|
parsed->bacnet_server));
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
LockGuard guard(lock);
|
|
bridge_config = parsed->bridge;
|
|
modbus_config = parsed->modbus;
|
|
knx_config = parsed->knx;
|
|
bacnet_server_config = parsed->bacnet_server;
|
|
bridge_config_loaded = true;
|
|
applyBridgeConfigLocked();
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t clearBridgeConfig() {
|
|
BridgeProvisioningStore store(bridgeNamespace());
|
|
const esp_err_t err = store.clear();
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
LockGuard guard(lock);
|
|
bridge_config = BridgeRuntimeConfig{};
|
|
modbus_config.reset();
|
|
knx_config.reset();
|
|
bacnet_server_config.reset();
|
|
bridge_config_loaded = false;
|
|
applyBridgeConfigLocked();
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t saveCloudConfig(std::string_view json) {
|
|
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
|
|
if (root == nullptr) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
const GatewayCloudConfig parsed = GatewayCloudConfigFromJson(root);
|
|
cJSON_Delete(root);
|
|
GatewayProvisioningStore store(cloudNamespace());
|
|
const esp_err_t err = store.save(parsed);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
LockGuard guard(lock);
|
|
cloud_config = parsed;
|
|
cloud_config_loaded = true;
|
|
if (cloud_started) {
|
|
startCloudLocked();
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t clearCloudConfig() {
|
|
GatewayProvisioningStore store(cloudNamespace());
|
|
const esp_err_t err = store.clear();
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
LockGuard guard(lock);
|
|
if (cloud != nullptr) {
|
|
cloud->stop();
|
|
}
|
|
cloud_config.reset();
|
|
cloud_config_loaded = false;
|
|
cloud_started = false;
|
|
applyCloudModelsLocked();
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t startCloud() {
|
|
LockGuard guard(lock);
|
|
return startCloudLocked();
|
|
}
|
|
|
|
esp_err_t startCloudLocked() {
|
|
if (!service_config.cloud_enabled) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
if (!cloud_config.has_value()) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
if (cloud == nullptr) {
|
|
applyCloudModelsLocked();
|
|
}
|
|
if (cloud->start(cloud_config.value())) {
|
|
cloud_started = true;
|
|
return ESP_OK;
|
|
}
|
|
cloud_started = false;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
esp_err_t stopCloud() {
|
|
LockGuard guard(lock);
|
|
if (cloud != nullptr) {
|
|
cloud->stop();
|
|
}
|
|
cloud_started = false;
|
|
return ESP_OK;
|
|
}
|
|
|
|
std::string modelName(const std::string& model_id) const {
|
|
const auto model = std::find_if(bridge_config.models.begin(), bridge_config.models.end(),
|
|
[&model_id](const auto& item) { return item.id == model_id; });
|
|
return model == bridge_config.models.end() ? model_id : model->displayName();
|
|
}
|
|
|
|
bool shouldPublishBacnetBindingLocked(const GatewayBacnetModelBinding& 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<GatewayBacnetModelBinding> effectiveBacnetObjectsLocked() {
|
|
std::vector<GatewayBacnetModelBinding> 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;
|
|
config.device_name = channel.name.empty() ? "DALI Gateway " + std::to_string(channel.gateway_id)
|
|
: channel.name;
|
|
config.task_stack_size = service_config.bacnet_task_stack_size;
|
|
config.task_priority = service_config.bacnet_task_priority;
|
|
if (bacnet_server_config.has_value()) {
|
|
config.device_instance = bacnet_server_config->deviceInstance;
|
|
config.local_address = bacnet_server_config->localAddress;
|
|
config.udp_port = bacnet_server_config->udpPort;
|
|
}
|
|
return config;
|
|
}
|
|
|
|
std::optional<uint32_t> generatedBacnetBinaryInputInstance(uint16_t modbus_address) const {
|
|
if (modbus_address < kModbusDiscreteInputBase) {
|
|
return std::nullopt;
|
|
}
|
|
const uint32_t relative = static_cast<uint32_t>(modbus_address - kModbusDiscreteInputBase);
|
|
if (relative >= kBacnetGeneratedBinaryInputChannelStride) {
|
|
return std::nullopt;
|
|
}
|
|
const uint32_t instance = kBacnetGeneratedBinaryInputBase +
|
|
static_cast<uint32_t>(channel.channel_index) *
|
|
kBacnetGeneratedBinaryInputChannelStride +
|
|
relative;
|
|
if (instance > kBacnetMaxObjectInstance) {
|
|
return std::nullopt;
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
std::optional<GatewayModbusPoint> generatedBacnetPointForObjectLocked(
|
|
BridgeObjectType object_type, uint32_t object_instance) const {
|
|
if (object_type != BridgeObjectType::binaryInput || modbus == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
const uint32_t channel_base = kBacnetGeneratedBinaryInputBase +
|
|
static_cast<uint32_t>(channel.channel_index) *
|
|
kBacnetGeneratedBinaryInputChannelStride;
|
|
if (object_instance < channel_base ||
|
|
object_instance >= channel_base + kBacnetGeneratedBinaryInputChannelStride) {
|
|
return std::nullopt;
|
|
}
|
|
const uint32_t relative = object_instance - channel_base;
|
|
if (relative > UINT16_MAX - kModbusDiscreteInputBase) {
|
|
return std::nullopt;
|
|
}
|
|
const auto point = modbus->findPoint(
|
|
GatewayModbusSpace::kDiscreteInput,
|
|
static_cast<uint16_t>(kModbusDiscreteInputBase + relative));
|
|
if (!point.has_value() || !point->generated) {
|
|
return std::nullopt;
|
|
}
|
|
return point;
|
|
}
|
|
|
|
bool shouldPublishGeneratedBacnetPointLocked(const GatewayModbusPoint& point) {
|
|
if (!point.generated || point.space != GatewayModbusSpace::kDiscreteInput ||
|
|
point.access != GatewayModbusAccess::kReadOnly ||
|
|
!ValidShortAddress(point.short_address)) {
|
|
return false;
|
|
}
|
|
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
|
if (discovery == nullptr) {
|
|
return false;
|
|
}
|
|
if (point.diagnostic_device_type > 0 &&
|
|
!SnapshotHasDeviceType(discovery->discovery, point.diagnostic_device_type)) {
|
|
return false;
|
|
}
|
|
return generatedBacnetBinaryInputInstance(point.address).has_value();
|
|
}
|
|
|
|
std::vector<GatewayBacnetObjectBinding> generatedBacnetObjectBindingsLocked() {
|
|
std::vector<GatewayBacnetObjectBinding> bindings;
|
|
if (modbus == nullptr) {
|
|
return bindings;
|
|
}
|
|
std::vector<GatewayModbusPoint> generated_points;
|
|
generated_points.reserve(192);
|
|
for (const auto& inventory_entry : discovery_inventory) {
|
|
if (!ValidShortAddress(inventory_entry.first)) {
|
|
continue;
|
|
}
|
|
generated_points.clear();
|
|
modbus->appendGeneratedPointsForShortAddress(
|
|
static_cast<uint8_t>(inventory_entry.first), &generated_points);
|
|
for (const auto& point : generated_points) {
|
|
if (!shouldPublishGeneratedBacnetPointLocked(point)) {
|
|
continue;
|
|
}
|
|
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
|
const auto object_instance = generatedBacnetBinaryInputInstance(point.address);
|
|
if (discovery == nullptr || !object_instance.has_value()) {
|
|
continue;
|
|
}
|
|
const auto binding = modbus->describePoint(point);
|
|
const bool out_of_service = !discovery->online;
|
|
bindings.push_back(GatewayBacnetObjectBinding{channel.gateway_id,
|
|
binding.id,
|
|
binding.name,
|
|
BridgeObjectType::binaryInput,
|
|
object_instance.value(),
|
|
"presentValue",
|
|
out_of_service,
|
|
out_of_service
|
|
? kBacnetReliabilityCommunicationFailure
|
|
: kBacnetReliabilityNoFaultDetected,
|
|
true});
|
|
}
|
|
}
|
|
return bindings;
|
|
}
|
|
|
|
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() {
|
|
std::vector<GatewayBacnetObjectBinding> bindings;
|
|
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,
|
|
out_of_service,
|
|
reliability,
|
|
BridgeOperationReadable(binding.operation)});
|
|
}
|
|
auto generated = generatedBacnetObjectBindingsLocked();
|
|
bindings.insert(bindings.end(), generated.begin(), generated.end());
|
|
return bindings;
|
|
}
|
|
|
|
bool handleBacnetWrite(BridgeObjectType object_type, uint32_t object_instance,
|
|
const std::string& property, const DaliValue& value) {
|
|
LockGuard guard(lock);
|
|
if (bacnet == nullptr) {
|
|
return false;
|
|
}
|
|
const DaliBridgeResult result = bacnet->handlePropertyWrite(
|
|
object_type, static_cast<int>(object_instance), property, value);
|
|
if (!result.ok) {
|
|
ESP_LOGW(kTag, "gateway=%u BACnet write rejected: %s", channel.gateway_id,
|
|
result.error.c_str());
|
|
}
|
|
return result.ok;
|
|
}
|
|
|
|
std::optional<DaliValue> readBacnetValue(BridgeObjectType object_type,
|
|
uint32_t object_instance,
|
|
const std::string& property) {
|
|
LockGuard guard(lock);
|
|
if (const auto point = generatedBacnetPointForObjectLocked(object_type, object_instance)) {
|
|
const auto value = readGeneratedBoolPointLocked(point.value());
|
|
return value.has_value() ? std::optional<DaliValue>(DaliValue(value.value()))
|
|
: std::nullopt;
|
|
}
|
|
if (bacnet == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
const auto binding = bacnet->findObject(object_type, static_cast<int>(object_instance),
|
|
property.empty() ? "presentValue" : property);
|
|
if (!binding.has_value() || !BridgeOperationReadable(binding->operation)) {
|
|
return std::nullopt;
|
|
}
|
|
const DaliBridgeResult result = bacnet->readProperty(
|
|
object_type, static_cast<int>(object_instance),
|
|
property.empty() ? "presentValue" : property);
|
|
if (!result.ok || !result.data.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
if ((object_type == BridgeObjectType::binaryInput ||
|
|
object_type == BridgeObjectType::binaryValue ||
|
|
object_type == BridgeObjectType::binaryOutput) &&
|
|
binding->bitIndex.has_value()) {
|
|
const int bit = binding->bitIndex.value();
|
|
if (bit < 0 || bit >= 32) {
|
|
return std::nullopt;
|
|
}
|
|
return DaliValue(((result.data.value() >> bit) & 0x1) != 0);
|
|
}
|
|
if (object_type == BridgeObjectType::binaryInput ||
|
|
object_type == BridgeObjectType::binaryValue ||
|
|
object_type == BridgeObjectType::binaryOutput) {
|
|
return DaliValue(result.data.value() != 0);
|
|
}
|
|
return DaliValue(result.data.value());
|
|
}
|
|
|
|
esp_err_t startBacnet() {
|
|
LockGuard guard(lock);
|
|
if (!service_config.bacnet_enabled) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
if (bacnet == nullptr) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
const esp_err_t err = syncBacnetServerLocked();
|
|
bacnet_started = err == ESP_OK;
|
|
return err;
|
|
}
|
|
#else
|
|
esp_err_t startBacnet() {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
#endif
|
|
|
|
esp_err_t startKnx(std::set<uint16_t>* used_ports = nullptr,
|
|
std::set<int>* used_uarts = nullptr) {
|
|
LockGuard guard(lock);
|
|
if (!service_config.knx_enabled) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
if (knx == nullptr || knx_router == nullptr) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
const auto config = activeKnxConfigLocked();
|
|
if (!config.has_value()) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
std::string validation_error;
|
|
const esp_err_t validation_err = validateKnxConfigLocked(
|
|
config.value(), activeModbusConfigLocked(), &validation_error);
|
|
if (validation_err != ESP_OK) {
|
|
knx_last_error = validation_error;
|
|
return validation_err;
|
|
}
|
|
if (config->ip_router_enabled && used_ports != nullptr) {
|
|
if (used_ports->find(config->udp_port) != used_ports->end()) {
|
|
knx_last_error = "duplicate KNXnet/IP UDP port " + std::to_string(config->udp_port);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
used_ports->insert(config->udp_port);
|
|
}
|
|
if (config->ip_router_enabled && used_uarts != nullptr) {
|
|
const int uart_port = config->tp_uart.uart_port;
|
|
if (used_uarts->find(uart_port) != used_uarts->end()) {
|
|
knx_last_error = "KNX TP-UART UART" + std::to_string(uart_port) +
|
|
" is already used by another runtime";
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
used_uarts->insert(uart_port);
|
|
}
|
|
knx->setConfig(config.value());
|
|
knx_router->setConfig(config.value());
|
|
if (!config->ip_router_enabled) {
|
|
knx_started = false;
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
knx_last_error.clear();
|
|
const esp_err_t err = knx_router->start(service_config.knx_task_stack_size,
|
|
service_config.knx_task_priority);
|
|
knx_started = err == ESP_OK;
|
|
if (err != ESP_OK) {
|
|
knx_last_error = knx_router->lastError().empty()
|
|
? "failed to start KNX TP-UART router"
|
|
: knx_router->lastError();
|
|
}
|
|
return err;
|
|
}
|
|
|
|
esp_err_t stopKnx() {
|
|
LockGuard guard(lock);
|
|
if (knx_router != nullptr) {
|
|
const esp_err_t err = knx_router->stop();
|
|
knx_started = false;
|
|
return err;
|
|
}
|
|
knx_started = false;
|
|
return ESP_OK;
|
|
}
|
|
|
|
GatewayBridgeHttpResponse execute(std::string_view json) {
|
|
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
|
|
if (root == nullptr) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid execute JSON");
|
|
}
|
|
const DaliBridgeRequest request = BridgeRequestFromJson(root);
|
|
cJSON_Delete(root);
|
|
|
|
LockGuard guard(lock);
|
|
if (engine == nullptr) {
|
|
return ErrorResponse(ESP_ERR_INVALID_STATE, "bridge engine is not ready");
|
|
}
|
|
const DaliBridgeResult result = engine->execute(request);
|
|
return JsonOk(BridgeResultToCjson(result));
|
|
}
|
|
|
|
cJSON* statusCjson() const {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (root == nullptr) {
|
|
return nullptr;
|
|
}
|
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
|
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();
|
|
if (modbus_json != nullptr) {
|
|
cJSON_AddBoolToObject(modbus_json, "enabled", service_config.modbus_enabled);
|
|
cJSON_AddBoolToObject(modbus_json, "started", modbus_started);
|
|
cJSON_AddStringToObject(modbus_json, "lastError", modbus_last_error.c_str());
|
|
if (modbus_config.has_value()) {
|
|
cJSON_AddStringToObject(modbus_json, "transport", modbus_config->transport.c_str());
|
|
cJSON_AddNumberToObject(modbus_json, "port", modbus_config->port);
|
|
cJSON_AddNumberToObject(modbus_json, "unitID", modbus_config->unit_id);
|
|
if (GatewayModbusTransportIsSerial(modbus_config->transport)) {
|
|
cJSON* serial_json = cJSON_CreateObject();
|
|
if (serial_json != nullptr) {
|
|
cJSON_AddNumberToObject(serial_json, "uartPort", modbus_config->serial.uart_port);
|
|
cJSON_AddNumberToObject(serial_json, "txPin", modbus_config->serial.tx_pin);
|
|
cJSON_AddNumberToObject(serial_json, "rxPin", modbus_config->serial.rx_pin);
|
|
cJSON_AddNumberToObject(serial_json, "baudrate", modbus_config->serial.baudrate);
|
|
cJSON_AddStringToObject(serial_json, "parity", modbus_config->serial.parity.c_str());
|
|
cJSON_AddNumberToObject(serial_json, "stopBits", modbus_config->serial.stop_bits);
|
|
cJSON* rs485_json = cJSON_CreateObject();
|
|
if (rs485_json != nullptr) {
|
|
cJSON_AddBoolToObject(rs485_json, "enabled", modbus_config->serial.rs485.enabled);
|
|
cJSON_AddNumberToObject(rs485_json, "dePin", modbus_config->serial.rs485.de_pin);
|
|
cJSON_AddItemToObject(serial_json, "rs485", rs485_json);
|
|
}
|
|
cJSON_AddItemToObject(modbus_json, "serial", serial_json);
|
|
}
|
|
}
|
|
}
|
|
cJSON_AddItemToObject(root, "modbus", modbus_json);
|
|
}
|
|
|
|
cJSON* bacnet_json = cJSON_CreateObject();
|
|
if (bacnet_json != nullptr) {
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
const auto server_status = GatewayBacnetServer::instance().status();
|
|
#endif
|
|
cJSON_AddBoolToObject(bacnet_json, "enabled", service_config.bacnet_enabled);
|
|
cJSON_AddBoolToObject(bacnet_json, "startupEnabled", service_config.bacnet_startup_enabled);
|
|
cJSON_AddBoolToObject(bacnet_json, "started", bacnet_started);
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
cJSON_AddBoolToObject(bacnet_json, "serverStarted", server_status.started);
|
|
cJSON_AddNumberToObject(bacnet_json, "serverObjectCount",
|
|
static_cast<double>(server_status.object_count));
|
|
#else
|
|
cJSON_AddBoolToObject(bacnet_json, "serverStarted", false);
|
|
cJSON_AddNumberToObject(bacnet_json, "serverObjectCount", 0);
|
|
#endif
|
|
if (bacnet_server_config.has_value()) {
|
|
cJSON_AddNumberToObject(bacnet_json, "deviceInstance",
|
|
bacnet_server_config->deviceInstance);
|
|
cJSON_AddStringToObject(bacnet_json, "localAddress",
|
|
bacnet_server_config->localAddress.c_str());
|
|
cJSON_AddNumberToObject(bacnet_json, "udpPort", bacnet_server_config->udpPort);
|
|
}
|
|
cJSON_AddItemToObject(root, "bacnet", bacnet_json);
|
|
}
|
|
|
|
cJSON* knx_json = cJSON_CreateObject();
|
|
if (knx_json != nullptr) {
|
|
const auto effective_knx = knx_config.has_value() ? knx_config : service_config.default_knx_config;
|
|
cJSON_AddBoolToObject(knx_json, "enabled", service_config.knx_enabled);
|
|
cJSON_AddBoolToObject(knx_json, "startupEnabled", service_config.knx_startup_enabled);
|
|
cJSON_AddBoolToObject(knx_json, "started", knx_started);
|
|
cJSON_AddBoolToObject(knx_json, "routerReady", knx_router != nullptr && knx_router->started());
|
|
const std::string router_error = knx_router == nullptr ? "" : knx_router->lastError();
|
|
cJSON_AddStringToObject(knx_json, "lastError",
|
|
knx_last_error.empty() ? router_error.c_str()
|
|
: knx_last_error.c_str());
|
|
if (effective_knx.has_value()) {
|
|
cJSON_AddBoolToObject(knx_json, "daliRouterEnabled",
|
|
effective_knx->dali_router_enabled);
|
|
cJSON_AddBoolToObject(knx_json, "ipRouterEnabled",
|
|
effective_knx->ip_router_enabled);
|
|
cJSON_AddBoolToObject(knx_json, "tunnelEnabled", effective_knx->tunnel_enabled);
|
|
cJSON_AddBoolToObject(knx_json, "multicastEnabled",
|
|
effective_knx->multicast_enabled);
|
|
cJSON_AddNumberToObject(knx_json, "mainGroup", effective_knx->main_group);
|
|
cJSON_AddNumberToObject(knx_json, "udpPort", effective_knx->udp_port);
|
|
cJSON_AddStringToObject(knx_json, "multicastAddress",
|
|
effective_knx->multicast_address.c_str());
|
|
cJSON_AddNumberToObject(knx_json, "individualAddress",
|
|
effective_knx->individual_address);
|
|
cJSON* serial_json = cJSON_CreateObject();
|
|
if (serial_json != nullptr) {
|
|
cJSON_AddNumberToObject(serial_json, "uartPort", effective_knx->tp_uart.uart_port);
|
|
cJSON_AddNumberToObject(serial_json, "txPin", effective_knx->tp_uart.tx_pin);
|
|
cJSON_AddNumberToObject(serial_json, "rxPin", effective_knx->tp_uart.rx_pin);
|
|
cJSON_AddNumberToObject(serial_json, "baudrate", effective_knx->tp_uart.baudrate);
|
|
cJSON_AddItemToObject(knx_json, "tpUart", serial_json);
|
|
}
|
|
}
|
|
cJSON_AddItemToObject(root, "knx", knx_json);
|
|
}
|
|
|
|
cJSON* cloud_json = cJSON_CreateObject();
|
|
if (cloud_json != nullptr) {
|
|
cJSON_AddBoolToObject(cloud_json, "enabled", service_config.cloud_enabled);
|
|
cJSON_AddBoolToObject(cloud_json, "configured", cloud_config_loaded);
|
|
cJSON_AddBoolToObject(cloud_json, "started", cloud_started);
|
|
cJSON_AddBoolToObject(cloud_json, "connected", cloud != nullptr && cloud->isConnected());
|
|
if (cloud_config.has_value()) {
|
|
cJSON_AddStringToObject(cloud_json, "deviceID", cloud_config->deviceID.c_str());
|
|
cJSON_AddStringToObject(cloud_json, "topicPrefix", cloud_config->topicPrefix.c_str());
|
|
}
|
|
cJSON_AddItemToObject(root, "cloud", cloud_json);
|
|
}
|
|
|
|
return root;
|
|
}
|
|
|
|
GatewayBridgeHttpResponse configJson() const {
|
|
return GatewayBridgeHttpResponse{ESP_OK,
|
|
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config,
|
|
knx_config,
|
|
bacnet_server_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);
|
|
cJSON* bindings = cJSON_CreateArray();
|
|
if (bindings != nullptr && modbus != nullptr) {
|
|
for (const auto& binding : modbus->describePoints()) {
|
|
cJSON* item = cJSON_CreateObject();
|
|
if (item == nullptr) {
|
|
continue;
|
|
}
|
|
if (!binding.model_id.empty()) {
|
|
cJSON_AddStringToObject(item, "model", binding.model_id.c_str());
|
|
}
|
|
cJSON_AddStringToObject(item, "space", GatewayModbusSpaceToString(binding.space));
|
|
cJSON_AddNumberToObject(item, "address", binding.address);
|
|
cJSON_AddStringToObject(item, "id", binding.id.c_str());
|
|
cJSON_AddStringToObject(item, "name", binding.name.c_str());
|
|
cJSON_AddStringToObject(item, "access", GatewayModbusAccessToString(binding.access));
|
|
cJSON_AddBoolToObject(item, "generated", binding.generated);
|
|
if (binding.generated) {
|
|
cJSON_AddStringToObject(item, "generatedKind",
|
|
GatewayModbusGeneratedKindToString(binding.generated_kind));
|
|
}
|
|
if (binding.short_address >= 0) {
|
|
cJSON_AddNumberToObject(item, "shortAddress", binding.short_address);
|
|
}
|
|
if (binding.bit_index.has_value()) {
|
|
cJSON_AddNumberToObject(item, "bitIndex", binding.bit_index.value());
|
|
}
|
|
if (!binding.diagnostic_snapshot.empty()) {
|
|
cJSON_AddStringToObject(item, "diagnosticSnapshot",
|
|
binding.diagnostic_snapshot.c_str());
|
|
}
|
|
if (!binding.diagnostic_bool.empty()) {
|
|
cJSON_AddStringToObject(item, "diagnosticBool", binding.diagnostic_bool.c_str());
|
|
}
|
|
if (binding.diagnostic_device_type >= 0) {
|
|
cJSON_AddNumberToObject(item, "diagnosticDeviceType",
|
|
binding.diagnostic_device_type);
|
|
}
|
|
cJSON_AddItemToArray(bindings, item);
|
|
}
|
|
}
|
|
cJSON_AddItemToObject(root, "bindings", bindings);
|
|
return JsonOk(root);
|
|
}
|
|
|
|
GatewayBridgeHttpResponse knxBindingsJson() const {
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
|
cJSON* bindings = cJSON_CreateArray();
|
|
if (bindings != nullptr && knx != nullptr) {
|
|
for (const auto& binding : knx->describeDaliBindings()) {
|
|
cJSON* item = cJSON_CreateObject();
|
|
if (item == nullptr) {
|
|
continue;
|
|
}
|
|
cJSON_AddStringToObject(item, "address", binding.address.c_str());
|
|
cJSON_AddNumberToObject(item, "rawAddress", binding.group_address);
|
|
cJSON_AddNumberToObject(item, "mainGroup", binding.main_group);
|
|
cJSON_AddNumberToObject(item, "middleGroup", binding.middle_group);
|
|
cJSON_AddNumberToObject(item, "subGroup", binding.sub_group);
|
|
cJSON_AddStringToObject(item, "name", binding.name.c_str());
|
|
cJSON_AddStringToObject(item, "datapointType", binding.datapoint_type.c_str());
|
|
cJSON_AddStringToObject(item, "dataType",
|
|
GatewayKnxDataTypeToString(binding.data_type));
|
|
cJSON_AddStringToObject(item, "targetKind",
|
|
GatewayKnxTargetKindToString(binding.target.kind));
|
|
if (binding.target.address >= 0) {
|
|
cJSON_AddNumberToObject(item, "targetAddress", binding.target.address);
|
|
}
|
|
cJSON_AddItemToArray(bindings, item);
|
|
}
|
|
}
|
|
cJSON_AddItemToObject(root, "bindings", bindings);
|
|
return JsonOk(root);
|
|
}
|
|
|
|
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 : effectiveBacnetObjectsLocked()) {
|
|
cJSON* item = cJSON_CreateObject();
|
|
if (item == nullptr) {
|
|
continue;
|
|
}
|
|
cJSON_AddStringToObject(item, "model", binding.modelID.c_str());
|
|
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);
|
|
}
|
|
if (modbus != nullptr) {
|
|
std::vector<GatewayModbusPoint> generated_points;
|
|
generated_points.reserve(192);
|
|
for (const auto& inventory_entry : discovery_inventory) {
|
|
if (!ValidShortAddress(inventory_entry.first)) {
|
|
continue;
|
|
}
|
|
generated_points.clear();
|
|
modbus->appendGeneratedPointsForShortAddress(
|
|
static_cast<uint8_t>(inventory_entry.first), &generated_points);
|
|
for (const auto& point : generated_points) {
|
|
if (!shouldPublishGeneratedBacnetPointLocked(point)) {
|
|
continue;
|
|
}
|
|
const auto object_instance = generatedBacnetBinaryInputInstance(point.address);
|
|
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
|
if (!object_instance.has_value() || discovery == nullptr) {
|
|
continue;
|
|
}
|
|
cJSON* item = cJSON_CreateObject();
|
|
if (item == nullptr) {
|
|
continue;
|
|
}
|
|
const auto binding = modbus->describePoint(point);
|
|
cJSON_AddStringToObject(item, "model", binding.id.c_str());
|
|
cJSON_AddStringToObject(item, "name", binding.name.c_str());
|
|
cJSON_AddStringToObject(item, "objectType", "binaryInput");
|
|
cJSON_AddNumberToObject(item, "objectInstance", object_instance.value());
|
|
cJSON_AddStringToObject(item, "property", "presentValue");
|
|
cJSON_AddBoolToObject(item, "generated", true);
|
|
cJSON_AddStringToObject(item, "generatedKind",
|
|
GatewayModbusGeneratedKindToString(binding.generated_kind));
|
|
cJSON_AddNumberToObject(item, "shortAddress", binding.short_address);
|
|
if (!binding.diagnostic_snapshot.empty()) {
|
|
cJSON_AddStringToObject(item, "diagnosticSnapshot",
|
|
binding.diagnostic_snapshot.c_str());
|
|
}
|
|
if (!binding.diagnostic_bool.empty()) {
|
|
cJSON_AddStringToObject(item, "diagnosticBool", binding.diagnostic_bool.c_str());
|
|
}
|
|
if (binding.diagnostic_device_type >= 0) {
|
|
cJSON_AddNumberToObject(item, "diagnosticDeviceType",
|
|
binding.diagnostic_device_type);
|
|
}
|
|
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));
|
|
cJSON_AddItemToArray(bindings, item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
cJSON_AddItemToObject(root, "bindings", bindings);
|
|
cJSON* server_json = cJSON_CreateObject();
|
|
if (server_json != nullptr) {
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
const auto server_status = GatewayBacnetServer::instance().status();
|
|
cJSON_AddBoolToObject(server_json, "started", server_status.started);
|
|
cJSON_AddNumberToObject(server_json, "deviceInstance", server_status.device_instance);
|
|
cJSON_AddNumberToObject(server_json, "udpPort", server_status.udp_port);
|
|
cJSON_AddNumberToObject(server_json, "channelCount",
|
|
static_cast<double>(server_status.channel_count));
|
|
cJSON_AddNumberToObject(server_json, "objectCount",
|
|
static_cast<double>(server_status.object_count));
|
|
#else
|
|
cJSON_AddBoolToObject(server_json, "started", false);
|
|
cJSON_AddNumberToObject(server_json, "deviceInstance", 0);
|
|
cJSON_AddNumberToObject(server_json, "udpPort", 0);
|
|
cJSON_AddNumberToObject(server_json, "channelCount", 0);
|
|
cJSON_AddNumberToObject(server_json, "objectCount", 0);
|
|
#endif
|
|
cJSON_AddItemToObject(root, "server", server_json);
|
|
}
|
|
return JsonOk(root);
|
|
}
|
|
|
|
GatewayBridgeHttpResponse cloudJson() const {
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
|
cJSON_AddBoolToObject(root, "configured", cloud_config_loaded);
|
|
cJSON_AddBoolToObject(root, "started", cloud_started);
|
|
cJSON_AddBoolToObject(root, "connected", cloud != nullptr && cloud->isConnected());
|
|
if (cloud_config.has_value()) {
|
|
cJSON_AddItemToObject(root, "config", GatewayCloudConfigToCjson(cloud_config.value()));
|
|
}
|
|
return JsonOk(root);
|
|
}
|
|
|
|
std::optional<bool> readGeneratedBoolPointLocked(const GatewayModbusPoint& point) {
|
|
if (!point.generated || !ValidShortAddress(point.short_address)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
|
const auto state = cache.daliAddressState(channel.gateway_id,
|
|
static_cast<uint8_t>(point.short_address));
|
|
switch (point.generated_kind) {
|
|
case GatewayModbusGeneratedKind::kShortDiscovered:
|
|
return discovery != nullptr;
|
|
case GatewayModbusGeneratedKind::kShortOnline:
|
|
return discovery != nullptr && discovery->online;
|
|
case GatewayModbusGeneratedKind::kShortSupportsDt1:
|
|
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 1);
|
|
case GatewayModbusGeneratedKind::kShortSupportsDt4:
|
|
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 4);
|
|
case GatewayModbusGeneratedKind::kShortSupportsDt5:
|
|
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 5);
|
|
case GatewayModbusGeneratedKind::kShortSupportsDt6:
|
|
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 6);
|
|
case GatewayModbusGeneratedKind::kShortSupportsDt8:
|
|
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 8);
|
|
case GatewayModbusGeneratedKind::kShortGroupMaskKnown:
|
|
return state.group_mask_known;
|
|
case GatewayModbusGeneratedKind::kShortActualLevelKnown:
|
|
return state.status.actual_level.has_value();
|
|
case GatewayModbusGeneratedKind::kShortSceneKnown:
|
|
return state.status.scene_id.has_value();
|
|
case GatewayModbusGeneratedKind::kShortSettingsKnown:
|
|
return state.settings.anyKnown();
|
|
case GatewayModbusGeneratedKind::kShortControlGearPresent:
|
|
return readSnapshotBoolLocked(point.short_address, "base_status", "controlGearPresent");
|
|
case GatewayModbusGeneratedKind::kShortLampFailure:
|
|
return readSnapshotBoolLocked(point.short_address, "base_status", "lampFailure");
|
|
case GatewayModbusGeneratedKind::kShortLampPowerOn:
|
|
return readSnapshotBoolLocked(point.short_address, "base_status", "lampPowerOn");
|
|
case GatewayModbusGeneratedKind::kShortLimitError:
|
|
return readSnapshotBoolLocked(point.short_address, "base_status", "limitError");
|
|
case GatewayModbusGeneratedKind::kShortFadingCompleted:
|
|
return readSnapshotBoolLocked(point.short_address, "base_status", "fadingCompleted");
|
|
case GatewayModbusGeneratedKind::kShortResetState:
|
|
return readSnapshotBoolLocked(point.short_address, "base_status", "resetState");
|
|
case GatewayModbusGeneratedKind::kShortMissingShortAddress:
|
|
return readSnapshotBoolLocked(point.short_address, "base_status", "missingShortAddress");
|
|
case GatewayModbusGeneratedKind::kShortPowerSupplyFault:
|
|
return readSnapshotBoolLocked(point.short_address, "base_status", "powerSupplyFault");
|
|
case GatewayModbusGeneratedKind::kShortDiagnosticBit:
|
|
return readDiagnosticBoolPointLocked(point);
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
std::optional<uint16_t> readGeneratedRegisterPointLocked(const GatewayModbusPoint& point) {
|
|
if (!point.generated || !ValidShortAddress(point.short_address)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
|
const auto state = cache.daliAddressState(channel.gateway_id,
|
|
static_cast<uint8_t>(point.short_address));
|
|
switch (point.generated_kind) {
|
|
case GatewayModbusGeneratedKind::kShortInventoryState:
|
|
if (discovery == nullptr) {
|
|
return 0;
|
|
}
|
|
return discovery->online ? 2 : 1;
|
|
case GatewayModbusGeneratedKind::kShortPrimaryType: {
|
|
if (discovery == nullptr) {
|
|
return kModbusUnknownRegister;
|
|
}
|
|
const auto primary = discovery->discovery.ints.find("primaryType");
|
|
return primary == discovery->discovery.ints.end()
|
|
? kModbusUnknownRegister
|
|
: static_cast<uint16_t>(primary->second);
|
|
}
|
|
case GatewayModbusGeneratedKind::kShortTypeMask:
|
|
return discovery == nullptr ? kModbusUnknownRegister : DeviceTypeMask(discovery->discovery);
|
|
case GatewayModbusGeneratedKind::kShortBrightness:
|
|
case GatewayModbusGeneratedKind::kShortActualLevel:
|
|
return state.status.actual_level.has_value()
|
|
? static_cast<uint16_t>(state.status.actual_level.value())
|
|
: kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortSceneId:
|
|
return state.status.scene_id.has_value()
|
|
? static_cast<uint16_t>(state.status.scene_id.value())
|
|
: kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortRawStatus: {
|
|
const auto snapshot = diagnosticSnapshotLocked(point.short_address, "base_status");
|
|
if (snapshot.has_value()) {
|
|
const auto raw_status = SnapshotIntValue(snapshot.value(), "rawStatus");
|
|
return raw_status.has_value() ? static_cast<uint16_t>(raw_status.value() & 0xFF)
|
|
: kModbusUnknownRegister;
|
|
}
|
|
return kModbusUnknownRegister;
|
|
}
|
|
case GatewayModbusGeneratedKind::kShortGroupMask:
|
|
return state.group_mask_known ? state.group_mask : kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
|
|
return state.settings.power_on_level.has_value()
|
|
? static_cast<uint16_t>(state.settings.power_on_level.value())
|
|
: kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
|
|
return state.settings.system_failure_level.has_value()
|
|
? static_cast<uint16_t>(state.settings.system_failure_level.value())
|
|
: kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortMinLevel:
|
|
return state.settings.min_level.has_value()
|
|
? static_cast<uint16_t>(state.settings.min_level.value())
|
|
: kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortMaxLevel:
|
|
return state.settings.max_level.has_value()
|
|
? static_cast<uint16_t>(state.settings.max_level.value())
|
|
: kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortFadeTime:
|
|
return state.settings.fade_time.has_value()
|
|
? static_cast<uint16_t>(state.settings.fade_time.value())
|
|
: kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortFadeRate:
|
|
return state.settings.fade_rate.has_value()
|
|
? static_cast<uint16_t>(state.settings.fade_rate.value())
|
|
: kModbusUnknownRegister;
|
|
case GatewayModbusGeneratedKind::kShortColorTemperature:
|
|
return kModbusUnknownRegister;
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
bool writeGeneratedCoilPointLocked(const GatewayModbusPoint& point, bool value) {
|
|
if (!point.generated || !ValidShortAddress(point.short_address)) {
|
|
return false;
|
|
}
|
|
if (!value) {
|
|
return true;
|
|
}
|
|
|
|
const uint8_t raw_command_address = RawCommandAddressFromDec(point.short_address);
|
|
bool sent = false;
|
|
uint8_t mirrored_command = 0;
|
|
switch (point.generated_kind) {
|
|
case GatewayModbusGeneratedKind::kShortOn:
|
|
case GatewayModbusGeneratedKind::kShortRecallMax:
|
|
sent = domain.on(channel.gateway_id, point.short_address);
|
|
mirrored_command = DALI_CMD_RECALL_MAX;
|
|
break;
|
|
case GatewayModbusGeneratedKind::kShortOff:
|
|
sent = domain.off(channel.gateway_id, point.short_address);
|
|
mirrored_command = DALI_CMD_OFF;
|
|
break;
|
|
case GatewayModbusGeneratedKind::kShortRecallMin:
|
|
sent = domain.sendRaw(channel.gateway_id, raw_command_address, DALI_CMD_RECALL_MIN);
|
|
mirrored_command = DALI_CMD_RECALL_MIN;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
if (sent) {
|
|
cache.mirrorDaliCommand(channel.gateway_id, raw_command_address, mirrored_command);
|
|
}
|
|
return sent;
|
|
}
|
|
|
|
bool writeGeneratedRegisterPointLocked(const GatewayModbusPoint& point, uint16_t value) {
|
|
if (!point.generated || !ValidShortAddress(point.short_address)) {
|
|
return false;
|
|
}
|
|
|
|
switch (point.generated_kind) {
|
|
case GatewayModbusGeneratedKind::kShortBrightness:
|
|
if (value > 254) {
|
|
return false;
|
|
}
|
|
if (domain.setBright(channel.gateway_id, point.short_address, value)) {
|
|
cache.mirrorDaliCommand(channel.gateway_id, RawArcAddressFromDec(point.short_address),
|
|
static_cast<uint8_t>(value));
|
|
return true;
|
|
}
|
|
return false;
|
|
case GatewayModbusGeneratedKind::kShortColorTemperature:
|
|
return domain.setColTemp(channel.gateway_id, point.short_address, value);
|
|
case GatewayModbusGeneratedKind::kShortGroupMask:
|
|
if (domain.applyGroupMask(channel.gateway_id, point.short_address, value)) {
|
|
cache.setDaliGroupMask(channel.gateway_id, static_cast<uint8_t>(point.short_address),
|
|
value);
|
|
return true;
|
|
}
|
|
return false;
|
|
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
|
|
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
|
|
case GatewayModbusGeneratedKind::kShortMinLevel:
|
|
case GatewayModbusGeneratedKind::kShortMaxLevel:
|
|
case GatewayModbusGeneratedKind::kShortFadeTime:
|
|
case GatewayModbusGeneratedKind::kShortFadeRate: {
|
|
if (value > 255) {
|
|
return false;
|
|
}
|
|
auto current = cache.daliAddressState(channel.gateway_id,
|
|
static_cast<uint8_t>(point.short_address)).settings;
|
|
switch (point.generated_kind) {
|
|
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
|
|
current.power_on_level = static_cast<uint8_t>(value);
|
|
break;
|
|
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
|
|
current.system_failure_level = static_cast<uint8_t>(value);
|
|
break;
|
|
case GatewayModbusGeneratedKind::kShortMinLevel:
|
|
current.min_level = static_cast<uint8_t>(value);
|
|
break;
|
|
case GatewayModbusGeneratedKind::kShortMaxLevel:
|
|
current.max_level = static_cast<uint8_t>(value);
|
|
break;
|
|
case GatewayModbusGeneratedKind::kShortFadeTime:
|
|
current.fade_time = static_cast<uint8_t>(value);
|
|
break;
|
|
case GatewayModbusGeneratedKind::kShortFadeRate:
|
|
current.fade_rate = static_cast<uint8_t>(value);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
DaliAddressSettingsSnapshot domain_settings;
|
|
domain_settings.power_on_level = current.power_on_level;
|
|
domain_settings.system_failure_level = current.system_failure_level;
|
|
domain_settings.min_level = current.min_level;
|
|
domain_settings.max_level = current.max_level;
|
|
domain_settings.fade_time = current.fade_time;
|
|
domain_settings.fade_rate = current.fade_rate;
|
|
if (domain.applyAddressSettings(channel.gateway_id, point.short_address, domain_settings)) {
|
|
cache.setDaliSettings(channel.gateway_id, static_cast<uint8_t>(point.short_address),
|
|
current);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::optional<bool> readModbusBoolPoint(GatewayModbusSpace space, uint16_t address) {
|
|
LockGuard guard(lock);
|
|
if (modbus == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
const auto point = modbus->findPoint(space, address);
|
|
if (!point.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
if (point->generated) {
|
|
return readGeneratedBoolPointLocked(point.value()).value_or(false);
|
|
}
|
|
const DaliBridgeResult result = modbus->readModelPoint(point.value());
|
|
if (!result.ok || !result.data.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
if (point->bit_index.has_value() && point->bit_index.value() >= 0 &&
|
|
point->bit_index.value() < 16) {
|
|
return (result.data.value() & (1 << point->bit_index.value())) != 0;
|
|
}
|
|
return result.data.value() != 0;
|
|
}
|
|
|
|
std::optional<uint16_t> readModbusRegisterPoint(GatewayModbusSpace space, uint16_t address) {
|
|
LockGuard guard(lock);
|
|
if (modbus == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
const auto point = modbus->findPoint(space, address);
|
|
if (!point.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
if (point->generated) {
|
|
return readGeneratedRegisterPointLocked(point.value()).value_or(kModbusUnknownRegister);
|
|
}
|
|
const DaliBridgeResult result = modbus->readModelPoint(point.value());
|
|
if (!result.ok || !result.data.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<uint16_t>(result.data.value() & 0xFFFF);
|
|
}
|
|
|
|
bool writeModbusCoilPoint(uint16_t address, bool value) {
|
|
LockGuard guard(lock);
|
|
if (modbus == nullptr) {
|
|
return false;
|
|
}
|
|
const auto point = modbus->findPoint(GatewayModbusSpace::kCoil, address);
|
|
if (!point.has_value()) {
|
|
return false;
|
|
}
|
|
if (point->generated) {
|
|
return writeGeneratedCoilPointLocked(point.value(), value);
|
|
}
|
|
const DaliBridgeResult result = modbus->writeCoilPoint(point.value(), value);
|
|
return result.ok;
|
|
}
|
|
|
|
bool writeModbusRegisterPoint(uint16_t address, uint16_t value) {
|
|
LockGuard guard(lock);
|
|
if (modbus == nullptr) {
|
|
return false;
|
|
}
|
|
const auto point = modbus->findPoint(GatewayModbusSpace::kHoldingRegister, address);
|
|
if (!point.has_value()) {
|
|
return false;
|
|
}
|
|
if (point->generated) {
|
|
return writeGeneratedRegisterPointLocked(point.value(), value);
|
|
}
|
|
const DaliBridgeResult result = modbus->writeRegisterPoint(point.value(), value);
|
|
return result.ok;
|
|
}
|
|
|
|
std::optional<GatewayModbusConfig> activeModbusConfigLocked() const {
|
|
if (modbus_config.has_value()) {
|
|
return modbus_config;
|
|
}
|
|
return service_config.default_modbus_config;
|
|
}
|
|
|
|
std::optional<GatewayModbusConfig> activeModbusConfig() const {
|
|
LockGuard guard(lock);
|
|
return activeModbusConfigLocked();
|
|
}
|
|
|
|
bool isReservedUartLocked(int uart_port) const {
|
|
return std::find(service_config.reserved_uart_ports.begin(),
|
|
service_config.reserved_uart_ports.end(), uart_port) !=
|
|
service_config.reserved_uart_ports.end();
|
|
}
|
|
|
|
esp_err_t validateSerialModbusConfigLocked(
|
|
const GatewayModbusConfig& config,
|
|
const std::optional<GatewayKnxConfig>& candidate_knx,
|
|
std::string* error_message = nullptr) const {
|
|
const int uart_port = config.serial.uart_port;
|
|
if (uart_port < 0 || uart_port > 2) {
|
|
if (error_message != nullptr) {
|
|
*error_message = "Modbus serial UART port must be 0, 1, or 2";
|
|
}
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (uart_port == 0 && !service_config.allow_modbus_uart0) {
|
|
if (error_message != nullptr) {
|
|
*error_message =
|
|
"Modbus serial on UART0 requires moving the ESP-IDF console and UART0 control off UART0";
|
|
}
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (isReservedUartLocked(uart_port)) {
|
|
if (error_message != nullptr) {
|
|
*error_message = "Modbus serial UART" + std::to_string(uart_port) +
|
|
" is already reserved by a DALI serial PHY";
|
|
}
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (service_config.knx_enabled && candidate_knx.has_value() &&
|
|
candidate_knx->ip_router_enabled && candidate_knx->tp_uart.uart_port == uart_port) {
|
|
if (error_message != nullptr) {
|
|
*error_message = "Modbus serial UART" + std::to_string(uart_port) +
|
|
" conflicts with KNX TP-UART; choose another free UART for RS485";
|
|
}
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t validateKnxConfigLocked(const GatewayKnxConfig& config,
|
|
const std::optional<GatewayModbusConfig>& candidate_modbus,
|
|
std::string* error_message = nullptr) const {
|
|
if (!config.ip_router_enabled) {
|
|
return ESP_OK;
|
|
}
|
|
const int uart_port = config.tp_uart.uart_port;
|
|
if (uart_port < 0 || uart_port > 2) {
|
|
if (error_message != nullptr) {
|
|
*error_message = "KNX TP-UART port must be 0, 1, or 2";
|
|
}
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (uart_port == 0 && !service_config.allow_knx_uart0) {
|
|
if (error_message != nullptr) {
|
|
*error_message =
|
|
"KNX TP-UART on UART0 requires moving the ESP-IDF console and UART0 control off UART0";
|
|
}
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (isReservedUartLocked(uart_port)) {
|
|
if (error_message != nullptr) {
|
|
*error_message = "KNX TP-UART UART" + std::to_string(uart_port) +
|
|
" is already reserved by a DALI serial PHY";
|
|
}
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (service_config.modbus_enabled && candidate_modbus.has_value() &&
|
|
GatewayModbusTransportIsSerial(candidate_modbus->transport) &&
|
|
candidate_modbus->serial.uart_port == uart_port) {
|
|
if (error_message != nullptr) {
|
|
*error_message = "KNX TP-UART UART" + std::to_string(uart_port) +
|
|
" conflicts with Modbus serial UART";
|
|
}
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t validateStoredBridgeConfigLocked(const GatewayBridgeStoredConfig& config) {
|
|
const auto candidate_modbus = config.modbus.has_value() ? config.modbus
|
|
: service_config.default_modbus_config;
|
|
const auto candidate_knx = config.knx.has_value() ? config.knx
|
|
: service_config.default_knx_config;
|
|
std::string validation_error;
|
|
if (candidate_modbus.has_value() &&
|
|
GatewayModbusTransportIsSerial(candidate_modbus->transport)) {
|
|
const esp_err_t err = validateSerialModbusConfigLocked(
|
|
candidate_modbus.value(), candidate_knx, &validation_error);
|
|
if (err != ESP_OK) {
|
|
modbus_last_error = validation_error;
|
|
return err;
|
|
}
|
|
}
|
|
if (candidate_knx.has_value()) {
|
|
const esp_err_t err = validateKnxConfigLocked(candidate_knx.value(), candidate_modbus,
|
|
&validation_error);
|
|
if (err != ESP_OK) {
|
|
knx_last_error = validation_error;
|
|
return err;
|
|
}
|
|
}
|
|
modbus_last_error.clear();
|
|
knx_last_error.clear();
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t saveModbusConfig(const GatewayModbusConfig& config) {
|
|
LockGuard guard(lock);
|
|
if (GatewayModbusTransportIsSerial(config.transport)) {
|
|
std::string validation_error;
|
|
const esp_err_t validation_err = validateSerialModbusConfigLocked(
|
|
config, activeKnxConfigLocked(), &validation_error);
|
|
if (validation_err != ESP_OK) {
|
|
modbus_last_error = validation_error;
|
|
return validation_err;
|
|
}
|
|
}
|
|
BridgeProvisioningStore store(bridgeNamespace());
|
|
const esp_err_t err = store.saveObject(
|
|
kBridgeConfigKey,
|
|
GatewayBridgeStoredConfigToValue(bridge_config, config, knx_config,
|
|
bacnet_server_config));
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
modbus_config = config;
|
|
bridge_config_loaded = true;
|
|
if (modbus != nullptr) {
|
|
modbus->setConfig(config);
|
|
}
|
|
modbus_last_error.clear();
|
|
return ESP_OK;
|
|
}
|
|
|
|
std::optional<GatewayKnxConfig> activeKnxConfigLocked() const {
|
|
if (knx_config.has_value()) {
|
|
return knx_config;
|
|
}
|
|
return service_config.default_knx_config;
|
|
}
|
|
|
|
esp_err_t saveKnxConfig(const GatewayKnxConfig& config,
|
|
std::set<uint16_t>* used_ports = nullptr,
|
|
std::set<int>* used_uarts = nullptr) {
|
|
LockGuard guard(lock);
|
|
std::string validation_error;
|
|
const esp_err_t validation_err = validateKnxConfigLocked(
|
|
config, activeModbusConfigLocked(), &validation_error);
|
|
if (validation_err != ESP_OK) {
|
|
knx_last_error = validation_error;
|
|
return validation_err;
|
|
}
|
|
const bool restart_router = knx_started || (knx_router != nullptr && knx_router->started());
|
|
if (restart_router && config.ip_router_enabled && used_ports != nullptr &&
|
|
used_ports->find(config.udp_port) != used_ports->end()) {
|
|
knx_last_error = "duplicate KNXnet/IP UDP port " + std::to_string(config.udp_port);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (restart_router && config.ip_router_enabled && used_uarts != nullptr &&
|
|
used_uarts->find(config.tp_uart.uart_port) != used_uarts->end()) {
|
|
knx_last_error = "KNX TP-UART UART" + std::to_string(config.tp_uart.uart_port) +
|
|
" is already used by another runtime";
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (restart_router && knx_router != nullptr) {
|
|
knx_router->stop();
|
|
knx_started = false;
|
|
}
|
|
BridgeProvisioningStore store(bridgeNamespace());
|
|
const esp_err_t err = store.saveObject(
|
|
kBridgeConfigKey,
|
|
GatewayBridgeStoredConfigToValue(bridge_config, modbus_config, config,
|
|
bacnet_server_config));
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
knx_config = config;
|
|
bridge_config_loaded = true;
|
|
if (knx != nullptr) {
|
|
knx->setConfig(config);
|
|
}
|
|
if (knx_router != nullptr) {
|
|
knx_router->setConfig(config);
|
|
}
|
|
if (restart_router) {
|
|
return startKnx(used_ports, used_uarts);
|
|
}
|
|
knx_last_error.clear();
|
|
return ESP_OK;
|
|
}
|
|
|
|
std::vector<uint8_t> processModbusPdu(const GatewayModbusConfig& config,
|
|
uint8_t unit_id,
|
|
const std::vector<uint8_t>& pdu) {
|
|
if (pdu.empty()) {
|
|
return {};
|
|
}
|
|
if (config.unit_id != 0 && unit_id != config.unit_id) {
|
|
return ModbusExceptionPdu(pdu[0], 0x0B);
|
|
}
|
|
|
|
if ((pdu[0] == 0x01 || pdu[0] == 0x02) && pdu.size() == 5) {
|
|
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
|
|
const uint16_t start_address = ReadBe16(&pdu[1]);
|
|
const uint16_t quantity = ReadBe16(&pdu[3]);
|
|
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadBits) {
|
|
return ModbusExceptionPdu(pdu[0], 0x03);
|
|
}
|
|
const uint8_t byte_count = static_cast<uint8_t>((quantity + 7U) / 8U);
|
|
std::vector<uint8_t> response(2 + byte_count, 0);
|
|
response[0] = pdu[0];
|
|
response[1] = byte_count;
|
|
for (uint16_t index = 0; index < quantity; ++index) {
|
|
const auto human_address = static_cast<uint16_t>(
|
|
GatewayModbusHumanAddressFromWire(space.value(), start_address + index));
|
|
const auto value = readModbusBoolPoint(space.value(), human_address);
|
|
if (!value.has_value()) {
|
|
return ModbusExceptionPdu(pdu[0], 0x02);
|
|
}
|
|
if (value.value()) {
|
|
response[2 + (index / 8)] |= static_cast<uint8_t>(1U << (index % 8));
|
|
}
|
|
}
|
|
return response;
|
|
}
|
|
|
|
if ((pdu[0] == 0x03 || pdu[0] == 0x04) && pdu.size() == 5) {
|
|
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
|
|
const uint16_t start_address = ReadBe16(&pdu[1]);
|
|
const uint16_t quantity = ReadBe16(&pdu[3]);
|
|
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadRegisters) {
|
|
return ModbusExceptionPdu(pdu[0], 0x03);
|
|
}
|
|
std::vector<uint8_t> response(2 + quantity * 2);
|
|
response[0] = pdu[0];
|
|
response[1] = static_cast<uint8_t>(quantity * 2);
|
|
for (uint16_t index = 0; index < quantity; ++index) {
|
|
const auto human_address = static_cast<uint16_t>(
|
|
GatewayModbusHumanAddressFromWire(space.value(), start_address + index));
|
|
const auto value = readModbusRegisterPoint(space.value(), human_address);
|
|
if (!value.has_value()) {
|
|
return ModbusExceptionPdu(pdu[0], 0x02);
|
|
}
|
|
WriteBe16(&response[2 + index * 2], value.value());
|
|
}
|
|
return response;
|
|
}
|
|
|
|
if (pdu[0] == 0x05 && pdu.size() == 5) {
|
|
const uint16_t wire_address = ReadBe16(&pdu[1]);
|
|
const uint16_t raw_value = ReadBe16(&pdu[3]);
|
|
if (raw_value != 0x0000 && raw_value != 0xFF00) {
|
|
return ModbusExceptionPdu(pdu[0], 0x03);
|
|
}
|
|
const auto coil = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
|
GatewayModbusSpace::kCoil, wire_address));
|
|
if (!writeModbusCoilPoint(coil, raw_value == 0xFF00)) {
|
|
return ModbusExceptionPdu(pdu[0], 0x04);
|
|
}
|
|
return pdu;
|
|
}
|
|
|
|
if (pdu[0] == 0x06 && pdu.size() == 5) {
|
|
const uint16_t wire_register = ReadBe16(&pdu[1]);
|
|
const uint16_t value = ReadBe16(&pdu[3]);
|
|
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
|
GatewayModbusSpace::kHoldingRegister, wire_register));
|
|
if (!writeModbusRegisterPoint(holding_register, value)) {
|
|
return ModbusExceptionPdu(pdu[0], 0x04);
|
|
}
|
|
return pdu;
|
|
}
|
|
|
|
if (pdu[0] == 0x0F && pdu.size() >= 6) {
|
|
const uint16_t start_address = ReadBe16(&pdu[1]);
|
|
const uint16_t quantity = ReadBe16(&pdu[3]);
|
|
const uint8_t byte_count = pdu[5];
|
|
if (quantity == 0 || quantity > kGatewayModbusMaxWriteBits ||
|
|
pdu.size() != static_cast<size_t>(6 + byte_count) ||
|
|
byte_count != static_cast<uint8_t>((quantity + 7U) / 8U)) {
|
|
return ModbusExceptionPdu(pdu[0], 0x03);
|
|
}
|
|
for (uint16_t index = 0; index < quantity; ++index) {
|
|
const bool value = (pdu[6 + (index / 8)] & (1U << (index % 8))) != 0;
|
|
const auto coil = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
|
GatewayModbusSpace::kCoil, start_address + index));
|
|
if (!writeModbusCoilPoint(coil, value)) {
|
|
return ModbusExceptionPdu(pdu[0], 0x04);
|
|
}
|
|
}
|
|
std::vector<uint8_t> response(5);
|
|
response[0] = pdu[0];
|
|
WriteBe16(&response[1], start_address);
|
|
WriteBe16(&response[3], quantity);
|
|
return response;
|
|
}
|
|
|
|
if (pdu[0] == 0x10 && pdu.size() >= 6) {
|
|
const uint16_t start_register = ReadBe16(&pdu[1]);
|
|
const uint16_t quantity = ReadBe16(&pdu[3]);
|
|
const uint8_t byte_count = pdu[5];
|
|
if (quantity == 0 || quantity > kGatewayModbusMaxWriteRegisters ||
|
|
pdu.size() != static_cast<size_t>(6 + byte_count) || byte_count != quantity * 2) {
|
|
return ModbusExceptionPdu(pdu[0], 0x03);
|
|
}
|
|
for (uint16_t index = 0; index < quantity; ++index) {
|
|
const size_t offset = 6 + (index * 2);
|
|
const uint16_t value = ReadBe16(&pdu[offset]);
|
|
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
|
GatewayModbusSpace::kHoldingRegister, start_register + index));
|
|
if (!writeModbusRegisterPoint(holding_register, value)) {
|
|
return ModbusExceptionPdu(pdu[0], 0x04);
|
|
}
|
|
}
|
|
std::vector<uint8_t> response(5);
|
|
response[0] = pdu[0];
|
|
WriteBe16(&response[1], start_register);
|
|
WriteBe16(&response[3], quantity);
|
|
return response;
|
|
}
|
|
|
|
return ModbusExceptionPdu(pdu[0], 0x01);
|
|
}
|
|
|
|
esp_err_t startModbus(std::set<uint16_t>* used_ports = nullptr,
|
|
std::set<int>* used_uarts = nullptr) {
|
|
LockGuard guard(lock);
|
|
if (!service_config.modbus_enabled) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
if (modbus_started || modbus_task_handle != nullptr) {
|
|
return ESP_OK;
|
|
}
|
|
const auto config = activeModbusConfigLocked();
|
|
if (!config.has_value()) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
if (GatewayModbusTransportIsSerial(config->transport)) {
|
|
std::string validation_error;
|
|
const esp_err_t serial_err = validateSerialModbusConfigLocked(
|
|
config.value(), activeKnxConfigLocked(), &validation_error);
|
|
if (serial_err != ESP_OK) {
|
|
modbus_last_error = validation_error;
|
|
return serial_err;
|
|
}
|
|
if (used_uarts != nullptr) {
|
|
const int uart_port = config->serial.uart_port;
|
|
if (used_uarts->find(uart_port) != used_uarts->end()) {
|
|
modbus_last_error = "Modbus serial UART" + std::to_string(uart_port) +
|
|
" is already used by another runtime";
|
|
ESP_LOGW(kTag, "gateway=%u skips duplicate Modbus serial UART%d", channel.gateway_id,
|
|
uart_port);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
used_uarts->insert(uart_port);
|
|
}
|
|
}
|
|
const uint16_t port = config->port == 0 ? kGatewayModbusDefaultTcpPort : config->port;
|
|
if (GatewayModbusTransportIsTcp(config->transport) && used_ports != nullptr) {
|
|
if (used_ports->find(port) != used_ports->end()) {
|
|
modbus_last_error = "duplicate Modbus TCP port " + std::to_string(port);
|
|
ESP_LOGW(kTag, "gateway=%u skips duplicate Modbus TCP port %u", channel.gateway_id, port);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
used_ports->insert(port);
|
|
}
|
|
modbus_stop_requested = false;
|
|
modbus_restart_requested = false;
|
|
modbus_last_error.clear();
|
|
const char* task_name = GatewayModbusTransportIsTcp(config->transport)
|
|
? "gw_modbus_tcp"
|
|
: (GatewayModbusTransportIsAscii(config->transport)
|
|
? "gw_modbus_ascii"
|
|
: "gw_modbus_rtu");
|
|
const BaseType_t created = xTaskCreate(&ChannelRuntime::ModbusTaskEntry, task_name,
|
|
service_config.modbus_task_stack_size, this,
|
|
service_config.modbus_task_priority,
|
|
&modbus_task_handle);
|
|
if (created != pdPASS) {
|
|
modbus_task_handle = nullptr;
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
modbus_started = true;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t stopModbus() {
|
|
LockGuard guard(lock);
|
|
if (!modbus_started && modbus_task_handle == nullptr) {
|
|
return ESP_OK;
|
|
}
|
|
modbus_stop_requested = true;
|
|
modbus_restart_requested = false;
|
|
if (modbus_client_sock >= 0) {
|
|
shutdown(modbus_client_sock, SHUT_RDWR);
|
|
}
|
|
if (modbus_listen_sock >= 0) {
|
|
shutdown(modbus_listen_sock, SHUT_RDWR);
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
void modbusTaskLoop() {
|
|
const auto config = activeModbusConfig();
|
|
if (!config.has_value()) {
|
|
modbus_last_error = "missing Modbus config";
|
|
finishModbusTask();
|
|
return;
|
|
}
|
|
if (GatewayModbusTransportIsTcp(config->transport)) {
|
|
modbusTcpTaskLoop(config.value());
|
|
} else if (GatewayModbusTransportIsSerial(config->transport)) {
|
|
modbusSerialTaskLoop(config.value());
|
|
} else {
|
|
modbus_last_error = "unsupported Modbus transport";
|
|
ESP_LOGE(kTag, "gateway=%u unsupported Modbus transport %s", channel.gateway_id,
|
|
config->transport.c_str());
|
|
}
|
|
finishModbusTask();
|
|
}
|
|
|
|
void finishModbusTask() {
|
|
const bool restart = modbus_restart_requested.exchange(false);
|
|
{
|
|
LockGuard guard(lock);
|
|
modbus_started = false;
|
|
modbus_task_handle = nullptr;
|
|
modbus_stop_requested = false;
|
|
modbus_listen_sock = -1;
|
|
modbus_client_sock = -1;
|
|
modbus_uart_port = -1;
|
|
}
|
|
if (restart) {
|
|
startModbus();
|
|
}
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
void modbusTcpTaskLoop(const GatewayModbusConfig& config) {
|
|
const uint16_t port = config.port != 0 ? config.port : kGatewayModbusDefaultTcpPort;
|
|
const int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
|
if (listen_sock < 0) {
|
|
ESP_LOGE(kTag, "gateway=%u failed to create Modbus socket", channel.gateway_id);
|
|
modbus_last_error = "failed to create Modbus TCP socket";
|
|
return;
|
|
}
|
|
modbus_listen_sock = listen_sock;
|
|
|
|
int reuse = 1;
|
|
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
|
timeval timeout{};
|
|
timeout.tv_sec = 1;
|
|
timeout.tv_usec = 0;
|
|
setsockopt(listen_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
|
|
|
sockaddr_in address = {};
|
|
address.sin_family = AF_INET;
|
|
address.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
address.sin_port = htons(port);
|
|
|
|
if (bind(listen_sock, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0 ||
|
|
listen(listen_sock, 2) != 0) {
|
|
ESP_LOGE(kTag, "gateway=%u failed to bind Modbus TCP port %u", channel.gateway_id, port);
|
|
modbus_last_error = "failed to bind Modbus TCP port";
|
|
close(listen_sock);
|
|
return;
|
|
}
|
|
|
|
ESP_LOGI(kTag, "gateway=%u Modbus TCP listening on port %u", channel.gateway_id, port);
|
|
while (!modbus_stop_requested) {
|
|
sockaddr_in client_address = {};
|
|
socklen_t client_len = sizeof(client_address);
|
|
const int client_sock = accept(listen_sock, reinterpret_cast<sockaddr*>(&client_address),
|
|
&client_len);
|
|
if (client_sock < 0) {
|
|
continue;
|
|
}
|
|
setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
|
modbus_client_sock = client_sock;
|
|
handleModbusClient(config, client_sock);
|
|
modbus_client_sock = -1;
|
|
close(client_sock);
|
|
}
|
|
close(listen_sock);
|
|
modbus_listen_sock = -1;
|
|
}
|
|
|
|
void handleModbusClient(const GatewayModbusConfig& config, int client_sock) {
|
|
uint8_t header[7] = {};
|
|
while (!modbus_stop_requested && RecvAll(client_sock, header, sizeof(header))) {
|
|
const uint16_t protocol_id = ReadBe16(&header[2]);
|
|
const uint16_t length = ReadBe16(&header[4]);
|
|
if (protocol_id != 0 || length < 2 || length > kGatewayModbusMaxPduBytes) {
|
|
break;
|
|
}
|
|
|
|
std::vector<uint8_t> pdu(length - 1);
|
|
if (!RecvAll(client_sock, pdu.data(), pdu.size()) || pdu.empty()) {
|
|
break;
|
|
}
|
|
SendModbusFrame(client_sock, header, processModbusPdu(config, header[6], pdu));
|
|
}
|
|
}
|
|
|
|
esp_err_t installSerialModbus(const GatewayModbusConfig& config) {
|
|
const auto uart_port = static_cast<uart_port_t>(config.serial.uart_port);
|
|
uart_config_t uart_config{};
|
|
uart_config.baud_rate = static_cast<int>(config.serial.baudrate);
|
|
uart_config.data_bits = UartWordLength(config.serial.data_bits);
|
|
uart_config.parity = UartParity(config.serial.parity);
|
|
uart_config.stop_bits = UartStopBits(config.serial.stop_bits);
|
|
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
|
uart_config.source_clk = UART_SCLK_DEFAULT;
|
|
|
|
esp_err_t err = uart_param_config(uart_port, &uart_config);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
const int rts_pin = config.serial.rs485.enabled ? config.serial.rs485.de_pin
|
|
: UART_PIN_NO_CHANGE;
|
|
err = uart_set_pin(uart_port, config.serial.tx_pin, config.serial.rx_pin, rts_pin,
|
|
UART_PIN_NO_CHANGE);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
err = uart_driver_install(uart_port, static_cast<int>(config.serial.rx_buffer_size),
|
|
static_cast<int>(config.serial.tx_buffer_size), 0, nullptr, 0);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
if (config.serial.rs485.enabled) {
|
|
err = uart_set_mode(uart_port, UART_MODE_RS485_HALF_DUPLEX);
|
|
if (err != ESP_OK) {
|
|
uart_driver_delete(uart_port);
|
|
return err;
|
|
}
|
|
}
|
|
modbus_uart_port = config.serial.uart_port;
|
|
return ESP_OK;
|
|
}
|
|
|
|
void modbusSerialTaskLoop(const GatewayModbusConfig& config) {
|
|
const esp_err_t err = installSerialModbus(config);
|
|
if (err != ESP_OK) {
|
|
modbus_last_error = "failed to install Modbus serial UART";
|
|
ESP_LOGE(kTag, "gateway=%u failed to install Modbus serial UART%d: %s",
|
|
channel.gateway_id, config.serial.uart_port, esp_err_to_name(err));
|
|
return;
|
|
}
|
|
ESP_LOGI(kTag, "gateway=%u Modbus %s listening on UART%d baud=%lu", channel.gateway_id,
|
|
GatewayModbusTransportIsAscii(config.transport) ? "ASCII" : "RTU",
|
|
config.serial.uart_port, static_cast<unsigned long>(config.serial.baudrate));
|
|
if (GatewayModbusTransportIsAscii(config.transport)) {
|
|
modbusAsciiTaskLoop(config);
|
|
} else {
|
|
modbusRtuTaskLoop(config);
|
|
}
|
|
uart_driver_delete(static_cast<uart_port_t>(config.serial.uart_port));
|
|
}
|
|
|
|
void modbusRtuTaskLoop(const GatewayModbusConfig& config) {
|
|
const auto uart_port = static_cast<uart_port_t>(config.serial.uart_port);
|
|
std::vector<uint8_t> frame;
|
|
std::array<uint8_t, 128> read_buffer{};
|
|
const TickType_t timeout = pdMS_TO_TICKS(config.serial.response_timeout_ms);
|
|
while (!modbus_stop_requested) {
|
|
const int read_len = uart_read_bytes(uart_port, read_buffer.data(), read_buffer.size(), timeout);
|
|
if (read_len > 0) {
|
|
frame.insert(frame.end(), read_buffer.begin(), read_buffer.begin() + read_len);
|
|
if (!frame.empty() && frame.front() == '@' &&
|
|
std::find(frame.begin(), frame.end(), '\n') != frame.end()) {
|
|
const std::string line(frame.begin(), frame.end());
|
|
handleModbusManagementLine(config.serial.uart_port, line);
|
|
frame.clear();
|
|
} else if (frame.size() > 512) {
|
|
frame.clear();
|
|
}
|
|
continue;
|
|
}
|
|
if (frame.empty() || frame.front() == '@') {
|
|
continue;
|
|
}
|
|
handleModbusRtuFrame(config, frame);
|
|
frame.clear();
|
|
}
|
|
}
|
|
|
|
void handleModbusRtuFrame(const GatewayModbusConfig& config, const std::vector<uint8_t>& frame) {
|
|
if (frame.size() < 4) {
|
|
return;
|
|
}
|
|
const uint16_t received_crc = static_cast<uint16_t>(frame[frame.size() - 2] |
|
|
(frame[frame.size() - 1] << 8));
|
|
if (ModbusCrc16(frame.data(), frame.size() - 2) != received_crc) {
|
|
return;
|
|
}
|
|
const uint8_t unit_id = frame[0];
|
|
if (unit_id == 0) {
|
|
return;
|
|
}
|
|
const std::vector<uint8_t> pdu(frame.begin() + 1, frame.end() - 2);
|
|
const auto response_pdu = processModbusPdu(config, unit_id, pdu);
|
|
if (response_pdu.empty()) {
|
|
return;
|
|
}
|
|
std::vector<uint8_t> response;
|
|
response.reserve(1 + response_pdu.size() + 2);
|
|
response.push_back(unit_id);
|
|
response.insert(response.end(), response_pdu.begin(), response_pdu.end());
|
|
const uint16_t crc = ModbusCrc16(response.data(), response.size());
|
|
response.push_back(static_cast<uint8_t>(crc & 0xFF));
|
|
response.push_back(static_cast<uint8_t>((crc >> 8) & 0xFF));
|
|
uart_write_bytes(static_cast<uart_port_t>(config.serial.uart_port), response.data(),
|
|
response.size());
|
|
}
|
|
|
|
void modbusAsciiTaskLoop(const GatewayModbusConfig& config) {
|
|
const auto uart_port = static_cast<uart_port_t>(config.serial.uart_port);
|
|
std::string line;
|
|
std::array<uint8_t, 128> read_buffer{};
|
|
const TickType_t timeout = pdMS_TO_TICKS(config.serial.response_timeout_ms);
|
|
while (!modbus_stop_requested) {
|
|
const int read_len = uart_read_bytes(uart_port, read_buffer.data(), read_buffer.size(), timeout);
|
|
if (read_len <= 0) {
|
|
continue;
|
|
}
|
|
for (int i = 0; i < read_len; ++i) {
|
|
const char ch = static_cast<char>(read_buffer[i]);
|
|
if (line.empty()) {
|
|
if (ch != ':' && ch != '@') {
|
|
continue;
|
|
}
|
|
}
|
|
line.push_back(ch);
|
|
if (ch == '\n') {
|
|
if (LineStartsWith(line, kModbusManagementPrefix)) {
|
|
handleModbusManagementLine(config.serial.uart_port, line);
|
|
} else if (!line.empty() && line.front() == ':') {
|
|
handleModbusAsciiFrame(config, line);
|
|
}
|
|
line.clear();
|
|
} else if (line.size() > 1024) {
|
|
line.clear();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void handleModbusAsciiFrame(const GatewayModbusConfig& config, std::string_view line) {
|
|
const auto decoded = DecodeModbusAsciiLine(line);
|
|
if (!decoded.has_value() || decoded->size() < 4) {
|
|
return;
|
|
}
|
|
const uint8_t unit_id = decoded->front();
|
|
if (unit_id == 0) {
|
|
return;
|
|
}
|
|
const std::vector<uint8_t> pdu(decoded->begin() + 1, decoded->end() - 1);
|
|
const auto response_pdu = processModbusPdu(config, unit_id, pdu);
|
|
if (response_pdu.empty()) {
|
|
return;
|
|
}
|
|
std::vector<uint8_t> response;
|
|
response.reserve(1 + response_pdu.size() + 1);
|
|
response.push_back(unit_id);
|
|
response.insert(response.end(), response_pdu.begin(), response_pdu.end());
|
|
response.push_back(ModbusAsciiLrc(response.data(), response.size()));
|
|
const std::string encoded = EncodeModbusAsciiLine(response);
|
|
uart_write_bytes(static_cast<uart_port_t>(config.serial.uart_port), encoded.data(),
|
|
encoded.size());
|
|
}
|
|
|
|
void handleModbusManagementLine(int uart_port, std::string_view line) {
|
|
while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) {
|
|
line.remove_suffix(1);
|
|
}
|
|
if (!LineStartsWith(line, kModbusManagementPrefix)) {
|
|
return;
|
|
}
|
|
line.remove_prefix(std::char_traits<char>::length(kModbusManagementPrefix));
|
|
while (!line.empty() && line.front() == ' ') {
|
|
line.remove_prefix(1);
|
|
}
|
|
cJSON* root = line.empty() ? cJSON_CreateObject() : cJSON_ParseWithLength(line.data(), line.size());
|
|
if (root == nullptr || !cJSON_IsObject(root)) {
|
|
cJSON_Delete(root);
|
|
writeModbusManagementResponse(uart_port, false, "unknown", "invalid JSON");
|
|
return;
|
|
}
|
|
const auto gateway_id = JsonGatewayId(root);
|
|
if (gateway_id.has_value() && gateway_id.value() != channel.gateway_id) {
|
|
cJSON_Delete(root);
|
|
writeModbusManagementResponse(uart_port, false, "unknown", "gateway id mismatch");
|
|
return;
|
|
}
|
|
const char* action_raw = JsonString(root, "action");
|
|
const std::string action = action_raw == nullptr ? "modbus_status" : action_raw;
|
|
if (action == "modbus_config") {
|
|
const cJSON* modbus_node = cJSON_GetObjectItemCaseSensitive(root, "modbus");
|
|
if (modbus_node == nullptr) {
|
|
modbus_node = root;
|
|
}
|
|
const DaliValue modbus_value = FromCjson(modbus_node);
|
|
const auto parsed = GatewayModbusConfigFromValue(&modbus_value);
|
|
if (!parsed.has_value()) {
|
|
cJSON_Delete(root);
|
|
writeModbusManagementResponse(uart_port, false, action.c_str(), "invalid modbus config");
|
|
return;
|
|
}
|
|
const esp_err_t err = saveModbusConfig(parsed.value());
|
|
cJSON_Delete(root);
|
|
if (err != ESP_OK) {
|
|
writeModbusManagementResponse(
|
|
uart_port, false, action.c_str(),
|
|
modbus_last_error.empty() ? esp_err_to_name(err) : modbus_last_error.c_str());
|
|
return;
|
|
}
|
|
writeModbusManagementResponse(uart_port, true, action.c_str(), nullptr);
|
|
modbus_restart_requested = true;
|
|
modbus_stop_requested = true;
|
|
return;
|
|
}
|
|
if (action == "modbus_stop") {
|
|
cJSON_Delete(root);
|
|
writeModbusManagementResponse(uart_port, true, action.c_str(), nullptr);
|
|
modbus_stop_requested = true;
|
|
return;
|
|
}
|
|
if (action == "modbus_start" || action == "modbus_status") {
|
|
cJSON_Delete(root);
|
|
writeModbusManagementResponse(uart_port, true, action.c_str(), nullptr);
|
|
return;
|
|
}
|
|
cJSON_Delete(root);
|
|
writeModbusManagementResponse(uart_port, false, action.c_str(), "unknown action");
|
|
}
|
|
|
|
void writeModbusManagementResponse(int uart_port, const bool ok, const char* action,
|
|
const char* error) const {
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(root, "ok", ok);
|
|
cJSON_AddNumberToObject(root, "gw", channel.gateway_id);
|
|
cJSON_AddStringToObject(root, "action", action == nullptr ? "unknown" : action);
|
|
if (const auto config = activeModbusConfig()) {
|
|
cJSON_AddStringToObject(root, "transport", config->transport.c_str());
|
|
cJSON_AddNumberToObject(root, "unitID", config->unit_id);
|
|
if (GatewayModbusTransportIsSerial(config->transport)) {
|
|
cJSON_AddNumberToObject(root, "uartPort", config->serial.uart_port);
|
|
cJSON_AddNumberToObject(root, "baudrate", config->serial.baudrate);
|
|
} else {
|
|
cJSON_AddNumberToObject(root, "port", config->port);
|
|
}
|
|
}
|
|
if (error != nullptr) {
|
|
cJSON_AddStringToObject(root, "error", error);
|
|
}
|
|
const std::string body = "@DALIGW " + PrintJson(root) + "\n";
|
|
cJSON_Delete(root);
|
|
uart_write_bytes(static_cast<uart_port_t>(uart_port), body.data(), body.size());
|
|
}
|
|
|
|
};
|
|
|
|
GatewayBridgeService::GatewayBridgeService(DaliDomainService& dali_domain,
|
|
GatewayCache& cache,
|
|
GatewayBridgeServiceConfig config)
|
|
: dali_domain_(dali_domain), cache_(cache), config_(config) {}
|
|
|
|
GatewayBridgeService::~GatewayBridgeService() = default;
|
|
|
|
esp_err_t GatewayBridgeService::start() {
|
|
if (!config_.bridge_enabled) {
|
|
ESP_LOGI(kTag, "bridge service disabled");
|
|
return ESP_OK;
|
|
}
|
|
if (!runtimes_.empty()) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
const auto channels = dali_domain_.channelInfo();
|
|
runtimes_.reserve(channels.size());
|
|
for (const auto& channel : channels) {
|
|
auto runtime = std::make_unique<ChannelRuntime>(dali_domain_, cache_, channel, config_);
|
|
const esp_err_t err = runtime->start();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(kTag, "failed to start bridge runtime gateway=%u: %s", channel.gateway_id,
|
|
esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
runtimes_.push_back(std::move(runtime));
|
|
}
|
|
|
|
std::set<int> used_serial_uarts;
|
|
if (config_.modbus_enabled && config_.modbus_startup_enabled) {
|
|
std::set<uint16_t> used_modbus_ports;
|
|
for (const auto& runtime : runtimes_) {
|
|
const esp_err_t err = runtime->startModbus(&used_modbus_ports, &used_serial_uarts);
|
|
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
|
|
ESP_LOGW(kTag, "gateway=%u Modbus startup skipped: %s", runtime->channel.gateway_id,
|
|
esp_err_to_name(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (config_.knx_enabled && config_.knx_startup_enabled) {
|
|
std::set<uint16_t> used_knx_ports;
|
|
for (const auto& runtime : runtimes_) {
|
|
const esp_err_t err = runtime->startKnx(&used_knx_ports, &used_serial_uarts);
|
|
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
|
|
ESP_LOGW(kTag, "gateway=%u KNX/IP startup skipped: %s", runtime->channel.gateway_id,
|
|
esp_err_to_name(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (config_.bacnet_enabled && config_.bacnet_startup_enabled) {
|
|
for (const auto& runtime : runtimes_) {
|
|
const esp_err_t err = runtime->startBacnet();
|
|
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
|
|
ESP_LOGW(kTag, "gateway=%u BACnet startup skipped: %s", runtime->channel.gateway_id,
|
|
esp_err_to_name(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
ESP_LOGI(kTag, "bridge service started channels=%u", static_cast<unsigned>(runtimes_.size()));
|
|
return ESP_OK;
|
|
}
|
|
|
|
GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(uint8_t gateway_id) {
|
|
for (const auto& runtime : runtimes_) {
|
|
if (runtime->channel.gateway_id == gateway_id) {
|
|
return runtime.get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(
|
|
uint8_t gateway_id) const {
|
|
for (const auto& runtime : runtimes_) {
|
|
if (runtime->channel.gateway_id == gateway_id) {
|
|
return runtime.get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void GatewayBridgeService::collectUsedRuntimeResources(
|
|
uint8_t except_gateway_id,
|
|
std::set<uint16_t>* modbus_tcp_ports,
|
|
std::set<uint16_t>* knx_udp_ports,
|
|
std::set<int>* serial_uarts) const {
|
|
for (const auto& runtime : runtimes_) {
|
|
if (runtime->channel.gateway_id == except_gateway_id) {
|
|
continue;
|
|
}
|
|
LockGuard guard(runtime->lock);
|
|
if (runtime->modbus_started) {
|
|
const auto modbus_config = runtime->activeModbusConfigLocked();
|
|
if (modbus_config.has_value()) {
|
|
if (GatewayModbusTransportIsSerial(modbus_config->transport) && serial_uarts != nullptr) {
|
|
serial_uarts->insert(modbus_config->serial.uart_port);
|
|
} else if (GatewayModbusTransportIsTcp(modbus_config->transport) &&
|
|
modbus_tcp_ports != nullptr) {
|
|
const uint16_t port = modbus_config->port == 0 ? kGatewayModbusDefaultTcpPort
|
|
: modbus_config->port;
|
|
modbus_tcp_ports->insert(port);
|
|
}
|
|
}
|
|
}
|
|
if (runtime->knx_started ||
|
|
(runtime->knx_router != nullptr && runtime->knx_router->started())) {
|
|
const auto knx_config = runtime->activeKnxConfigLocked();
|
|
if (knx_config.has_value() && knx_config->ip_router_enabled) {
|
|
if (knx_udp_ports != nullptr) {
|
|
knx_udp_ports->insert(knx_config->udp_port);
|
|
}
|
|
if (serial_uarts != nullptr) {
|
|
serial_uarts->insert(knx_config->tp_uart.uart_port);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|
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");
|
|
}
|
|
std::string_view action(action_arg);
|
|
std::string_view query(query_arg);
|
|
std::optional<uint8_t> gateway_id;
|
|
if (gateway_id_arg >= 0 && gateway_id_arg <= 255) {
|
|
gateway_id = static_cast<uint8_t>(gateway_id_arg);
|
|
}
|
|
if (action.empty()) {
|
|
action = "status";
|
|
}
|
|
|
|
if (action == "status" && !gateway_id.has_value()) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(root, "enabled", true);
|
|
cJSON_AddNumberToObject(root, "count", static_cast<double>(runtimes_.size()));
|
|
cJSON* channels = cJSON_CreateArray();
|
|
if (channels != nullptr) {
|
|
for (const auto& runtime : runtimes_) {
|
|
cJSON_AddItemToArray(channels, runtime->statusCjson());
|
|
}
|
|
cJSON_AddItemToObject(root, "channels", channels);
|
|
}
|
|
return JsonOk(root);
|
|
}
|
|
|
|
if (!gateway_id.has_value()) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required");
|
|
}
|
|
auto* runtime = findRuntime(gateway_id.value());
|
|
if (runtime == nullptr) {
|
|
return ErrorResponse(ESP_ERR_NOT_FOUND, "unknown gateway id");
|
|
}
|
|
|
|
if (action == "status") {
|
|
return JsonOk(runtime->statusCjson());
|
|
}
|
|
if (action == "config") {
|
|
return runtime->configJson();
|
|
}
|
|
if (action == "modbus") {
|
|
return runtime->modbusBindingsJson();
|
|
}
|
|
if (action == "knx") {
|
|
return runtime->knxBindingsJson();
|
|
}
|
|
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();
|
|
}
|
|
if (action == "device") {
|
|
const auto address = QueryInt(query, "addr", "address");
|
|
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
|
}
|
|
{
|
|
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 == "base_status" || action == "status_bits") {
|
|
const auto address = QueryInt(query, "addr", "address");
|
|
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
|
}
|
|
return SnapshotResponse(dali_domain_.baseStatusSnapshot(gateway_id.value(), address.value()),
|
|
"base status snapshot is unavailable");
|
|
}
|
|
if (action == "dt1" || action == "dt4" || action == "dt5" || action == "dt6" ||
|
|
action == "dt8_status") {
|
|
const auto address = QueryInt(query, "addr", "address");
|
|
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
|
}
|
|
if (action == "dt1") {
|
|
return SnapshotResponse(dali_domain_.dt1Snapshot(gateway_id.value(), address.value()),
|
|
"DT1 snapshot is unavailable");
|
|
}
|
|
if (action == "dt4") {
|
|
return SnapshotResponse(dali_domain_.dt4Snapshot(gateway_id.value(), address.value()),
|
|
"DT4 snapshot is unavailable");
|
|
}
|
|
if (action == "dt5") {
|
|
return SnapshotResponse(dali_domain_.dt5Snapshot(gateway_id.value(), address.value()),
|
|
"DT5 snapshot is unavailable");
|
|
}
|
|
if (action == "dt6") {
|
|
return SnapshotResponse(dali_domain_.dt6Snapshot(gateway_id.value(), address.value()),
|
|
"DT6 snapshot is unavailable");
|
|
}
|
|
return SnapshotResponse(dali_domain_.dt8StatusSnapshot(gateway_id.value(), address.value()),
|
|
"DT8 status snapshot is unavailable");
|
|
}
|
|
if (action == "dt8_scene") {
|
|
const auto address = QueryInt(query, "addr", "address");
|
|
const auto scene = QueryInt(query, "scene");
|
|
if (!address.has_value() || !scene.has_value() || !ValidDaliAddress(address.value()) ||
|
|
scene.value() < 0 || scene.value() > 15) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr and scene are required");
|
|
}
|
|
return SnapshotResponse(dali_domain_.dt8SceneColorReport(gateway_id.value(), address.value(),
|
|
scene.value()),
|
|
"DT8 scene color report is unavailable");
|
|
}
|
|
if (action == "dt8_power_on") {
|
|
const auto address = QueryInt(query, "addr", "address");
|
|
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
|
}
|
|
return SnapshotResponse(dali_domain_.dt8PowerOnLevelColorReport(gateway_id.value(),
|
|
address.value()),
|
|
"DT8 power-on color report is unavailable");
|
|
}
|
|
if (action == "dt8_system_failure") {
|
|
const auto address = QueryInt(query, "addr", "address");
|
|
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
|
}
|
|
return SnapshotResponse(dali_domain_.dt8SystemFailureLevelColorReport(gateway_id.value(),
|
|
address.value()),
|
|
"DT8 system-failure color report is unavailable");
|
|
}
|
|
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge GET action");
|
|
}
|
|
|
|
GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
|
const std::string& action_arg, int gateway_id_arg, const std::string& body_arg) {
|
|
if (!config_.bridge_enabled) {
|
|
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "bridge service is disabled");
|
|
}
|
|
|
|
std::string_view action(action_arg);
|
|
std::string_view body(body_arg);
|
|
std::optional<uint8_t> gateway_id;
|
|
if (gateway_id_arg >= 0 && gateway_id_arg <= 255) {
|
|
gateway_id = static_cast<uint8_t>(gateway_id_arg);
|
|
}
|
|
|
|
cJSON* root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
|
if (!body.empty() && root == nullptr) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid JSON body");
|
|
}
|
|
if (action.empty() && root != nullptr) {
|
|
if (const char* body_action = JsonString(root, "action")) {
|
|
action = body_action;
|
|
}
|
|
}
|
|
if (!gateway_id.has_value()) {
|
|
gateway_id = JsonGatewayId(root);
|
|
}
|
|
cJSON_Delete(root);
|
|
|
|
if (action.empty()) {
|
|
action = "execute";
|
|
}
|
|
if (!gateway_id.has_value()) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required");
|
|
}
|
|
|
|
auto* runtime = findRuntime(gateway_id.value());
|
|
if (runtime == nullptr) {
|
|
return ErrorResponse(ESP_ERR_NOT_FOUND, "unknown gateway id");
|
|
}
|
|
|
|
if (action == "dt8_scene_snapshot" || action == "store_dt8_scene_snapshot") {
|
|
return StoreDt8SceneSnapshot(dali_domain_, gateway_id.value(), body);
|
|
}
|
|
if (action == "dt8_power_on_snapshot" || action == "store_dt8_power_on_snapshot") {
|
|
return StoreDt8LevelSnapshot(dali_domain_, gateway_id.value(), body, true);
|
|
}
|
|
if (action == "dt8_system_failure_snapshot" ||
|
|
action == "store_dt8_system_failure_snapshot") {
|
|
return StoreDt8LevelSnapshot(dali_domain_, gateway_id.value(), body, false);
|
|
}
|
|
|
|
if (action == "execute") {
|
|
return runtime->execute(body);
|
|
}
|
|
if (action == "config" || action == "save_config") {
|
|
const esp_err_t err = runtime->saveBridgeConfig(body);
|
|
if (err != ESP_OK) {
|
|
const char* message = !runtime->knx_last_error.empty()
|
|
? runtime->knx_last_error.c_str()
|
|
: (!runtime->modbus_last_error.empty()
|
|
? runtime->modbus_last_error.c_str()
|
|
: "failed to save bridge config");
|
|
return ErrorResponse(err, message);
|
|
}
|
|
return handleGet("config", gateway_id.value());
|
|
}
|
|
if (action == "clear_config") {
|
|
const esp_err_t err = runtime->clearBridgeConfig();
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, "failed to clear bridge config");
|
|
}
|
|
return handleGet("config", gateway_id.value());
|
|
}
|
|
if (action == "cloud" || action == "save_cloud") {
|
|
const esp_err_t err = runtime->saveCloudConfig(body);
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, "failed to save cloud config");
|
|
}
|
|
return handleGet("cloud", gateway_id.value());
|
|
}
|
|
if (action == "cloud_start") {
|
|
const esp_err_t err = runtime->startCloud();
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, "failed to start cloud bridge");
|
|
}
|
|
return handleGet("cloud", gateway_id.value());
|
|
}
|
|
if (action == "cloud_stop") {
|
|
const esp_err_t err = runtime->stopCloud();
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, "failed to stop cloud bridge");
|
|
}
|
|
return handleGet("cloud", gateway_id.value());
|
|
}
|
|
if (action == "cloud_clear") {
|
|
const esp_err_t err = runtime->clearCloudConfig();
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, "failed to clear cloud config");
|
|
}
|
|
return handleGet("cloud", gateway_id.value());
|
|
}
|
|
if (action == "modbus_start") {
|
|
std::set<uint16_t> used_modbus_ports;
|
|
std::set<int> used_serial_uarts;
|
|
collectUsedRuntimeResources(gateway_id.value(), &used_modbus_ports, nullptr,
|
|
&used_serial_uarts);
|
|
const esp_err_t err = runtime->startModbus(&used_modbus_ports, &used_serial_uarts);
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, runtime->modbus_last_error.empty()
|
|
? "failed to start Modbus bridge"
|
|
: runtime->modbus_last_error.c_str());
|
|
}
|
|
return handleGet("modbus", gateway_id.value());
|
|
}
|
|
if (action == "modbus_stop") {
|
|
const esp_err_t err = runtime->stopModbus();
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, "failed to stop Modbus bridge");
|
|
}
|
|
return handleGet("modbus", gateway_id.value());
|
|
}
|
|
if (action == "knx_config" || action == "save_knx") {
|
|
cJSON* knx_root = cJSON_ParseWithLength(body.data(), body.size());
|
|
if (knx_root == nullptr) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid KNX config JSON");
|
|
}
|
|
const cJSON* knx_node = cJSON_GetObjectItemCaseSensitive(knx_root, "knx");
|
|
if (knx_node == nullptr) {
|
|
knx_node = knx_root;
|
|
}
|
|
const DaliValue knx_value = FromCjson(knx_node);
|
|
cJSON_Delete(knx_root);
|
|
const auto parsed = GatewayKnxConfigFromValue(&knx_value);
|
|
if (!parsed.has_value()) {
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid KNX config");
|
|
}
|
|
std::set<uint16_t> used_knx_ports;
|
|
std::set<int> used_serial_uarts;
|
|
collectUsedRuntimeResources(gateway_id.value(), nullptr, &used_knx_ports,
|
|
&used_serial_uarts);
|
|
const esp_err_t err = runtime->saveKnxConfig(parsed.value(), &used_knx_ports,
|
|
&used_serial_uarts);
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, runtime->knx_last_error.empty()
|
|
? "failed to save KNX bridge config"
|
|
: runtime->knx_last_error.c_str());
|
|
}
|
|
return handleGet("knx", gateway_id.value());
|
|
}
|
|
if (action == "knx_start") {
|
|
std::set<uint16_t> used_knx_ports;
|
|
std::set<int> used_serial_uarts;
|
|
collectUsedRuntimeResources(gateway_id.value(), nullptr, &used_knx_ports,
|
|
&used_serial_uarts);
|
|
const esp_err_t err = runtime->startKnx(&used_knx_ports, &used_serial_uarts);
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, runtime->knx_last_error.empty()
|
|
? "failed to start KNX/IP bridge"
|
|
: runtime->knx_last_error.c_str());
|
|
}
|
|
return handleGet("knx", gateway_id.value());
|
|
}
|
|
if (action == "knx_stop") {
|
|
const esp_err_t err = runtime->stopKnx();
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, "failed to stop KNX/IP bridge");
|
|
}
|
|
return handleGet("knx", gateway_id.value());
|
|
}
|
|
if (action == "bacnet_start") {
|
|
const esp_err_t err = runtime->startBacnet();
|
|
if (err != ESP_OK) {
|
|
return ErrorResponse(err, "failed to start BACnet/IP bridge");
|
|
}
|
|
return handleGet("bacnet", gateway_id.value());
|
|
}
|
|
if (action == "scan") {
|
|
return runtime->scanInventory(body);
|
|
}
|
|
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge POST action");
|
|
}
|
|
|
|
} // namespace gateway
|