feat(gateway): add ESP-Touch smartconfig provisioning support and enhance network management

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Tony
2026-04-30 18:49:38 +08:00
parent 4ce3513dd2
commit ae4669e1b3
10 changed files with 338 additions and 43 deletions
+2 -2
View File
@@ -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.
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.
+22
View File
@@ -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
+20
View File
@@ -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
+3
View File
@@ -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
@@ -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<uint8_t, SceneStore> scenes_;
std::map<uint8_t, GroupStore> groups_;
bool setup_mode_{false};
bool wireless_setup_mode_{false};
bool ble_enabled_{false};
bool wifi_enabled_{false};
bool ip_router_enabled_{true};
@@ -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<uint8_t>& 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<uint8_t>& 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<uint8_t>(brightness, 254);
scene_data->color_mode = std::min<uint8_t>(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<uint8_t>(brightness, 254);
const uint8_t next_color_mode = std::min<uint8_t>(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;
@@ -2,6 +2,7 @@
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
@@ -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<WirelessInfo> smartconfig_pending_wireless_;
bool espnow_connected_{false};
std::array<uint8_t, 6> espnow_peer_{};
TaskHandle_t boot_button_task_handle_{nullptr};
@@ -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<const char*>(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<smartconfig_event_got_ssid_pswd_t*>(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<ip_event_got_ip_t*>(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<uint8_t>
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);
@@ -4,6 +4,7 @@
#include <cstdint>
#include <deque>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <string_view>
@@ -127,6 +128,7 @@ class GatewayRuntime {
GatewaySettingsStore settings_;
std::optional<std::vector<uint8_t>> current_command_;
std::deque<std::vector<uint8_t>> pending_commands_;
mutable std::map<uint8_t, std::string> gateway_names_;
size_t gateway_count_{0};
bool ble_enabled_{false};
CommandDropReason last_enqueue_drop_reason_{CommandDropReason::kNone};
@@ -119,7 +119,14 @@ std::optional<std::string> 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 {