From 226855362b2cd5cbae603b438411bf57991ac522 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 19 May 2026 05:52:36 +0800 Subject: [PATCH] feat(gateway): integrate bridge service into gateway controller for transport handling Signed-off-by: Tony --- apps/gateway/main/app_main.cpp | 1 + .../gateway_bridge/include/gateway_bridge.hpp | 1 + .../gateway_bridge/src/gateway_bridge.cpp | 355 ++++++++++++------ components/gateway_controller/CMakeLists.txt | 2 +- .../include/gateway_controller.hpp | 14 + .../src/gateway_controller.cpp | 130 +++++++ 6 files changed, 391 insertions(+), 112 deletions(-) diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 3288c60..e94beb7 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -926,6 +926,7 @@ extern "C" void app_main(void) { static_cast(CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY); s_bridge = std::make_unique(*s_dali_domain, *s_cache, bridge_config); + s_controller->setBridgeService(s_bridge.get()); } if (profile.enable_wifi || profile.enable_eth) { diff --git a/components/gateway_bridge/include/gateway_bridge.hpp b/components/gateway_bridge/include/gateway_bridge.hpp index eaca6c1..75c39db 100644 --- a/components/gateway_bridge/include/gateway_bridge.hpp +++ b/components/gateway_bridge/include/gateway_bridge.hpp @@ -61,6 +61,7 @@ class GatewayBridgeService { const std::string& query = {}); GatewayBridgeHttpResponse handlePost(const std::string& action, int gateway_id, const std::string& body); + std::string handleTransportRequest(uint8_t gateway_id, std::string_view request); private: struct ChannelRuntime; diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 8932bc3..3ac31ff 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -257,6 +257,66 @@ std::optional JsonGatewayId(const cJSON* root) { return static_cast(gateway); } +int BridgeTransportStatusCode(esp_err_t err) { + if (err == ESP_OK) { + return 200; + } + if (err == ESP_ERR_INVALID_ARG) { + return 400; + } + if (err == ESP_ERR_NOT_FOUND) { + return 404; + } + return 500; +} + +std::string BuildBridgeTransportEnvelope(const GatewayBridgeHttpResponse& response) { + cJSON* root = cJSON_CreateObject(); + if (root == nullptr) { + return "{}"; + } + + cJSON_AddNumberToObject(root, "statusCode", + static_cast(BridgeTransportStatusCode(response.err))); + if (response.err == ESP_OK) { + cJSON* data = response.body.empty() + ? cJSON_CreateObject() + : cJSON_ParseWithLength(response.body.data(), response.body.size()); + if (data == nullptr) { + cJSON_AddNumberToObject(root, "statusCode", 500); + cJSON_AddStringToObject(root, "error", "bridge response is not valid JSON"); + cJSON_AddStringToObject(root, "message", "bridge response is not valid JSON"); + } else { + cJSON_AddItemToObject(root, "data", data); + } + const std::string body = PrintJson(root); + cJSON_Delete(root); + return body; + } + + const char* message = "bridge request failed"; + cJSON* details = response.body.empty() + ? nullptr + : cJSON_ParseWithLength(response.body.data(), response.body.size()); + if (details != nullptr && cJSON_IsObject(details)) { + if (const char* error = JsonString(details, "error")) { + message = error; + } + cJSON_AddItemToObject(root, "details", details); + details = nullptr; + } else { + cJSON_Delete(details); + if (!response.body.empty()) { + message = response.body.c_str(); + } + } + cJSON_AddStringToObject(root, "error", message); + cJSON_AddStringToObject(root, "message", message); + const std::string body = PrintJson(root); + cJSON_Delete(root); + return body; +} + std::string QueryValue(std::string_view query, std::string_view key) { if (query.empty() || key.empty()) { return {}; @@ -2131,6 +2191,125 @@ struct GatewayBridgeService::ChannelRuntime { return JsonOk(BridgeResultToCjson(result)); } + cJSON* knxStatusCjson() const { + cJSON* knx_json = cJSON_CreateObject(); + if (knx_json == nullptr) { + return nullptr; + } + auto* endpoint_runtime = service.knx_endpoint_runtime_; + if (endpoint_runtime == nullptr) { + endpoint_runtime = const_cast(service).selectKnxEndpointRuntime(); + } + bool programming_mode = false; + bool programming_control_available = false; + int endpoint_owner_gateway_id = -1; + if (endpoint_runtime != nullptr) { + LockGuard owner_guard(endpoint_runtime->lock); + endpoint_owner_gateway_id = endpoint_runtime->channel.gateway_id; + programming_control_available = endpoint_runtime->knx_router != nullptr && + endpoint_runtime->knx_router->started(); + if (programming_control_available) { + programming_mode = endpoint_runtime->knx_router->programmingMode(); + } + } + 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()); + cJSON_AddBoolToObject(knx_json, "programmingMode", programming_mode); + cJSON_AddBoolToObject(knx_json, "programmingControlAvailable", + programming_control_available); + cJSON_AddBoolToObject(knx_json, "endpointOwner", + endpoint_owner_gateway_id == channel.gateway_id); + if (endpoint_owner_gateway_id >= 0) { + cJSON_AddNumberToObject(knx_json, "endpointOwnerGatewayId", + endpoint_owner_gateway_id); + } + 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()); + cJSON* security_json = cJSON_CreateObject(); + if (security_json != nullptr) { +#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED) + cJSON_AddBoolToObject(security_json, "dataSecureCompiled", true); +#else + cJSON_AddBoolToObject(security_json, "dataSecureCompiled", false); +#endif +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", true); +#else + cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", false); +#endif +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", true); +#else + cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", false); +#endif + cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false); +#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) + cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true); +#else + cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", false); +#endif +#if defined(CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS) + cJSON_AddBoolToObject(security_json, "plainNvsStorage", true); + cJSON_AddStringToObject(security_json, "storage", "plain_nvs_development"); +#else + cJSON_AddBoolToObject(security_json, "plainNvsStorage", false); + cJSON_AddStringToObject(security_json, "storage", "none"); +#endif +#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED) + const auto fdsk_info = openknx::LoadFactoryFdskInfo(); + cJSON* fdsk_json = FactoryFdskInfoToCjson(fdsk_info, true); + if (fdsk_json != nullptr) { + cJSON_AddItemToObject(security_json, "factorySetupKey", fdsk_json); + } + cJSON* certificate_json = + FactoryCertificateToCjson(openknx::BuildFactoryCertificatePayload(), false); + if (certificate_json != nullptr) { + cJSON_AddItemToObject(security_json, "factoryCertificate", certificate_json); + } + cJSON* failures_json = SecurityFailuresToCjson(); + if (failures_json != nullptr) { + cJSON_AddItemToObject(security_json, "failures", failures_json); + } +#endif + cJSON_AddItemToObject(knx_json, "security", security_json); + } + 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_AddBoolToObject(knx_json, "etsDatabaseEnabled", effective_knx->ets_database_enabled); + cJSON_AddNumberToObject(knx_json, "etsBindingCount", + knx == nullptr ? 0 : knx->etsBindingCount()); + cJSON_AddStringToObject(knx_json, "mappingMode", + GatewayKnxMappingModeToString(effective_knx->mapping_mode)); + 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, "ipInterfaceIndividualAddress", + effective_knx->ip_interface_individual_address); + 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_AddBoolToObject(serial_json, "nineBitMode", effective_knx->tp_uart.nine_bit_mode); + cJSON_AddItemToObject(knx_json, "tpUart", serial_json); + } + } + return knx_json; + } + cJSON* statusCjson() const { cJSON* root = cJSON_CreateObject(); if (root == nullptr) { @@ -2202,118 +2381,8 @@ struct GatewayBridgeService::ChannelRuntime { cJSON_AddItemToObject(root, "bacnet", bacnet_json); } - cJSON* knx_json = cJSON_CreateObject(); + cJSON* knx_json = knxStatusCjson(); if (knx_json != nullptr) { - auto* endpoint_runtime = service.knx_endpoint_runtime_; - if (endpoint_runtime == nullptr) { - endpoint_runtime = const_cast(service).selectKnxEndpointRuntime(); - } - bool programming_mode = false; - bool programming_control_available = false; - int endpoint_owner_gateway_id = -1; - if (endpoint_runtime != nullptr) { - LockGuard owner_guard(endpoint_runtime->lock); - endpoint_owner_gateway_id = endpoint_runtime->channel.gateway_id; - programming_control_available = endpoint_runtime->knx_router != nullptr && - endpoint_runtime->knx_router->started(); - if (programming_control_available) { - programming_mode = endpoint_runtime->knx_router->programmingMode(); - } - } - 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()); - cJSON_AddBoolToObject(knx_json, "programmingMode", programming_mode); - cJSON_AddBoolToObject(knx_json, "programmingControlAvailable", - programming_control_available); - cJSON_AddBoolToObject(knx_json, "endpointOwner", - endpoint_owner_gateway_id == channel.gateway_id); - if (endpoint_owner_gateway_id >= 0) { - cJSON_AddNumberToObject(knx_json, "endpointOwnerGatewayId", - endpoint_owner_gateway_id); - } - 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()); - cJSON* security_json = cJSON_CreateObject(); - if (security_json != nullptr) { -#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED) - cJSON_AddBoolToObject(security_json, "dataSecureCompiled", true); -#else - cJSON_AddBoolToObject(security_json, "dataSecureCompiled", false); -#endif -#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) - cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", true); -#else - cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", false); -#endif -#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) - cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", true); -#else - cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", false); -#endif - cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false); -#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) - cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true); -#else - cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", false); -#endif -#if defined(CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS) - cJSON_AddBoolToObject(security_json, "plainNvsStorage", true); - cJSON_AddStringToObject(security_json, "storage", "plain_nvs_development"); -#else - cJSON_AddBoolToObject(security_json, "plainNvsStorage", false); - cJSON_AddStringToObject(security_json, "storage", "none"); -#endif -#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED) - const auto fdsk_info = openknx::LoadFactoryFdskInfo(); - cJSON* fdsk_json = FactoryFdskInfoToCjson(fdsk_info, false); - if (fdsk_json != nullptr) { - cJSON_AddItemToObject(security_json, "factorySetupKey", fdsk_json); - } - cJSON* certificate_json = FactoryCertificateToCjson( - openknx::BuildFactoryCertificatePayload(), false); - if (certificate_json != nullptr) { - cJSON_AddItemToObject(security_json, "factoryCertificate", certificate_json); - } - cJSON* failures_json = SecurityFailuresToCjson(); - if (failures_json != nullptr) { - cJSON_AddItemToObject(security_json, "failures", failures_json); - } -#endif - cJSON_AddItemToObject(knx_json, "security", security_json); - } - 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_AddBoolToObject(knx_json, "etsDatabaseEnabled", effective_knx->ets_database_enabled); - cJSON_AddNumberToObject(knx_json, "etsBindingCount", - knx == nullptr ? 0 : knx->etsBindingCount()); - cJSON_AddStringToObject(knx_json, "mappingMode", - GatewayKnxMappingModeToString(effective_knx->mapping_mode)); - 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, "ipInterfaceIndividualAddress", - effective_knx->ip_interface_individual_address); - 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_AddBoolToObject(serial_json, "nineBitMode", effective_knx->tp_uart.nine_bit_mode); - cJSON_AddItemToObject(knx_json, "tpUart", serial_json); - } - } cJSON_AddItemToObject(root, "knx", knx_json); } @@ -2333,6 +2402,21 @@ struct GatewayBridgeService::ChannelRuntime { return root; } + GatewayBridgeHttpResponse knxStatusJson() const { + cJSON* root = cJSON_CreateObject(); + if (root == nullptr) { + return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate KNX status JSON"); + } + cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id); + cJSON* knx_json = knxStatusCjson(); + if (knx_json == nullptr) { + cJSON_Delete(root); + return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate KNX status JSON"); + } + cJSON_AddItemToObject(root, "knx", knx_json); + return JsonOk(root); + } + GatewayBridgeHttpResponse configJson() const { return GatewayBridgeHttpResponse{ESP_OK, GatewayBridgeStoredConfigToJson(bridge_config, modbus_config, @@ -4199,6 +4283,9 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet( if (action == "status") { return JsonOk(runtime->statusCjson()); } + if (action == "knx_status") { + return runtime->knxStatusJson(); + } if (action == "config") { return runtime->configJson(); } @@ -4718,4 +4805,50 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost( return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge POST action"); } +std::string GatewayBridgeService::handleTransportRequest(uint8_t gateway_id, + std::string_view request) { + cJSON* root = request.empty() ? nullptr : cJSON_ParseWithLength(request.data(), request.size()); + if (root == nullptr || !cJSON_IsObject(root)) { + cJSON_Delete(root); + return BuildBridgeTransportEnvelope( + ErrorResponse(ESP_ERR_INVALID_ARG, "invalid bridge request JSON")); + } + + const char* action_raw = JsonString(root, "action"); + const char* method_raw = JsonString(root, "method"); + if (action_raw == nullptr || method_raw == nullptr) { + cJSON_Delete(root); + return BuildBridgeTransportEnvelope( + ErrorResponse(ESP_ERR_INVALID_ARG, "bridge request requires action and method")); + } + + const auto request_gateway_id = JsonGatewayId(root); + if (request_gateway_id.has_value() && request_gateway_id.value() != gateway_id) { + cJSON_Delete(root); + return BuildBridgeTransportEnvelope( + ErrorResponse(ESP_ERR_INVALID_ARG, "bridge request gateway id mismatch")); + } + + std::string method(method_raw); + std::transform(method.begin(), method.end(), method.begin(), + [](unsigned char ch) { return static_cast(std::toupper(ch)); }); + + GatewayBridgeHttpResponse response; + if (method == "GET") { + const char* query = JsonString(root, "query"); + response = handleGet(action_raw, gateway_id, + query == nullptr ? std::string() : std::string(query)); + } else if (method == "POST") { + const cJSON* body_node = cJSON_GetObjectItemCaseSensitive(root, "body"); + response = handlePost(action_raw, gateway_id, + body_node == nullptr ? std::string("{}") + : PrintJson(const_cast(body_node))); + } else { + response = ErrorResponse(ESP_ERR_INVALID_ARG, "unsupported bridge request method"); + } + + cJSON_Delete(root); + return BuildBridgeTransportEnvelope(response); +} + } // namespace gateway diff --git a/components/gateway_controller/CMakeLists.txt b/components/gateway_controller/CMakeLists.txt index f27fe1b..f93d3b9 100644 --- a/components/gateway_controller/CMakeLists.txt +++ b/components/gateway_controller/CMakeLists.txt @@ -1,7 +1,7 @@ idf_component_register( SRCS "src/gateway_controller.cpp" INCLUDE_DIRS "include" - REQUIRES dali_domain gateway_runtime gateway_cache freertos log + REQUIRES dali_domain gateway_runtime gateway_cache gateway_bridge freertos log ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index f55807b..2020fef 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -19,6 +19,7 @@ namespace gateway { class DaliDomainService; struct DaliRawFrame; +class GatewayBridgeService; class GatewayRuntime; struct GatewayControllerConfig { @@ -76,6 +77,7 @@ class GatewayController { void addBleStateSink(BleStateSink sink); void addWifiStateSink(WifiStateSink sink); void addGatewayNameSink(GatewayNameSink sink); + void setBridgeService(GatewayBridgeService* bridge_service); bool setupMode() const; bool wirelessSetupMode() const; @@ -100,6 +102,13 @@ class GatewayController { uint8_t scene_id{0}; }; + struct BridgeTransportRequestState { + uint8_t version{0}; + uint16_t payload_length{0}; + uint8_t total_chunks{0}; + std::map> chunks; + }; + static void TaskEntry(void* arg); void taskLoop(); void dispatchCommand(const std::vector& command); @@ -156,10 +165,14 @@ class GatewayController { void handleAllocationCommand(uint8_t gateway_id, const std::vector& command); void handleInternalSceneCommand(uint8_t gateway_id, const std::vector& command); void handleInternalGroupCommand(uint8_t gateway_id, const std::vector& command); + void handleBridgeTransportCommand(uint8_t gateway_id, const std::vector& command); + void publishBridgeTransportResponse(uint8_t gateway_id, uint8_t version, uint8_t sequence, + std::string_view response); GatewayRuntime& runtime_; DaliDomainService& dali_domain_; GatewayCache& cache_; + GatewayBridgeService* bridge_service_{nullptr}; GatewayControllerConfig config_; TaskHandle_t task_handle_{nullptr}; SemaphoreHandle_t maintenance_lock_{nullptr}; @@ -167,6 +180,7 @@ class GatewayController { std::vector ble_state_sinks_; std::vector wifi_state_sinks_; std::vector gateway_name_sinks_; + std::map bridge_transport_requests_; std::map reconciliation_jobs_; std::atomic maintenance_activity_gateway_{-1}; bool setup_mode_{false}; diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index 19edb33..86e4ae7 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -3,6 +3,7 @@ #include "dali_domain.hpp" #include "esp_log.h" #include "esp_system.h" +#include "gateway_bridge.hpp" #include "gateway_runtime.hpp" #include @@ -21,6 +22,16 @@ constexpr uint8_t kDaliSceneCount = 16; constexpr uint8_t kDaliCmdOff = 0x00; constexpr uint8_t kDaliCmdRecallMax = 0x05; constexpr TickType_t kMaintenancePollTicks = pdMS_TO_TICKS(20); +constexpr uint8_t kBridgeTransportRequestOpcode = 0xB0; +constexpr uint8_t kBridgeTransportResponseOpcode = 0xB1; +constexpr uint8_t kBridgeTransportVersion = 1; +constexpr size_t kBridgeTransportMaxChunkBytes = 120; +constexpr const char* kBridgeTransportInvalidFrameResponse = + "{\"statusCode\":400,\"error\":\"invalid bridge transport frame\"," + "\"message\":\"invalid bridge transport frame\"}"; +constexpr const char* kBridgeTransportUnavailableResponse = + "{\"statusCode\":500,\"error\":\"bridge service is not enabled\"," + "\"message\":\"bridge service is not enabled\"}"; class LockGuard { public: @@ -73,6 +84,10 @@ void AppendStringBytes(std::vector& out, std::string_view value) { } } +uint16_t BridgeTransportRequestKey(uint8_t gateway_id, uint8_t sequence) { + return static_cast((static_cast(gateway_id) << 8) | sequence); +} + void AppendPaddedName(std::vector& out, std::string_view name) { const auto normalized = NormalizeName(name); out.push_back(static_cast(normalized.size())); @@ -185,6 +200,10 @@ void GatewayController::addGatewayNameSink(GatewayNameSink sink) { } } +void GatewayController::setBridgeService(GatewayBridgeService* bridge_service) { + bridge_service_ = bridge_service; +} + bool GatewayController::setupMode() const { return setup_mode_; } @@ -659,6 +678,9 @@ void GatewayController::dispatchCommand(const std::vector& command) { case 0xA2: handleInternalGroupCommand(gateway_id, command); break; + case kBridgeTransportRequestOpcode: + handleBridgeTransportCommand(gateway_id, command); + break; default: ESP_LOGW(kTag, "unhandled opcode=0x%02x gateway=%u", opcode, gateway_id); break; @@ -714,12 +736,120 @@ void GatewayController::publishPayload(uint8_t, const std::vector& payl publishFrame(GatewayRuntime::buildNotificationFrame(payload)); } +void GatewayController::publishBridgeTransportResponse(uint8_t gateway_id, uint8_t version, + uint8_t sequence, + std::string_view response) { + const size_t total_chunks = + std::max(1, (response.size() + kBridgeTransportMaxChunkBytes - 1) / + kBridgeTransportMaxChunkBytes); + for (size_t index = 0; index < total_chunks; ++index) { + const size_t start = index * kBridgeTransportMaxChunkBytes; + const size_t chunk_length = + std::min(kBridgeTransportMaxChunkBytes, response.size() - start); + std::vector payload{ + kBridgeTransportResponseOpcode, + gateway_id, + version, + sequence, + static_cast(total_chunks), + static_cast(index), + static_cast(response.size() & 0xFF), + static_cast((response.size() >> 8) & 0xFF), + static_cast(chunk_length & 0xFF), + static_cast((chunk_length >> 8) & 0xFF), + }; + payload.reserve(payload.size() + chunk_length); + for (size_t offset = 0; offset < chunk_length; ++offset) { + payload.push_back(static_cast(response[start + offset])); + } + publishPayload(gateway_id, payload); + } +} + void GatewayController::publishFrame(const std::vector& frame) { for (const auto& sink : notification_sinks_) { sink(frame); } } +void GatewayController::handleBridgeTransportCommand(uint8_t gateway_id, + const std::vector& command) { + const uint8_t version = command.size() > 4 ? command[4] : kBridgeTransportVersion; + const uint8_t sequence = command.size() > 5 ? command[5] : 0; + const uint16_t request_key = BridgeTransportRequestKey(gateway_id, sequence); + if (command.size() < 11) { + bridge_transport_requests_.erase(request_key); + publishBridgeTransportResponse(gateway_id, version, sequence, + kBridgeTransportInvalidFrameResponse); + return; + } + + const uint8_t total_chunks = command[6]; + const uint8_t chunk_index = command[7]; + const uint16_t payload_length = + static_cast(command[8] | (static_cast(command[9]) << 8)); + if (version != kBridgeTransportVersion || total_chunks == 0 || chunk_index >= total_chunks) { + bridge_transport_requests_.erase(request_key); + publishBridgeTransportResponse(gateway_id, version, sequence, + kBridgeTransportInvalidFrameResponse); + return; + } + + auto& state = bridge_transport_requests_[request_key]; + if (chunk_index == 0 || state.version != version || state.payload_length != payload_length || + state.total_chunks != total_chunks) { + state = BridgeTransportRequestState{}; + state.version = version; + state.payload_length = payload_length; + state.total_chunks = total_chunks; + } + + const size_t payload_start = 10; + const size_t payload_end = command.size() - 1; + if (payload_end < payload_start) { + bridge_transport_requests_.erase(request_key); + publishBridgeTransportResponse(gateway_id, version, sequence, + kBridgeTransportInvalidFrameResponse); + return; + } + + if (state.chunks.find(chunk_index) == state.chunks.end()) { + state.chunks[chunk_index] = + std::vector(command.begin() + payload_start, command.begin() + payload_end); + } + if (state.chunks.size() < total_chunks) { + return; + } + + std::vector request_bytes; + request_bytes.reserve(payload_length); + for (uint8_t index = 0; index < total_chunks; ++index) { + const auto it = state.chunks.find(index); + if (it == state.chunks.end()) { + bridge_transport_requests_.erase(request_key); + publishBridgeTransportResponse(gateway_id, version, sequence, + kBridgeTransportInvalidFrameResponse); + return; + } + request_bytes.insert(request_bytes.end(), it->second.begin(), it->second.end()); + } + bridge_transport_requests_.erase(request_key); + if (request_bytes.size() != payload_length) { + publishBridgeTransportResponse(gateway_id, version, sequence, + kBridgeTransportInvalidFrameResponse); + return; + } + + const std::string response = + bridge_service_ == nullptr + ? std::string(kBridgeTransportUnavailableResponse) + : bridge_service_->handleTransportRequest( + gateway_id, + std::string_view(reinterpret_cast(request_bytes.data()), + request_bytes.size())); + publishBridgeTransportResponse(gateway_id, version, sequence, response); +} + void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) { if (frame.data.size() != 2 && frame.data.size() != 3) { return;