Merge pull request #9796 from esphome/bump-2025.7.3

2025.7.3
This commit is contained in:
Jesse Hills 2025-07-23 08:09:40 +12:00 committed by GitHub
commit 2b5cceda58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 194 additions and 75 deletions

View File

@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2025.7.2 PROJECT_NUMBER = 2025.7.3
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, i2c from esphome.components import esp32, i2c
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework
CODEOWNERS = ["@trvrnrth"] CODEOWNERS = ["@trvrnrth"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All(
): cv.positive_time_period_minutes, ): cv.positive_time_period_minutes,
} }
).extend(i2c.i2c_device_schema(0x76)), ).extend(i2c.i2c_device_schema(0x76)),
cv.only_with_arduino, cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"bme68x_bsec2_i2c",
"sensor/bme68x_bsec2",
)
},
),
cv.Any( cv.Any(
cv.only_on_esp8266, cv.only_on_esp8266,
cv.All( cv.All(

View File

@ -16,6 +16,8 @@ namespace esp32_touch {
static const char *const TAG = "esp32_touch"; static const char *const TAG = "esp32_touch";
static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF;
void ESP32TouchComponent::setup() { void ESP32TouchComponent::setup() {
// Create queue for touch events // Create queue for touch events
// Queue size calculation: children * 4 allows for burst scenarios where ISR // Queue size calculation: children * 4 allows for burst scenarios where ISR
@ -44,8 +46,12 @@ void ESP32TouchComponent::setup() {
// Configure each touch pad // Configure each touch pad
for (auto *child : this->children_) { for (auto *child : this->children_) {
if (this->setup_mode_) {
touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD);
} else {
touch_pad_config(child->get_touch_pad(), child->get_threshold()); touch_pad_config(child->get_touch_pad(), child->get_threshold());
} }
}
// Register ISR handler // Register ISR handler
esp_err_t err = touch_pad_isr_register(touch_isr_handler, this); esp_err_t err = touch_pad_isr_register(touch_isr_handler, this);
@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() {
child->publish_state(new_state); child->publish_state(new_state);
// Original ESP32: ISR only fires when touched, release is detected by timeout // Original ESP32: ISR only fires when touched, release is detected by timeout
// Note: ESP32 v1 uses inverted logic - touched when value < threshold // Note: ESP32 v1 uses inverted logic - touched when value < threshold
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")", ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")",
child->get_name().c_str(), event.value, child->get_threshold()); child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold());
} }
break; // Exit inner loop after processing matching pad break; // Exit inner loop after processing matching pad
} }
@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
// as any pad remains touched. This allows us to detect both new touches and // as any pad remains touched. This allows us to detect both new touches and
// continued touches, but releases must be detected by timeout in the main loop. // continued touches, but releases must be detected by timeout in the main loop.
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
// Process all configured pads to check their current state // Process all configured pads to check their current state
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
// so we must scan all configured pads to find which ones were touched // so we must scan all configured pads to find which ones were touched
@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
} }
// Skip pads that arent in the trigger mask // Skip pads that arent in the trigger mask
bool is_touched = (mask >> pad) & 1; if (((mask >> pad) & 1) == 0) {
if (!is_touched) {
continue; continue;
} }
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
bool is_touched = value < child->get_threshold();
// Always send the current state - the main loop will filter for changes // Always send the current state - the main loop will filter for changes
// We send both touched and untouched states because the ISR doesn't // We send both touched and untouched states because the ISR doesn't
// track previous state (to keep ISR fast and simple) // track previous state (to keep ISR fast and simple)

View File

@ -2,7 +2,13 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fastled_base from esphome.components import fastled_base
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER from esphome.const import (
CONF_CHIPSET,
CONF_NUM_LEDS,
CONF_PIN,
CONF_RGB_ORDER,
Framework,
)
AUTO_LOAD = ["fastled_base"] AUTO_LOAD = ["fastled_base"]
@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
} }
), ),
_validate, cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"esp32_rmt_led_strip",
"light/esp32_rmt_led_strip",
)
},
),
cv.require_framework_version( cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 4), esp8266_arduino=cv.Version(2, 7, 4),
esp32_arduino=cv.Version(99, 0, 0), esp32_arduino=cv.Version(99, 0, 0),
max_version=True, max_version=True,
extra_message="Please see note on documentation for FastLED", extra_message="Please see note on documentation for FastLED",
), ),
_validate,
) )

