Merge remote-tracking branch 'upstream/dev' into millis64-host-native

# Conflicts:
#	esphome/components/host/core.cpp
#	esphome/core/scheduler.cpp
#	esphome/core/scheduler.h
This commit is contained in:
J. Nick Koston
2026-02-27 07:53:14 -10:00
20 changed files with 300 additions and 70 deletions
+1 -1
View File
@@ -214,4 +214,4 @@ async def to_code(config):
cg.add_define("USE_AUDIO_MP3_SUPPORT")
if data.opus_support:
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
add_idf_component(name="esphome/micro-opus", ref="0.3.3")
add_idf_component(name="esphome/micro-opus", ref="0.3.4")
+52
View File
@@ -21,6 +21,7 @@ from esphome.const import (
)
from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
import esphome.final_validate as fv
from esphome.types import ConfigType
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
@@ -188,6 +189,9 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
CONF_BLE_ID = "ble_id"
CONF_IO_CAPABILITY = "io_capability"
CONF_AUTH_REQ_MODE = "auth_req_mode"
CONF_MAX_KEY_SIZE = "max_key_size"
CONF_MIN_KEY_SIZE = "min_key_size"
CONF_ADVERTISING = "advertising"
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
CONF_DISABLE_BT_LOGS = "disable_bt_logs"
@@ -238,6 +242,18 @@ IO_CAPABILITY = {
"display_yes_no": IoCapability.IO_CAP_IO,
}
AuthReqMode = esp32_ble_ns.enum("AuthReqMode")
AUTH_REQ_MODE = {
"no_bond": AuthReqMode.AUTH_REQ_NO_BOND,
"bond": AuthReqMode.AUTH_REQ_BOND,
"mitm": AuthReqMode.AUTH_REQ_MITM,
"bond_mitm": AuthReqMode.AUTH_REQ_BOND_MITM,
"sc_only": AuthReqMode.AUTH_REQ_SC_ONLY,
"sc_bond": AuthReqMode.AUTH_REQ_SC_BOND,
"sc_mitm": AuthReqMode.AUTH_REQ_SC_MITM,
"sc_mitm_bond": AuthReqMode.AUTH_REQ_SC_MITM_BOND,
}
esp_power_level_t = cg.global_ns.enum("esp_power_level_t")
TX_POWER_LEVELS = {
@@ -258,6 +274,10 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum(
IO_CAPABILITY, lower=True
),
# note: no defaults so we can action them not being present
cv.Optional(CONF_AUTH_REQ_MODE): cv.enum(AUTH_REQ_MODE, lower=True),
cv.Optional(CONF_MAX_KEY_SIZE): cv.int_range(min=7, max=16),
cv.Optional(CONF_MIN_KEY_SIZE): cv.int_range(min=7, max=16),
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_ADVERTISING, default=False): cv.boolean,
cv.Optional(
@@ -279,6 +299,23 @@ CONFIG_SCHEMA = cv.Schema(
).extend(cv.COMPONENT_SCHEMA)
def _validate_key_sizes(config: ConfigType) -> ConfigType:
if (
CONF_MIN_KEY_SIZE in config
and CONF_MAX_KEY_SIZE in config
and config[CONF_MIN_KEY_SIZE] > config[CONF_MAX_KEY_SIZE]
):
raise cv.Invalid(
f"min_key_size ({config[CONF_MIN_KEY_SIZE]}) must be "
f"less than or equal to "
f"max_key_size ({config[CONF_MAX_KEY_SIZE]})"
)
return config
CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes)
bt_uuid16_format = "XXXX"
bt_uuid32_format = "XXXXXXXX"
bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
@@ -487,6 +524,21 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY]))
if (
CONF_AUTH_REQ_MODE in config
or CONF_MAX_KEY_SIZE in config
or CONF_MIN_KEY_SIZE in config
):
cg.add_define("ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS", None)
if CONF_AUTH_REQ_MODE in config:
cg.add(var.set_auth_req(config[CONF_AUTH_REQ_MODE]))
if CONF_MAX_KEY_SIZE in config:
cg.add(var.set_max_key_size(config[CONF_MAX_KEY_SIZE]))
if CONF_MIN_KEY_SIZE in config:
cg.add(var.set_min_key_size(config[CONF_MIN_KEY_SIZE]))
cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME]))
if (name := config.get(CONF_NAME)) is not None:
cg.add(var.set_name(name))
+72 -2
View File
@@ -296,12 +296,39 @@ bool ESP32BLE::ble_setup_() {
return false;
}
err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t));
err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(esp_ble_io_cap_t));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err);
ESP_LOGE(TAG, "esp_ble_gap_set_security_param iocap_mode failed: %d", err);
return false;
}
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
if (this->max_key_size_) {
err = esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &(this->max_key_size_), sizeof(uint8_t));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_set_security_param max_key_size failed: %d", err);
return false;
}
}
if (this->min_key_size_) {
err = esp_ble_gap_set_security_param(ESP_BLE_SM_MIN_KEY_SIZE, &(this->min_key_size_), sizeof(uint8_t));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_set_security_param min_key_size failed: %d", err);
return false;
}
}
if (this->auth_req_mode_) {
err = esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &(this->auth_req_mode_.value()),
sizeof(esp_ble_auth_req_t));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_set_security_param authen_req_mode failed: %d", err);
return false;
}
}
#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
// BLE takes some time to be fully set up, 200ms should be more than enough
delay(200); // NOLINT
@@ -645,6 +672,7 @@ void ESP32BLE::dump_config() {
io_capability_s = "invalid";
break;
}
char mac_s[18];
format_mac_addr_upper(mac_address, mac_s);
ESP_LOGCONFIG(TAG,
@@ -652,6 +680,48 @@ void ESP32BLE::dump_config() {
" MAC address: %s\n"
" IO Capability: %s",
mac_s, io_capability_s);
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
const char *auth_req_mode_s = "<default>";
if (this->auth_req_mode_) {
switch (this->auth_req_mode_.value()) {
case AUTH_REQ_NO_BOND:
auth_req_mode_s = "no_bond";
break;
case AUTH_REQ_BOND:
auth_req_mode_s = "bond";
break;
case AUTH_REQ_MITM:
auth_req_mode_s = "mitm";
break;
case AUTH_REQ_BOND_MITM:
auth_req_mode_s = "bond_mitm";
break;
case AUTH_REQ_SC_ONLY:
auth_req_mode_s = "sc_only";
break;
case AUTH_REQ_SC_BOND:
auth_req_mode_s = "sc_bond";
break;
case AUTH_REQ_SC_MITM:
auth_req_mode_s = "sc_mitm";
break;
case AUTH_REQ_SC_MITM_BOND:
auth_req_mode_s = "sc_mitm_bond";
break;
}
}
ESP_LOGCONFIG(TAG, " Auth Req Mode: %s", auth_req_mode_s);
if (this->max_key_size_ && this->min_key_size_) {
ESP_LOGCONFIG(TAG, " Key Size: %u - %u", this->min_key_size_, this->max_key_size_);
} else if (this->max_key_size_) {
ESP_LOGCONFIG(TAG, " Key Size: <default> - %u", this->max_key_size_);
} else if (this->min_key_size_) {
ESP_LOGCONFIG(TAG, " Key Size: %u - <default>", this->min_key_size_);
}
#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
} else {
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
}
+26
View File
@@ -52,6 +52,19 @@ enum IoCapability {
IO_CAP_KBDISP = ESP_IO_CAP_KBDISP,
};
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
enum AuthReqMode {
AUTH_REQ_NO_BOND = ESP_LE_AUTH_NO_BOND,
AUTH_REQ_BOND = ESP_LE_AUTH_BOND,
AUTH_REQ_MITM = ESP_LE_AUTH_REQ_MITM,
AUTH_REQ_BOND_MITM = ESP_LE_AUTH_REQ_BOND_MITM,
AUTH_REQ_SC_ONLY = ESP_LE_AUTH_REQ_SC_ONLY,
AUTH_REQ_SC_BOND = ESP_LE_AUTH_REQ_SC_BOND,
AUTH_REQ_SC_MITM = ESP_LE_AUTH_REQ_SC_MITM,
AUTH_REQ_SC_MITM_BOND = ESP_LE_AUTH_REQ_SC_MITM_BOND,
};
#endif
enum BLEComponentState : uint8_t {
/** Nothing has been initialized yet. */
BLE_COMPONENT_STATE_OFF = 0,
@@ -100,6 +113,12 @@ class ESP32BLE : public Component {
public:
void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; }
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
void set_max_key_size(uint8_t key_size) { this->max_key_size_ = key_size; }
void set_min_key_size(uint8_t key_size) { this->min_key_size_ = key_size; }
void set_auth_req(AuthReqMode req) { this->auth_req_mode_ = (esp_ble_auth_req_t) req; }
#endif
void set_advertising_cycle_time(uint32_t advertising_cycle_time) {
this->advertising_cycle_time_ = advertising_cycle_time;
}
@@ -209,6 +228,13 @@ class ESP32BLE : public Component {
// 1-byte aligned members (grouped together to minimize padding)
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
bool enable_on_boot_{}; // 1 byte
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
optional<esp_ble_auth_req_t> auth_req_mode_;
uint8_t max_key_size_{0}; // range is 7..16, 0 is unset
uint8_t min_key_size_{0}; // range is 7..16, 0 is unset
#endif
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
@@ -23,6 +23,7 @@ CONF_D1_PIN = "d1_pin"
CONF_D2_PIN = "d2_pin"
CONF_D3_PIN = "d3_pin"
CONF_SLOT = "slot"
CONF_SDIO_FREQUENCY = "sdio_frequency"
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -37,6 +38,9 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1),
cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All(
cv.frequency, cv.Range(min=400e3, max=50e6)
),
}
),
)
@@ -91,6 +95,10 @@ async def to_code(config):
config[CONF_D3_PIN],
)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ",
int(config[CONF_SDIO_FREQUENCY] // 1000),
)
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
+1
View File
@@ -1,5 +1,6 @@
#ifdef USE_HOST
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
+1 -1
View File
@@ -163,7 +163,7 @@ async def to_code(config):
cg.add_library("LEAmDNS", None)
if CORE.is_esp32:
add_idf_component(name="espressif/mdns", ref="1.9.1")
add_idf_component(name="espressif/mdns", ref="1.10.0")
cg.add_define("USE_MDNS")
+8 -3
View File
@@ -149,9 +149,10 @@ def require_wake_loop_threadsafe() -> None:
):
CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
cg.add_define("USE_WAKE_LOOP_THREADSAFE")
if not CORE.is_esp32:
# Only non-ESP32 platforms need a UDP socket for wake notifications.
# ESP32 uses FreeRTOS task notifications instead (no socket needed).
if not CORE.is_esp32 and not CORE.is_libretiny:
# Only platforms without fast select need a UDP socket for wake
# notifications. ESP32 and LibreTiny use FreeRTOS task notifications
# instead (no socket needed).
consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({})
@@ -187,6 +188,10 @@ async def to_code(config):
elif impl == IMPLEMENTATION_BSD_SOCKETS:
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
cg.add_define("USE_SOCKET_SELECT_SUPPORT")
# ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket()
# and FreeRTOS task notifications — enable fast select to bypass lwip_select()
if CORE.is_esp32 or CORE.is_libretiny:
cg.add_define("USE_LWIP_FAST_SELECT")
def FILTER_SOURCE_FILES() -> list[str]:
+2 -1
View File
@@ -8,7 +8,7 @@ from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data
from esphome.components.zephyr.const import KEY_BOOTLOADER
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME
from esphome.core import CORE
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
from .const_zephyr import (
@@ -96,6 +96,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
)
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_ZIGBEE")
if CORE.using_zephyr:
+7 -1
View File
@@ -179,6 +179,13 @@ async def zephyr_to_code(config: ConfigType) -> None:
"USE_ZIGBEE_WIPE_ON_BOOT_MAGIC", random.randint(0x000001, 0xFFFFFF)
)
cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT")
# Generate attribute lists before any await that could yield (e.g., build_automation
# waiting for variables from other components). If the hub's priority decays while
# yielding, deferred entity jobs may add cluster list globals that reference these
# attribute lists before they're declared.
await _attr_to_code(config)
var = cg.new_Pvariable(config[CONF_ID])
if on_join_config := config.get(CONF_ON_JOIN):
@@ -186,7 +193,6 @@ async def zephyr_to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await _attr_to_code(config)
CORE.add_job(_ctx_to_code, config)
+25 -18
View File
@@ -9,10 +9,17 @@
#endif
#ifdef USE_ESP32
#include <esp_chip_info.h>
#endif
#ifdef USE_LWIP_FAST_SELECT
#include "esphome/core/lwip_fast_select.h"
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#else
#include <FreeRTOS.h>
#include <task.h>
#endif
#endif // USE_LWIP_FAST_SELECT
#include "esphome/core/version.h"
#include "esphome/core/hal.h"
#include <algorithm>
@@ -147,14 +154,14 @@ void Application::setup() {
clear_setup_priority_overrides();
#endif
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT)
// Initialize fast select: saves main loop task handle for xTaskNotifyGive wake.
// Always init on ESP32 — the fast path (rcvevent reads + ulTaskNotifyTake) is used
// unconditionally when USE_SOCKET_SELECT_SUPPORT is enabled.
// The fast path (rcvevent reads + ulTaskNotifyTake) is used unconditionally
// when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny).
esphome_lwip_fast_select_init();
#endif
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
// Set up wake socket for waking main loop from tasks (non-ESP32 only)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
// Set up wake socket for waking main loop from tasks (platforms without fast select only)
this->setup_wake_loop_threadsafe_();
#endif
@@ -532,7 +539,7 @@ void Application::enable_pending_loops_() {
}
void Application::before_loop_tasks_(uint32_t loop_start_time) {
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
// Drain wake notifications first to clear socket for next wake
this->drain_wake_notifications_();
#endif
@@ -585,7 +592,7 @@ bool Application::register_socket_fd(int fd) {
#endif
this->socket_fds_.push_back(fd);
#ifdef USE_ESP32
#ifdef USE_LWIP_FAST_SELECT
// Hook the socket's netconn callback for instant wake on receive events
esphome_lwip_hook_socket(fd);
#else
@@ -609,12 +616,13 @@ void Application::unregister_socket_fd(int fd) {
continue;
// Swap with last element and pop - O(1) removal since order doesn't matter.
// No need to unhook the netconn callback on ESP32 — all LwIP sockets share
// the same static event_callback, and the socket will be closed by the caller.
// No need to unhook the netconn callback on fast select platforms — all LwIP
// sockets share the same static event_callback, and the socket will be closed
// by the caller.
if (i < this->socket_fds_.size() - 1)
this->socket_fds_[i] = this->socket_fds_.back();
this->socket_fds_.pop_back();
#ifndef USE_ESP32
#ifndef USE_LWIP_FAST_SELECT
this->socket_fds_changed_ = true;
// Only recalculate max_fd if we removed the current max
if (fd == this->max_fd_) {
@@ -633,8 +641,8 @@ void Application::unregister_socket_fd(int fd) {
void Application::yield_with_select_(uint32_t delay_ms) {
// Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run.
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32)
// ESP32 fast path: reads rcvevent directly via lwip_socket_dbg_get_socket() (~215 ns per socket).
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT)
// Fast path (ESP32/LibreTiny): reads rcvevent directly via lwip_socket_dbg_get_socket().
// Safe because this runs on the main loop which owns socket lifetime (create, read, close).
if (delay_ms == 0) [[unlikely]] {
yield();
@@ -659,9 +667,8 @@ void Application::yield_with_select_(uint32_t delay_ms) {
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms));
#elif defined(USE_SOCKET_SELECT_SUPPORT)
// Non-ESP32 select() path (LibreTiny bk72xx/rtl87xx, host platform).
// ESP32 is excluded by the #if above — both BSD_SOCKETS and LWIP_SOCKETS on ESP32
// use LwIP under the hood, so the fast path handles all ESP32 socket implementations.
// Fallback select() path (host platform and any future platforms without fast select).
// ESP32 and LibreTiny are excluded by the #if above — they use the fast path.
if (!this->socket_fds_.empty()) [[likely]] {
// Update fd_set if socket list has changed
if (this->socket_fds_changed_) [[unlikely]] {
@@ -725,12 +732,12 @@ alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
#ifdef USE_ESP32
#ifdef USE_LWIP_FAST_SELECT
void Application::wake_loop_threadsafe() {
// Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe)
esphome_lwip_wake_main_loop();
}
#else // !USE_ESP32
#else // !USE_LWIP_FAST_SELECT
void Application::setup_wake_loop_threadsafe_() {
// Create UDP socket for wake notifications
@@ -798,7 +805,7 @@ void Application::wake_loop_threadsafe() {
lwip_send(this->wake_socket_fd_, &dummy, 1, 0);
}
}
#endif // USE_ESP32
#endif // USE_LWIP_FAST_SELECT
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
+14 -13
View File
@@ -24,7 +24,7 @@
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
#ifdef USE_ESP32
#ifdef USE_LWIP_FAST_SELECT
#include "esphome/core/lwip_fast_select.h"
#else
#include <sys/select.h>
@@ -511,10 +511,11 @@ class Application {
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Fast path for Socket::ready() via friendship - skips negative fd check.
/// Main loop only — on ESP32, reads rcvevent via lwip_socket_dbg_get_socket()
/// which has no refcount; safe only because the main loop owns socket lifetime
/// (creates, reads, and closes sockets on the same thread).
#ifdef USE_ESP32
/// Main loop only — with USE_LWIP_FAST_SELECT, reads rcvevent via
/// lwip_socket_dbg_get_socket(), which has no refcount; safe only because
/// the main loop owns socket lifetime (creates, reads, and closes sockets
/// on the same thread).
#ifdef USE_LWIP_FAST_SELECT
bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); }
#else
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
@@ -546,7 +547,7 @@ class Application {
/// Perform a delay while also monitoring socket file descriptors for readiness
void yield_with_select_(uint32_t delay_ms);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
void setup_wake_loop_threadsafe_(); // Create wake notification socket
inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined)
#endif
@@ -576,7 +577,7 @@ class Application {
FixedVector<Component *> looping_components_{};
#ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks
#endif
#endif
@@ -589,7 +590,7 @@ class Application {
uint32_t last_loop_{0};
uint32_t loop_component_start_time_{0};
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32)
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
int max_fd_{-1}; // Highest file descriptor number for select()
#endif
@@ -605,12 +606,12 @@ class Application {
bool in_loop_{false};
volatile bool has_pending_enable_loop_requests_{false};
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32)
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes
#endif
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32)
// Variable-sized members (not needed on ESP32 — is_socket_ready_ reads rcvevent directly)
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
// Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly)
fd_set read_fds_{}; // Working fd_set: populated by select()
fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes
#endif
@@ -699,7 +700,7 @@ class Application {
/// Global storage of Application pointer - only one Application can exist.
extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
// Inline implementations for hot-path functions
// drain_wake_notifications_() is called on every loop iteration
@@ -721,6 +722,6 @@ inline void Application::drain_wake_notifications_() {
}
}
}
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
} // namespace esphome
+3
View File
@@ -209,6 +209,7 @@
#define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1
#define ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT 1
#define ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT 2
#define ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
#define ESPHOME_LOOP_TASK_STACK_SIZE 8192
#define USE_ESP32_CAMERA_JPEG_ENCODER
#define USE_HTTP_REQUEST_RESPONSE
@@ -221,6 +222,7 @@
#define USE_SENDSPIN_PORT 8928 // NOLINT
#define USE_SOCKET_IMPL_BSD_SOCKETS
#define USE_SOCKET_SELECT_SUPPORT
#define USE_LWIP_FAST_SELECT
#define USE_WAKE_LOOP_THREADSAFE
#define USE_SPEAKER
#define USE_SPI
@@ -329,6 +331,7 @@
#define USE_CAPTIVE_PORTAL
#define USE_SOCKET_IMPL_LWIP_SOCKETS
#define USE_SOCKET_SELECT_SUPPORT
#define USE_LWIP_FAST_SELECT
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_PORT 80 // NOLINT
+33 -21
View File
@@ -1,11 +1,12 @@
// Fast socket monitoring for ESP32 (ESP-IDF LwIP)
// Fast socket monitoring for ESP32 and LibreTiny (LwIP >= 2.1.3)
// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications.
//
// This must be a .c file (not .cpp) because:
// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units that include bootloader headers
// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units
// 2. The netconn callback is a C function pointer
//
// defines.h is force-included by the build system (-include flag), providing USE_ESP32 etc.
// USE_ESP32 and USE_LIBRETINY platform flags (-D) control compilation of this file.
// See the guard at the bottom of the header comment for details.
//
// Thread safety analysis
// ======================
@@ -81,20 +82,21 @@
// Written by main loop in hook_socket(). Never restored — all LwIP sockets share
// the same static event_callback (DEFAULT_SOCKET_EVENTCB), so the wrapper stays permanently.
// Read by TCP/IP thread when invoking the callback.
// Safe: 32-bit aligned pointer writes are atomic on Xtensa and RISC-V (ESP32).
// The TCP/IP thread will see either the old or new pointer atomically — never a
// torn value. Both the wrapper and original callbacks are valid at all times
// (the wrapper itself calls the original), so either value is correct.
// Safe: 32-bit aligned pointer writes are atomic on Xtensa, RISC-V (ESP32),
// and ARM Cortex-M (LibreTiny). The TCP/IP thread will see either the old or
// new pointer atomically — never a torn value. Both the wrapper and original
// callbacks are valid at all times (the wrapper itself calls the original),
// so either value is correct.
//
// sock->rcvevent (s16_t, 2 bytes):
// Written by TCP/IP thread in event_callback under SYS_ARCH_PROTECT.
// Read by main loop in has_data() via volatile cast.
// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex, which internally
// uses a critical section with memory barrier (rsync on dual-core Xtensa; on
// single-core builds the spinlock is compiled out, but cross-core visibility is
// not an issue). The volatile cast prevents the compiler
// from caching the read. Aligned 16-bit reads are single-instruction loads on
// Xtensa (L16SI) and RISC-V (LH), which cannot produce torn values.
// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex (ESP32) or resumes the
// scheduler (LibreTiny), both providing a memory barrier. The volatile cast
// prevents the compiler from caching the read. Aligned 16-bit reads are
// single-instruction loads on Xtensa (L16SI), RISC-V (LH), and ARM Cortex-M
// (LDRH), which cannot produce torn values. On single-core chips (LibreTiny,
// ESP32-C3/C6/H2) cross-core visibility is not an issue.
//
// FreeRTOS task notification value:
// Written by TCP/IP thread (xTaskNotifyGive in callback) and background tasks
@@ -103,20 +105,30 @@
// critical sections). Multiple concurrent xTaskNotifyGive calls are safe —
// the notification count simply increments.
#ifdef USE_ESP32
// USE_ESP32 and USE_LIBRETINY are compiler -D flags, so they are always visible in this .c file.
// Feature macros like USE_LWIP_FAST_SELECT may come from generated headers that are not included here,
// so this implementation is enabled based on platform flags instead of USE_LWIP_FAST_SELECT.
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc.
#include <lwip/api.h>
#include <lwip/priv/sockets_priv.h>
// FreeRTOS include paths differ: ESP-IDF uses freertos/ prefix, LibreTiny does not
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#else
#include <FreeRTOS.h>
#include <task.h>
#endif
#include "esphome/core/lwip_fast_select.h"
#include <stddef.h>
// Compile-time verification of thread safety assumptions.
// On ESP32 (Xtensa/RISC-V), naturally-aligned reads/writes up to 32 bits are atomic.
// On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned
// reads/writes up to 32 bits are atomic.
// These asserts ensure our cross-thread shared state meets those requirements.
// Pointer types must fit in a single 32-bit store (atomic write)
@@ -126,7 +138,7 @@ _Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 byt
// rcvevent must fit in a single atomic read
_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access");
// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V.
// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V/ARM.
// Misaligned access would not be atomic even if the size is <= 4 bytes.
_Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == 0,
"netconn.callback must be naturally aligned for atomic access");
@@ -183,9 +195,9 @@ bool esphome_lwip_socket_has_data(int fd) {
return false;
// volatile prevents the compiler from caching/reordering this cross-thread read.
// The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a
// FreeRTOS mutex with a memory barrier (rsync on Xtensa), ensuring the value is
// visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH) on
// Xtensa/RISC-V and cannot produce torn values.
// FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value
// is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on
// Xtensa/RISC-V/ARM and cannot produce torn values.
return *(volatile s16_t *) &sock->rcvevent > 0;
}
@@ -200,7 +212,7 @@ void esphome_lwip_hook_socket(int fd) {
s_original_callback = sock->conn->callback;
}
// Replace with our wrapper. Atomic on ESP32 (32-bit aligned pointer write).
// Replace with our wrapper. Atomic on all supported platforms (32-bit aligned pointer write).
// TCP/IP thread sees either old or new pointer — both are valid.
sock->conn->callback = esphome_socket_event_callback;
}
@@ -213,4 +225,4 @@ void esphome_lwip_wake_main_loop(void) {
}
}
#endif // USE_ESP32
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
// Fast socket monitoring for ESP32 (ESP-IDF LwIP)
// Fast socket monitoring for ESP32 and LibreTiny (LwIP >= 2.1.3)
// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications.
#include <stdbool.h>
+2 -2
View File
@@ -4,13 +4,13 @@ dependencies:
esphome/esp-audio-libs:
version: 2.0.3
esphome/micro-opus:
version: 0.3.3
version: 0.3.4
espressif/esp-tflite-micro:
version: 1.3.3~1
espressif/esp32-camera:
version: 2.1.1
espressif/mdns:
version: 1.9.1
version: 1.10.0
espressif/esp_wifi_remote:
version: 1.3.2
rules:
@@ -0,0 +1,6 @@
esp32_ble:
io_capability: keyboard_display
# Explicitly not setting some parameters to test ifdef selection
# max_key_size: 16
# min_key_size: 7
auth_req_mode: sc_mitm_bond
@@ -0,0 +1,5 @@
esp32_ble:
io_capability: keyboard_display
max_key_size: 16
min_key_size: 7
auth_req_mode: sc_mitm_bond
@@ -0,0 +1,16 @@
esp32_hosted:
variant: ESP32C6
slot: 1
active_high: true
reset_pin: GPIO15
cmd_pin: GPIO13
clk_pin: GPIO12
d0_pin: GPIO11
d1_pin: GPIO10
d2_pin: GPIO9
d3_pin: GPIO8
sdio_frequency: 8MHz
wifi:
ssid: MySSID
password: password1
@@ -1,9 +1,14 @@
import pytest
from esphome.components import socket
from esphome.const import (
KEY_CORE,
KEY_TARGET_PLATFORM,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
)
from esphome.core import CORE
@@ -90,9 +95,15 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -
assert udp_consumers == initial_udp
def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
"""Test that ESP32 uses task notifications instead of UDP socket."""
_setup_platform(PLATFORM_ESP32)
@pytest.mark.parametrize(
"platform",
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X],
)
def test_require_wake_loop_threadsafe__fast_select_no_udp_socket(
platform: str,
) -> None:
"""Test that fast select platforms use task notifications instead of UDP socket."""
_setup_platform(platform)
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
@@ -100,13 +111,13 @@ def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
# Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications)
# Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications)
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
assert "socket.wake_loop_threadsafe" not in udp_consumers
def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None:
"""Test that non-ESP32 platforms consume a UDP socket for wake notifications."""
def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None:
"""Test that platforms without fast select consume a UDP socket for wake notifications."""
_setup_platform(PLATFORM_ESP8266)
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()