From 3b22c4cd6260a1b5eb340fd73a5b9ebf34a1b3d2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:53:07 -0400 Subject: [PATCH 01/15] Capture both cores' backtraces in crash handler On dual-core ESP32 chips, the crash handler now captures the backtrace from both cores during a panic. This is especially useful for interrupt WDT crashes where the panic runs on CPU0 (idle task) but the actual problem is on CPU1 (where ESPHome's main loop runs). --- esphome/components/esp32/crash_handler.cpp | 115 ++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index ecf30d78781..c0fa284d2ed 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -66,7 +66,7 @@ static inline bool is_return_addr(uint32_t addr) { // Magic is second to validate the data. Remaining fields can change between versions. // Version is uint32_t because it would be padded to 4 bytes anyway before the next // uint32_t field, so we use the full width rather than wasting 3 bytes of padding. -static constexpr uint32_t CRASH_DATA_VERSION = 1; +static constexpr uint32_t CRASH_DATA_VERSION = 2; struct RawCrashData { uint32_t version; uint32_t magic; @@ -77,6 +77,13 @@ struct RawCrashData { uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic) uint32_t backtrace[MAX_BACKTRACE]; uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) + uint8_t crashed_core; +#if SOC_CPU_CORES_NUM > 1 + uint8_t other_backtrace_count; + uint8_t other_reg_frame_count; + // 1 byte implicit padding before uint32_t array + uint32_t other_backtrace[MAX_BACKTRACE]; +#endif }; static RawCrashData __attribute__((section(".noinit"))) s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -100,6 +107,14 @@ void crash_handler_read_and_clear() { s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT if (s_raw_crash_data.pseudo_excause > 1) s_raw_crash_data.pseudo_excause = 0; + if (s_raw_crash_data.crashed_core >= SOC_CPU_CORES_NUM) + s_raw_crash_data.crashed_core = 0; +#if SOC_CPU_CORES_NUM > 1 + if (s_raw_crash_data.other_backtrace_count > MAX_BACKTRACE) + s_raw_crash_data.other_backtrace_count = MAX_BACKTRACE; + if (s_raw_crash_data.other_reg_frame_count > s_raw_crash_data.other_backtrace_count) + s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count; +#endif } // Clear magic regardless so we don't re-report on next normal reboot s_raw_crash_data.magic = 0; @@ -228,6 +243,7 @@ void crash_handler_log() { } else { ESP_LOGE(TAG, " Reason: %s", get_exception_type()); } + ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core); ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc); uint8_t bt_num = 0; for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) { @@ -244,6 +260,29 @@ void crash_handler_log() { #endif ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); } + +#if SOC_CPU_CORES_NUM > 1 + // Log the other core's backtrace if captured + if (s_raw_crash_data.other_backtrace_count > 0) { + int other_core = 1 - s_raw_crash_data.crashed_core; + ESP_LOGE(TAG, " Other core (%d) backtrace:", other_core); + bt_num = 0; + for (uint8_t i = 0; i < s_raw_crash_data.other_backtrace_count; i++) { + uint32_t addr = s_raw_crash_data.other_backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= s_raw_crash_data.other_reg_frame_count && !is_return_addr(addr)) + continue; +#endif +#if CONFIG_IDF_TARGET_ARCH_RISCV + const char *source = (i < s_raw_crash_data.other_reg_frame_count) ? "backtrace" : "stack scan"; +#else + const char *source = "backtrace"; +#endif + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); + } + } +#endif + // Build addr2line hint with all captured addresses for easy copy-paste char hint[256]; int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc); @@ -255,6 +294,16 @@ void crash_handler_log() { #endif pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); } +#if SOC_CPU_CORES_NUM > 1 + for (uint8_t i = 0; i < s_raw_crash_data.other_backtrace_count && pos < (int) sizeof(hint) - 12; i++) { + uint32_t addr = s_raw_crash_data.other_backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= s_raw_crash_data.other_reg_frame_count && !is_return_addr(addr)) + continue; +#endif + pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); + } +#endif ESP_LOGE(TAG, "%s", hint); } @@ -276,6 +325,11 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { s_raw_crash_data.reg_frame_count = 0; s_raw_crash_data.exception = (uint8_t) info->exception; s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; + s_raw_crash_data.crashed_core = (uint8_t) info->core; +#if SOC_CPU_CORES_NUM > 1 + s_raw_crash_data.other_backtrace_count = 0; + s_raw_crash_data.other_reg_frame_count = 0; +#endif #if CONFIG_IDF_TARGET_ARCH_XTENSA // Xtensa: walk the backtrace using the public API @@ -308,6 +362,39 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { s_raw_crash_data.backtrace_count = count; } +#if SOC_CPU_CORES_NUM > 1 + // Capture the other core's backtrace from the global frame array. + // Both cores save their frames to g_exc_frames[] before esp_panic_handler + // is called, so the other core's frame is available here. + { + int other_core = 1 - info->core; + auto *other_frame = (XtExcFrame *) g_exc_frames[other_core]; + if (other_frame != nullptr) { + esp_backtrace_frame_t bt_frame = { + .pc = (uint32_t) other_frame->pc, + .sp = (uint32_t) other_frame->a1, + .next_pc = (uint32_t) other_frame->a0, + .exc_frame = other_frame, + }; + uint8_t count = 0; + uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(first_pc)) { + s_raw_crash_data.other_backtrace[count++] = first_pc; + } + while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) { + if (!esp_backtrace_get_next_frame(&bt_frame)) { + break; + } + uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(pc)) { + s_raw_crash_data.other_backtrace[count++] = pc; + } + } + s_raw_crash_data.other_backtrace_count = count; + } + } +#endif + #elif CONFIG_IDF_TARGET_ARCH_RISCV // RISC-V: capture MEPC + RA, then scan stack for code addresses if (info->frame != nullptr) { @@ -338,6 +425,32 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { } s_raw_crash_data.backtrace_count = count; } + +#if SOC_CPU_CORES_NUM > 1 + // Capture the other core's backtrace from the global frame array. + { + int other_core = 1 - info->core; + auto *other_frame = (RvExcFrame *) g_exc_frames[other_core]; + if (other_frame != nullptr) { + uint8_t count = 0; + if (is_code_addr(other_frame->mepc)) { + s_raw_crash_data.other_backtrace[count++] = other_frame->mepc; + } + if (is_code_addr(other_frame->ra) && other_frame->ra != other_frame->mepc) { + s_raw_crash_data.other_backtrace[count++] = other_frame->ra; + } + s_raw_crash_data.other_reg_frame_count = count; + auto *scan_start = (uint32_t *) other_frame->sp; + for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) { + uint32_t val = scan_start[i]; + if (is_code_addr(val) && val != other_frame->mepc && val != other_frame->ra) { + s_raw_crash_data.other_backtrace[count++] = val; + } + } + s_raw_crash_data.other_backtrace_count = count; + } + } +#endif #endif // Write version and magic last — ensures all data is written before we mark it valid From 4f06a913a8f55242c5b5bc3c3ff92aa4d77f4e08 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:58:51 -0400 Subject: [PATCH 02/15] Remove unnecessary comment --- esphome/components/esp32/crash_handler.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index c0fa284d2ed..baffdce11e9 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -81,7 +81,6 @@ struct RawCrashData { #if SOC_CPU_CORES_NUM > 1 uint8_t other_backtrace_count; uint8_t other_reg_frame_count; - // 1 byte implicit padding before uint32_t array uint32_t other_backtrace[MAX_BACKTRACE]; #endif }; From 2b07bebe8dfe6ed7d98aca6510d9b0b17e0e0df7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:00:43 -0400 Subject: [PATCH 03/15] Add bounds check on info->core and static_assert for dual-core --- esphome/components/esp32/crash_handler.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index baffdce11e9..6e0e59fea70 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -79,6 +79,7 @@ struct RawCrashData { uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) uint8_t crashed_core; #if SOC_CPU_CORES_NUM > 1 + static_assert(SOC_CPU_CORES_NUM == 2, "Dual-core logic assumes exactly 2 cores"); uint8_t other_backtrace_count; uint8_t other_reg_frame_count; uint32_t other_backtrace[MAX_BACKTRACE]; @@ -365,7 +366,7 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { // Capture the other core's backtrace from the global frame array. // Both cores save their frames to g_exc_frames[] before esp_panic_handler // is called, so the other core's frame is available here. - { + if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) { int other_core = 1 - info->core; auto *other_frame = (XtExcFrame *) g_exc_frames[other_core]; if (other_frame != nullptr) { @@ -427,7 +428,7 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { #if SOC_CPU_CORES_NUM > 1 // Capture the other core's backtrace from the global frame array. - { + if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) { int other_core = 1 - info->core; auto *other_frame = (RvExcFrame *) g_exc_frames[other_core]; if (other_frame != nullptr) { From e1aa92b9831a605c91997953eed3864e879ef943 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 08:13:37 -1000 Subject: [PATCH 04/15] [rotary_encoder] Fix templatable value type to use cg.int32 (#15567) --- esphome/components/rotary_encoder/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 246db023f45..21239863e45 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -129,6 +129,6 @@ async def to_code(config): async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, int) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.int32) cg.add(var.set_value(template_)) return var From b83edf6c175120fbc0319bf58cf9e1a836038c97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 08:57:56 -1000 Subject: [PATCH 05/15] [script] Resolve IncludeFile objects in component config merge (#15575) --- script/merge_component_configs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 41bbafcd021..df7ad4a28c9 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -288,6 +288,9 @@ def merge_component_configs( for pkg_name, pkg_value in list(packages_value.items()): if pkg_name in common_bus_packages: continue + # Resolve deferred !include files before checking type + if isinstance(pkg_value, yaml_util.IncludeFile): + pkg_value = pkg_value.load() if not isinstance(pkg_value, dict): continue # Component-specific package - expand its content into top level @@ -295,6 +298,9 @@ def merge_component_configs( elif isinstance(packages_value, list): # List format - expand all package includes for pkg_value in packages_value: + # Resolve deferred !include files before checking type + if isinstance(pkg_value, yaml_util.IncludeFile): + pkg_value = pkg_value.load() if not isinstance(pkg_value, dict): continue comp_data = merge_config(comp_data, pkg_value) From 869cace2f3ce753bbd1ddd0120aa0a31d838466c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 08:59:49 -1000 Subject: [PATCH 06/15] [web_server] Truncate update entity summary to 256 characters (#15570) --- esphome/components/web_server/web_server.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a57a8d26ffd..1daec1786d9 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2207,7 +2207,11 @@ json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, J if (start_config == DETAIL_ALL) { root[ESPHOME_F("current_version")] = obj->update_info.current_version; root[ESPHOME_F("title")] = obj->update_info.title; - root[ESPHOME_F("summary")] = obj->update_info.summary; + // Truncate long changelogs — full text available via release_url + constexpr size_t max_summary_len = 256; + root[ESPHOME_F("summary")] = obj->update_info.summary.size() <= max_summary_len + ? obj->update_info.summary + : obj->update_info.summary.substr(0, max_summary_len); root[ESPHOME_F("release_url")] = obj->update_info.release_url; this->add_sorting_info_(root, obj); } From a2bd83382b585de1b0e19647cfe712d485670191 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 09:00:59 -1000 Subject: [PATCH 07/15] [codegen] Fix templatable uint8 type to use cg.uint8 (#15572) --- esphome/components/ags10/sensor.py | 2 +- esphome/components/aic3204/audio_dac.py | 2 +- esphome/components/grove_tb6612fng/__init__.py | 8 ++++---- esphome/components/htu21d/sensor.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index e94504ff1af..6491d7d810d 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -97,7 +97,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( async def ags10newi2caddress_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - address = await cg.templatable(config[CONF_ADDRESS], args, int) + address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) cg.add(var.set_new_address(address)) return var diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py index a644638f696..b478b573a35 100644 --- a/esphome/components/aic3204/audio_dac.py +++ b/esphome/components/aic3204/audio_dac.py @@ -43,7 +43,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config.get(CONF_MODE), args, int) + template_ = await cg.templatable(config.get(CONF_MODE), args, cg.uint8) cg.add(var.set_auto_mute_mode(template_)) return var diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index 27a47953b34..ae64c049f53 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -78,7 +78,7 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16) cg.add(var.set_channel(template_channel)) cg.add(var.set_speed(template_speed)) @@ -101,7 +101,7 @@ async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) cg.add(var.set_channel(template_channel)) return var @@ -121,7 +121,7 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) cg.add(var.set_channel(template_channel)) return var @@ -175,6 +175,6 @@ async def grove_tb6612fng_change_address_to_code(config, action_id, template_arg var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_channel = await cg.templatable(config[CONF_ADDRESS], args, int) + template_channel = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) cg.add(var.set_address(template_channel)) return var diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index ed4fb5968a7..942a28475a3 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -98,7 +98,7 @@ async def to_code(config): async def set_heater_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - level_ = await cg.templatable(config[CONF_LEVEL], args, int) + level_ = await cg.templatable(config[CONF_LEVEL], args, cg.uint8) cg.add(var.set_level(level_)) return var From 063a8ce666d0cd79d145670a311efdd48aed838a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 09:03:25 -1000 Subject: [PATCH 08/15] [codegen] Fix templatable uint32 type to use cg.uint32 (#15574) --- esphome/components/pulse_counter/sensor.py | 2 +- esphome/components/pulse_meter/sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index c09d778eda2..3326745846b 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -160,6 +160,6 @@ async def to_code(config): async def set_total_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, int) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint32) cg.add(var.set_total_pulses(template_)) return var diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 499b7309c88..ab3dd2a2499 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -110,6 +110,6 @@ async def to_code(config): async def set_total_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, int) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint32) cg.add(var.set_total_pulses(template_)) return var From 0a42a11f1cc8b1c6334dc281604165f09846434b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 09:10:46 -1000 Subject: [PATCH 09/15] [at581x] Fix non-templated frequency/power_consumption constants for TemplatableFn (#15576) --- esphome/components/at581x/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 4923491f0c2..0bd26bbe5f8 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -173,10 +173,9 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_hw_frontend_reset(template_)) if freq := config.get(CONF_FREQUENCY): - if cg.is_template(freq): - template_ = await cg.templatable(freq, args, cg.int32) - else: - template_ = int(freq / 1000000) + if not cg.is_template(freq): + freq = int(freq / 1000000) + template_ = await cg.templatable(freq, args, cg.int_) cg.add(var.set_frequency(template_)) if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None: @@ -204,10 +203,9 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_stage_gain(template_)) if power := config.get(CONF_POWER_CONSUMPTION): - if cg.is_template(power): - template_ = await cg.templatable(power, args, cg.int32) - else: - template_ = int(power * 1000000) + if not cg.is_template(power): + power = int(power * 1000000) + template_ = await cg.templatable(power, args, cg.int_) cg.add(var.set_power_consumption(template_)) return var From cfa41b34677a682b7464de1607240c4b0aa74f52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 09:20:16 -1000 Subject: [PATCH 10/15] [codegen] Add cg.int8 type and fix templatable int8 types (#15573) --- esphome/codegen.py | 1 + esphome/components/at581x/__init__.py | 2 +- esphome/components/dfrobot_sen0395/__init__.py | 4 ++-- esphome/cpp_types.py | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 30e31353607..a5b5abe4479 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -79,6 +79,7 @@ from esphome.cpp_types import ( # noqa: F401 float_, global_ns, gpio_Flags, + int8, int16, int32, int64, diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 0bd26bbe5f8..acd2bcc608d 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -169,7 +169,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): # Radar configuration if frontend_reset := config.get(CONF_HW_FRONTEND_RESET): - template_ = await cg.templatable(frontend_reset, args, int) + template_ = await cg.templatable(frontend_reset, args, cg.int8) cg.add(var.set_hw_frontend_reset(template_)) if freq := config.get(CONF_FREQUENCY): diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index 0becaf35430..feb79eeacf6 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -159,7 +159,7 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args await cg.register_parented(var, config[CONF_ID]) if factory_reset_config := config.get(CONF_FACTORY_RESET): - template_ = await cg.templatable(factory_reset_config, args, int) + template_ = await cg.templatable(factory_reset_config, args, cg.int8) cg.add(var.set_factory_reset(template_)) if CONF_DETECTION_SEGMENTS in config: @@ -200,7 +200,7 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args template_ = template_.total_milliseconds / 1000 cg.add(var.set_delay_after_disappear(template_)) if CONF_SENSITIVITY in config: - template_ = await cg.templatable(config[CONF_SENSITIVITY], args, int) + template_ = await cg.templatable(config[CONF_SENSITIVITY], args, cg.int8) cg.add(var.set_sensitivity(template_)) return var diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 8dd77de8434..aeaa4480a8c 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -13,6 +13,7 @@ std_string = std_ns.class_("string") std_string_ref = std_ns.namespace("string &") std_vector = std_ns.class_("vector") std_span = std_ns.class_("span") +int8 = global_ns.namespace("int8_t") uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") From 12d1dedb59a7c73b988a91f4e6b81f21f88cff8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 09:20:41 -1000 Subject: [PATCH 11/15] minimize iram code duplication --- esphome/components/esp32/crash_handler.cpp | 228 +++++++++------------ 1 file changed, 97 insertions(+), 131 deletions(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index 6e0e59fea70..d847d17fd29 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -59,6 +59,59 @@ static inline bool is_return_addr(uint32_t addr) { } #endif +// --- Architecture-specific backtrace helpers --- +// These run from IRAM during panic (no flash access). + +#if CONFIG_IDF_TARGET_ARCH_XTENSA +// Walk Xtensa backtrace from an exception frame, writing PCs to out[]. +// Returns number of entries written. +static uint8_t IRAM_ATTR walk_xtensa_backtrace(XtExcFrame *frame, uint32_t *out, uint8_t max) { + esp_backtrace_frame_t bt_frame = { + .pc = (uint32_t) frame->pc, + .sp = (uint32_t) frame->a1, + .next_pc = (uint32_t) frame->a0, + .exc_frame = frame, + }; + uint8_t count = 0; + uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(first_pc)) { + out[count++] = first_pc; + } + while (count < max && bt_frame.next_pc != 0) { + if (!esp_backtrace_get_next_frame(&bt_frame)) + break; + uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(pc)) { + out[count++] = pc; + } + } + return count; +} +#endif + +#if CONFIG_IDF_TARGET_ARCH_RISCV +// Capture RISC-V backtrace: MEPC + RA from registers, then stack scan. +// Returns total count; *reg_count receives number of register-sourced entries. +static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *out, uint8_t max, uint8_t *reg_count) { + uint8_t count = 0; + if (is_code_addr(frame->mepc)) { + out[count++] = frame->mepc; + } + if (is_code_addr(frame->ra) && frame->ra != frame->mepc) { + out[count++] = frame->ra; + } + *reg_count = count; + auto *scan_start = (uint32_t *) frame->sp; + for (uint32_t i = 0; i < 64 && count < max; i++) { + uint32_t val = scan_start[i]; + if (is_code_addr(val) && val != frame->mepc && val != frame->ra) { + out[count++] = val; + } + } + return count; +} +#endif + // Raw crash data written by the panic handler wrapper. // Lives in .noinit so it survives software reset but contains garbage after power cycle. // Validated by magic marker. Static linkage since it's only used within this file. @@ -227,6 +280,36 @@ static const char *get_exception_type() { return "Unknown"; } +// Log backtrace entries, filtering stack-scanned addresses on RISC-V. +static void log_backtrace(const uint32_t *addrs, uint8_t count, uint8_t reg_frame_count) { + uint8_t bt_num = 0; + for (uint8_t i = 0; i < count; i++) { + uint32_t addr = addrs[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= reg_frame_count && !is_return_addr(addr)) + continue; + const char *source = (i < reg_frame_count) ? "backtrace" : "stack scan"; +#else + const char *source = "backtrace"; +#endif + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); + } +} + +// Append backtrace addresses to the addr2line hint buffer. +static int append_addrs_to_hint(char *buf, int size, int pos, const uint32_t *addrs, uint8_t count, + uint8_t reg_frame_count) { + for (uint8_t i = 0; i < count && pos < size - 12; i++) { + uint32_t addr = addrs[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= reg_frame_count && !is_return_addr(addr)) + continue; +#endif + pos += snprintf(buf + pos, size - pos, " 0x%08" PRIX32, addr); + } + return pos; +} + // Intentionally uses separate ESP_LOGE calls per line instead of combining into // one multi-line log message. This ensures each address appears as its own line // on the serial console, making it possible to see partial output if the device @@ -245,64 +328,25 @@ void crash_handler_log() { } ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core); ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc); - uint8_t bt_num = 0; - for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) { - uint32_t addr = s_raw_crash_data.backtrace[i]; -#if CONFIG_IDF_TARGET_ARCH_RISCV - // Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones. - if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) - continue; -#endif -#if CONFIG_IDF_TARGET_ARCH_RISCV - const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan"; -#else - const char *source = "backtrace"; -#endif - ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); - } + log_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count); #if SOC_CPU_CORES_NUM > 1 - // Log the other core's backtrace if captured if (s_raw_crash_data.other_backtrace_count > 0) { int other_core = 1 - s_raw_crash_data.crashed_core; ESP_LOGE(TAG, " Other core (%d) backtrace:", other_core); - bt_num = 0; - for (uint8_t i = 0; i < s_raw_crash_data.other_backtrace_count; i++) { - uint32_t addr = s_raw_crash_data.other_backtrace[i]; -#if CONFIG_IDF_TARGET_ARCH_RISCV - if (i >= s_raw_crash_data.other_reg_frame_count && !is_return_addr(addr)) - continue; -#endif -#if CONFIG_IDF_TARGET_ARCH_RISCV - const char *source = (i < s_raw_crash_data.other_reg_frame_count) ? "backtrace" : "stack scan"; -#else - const char *source = "backtrace"; -#endif - ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); - } + log_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count, + s_raw_crash_data.other_reg_frame_count); } #endif // Build addr2line hint with all captured addresses for easy copy-paste char hint[256]; int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc); - for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) { - uint32_t addr = s_raw_crash_data.backtrace[i]; -#if CONFIG_IDF_TARGET_ARCH_RISCV - if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) - continue; -#endif - pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); - } + pos = append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, + s_raw_crash_data.reg_frame_count); #if SOC_CPU_CORES_NUM > 1 - for (uint8_t i = 0; i < s_raw_crash_data.other_backtrace_count && pos < (int) sizeof(hint) - 12; i++) { - uint32_t addr = s_raw_crash_data.other_backtrace[i]; -#if CONFIG_IDF_TARGET_ARCH_RISCV - if (i >= s_raw_crash_data.other_reg_frame_count && !is_return_addr(addr)) - continue; -#endif - pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); - } + pos = append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace, + s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count); #endif ESP_LOGE(TAG, "%s", hint); } @@ -336,30 +380,7 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { if (info->frame != nullptr) { auto *xt_frame = (XtExcFrame *) info->frame; s_raw_crash_data.cause = xt_frame->exccause; - esp_backtrace_frame_t bt_frame = { - .pc = (uint32_t) xt_frame->pc, - .sp = (uint32_t) xt_frame->a1, - .next_pc = (uint32_t) xt_frame->a0, - .exc_frame = xt_frame, - }; - - uint8_t count = 0; - // First frame PC - uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); - if (is_code_addr(first_pc)) { - s_raw_crash_data.backtrace[count++] = first_pc; - } - // Walk remaining frames - while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) { - if (!esp_backtrace_get_next_frame(&bt_frame)) { - break; - } - uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); - if (is_code_addr(pc)) { - s_raw_crash_data.backtrace[count++] = pc; - } - } - s_raw_crash_data.backtrace_count = count; + s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE); } #if SOC_CPU_CORES_NUM > 1 @@ -370,27 +391,8 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { int other_core = 1 - info->core; auto *other_frame = (XtExcFrame *) g_exc_frames[other_core]; if (other_frame != nullptr) { - esp_backtrace_frame_t bt_frame = { - .pc = (uint32_t) other_frame->pc, - .sp = (uint32_t) other_frame->a1, - .next_pc = (uint32_t) other_frame->a0, - .exc_frame = other_frame, - }; - uint8_t count = 0; - uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); - if (is_code_addr(first_pc)) { - s_raw_crash_data.other_backtrace[count++] = first_pc; - } - while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) { - if (!esp_backtrace_get_next_frame(&bt_frame)) { - break; - } - uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); - if (is_code_addr(pc)) { - s_raw_crash_data.other_backtrace[count++] = pc; - } - } - s_raw_crash_data.other_backtrace_count = count; + s_raw_crash_data.other_backtrace_count = + walk_xtensa_backtrace(other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE); } } #endif @@ -400,30 +402,8 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { if (info->frame != nullptr) { auto *rv_frame = (RvExcFrame *) info->frame; s_raw_crash_data.cause = rv_frame->mcause; - uint8_t count = 0; - - // Save MEPC (fault PC) and RA (return address) - if (is_code_addr(rv_frame->mepc)) { - s_raw_crash_data.backtrace[count++] = rv_frame->mepc; - } - if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) { - s_raw_crash_data.backtrace[count++] = rv_frame->ra; - } - - // Track how many entries came from registers (MEPC/RA) so we can - // skip return-address validation for them at log time. - s_raw_crash_data.reg_frame_count = count; - - // Scan stack for code addresses — captures broadly during panic, - // filtered by is_return_addr() at log time when flash is accessible. - auto *scan_start = (uint32_t *) rv_frame->sp; - for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) { - uint32_t val = scan_start[i]; - if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) { - s_raw_crash_data.backtrace[count++] = val; - } - } - s_raw_crash_data.backtrace_count = count; + s_raw_crash_data.backtrace_count = + capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count); } #if SOC_CPU_CORES_NUM > 1 @@ -432,22 +412,8 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { int other_core = 1 - info->core; auto *other_frame = (RvExcFrame *) g_exc_frames[other_core]; if (other_frame != nullptr) { - uint8_t count = 0; - if (is_code_addr(other_frame->mepc)) { - s_raw_crash_data.other_backtrace[count++] = other_frame->mepc; - } - if (is_code_addr(other_frame->ra) && other_frame->ra != other_frame->mepc) { - s_raw_crash_data.other_backtrace[count++] = other_frame->ra; - } - s_raw_crash_data.other_reg_frame_count = count; - auto *scan_start = (uint32_t *) other_frame->sp; - for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) { - uint32_t val = scan_start[i]; - if (is_code_addr(val) && val != other_frame->mepc && val != other_frame->ra) { - s_raw_crash_data.other_backtrace[count++] = val; - } - } - s_raw_crash_data.other_backtrace_count = count; + s_raw_crash_data.other_backtrace_count = capture_riscv_backtrace( + other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE, &s_raw_crash_data.other_reg_frame_count); } } #endif From 7de060ed554bd0a03c3f0037d6e152a1ab2c176b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 9 Apr 2026 05:22:24 +1000 Subject: [PATCH 12/15] [lvgl] Fix args for lambda in set_rotation action (#15555) --- esphome/components/lvgl/automation.py | 6 +++--- esphome/components/lvgl/widgets/tabview.py | 3 ++- tests/components/lvgl/lvgl-package.yaml | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index b825320a407..977f1af9b49 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -2,6 +2,7 @@ from collections.abc import Callable from typing import Any from esphome import automation +from esphome.automation import StatelessLambdaAction import esphome.codegen as cg from esphome.components.display import validate_rotation import esphome.config_validation as cv @@ -201,7 +202,7 @@ def _validate_rotation(value): @automation.register_action( "lvgl.display.set_rotation", - ObjUpdateAction, + StatelessLambdaAction, cv.maybe_simple_value( LVGL_SCHEMA.extend( { @@ -214,8 +215,7 @@ def _validate_rotation(value): ) async def lvgl_set_rotation(config, action_id, template_arg, args): lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) - async with LambdaContext() as context: - add_line_marks(where=action_id) + async with LambdaContext(args, where=action_id) as context: lv_add(lv_comp.set_rotation(config[CONF_ROTATION])) return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 7629b03e9db..108bb38df58 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -2,6 +2,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_BUTTON, CONF_ID, CONF_INDEX, CONF_ITEMS, @@ -73,7 +74,7 @@ class TabviewType(WidgetType): ) def get_uses(self): - return CONF_BUTTONMATRIX, TYPE_FLEX + return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON async def to_code(self, w: Widget, config: dict): await w.set_property( diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 967fe515927..d3565c6c59a 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -194,6 +194,7 @@ lvgl: text: "Close" on_click: then: + - lvgl.display.set_rotation: 0 - lvgl.widget.hide: message_box - lvgl.style.update: id: style_test From 2ab7f032a8aee0ece47708f6bfb4d4d0f664a59a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 09:31:30 -1000 Subject: [PATCH 13/15] [esp32] Preserve crash data across OTA rollback reboots Don't clear the crash data magic marker at boot time. Previously, crash_handler_read_and_clear() would clear the magic immediately, so if OTA rollback triggered a reboot before an API client connected, the crash trace was lost. Now the magic is only cleared after crash_handler_log() delivers the data to an API client via crash_handler_clear(). --- esphome/components/api/api_connection.h | 1 + esphome/components/esp32/crash_handler.cpp | 9 +++++++-- esphome/components/esp32/crash_handler.h | 8 +++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 2f685b0b8a5..4be5a73e810 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -276,6 +276,7 @@ class APIConnection final : public APIServerConnectionBase { App.schedule_dump_config(); #ifdef USE_ESP32_CRASH_HANDLER esp32::crash_handler_log(); + esp32::crash_handler_clear(); #endif #ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_log(); diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index ecf30d78781..05be0e1408e 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -101,12 +101,17 @@ void crash_handler_read_and_clear() { if (s_raw_crash_data.pseudo_excause > 1) s_raw_crash_data.pseudo_excause = 0; } - // Clear magic regardless so we don't re-report on next normal reboot - s_raw_crash_data.magic = 0; + // Don't clear magic here — crash data must survive OTA rollback reboots. + // Magic is cleared by crash_handler_clear() after an API client receives the data. } bool crash_handler_has_data() { return s_crash_data_valid; } +void crash_handler_clear() { + s_raw_crash_data.magic = 0; + s_crash_data_valid = false; +} + // Look up the exception cause as a human-readable string. // Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays // not exposed via any public API. diff --git a/esphome/components/esp32/crash_handler.h b/esphome/components/esp32/crash_handler.h index 97a4d4e1162..c5e7d145ece 100644 --- a/esphome/components/esp32/crash_handler.h +++ b/esphome/components/esp32/crash_handler.h @@ -4,12 +4,18 @@ namespace esphome::esp32 { -/// Read crash data from NOINIT memory and clear the magic marker. +/// Read and validate crash data from NOINIT memory. +/// Does not clear the magic marker — call crash_handler_clear() after +/// the data has been delivered to an API client so it survives OTA rollback reboots. void crash_handler_read_and_clear(); /// Log crash data if a crash was detected on previous boot. void crash_handler_log(); +/// Clear the magic marker and mark crash data as consumed. +/// Call after the data has been delivered to an API client. +void crash_handler_clear(); + /// Returns true if crash data was found this boot. bool crash_handler_has_data(); From 0f72491a784c664b9d50fa4016164b04bde1ab24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 09:31:59 -1000 Subject: [PATCH 14/15] Keep s_crash_data_valid after clearing magic Don't reset s_crash_data_valid in crash_handler_clear() so that additional API clients connecting during the same boot session can still receive the crash log. --- esphome/components/esp32/crash_handler.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index 05be0e1408e..55cf92a703b 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -108,8 +108,10 @@ void crash_handler_read_and_clear() { bool crash_handler_has_data() { return s_crash_data_valid; } void crash_handler_clear() { + // Only clear the magic so data doesn't survive the next reboot. + // Keep s_crash_data_valid so crash_handler_log() still works for + // additional API clients connecting during this boot session. s_raw_crash_data.magic = 0; - s_crash_data_valid = false; } // Look up the exception cause as a human-readable string. From ffd58ac8d58f94812b88a0103536bf6467682416 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 09:33:06 -1000 Subject: [PATCH 15/15] dead store --- esphome/components/esp32/crash_handler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index d847d17fd29..4ac72b820ae 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -345,8 +345,8 @@ void crash_handler_log() { pos = append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count); #if SOC_CPU_CORES_NUM > 1 - pos = append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace, - s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count); + append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace, + s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count); #endif ESP_LOGE(TAG, "%s", hint); }