diff --git a/README.md b/README.md index 868ddec..b26066d 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway. - `dali_domain/`: native DALI domain facade over `dali_cpp`. - `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`. - `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out. + - `gateway_network/`: initial HTTP `/info` and `/dali/cmd` plus UDP port `2020` control-plane ingress for the native gateway. - `gateway_runtime/`: persistent runtime state, command queueing, and device info services. ## Current status -The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app now also includes an initial `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications plus incoming `FFF1`/`FFF2`/`FFF3` writes into the native controller and DALI domain. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. \ No newline at end of file +The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app now also includes an initial `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications plus incoming `FFF1`/`FFF2`/`FFF3` writes into the native controller and DALI domain, and an initial `gateway_network` service that starts the native HTTP `/info` and `POST /dali/cmd` surfaces plus the UDP control-plane router on port `2020`. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. \ No newline at end of file diff --git a/apps/gateway/main/CMakeLists.txt b/apps/gateway/main/CMakeLists.txt index d130ac1..a95d1e4 100644 --- a/apps/gateway/main/CMakeLists.txt +++ b/apps/gateway/main/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( SRCS "app_main.cpp" - REQUIRES gateway_core gateway_controller dali_domain gateway_runtime gateway_ble log + REQUIRES gateway_core gateway_controller gateway_network dali_domain gateway_runtime gateway_ble log ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index 73b8328..3e19032 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -98,6 +98,18 @@ config GATEWAY_CHANNEL1_SERIAL_RX_BUFFER range 128 4096 default 512 +config GATEWAY_CHANNEL1_SERIAL_TX_BUFFER + int "Serial PHY TX buffer bytes" + depends on GATEWAY_CHANNEL1_PHY_UART1 || GATEWAY_CHANNEL1_PHY_UART2 + range 0 4096 + default 512 + +config GATEWAY_CHANNEL1_SERIAL_QUERY_TIMEOUT_MS + int "Serial PHY query timeout ms" + depends on GATEWAY_CHANNEL1_PHY_UART1 || GATEWAY_CHANNEL1_PHY_UART2 + range 10 5000 + default 500 + endmenu menu "Gateway Channel 2" @@ -193,6 +205,18 @@ config GATEWAY_CHANNEL2_SERIAL_RX_BUFFER range 128 4096 default 512 +config GATEWAY_CHANNEL2_SERIAL_TX_BUFFER + int "Serial PHY TX buffer bytes" + depends on GATEWAY_CHANNEL_COUNT >= 2 && (GATEWAY_CHANNEL2_PHY_UART1 || GATEWAY_CHANNEL2_PHY_UART2) + range 0 4096 + default 512 + +config GATEWAY_CHANNEL2_SERIAL_QUERY_TIMEOUT_MS + int "Serial PHY query timeout ms" + depends on GATEWAY_CHANNEL_COUNT >= 2 && (GATEWAY_CHANNEL2_PHY_UART1 || GATEWAY_CHANNEL2_PHY_UART2) + range 10 5000 + default 500 + endmenu config GATEWAY_ENABLE_DALI_BUS diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index c9d3d39..ed8b53f 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -2,11 +2,14 @@ #include "gateway_ble.hpp" #include "gateway_controller.hpp" #include "gateway_core.hpp" +#include "gateway_network.hpp" #include "gateway_runtime.hpp" +#include "esp_log.h" #include "sdkconfig.h" #include +#include #include #ifndef CONFIG_GATEWAY_CHANNEL_COUNT @@ -16,10 +19,12 @@ namespace { constexpr const char* kProjectName = "DALI_485_Gateway"; constexpr const char* kProjectVersion = "0.1.0"; +constexpr const char* kTag = "gateway_main"; std::unique_ptr s_dali_domain; std::unique_ptr s_runtime; std::unique_ptr s_controller; +std::unique_ptr s_network; std::unique_ptr s_ble_bridge; [[maybe_unused]] void LogBindError(const char* channel_name, esp_err_t err) { @@ -28,8 +33,111 @@ std::unique_ptr s_ble_bridge; } } -void BindConfiguredChannels(gateway::DaliDomainService& dali_domain, - const gateway::GatewayRuntime& runtime) { +struct ChannelBindingConfig { + bool enabled{false}; + bool native_phy{false}; + bool serial_phy{false}; + uint8_t gateway_id{0}; + uint8_t native_bus_id{0}; + uint32_t native_baudrate{0}; + int uart_port{-1}; +}; + +bool ValidateChannelBindings() { + ChannelBindingConfig channels[CONFIG_GATEWAY_CHANNEL_COUNT] = {}; + +#if CONFIG_GATEWAY_CHANNEL1_PHY_NATIVE + channels[0].enabled = true; + channels[0].native_phy = true; + channels[0].gateway_id = static_cast(CONFIG_GATEWAY_CHANNEL1_GW_ID); + channels[0].native_bus_id = static_cast(CONFIG_GATEWAY_CHANNEL1_NATIVE_BUS_ID); + channels[0].native_baudrate = static_cast(CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE); +#elif CONFIG_GATEWAY_CHANNEL1_PHY_UART1 + channels[0].enabled = true; + channels[0].serial_phy = true; + channels[0].gateway_id = static_cast(CONFIG_GATEWAY_CHANNEL1_GW_ID); + channels[0].uart_port = 1; +#elif CONFIG_GATEWAY_CHANNEL1_PHY_UART2 + channels[0].enabled = true; + channels[0].serial_phy = true; + channels[0].gateway_id = static_cast(CONFIG_GATEWAY_CHANNEL1_GW_ID); + channels[0].uart_port = 2; +#endif + +#if CONFIG_GATEWAY_CHANNEL_COUNT >= 2 +#if CONFIG_GATEWAY_CHANNEL2_PHY_NATIVE + channels[1].enabled = true; + channels[1].native_phy = true; + channels[1].gateway_id = static_cast(CONFIG_GATEWAY_CHANNEL2_GW_ID); + channels[1].native_bus_id = static_cast(CONFIG_GATEWAY_CHANNEL2_NATIVE_BUS_ID); + channels[1].native_baudrate = static_cast(CONFIG_GATEWAY_CHANNEL2_NATIVE_BAUDRATE); +#elif CONFIG_GATEWAY_CHANNEL2_PHY_UART1 + channels[1].enabled = true; + channels[1].serial_phy = true; + channels[1].gateway_id = static_cast(CONFIG_GATEWAY_CHANNEL2_GW_ID); + channels[1].uart_port = 1; +#elif CONFIG_GATEWAY_CHANNEL2_PHY_UART2 + channels[1].enabled = true; + channels[1].serial_phy = true; + channels[1].gateway_id = static_cast(CONFIG_GATEWAY_CHANNEL2_GW_ID); + channels[1].uart_port = 2; +#endif +#endif + + bool any_enabled = false; + bool saw_native = false; + uint32_t native_baudrate = 0; + + for (int i = 0; i < CONFIG_GATEWAY_CHANNEL_COUNT; ++i) { + if (!channels[i].enabled) { + continue; + } + any_enabled = true; + for (int j = i + 1; j < CONFIG_GATEWAY_CHANNEL_COUNT; ++j) { + if (!channels[j].enabled) { + continue; + } + if (channels[i].gateway_id == channels[j].gateway_id) { + ESP_LOGE(kTag, "duplicate gateway ids configured: %u", channels[i].gateway_id); + return false; + } + if (channels[i].serial_phy && channels[j].serial_phy && + channels[i].uart_port == channels[j].uart_port) { + ESP_LOGE(kTag, "duplicate serial PHY UART%d configured for multiple channels", + channels[i].uart_port); + return false; + } + if (channels[i].native_phy && channels[j].native_phy && + channels[i].native_bus_id == channels[j].native_bus_id) { + ESP_LOGE(kTag, "duplicate native DALI bus ids configured: %u", + channels[i].native_bus_id); + return false; + } + } + if (channels[i].native_phy) { + if (!saw_native) { + saw_native = true; + native_baudrate = channels[i].native_baudrate; + } else if (native_baudrate != channels[i].native_baudrate) { + ESP_LOGE(kTag, + "mixed native PHY baudrates are not supported by the shared DALI HAL: %lu vs %lu", + static_cast(native_baudrate), + static_cast(channels[i].native_baudrate)); + return false; + } + } + } + + if (!any_enabled) { + ESP_LOGE(kTag, "no DALI PHY is configured; enable at least one native or serial channel"); + return false; + } + + return true; +} + +esp_err_t BindConfiguredChannels(gateway::DaliDomainService& dali_domain, + const gateway::GatewayRuntime& runtime) { #if CONFIG_GATEWAY_CHANNEL1_PHY_NATIVE gateway::DaliHardwareBusConfig channel1{}; channel1.channel_index = 0; @@ -39,7 +147,11 @@ void BindConfiguredChannels(gateway::DaliDomainService& dali_domain, channel1.rx_pin = static_cast(CONFIG_GATEWAY_CHANNEL1_NATIVE_RX_PIN); channel1.baudrate = static_cast(CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE); channel1.name = runtime.gatewayName(channel1.gateway_id); - LogBindError("channel1 native DALI", dali_domain.bindHardwareBus(channel1)); + esp_err_t err = dali_domain.bindHardwareBus(channel1); + LogBindError("channel1 native DALI", err); + if (err != ESP_OK) { + return err; + } #elif CONFIG_GATEWAY_CHANNEL1_PHY_UART1 || CONFIG_GATEWAY_CHANNEL1_PHY_UART2 gateway::DaliSerialBusConfig channel1{}; channel1.channel_index = 0; @@ -53,9 +165,15 @@ void BindConfiguredChannels(gateway::DaliDomainService& dali_domain, channel1.rx_pin = CONFIG_GATEWAY_CHANNEL1_SERIAL_RX_PIN; channel1.baudrate = static_cast(CONFIG_GATEWAY_CHANNEL1_SERIAL_BAUDRATE); channel1.rx_buffer_size = static_cast(CONFIG_GATEWAY_CHANNEL1_SERIAL_RX_BUFFER); - channel1.tx_buffer_size = static_cast(CONFIG_GATEWAY_CHANNEL1_SERIAL_RX_BUFFER); + channel1.tx_buffer_size = static_cast(CONFIG_GATEWAY_CHANNEL1_SERIAL_TX_BUFFER); + channel1.query_timeout_ms = + static_cast(CONFIG_GATEWAY_CHANNEL1_SERIAL_QUERY_TIMEOUT_MS); channel1.name = runtime.gatewayName(channel1.gateway_id); - LogBindError("channel1 serial DALI", dali_domain.bindSerialBus(channel1)); + esp_err_t err = dali_domain.bindSerialBus(channel1); + LogBindError("channel1 serial DALI", err); + if (err != ESP_OK) { + return err; + } #endif #if CONFIG_GATEWAY_CHANNEL_COUNT >= 2 @@ -68,7 +186,11 @@ void BindConfiguredChannels(gateway::DaliDomainService& dali_domain, channel2.rx_pin = static_cast(CONFIG_GATEWAY_CHANNEL2_NATIVE_RX_PIN); channel2.baudrate = static_cast(CONFIG_GATEWAY_CHANNEL2_NATIVE_BAUDRATE); channel2.name = runtime.gatewayName(channel2.gateway_id); - LogBindError("channel2 native DALI", dali_domain.bindHardwareBus(channel2)); + esp_err_t err = dali_domain.bindHardwareBus(channel2); + LogBindError("channel2 native DALI", err); + if (err != ESP_OK) { + return err; + } #elif CONFIG_GATEWAY_CHANNEL2_PHY_UART1 || CONFIG_GATEWAY_CHANNEL2_PHY_UART2 gateway::DaliSerialBusConfig channel2{}; channel2.channel_index = 1; @@ -82,11 +204,19 @@ void BindConfiguredChannels(gateway::DaliDomainService& dali_domain, channel2.rx_pin = CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_PIN; channel2.baudrate = static_cast(CONFIG_GATEWAY_CHANNEL2_SERIAL_BAUDRATE); channel2.rx_buffer_size = static_cast(CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_BUFFER); - channel2.tx_buffer_size = static_cast(CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_BUFFER); + channel2.tx_buffer_size = static_cast(CONFIG_GATEWAY_CHANNEL2_SERIAL_TX_BUFFER); + channel2.query_timeout_ms = + static_cast(CONFIG_GATEWAY_CHANNEL2_SERIAL_QUERY_TIMEOUT_MS); channel2.name = runtime.gatewayName(channel2.gateway_id); - LogBindError("channel2 serial DALI", dali_domain.bindSerialBus(channel2)); + esp_err_t err = dali_domain.bindSerialBus(channel2); + LogBindError("channel2 serial DALI", err); + if (err != ESP_OK) { + return err; + } #endif #endif + + return ESP_OK; } } // namespace @@ -107,6 +237,8 @@ extern "C" void app_main(void) { gateway::GatewayCore core(profile); core.start(); + ESP_ERROR_CHECK(ValidateChannelBindings() ? ESP_OK : ESP_ERR_INVALID_STATE); + s_dali_domain = std::make_unique(); s_runtime = std::make_unique( profile, @@ -118,7 +250,7 @@ extern "C" void app_main(void) { s_dali_domain.get()); ESP_ERROR_CHECK(s_runtime->start()); s_runtime->setGatewayCount(CONFIG_GATEWAY_CHANNEL_COUNT); - BindConfiguredChannels(*s_dali_domain, *s_runtime); + ESP_ERROR_CHECK(BindConfiguredChannels(*s_dali_domain, *s_runtime)); gateway::GatewayControllerConfig controller_config; controller_config.setup_supported = true; @@ -132,6 +264,15 @@ extern "C" void app_main(void) { controller_config); ESP_ERROR_CHECK(s_controller->start()); + if (profile.enable_wifi || profile.enable_eth) { + gateway::GatewayNetworkServiceConfig network_config; + network_config.http_enabled = true; + network_config.udp_enabled = true; + s_network = std::make_unique(*s_controller, *s_runtime, + network_config); + ESP_ERROR_CHECK(s_network->start()); + } + if (profile.enable_ble) { s_ble_bridge = std::make_unique(*s_controller, *s_runtime, *s_dali_domain); diff --git a/components/gateway_network/CMakeLists.txt b/components/gateway_network/CMakeLists.txt new file mode 100644 index 0000000..6ce40d3 --- /dev/null +++ b/components/gateway_network/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS "src/gateway_network.cpp" + INCLUDE_DIRS "include" + REQUIRES esp_event esp_http_server esp_netif freertos gateway_controller gateway_runtime log lwip espressif__cjson +) + +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/gateway_network/include/gateway_network.hpp b/components/gateway_network/include/gateway_network.hpp new file mode 100644 index 0000000..90afb9c --- /dev/null +++ b/components/gateway_network/include/gateway_network.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include "esp_err.h" +#include "esp_http_server.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "lwip/sockets.h" + +namespace gateway { + +class GatewayController; +class GatewayRuntime; + +struct GatewayNetworkServiceConfig { + bool http_enabled{true}; + bool udp_enabled{true}; + uint16_t http_port{80}; + uint16_t udp_port{2020}; + uint32_t udp_task_stack_size{4096}; + UBaseType_t udp_task_priority{4}; +}; + +class GatewayNetworkService { + public: + GatewayNetworkService(GatewayController& controller, GatewayRuntime& runtime, + GatewayNetworkServiceConfig config = {}); + + esp_err_t start(); + + private: + static void UdpTaskEntry(void* arg); + static esp_err_t HandleInfoGet(httpd_req_t* req); + static esp_err_t HandleCommandPost(httpd_req_t* req); + + esp_err_t ensureNetworkStack(); + esp_err_t startHttpServer(); + esp_err_t startUdpTask(); + void udpTaskLoop(); + void handleGatewayNotification(const std::vector& frame); + std::string deviceInfoJson() const; + std::string deviceInfoDoubleEncodedJson() const; + + GatewayController& controller_; + GatewayRuntime& runtime_; + GatewayNetworkServiceConfig config_; + bool started_{false}; + httpd_handle_t http_server_{nullptr}; + TaskHandle_t udp_task_handle_{nullptr}; + int udp_socket_{-1}; + SemaphoreHandle_t udp_lock_{nullptr}; + bool has_udp_remote_{false}; + sockaddr_storage udp_remote_addr_{}; + socklen_t udp_remote_addr_len_{0}; +}; + +} // namespace gateway \ No newline at end of file diff --git a/components/gateway_network/src/gateway_network.cpp b/components/gateway_network/src/gateway_network.cpp new file mode 100644 index 0000000..377b005 --- /dev/null +++ b/components/gateway_network/src/gateway_network.cpp @@ -0,0 +1,379 @@ +#include "gateway_network.hpp" + +#include "gateway_controller.hpp" +#include "gateway_runtime.hpp" + +#include "cJSON.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "lwip/inet.h" + +#include +#include +#include +#include + +namespace gateway { + +namespace { + +constexpr const char* kTag = "gateway_network"; +constexpr size_t kUdpBufferSize = 256; + +class LockGuard { + public: + explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) { + if (lock_ != nullptr) { + xSemaphoreTake(lock_, portMAX_DELAY); + } + } + + ~LockGuard() { + if (lock_ != nullptr) { + xSemaphoreGive(lock_); + } + } + + private: + SemaphoreHandle_t lock_; +}; + +bool HexValue(char ch, uint8_t& value) { + if (ch >= '0' && ch <= '9') { + value = static_cast(ch - '0'); + return true; + } + if (ch >= 'a' && ch <= 'f') { + value = static_cast(10 + ch - 'a'); + return true; + } + if (ch >= 'A' && ch <= 'F') { + value = static_cast(10 + ch - 'A'); + return true; + } + return false; +} + +std::vector DecodeHex(std::string_view hex) { + if ((hex.size() & 1U) != 0U) { + return {}; + } + + std::vector bytes; + bytes.reserve(hex.size() / 2); + for (size_t i = 0; i < hex.size(); i += 2) { + uint8_t high = 0; + uint8_t low = 0; + if (!HexValue(hex[i], high) || !HexValue(hex[i + 1], low)) { + return {}; + } + bytes.push_back(static_cast((high << 4) | low)); + } + return bytes; +} + +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; +} + +esp_err_t ReadRequestBody(httpd_req_t* req, std::string& body) { + body.clear(); + if (req == nullptr) { + return ESP_ERR_INVALID_ARG; + } + + body.resize(static_cast(req->content_len)); + int received = 0; + while (received < req->content_len) { + const int ret = httpd_req_recv(req, body.data() + received, req->content_len - received); + if (ret <= 0) { + return ESP_FAIL; + } + received += ret; + } + return ESP_OK; +} + +} // namespace + +GatewayNetworkService::GatewayNetworkService(GatewayController& controller, + GatewayRuntime& runtime, + GatewayNetworkServiceConfig config) + : controller_(controller), runtime_(runtime), config_(config), + udp_lock_(xSemaphoreCreateMutex()) {} + +esp_err_t GatewayNetworkService::start() { + if (started_) { + return ESP_OK; + } + + esp_err_t err = ensureNetworkStack(); + if (err != ESP_OK) { + return err; + } + + controller_.addNotificationSink( + [this](const std::vector& frame) { handleGatewayNotification(frame); }); + + if (config_.http_enabled) { + err = startHttpServer(); + if (err != ESP_OK) { + return err; + } + } + + if (config_.udp_enabled) { + err = startUdpTask(); + if (err != ESP_OK) { + return err; + } + } + + started_ = true; + ESP_LOGI(kTag, "network service started http=%d udp=%d", config_.http_enabled, + config_.udp_enabled); + return ESP_OK; +} + +esp_err_t GatewayNetworkService::ensureNetworkStack() { + esp_err_t err = esp_netif_init(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(kTag, "failed to init esp_netif: %s", esp_err_to_name(err)); + return err; + } + + err = esp_event_loop_create_default(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(kTag, "failed to create default event loop: %s", esp_err_to_name(err)); + return err; + } + + return ESP_OK; +} + +esp_err_t GatewayNetworkService::startHttpServer() { + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = config_.http_port; + config.lru_purge_enable = true; + + esp_err_t err = httpd_start(&http_server_, &config); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to start HTTP server: %s", esp_err_to_name(err)); + return err; + } + + httpd_uri_t info_uri = {}; + info_uri.uri = "/info"; + info_uri.method = HTTP_GET; + info_uri.handler = &GatewayNetworkService::HandleInfoGet; + info_uri.user_ctx = this; + err = httpd_register_uri_handler(http_server_, &info_uri); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to register /info handler: %s", esp_err_to_name(err)); + return err; + } + + httpd_uri_t command_uri = {}; + command_uri.uri = "/dali/cmd"; + command_uri.method = HTTP_POST; + command_uri.handler = &GatewayNetworkService::HandleCommandPost; + command_uri.user_ctx = this; + err = httpd_register_uri_handler(http_server_, &command_uri); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to register /dali/cmd handler: %s", esp_err_to_name(err)); + return err; + } + + return ESP_OK; +} + +esp_err_t GatewayNetworkService::startUdpTask() { + if (udp_task_handle_ != nullptr) { + return ESP_OK; + } + + const BaseType_t created = + xTaskCreate(&GatewayNetworkService::UdpTaskEntry, "gateway_udp", + config_.udp_task_stack_size, this, config_.udp_task_priority, + &udp_task_handle_); + if (created != pdPASS) { + udp_task_handle_ = nullptr; + ESP_LOGE(kTag, "failed to create UDP task"); + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + +void GatewayNetworkService::UdpTaskEntry(void* arg) { + static_cast(arg)->udpTaskLoop(); +} + +void GatewayNetworkService::udpTaskLoop() { + udp_socket_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (udp_socket_ < 0) { + ESP_LOGE(kTag, "failed to create UDP socket"); + udp_task_handle_ = nullptr; + vTaskDelete(nullptr); + return; + } + + sockaddr_in local_addr = {}; + local_addr.sin_family = AF_INET; + local_addr.sin_port = htons(config_.udp_port); + local_addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(udp_socket_, reinterpret_cast(&local_addr), sizeof(local_addr)) != 0) { + ESP_LOGE(kTag, "failed to bind UDP socket on port %u", config_.udp_port); + close(udp_socket_); + udp_socket_ = -1; + udp_task_handle_ = nullptr; + vTaskDelete(nullptr); + return; + } + + ESP_LOGI(kTag, "UDP router listening on port %u", config_.udp_port); + while (true) { + uint8_t buffer[kUdpBufferSize] = {0}; + sockaddr_storage remote_addr = {}; + socklen_t remote_addr_len = sizeof(remote_addr); + const int read_len = recvfrom(udp_socket_, buffer, sizeof(buffer), 0, + reinterpret_cast(&remote_addr), + &remote_addr_len); + if (read_len <= 0) { + continue; + } + + { + LockGuard guard(udp_lock_); + udp_remote_addr_ = remote_addr; + udp_remote_addr_len_ = remote_addr_len; + has_udp_remote_ = true; + } + + controller_.enqueueCommandFrame(std::vector(buffer, buffer + read_len)); + } +} + +void GatewayNetworkService::handleGatewayNotification(const std::vector& frame) { + if (!config_.udp_enabled || udp_socket_ < 0 || frame.empty()) { + return; + } + + sockaddr_storage remote_addr = {}; + socklen_t remote_addr_len = 0; + { + LockGuard guard(udp_lock_); + if (!has_udp_remote_) { + return; + } + remote_addr = udp_remote_addr_; + remote_addr_len = udp_remote_addr_len_; + } + + sendto(udp_socket_, frame.data(), frame.size(), 0, + reinterpret_cast(&remote_addr), remote_addr_len); +} + +std::string GatewayNetworkService::deviceInfoJson() const { + const auto info = runtime_.deviceInfo(); + cJSON* root = cJSON_CreateObject(); + if (root == nullptr) { + return {}; + } + + cJSON_AddStringToObject(root, "serialId", info.serial_id.c_str()); + cJSON_AddStringToObject(root, "type", info.type.c_str()); + cJSON_AddStringToObject(root, "project", info.project.c_str()); + cJSON_AddStringToObject(root, "version", info.version.c_str()); + + cJSON* gateway_info = cJSON_CreateObject(); + if (gateway_info != nullptr) { + cJSON_AddNumberToObject(gateway_info, "count", + static_cast(info.dali_gateway_count)); + cJSON_AddItemToObject(root, "daliGatewayInfo", gateway_info); + } + + if (info.wlan.has_value()) { + cJSON* wlan = cJSON_CreateObject(); + if (wlan != nullptr) { + cJSON_AddStringToObject(wlan, "mac", info.wlan->mac.c_str()); + cJSON_AddStringToObject(wlan, "IP", info.wlan->ip.c_str()); + cJSON_AddStringToObject(wlan, "ssid", info.wlan->ssid.c_str()); + cJSON_AddStringToObject(wlan, "passwd", info.wlan->password.c_str()); + cJSON_AddItemToObject(root, "wlanInfo", wlan); + } + } + + const std::string rendered = PrintJson(root); + cJSON_Delete(root); + return rendered; +} + +std::string GatewayNetworkService::deviceInfoDoubleEncodedJson() const { + const std::string inner = deviceInfoJson(); + cJSON* outer = cJSON_CreateString(inner.c_str()); + if (outer == nullptr) { + return {}; + } + + const std::string rendered = PrintJson(outer); + cJSON_Delete(outer); + return rendered; +} + +esp_err_t GatewayNetworkService::HandleInfoGet(httpd_req_t* req) { + auto* service = static_cast(req->user_ctx); + if (service == nullptr) { + return ESP_FAIL; + } + + const std::string payload = service->deviceInfoDoubleEncodedJson(); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, payload.data(), payload.size()); +} + +esp_err_t GatewayNetworkService::HandleCommandPost(httpd_req_t* req) { + auto* service = static_cast(req->user_ctx); + if (service == nullptr) { + return ESP_FAIL; + } + + std::string body; + if (ReadRequestBody(req, body) != ESP_OK) { + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request"); + } + + cJSON* root = cJSON_ParseWithLength(body.c_str(), body.size()); + if (root == nullptr) { + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request: json decode error"); + } + + const cJSON* command = cJSON_GetObjectItemCaseSensitive(root, "command"); + if (!cJSON_IsString(command) || command->valuestring == nullptr) { + cJSON_Delete(root); + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request: missing command"); + } + + const auto frame = DecodeHex(command->valuestring); + cJSON_Delete(root); + if (frame.empty()) { + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request: invalid command hex"); + } + + service->controller_.enqueueCommandFrame(frame); + httpd_resp_set_type(req, "text/plain"); + return httpd_resp_sendstr(req, "ok"); +} + +} // namespace gateway \ No newline at end of file diff --git a/components/gateway_runtime/include/gateway_runtime.hpp b/components/gateway_runtime/include/gateway_runtime.hpp index d4577b8..f5452e7 100644 --- a/components/gateway_runtime/include/gateway_runtime.hpp +++ b/components/gateway_runtime/include/gateway_runtime.hpp @@ -10,6 +10,8 @@ #include #include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "gateway_core.hpp" #include "nvs.h" @@ -80,6 +82,7 @@ class GatewayRuntime { GatewayRuntime(BootProfile profile, GatewayRuntimeConfig config, DaliDomainService* dali_domain); + ~GatewayRuntime(); esp_err_t start(); @@ -126,6 +129,7 @@ class GatewayRuntime { CommandDropReason last_enqueue_drop_reason_{CommandDropReason::kNone}; std::function command_address_resolver_; std::optional wireless_info_; + SemaphoreHandle_t command_lock_{nullptr}; }; } // namespace gateway \ No newline at end of file diff --git a/components/gateway_runtime/src/gateway_runtime.cpp b/components/gateway_runtime/src/gateway_runtime.cpp index 4fa5161..d6245fe 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -25,6 +25,24 @@ constexpr uint8_t kCommandFramePrefix0 = 0x28; constexpr uint8_t kCommandFramePrefix1 = 0x01; constexpr uint8_t kNotifyFramePrefix = 0x22; +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_; +}; + } // namespace esp_err_t InitializeRuntimeNvs() { @@ -158,7 +176,15 @@ GatewayRuntime::GatewayRuntime(BootProfile profile, GatewayRuntimeConfig config, : profile_(profile), config_(std::move(config)), dali_domain_(dali_domain), - command_address_resolver_([](uint8_t, uint8_t raw_addr) { return raw_addr; }) {} + command_address_resolver_([](uint8_t, uint8_t raw_addr) { return raw_addr; }), + command_lock_(xSemaphoreCreateRecursiveMutex()) {} + +GatewayRuntime::~GatewayRuntime() { + if (command_lock_ != nullptr) { + vSemaphoreDelete(command_lock_); + command_lock_ = nullptr; + } +} esp_err_t GatewayRuntime::start() { const esp_err_t err = settings_.open(); @@ -230,6 +256,7 @@ std::vector GatewayRuntime::buildNotificationFrame(const std::vector command) { + LockGuard guard(command_lock_); last_enqueue_drop_reason_ = CommandDropReason::kNone; if (isQueryCommand(command) && hasPendingQueryCommand(command)) { last_enqueue_drop_reason_ = CommandDropReason::kDuplicate; @@ -246,6 +273,7 @@ bool GatewayRuntime::enqueueCommand(std::vector command) { } std::optional> GatewayRuntime::popNextCommand() { + LockGuard guard(command_lock_); if (pending_commands_.empty()) { current_command_.reset(); return std::nullopt; @@ -257,10 +285,12 @@ std::optional> GatewayRuntime::popNextCommand() { } void GatewayRuntime::completeCurrentCommand() { + LockGuard guard(command_lock_); current_command_.reset(); } bool GatewayRuntime::hasPendingQueryCommand(const std::vector& command) const { + LockGuard guard(command_lock_); const auto command_key = queryCommandKey(command); if (!command_key.has_value()) { return false; @@ -277,6 +307,7 @@ bool GatewayRuntime::hasPendingQueryCommand(const std::vector& command) } GatewayRuntime::CommandDropReason GatewayRuntime::lastEnqueueDropReason() const { + LockGuard guard(command_lock_); return last_enqueue_drop_reason_; } @@ -293,6 +324,7 @@ void GatewayRuntime::setWirelessInfo(WirelessInfo info) { void GatewayRuntime::setCommandAddressResolver( std::function resolver) { + LockGuard guard(command_lock_); if (resolver) { command_address_resolver_ = std::move(resolver); return;