From 3d8d00c3dd91852fdc84d91f7a9d41be6f42b87e Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 30 Apr 2026 00:54:53 +0800 Subject: [PATCH] feat(gateway): enhance GatewayNetworkService with HTTP and UDP support, add status LED control Co-authored-by: Copilot --- README.md | 4 +- apps/gateway/main/Kconfig.projbuild | 40 ++ apps/gateway/main/app_main.cpp | 29 ++ apps/gateway/sdkconfig | 22 +- .../include/gateway_controller.hpp | 27 ++ .../src/gateway_controller.cpp | 51 +++ components/gateway_network/CMakeLists.txt | 2 +- .../include/gateway_network.hpp | 22 + .../gateway_network/src/gateway_network.cpp | 418 +++++++++++++++++- .../gateway_runtime/src/gateway_runtime.cpp | 14 +- 10 files changed, 599 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index b26066d..c3b8760 100644 --- a/README.md +++ b/README.md @@ -12,9 +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_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, and setup AP mode 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, 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 +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 also includes a `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 a `gateway_network` service that starts the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, Wi-Fi STA startup from persisted credentials, and the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`. 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. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots. \ No newline at end of file diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index 3e19032..6f2c814 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -258,4 +258,44 @@ config GATEWAY_DALI_BAUDRATE help Runtime baudrate used when initializing the local DALI bus. +menu "Gateway Network Services" + +config GATEWAY_NETWORK_HTTP_ENABLED + bool "Enable HTTP gateway API" + default y + help + Enables Lua-compatible HTTP gateway routes such as /info and /dali/cmd. + +config GATEWAY_NETWORK_HTTP_PORT + int "HTTP API port" + depends on GATEWAY_NETWORK_HTTP_ENABLED + range 1 65535 + default 80 + +config GATEWAY_NETWORK_UDP_ROUTER_ENABLED + bool "Enable UDP IP router" + default y + help + Enables raw gateway command ingress and notification replies on UDP port 2020 by default. + +config GATEWAY_NETWORK_UDP_PORT + int "UDP IP router port" + depends on GATEWAY_NETWORK_UDP_ROUTER_ENABLED + range 1 65535 + default 2020 + +config GATEWAY_STATUS_LED_GPIO + int "Status LED GPIO" + range -1 48 + default -1 + help + GPIO used by the HTTP /led/1 and /led/0 routes. Set to -1 to disable GPIO control. + +config GATEWAY_STATUS_LED_ACTIVE_HIGH + bool "Status LED is active high" + depends on GATEWAY_STATUS_LED_GPIO >= 0 + default y + +endmenu + endmenu \ No newline at end of file diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index ed8b53f..f31f278 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -16,6 +16,18 @@ #define CONFIG_GATEWAY_CHANNEL_COUNT 2 #endif +#ifndef CONFIG_GATEWAY_NETWORK_HTTP_PORT +#define CONFIG_GATEWAY_NETWORK_HTTP_PORT 80 +#endif + +#ifndef CONFIG_GATEWAY_NETWORK_UDP_PORT +#define CONFIG_GATEWAY_NETWORK_UDP_PORT 2020 +#endif + +#ifndef CONFIG_GATEWAY_STATUS_LED_GPIO +#define CONFIG_GATEWAY_STATUS_LED_GPIO -1 +#endif + namespace { constexpr const char* kProjectName = "DALI_485_Gateway"; constexpr const char* kProjectVersion = "0.1.0"; @@ -266,8 +278,25 @@ extern "C" void app_main(void) { if (profile.enable_wifi || profile.enable_eth) { gateway::GatewayNetworkServiceConfig network_config; + network_config.wifi_enabled = profile.enable_wifi; + #ifdef CONFIG_GATEWAY_NETWORK_HTTP_ENABLED network_config.http_enabled = true; + #else + network_config.http_enabled = false; + #endif + #ifdef CONFIG_GATEWAY_NETWORK_UDP_ROUTER_ENABLED network_config.udp_enabled = true; + #else + network_config.udp_enabled = false; + #endif + network_config.http_port = static_cast(CONFIG_GATEWAY_NETWORK_HTTP_PORT); + network_config.udp_port = static_cast(CONFIG_GATEWAY_NETWORK_UDP_PORT); + network_config.status_led_gpio = CONFIG_GATEWAY_STATUS_LED_GPIO; + #ifdef CONFIG_GATEWAY_STATUS_LED_ACTIVE_HIGH + network_config.status_led_active_high = true; + #else + network_config.status_led_active_high = false; + #endif s_network = std::make_unique(*s_controller, *s_runtime, network_config); ESP_ERROR_CHECK(s_network->start()); diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index fc393f2..f3687bb 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -562,13 +562,13 @@ CONFIG_ESPTOOLPY_FLASHFREQ_80M=y CONFIG_ESPTOOLPY_FLASHFREQ="80m" # CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set -CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set -# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y # CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set -CONFIG_ESPTOOLPY_FLASHSIZE="4MB" +CONFIG_ESPTOOLPY_FLASHSIZE="16MB" # CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE is not set CONFIG_ESPTOOLPY_BEFORE_RESET=y # CONFIG_ESPTOOLPY_BEFORE_NORESET is not set @@ -584,11 +584,11 @@ CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 # # CONFIG_PARTITION_TABLE_SINGLE_APP is not set # CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set -CONFIG_PARTITION_TABLE_TWO_OTA=y +# CONFIG_PARTITION_TABLE_TWO_OTA is not set # CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set -# CONFIG_PARTITION_TABLE_CUSTOM is not set +CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_FILENAME="partitions_two_ota.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_OFFSET=0x8000 CONFIG_PARTITION_TABLE_MD5=y # end of Partition Table @@ -619,6 +619,16 @@ CONFIG_GATEWAY_CHANNEL2_PHY_DISABLED=y # end of Gateway Channel 2 # CONFIG_GATEWAY_ENABLE_DALI_BUS is not set + +# +# Gateway Network Services +# +CONFIG_GATEWAY_NETWORK_HTTP_ENABLED=y +CONFIG_GATEWAY_NETWORK_HTTP_PORT=80 +CONFIG_GATEWAY_NETWORK_UDP_ROUTER_ENABLED=y +CONFIG_GATEWAY_NETWORK_UDP_PORT=2020 +CONFIG_GATEWAY_STATUS_LED_GPIO=-1 +# end of Gateway Network Services # end of Gateway App # diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index b0a4330..de11072 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -31,10 +31,34 @@ struct GatewayControllerConfig { bool internal_group_supported{true}; }; +struct GatewayChannelSnapshot { + uint8_t channel_index{0}; + uint8_t gateway_id{0}; + std::string name; + std::string phy; + uint8_t scene_mask_low{0}; + uint8_t scene_mask_high{0}; + uint8_t group_mask_low{0}; + uint8_t group_mask_high{0}; + bool allocating{false}; + int last_alloc_addr{0}; +}; + +struct GatewayControllerSnapshot { + bool setup_mode{false}; + bool ble_enabled{false}; + bool wifi_enabled{false}; + bool ip_router_enabled{false}; + bool internal_scene_supported{false}; + bool internal_group_supported{false}; + std::vector channels; +}; + class GatewayController { public: using NotificationSink = std::function& frame)>; using BleStateSink = std::function; + using WifiStateSink = std::function; using GatewayNameSink = std::function; GatewayController(GatewayRuntime& runtime, DaliDomainService& dali_domain, @@ -45,12 +69,14 @@ class GatewayController { bool enqueueCommandFrame(const std::vector& frame); void addNotificationSink(NotificationSink sink); void addBleStateSink(BleStateSink sink); + void addWifiStateSink(WifiStateSink sink); void addGatewayNameSink(GatewayNameSink sink); bool setupMode() const; bool bleEnabled() const; bool wifiEnabled() const; bool ipRouterEnabled() const; + GatewayControllerSnapshot snapshot(); private: struct InternalScene { @@ -143,6 +169,7 @@ class GatewayController { nvs_handle_t storage_{0}; std::vector notification_sinks_; std::vector ble_state_sinks_; + std::vector wifi_state_sinks_; std::vector gateway_name_sinks_; std::map scenes_; std::map groups_; diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index 563f9d1..8898c64 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -80,6 +80,18 @@ void AppendPaddedName(std::vector& out, std::string_view name) { } } +const char* PhyKindToString(DaliPhyKind phy_kind) { + switch (phy_kind) { + case DaliPhyKind::kNativeHardware: + return "native"; + case DaliPhyKind::kSerialUart: + return "serial"; + case DaliPhyKind::kCustom: + default: + return "custom"; + } +} + } // namespace GatewayController::GatewayController(GatewayRuntime& runtime, DaliDomainService& dali_domain, @@ -157,6 +169,12 @@ void GatewayController::addBleStateSink(BleStateSink sink) { } } +void GatewayController::addWifiStateSink(WifiStateSink sink) { + if (sink) { + wifi_state_sinks_.push_back(std::move(sink)); + } +} + void GatewayController::addGatewayNameSink(GatewayNameSink sink) { if (sink) { gateway_name_sinks_.push_back(std::move(sink)); @@ -179,6 +197,36 @@ bool GatewayController::ipRouterEnabled() const { return ip_router_enabled_; } +GatewayControllerSnapshot GatewayController::snapshot() { + GatewayControllerSnapshot out; + out.setup_mode = setup_mode_; + out.ble_enabled = ble_enabled_; + out.wifi_enabled = wifi_enabled_; + out.ip_router_enabled = ip_router_enabled_; + out.internal_scene_supported = config_.internal_scene_supported; + out.internal_group_supported = config_.internal_group_supported; + + const auto channels = dali_domain_.channelInfo(); + out.channels.reserve(channels.size()); + for (const auto& channel : channels) { + const auto [scene_low, scene_high] = sceneMask(channel.gateway_id); + const auto [group_low, group_high] = groupMask(channel.gateway_id); + GatewayChannelSnapshot channel_snapshot; + channel_snapshot.channel_index = channel.channel_index; + channel_snapshot.gateway_id = channel.gateway_id; + channel_snapshot.name = channel.name; + channel_snapshot.phy = PhyKindToString(channel.phy_kind); + channel_snapshot.scene_mask_low = scene_low; + channel_snapshot.scene_mask_high = scene_high; + channel_snapshot.group_mask_low = group_low; + channel_snapshot.group_mask_high = group_high; + channel_snapshot.allocating = dali_domain_.isAllocAddr(channel.gateway_id); + channel_snapshot.last_alloc_addr = dali_domain_.lastAllocAddr(channel.gateway_id); + out.channels.push_back(std::move(channel_snapshot)); + } + return out; +} + void GatewayController::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } @@ -257,6 +305,9 @@ void GatewayController::dispatchCommand(const std::vector& command) { } else if (addr == 1 || addr == 101) { wifi_enabled_ = true; } + for (const auto& sink : wifi_state_sinks_) { + sink(addr); + } break; case 0x05: handleGatewayNameCommand(gateway_id, command); diff --git a/components/gateway_network/CMakeLists.txt b/components/gateway_network/CMakeLists.txt index 6ce40d3..a87a58a 100644 --- a/components/gateway_network/CMakeLists.txt +++ b/components/gateway_network/CMakeLists.txt @@ -1,7 +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 + REQUIRES esp_event esp_http_server esp_netif esp_wifi 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 index 90afb9c..4127dde 100644 --- a/components/gateway_network/include/gateway_network.hpp +++ b/components/gateway_network/include/gateway_network.hpp @@ -5,7 +5,9 @@ #include #include "esp_err.h" +#include "esp_event.h" #include "esp_http_server.h" +#include "esp_netif.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" @@ -17,10 +19,13 @@ class GatewayController; class GatewayRuntime; struct GatewayNetworkServiceConfig { + bool wifi_enabled{true}; bool http_enabled{true}; bool udp_enabled{true}; uint16_t http_port{80}; uint16_t udp_port{2020}; + int status_led_gpio{-1}; + bool status_led_active_high{true}; uint32_t udp_task_stack_size{4096}; UBaseType_t udp_task_priority{4}; }; @@ -35,21 +40,38 @@ class GatewayNetworkService { private: static void UdpTaskEntry(void* arg); static esp_err_t HandleInfoGet(httpd_req_t* req); + static esp_err_t HandleCommandGet(httpd_req_t* req); static esp_err_t HandleCommandPost(httpd_req_t* req); + static esp_err_t HandleLedOnGet(httpd_req_t* req); + static esp_err_t HandleLedOffGet(httpd_req_t* req); + static esp_err_t HandleJqJsGet(httpd_req_t* req); + static void HandleWifiEvent(void* arg, esp_event_base_t event_base, int32_t event_id, + void* event_data); esp_err_t ensureNetworkStack(); + esp_err_t startWifi(); + esp_err_t startSetupAp(); + esp_err_t configureStatusLed(); esp_err_t startHttpServer(); esp_err_t startUdpTask(); void udpTaskLoop(); void handleGatewayNotification(const std::vector& frame); + void handleWifiControl(uint8_t mode); + void handleWifiEvent(esp_event_base_t event_base, int32_t event_id, void* event_data); std::string deviceInfoJson() const; std::string deviceInfoDoubleEncodedJson() const; + std::string gatewaySnapshotJson(); + void setStatusLed(bool on); GatewayController& controller_; GatewayRuntime& runtime_; GatewayNetworkServiceConfig config_; bool started_{false}; httpd_handle_t http_server_{nullptr}; + esp_netif_t* wifi_sta_netif_{nullptr}; + esp_netif_t* wifi_ap_netif_{nullptr}; + bool wifi_started_{false}; + bool setup_ap_started_{false}; TaskHandle_t udp_task_handle_{nullptr}; int udp_socket_{-1}; SemaphoreHandle_t udp_lock_{nullptr}; diff --git a/components/gateway_network/src/gateway_network.cpp b/components/gateway_network/src/gateway_network.cpp index 377b005..17d0b2d 100644 --- a/components/gateway_network/src/gateway_network.cpp +++ b/components/gateway_network/src/gateway_network.cpp @@ -4,14 +4,19 @@ #include "gateway_runtime.hpp" #include "cJSON.h" +#include "driver/gpio.h" #include "esp_event.h" #include "esp_log.h" #include "esp_netif.h" +#include "esp_netif_ip_addr.h" +#include "esp_wifi.h" #include "lwip/inet.h" #include +#include #include #include +#include #include namespace gateway { @@ -19,6 +24,7 @@ namespace gateway { namespace { constexpr const char* kTag = "gateway_network"; +constexpr const char* kSetupApSsid = "LAMMIN_Gateway"; constexpr size_t kUdpBufferSize = 256; class LockGuard { @@ -73,6 +79,13 @@ std::vector DecodeHex(std::string_view hex) { return bytes; } +std::string MacToHex(const uint8_t mac[6]) { + char out[13] = {0}; + std::snprintf(out, sizeof(out), "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); + return std::string(out); +} + std::string PrintJson(cJSON* node) { if (node == nullptr) { return {}; @@ -105,6 +118,16 @@ esp_err_t ReadRequestBody(httpd_req_t* req, std::string& body) { return ESP_OK; } +esp_err_t RegisterUri(httpd_handle_t server, const char* uri, httpd_method_t method, + esp_err_t (*handler)(httpd_req_t*), void* user_ctx) { + httpd_uri_t route = {}; + route.uri = uri; + route.method = method; + route.handler = handler; + route.user_ctx = user_ctx; + return httpd_register_uri_handler(server, &route); +} + } // namespace GatewayNetworkService::GatewayNetworkService(GatewayController& controller, @@ -123,8 +146,21 @@ esp_err_t GatewayNetworkService::start() { return err; } + if (config_.wifi_enabled) { + err = startWifi(); + if (err != ESP_OK) { + return err; + } + } + + err = configureStatusLed(); + if (err != ESP_OK) { + return err; + } + controller_.addNotificationSink( [this](const std::vector& frame) { handleGatewayNotification(frame); }); + controller_.addWifiStateSink([this](uint8_t mode) { handleWifiControl(mode); }); if (config_.http_enabled) { err = startHttpServer(); @@ -162,6 +198,162 @@ esp_err_t GatewayNetworkService::ensureNetworkStack() { return ESP_OK; } +esp_err_t GatewayNetworkService::startWifi() { + if (wifi_started_) { + return ESP_OK; + } + setup_ap_started_ = false; + + if (wifi_sta_netif_ == nullptr) { + wifi_sta_netif_ = esp_netif_create_default_wifi_sta(); + if (wifi_sta_netif_ == nullptr) { + ESP_LOGE(kTag, "failed to create default Wi-Fi STA netif"); + return ESP_ERR_NO_MEM; + } + } + + wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); + esp_err_t err = esp_wifi_init(&wifi_init_config); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(kTag, "failed to init Wi-Fi: %s", esp_err_to_name(err)); + return err; + } + + err = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &GatewayNetworkService::HandleWifiEvent, this); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(kTag, "failed to register Wi-Fi event handler: %s", esp_err_to_name(err)); + return err; + } + + err = esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + &GatewayNetworkService::HandleWifiEvent, this); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(kTag, "failed to register IP event handler: %s", esp_err_to_name(err)); + return err; + } + + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + err = esp_wifi_set_mode(WIFI_MODE_STA); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to set Wi-Fi mode STA: %s", esp_err_to_name(err)); + return err; + } + + const auto device_info = runtime_.deviceInfo(); + if (device_info.wlan.has_value() && !device_info.wlan->ssid.empty()) { + wifi_config_t wifi_config = {}; + std::strncpy(reinterpret_cast(wifi_config.sta.ssid), device_info.wlan->ssid.c_str(), + sizeof(wifi_config.sta.ssid) - 1); + std::strncpy(reinterpret_cast(wifi_config.sta.password), + device_info.wlan->password.c_str(), sizeof(wifi_config.sta.password) - 1); + err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to set Wi-Fi credentials: %s", esp_err_to_name(err)); + return err; + } + } + + err = esp_wifi_start(); + if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) { + ESP_LOGE(kTag, "failed to start Wi-Fi: %s", esp_err_to_name(err)); + return err; + } + + uint8_t mac[6] = {0}; + if (device_info.wlan.has_value() && !device_info.wlan->ssid.empty() && + esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) { + WirelessInfo wireless = device_info.wlan.value_or(WirelessInfo{}); + wireless.mac = MacToHex(mac); + runtime_.setWirelessInfo(std::move(wireless)); + } + + wifi_started_ = true; + ESP_LOGI(kTag, "Wi-Fi STA started has_credentials=%d", + device_info.wlan.has_value() && !device_info.wlan->ssid.empty()); + return ESP_OK; +} + +esp_err_t GatewayNetworkService::startSetupAp() { + if (wifi_ap_netif_ == nullptr) { + wifi_ap_netif_ = esp_netif_create_default_wifi_ap(); + if (wifi_ap_netif_ == nullptr) { + ESP_LOGE(kTag, "failed to create default Wi-Fi AP netif"); + return ESP_ERR_NO_MEM; + } + + esp_netif_ip_info_t ip_info = {}; + IP4_ADDR(&ip_info.ip, 192, 168, 3, 1); + IP4_ADDR(&ip_info.gw, 192, 168, 3, 1); + IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_stop(wifi_ap_netif_)); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_set_ip_info(wifi_ap_netif_, &ip_info)); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_start(wifi_ap_netif_)); + } + + wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); + esp_err_t err = esp_wifi_init(&wifi_init_config); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(kTag, "failed to init Wi-Fi for setup AP: %s", esp_err_to_name(err)); + return err; + } + + if (wifi_started_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); + } + + wifi_config_t ap_config = {}; + std::strncpy(reinterpret_cast(ap_config.ap.ssid), kSetupApSsid, + sizeof(ap_config.ap.ssid) - 1); + ap_config.ap.ssid_len = std::strlen(kSetupApSsid); + ap_config.ap.channel = 1; + ap_config.ap.authmode = WIFI_AUTH_OPEN; + ap_config.ap.max_connection = 4; + + err = esp_wifi_set_mode(WIFI_MODE_AP); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to set Wi-Fi AP mode: %s", esp_err_to_name(err)); + return err; + } + err = esp_wifi_set_config(WIFI_IF_AP, &ap_config); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to configure setup AP: %s", esp_err_to_name(err)); + return err; + } + err = esp_wifi_start(); + if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) { + ESP_LOGE(kTag, "failed to start setup AP: %s", esp_err_to_name(err)); + return err; + } + + wifi_started_ = true; + setup_ap_started_ = true; + ESP_LOGI(kTag, "setup AP started ssid=%s ip=192.168.3.1", kSetupApSsid); + return ESP_OK; +} + +esp_err_t GatewayNetworkService::configureStatusLed() { + if (config_.status_led_gpio < 0) { + return ESP_OK; + } + + gpio_config_t io_config = {}; + io_config.pin_bit_mask = 1ULL << static_cast(config_.status_led_gpio); + io_config.mode = GPIO_MODE_OUTPUT; + io_config.pull_up_en = GPIO_PULLUP_DISABLE; + io_config.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_config.intr_type = GPIO_INTR_DISABLE; + const esp_err_t err = gpio_config(&io_config); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to configure status LED GPIO%d: %s", config_.status_led_gpio, + esp_err_to_name(err)); + return err; + } + setStatusLed(false); + return ESP_OK; +} + esp_err_t GatewayNetworkService::startHttpServer() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = config_.http_port; @@ -173,26 +365,27 @@ esp_err_t GatewayNetworkService::startHttpServer() { 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; - } + struct Route { + const char* uri; + httpd_method_t method; + esp_err_t (*handler)(httpd_req_t*); + }; - 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; + const Route routes[] = { + {"/info", HTTP_GET, &GatewayNetworkService::HandleInfoGet}, + {"/dali/cmd", HTTP_GET, &GatewayNetworkService::HandleCommandGet}, + {"/dali/cmd", HTTP_POST, &GatewayNetworkService::HandleCommandPost}, + {"/led/1", HTTP_GET, &GatewayNetworkService::HandleLedOnGet}, + {"/led/0", HTTP_GET, &GatewayNetworkService::HandleLedOffGet}, + {"/jq.js", HTTP_GET, &GatewayNetworkService::HandleJqJsGet}, + }; + + for (const auto& route : routes) { + err = RegisterUri(http_server_, route.uri, route.method, route.handler, this); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to register %s handler: %s", route.uri, esp_err_to_name(err)); + return err; + } } return ESP_OK; @@ -220,6 +413,52 @@ void GatewayNetworkService::UdpTaskEntry(void* arg) { static_cast(arg)->udpTaskLoop(); } +void GatewayNetworkService::HandleWifiEvent(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) { + auto* service = static_cast(arg); + if (service != nullptr) { + service->handleWifiEvent(event_base, event_id, event_data); + } +} + +void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t event_id, + void* event_data) { + if (!config_.wifi_enabled) { + return; + } + + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + const auto info = runtime_.deviceInfo(); + if (info.wlan.has_value() && !info.wlan->ssid.empty()) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); + } + return; + } + + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + auto info = runtime_.deviceInfo(); + if (info.wlan.has_value()) { + const bool has_credentials = !info.wlan->ssid.empty(); + info.wlan->ip.clear(); + runtime_.setWirelessInfo(std::move(*info.wlan)); + if (has_credentials) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); + } + } + return; + } + + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP && event_data != nullptr) { + auto* event = static_cast(event_data); + char ip[16] = {0}; + esp_ip4addr_ntoa(&event->ip_info.ip, ip, sizeof(ip)); + WirelessInfo wireless = runtime_.deviceInfo().wlan.value_or(WirelessInfo{}); + wireless.ip = ip; + runtime_.setWirelessInfo(std::move(wireless)); + ESP_LOGI(kTag, "Wi-Fi got IP %s", ip); + } +} + void GatewayNetworkService::udpTaskLoop() { udp_socket_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (udp_socket_ < 0) { @@ -285,6 +524,39 @@ void GatewayNetworkService::handleGatewayNotification(const std::vector reinterpret_cast(&remote_addr), remote_addr_len); } +void GatewayNetworkService::handleWifiControl(uint8_t mode) { + if (mode == 0) { + config_.wifi_enabled = false; + if (wifi_started_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); + wifi_started_ = false; + setup_ap_started_ = false; + } + auto info = runtime_.deviceInfo(); + if (info.wlan.has_value()) { + info.wlan->ip.clear(); + runtime_.setWirelessInfo(std::move(*info.wlan)); + } + return; + } + + if (mode == 101) { + config_.wifi_enabled = true; + ESP_ERROR_CHECK_WITHOUT_ABORT(startSetupAp()); + return; + } + if (mode == 1) { + config_.wifi_enabled = true; + if (setup_ap_started_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); + wifi_started_ = false; + setup_ap_started_ = false; + } + ESP_ERROR_CHECK_WITHOUT_ABORT(startWifi()); + } +} + std::string GatewayNetworkService::deviceInfoJson() const { const auto info = runtime_.deviceInfo(); cJSON* root = cJSON_CreateObject(); @@ -332,6 +604,61 @@ std::string GatewayNetworkService::deviceInfoDoubleEncodedJson() const { return rendered; } +std::string GatewayNetworkService::gatewaySnapshotJson() { + const auto snapshot = controller_.snapshot(); + cJSON* root = cJSON_CreateObject(); + if (root == nullptr) { + return {}; + } + + cJSON_AddNumberToObject(root, "count", static_cast(snapshot.channels.size())); + + cJSON* gw_info = cJSON_CreateObject(); + if (gw_info != nullptr) { + cJSON_AddBoolToObject(gw_info, "setupMode", snapshot.setup_mode); + cJSON_AddBoolToObject(gw_info, "bleEnabled", snapshot.ble_enabled); + cJSON_AddBoolToObject(gw_info, "wifiEnabled", snapshot.wifi_enabled); + cJSON_AddBoolToObject(gw_info, "IPRouter", snapshot.ip_router_enabled); + cJSON_AddBoolToObject(gw_info, "iSceneEnabled", snapshot.internal_scene_supported); + cJSON_AddBoolToObject(gw_info, "iGroupEnabled", snapshot.internal_group_supported); + cJSON_AddItemToObject(root, "gwInfo", gw_info); + } + + cJSON* channels = cJSON_CreateArray(); + if (channels != nullptr) { + for (const auto& channel : snapshot.channels) { + cJSON* item = cJSON_CreateObject(); + if (item == nullptr) { + continue; + } + cJSON_AddNumberToObject(item, "channel", channel.channel_index + 1); + cJSON_AddNumberToObject(item, "gw", channel.gateway_id); + cJSON_AddStringToObject(item, "name", channel.name.c_str()); + cJSON_AddStringToObject(item, "phy", channel.phy.c_str()); + cJSON_AddNumberToObject(item, "sceneMaskLow", channel.scene_mask_low); + cJSON_AddNumberToObject(item, "sceneMaskHigh", channel.scene_mask_high); + cJSON_AddNumberToObject(item, "groupMaskLow", channel.group_mask_low); + cJSON_AddNumberToObject(item, "groupMaskHigh", channel.group_mask_high); + cJSON_AddBoolToObject(item, "isAllocAddr", channel.allocating); + cJSON_AddNumberToObject(item, "lastAllocAddr", channel.last_alloc_addr); + cJSON_AddItemToArray(channels, item); + } + cJSON_AddItemToObject(root, "channels", channels); + } + + const std::string rendered = PrintJson(root); + cJSON_Delete(root); + return rendered; +} + +void GatewayNetworkService::setStatusLed(bool on) { + if (config_.status_led_gpio < 0) { + return; + } + const bool level = config_.status_led_active_high ? on : !on; + gpio_set_level(static_cast(config_.status_led_gpio), level ? 1 : 0); +} + esp_err_t GatewayNetworkService::HandleInfoGet(httpd_req_t* req) { auto* service = static_cast(req->user_ctx); if (service == nullptr) { @@ -343,6 +670,17 @@ esp_err_t GatewayNetworkService::HandleInfoGet(httpd_req_t* req) { return httpd_resp_send(req, payload.data(), payload.size()); } +esp_err_t GatewayNetworkService::HandleCommandGet(httpd_req_t* req) { + auto* service = static_cast(req->user_ctx); + if (service == nullptr) { + return ESP_FAIL; + } + + const std::string payload = service->gatewaySnapshotJson(); + 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) { @@ -376,4 +714,46 @@ esp_err_t GatewayNetworkService::HandleCommandPost(httpd_req_t* req) { return httpd_resp_sendstr(req, "ok"); } +esp_err_t GatewayNetworkService::HandleLedOnGet(httpd_req_t* req) { + auto* service = static_cast(req->user_ctx); + if (service == nullptr) { + return ESP_FAIL; + } + service->setStatusLed(true); + httpd_resp_set_type(req, "text/plain"); + return httpd_resp_sendstr(req, "ok"); +} + +esp_err_t GatewayNetworkService::HandleLedOffGet(httpd_req_t* req) { + auto* service = static_cast(req->user_ctx); + if (service == nullptr) { + return ESP_FAIL; + } + service->setStatusLed(false); + httpd_resp_set_type(req, "text/plain"); + return httpd_resp_sendstr(req, "ok"); +} + +esp_err_t GatewayNetworkService::HandleJqJsGet(httpd_req_t* req) { + FILE* file = std::fopen("/jq.js", "rb"); + if (file == nullptr) { + return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Not Found/jq.js"); + } + + httpd_resp_set_type(req, "application/javascript"); + char buffer[512] = {0}; + while (true) { + const size_t read_len = std::fread(buffer, 1, sizeof(buffer), file); + if (read_len > 0 && httpd_resp_send_chunk(req, buffer, read_len) != ESP_OK) { + std::fclose(file); + return ESP_FAIL; + } + if (read_len < sizeof(buffer)) { + break; + } + } + std::fclose(file); + return httpd_resp_send_chunk(req, nullptr, 0); +} + } // 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 d6245fe..6bda29c 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -312,14 +312,23 @@ GatewayRuntime::CommandDropReason GatewayRuntime::lastEnqueueDropReason() const } void GatewayRuntime::setGatewayCount(size_t gateway_count) { + LockGuard guard(command_lock_); gateway_count_ = gateway_count; } void GatewayRuntime::setWirelessInfo(WirelessInfo info) { - if (!info.ssid.empty() || !info.password.empty()) { + bool should_persist_credentials = false; + { + LockGuard guard(command_lock_); + should_persist_credentials = (!info.ssid.empty() || !info.password.empty()) && + (!wireless_info_.has_value() || + wireless_info_->ssid != info.ssid || + wireless_info_->password != info.password); + wireless_info_ = info; + } + if (should_persist_credentials) { settings_.setWifiCredentials(info.ssid, info.password); } - wireless_info_ = std::move(info); } void GatewayRuntime::setCommandAddressResolver( @@ -333,6 +342,7 @@ void GatewayRuntime::setCommandAddressResolver( } GatewayDeviceInfo GatewayRuntime::deviceInfo() const { + LockGuard guard(command_lock_); GatewayDeviceInfo info; info.serial_id = config_.serial_id; info.type = GatewayCore::RoleToString(profile_.role);