From 920e43b28ccbfda7f7354aab01dfc50cf7a633c8 Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sat, 28 Feb 2026 23:44:52 +0100 Subject: [PATCH 01/10] exec decoder: add support for Stack memory style --- monitor/filter_exception_decoder.py | 81 +++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index 8a4f1cb60..f4cb688a7 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -45,10 +45,16 @@ class Esp32ExceptionDecoder(DeviceMonitorFilterBase): ADDR_PATTERN = re.compile(r"((?:0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8}(?: |$))+)") ADDR_SPLIT = re.compile(r"[ :]") PREFIX_RE = re.compile(r"^ *") + + # Pattern for stack memory dump lines: "3fca0000: 0x3fce0000 0x3fce0000 ..." + STACK_MEM_LINE = re.compile( + r"^[0-9a-fA-F]{8}:\s+((?:0x[0-9a-fA-F]{8}\s*)+)" + ) # Patterns that indicate we're in an exception/backtrace context BACKTRACE_KEYWORDS = re.compile( r"(Backtrace:|" + r"Stack memory:|" r"\bPC:\s*0x[0-9a-fA-F]{8}\b|" r"abort\(\) was called at PC|" r"Guru Meditation Error:|" @@ -362,14 +368,22 @@ def rx(self, text): if not self.should_process_line(line): continue + # Check for PC:SP pair backtrace format m = self.ADDR_PATTERN.search(line) - if m is None: + if m is not None: + trace = self.build_backtrace(line, m.group(1)) + if trace: + text = text[: idx + 1] + trace + text[idx + 1 :] + last += len(trace) continue - trace = self.build_backtrace(line, m.group(1)) - if trace: - text = text[: idx + 1] + trace + text[idx + 1 :] - last += len(trace) + # Check for stack memory dump format + m = self.STACK_MEM_LINE.search(line) + if m is not None: + trace = self.build_stack_trace(line, m.group(1)) + if trace: + text = text[: idx + 1] + trace + text[idx + 1 :] + last += len(trace) return text def is_address_ignored(self, address): @@ -435,6 +449,63 @@ def decode_address(self, addr, elf_path): except subprocess.CalledProcessError: return None + def build_stack_trace(self, line, addresses_str): + """ + Build a decoded trace from a stack memory dump line. + + Extracts individual addresses from the line and attempts to decode + each one. Only addresses that resolve to known symbols are shown. + + Args: + line: Original stack memory line + addresses_str: Matched portion containing space-separated addresses + + Returns: + str: Formatted decoded trace, or empty string if nothing decoded + """ + addresses = re.findall(r"0x[0-9a-fA-F]{8}", addresses_str) + if not addresses: + return "" + + prefix_match = self.PREFIX_RE.match(line) + prefix = prefix_match.group(0) if prefix_match is not None else "" + + trace = "" + try: + for addr in addresses: + if self.is_address_ignored(addr): + continue + + output = self.decode_address(addr, self.firmware_path) + is_rom = False + + if output is None and self.rom_elf_path: + output = self.decode_address(addr, self.rom_elf_path) + if output is not None: + is_rom = True + + if output is None: + continue + + output = self.strip_project_dir(output) + + if is_rom: + parts = output.split(" at ", 1) + if len(parts) == 2: + output = f"{parts[0]} in ROM" + else: + output = f"{output} in ROM" + + trace += "%s %s: %s\n" % (prefix, addr, output) + + except subprocess.CalledProcessError as e: + sys.stderr.write( + "%s: failed to call %s: %s\n" + % (self.__class__.__name__, self.addr2line_path, e) + ) + + return trace + def build_backtrace(self, line, address_match): """ Build a decoded backtrace from a line containing addresses. From 0d6c59bdc964004bcd1f7f78d7c5870553e8441d Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sat, 28 Feb 2026 23:52:41 +0100 Subject: [PATCH 02/10] =?UTF-8?q?Rabbit:=20Fixed=20=E2=80=94=20^\s*=20now?= =?UTF-8?q?=20allows=20optional=20leading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- monitor/filter_exception_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index f4cb688a7..8b0515839 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -48,7 +48,7 @@ class Esp32ExceptionDecoder(DeviceMonitorFilterBase): # Pattern for stack memory dump lines: "3fca0000: 0x3fce0000 0x3fce0000 ..." STACK_MEM_LINE = re.compile( - r"^[0-9a-fA-F]{8}:\s+((?:0x[0-9a-fA-F]{8}\s*)+)" + r"^\s*[0-9a-fA-F]{8}:\s+((?:0x[0-9a-fA-F]{8}\s*)+)" ) # Patterns that indicate we're in an exception/backtrace context From 879f3652a59240af8146aacadc51e1606f26c148 Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sun, 1 Mar 2026 00:15:10 +0100 Subject: [PATCH 03/10] try to fix not working decoding --- monitor/filter_exception_decoder.py | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index 8b0515839..ba2b930d4 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -50,6 +50,11 @@ class Esp32ExceptionDecoder(DeviceMonitorFilterBase): STACK_MEM_LINE = re.compile( r"^\s*[0-9a-fA-F]{8}:\s+((?:0x[0-9a-fA-F]{8}\s*)+)" ) + + # Pattern for RISC-V register dump entries: "MEPC : 0x00000000" + REGISTER_ENTRY = re.compile( + r"([A-Z][A-Z0-9/]+)\s*:\s*(0x[0-9a-fA-F]{8})" + ) # Patterns that indicate we're in an exception/backtrace context BACKTRACE_KEYWORDS = re.compile( @@ -384,6 +389,15 @@ def rx(self, text): if trace: text = text[: idx + 1] + trace + text[idx + 1 :] last += len(trace) + continue + + # Check for RISC-V register dump lines + reg_matches = self.REGISTER_ENTRY.findall(line) + if len(reg_matches) >= 2: + trace = self.build_register_trace(line, reg_matches) + if trace: + text = text[: idx + 1] + trace + text[idx + 1 :] + last += len(trace) return text def is_address_ignored(self, address): @@ -449,6 +463,59 @@ def decode_address(self, addr, elf_path): except subprocess.CalledProcessError: return None + def build_register_trace(self, line, reg_matches): + """ + Build a decoded trace from a RISC-V register dump line. + + Tries to decode each register value; only registers whose addresses + resolve to known symbols are shown. + + Args: + line: Original register dump line + reg_matches: List of (register_name, address) tuples + + Returns: + str: Formatted decoded trace, or empty string if nothing decoded + """ + prefix_match = self.PREFIX_RE.match(line) + prefix = prefix_match.group(0) if prefix_match is not None else "" + + trace = "" + try: + for reg_name, addr in reg_matches: + if self.is_address_ignored(addr): + continue + + output = self.decode_address(addr, self.firmware_path) + is_rom = False + + if output is None and self.rom_elf_path: + output = self.decode_address(addr, self.rom_elf_path) + if output is not None: + is_rom = True + + if output is None: + continue + + output = self.strip_project_dir(output) + + if is_rom: + parts = output.split(" at ", 1) + if len(parts) == 2: + output = f"{parts[0]} in ROM" + else: + output = f"{output} in ROM" + + trace += "%s %s: %s: %s\n" % (prefix, reg_name, addr, output) + + except subprocess.CalledProcessError as e: + sys.stderr.write( + "%s: failed to call %s: %s\n" + % (self.__class__.__name__, self.addr2line_path, e) + ) + + return trace + def build_stack_trace(self, line, addresses_str): """ Build a decoded trace from a stack memory dump line. From df78410df9d035db52cd0a1304c5c9e0e07ab7bb Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sun, 1 Mar 2026 00:18:25 +0100 Subject: [PATCH 04/10] fix: no repeated subprocess spawns for the same address --- monitor/filter_exception_decoder.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index ba2b930d4..07fc0ff1f 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -104,6 +104,7 @@ def __call__(self): self.firmware_path = None self.addr2line_path = None self.rom_elf_path = None + self._addr_cache = {} self.enabled = self.setup_paths() if self.config.get("env:" + self.environment, "build_type") != "debug": @@ -441,6 +442,10 @@ def decode_address(self, addr, elf_path): Returns: str: Decoded function and location, or None if decoding failed """ + cache_key = (addr, elf_path) + if cache_key in self._addr_cache: + return self._addr_cache[cache_key] + enc = "mbcs" if IS_WINDOWS else "utf-8" args = [self.addr2line_path, u"-fipC", u"-e", elf_path, addr] @@ -456,11 +461,14 @@ def decode_address(self, addr, elf_path): # Check if address was found in ELF (handle common variants) if output in ("?? ??:0", "??:0") or output.strip().startswith("?? ") or output.strip() == "??": + self._addr_cache[cache_key] = None return None + self._addr_cache[cache_key] = output return output except subprocess.CalledProcessError: + self._addr_cache[cache_key] = None return None def build_register_trace(self, line, reg_matches): From ee9eb3f36c8ca00edc27cc6db6db5b831bde7205 Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sun, 1 Mar 2026 00:23:43 +0100 Subject: [PATCH 05/10] Duplicated decode-and-format logic / remove dead code --- monitor/filter_exception_decoder.py | 138 +++++++++------------------- 1 file changed, 42 insertions(+), 96 deletions(-) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index 07fc0ff1f..a02a3fed3 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -471,6 +471,38 @@ def decode_address(self, addr, elf_path): self._addr_cache[cache_key] = None return None + def _resolve_address(self, addr): + """ + Resolve a single address through firmware and ROM ELFs. + + Returns: + tuple: (decoded_output, is_rom) or (None, False) if unresolved + """ + if self.is_address_ignored(addr): + return None, False + + output = self.decode_address(addr, self.firmware_path) + is_rom = False + + if output is None and self.rom_elf_path: + output = self.decode_address(addr, self.rom_elf_path) + if output is not None: + is_rom = True + + if output is None: + return None, False + + output = self.strip_project_dir(output) + + if is_rom: + parts = output.split(" at ", 1) + if len(parts) == 2: + output = f"{parts[0]} in ROM" + else: + output = f"{output} in ROM" + + return output, is_rom + def build_register_trace(self, line, reg_matches): """ Build a decoded trace from a RISC-V register dump line. @@ -489,39 +521,11 @@ def build_register_trace(self, line, reg_matches): prefix = prefix_match.group(0) if prefix_match is not None else "" trace = "" - try: - for reg_name, addr in reg_matches: - if self.is_address_ignored(addr): - continue - - output = self.decode_address(addr, self.firmware_path) - is_rom = False - - if output is None and self.rom_elf_path: - output = self.decode_address(addr, self.rom_elf_path) - if output is not None: - is_rom = True - - if output is None: - continue - - output = self.strip_project_dir(output) - - if is_rom: - parts = output.split(" at ", 1) - if len(parts) == 2: - output = f"{parts[0]} in ROM" - else: - output = f"{output} in ROM" - + for reg_name, addr in reg_matches: + output, _ = self._resolve_address(addr) + if output is not None: trace += "%s %s: %s: %s\n" % (prefix, reg_name, addr, output) - except subprocess.CalledProcessError as e: - sys.stderr.write( - "%s: failed to call %s: %s\n" - % (self.__class__.__name__, self.addr2line_path, e) - ) - return trace def build_stack_trace(self, line, addresses_str): @@ -546,39 +550,11 @@ def build_stack_trace(self, line, addresses_str): prefix = prefix_match.group(0) if prefix_match is not None else "" trace = "" - try: - for addr in addresses: - if self.is_address_ignored(addr): - continue - - output = self.decode_address(addr, self.firmware_path) - is_rom = False - - if output is None and self.rom_elf_path: - output = self.decode_address(addr, self.rom_elf_path) - if output is not None: - is_rom = True - - if output is None: - continue - - output = self.strip_project_dir(output) - - if is_rom: - parts = output.split(" at ", 1) - if len(parts) == 2: - output = f"{parts[0]} in ROM" - else: - output = f"{output} in ROM" - + for addr in addresses: + output, _ = self._resolve_address(addr) + if output is not None: trace += "%s %s: %s\n" % (prefix, addr, output) - except subprocess.CalledProcessError as e: - sys.stderr.write( - "%s: failed to call %s: %s\n" - % (self.__class__.__name__, self.addr2line_path, e) - ) - return trace def build_backtrace(self, line, address_match): @@ -604,42 +580,12 @@ def build_backtrace(self, line, address_match): prefix = prefix_match.group(0) if prefix_match is not None else "" trace = "" - try: - i = 0 - for addr in addresses: - # First try to decode with application ELF - output = self.decode_address(addr, self.firmware_path) - is_rom = False - - # If not found in app ELF, try ROM ELF - if output is None and self.rom_elf_path: - output = self.decode_address(addr, self.rom_elf_path) - if output is not None: - is_rom = True - - # Skip if address couldn't be decoded - if output is None: - continue - - output = self.strip_project_dir(output) - - # Add "in ROM" suffix for ROM addresses - if is_rom: - # Extract function name (first part before "at") - parts = output.split(" at ", 1) - if len(parts) == 2: - output = f"{parts[0]} in ROM" - else: - output = f"{output} in ROM" - + i = 0 + for addr in addresses: + output, _ = self._resolve_address(addr) + if output is not None: trace += "%s #%-2d %s in %s\n" % (prefix, i, addr, output) i += 1 - - except subprocess.CalledProcessError as e: - sys.stderr.write( - "%s: failed to call %s: %s\n" - % (self.__class__.__name__, self.addr2line_path, e) - ) return trace + "\n" if trace else "" From 59e163dc2e325bdc2b9d55d4c38bdfd822b9e123 Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sun, 1 Mar 2026 00:32:03 +0100 Subject: [PATCH 06/10] Double "in" for ROM addresses in backtrace output --- monitor/filter_exception_decoder.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index a02a3fed3..b0343f5eb 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -582,9 +582,10 @@ def build_backtrace(self, line, address_match): trace = "" i = 0 for addr in addresses: - output, _ = self._resolve_address(addr) + output, is_rom = self._resolve_address(addr) if output is not None: - trace += "%s #%-2d %s in %s\n" % (prefix, i, addr, output) + fmt = "%s #%-2d %s %s\n" if is_rom else "%s #%-2d %s in %s\n" + trace += fmt % (prefix, i, addr, output) i += 1 return trace + "\n" if trace else "" From 5f45888fa4126fa09a0f7eb52c4600901eccb92a Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sun, 1 Mar 2026 01:01:06 +0100 Subject: [PATCH 07/10] Fix: hopefully one off --- monitor/filter_exception_decoder.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index b0343f5eb..5828eac63 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -471,21 +471,30 @@ def decode_address(self, addr, elf_path): self._addr_cache[cache_key] = None return None - def _resolve_address(self, addr): + def _resolve_address(self, addr, is_return_addr=False): """ Resolve a single address through firmware and ROM ELFs. + Args: + addr: Address string (e.g., "0x420022e4") + is_return_addr: If True, subtract 1 before lookup so addr2line + reports the call site rather than the instruction after it + Returns: tuple: (decoded_output, is_rom) or (None, False) if unresolved """ if self.is_address_ignored(addr): return None, False - output = self.decode_address(addr, self.firmware_path) + lookup = addr + if is_return_addr: + lookup = "0x%08x" % (int(addr, 16) - 1) + + output = self.decode_address(lookup, self.firmware_path) is_rom = False if output is None and self.rom_elf_path: - output = self.decode_address(addr, self.rom_elf_path) + output = self.decode_address(lookup, self.rom_elf_path) if output is not None: is_rom = True @@ -522,7 +531,7 @@ def build_register_trace(self, line, reg_matches): trace = "" for reg_name, addr in reg_matches: - output, _ = self._resolve_address(addr) + output, _ = self._resolve_address(addr, is_return_addr=(reg_name != "MEPC")) if output is not None: trace += "%s %s: %s: %s\n" % (prefix, reg_name, addr, output) @@ -551,7 +560,7 @@ def build_stack_trace(self, line, addresses_str): trace = "" for addr in addresses: - output, _ = self._resolve_address(addr) + output, _ = self._resolve_address(addr, is_return_addr=True) if output is not None: trace += "%s %s: %s\n" % (prefix, addr, output) @@ -581,8 +590,8 @@ def build_backtrace(self, line, address_match): trace = "" i = 0 - for addr in addresses: - output, is_rom = self._resolve_address(addr) + for j, addr in enumerate(addresses): + output, is_rom = self._resolve_address(addr, is_return_addr=(j > 0)) if output is not None: fmt = "%s #%-2d %s %s\n" if is_rom else "%s #%-2d %s in %s\n" trace += fmt % (prefix, i, addr, output) From 1d0e6e5a95dfa93a41f348667c7fe3d61a1804e7 Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sun, 1 Mar 2026 01:08:29 +0100 Subject: [PATCH 08/10] reg_name == "RA" --- monitor/filter_exception_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index 5828eac63..cb0a48f98 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -531,7 +531,7 @@ def build_register_trace(self, line, reg_matches): trace = "" for reg_name, addr in reg_matches: - output, _ = self._resolve_address(addr, is_return_addr=(reg_name != "MEPC")) + output, _ = self._resolve_address(addr, is_return_addr=(reg_name == "RA")) if output is not None: trace += "%s %s: %s: %s\n" % (prefix, reg_name, addr, output) From 96b66d6863627b48d48da1d5c3b4e7b81e68d9e3 Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sun, 1 Mar 2026 13:35:41 +0100 Subject: [PATCH 09/10] add Xtensa exception causes --- monitor/filter_exception_decoder.py | 133 ++++++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 5 deletions(-) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index cb0a48f98..1a31298ef 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -69,8 +69,10 @@ class Esp32ExceptionDecoder(DeviceMonitorFilterBase): r"CORRUPT HEAP:|" r"assertion .* failed:|" r"Debug exception reason:|" - r"Undefined behavior of type)", - re.IGNORECASE + r"Undefined behavior of type|" + r"^Exception\s+\(\d+\):|" + r"ELF file SHA256:)", + re.IGNORECASE | re.MULTILINE ) # Chip name mapping for ROM ELF files @@ -83,9 +85,76 @@ class Esp32ExceptionDecoder(DeviceMonitorFilterBase): "esp32c5": "esp32c5", "esp32c6": "esp32c6", "esp32h2": "esp32h2", + "esp32h4": "esp32h4", "esp32p4": "esp32p4", } + # Xtensa exception causes (EXCCAUSE register values) + # From Xtensa ISA Reference Manual / ESP-IDF EspExceptionDecoder + XTENSA_EXCEPTIONS = [ + "IllegalInstruction", # 0 + "Syscall", # 1 + "InstructionFetchError", # 2 + "LoadStoreError", # 3 + "Level1Interrupt", # 4 + "Alloca", # 5 + "IntegerDivideByZero", # 6 + "reserved", # 7 + "Privileged", # 8 + "LoadStoreAlignment", # 9 + "reserved", # 10 + "reserved", # 11 + "InstrPIFDataError", # 12 + "LoadStorePIFDataError", # 13 + "InstrPIFAddrError", # 14 + "LoadStorePIFAddrError", # 15 + "InstTLBMiss", # 16 + "InstTLBMultiHit", # 17 + "InstFetchPrivilege", # 18 + "reserved", # 19 + "InstFetchProhibited", # 20 + "reserved", # 21 + "reserved", # 22 + "reserved", # 23 + "LoadStoreTLBMiss", # 24 + "LoadStoreTLBMultiHit", # 25 + "LoadStorePrivilege", # 26 + "reserved", # 27 + "LoadProhibited", # 28 + "StoreProhibited", # 29 + ] + + # RISC-V exception causes (MCAUSE register values) + RISCV_EXCEPTIONS = { + 0x0: "Instruction address misaligned", + 0x1: "Instruction access fault", + 0x2: "Illegal instruction", + 0x3: "Breakpoint", + 0x4: "Load address misaligned", + 0x5: "Load access fault", + 0x6: "Store/AMO address misaligned", + 0x7: "Store/AMO access fault", + 0x8: "Environment call from U-mode", + 0x9: "Environment call from S-mode", + 0xb: "Environment call from M-mode", + 0xc: "Instruction page fault", + 0xd: "Load page fault", + 0xf: "Store/AMO page fault", + } + + # Registers containing exception metadata, not code addresses + NON_CODE_REGISTERS = { + "EXCVADDR", # Xtensa fault address + "MTVAL", # RISC-V fault address + "MSTATUS", "MHARTID", # RISC-V status registers + "PS", # Xtensa processor status + "SAR", # Xtensa shift amount register + "LBEG", "LEND", "LCOUNT", # Xtensa loop registers + } + + # Pattern to detect device reboot (terminates exception context) + REBOOT_RE = re.compile(r"^Rebooting\.\.\.", re.IGNORECASE) + def __call__(self): """ Initialize the filter instance. @@ -308,6 +377,34 @@ def is_backtrace_context(self, line): """ return self.BACKTRACE_KEYWORDS.search(line) is not None + def get_xtensa_exception(self, code): + """ + Look up Xtensa exception description by EXCCAUSE code. + + Args: + code: Integer EXCCAUSE value + + Returns: + str: Exception description, or None if code is unknown + """ + if 0 <= code < len(self.XTENSA_EXCEPTIONS): + desc = self.XTENSA_EXCEPTIONS[code] + if desc != "reserved": + return desc + return None + + def get_riscv_exception(self, code): + """ + Look up RISC-V exception description by MCAUSE code. + + Args: + code: Integer MCAUSE value + + Returns: + str: Exception description, or None if code is unknown + """ + return self.RISCV_EXCEPTIONS.get(code) + def should_process_line(self, line): """ Determine if a line should be processed for address decoding. @@ -321,6 +418,11 @@ def should_process_line(self, line): Returns: bool: True if line should be processed for address decoding """ + # Rebooting... terminates the exception context + if self.REBOOT_RE.match(line): + self.in_backtrace_context = False + return False + # Check if this line starts a backtrace context if self.is_backtrace_context(line): self.in_backtrace_context = True @@ -514,10 +616,11 @@ def _resolve_address(self, addr, is_return_addr=False): def build_register_trace(self, line, reg_matches): """ - Build a decoded trace from a RISC-V register dump line. + Build a decoded trace from a register dump line. - Tries to decode each register value; only registers whose addresses - resolve to known symbols are shown. + Annotates exception cause registers (EXCCAUSE/MCAUSE) with + human-readable descriptions. Tries to decode code-address registers + (PC, MEPC, RA, etc.) via addr2line. Skips non-code registers. Args: line: Original register dump line @@ -531,6 +634,26 @@ def build_register_trace(self, line, reg_matches): trace = "" for reg_name, addr in reg_matches: + # Annotate Xtensa exception cause with description + if reg_name == "EXCCAUSE": + code = int(addr, 16) + desc = self.get_xtensa_exception(code) + if desc: + trace += "%s %s: %s (%s)\n" % (prefix, reg_name, addr, desc) + continue + + # Annotate RISC-V exception cause with description + if reg_name == "MCAUSE": + code = int(addr, 16) + desc = self.get_riscv_exception(code) + if desc: + trace += "%s %s: %s (%s)\n" % (prefix, reg_name, addr, desc) + continue + + # Skip registers that don't contain code addresses + if reg_name in self.NON_CODE_REGISTERS: + continue + output, _ = self._resolve_address(addr, is_return_addr=(reg_name == "RA")) if output is not None: trace += "%s %s: %s: %s\n" % (prefix, reg_name, addr, output) From 13403aada879518152c31b7fc2e1a785b2cc42c8 Mon Sep 17 00:00:00 2001 From: Jason2866 Date: Sun, 1 Mar 2026 13:43:57 +0100 Subject: [PATCH 10/10] Immutable types --- monitor/filter_exception_decoder.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py index 1a31298ef..95c2beffc 100644 --- a/monitor/filter_exception_decoder.py +++ b/monitor/filter_exception_decoder.py @@ -17,6 +17,7 @@ import subprocess import sys import glob +import types from platformio.compat import IS_WINDOWS from platformio.exception import PlatformioException @@ -91,7 +92,7 @@ class Esp32ExceptionDecoder(DeviceMonitorFilterBase): # Xtensa exception causes (EXCCAUSE register values) # From Xtensa ISA Reference Manual / ESP-IDF EspExceptionDecoder - XTENSA_EXCEPTIONS = [ + XTENSA_EXCEPTIONS = ( "IllegalInstruction", # 0 "Syscall", # 1 "InstructionFetchError", # 2 @@ -122,10 +123,10 @@ class Esp32ExceptionDecoder(DeviceMonitorFilterBase): "reserved", # 27 "LoadProhibited", # 28 "StoreProhibited", # 29 - ] + ) # RISC-V exception causes (MCAUSE register values) - RISCV_EXCEPTIONS = { + RISCV_EXCEPTIONS = types.MappingProxyType({ 0x0: "Instruction address misaligned", 0x1: "Instruction access fault", 0x2: "Illegal instruction", @@ -140,20 +141,20 @@ class Esp32ExceptionDecoder(DeviceMonitorFilterBase): 0xc: "Instruction page fault", 0xd: "Load page fault", 0xf: "Store/AMO page fault", - } + }) # Registers containing exception metadata, not code addresses - NON_CODE_REGISTERS = { + NON_CODE_REGISTERS = frozenset({ "EXCVADDR", # Xtensa fault address "MTVAL", # RISC-V fault address "MSTATUS", "MHARTID", # RISC-V status registers "PS", # Xtensa processor status "SAR", # Xtensa shift amount register "LBEG", "LEND", "LCOUNT", # Xtensa loop registers - } + }) # Pattern to detect device reboot (terminates exception context) - REBOOT_RE = re.compile(r"^Rebooting\.\.\.", re.IGNORECASE) + REBOOT_RE = re.compile(r"^\s*Rebooting\.\.\.", re.IGNORECASE) def __call__(self): """