Compare commits

...
Author SHA1 Message Date
J. Nick Koston 6ea12bb5c3 [wifi] Test that EAP strings stay plain literals off ESP8266 2026-11-15 03:45:52 -06:00
J. Nick Koston a460c21e96 [wifi] Copy flash credentials through a stack buffer
Keep the CompactString constructor as on dev and add an ESP8266-only
CompactString::from_progmem helper instead, which is smaller. Drop the
static_assert and the CompactString rewrite tests; keep the runtime
setter test.
2026-11-15 03:34:02 -06:00
J. Nick Koston 6bd1de6860 [wifi] Keep configured SSIDs and passwords in flash on ESP8266
Codegen passes the configured SSID and password as ESPHOME_F literals.
On ESP8266 a ProgmemStr overload of WiFiAP::set_ssid/set_password copies
them out of flash into the CompactString, so the literals leave .rodata.
EAP identity, username and password are built with progmem_string.
Other platforms resolve to the existing const char * setters. Runtime
setters (std::string, const char *, StringRef) are unchanged.
2026-11-15 03:27:43 -06:00
8 changed files with 144 additions and 5 deletions

No files matched your search

+13 -5
View File
@@ -784,6 +784,11 @@ CONFIG_SCHEMA = cv.All(
)
def _eap_string(value: str | None) -> cg.Expression | str:
"""A std::string field whose literal stays in flash on ESP8266."""
return cg.progmem_string(value) if value else ""
def eap_auth(config):
if config is None:
return None
@@ -798,9 +803,9 @@ def eap_auth(config):
key = wpa2_eap.read_relative_config_path(config[CONF_KEY])
return cg.StructInitializer(
EAPAuth,
("identity", config.get(CONF_IDENTITY, "")),
("username", config.get(CONF_USERNAME, "")),
("password", config.get(CONF_PASSWORD, "")),
("identity", _eap_string(config.get(CONF_IDENTITY))),
("username", _eap_string(config.get(CONF_USERNAME))),
("password", _eap_string(config.get(CONF_PASSWORD))),
("ca_cert", ca_cert),
("client_cert", client_cert),
("client_key", key),
@@ -822,8 +827,11 @@ def manual_ip_entry(config: ConfigType) -> bytes:
def wifi_network(config, ap, manual_ip_index: int | None):
cg.add_optional(ap.set_ssid, config.get(CONF_SSID))
cg.add_optional(ap.set_password, config.get(CONF_PASSWORD))
# The setters copy the text, so the literal can stay in flash on ESP8266.
if (ssid := config.get(CONF_SSID)) is not None:
cg.add(ap.set_ssid(cg.FlashStringLiteral(ssid)))
if (password := config.get(CONF_PASSWORD)) is not None:
cg.add(ap.set_password(cg.FlashStringLiteral(password)))
if CONF_EAP in config:
cg.add(ap.set_eap(eap_auth(config[CONF_EAP])))
cg.add_define("USE_WIFI_WPA2_EAP")
@@ -78,6 +78,15 @@ CompactString::CompactString(const char *str, size_t len) {
}
}
#ifdef USE_ESP8266
CompactString CompactString::from_progmem(ProgmemStr str) {
char buf[MAX_LENGTH + 1];
ESPHOME_strncpy_P(buf, reinterpret_cast<const char *>(str), MAX_LENGTH);
buf[MAX_LENGTH] = '\0';
return CompactString(buf, strlen(buf));
}
#endif
CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {}
CompactString &CompactString::operator=(const CompactString &other) {
+11
View File
@@ -6,6 +6,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#ifdef USE_ESP32
#include "esphome/core/lock_free_queue.h"
#endif
@@ -241,6 +242,10 @@ class CompactString {
CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; }
CompactString(const char *str, size_t len);
#ifdef USE_ESP8266
/// Copy a string kept in flash with ESPHOME_F.
static CompactString from_progmem(ProgmemStr str);
#endif
CompactString(const CompactString &other);
CompactString(CompactString &&other) noexcept;
CompactString &operator=(const CompactString &other);
@@ -296,11 +301,17 @@ class WiFiAP {
void set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void set_ssid(const char *ssid) { this->set_ssid(StringRef(ssid)); }
void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
#ifdef USE_ESP8266
void set_ssid(ProgmemStr ssid) { this->ssid_ = CompactString::from_progmem(ssid); }
#endif
void set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void clear_bssid() { this->bssid_ = {}; }
void set_password(const std::string &password) { this->password_ = CompactString(password.c_str(), password.size()); }
void set_password(const char *password) { this->set_password(StringRef(password)); }
void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); }
#ifdef USE_ESP8266
void set_password(ProgmemStr password) { this->password_ = CompactString::from_progmem(password); }
#endif
#ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif // USE_WIFI_WPA2_EAP
@@ -0,0 +1,13 @@
esphome:
name: test
esp8266:
board: d1_mini
wifi:
networks:
- ssid: enterprise
eap:
identity: eap_identity
username: eap_username
password: eap_password
@@ -0,0 +1,15 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
networks:
- ssid: enterprise
eap:
identity: eap_identity
username: eap_username
password: eap_password
@@ -0,0 +1,45 @@
"""Tests that wifi codegen keeps the configured credentials in flash on ESP8266."""
from collections.abc import Callable
from pathlib import Path
import pytest
@pytest.mark.parametrize("config_file", ["bare.yaml", "bare_ln882x.yaml"])
def test_ssid_and_password_use_flash_literals(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
) -> None:
"""Codegen emits ESPHOME_F for station and AP credentials; the macro decides placement."""
main_cpp = generate_main(component_config_path(config_file))
assert 'set_ssid(ESPHOME_F("test"));' in main_cpp
assert 'set_password(ESPHOME_F("testtest"));' in main_cpp
assert 'set_ssid(ESPHOME_F("fallback"));' in main_cpp
assert 'set_ssid("' not in main_cpp
assert 'set_password("' not in main_cpp
def test_eap_strings_use_flash_literals(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""EAP identity, username and password are built from flash on ESP8266."""
main_cpp = generate_main(component_config_path("eap.yaml"))
for value in ("eap_identity", "eap_username", "eap_password"):
assert f'progmem_string(ESPHOME_F("{value}"))' in main_cpp
def test_eap_strings_plain_off_esp8266(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Off ESP8266 the EAP strings stay plain literals, with no copy from flash."""
main_cpp = generate_main(component_config_path("eap_esp32.yaml"))
assert "progmem_string(" not in main_cpp
for value in ("eap_identity", "eap_username", "eap_password"):
assert f'"{value}"' in main_cpp
+10
View File
@@ -33,6 +33,16 @@ esphome:
then:
- logger.log: "WiFi is enabled!"
- wifi.roam
# Runtime credentials keep resolving to the RAM setters, never the flash one
- lambda: |-
wifi::WiFiAP sta;
std::string ssid = "RuntimeSSID";
sta.set_ssid(ssid);
char password[16] = "runtime_pass";
sta.set_password(password);
const char *ssid_ptr = ssid.c_str();
sta.set_ssid(ssid_ptr);
sta.set_password(StringRef(password));
wifi:
networks:
@@ -0,0 +1,28 @@
#include <gtest/gtest.h>
#include <string>
#include "esphome/components/wifi/wifi_component.h"
namespace esphome::wifi::testing {
TEST(WiFiAP, RuntimeSetters) {
// Credentials from improv, captive portal, wifi.configure and lambdas arrive in RAM and are copied.
WiFiAP ap;
std::string ssid = "runtime_ssid";
ap.set_ssid(ssid);
char password[] = "runtime_pass";
ap.set_password(password);
ssid[0] = 'X';
password[0] = 'X';
EXPECT_EQ(ap.get_ssid(), "runtime_ssid");
EXPECT_EQ(ap.get_password(), "runtime_pass");
const char *ssid_ptr = ssid.c_str();
ap.set_ssid(ssid_ptr);
ap.set_password(StringRef(password, 4));
EXPECT_EQ(ap.get_ssid(), "Xuntime_ssid");
EXPECT_EQ(ap.get_password(), "Xunt");
}
} // namespace esphome::wifi::testing