diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index caaea891432..d3e9389efca 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -84,6 +84,8 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +// Threshold in ms (computed from centiseconds constant in component.h) +static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast(WARN_IF_BLOCKING_OVER_CS) * 10U; #ifdef USE_LOOP_PRIORITY float Component::get_loop_priority() const { return 0.0f; } @@ -273,14 +275,14 @@ void Component::call() { } } bool Component::should_warn_of_blocking(uint32_t blocking_time) { - if (blocking_time > this->warn_if_blocking_over_) { - // Prevent overflow when adding increment - if we're about to overflow, just max out - if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time || - blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits::max()) { - this->warn_if_blocking_over_ = std::numeric_limits::max(); - } else { - this->warn_if_blocking_over_ = static_cast(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS); - } + // Convert centisecond threshold to milliseconds for comparison + uint32_t threshold_ms = static_cast(this->warn_if_blocking_over_) * 10U; + if (blocking_time > threshold_ms) { + // Set new threshold: blocking_time + increment, converted back to centiseconds + uint32_t new_threshold_ms = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS; + uint32_t new_cs = new_threshold_ms / 10U; + // Saturate at uint8_t max (255 = 2550ms) + this->warn_if_blocking_over_ = static_cast(new_cs > 255U ? 255U : new_cs); return true; } return false; @@ -541,4 +543,7 @@ void clear_setup_priority_overrides() { } #endif +// Weak default for component_source_lookup - overridden by generated code +__attribute__((weak)) const LogString *component_source_lookup(uint16_t) { return LOG_STR(""); } + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 46cd77b0343..6b69190d07e 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -79,11 +79,19 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Component loop override flag uses bit 5 (set at registration time) inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; +// High bit of 9-bit component_source index (bit 6 of component_state_) +inline constexpr uint8_t COMPONENT_SOURCE_HIGH_BIT = 0x40; +// Mask for the 9th bit of the component source index (bit 8) +inline constexpr uint16_t COMPONENT_SOURCE_INDEX_HIGH = 0x100; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; -inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; +inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms) + +/// Lookup component source name by index (1-based). Generated by Python codegen. +/// Weak default returns "" so builds without codegen still link. +const LogString *component_source_lookup(uint16_t index); class Component { public: @@ -143,7 +151,10 @@ class Component { */ virtual void on_powerdown() {} - uint8_t get_component_state() const { return this->component_state_; } + uint8_t get_component_state() const { + // Mask out COMPONENT_SOURCE_HIGH_BIT — it's internal to component source indexing + return this->component_state_ & ~COMPONENT_SOURCE_HIGH_BIT; + } /** Reset this component back to the construction state to allow setup to run again. * @@ -285,17 +296,15 @@ class Component { bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; } - /** Set where this component was loaded from for some debug messages. - * - * This is set by the ESPHome core, and should not be called manually. - */ - void set_component_source(const LogString *source) { component_source_ = source; } /** Get the integration where this component was declared as a LogString for logging. * * Returns LOG_STR("") if source not set */ const LogString *get_component_log_str() const { - return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; + uint16_t idx = this->component_source_index_; + if (this->component_state_ & COMPONENT_SOURCE_HIGH_BIT) + idx |= COMPONENT_SOURCE_INDEX_HIGH; + return idx == 0 ? LOG_STR("") : component_source_lookup(idx); } bool should_warn_of_blocking(uint32_t blocking_time); @@ -303,6 +312,20 @@ class Component { protected: friend class Application; + /** Set where this component was loaded from for some debug messages. + * + * This is set by the ESPHome core during setup, and should not be called manually. + * @param index 1-based index into the component source lookup table (0 = not set) + */ + void set_component_source_(uint16_t index) { + this->component_source_index_ = static_cast(index & 0xFF); + if (index & COMPONENT_SOURCE_INDEX_HIGH) { + this->component_state_ |= COMPONENT_SOURCE_HIGH_BIT; + } else { + this->component_state_ &= ~COMPONENT_SOURCE_HIGH_BIT; + } + } + virtual void call_setup(); void call_dump_config_(); @@ -519,15 +542,16 @@ class Component { void status_clear_warning_slow_path_(); void status_clear_error_slow_path_(); - // Ordered for optimal packing on 32-bit systems - const LogString *component_source_{nullptr}; - uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) + // Ordered for optimal packing on 32-bit systems (8 bytes total with vtable) + uint8_t component_source_index_{0}; ///< Lower 8 bits of 9-bit component source index + uint8_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_CS}; ///< Warn threshold in centiseconds (max 2550ms) /// State of this component - each bit has a purpose: /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) /// Bit 3: STATUS_LED_WARNING /// Bit 4: STATUS_LED_ERROR /// Bit 5: Has overridden loop() (set at registration time) - /// Bits 6-7: Unused - reserved for future expansion + /// Bit 6: High bit of 9-bit component_source index + /// Bit 7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; @@ -598,6 +622,8 @@ class WarnIfComponentBlockingGuard { this->record_runtime_stats_(); #endif #ifndef USE_BENCHMARK + // Fast path: compare against constant threshold in ms (computed at compile time from centiseconds) + static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast(WARN_IF_BLOCKING_OVER_CS) * 10U; if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { warn_blocking(this->component_, blocking_time); } diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 8f8c6931403..22d26ef3ba7 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import logging from esphome.const import ( @@ -7,15 +8,128 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, ID, coroutine +from esphome.core import CORE, ID, CoroPriority, coroutine, coroutine_with_priority from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable +from esphome.cpp_generator import ( + RawStatement, + add, + add_define, + add_global, + get_variable, +) from esphome.cpp_types import App +from esphome.helpers import cpp_string_escape from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry _LOGGER = logging.getLogger(__name__) +_COMPONENT_SOURCE_DOMAIN = "component_source_pool" + +# Maximum unique component source names (9-bit index, 0 = not set) +_MAX_COMPONENT_SOURCES = 510 + + +@dataclass +class ComponentSourcePool: + """Pool of component source names for PROGMEM lookup table. + + Source names are registered during to_code() and assigned 1-based indices. + Index 0 means "not set" (returns LOG_STR("")). At render time, + the pool generates a C++ PROGMEM table + lookup function. + """ + + sources: dict[str, int] = field(default_factory=dict) + table_registered: bool = False + + +def _get_source_pool() -> ComponentSourcePool: + """Get or create the component source pool from CORE.data.""" + if _COMPONENT_SOURCE_DOMAIN not in CORE.data: + CORE.data[_COMPONENT_SOURCE_DOMAIN] = ComponentSourcePool() + return CORE.data[_COMPONENT_SOURCE_DOMAIN] + + +def _ensure_source_table_registered() -> None: + """Schedule the table generation job (once).""" + pool = _get_source_pool() + if pool.table_registered: + return + pool.table_registered = True + CORE.add_job(_generate_component_source_table) + + +def register_component_source(name: str) -> int: + """Register a component source name and return its 1-based index. + + Deduplicates: multiple components from the same source share one index. + """ + if not name: + return 0 + pool = _get_source_pool() + if name in pool.sources: + return pool.sources[name] + idx = len(pool.sources) + 1 + if idx > _MAX_COMPONENT_SOURCES: + raise ValueError( + f"Too many unique component source names (max {_MAX_COMPONENT_SOURCES}), got {idx}: '{name}'" + ) + pool.sources[name] = idx + _ensure_source_table_registered() + return idx + + +def _generate_source_table_code( + table_var: str, + lookup_fn: str, + strings: dict[str, int], +) -> str: + """Generate C++ PROGMEM table + LogString* lookup for component sources. + + Same pattern as entity_helpers._generate_category_code but returns + const LogString* instead of const char* (needed for LOG_STR_ARG). + """ + if not strings: + return "" + + sorted_strings = sorted(strings.items(), key=lambda x: x[1]) + count = len(sorted_strings) + + # Emit individual PROGMEM char arrays so string data lives in flash on ESP8266 + lines: list[str] = [] + var_names: list[str] = [] + for i, (s, _) in enumerate(sorted_strings): + var_name = f"{table_var}_STR_{i}" + var_names.append(var_name) + lines.append( + f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};" + ) + + entries = ", ".join(var_names) + lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") + lines.append(f"const LogString *{lookup_fn}(uint16_t index) {{") + lines.append(f' if (index == 0 || index > {count}) return LOG_STR("");') + lines.append(" return reinterpret_cast(") + lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));") + lines.append("}") + return "\n".join(lines) + "\n" + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _generate_component_source_table() -> None: + """Generate the component source lookup table as a FINAL-priority job. + + Runs after all component to_code() calls have registered their sources. + """ + pool = _get_source_pool() + code = _generate_source_table_code( + "COMP_SRC_TABLE", "component_source_lookup", pool.sources + ) + if code: + add_global( + RawStatement(f"namespace esphome {{\n{code}}} // namespace esphome") + ) + async def gpio_pin_expression(conf): """Generate an expression for the given pin option. @@ -77,7 +191,8 @@ async def register_component(var, config): "Error while finding name of component, please report this", exc_info=e ) if name is not None: - add(var.set_component_source(LogStringLiteral(name))) + idx = register_component_source(name) + add(var.set_component_source_(idx)) add(App.register_component_(var))