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. - `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_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_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_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. - `gateway_usb_setup/`: optional USB Serial/JTAG setup bridge; disabled by default so USB remains available for debug at boot.
## Current status ## 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 help
Enables ESP-NOW setup ingress when setup AP mode is entered. 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 config GATEWAY_START_ESPNOW_SETUP_ENABLED
bool "Enter ESP-NOW setup mode at startup" bool "Enter ESP-NOW setup mode at startup"
depends on GATEWAY_ESPNOW_SETUP_SUPPORTED depends on GATEWAY_ESPNOW_SETUP_SUPPORTED
@@ -300,6 +307,21 @@ config GATEWAY_START_ESPNOW_SETUP_ENABLED
help help
Starts the setup AP and ESP-NOW setup ingress immediately at boot. Disabled by default. 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 choice GATEWAY_USB_STARTUP_MODE
prompt "USB Serial/JTAG startup mode" prompt "USB Serial/JTAG startup mode"
default GATEWAY_USB_STARTUP_DEBUG_JTAG default GATEWAY_USB_STARTUP_DEBUG_JTAG
+20
View File
@@ -53,6 +53,10 @@
#define CONFIG_GATEWAY_USB_SETUP_READ_TIMEOUT_MS 20 #define CONFIG_GATEWAY_USB_SETUP_READ_TIMEOUT_MS 20
#endif #endif
#ifndef CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC
#define CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC 60
#endif
namespace { namespace {
constexpr const char* kProjectName = "DALI_485_Gateway"; constexpr const char* kProjectName = "DALI_485_Gateway";
constexpr const char* kProjectVersion = "0.1.0"; constexpr const char* kProjectVersion = "0.1.0";
@@ -94,6 +98,18 @@ constexpr bool kEspnowSetupStartupEnabled = true;
constexpr bool kEspnowSetupStartupEnabled = false; constexpr bool kEspnowSetupStartupEnabled = false;
#endif #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 #ifdef CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL
constexpr bool kUsbSetupStartupEnabled = true; constexpr bool kUsbSetupStartupEnabled = true;
#else #else
@@ -351,6 +367,10 @@ extern "C" void app_main(void) {
network_config.espnow_setup_enabled = profile.enable_espnow; network_config.espnow_setup_enabled = profile.enable_espnow;
network_config.espnow_setup_startup_enabled = network_config.espnow_setup_startup_enabled =
profile.enable_espnow && kEspnowSetupStartupEnabled; 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 #ifdef CONFIG_GATEWAY_NETWORK_HTTP_ENABLED
network_config.http_enabled = true; network_config.http_enabled = true;
#else #else
+3
View File
@@ -628,7 +628,10 @@ CONFIG_GATEWAY_START_BLE_ENABLED=y
CONFIG_GATEWAY_WIFI_SUPPORTED=y CONFIG_GATEWAY_WIFI_SUPPORTED=y
# CONFIG_GATEWAY_START_WIFI_STA_ENABLED is not set # CONFIG_GATEWAY_START_WIFI_STA_ENABLED is not set
CONFIG_GATEWAY_ESPNOW_SETUP_SUPPORTED=y CONFIG_GATEWAY_ESPNOW_SETUP_SUPPORTED=y
CONFIG_GATEWAY_SMARTCONFIG_SUPPORTED=y
# CONFIG_GATEWAY_START_ESPNOW_SETUP_ENABLED is not set # 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_DEBUG_JTAG=y
# CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set # CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set
# end of Gateway Startup Services # end of Gateway Startup Services
@@ -46,6 +46,7 @@ struct GatewayChannelSnapshot {
struct GatewayControllerSnapshot { struct GatewayControllerSnapshot {
bool setup_mode{false}; bool setup_mode{false};
bool wireless_setup_mode{false};
bool ble_enabled{false}; bool ble_enabled{false};
bool wifi_enabled{false}; bool wifi_enabled{false};
bool ip_router_enabled{false}; bool ip_router_enabled{false};
@@ -73,6 +74,8 @@ class GatewayController {
void addGatewayNameSink(GatewayNameSink sink); void addGatewayNameSink(GatewayNameSink sink);
bool setupMode() const; bool setupMode() const;
bool wirelessSetupMode() const;
void setWirelessSetupMode(bool enabled);
bool bleEnabled() const; bool bleEnabled() const;
bool wifiEnabled() const; bool wifiEnabled() const;
bool ipRouterEnabled() const; bool ipRouterEnabled() const;
@@ -174,6 +177,7 @@ class GatewayController {
std::map<uint8_t, SceneStore> scenes_; std::map<uint8_t, SceneStore> scenes_;
std::map<uint8_t, GroupStore> groups_; std::map<uint8_t, GroupStore> groups_;
bool setup_mode_{false}; bool setup_mode_{false};
bool wireless_setup_mode_{false};
bool ble_enabled_{false}; bool ble_enabled_{false};
bool wifi_enabled_{false}; bool wifi_enabled_{false};
bool ip_router_enabled_{true}; bool ip_router_enabled_{true};
@@ -185,6 +185,14 @@ bool GatewayController::setupMode() const {
return setup_mode_; return setup_mode_;
} }
bool GatewayController::wirelessSetupMode() const {
return wireless_setup_mode_;
}
void GatewayController::setWirelessSetupMode(bool enabled) {
wireless_setup_mode_ = enabled;
}
bool GatewayController::bleEnabled() const { bool GatewayController::bleEnabled() const {
return ble_enabled_; return ble_enabled_;
} }
@@ -200,6 +208,7 @@ bool GatewayController::ipRouterEnabled() const {
GatewayControllerSnapshot GatewayController::snapshot() { GatewayControllerSnapshot GatewayController::snapshot() {
GatewayControllerSnapshot out; GatewayControllerSnapshot out;
out.setup_mode = setup_mode_; out.setup_mode = setup_mode_;
out.wireless_setup_mode = wireless_setup_mode_;
out.ble_enabled = ble_enabled_; out.ble_enabled = ble_enabled_;
out.wifi_enabled = wifi_enabled_; out.wifi_enabled = wifi_enabled_;
out.ip_router_enabled = ip_router_enabled_; out.ip_router_enabled = ip_router_enabled_;
@@ -302,8 +311,13 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
case 0x04: case 0x04:
if (addr == 0) { if (addr == 0) {
wifi_enabled_ = false; wifi_enabled_ = false;
} else if (addr == 1 || addr == 101) { wireless_setup_mode_ = false;
} else if (addr == 1) {
wifi_enabled_ = true; 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_) { for (const auto& sink : wifi_state_sinks_) {
sink(addr); sink(addr);
@@ -317,12 +331,9 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
if (setup_mode_ && config_.setup_supported) { if (setup_mode_ && config_.setup_supported) {
feature |= 0x01; feature |= 0x01;
} }
if (config_.ble_supported) { if (wireless_setup_mode_ && config_.wifi_supported) {
feature |= 0x02; feature |= 0x02;
} }
if (config_.wifi_supported) {
feature |= 0x04;
}
if (config_.ip_router_supported && ip_router_enabled_) { if (config_.ip_router_supported && ip_router_enabled_) {
feature |= 0x08; feature |= 0x08;
} }
@@ -593,6 +604,9 @@ bool GatewayController::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bo
if (scene_data == nullptr) { if (scene_data == nullptr) {
return false; return false;
} }
if (scene_data->enabled == enabled) {
return true;
}
scene_data->enabled = enabled; scene_data->enabled = enabled;
return saveScene(gateway_id, scene_id); 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) { if (scene_data == nullptr) {
return false; return false;
} }
scene_data->brightness = std::min<uint8_t>(brightness, 254); const uint8_t next_brightness = std::min<uint8_t>(brightness, 254);
scene_data->color_mode = std::min<uint8_t>(color_mode, 2); const uint8_t next_color_mode = std::min<uint8_t>(color_mode, 2);
if (scene_data->color_mode == 0) { uint8_t next_data1 = 0;
scene_data->data1 = data1; uint8_t next_data2 = 0;
scene_data->data2 = data2; uint8_t next_data3 = 0;
scene_data->data3 = 0; if (next_color_mode == 0) {
} else if (scene_data->color_mode == 1) { next_data1 = data1;
scene_data->data1 = data1; next_data2 = data2;
scene_data->data2 = data2; } else if (next_color_mode == 1) {
scene_data->data3 = data3; next_data1 = data1;
} else { next_data2 = data2;
scene_data->data1 = 0; next_data3 = data3;
scene_data->data2 = 0;
scene_data->data3 = 0;
} }
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); 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) { if (scene_data == nullptr) {
return false; 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); 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) { if (scene_data == nullptr) {
return false; 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{}; *scene_data = InternalScene{};
deleteSceneStorage(gateway_id, scene_id); deleteSceneStorage(gateway_id, scene_id);
deleteSceneNameStorage(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) { if (group_data == nullptr) {
return false; return false;
} }
if (group_data->enabled == enabled) {
return true;
}
group_data->enabled = enabled; group_data->enabled = enabled;
return saveGroup(gateway_id, group_id); 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) { if (group_data == nullptr) {
return false; return false;
} }
group_data->target_type = normalizeGroupTargetType(target_type); const uint8_t next_target_type = normalizeGroupTargetType(target_type);
group_data->target_value = normalizeGroupTargetValue(group_data->target_type, target_value); 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); 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) { if (group_data == nullptr) {
return false; 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); 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) { if (group_data == nullptr) {
return false; 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{}; *group_data = InternalGroup{};
deleteGroupStorage(gateway_id, group_id); deleteGroupStorage(gateway_id, group_id);
deleteGroupNameStorage(gateway_id, group_id); deleteGroupNameStorage(gateway_id, group_id);
@@ -1125,7 +1175,10 @@ bool GatewayController::eraseKey(std::string_view key) {
return false; return false;
} }
const esp_err_t err = nvs_erase_key(storage_, std::string(key).c_str()); 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 false;
} }
return nvs_commit(storage_) == ESP_OK; return nvs_commit(storage_) == ESP_OK;
@@ -2,6 +2,7 @@
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -13,6 +14,7 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/semphr.h" #include "freertos/semphr.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "gateway_runtime.hpp"
#include "lwip/sockets.h" #include "lwip/sockets.h"
namespace gateway { namespace gateway {
@@ -20,12 +22,14 @@ namespace gateway {
class GatewayController; class GatewayController;
class DaliDomainService; class DaliDomainService;
struct DaliRawFrame; struct DaliRawFrame;
class GatewayRuntime;
struct GatewayNetworkServiceConfig { struct GatewayNetworkServiceConfig {
bool wifi_enabled{true}; bool wifi_enabled{true};
bool espnow_setup_enabled{true}; bool espnow_setup_enabled{true};
bool espnow_setup_startup_enabled{false}; 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 http_enabled{true};
bool udp_enabled{true}; bool udp_enabled{true};
uint16_t http_port{80}; uint16_t http_port{80};
@@ -65,6 +69,8 @@ class GatewayNetworkService {
esp_err_t ensureNetworkStack(); esp_err_t ensureNetworkStack();
esp_err_t startWifi(); esp_err_t startWifi();
esp_err_t startSetupAp(); esp_err_t startSetupAp();
esp_err_t startSmartconfig();
void stopSmartconfig();
esp_err_t startEspNow(); esp_err_t startEspNow();
void stopEspNow(); void stopEspNow();
esp_err_t addEspNowPeer(const uint8_t* mac, bool broadcast = false); 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_sta_netif_{nullptr};
esp_netif_t* wifi_ap_netif_{nullptr}; esp_netif_t* wifi_ap_netif_{nullptr};
bool wifi_started_{false}; bool wifi_started_{false};
bool wifi_event_handlers_registered_{false};
bool setup_ap_started_{false}; bool setup_ap_started_{false};
bool espnow_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}; bool espnow_connected_{false};
std::array<uint8_t, 6> espnow_peer_{}; std::array<uint8_t, 6> espnow_peer_{};
TaskHandle_t boot_button_task_handle_{nullptr}; TaskHandle_t boot_button_task_handle_{nullptr};
@@ -10,6 +10,7 @@
#include "esp_log.h" #include "esp_log.h"
#include "esp_netif.h" #include "esp_netif.h"
#include "esp_netif_ip_addr.h" #include "esp_netif_ip_addr.h"
#include "esp_smartconfig.h"
#include "esp_system.h" #include "esp_system.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "lwip/inet.h" #include "lwip/inet.h"
@@ -91,6 +92,17 @@ std::string MacToHex(const uint8_t mac[6]) {
return std::string(out); 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) { std::string LocalMacHex(wifi_interface_t interface) {
uint8_t mac[6] = {}; uint8_t mac[6] = {};
if (esp_wifi_get_mac(interface, mac) != ESP_OK) { if (esp_wifi_get_mac(interface, mac) != ESP_OK) {
@@ -194,6 +206,11 @@ esp_err_t GatewayNetworkService::start() {
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
} }
} else if (config_.smartconfig_startup_enabled) {
err = startSmartconfig();
if (err != ESP_OK) {
return err;
}
} else if (config_.wifi_enabled) { } else if (config_.wifi_enabled) {
err = startWifi(); err = startWifi();
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -261,6 +278,7 @@ esp_err_t GatewayNetworkService::startWifi() {
if (wifi_started_) { if (wifi_started_) {
return ESP_OK; return ESP_OK;
} }
stopSmartconfig();
stopEspNow(); stopEspNow();
setup_ap_started_ = false; setup_ap_started_ = false;
@@ -279,18 +297,21 @@ esp_err_t GatewayNetworkService::startWifi() {
return err; return err;
} }
err = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, if (!wifi_event_handlers_registered_) {
&GatewayNetworkService::HandleWifiEvent, this); err = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { &GatewayNetworkService::HandleWifiEvent, this);
ESP_LOGE(kTag, "failed to register Wi-Fi event handler: %s", esp_err_to_name(err)); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
return err; 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, err = esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&GatewayNetworkService::HandleWifiEvent, this); &GatewayNetworkService::HandleWifiEvent, this);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(kTag, "failed to register IP event handler: %s", esp_err_to_name(err)); ESP_LOGE(kTag, "failed to register IP event handler: %s", esp_err_to_name(err));
return err; return err;
}
wifi_event_handlers_registered_ = true;
} }
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_storage(WIFI_STORAGE_RAM)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_storage(WIFI_STORAGE_RAM));
@@ -329,12 +350,14 @@ esp_err_t GatewayNetworkService::startWifi() {
} }
wifi_started_ = true; wifi_started_ = true;
controller_.setWirelessSetupMode(false);
ESP_LOGI(kTag, "Wi-Fi STA started has_credentials=%d", ESP_LOGI(kTag, "Wi-Fi STA started has_credentials=%d",
device_info.wlan.has_value() && !device_info.wlan->ssid.empty()); device_info.wlan.has_value() && !device_info.wlan->ssid.empty());
return ESP_OK; return ESP_OK;
} }
esp_err_t GatewayNetworkService::startSetupAp() { esp_err_t GatewayNetworkService::startSetupAp() {
stopSmartconfig();
if (wifi_ap_netif_ == nullptr) { if (wifi_ap_netif_ == nullptr) {
wifi_ap_netif_ = esp_netif_create_default_wifi_ap(); wifi_ap_netif_ = esp_netif_create_default_wifi_ap();
if (wifi_ap_netif_ == nullptr) { if (wifi_ap_netif_ == nullptr) {
@@ -397,10 +420,73 @@ esp_err_t GatewayNetworkService::startSetupAp() {
wifi_started_ = true; wifi_started_ = true;
setup_ap_started_ = true; setup_ap_started_ = true;
controller_.setWirelessSetupMode(true);
ESP_LOGI(kTag, "setup AP started ssid=%s ip=192.168.3.1", kSetupApSsid); ESP_LOGI(kTag, "setup AP started ssid=%s ip=192.168.3.1", kSetupApSsid);
return ESP_OK; 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() { esp_err_t GatewayNetworkService::startEspNow() {
if (espnow_started_) { if (espnow_started_) {
return ESP_OK; return ESP_OK;
@@ -629,6 +715,42 @@ void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t
return; 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) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
const auto info = runtime_.deviceInfo(); const auto info = runtime_.deviceInfo();
if (info.wlan.has_value() && !info.wlan->ssid.empty()) { 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(); const bool has_credentials = !info.wlan->ssid.empty();
info.wlan->ip.clear(); info.wlan->ip.clear();
runtime_.setWirelessInfo(std::move(*info.wlan)); runtime_.setWirelessInfo(std::move(*info.wlan));
if (has_credentials) { if (has_credentials && !smartconfig_started_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); 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); auto* event = static_cast<ip_event_got_ip_t*>(event_data);
char ip[16] = {0}; char ip[16] = {0};
esp_ip4addr_ntoa(&event->ip_info.ip, ip, sizeof(ip)); 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; 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)); runtime_.setWirelessInfo(std::move(wireless));
ESP_LOGI(kTag, "Wi-Fi got IP %s", ip); 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) { void GatewayNetworkService::handleWifiControl(uint8_t mode) {
if (mode == 0) { if (mode == 0) {
config_.wifi_enabled = false; config_.wifi_enabled = false;
stopSmartconfig();
stopEspNow(); stopEspNow();
controller_.setWirelessSetupMode(false);
if (wifi_started_) { if (wifi_started_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
@@ -923,8 +1052,13 @@ void GatewayNetworkService::handleWifiControl(uint8_t mode) {
ESP_ERROR_CHECK_WITHOUT_ABORT(startSetupAp()); ESP_ERROR_CHECK_WITHOUT_ABORT(startSetupAp());
return; return;
} }
if (mode == 100) {
ESP_ERROR_CHECK_WITHOUT_ABORT(startSmartconfig());
return;
}
if (mode == 1) { if (mode == 1) {
config_.wifi_enabled = true; config_.wifi_enabled = true;
stopSmartconfig();
if (setup_ap_started_) { if (setup_ap_started_) {
stopEspNow(); stopEspNow();
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
@@ -994,6 +1128,7 @@ std::string GatewayNetworkService::gatewaySnapshotJson() {
cJSON* gw_info = cJSON_CreateObject(); cJSON* gw_info = cJSON_CreateObject();
if (gw_info != nullptr) { if (gw_info != nullptr) {
cJSON_AddBoolToObject(gw_info, "setupMode", snapshot.setup_mode); 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, "bleEnabled", snapshot.ble_enabled);
cJSON_AddBoolToObject(gw_info, "wifiEnabled", snapshot.wifi_enabled); cJSON_AddBoolToObject(gw_info, "wifiEnabled", snapshot.wifi_enabled);
cJSON_AddBoolToObject(gw_info, "IPRouter", snapshot.ip_router_enabled); cJSON_AddBoolToObject(gw_info, "IPRouter", snapshot.ip_router_enabled);
@@ -4,6 +4,7 @@
#include <cstdint> #include <cstdint>
#include <deque> #include <deque>
#include <functional> #include <functional>
#include <map>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -127,6 +128,7 @@ class GatewayRuntime {
GatewaySettingsStore settings_; GatewaySettingsStore settings_;
std::optional<std::vector<uint8_t>> current_command_; std::optional<std::vector<uint8_t>> current_command_;
std::deque<std::vector<uint8_t>> pending_commands_; std::deque<std::vector<uint8_t>> pending_commands_;
mutable std::map<uint8_t, std::string> gateway_names_;
size_t gateway_count_{0}; size_t gateway_count_{0};
bool ble_enabled_{false}; bool ble_enabled_{false};
CommandDropReason last_enqueue_drop_reason_{CommandDropReason::kNone}; 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, bool GatewaySettingsStore::setWifiCredentials(std::string_view ssid,
std::string_view password) { 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() { bool GatewaySettingsStore::clearWifiCredentials() {
@@ -129,12 +136,17 @@ bool GatewaySettingsStore::clearWifiCredentials() {
esp_err_t ssid_err = nvs_erase_key(handle_, kWifiSsidKey); esp_err_t ssid_err = nvs_erase_key(handle_, kWifiSsidKey);
esp_err_t password_err = nvs_erase_key(handle_, kWifiPasswordKey); 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) { if (ssid_err == ESP_ERR_NVS_NOT_FOUND) {
ssid_err = ESP_OK; ssid_err = ESP_OK;
} }
if (password_err == ESP_ERR_NVS_NOT_FOUND) { if (password_err == ESP_ERR_NVS_NOT_FOUND) {
password_err = ESP_OK; 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; 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 GatewayRuntime::clearWirelessInfo() {
bool had_credentials = false;
{ {
LockGuard guard(command_lock_); LockGuard guard(command_lock_);
had_credentials = wireless_info_.has_value() &&
(!wireless_info_->ssid.empty() || !wireless_info_->password.empty());
wireless_info_.reset(); wireless_info_.reset();
} }
if (!had_credentials) {
return true;
}
return settings_.clearWifiCredentials(); return settings_.clearWifiCredentials();
} }
@@ -379,19 +397,37 @@ GatewayDeviceInfo GatewayRuntime::deviceInfo() const {
} }
bool GatewayRuntime::bleEnabled() const { bool GatewayRuntime::bleEnabled() const {
LockGuard guard(command_lock_);
return ble_enabled_; return ble_enabled_;
} }
bool GatewayRuntime::setBleEnabled(bool enabled) { bool GatewayRuntime::setBleEnabled(bool enabled) {
{
LockGuard guard(command_lock_);
if (ble_enabled_ == enabled) {
return true;
}
}
if (!settings_.setBleEnabled(enabled)) { if (!settings_.setBleEnabled(enabled)) {
return false; return false;
} }
LockGuard guard(command_lock_);
ble_enabled_ = enabled; ble_enabled_ = enabled;
return true; return true;
} }
std::string GatewayRuntime::gatewayName(uint8_t gateway_id) const { 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) { 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); 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 { std::string GatewayRuntime::gatewaySerialHex(uint8_t gateway_id) const {