Cannot confirm wifi_station_disconnect() synchronously clears the
lwIP netif LINK_UP flag on ESP8266 NONOS SDK. The comment doesn't
need to make claims about link state since the fix is simply that
the hostname never changes at runtime, making dhcp_renew() pointless.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The dhcp_renew() loop was cargo-culted from Arduino ESP8266's
WiFi.hostname() which was designed for changing the hostname on
an already-connected system with multiple interfaces (WiFi+Ethernet).
In ESPHome, wifi_apply_hostname_() is only called from:
- setup_() — before WiFi connects (link never up)
- wifi_sta_connect_() — after wifi_disconnect_() (link always down)
The hostname is fixed at compile time and never changes at runtime.
Setting intf->hostname is sufficient — lwIP automatically includes
it in DHCP DISCOVER/REQUEST packets via LWIP_NETIF_HOSTNAME.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DHCP_STATE_BOUND alone is insufficient — during reconnection, DHCP
can remain BOUND from a previous connection while the link is down
(wifi_disconnect_() doesn't stop DHCP). Both conditions are needed:
DHCP must be BOUND and the interface must have link.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
netif_is_link_up() is insufficient — if wifi_station_connect()
completes quickly (e.g. fast_connect), the setup() call at line 710
could reach dhcp_renew() with link up but DHCP still in SELECTING
or REQUESTING state, causing the same state corruption.
Check dhcp->state == DHCP_STATE_BOUND directly to ensure dhcp_renew()
is only called when there is an actual lease to renew.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wifi_apply_hostname_() calls dhcp_renew() on all interfaces with DHCP
data, including when WiFi is not yet connected. lwIP's dhcp_renew()
unconditionally sets the DHCP state to RENEWING (line 1159 in dhcp.c)
before attempting to send, and never rolls back the state on failure.
This corrupts the DHCP state machine: when WiFi later connects and
dhcp_network_changed() is called, it sees RENEWING state and calls
dhcp_reboot() instead of dhcp_discover(). dhcp_reboot() sends a
broadcast DHCP REQUEST for IP 0.0.0.0 (since no lease was ever
obtained), which can put some routers into a persistent bad state
that requires a router restart to clear.
This bug has existed since commit 072b2c445c (Dec 2019, "Add ESP8266
core v2.6.2") and affects every ESP8266 WiFi connection attempt. Most
routers handle the bogus DHCP REQUEST gracefully (NAK then fallback
to DISCOVER), but affected routers get stuck and refuse connections
from the device until restarted.
Fix: guard the dhcp_renew() call with netif_is_link_up() so it only
runs when the interface actually has an active link. The hostname is
still set on the netif regardless, so it will be included in DHCP
packets when the connection is established normally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace heap-allocated std::vector constants with static constexpr
std::array in MIFARE Classic and Ultralight write operations across
pn532, pn7150, and pn7160 components.
Changes:
- Convert write_mifare_classic_block_ and write_mifare_ultralight_page_
signatures from std::vector<uint8_t>& to const uint8_t*/size_t
- Replace 24 constant 16-byte vectors in format functions with
static constexpr std::array (data now lives in flash/rodata)
- Replace 3 constant 4-byte blank_data vectors with static constexpr
std::array
- Eliminate per-iteration vector copies in write_tag loops by passing
pointers directly into the encoded buffer
When USE_ENTITY_ICON is not defined, LOG_ENTITY_ICON and log_entity_icon
are now completely compiled out rather than calling a function that
checks an always-empty icon reference.
Two fixes for ESP8266 with noise encryption:
1. Cache socket ready() before the handshake loop. On ESP8266 LWIP raw
TCP, ready() returns the live state (false once rx buffer is consumed),
unlike ESP32 where it is cached until the next main loop. Re-checking
each iteration blocked handshake writes that must follow reads,
deadlocking the handshake.
2. Process client removal immediately after loop() instead of deferring
to the next server loop iteration. This closes the socket promptly
to free LWIP PCB resources and prevent retransmit crashes on ESP8266.
On ESP8266 (GCC 10.3), std::vector::push_back/emplace_back emit
separate _M_realloc_insert functions even when called from only
one site. Adding __attribute__((flatten)) inlines the realloc
path, saving the out-of-line function overhead.
Changes:
- wifi: Move set_sta_priority from header to .cpp (eliminates
duplicate instantiation at 2 call sites) and add flatten
- web_server_base: Flatten add_handler (single push_back site)
- api: Flatten accept_new_connections_ (single emplace_back site)
- api: Extract push_item_ as single flattened push_back site for
DeferredBatch (was 2 call sites in add_item/add_item_front)
Saves ~200 bytes of flash on ESP8266, ~40 bytes on ESP32.
On ESP8266 (GCC 10.3), std::vector::push_back/emplace_back emit
separate _M_realloc_insert functions even when called from only
one site. Adding __attribute__((flatten)) inlines the realloc
path, saving the out-of-line function overhead.
Changes:
- wifi: Move set_sta_priority from header to .cpp (eliminates
duplicate instantiation at 2 call sites) and add flatten
- web_server_base: Flatten add_handler (single push_back site)
- api: Flatten accept_new_connections_ (single emplace_back site)
Saves 160 bytes of flash on ESP8266.
On ESP8266 (GCC 10.3), std::vector::push_back() emits a separate
_M_realloc_insert<DeferredEvent> function (198 bytes) that is only
called from one site. Adding __attribute__((flatten)) forces the
compiler to inline it, eliminating the out-of-line function's
prologue/epilogue and call overhead.
This matches what GCC 14.2 (ESP-IDF toolchain) already does
naturally for the equivalent code in web_server_idf.cpp.
Saves 80 bytes of flash on ESP8266.
Extract the keepalive ping sending logic from APIConnection::loop()
into a separate noinline send_keepalive_ping_() method. This code
only fires once per keepalive interval (~60s) making it cold relative
to the ~111 Hz loop rate. Moving it out reduces loop() from 337 to
258 bytes, keeping the hot path smaller and more cache-friendly.
- Add #include <type_traits> to both .cpp and .h for static_asserts
- Clarify CompactString comment: explicitly note it is not trivially
copyable, and that memcpy safety relies on validated layout property
- Use memcpy_fn indirection to suppress both GCC -Wclass-memaccess
and clang-tidy bugprone-undefined-memory-manipulation without
platform-specific pragma guards
Replace copy-assignment with raw memcpy in the WiFi scan result
insertion sort. Copy assignment on WiFiScanResult calls
CompactString's destructor then placement-new for every shift,
which means delete[]/new[] per shift for heap-allocated SSIDs.
With 70+ networks visible (e.g., during captive portal transition
showing full scan results), this caused event loop blocking from
hundreds of heap allocations in a tight loop on an 80MHz ESP8266.
This optimization is safe because we're permuting elements within
the same array - each slot is overwritten exactly once, so no
ownership duplication occurs. CompactString stores either inline
data or a heap pointer, never a self-referential pointer (unlike
libstdc++ std::string SSO). This was made possible by PR#13472
which replaced std::string with CompactString.
Static asserts guard the memcpy safety assumptions at compile time.
Confirmed on real device: event loop blocking during captive portal
transition is eliminated and WiFi connection is slightly faster.