View File

@ -9,6 +9,7 @@ from esphome.const import (
CONF_DATA_RATE, CONF_DATA_RATE,
CONF_NUM_LEDS, CONF_NUM_LEDS,
CONF_RGB_ORDER, CONF_RGB_ORDER,
Framework,
) )
AUTO_LOAD = ["fastled_base"] AUTO_LOAD = ["fastled_base"]
@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DATA_RATE): cv.frequency, cv.Optional(CONF_DATA_RATE): cv.frequency,
} }
), ),
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"spi_led_strip",
"light/spi_led_strip",
)
},
),
cv.require_framework_version( cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 4), esp8266_arduino=cv.Version(2, 7, 4),
esp32_arduino=cv.Version(99, 0, 0), esp32_arduino=cv.Version(99, 0, 0),

View File

@ -4,7 +4,13 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor from esphome.components import binary_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN from esphome.const import (
CONF_ALLOW_OTHER_USES,
CONF_ID,
CONF_NAME,
CONF_NUMBER,
CONF_PIN,
)
from esphome.core import CORE from esphome.core import CORE
from .. import gpio_ns from .. import gpio_ns
@ -76,6 +82,18 @@ async def to_code(config):
) )
use_interrupt = False use_interrupt = False
# Check if pin is shared with other components (allow_other_uses)
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
"The sensor will use polling mode for compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
config[CONF_PIN][CONF_NUMBER],
)
use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt)) cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt: if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))

View File

@ -15,6 +15,7 @@ from esphome.const import (
CONF_PIN, CONF_PIN,
CONF_TYPE, CONF_TYPE,
CONF_VARIANT, CONF_VARIANT,
Framework,
) )
from esphome.core import CORE from esphome.core import CORE
@ -162,7 +163,15 @@ def _validate_method(value):
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.only_with_arduino, cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"esp32_rmt_led_strip",
"light/esp32_rmt_led_strip",
)
},
),
cv.require_framework_version( cv.require_framework_version(
esp8266_arduino=cv.Version(2, 4, 0), esp8266_arduino=cv.Version(2, 4, 0),
esp32_arduino=cv.Version(0, 0, 0), esp32_arduino=cv.Version(0, 0, 0),

View File

@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
} }
void Sdl::draw_pixel_at(int x, int y, Color color) { void Sdl::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
return;
SDL_Rect rect{x, y, 1, 1}; SDL_Rect rect{x, y, 1, 1};
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
SDL_UpdateTexture(this->texture_, &rect, &data, 2); SDL_UpdateTexture(this->texture_, &rect, &data, 2);

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fan from esphome.components import fan
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
from .. import CONF_TUYA_ID, Tuya, tuya_ns from .. import CONF_TUYA_ID, Tuya, tuya_ns
@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint"
TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan) TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
fan.FAN_SCHEMA.extend( fan.fan_schema(TuyaFan)
.extend(
{ {
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan),
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
} }
).extend(cv.COMPONENT_SCHEMA), )
.extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT), cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
) )
@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
parent = await cg.get_variable(config[CONF_TUYA_ID]) parent = await cg.get_variable(config[CONF_TUYA_ID])
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], parent, config[CONF_SPEED_COUNT]) var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT])
await cg.register_component(var, config) await cg.register_component(var, config)
await fan.register_fan(var, config) await fan.register_fan(var, config)

View File

@ -76,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
} else { } else {
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
} }
#ifdef USE_OTA_STATE_CALLBACK #ifdef USE_OTA_STATE_CALLBACK
// Report progress - use call_deferred since we're in web server task // Report progress - use call_deferred since we're in web server task
@ -171,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
// Finalize // Finalize
if (final) { if (final) {
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len, ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
this->ota_read_length_, request->contentLength()); this->ota_read_length_, request->contentLength());
// For Arduino framework, the Update library tracks expected size from firmware header // For Arduino framework, the Update library tracks expected size from firmware header

