diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index ad6d584245..f399cf8172 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -48,6 +48,7 @@ void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint16_t progmem_read_word(const uint16_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 50345f8bc3..db56dcbcae 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -35,6 +35,9 @@ uint8_t progmem_read_byte(const uint8_t *addr) { uint16_t progmem_read_word(const uint16_t *addr) { return pgm_read_word(addr); // NOLINT } +const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 296ada2c3c..85d9f0b530 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -54,6 +54,7 @@ void HOT arch_feed_wdt() { uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint16_t progmem_read_word(const uint16_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index a1a7e7745e..ae48d5fc33 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -35,6 +35,7 @@ uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint16_t progmem_read_word(const uint16_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } } // namespace esphome diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index ad8c3a414a..feda41a962 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -35,6 +35,9 @@ uint8_t progmem_read_byte(const uint8_t *addr) { uint16_t progmem_read_word(const uint16_t *addr) { return pgm_read_word(addr); // NOLINT } +const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index 7b41fa30cd..1eef66aa73 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -60,6 +60,7 @@ uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint16_t progmem_read_word(const uint16_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } Mutex::Mutex() { auto *mutex = new k_mutex(); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index e521f2e658..d269aecafc 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -19,7 +19,7 @@ from esphome.const import ( from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv -from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case +from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case from esphome.types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) @@ -50,11 +50,11 @@ _MAX_ICONS = 0xFFF # 12 bits → 4095 @dataclass class EntityStringPool: - """Pool of entity string properties packed into contiguous blobs. + """Pool of entity string properties for PROGMEM pointer tables. Strings are registered during to_code() and assigned 1-based indices. Index 0 means "not set" (empty string). At render time, the pool - generates C++ blob + PROGMEM offset table + lookup function per category. + generates C++ PROGMEM pointer table + lookup function per category. """ device_classes: dict[str, int] = field(default_factory=dict) @@ -79,66 +79,38 @@ def _ensure_tables_registered() -> None: CORE.add_job(_generate_tables_job) -def _generate_blob_and_offsets( - strings: dict[str, int], -) -> tuple[bytes, list[int]]: - """Build a packed blob and offset list from a string dict. - - Returns (blob_bytes, offsets) where offsets are 1-based (matching the - 1-based indices returned by _register_string). Index 0 means "not set" - and is handled by the caller before reaching the lookup function. - """ - # Sort by assigned index to ensure deterministic output - sorted_strings = sorted(strings.items(), key=lambda x: x[1]) - blob = bytearray() - offsets = [] - for s, _idx in sorted_strings: - offsets.append(len(blob)) - blob.extend(s.encode("utf-8")) - blob.append(0) # null terminator - return bytes(blob), offsets - - def _generate_category_code( - blob_var: str, - offsets_var: str, + table_var: str, lookup_fn: str, strings: dict[str, int], ) -> str: - """Generate C++ code for one string category (blob + offsets + lookup). + """Generate C++ code for one string category (PROGMEM pointer table + lookup). - Index 0 means "not set" and is handled by get_*_ref() before calling - the lookup function. The lookup uses 1-based indexing directly. + Uses a PROGMEM array of string pointers. On ESP8266, pointers are stored + in flash (via PROGMEM) and read with progmem_read_ptr(). String literals + themselves remain in RAM but benefit from linker string deduplication. + Index 0 means "not set" and returns empty string. """ if not strings: return "" - blob_bytes, offsets = _generate_blob_and_offsets(strings) - use_uint16 = len(blob_bytes) > 255 - offset_type = "uint16_t" if use_uint16 else "uint8_t" - read_fn = "progmem_read_word" if use_uint16 else "progmem_read_byte" - count = len(offsets) - blob_escaped = cpp_string_escape(blob_bytes) - offsets_str = ", ".join(str(o) for o in offsets) - - # The blob's last byte is always '\0' (null terminator of last string). - # Point there for index 0 ("not set") and out-of-range to return "". - empty_offset = len(blob_bytes) - 1 + sorted_strings = sorted(strings.items(), key=lambda x: x[1]) + entries = ", ".join(f'"{s}"' for s, _ in sorted_strings) + count = len(sorted_strings) return ( - f"static const char {blob_var}[] = {blob_escaped};\n" - f"static const {offset_type} {offsets_var}[] PROGMEM = {{{offsets_str}}};\n" + f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n" f"const char *{lookup_fn}(uint16_t index) {{\n" - f" if (index == 0 || index > {count}) return &{blob_var}[{empty_offset}];\n" - f" return &{blob_var}[{read_fn}(&{offsets_var}[index - 1])];\n" + f' if (index == 0 || index > {count}) return "";\n' + f" return progmem_read_ptr(&{table_var}[index - 1]);\n" f"}}\n" ) _CATEGORY_CONFIGS = ( - ("ENTITY_DC", "entity_device_class_lookup", "device_classes"), - ("ENTITY_UOM", "entity_uom_lookup", "units"), - ("ENTITY_ICON", "entity_icon_lookup", "icons"), + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"), + ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"), + ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"), ) @@ -150,10 +122,8 @@ async def _generate_tables_job() -> None: """ pool = _get_pool() parts = ["namespace esphome {"] - for prefix, lookup_fn, attr in _CATEGORY_CONFIGS: - code = _generate_category_code( - f"{prefix}_BLOB", f"{prefix}_OFFSETS", lookup_fn, getattr(pool, attr) - ) + for table_var, lookup_fn, attr in _CATEGORY_CONFIGS: + code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr)) if code: parts.append(code) parts.append("} // namespace esphome") diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 25c327a5dd..d6c7695895 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -42,5 +42,6 @@ uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); uint8_t progmem_read_byte(const uint8_t *addr); uint16_t progmem_read_word(const uint16_t *addr); +const char *progmem_read_ptr(const char *const *addr); } // namespace esphome diff --git a/script/clang-tidy b/script/clang-tidy index 17bcafacc7..9c2899026d 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -79,6 +79,7 @@ def clang_options(idedata): "-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))", "-Dpgm_read_word(s)=(*(const uint16_t *)(s))", "-Dpgm_read_dword(s)=(*(const uint32_t *)(s))", + "-Dpgm_read_ptr(s)=(*(const void *const *)(s))", "-DPROGMEM=", "-DPGM_P=const char *", "-DPSTR(s)=(s)",