From 3a491722b2d91357de8148a35f132518951bde76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Mar 2026 15:23:04 -1000 Subject: [PATCH 1/3] [esp32] Use ESP-IDF Log V2 to reduce flash usage Switch from ESP-IDF Log V1 to V2, which centralizes log formatting inside esp_log() instead of expanding esp_log_timestamp(), color codes, and LOG_FORMAT() at every ESP_LOGx macro call site. This saves ~9KB of flash by eliminating ~500 per-site macro expansions in ESP-IDF library code (gpio, ethernet, mdns, uart, wifi, etc.). Override esp_log_format() to skip ESP-IDF's own formatting after the ESPHome logger hook is installed, since ESPHome does its own formatting. For early boot and constrained environments (ISR, cache disabled), format messages in ESPHome style with colors using a stack buffer. --- esphome/components/esp32/__init__.py | 5 ++ esphome/core/log.cpp | 70 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 475de6aa3e..e225721d90 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1614,6 +1614,11 @@ async def to_code(config): # This saves ~250 bytes of RAM (tag cache) and associated code add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True) + # Use ESP-IDF Log V2 to eliminate per-site esp_log_timestamp() macro expansions + # V2 centralizes formatting inside esp_log(), reducing flash usage + add_idf_sdkconfig_option("CONFIG_LOG_VERSION_1", False) + add_idf_sdkconfig_option("CONFIG_LOG_VERSION_2", True) + # Reduce PHY TX power in the event of a brownout add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 0da457adec..f01f0053d4 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -86,3 +86,73 @@ int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT #endif } // namespace esphome + +#if defined(USE_ESP32) && !defined(BOOTLOADER_BUILD) +// Override esp_log_format to disable ESP-IDF's own log formatting so that +// the vprintf hook receives a single call per message (just the user's format +// string + args). Without this, Log V2 makes 3 vprintf calls per message +// (header, body, newline) which fragments the output in ESPHome's logger. +// This strong definition overrides the archive symbol from ESP-IDF's liblog. +// It affects all callers including precompiled blobs (e.g. wifi). +// +// Before the ESPHome logger hook is installed (early boot), we fall through +// to the original ESP-IDF formatting so boot messages have proper formatting. +#include +#include +#include +#include + +// Outlined cold path for early boot / constrained environment logging. +// Uses esp_log_printf/esp_log_vprintf which dispatch to esp_rom_vprintf +// for constrained environments (same as ESP-IDF's original esp_log_format). +// Must be in IRAM since it's called from the IRAM esp_log_format during +// early boot when the scheduler isn't running (constrained_env=1). +static void IRAM_ATTR __attribute__((noinline)) esp_log_format_early_(esp_log_msg_t *message) { + // ESP-IDF levels: NONE=0 ERROR=1 WARN=2 INFO=3 DEBUG=4 VERBOSE=5 + // Color digits: E=1(red) W=3(yellow) I=2(green) D=6(cyan) V=7(gray) + // DRAM_ATTR required since this function is in IRAM and can't access flash constants + static DRAM_ATTR const char color_digit[] = {'\0', '1', '3', '2', '6', '7'}; + static DRAM_ATTR const char lvl[] = {'\0', 'E', 'W', 'I', 'D', 'V'}; + uint8_t level = message->config.opts.log_level; +#if CONFIG_LIBC_NEWLIB + if (!message->config.opts.constrained_env) { + flockfile(stdout); + } +#endif + if (level > 0 && level < sizeof(lvl)) { + esp_log_printf(message->config, "\033[0;3%cm[%c][%s:000]: ", color_digit[level], lvl[level], + message->tag ? message->tag : "esp-idf"); + } + esp_log_vprintf(message->config, message->format, message->args); + if (level > 0 && level < sizeof(lvl)) { + esp_log_printf(message->config, "\033[0m\n"); + } else { + esp_log_printf(message->config, "\n"); + } +#if CONFIG_LIBC_NEWLIB + if (!message->config.opts.constrained_env) { + funlockfile(stdout); + } +#endif +} + +extern "C" { +// IRAM_ATTR required because ESP-IDF places esp_log_format in IRAM when +// CONFIG_LOG_IN_IRAM is enabled, and it may be called from constrained +// environments (ISR, cache disabled) where flash is inaccessible. +void IRAM_ATTR esp_log_format(esp_log_msg_t *message) { + // Check if ESPHome's vprintf hook is installed by comparing against default. + // Before logger init, esp_log_vprint_func == &vprintf (the default). + extern vprintf_like_t esp_log_vprint_func; + extern int vprintf(const char *, __gnuc_va_list); // NOLINT + if (esp_log_vprint_func == &vprintf || message->config.opts.constrained_env) [[unlikely]] { + // Early boot or constrained env (ISR, cache disabled): + // use ROM functions only — flash may be inaccessible. + esp_log_format_early_(message); + return; + } + // After hook installed, normal environment: skip formatting, forward body only + esp_log_vprintf(message->config, message->format, message->args); +} +} // extern "C" +#endif From 99e0dcf5631dc13f134be47d9eee61a8cb3e9c2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Mar 2026 17:03:14 -1000 Subject: [PATCH 2/3] [esp32] Use DRAM_ATTR for format strings in IRAM log override ESP-IDF places log_format_text.c in IRAM/DRAM via linker fragment (noflash) when CONFIG_LOG_IN_IRAM=y. Our override is in a different compilation unit so string literals would default to flash. In constrained environments where flash cache is disabled, reading flash-resident format strings would fault. Move all format string constants to DRAM_ATTR to match ESP-IDF's own behavior. --- esphome/core/log.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index f01f0053d4..098bfd8abf 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -110,9 +110,16 @@ int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT static void IRAM_ATTR __attribute__((noinline)) esp_log_format_early_(esp_log_msg_t *message) { // ESP-IDF levels: NONE=0 ERROR=1 WARN=2 INFO=3 DEBUG=4 VERBOSE=5 // Color digits: E=1(red) W=3(yellow) I=2(green) D=6(cyan) V=7(gray) - // DRAM_ATTR required since this function is in IRAM and can't access flash constants + // DRAM_ATTR required: this function is IRAM_ATTR and may be called from constrained + // environments where flash cache is disabled. All string constants must be in DRAM. + // ESP-IDF's own log_format_text.c achieves this via linker fragment (noflash), but + // our override is in a different compilation unit so we must use DRAM_ATTR explicitly. static DRAM_ATTR const char color_digit[] = {'\0', '1', '3', '2', '6', '7'}; static DRAM_ATTR const char lvl[] = {'\0', 'E', 'W', 'I', 'D', 'V'}; + static DRAM_ATTR const char fmt_header[] = "\033[0;3%cm[%c][%s:000]: "; + static DRAM_ATTR const char fmt_reset_nl[] = "\033[0m\n"; + static DRAM_ATTR const char fmt_nl[] = "\n"; + static DRAM_ATTR const char tag_fallback[] = "esp-idf"; uint8_t level = message->config.opts.log_level; #if CONFIG_LIBC_NEWLIB if (!message->config.opts.constrained_env) { @@ -120,14 +127,14 @@ static void IRAM_ATTR __attribute__((noinline)) esp_log_format_early_(esp_log_ms } #endif if (level > 0 && level < sizeof(lvl)) { - esp_log_printf(message->config, "\033[0;3%cm[%c][%s:000]: ", color_digit[level], lvl[level], - message->tag ? message->tag : "esp-idf"); + esp_log_printf(message->config, fmt_header, color_digit[level], lvl[level], + message->tag ? message->tag : tag_fallback); } esp_log_vprintf(message->config, message->format, message->args); if (level > 0 && level < sizeof(lvl)) { - esp_log_printf(message->config, "\033[0m\n"); + esp_log_printf(message->config, fmt_reset_nl); } else { - esp_log_printf(message->config, "\n"); + esp_log_printf(message->config, fmt_nl); } #if CONFIG_LIBC_NEWLIB if (!message->config.opts.constrained_env) { From d3055ea6ea1314b7cfcba657d04e44d2cdb77f8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Mar 2026 17:06:45 -1000 Subject: [PATCH 3/3] [esp32] Drop :000 line number and shorten fallback tag --- esphome/core/log.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 098bfd8abf..705717b317 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -116,10 +116,10 @@ static void IRAM_ATTR __attribute__((noinline)) esp_log_format_early_(esp_log_ms // our override is in a different compilation unit so we must use DRAM_ATTR explicitly. static DRAM_ATTR const char color_digit[] = {'\0', '1', '3', '2', '6', '7'}; static DRAM_ATTR const char lvl[] = {'\0', 'E', 'W', 'I', 'D', 'V'}; - static DRAM_ATTR const char fmt_header[] = "\033[0;3%cm[%c][%s:000]: "; + static DRAM_ATTR const char fmt_header[] = "\033[0;3%cm[%c][%s]: "; static DRAM_ATTR const char fmt_reset_nl[] = "\033[0m\n"; static DRAM_ATTR const char fmt_nl[] = "\n"; - static DRAM_ATTR const char tag_fallback[] = "esp-idf"; + static DRAM_ATTR const char tag_fallback[] = "idf"; uint8_t level = message->config.opts.log_level; #if CONFIG_LIBC_NEWLIB if (!message->config.opts.constrained_env) {