View File

@ -73,6 +73,7 @@ from esphome.const import (
TYPE_GIT, TYPE_GIT,
TYPE_LOCAL, TYPE_LOCAL,
VALID_SUBSTITUTIONS_CHARACTERS, VALID_SUBSTITUTIONS_CHARACTERS,
Framework,
__version__ as ESPHOME_VERSION, __version__ as ESPHOME_VERSION,
) )
from esphome.core import ( from esphome.core import (
@ -282,6 +283,38 @@ class FinalExternalInvalid(Invalid):
"""Represents an invalid value in the final validation phase where the path should not be prepended.""" """Represents an invalid value in the final validation phase where the path should not be prepended."""
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int
patch: int
extra: str = ""
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
@classmethod
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")
major = int(match[1])
minor = int(match[2])
patch = int(match[3])
extra = match[4] or ""
return Version(major=major, minor=minor, patch=patch, extra=extra)
@property
def is_beta(self) -> bool:
"""Check if this version is a beta version."""
return self.extra.startswith("b")
@property
def is_dev(self) -> bool:
"""Check if this version is a development version."""
return self.extra.startswith("dev")
def check_not_templatable(value): def check_not_templatable(value):
if isinstance(value, Lambda): if isinstance(value, Lambda):
raise Invalid("This option is not templatable!") raise Invalid("This option is not templatable!")
@ -619,16 +652,35 @@ def only_on(platforms):
return validator_ return validator_
def only_with_framework(frameworks): def only_with_framework(
frameworks: Framework | str | list[Framework | str], suggestions=None
):
"""Validate that this option can only be specified on the given frameworks.""" """Validate that this option can only be specified on the given frameworks."""
if not isinstance(frameworks, list): if not isinstance(frameworks, list):
frameworks = [frameworks] frameworks = [frameworks]
frameworks = [Framework(framework) for framework in frameworks]
if suggestions is None:
suggestions = {}
version = Version.parse(ESPHOME_VERSION)
if version.is_beta:
docs_format = "https://beta.esphome.io/components/{path}"
elif version.is_dev:
docs_format = "https://next.esphome.io/components/{path}"
else:
docs_format = "https://esphome.io/components/{path}"
def validator_(obj): def validator_(obj):
if CORE.target_framework not in frameworks: if CORE.target_framework not in frameworks:
raise Invalid( err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}"
f"This feature is only available with frameworks {frameworks}" if suggestion := suggestions.get(CORE.target_framework, None):
) (component, docs_path) = suggestion
err_str += f"\nPlease use '{component}'"
if docs_path:
err_str += f": {docs_format.format(path=docs_path)}"
raise Invalid(err_str)
return obj return obj
return validator_ return validator_
@ -637,8 +689,8 @@ def only_with_framework(frameworks):
only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp32 = only_on(PLATFORM_ESP32)
only_on_esp8266 = only_on(PLATFORM_ESP8266) only_on_esp8266 = only_on(PLATFORM_ESP8266)
only_on_rp2040 = only_on(PLATFORM_RP2040) only_on_rp2040 = only_on(PLATFORM_RP2040)
only_with_arduino = only_with_framework("arduino") only_with_arduino = only_with_framework(Framework.ARDUINO)
only_with_esp_idf = only_with_framework("esp-idf") only_with_esp_idf = only_with_framework(Framework.ESP_IDF)
# Adapted from: # Adapted from:
@ -1965,26 +2017,6 @@ def source_refresh(value: str):
return positive_time_period_seconds(value) return positive_time_period_seconds(value)
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int
patch: int
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
@classmethod
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")
major = int(match[1])
minor = int(match[2])
patch = int(match[3])
return Version(major=major, minor=minor, patch=patch)
def version_number(value): def version_number(value):
value = string_strict(value) value = string_strict(value)
try: try:

View File

@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum from esphome.enum import StrEnum
__version__ = "2025.7.2" __version__ = "2025.7.3"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@ -68,8 +68,11 @@ void Application::setup() {
do { do {
uint8_t new_app_state = STATUS_LED_WARNING; uint8_t new_app_state = STATUS_LED_WARNING;
this->scheduler.call(); uint32_t now = millis();
this->feed_wdt();
// Process pending loop enables to handle GPIO interrupts during setup
this->before_loop_tasks_(now);
for (uint32_t j = 0; j <= i; j++) { for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component // Update loop_component_start_time_ right before calling each component
this->loop_component_start_time_ = millis(); this->loop_component_start_time_ = millis();
@ -78,6 +81,8 @@ void Application::setup() {
this->app_state_ |= new_app_state; this->app_state_ |= new_app_state;
this->feed_wdt(); this->feed_wdt();
} }
this->after_loop_tasks_();
this->app_state_ = new_app_state; this->app_state_ = new_app_state;
yield(); yield();
} while (!component->can_proceed()); } while (!component->can_proceed());
@ -94,30 +99,10 @@ void Application::setup() {
void Application::loop() { void Application::loop() {
uint8_t new_app_state = 0; uint8_t new_app_state = 0;
this->scheduler.call();
// Get the initial loop time at the start // Get the initial loop time at the start
uint32_t last_op_end_time = millis(); uint32_t last_op_end_time = millis();
// Feed WDT with time this->before_loop_tasks_(last_op_end_time);
this->feed_wdt(last_op_end_time);
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
if (this->has_pending_enable_loop_requests_) {
// Clear flag BEFORE processing to avoid race condition
// If ISR sets it during processing, we'll catch it next loop iteration
// This is safe because:
// 1. Each component has its own pending_enable_loop_ flag that we check
// 2. If we can't process a component (wrong state), enable_pending_loops_()
// will set this flag back to true
// 3. Any new ISR requests during processing will set the flag again
this->has_pending_enable_loop_requests_ = false;
this->enable_pending_loops_();
}
// Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
this->current_loop_index_++) { this->current_loop_index_++) {
@ -138,7 +123,7 @@ void Application::loop() {
this->feed_wdt(last_op_end_time); this->feed_wdt(last_op_end_time);
} }
this->in_loop_ = false; this->after_loop_tasks_();
this->app_state_ = new_app_state; this->app_state_ = new_app_state;
// Use the last component's end time instead of calling millis() again // Use the last component's end time instead of calling millis() again
@ -400,6 +385,36 @@ void Application::enable_pending_loops_() {
} }
} }
void Application::before_loop_tasks_(uint32_t loop_start_time) {
// Process scheduled tasks
this->scheduler.call();
// Feed the watchdog timer
this->feed_wdt(loop_start_time);
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
if (this->has_pending_enable_loop_requests_) {
// Clear flag BEFORE processing to avoid race condition
// If ISR sets it during processing, we'll catch it next loop iteration
// This is safe because:
// 1. Each component has its own pending_enable_loop_ flag that we check
// 2. If we can't process a component (wrong state), enable_pending_loops_()
// will set this flag back to true
// 3. Any new ISR requests during processing will set the flag again
this->has_pending_enable_loop_requests_ = false;
this->enable_pending_loops_();
}
// Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
}
void Application::after_loop_tasks_() {
// Clear the in_loop_ flag to indicate we're done processing components
this->in_loop_ = false;
}
#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_SOCKET_SELECT_SUPPORT
bool Application::register_socket_fd(int fd) { bool Application::register_socket_fd(int fd) {
// WARNING: This function is NOT thread-safe and must only be called from the main loop // WARNING: This function is NOT thread-safe and must only be called from the main loop

View File

@ -504,6 +504,8 @@ class Application {
void enable_component_loop_(Component *component); void enable_component_loop_(Component *component);
void enable_pending_loops_(); void enable_pending_loops_();
void activate_looping_component_(uint16_t index); void activate_looping_component_(uint16_t index);
void before_loop_tasks_(uint32_t loop_start_time);
void after_loop_tasks_();
void feed_wdt_arch_(); void feed_wdt_arch_();