diff --git a/README.md b/README.md index 3329bde..d6333ff 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway. - `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks. - `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications. - `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out. - - `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, setup AP mode, ESP-NOW setup ingress, and BOOT-button Wi-Fi reset 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, ESP-Touch smartconfig, setup AP mode, ESP-NOW setup ingress, and BOOT-button Wi-Fi reset for the native gateway. - `gateway_runtime/`: persistent runtime state, command queueing, and device info services. - `gateway_usb_setup/`: optional USB Serial/JTAG setup bridge; disabled by default so USB remains available for debug at boot. ## 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 with raw receive fan-out, 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, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, and a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, Wi-Fi STA lifecycle, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, and BOOT-button Wi-Fi credential clearing. Startup behavior is configured in `main/Kconfig.projbuild`: BLE is enabled by default, Wi-Fi STA and ESP-NOW setup mode are disabled by default, and the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected. 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 +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 with raw receive fan-out, 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, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, and a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, and BOOT-button Wi-Fi credential clearing. Startup behavior is configured in `main/Kconfig.projbuild`: BLE is enabled by default, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, and the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. 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 8b970bf..af10850 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -293,6 +293,13 @@ config GATEWAY_ESPNOW_SETUP_SUPPORTED help Enables ESP-NOW setup ingress when setup AP mode is entered. +config GATEWAY_SMARTCONFIG_SUPPORTED + bool "ESP-Touch smartconfig provisioning is supported" + depends on GATEWAY_WIFI_SUPPORTED + default y + help + Enables Lua-compatible ESP-Touch smartconfig provisioning for Wi-Fi credentials. + config GATEWAY_START_ESPNOW_SETUP_ENABLED bool "Enter ESP-NOW setup mode at startup" depends on GATEWAY_ESPNOW_SETUP_SUPPORTED @@ -300,6 +307,21 @@ config GATEWAY_START_ESPNOW_SETUP_ENABLED help Starts the setup AP and ESP-NOW setup ingress immediately at boot. Disabled by default. +config GATEWAY_START_SMARTCONFIG_ENABLED + bool "Start ESP-Touch smartconfig at startup" + depends on GATEWAY_SMARTCONFIG_SUPPORTED + default n + help + Starts ESP-Touch provisioning at boot. Disabled by default so Wi-Fi stays off unless configured. + +config GATEWAY_SMARTCONFIG_TIMEOUT_SEC + int "ESP-Touch smartconfig timeout seconds" + depends on GATEWAY_SMARTCONFIG_SUPPORTED + range 15 255 + default 60 + help + Timeout passed to ESP-IDF smartconfig before provisioning restarts internally. + choice GATEWAY_USB_STARTUP_MODE prompt "USB Serial/JTAG startup mode" default GATEWAY_USB_STARTUP_DEBUG_JTAG diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 4744c0d..b6e731f 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -53,6 +53,10 @@ #define CONFIG_GATEWAY_USB_SETUP_READ_TIMEOUT_MS 20 #endif +#ifndef CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC +#define CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC 60 +#endif + namespace { constexpr const char* kProjectName = "DALI_485_Gateway"; constexpr const char* kProjectVersion = "0.1.0"; @@ -94,6 +98,18 @@ constexpr bool kEspnowSetupStartupEnabled = true; constexpr bool kEspnowSetupStartupEnabled = false; #endif +#ifdef CONFIG_GATEWAY_SMARTCONFIG_SUPPORTED +constexpr bool kSmartconfigSupported = true; +#else +constexpr bool kSmartconfigSupported = false; +#endif + +#ifdef CONFIG_GATEWAY_START_SMARTCONFIG_ENABLED +constexpr bool kSmartconfigStartupEnabled = true; +#else +constexpr bool kSmartconfigStartupEnabled = false; +#endif + #ifdef CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL constexpr bool kUsbSetupStartupEnabled = true; #else @@ -351,6 +367,10 @@ extern "C" void app_main(void) { network_config.espnow_setup_enabled = profile.enable_espnow; network_config.espnow_setup_startup_enabled = profile.enable_espnow && kEspnowSetupStartupEnabled; + network_config.smartconfig_enabled = profile.enable_wifi && kSmartconfigSupported; + network_config.smartconfig_startup_enabled = + profile.enable_wifi && kSmartconfigSupported && kSmartconfigStartupEnabled; + network_config.smartconfig_timeout_sec = CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC; #ifdef CONFIG_GATEWAY_NETWORK_HTTP_ENABLED network_config.http_enabled = true; #else diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index cc5abec..820310c 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -628,7 +628,10 @@ CONFIG_GATEWAY_START_BLE_ENABLED=y CONFIG_GATEWAY_WIFI_SUPPORTED=y # CONFIG_GATEWAY_START_WIFI_STA_ENABLED is not set CONFIG_GATEWAY_ESPNOW_SETUP_SUPPORTED=y +CONFIG_GATEWAY_SMARTCONFIG_SUPPORTED=y # CONFIG_GATEWAY_START_ESPNOW_SETUP_ENABLED is not set +# CONFIG_GATEWAY_START_SMARTCONFIG_ENABLED is not set +CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC=60 CONFIG_GATEWAY_USB_STARTUP_DEBUG_JTAG=y # CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set # end of Gateway Startup Services diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index de11072..fc5cb6c 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -46,6 +46,7 @@ struct GatewayChannelSnapshot { struct GatewayControllerSnapshot { bool setup_mode{false}; + bool wireless_setup_mode{false}; bool ble_enabled{false}; bool wifi_enabled{false}; bool ip_router_enabled{false}; @@ -73,6 +74,8 @@ class GatewayController { void addGatewayNameSink(GatewayNameSink sink); bool setupMode() const; + bool wirelessSetupMode() const; + void setWirelessSetupMode(bool enabled); bool bleEnabled() const; bool wifiEnabled() const; bool ipRouterEnabled() const; @@ -174,6 +177,7 @@ class GatewayController { std::map scenes_; std::map groups_; bool setup_mode_{false}; + bool wireless_setup_mode_{false}; bool ble_enabled_{false}; bool wifi_enabled_{false}; bool ip_router_enabled_{true}; diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index 2395347..165ec2c 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -185,6 +185,14 @@ bool GatewayController::setupMode() const { return setup_mode_; } +bool GatewayController::wirelessSetupMode() const { + return wireless_setup_mode_; +} + +void GatewayController::setWirelessSetupMode(bool enabled) { + wireless_setup_mode_ = enabled; +} + bool GatewayController::bleEnabled() const { return ble_enabled_; } @@ -200,6 +208,7 @@ bool GatewayController::ipRouterEnabled() const { GatewayControllerSnapshot GatewayController::snapshot() { GatewayControllerSnapshot out; out.setup_mode = setup_mode_; + out.wireless_setup_mode = wireless_setup_mode_; out.ble_enabled = ble_enabled_; out.wifi_enabled = wifi_enabled_; out.ip_router_enabled = ip_router_enabled_; @@ -302,8 +311,13 @@ void GatewayController::dispatchCommand(const std::vector& command) { case 0x04: if (addr == 0) { wifi_enabled_ = false; - } else if (addr == 1 || addr == 101) { + wireless_setup_mode_ = false; + } else if (addr == 1) { wifi_enabled_ = true; + wireless_setup_mode_ = false; + } else if (addr == 100 || addr == 101) { + wifi_enabled_ = true; + wireless_setup_mode_ = true; } for (const auto& sink : wifi_state_sinks_) { sink(addr); @@ -317,12 +331,9 @@ void GatewayController::dispatchCommand(const std::vector& command) { if (setup_mode_ && config_.setup_supported) { feature |= 0x01; } - if (config_.ble_supported) { + if (wireless_setup_mode_ && config_.wifi_supported) { feature |= 0x02; } - if (config_.wifi_supported) { - feature |= 0x04; - } if (config_.ip_router_supported && ip_router_enabled_) { feature |= 0x08; } @@ -593,6 +604,9 @@ bool GatewayController::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bo if (scene_data == nullptr) { return false; } + if (scene_data->enabled == enabled) { + return true; + } scene_data->enabled = enabled; return saveScene(gateway_id, scene_id); } @@ -604,21 +618,29 @@ bool GatewayController::setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uin if (scene_data == nullptr) { return false; } - scene_data->brightness = std::min(brightness, 254); - scene_data->color_mode = std::min(color_mode, 2); - if (scene_data->color_mode == 0) { - scene_data->data1 = data1; - scene_data->data2 = data2; - scene_data->data3 = 0; - } else if (scene_data->color_mode == 1) { - scene_data->data1 = data1; - scene_data->data2 = data2; - scene_data->data3 = data3; - } else { - scene_data->data1 = 0; - scene_data->data2 = 0; - scene_data->data3 = 0; + const uint8_t next_brightness = std::min(brightness, 254); + const uint8_t next_color_mode = std::min(color_mode, 2); + uint8_t next_data1 = 0; + uint8_t next_data2 = 0; + uint8_t next_data3 = 0; + if (next_color_mode == 0) { + next_data1 = data1; + next_data2 = data2; + } else if (next_color_mode == 1) { + next_data1 = data1; + next_data2 = data2; + next_data3 = data3; } + if (scene_data->brightness == next_brightness && scene_data->color_mode == next_color_mode && + scene_data->data1 == next_data1 && scene_data->data2 == next_data2 && + scene_data->data3 == next_data3) { + return true; + } + scene_data->brightness = next_brightness; + scene_data->color_mode = next_color_mode; + scene_data->data1 = next_data1; + scene_data->data2 = next_data2; + scene_data->data3 = next_data3; return saveScene(gateway_id, scene_id); } @@ -627,7 +649,11 @@ bool GatewayController::setSceneName(uint8_t gateway_id, uint8_t scene_id, std:: if (scene_data == nullptr) { return false; } - scene_data->name = NormalizeName(name); + const auto normalized = NormalizeName(name); + if (scene_data->name == normalized) { + return true; + } + scene_data->name = normalized; return saveSceneName(gateway_id, scene_id, scene_data->name); } @@ -636,6 +662,13 @@ bool GatewayController::deleteScene(uint8_t gateway_id, uint8_t scene_id) { if (scene_data == nullptr) { return false; } + const bool already_default = !scene_data->enabled && scene_data->brightness == 254 && + scene_data->color_mode == 2 && scene_data->data1 == 0 && + scene_data->data2 == 0 && scene_data->data3 == 0 && + scene_data->name.empty(); + if (already_default) { + return true; + } *scene_data = InternalScene{}; deleteSceneStorage(gateway_id, scene_id); deleteSceneNameStorage(gateway_id, scene_id); @@ -688,6 +721,9 @@ bool GatewayController::setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bo if (group_data == nullptr) { return false; } + if (group_data->enabled == enabled) { + return true; + } group_data->enabled = enabled; return saveGroup(gateway_id, group_id); } @@ -698,8 +734,13 @@ bool GatewayController::setGroupDetail(uint8_t gateway_id, uint8_t group_id, uin if (group_data == nullptr) { return false; } - group_data->target_type = normalizeGroupTargetType(target_type); - group_data->target_value = normalizeGroupTargetValue(group_data->target_type, target_value); + const uint8_t next_target_type = normalizeGroupTargetType(target_type); + const uint8_t next_target_value = normalizeGroupTargetValue(next_target_type, target_value); + if (group_data->target_type == next_target_type && group_data->target_value == next_target_value) { + return true; + } + group_data->target_type = next_target_type; + group_data->target_value = next_target_value; return saveGroup(gateway_id, group_id); } @@ -708,7 +749,11 @@ bool GatewayController::setGroupName(uint8_t gateway_id, uint8_t group_id, std:: if (group_data == nullptr) { return false; } - group_data->name = NormalizeName(name); + const auto normalized = NormalizeName(name); + if (group_data->name == normalized) { + return true; + } + group_data->name = normalized; return saveGroupName(gateway_id, group_id, group_data->name); } @@ -717,6 +762,11 @@ bool GatewayController::deleteGroup(uint8_t gateway_id, uint8_t group_id) { if (group_data == nullptr) { return false; } + const bool already_default = !group_data->enabled && group_data->target_type == 2 && + group_data->target_value == 0 && group_data->name.empty(); + if (already_default) { + return true; + } *group_data = InternalGroup{}; deleteGroupStorage(gateway_id, group_id); deleteGroupNameStorage(gateway_id, group_id); @@ -1125,7 +1175,10 @@ bool GatewayController::eraseKey(std::string_view key) { return false; } const esp_err_t err = nvs_erase_key(storage_, std::string(key).c_str()); - if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) { + if (err == ESP_ERR_NVS_NOT_FOUND) { + return true; + } + if (err != ESP_OK) { return false; } return nvs_commit(storage_) == ESP_OK; diff --git a/components/gateway_network/include/gateway_network.hpp b/components/gateway_network/include/gateway_network.hpp index fcafaee..58d395a 100644 --- a/components/gateway_network/include/gateway_network.hpp +++ b/components/gateway_network/include/gateway_network.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -13,6 +14,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" +#include "gateway_runtime.hpp" #include "lwip/sockets.h" namespace gateway { @@ -20,12 +22,14 @@ namespace gateway { class GatewayController; class DaliDomainService; struct DaliRawFrame; -class GatewayRuntime; struct GatewayNetworkServiceConfig { bool wifi_enabled{true}; bool espnow_setup_enabled{true}; bool espnow_setup_startup_enabled{false}; + bool smartconfig_enabled{true}; + bool smartconfig_startup_enabled{false}; + uint8_t smartconfig_timeout_sec{60}; bool http_enabled{true}; bool udp_enabled{true}; uint16_t http_port{80}; @@ -65,6 +69,8 @@ class GatewayNetworkService { esp_err_t ensureNetworkStack(); esp_err_t startWifi(); esp_err_t startSetupAp(); + esp_err_t startSmartconfig(); + void stopSmartconfig(); esp_err_t startEspNow(); void stopEspNow(); esp_err_t addEspNowPeer(const uint8_t* mac, bool broadcast = false); @@ -96,8 +102,12 @@ class GatewayNetworkService { esp_netif_t* wifi_sta_netif_{nullptr}; esp_netif_t* wifi_ap_netif_{nullptr}; bool wifi_started_{false}; + bool wifi_event_handlers_registered_{false}; bool setup_ap_started_{false}; bool espnow_started_{false}; + bool smartconfig_started_{false}; + bool smartconfig_event_handler_registered_{false}; + std::optional smartconfig_pending_wireless_; bool espnow_connected_{false}; std::array espnow_peer_{}; TaskHandle_t boot_button_task_handle_{nullptr}; diff --git a/components/gateway_network/src/gateway_network.cpp b/components/gateway_network/src/gateway_network.cpp index 8a0617c..6457640 100644 --- a/components/gateway_network/src/gateway_network.cpp +++ b/components/gateway_network/src/gateway_network.cpp @@ -10,6 +10,7 @@ #include "esp_log.h" #include "esp_netif.h" #include "esp_netif_ip_addr.h" +#include "esp_smartconfig.h" #include "esp_system.h" #include "esp_wifi.h" #include "lwip/inet.h" @@ -91,6 +92,17 @@ std::string MacToHex(const uint8_t mac[6]) { return std::string(out); } +std::string BoundedString(const uint8_t* data, size_t len) { + if (data == nullptr) { + return {}; + } + size_t actual_len = 0; + while (actual_len < len && data[actual_len] != 0) { + ++actual_len; + } + return std::string(reinterpret_cast(data), actual_len); +} + std::string LocalMacHex(wifi_interface_t interface) { uint8_t mac[6] = {}; if (esp_wifi_get_mac(interface, mac) != ESP_OK) { @@ -194,6 +206,11 @@ esp_err_t GatewayNetworkService::start() { if (err != ESP_OK) { return err; } + } else if (config_.smartconfig_startup_enabled) { + err = startSmartconfig(); + if (err != ESP_OK) { + return err; + } } else if (config_.wifi_enabled) { err = startWifi(); if (err != ESP_OK) { @@ -261,6 +278,7 @@ esp_err_t GatewayNetworkService::startWifi() { if (wifi_started_) { return ESP_OK; } + stopSmartconfig(); stopEspNow(); setup_ap_started_ = false; @@ -279,18 +297,21 @@ esp_err_t GatewayNetworkService::startWifi() { 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; - } + if (!wifi_event_handlers_registered_) { + 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; + 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; + } + wifi_event_handlers_registered_ = true; } ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_storage(WIFI_STORAGE_RAM)); @@ -329,12 +350,14 @@ esp_err_t GatewayNetworkService::startWifi() { } wifi_started_ = true; + controller_.setWirelessSetupMode(false); 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() { + stopSmartconfig(); if (wifi_ap_netif_ == nullptr) { wifi_ap_netif_ = esp_netif_create_default_wifi_ap(); if (wifi_ap_netif_ == nullptr) { @@ -397,10 +420,73 @@ esp_err_t GatewayNetworkService::startSetupAp() { wifi_started_ = true; setup_ap_started_ = true; + controller_.setWirelessSetupMode(true); ESP_LOGI(kTag, "setup AP started ssid=%s ip=192.168.3.1", kSetupApSsid); return ESP_OK; } +esp_err_t GatewayNetworkService::startSmartconfig() { + if (!config_.smartconfig_enabled) { + ESP_LOGW(kTag, "smartconfig requested but not supported"); + return ESP_ERR_NOT_SUPPORTED; + } + if (smartconfig_started_) { + return ESP_OK; + } + + config_.wifi_enabled = true; + if (setup_ap_started_) { + stopEspNow(); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); + wifi_started_ = false; + setup_ap_started_ = false; + } + + esp_err_t err = startWifi(); + if (err != ESP_OK) { + return err; + } + + if (!smartconfig_event_handler_registered_) { + err = esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, + &GatewayNetworkService::HandleWifiEvent, this); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(kTag, "failed to register smartconfig event handler: %s", esp_err_to_name(err)); + return err; + } + smartconfig_event_handler_registered_ = true; + } + + err = esp_smartconfig_set_type(SC_TYPE_ESPTOUCH); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to set smartconfig type: %s", esp_err_to_name(err)); + return err; + } + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_esptouch_set_timeout(config_.smartconfig_timeout_sec)); + + smartconfig_start_config_t smartconfig = SMARTCONFIG_START_CONFIG_DEFAULT(); + err = esp_smartconfig_start(&smartconfig); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to start smartconfig: %s", esp_err_to_name(err)); + return err; + } + + smartconfig_pending_wireless_.reset(); + smartconfig_started_ = true; + controller_.setWirelessSetupMode(true); + ESP_LOGI(kTag, "ESP-Touch smartconfig started timeout=%us", config_.smartconfig_timeout_sec); + return ESP_OK; +} + +void GatewayNetworkService::stopSmartconfig() { + if (!smartconfig_started_) { + return; + } + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_smartconfig_stop()); + smartconfig_started_ = false; + smartconfig_pending_wireless_.reset(); +} + esp_err_t GatewayNetworkService::startEspNow() { if (espnow_started_) { return ESP_OK; @@ -629,6 +715,42 @@ void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t return; } + if (event_base == SC_EVENT) { + if (event_id == SC_EVENT_SCAN_DONE) { + ESP_LOGI(kTag, "smartconfig scan done"); + } else if (event_id == SC_EVENT_FOUND_CHANNEL) { + ESP_LOGI(kTag, "smartconfig found channel"); + } else if (event_id == SC_EVENT_GOT_SSID_PSWD && event_data != nullptr) { + auto* event = static_cast(event_data); + wifi_config_t wifi_config = {}; + std::memcpy(wifi_config.sta.ssid, event->ssid, sizeof(wifi_config.sta.ssid)); + std::memcpy(wifi_config.sta.password, event->password, sizeof(wifi_config.sta.password)); + wifi_config.sta.bssid_set = event->bssid_set; + if (event->bssid_set) { + std::memcpy(wifi_config.sta.bssid, event->bssid, sizeof(wifi_config.sta.bssid)); + } + + WirelessInfo wireless; + wireless.ssid = BoundedString(event->ssid, sizeof(event->ssid)); + wireless.password = BoundedString(event->password, sizeof(event->password)); + uint8_t mac[6] = {}; + if (esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) { + wireless.mac = MacToHex(mac); + } + smartconfig_pending_wireless_ = wireless; + + ESP_LOGI(kTag, "smartconfig got credentials ssid=%s", wireless.ssid.c_str()); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); + } else if (event_id == SC_EVENT_SEND_ACK_DONE) { + stopSmartconfig(); + controller_.setWirelessSetupMode(false); + ESP_LOGI(kTag, "smartconfig ACK sent"); + } + 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()) { @@ -643,7 +765,7 @@ void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t const bool has_credentials = !info.wlan->ssid.empty(); info.wlan->ip.clear(); runtime_.setWirelessInfo(std::move(*info.wlan)); - if (has_credentials) { + if (has_credentials && !smartconfig_started_) { ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } } @@ -654,8 +776,13 @@ void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t 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{}); + WirelessInfo wireless = smartconfig_pending_wireless_.value_or( + runtime_.deviceInfo().wlan.value_or(WirelessInfo{})); wireless.ip = ip; + uint8_t mac[6] = {}; + if (esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) { + wireless.mac = MacToHex(mac); + } runtime_.setWirelessInfo(std::move(wireless)); ESP_LOGI(kTag, "Wi-Fi got IP %s", ip); } @@ -903,7 +1030,9 @@ void GatewayNetworkService::handleGatewayNotification(const std::vector void GatewayNetworkService::handleWifiControl(uint8_t mode) { if (mode == 0) { config_.wifi_enabled = false; + stopSmartconfig(); stopEspNow(); + controller_.setWirelessSetupMode(false); if (wifi_started_) { ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); @@ -923,8 +1052,13 @@ void GatewayNetworkService::handleWifiControl(uint8_t mode) { ESP_ERROR_CHECK_WITHOUT_ABORT(startSetupAp()); return; } + if (mode == 100) { + ESP_ERROR_CHECK_WITHOUT_ABORT(startSmartconfig()); + return; + } if (mode == 1) { config_.wifi_enabled = true; + stopSmartconfig(); if (setup_ap_started_) { stopEspNow(); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); @@ -994,6 +1128,7 @@ std::string GatewayNetworkService::gatewaySnapshotJson() { cJSON* gw_info = cJSON_CreateObject(); if (gw_info != nullptr) { cJSON_AddBoolToObject(gw_info, "setupMode", snapshot.setup_mode); + cJSON_AddBoolToObject(gw_info, "wlSetupMode", snapshot.wireless_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); diff --git a/components/gateway_runtime/include/gateway_runtime.hpp b/components/gateway_runtime/include/gateway_runtime.hpp index 3898093..c4a9f3e 100644 --- a/components/gateway_runtime/include/gateway_runtime.hpp +++ b/components/gateway_runtime/include/gateway_runtime.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -127,6 +128,7 @@ class GatewayRuntime { GatewaySettingsStore settings_; std::optional> current_command_; std::deque> pending_commands_; + mutable std::map gateway_names_; size_t gateway_count_{0}; bool ble_enabled_{false}; CommandDropReason last_enqueue_drop_reason_{CommandDropReason::kNone}; diff --git a/components/gateway_runtime/src/gateway_runtime.cpp b/components/gateway_runtime/src/gateway_runtime.cpp index 0260897..24f6627 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -119,7 +119,14 @@ std::optional GatewaySettingsStore::getWifiPassword() const { bool GatewaySettingsStore::setWifiCredentials(std::string_view ssid, std::string_view password) { - return writeString(kWifiSsidKey, ssid) && writeString(kWifiPasswordKey, password); + if (handle_ == 0) { + return false; + } + + const esp_err_t ssid_err = nvs_set_str(handle_, kWifiSsidKey, std::string(ssid).c_str()); + const esp_err_t password_err = + nvs_set_str(handle_, kWifiPasswordKey, std::string(password).c_str()); + return ssid_err == ESP_OK && password_err == ESP_OK && nvs_commit(handle_) == ESP_OK; } bool GatewaySettingsStore::clearWifiCredentials() { @@ -129,12 +136,17 @@ bool GatewaySettingsStore::clearWifiCredentials() { esp_err_t ssid_err = nvs_erase_key(handle_, kWifiSsidKey); esp_err_t password_err = nvs_erase_key(handle_, kWifiPasswordKey); + const bool ssid_missing = ssid_err == ESP_ERR_NVS_NOT_FOUND; + const bool password_missing = password_err == ESP_ERR_NVS_NOT_FOUND; if (ssid_err == ESP_ERR_NVS_NOT_FOUND) { ssid_err = ESP_OK; } if (password_err == ESP_ERR_NVS_NOT_FOUND) { password_err = ESP_OK; } + if (ssid_missing && password_missing) { + return true; + } return ssid_err == ESP_OK && password_err == ESP_OK && nvs_commit(handle_) == ESP_OK; } @@ -348,10 +360,16 @@ void GatewayRuntime::setWirelessInfo(WirelessInfo info) { } bool GatewayRuntime::clearWirelessInfo() { + bool had_credentials = false; { LockGuard guard(command_lock_); + had_credentials = wireless_info_.has_value() && + (!wireless_info_->ssid.empty() || !wireless_info_->password.empty()); wireless_info_.reset(); } + if (!had_credentials) { + return true; + } return settings_.clearWifiCredentials(); } @@ -379,19 +397,37 @@ GatewayDeviceInfo GatewayRuntime::deviceInfo() const { } bool GatewayRuntime::bleEnabled() const { + LockGuard guard(command_lock_); return ble_enabled_; } bool GatewayRuntime::setBleEnabled(bool enabled) { + { + LockGuard guard(command_lock_); + if (ble_enabled_ == enabled) { + return true; + } + } if (!settings_.setBleEnabled(enabled)) { return false; } + LockGuard guard(command_lock_); ble_enabled_ = enabled; return true; } std::string GatewayRuntime::gatewayName(uint8_t gateway_id) const { - return settings_.getGatewayName(gateway_id, defaultGatewayName(gateway_id)); + LockGuard guard(command_lock_); + const auto cached = gateway_names_.find(gateway_id); + if (cached != gateway_names_.end()) { + return cached->second; + } + auto name = settings_.getGatewayName(gateway_id, defaultGatewayName(gateway_id)); + if (name.size() > kMaxGatewayNameBytes) { + name.resize(kMaxGatewayNameBytes); + } + gateway_names_[gateway_id] = name; + return name; } bool GatewayRuntime::setGatewayName(uint8_t gateway_id, std::string_view name) { @@ -403,7 +439,17 @@ bool GatewayRuntime::setGatewayName(uint8_t gateway_id, std::string_view name) { normalized = defaultGatewayName(gateway_id); } - return settings_.setGatewayName(gateway_id, normalized); + if (gatewayName(gateway_id) == normalized) { + return true; + } + + if (!settings_.setGatewayName(gateway_id, normalized)) { + return false; + } + + LockGuard guard(command_lock_); + gateway_names_[gateway_id] = normalized; + return true; } std::string GatewayRuntime::gatewaySerialHex(uint8_t gateway_id) const {