A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Debugging monitor script (#555)

## Summary

* **What is the goal of this PR?**
Add a debugging script to help developers monitor the ESP32 serial port
directly from a PC.

* **What changes are included?**
Added a new script: scripts/debugging_monitor.py

## Additional Context

While working on a new Crosspoint-Reader feature, it quickly became
clear that watching the ESP32 serial output without any visual cues was
inconvenient and easy to mess up.

This script improves the debugging experience by reading data from the
serial port and providing:

1. A timestamp prefix for every log line (instead of milliseconds since
power-up)
2. Color-coded output for different message types
3. A secondary window displaying a live graph of RAM usage, which is
especially useful for tracking the memory impact of new features

<img width="1916" height="1049" alt="Screenshot_20260126_183811"
src="https://github.com/user-attachments/assets/6291887f-ac17-43ac-9e43-f5dec8a7097e"
/>

---

### AI Usage

Did you use AI tools to help write this code? _**< PARTIALLY >**_
I wrote the initial version of the script. Gemini was used to help add
the Matplotlib-based graphing and threading logic.

authored by

Uri Tauber and committed by
GitHub
e5c0ddc9 b1dcb773

+228
+14
README.md
··· 95 95 ```sh 96 96 pio run --target upload 97 97 ``` 98 + ### Debugging 99 + 100 + After flashing the new features, it’s recommended to capture detailed logs from the serial port. 101 + 102 + First, make sure all required Python packages are installed: 103 + 104 + ```python 105 + python3 -m pip install serial colorama matplotlib 106 + ``` 107 + after that run the script: 108 + ```sh 109 + python3 scripts/debugging_monitor.py 110 + ``` 111 + This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS. 98 112 99 113 ## Internals 100 114
+214
scripts/debugging_monitor.py
··· 1 + import sys 2 + import argparse 3 + import re 4 + import threading 5 + from datetime import datetime 6 + from collections import deque 7 + import time 8 + 9 + # Try to import potentially missing packages 10 + try: 11 + import serial 12 + from colorama import init, Fore, Style 13 + import matplotlib.pyplot as plt 14 + import matplotlib.animation as animation 15 + except ImportError as e: 16 + missing_package = e.name 17 + print("\n" + "!" * 50) 18 + print(f" Error: The required package '{missing_package}' is not installed.") 19 + print("!" * 50) 20 + 21 + print(f"\nTo fix this, please run the following command in your terminal:\n") 22 + 23 + install_cmd = "pip install " 24 + packages = [] 25 + if 'serial' in str(e): packages.append("pyserial") 26 + if 'colorama' in str(e): packages.append("colorama") 27 + if 'matplotlib' in str(e): packages.append("matplotlib") 28 + 29 + print(f" {install_cmd}{' '.join(packages)}") 30 + 31 + print("\nExiting...") 32 + sys.exit(1) 33 + 34 + # --- Global Variables for Data Sharing --- 35 + # Store last 50 data points 36 + MAX_POINTS = 50 37 + time_data = deque(maxlen=MAX_POINTS) 38 + free_mem_data = deque(maxlen=MAX_POINTS) 39 + total_mem_data = deque(maxlen=MAX_POINTS) 40 + data_lock = threading.Lock() # Prevent reading while writing 41 + 42 + # Initialize colors 43 + init(autoreset=True) 44 + 45 + def get_color_for_line(line): 46 + """ 47 + Classify log lines by type and assign appropriate colors. 48 + """ 49 + line_upper = line.upper() 50 + 51 + if any(keyword in line_upper for keyword in ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"]): 52 + return Fore.RED 53 + if "[MEM]" in line_upper or "FREE:" in line_upper: 54 + return Fore.CYAN 55 + if any(keyword in line_upper for keyword in ["[GFX]", "[ERS]", "DISPLAY", "RAM WRITE", "RAM COMPLETE", "REFRESH", "POWERING ON", "FRAME BUFFER", "LUT"]): 56 + return Fore.MAGENTA 57 + if any(keyword in line_upper for keyword in ["[EBP]", "[BMC]", "[ZIP]", "[PARSER]", "[EHP]", "LOADING EPUB", "CACHE", "DECOMPRESSED", "PARSING"]): 58 + return Fore.GREEN 59 + if "[ACT]" in line_upper or "ENTERING ACTIVITY" in line_upper or "EXITING ACTIVITY" in line_upper: 60 + return Fore.YELLOW 61 + if any(keyword in line_upper for keyword in ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"]): 62 + return Fore.BLUE 63 + if any(keyword in line_upper for keyword in ["[CPS]", "SETTINGS", "[CLEAR_CACHE]"]): 64 + return Fore.LIGHTYELLOW_EX 65 + if any(keyword in line_upper for keyword in ["ESP-ROM", "BUILD:", "RST:", "BOOT:", "SPIWP:", "MODE:", "LOAD:", "ENTRY", "[SD]", "STARTING CROSSPOINT", "VERSION"]): 66 + return Fore.LIGHTBLACK_EX 67 + if "[RBS]" in line_upper: 68 + return Fore.LIGHTCYAN_EX 69 + if "[KRS]" in line_upper: 70 + return Fore.LIGHTMAGENTA_EX 71 + if any(keyword in line_upper for keyword in ["EINKDISPLAY:", "STATIC FRAME", "INITIALIZING", "SPI INITIALIZED", "GPIO PINS", "RESETTING", "SSD1677", "E-INK"]): 72 + return Fore.LIGHTMAGENTA_EX 73 + if any(keyword in line_upper for keyword in ["[FNS]", "FOOTNOTE"]): 74 + return Fore.LIGHTGREEN_EX 75 + if any(keyword in line_upper for keyword in ["[CHAP]", "[OPDS]", "[COF]"]): 76 + return Fore.LIGHTYELLOW_EX 77 + 78 + return Fore.WHITE 79 + 80 + def parse_memory_line(line): 81 + """ 82 + Extracts Free and Total bytes from the specific log line. 83 + Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes 84 + """ 85 + # Regex to find 'Free: <digits>' and 'Total: <digits>' 86 + match = re.search(r"Free:\s*(\d+).*Total:\s*(\d+)", line) 87 + if match: 88 + try: 89 + free_bytes = int(match.group(1)) 90 + total_bytes = int(match.group(2)) 91 + return free_bytes, total_bytes 92 + except ValueError: 93 + return None, None 94 + return None, None 95 + 96 + def serial_worker(port, baud): 97 + """ 98 + Runs in a background thread. Handles reading serial, printing to console, 99 + and updating the data lists. 100 + """ 101 + print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}") 102 + 103 + try: 104 + ser = serial.Serial(port, baud, timeout=0.1) 105 + ser.dtr = False 106 + ser.rts = False 107 + except serial.SerialException as e: 108 + print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}") 109 + return 110 + 111 + try: 112 + while True: 113 + try: 114 + raw_data = ser.readline().decode('utf-8', errors='replace') 115 + 116 + if not raw_data: 117 + continue 118 + 119 + clean_line = raw_data.strip() 120 + if not clean_line: 121 + continue 122 + 123 + # Add PC timestamp 124 + pc_time = datetime.now().strftime("%H:%M:%S") 125 + formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line) 126 + 127 + # Check for Memory Line 128 + if "[MEM]" in formatted_line: 129 + free_val, total_val = parse_memory_line(formatted_line) 130 + if free_val is not None: 131 + with data_lock: 132 + time_data.append(pc_time) 133 + free_mem_data.append(free_val / 1024) # Convert to KB 134 + total_mem_data.append(total_val / 1024) # Convert to KB 135 + 136 + # Print to console 137 + line_color = get_color_for_line(formatted_line) 138 + print(f"{line_color}{formatted_line}") 139 + 140 + except OSError: 141 + print(f"{Fore.RED}Device disconnected.{Style.RESET_ALL}") 142 + break 143 + except Exception as e: 144 + # If thread is killed violently (e.g. main exit), silence errors 145 + pass 146 + finally: 147 + if 'ser' in locals() and ser.is_open: 148 + ser.close() 149 + 150 + def update_graph(frame): 151 + """ 152 + Called by Matplotlib animation to redraw the chart. 153 + """ 154 + with data_lock: 155 + if not time_data: 156 + return 157 + 158 + # Convert deques to lists for plotting 159 + x = list(time_data) 160 + y_free = list(free_mem_data) 161 + y_total = list(total_mem_data) 162 + 163 + plt.cla() # Clear axis 164 + 165 + # Plot Total RAM 166 + plt.plot(x, y_total, label='Total RAM (KB)', color='red', linestyle='--') 167 + 168 + # Plot Free RAM 169 + plt.plot(x, y_free, label='Free RAM (KB)', color='green', marker='o') 170 + 171 + # Fill area under Free RAM 172 + plt.fill_between(x, y_free, color='green', alpha=0.1) 173 + 174 + plt.title("ESP32 Memory Monitor") 175 + plt.ylabel("Memory (KB)") 176 + plt.xlabel("Time") 177 + plt.legend(loc='upper left') 178 + plt.grid(True, linestyle=':', alpha=0.6) 179 + 180 + # Rotate date labels 181 + plt.xticks(rotation=45, ha='right') 182 + plt.tight_layout() 183 + 184 + def main(): 185 + parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph") 186 + parser.add_argument("port", nargs="?", default="/dev/ttyACM0", help="Serial port") 187 + parser.add_argument("--baud", type=int, default=115200, help="Baud rate") 188 + args = parser.parse_args() 189 + 190 + # 1. Start the Serial Reader in a separate thread 191 + # Daemon=True means this thread dies when the main program closes 192 + t = threading.Thread(target=serial_worker, args=(args.port, args.baud), daemon=True) 193 + t.start() 194 + 195 + # 2. Set up the Graph (Main Thread) 196 + try: 197 + plt.style.use('light_background') 198 + except: 199 + pass 200 + 201 + fig = plt.figure(figsize=(10, 6)) 202 + 203 + # Update graph every 1000ms 204 + ani = animation.FuncAnimation(fig, update_graph, interval=1000) 205 + 206 + try: 207 + print(f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}") 208 + plt.show() 209 + except KeyboardInterrupt: 210 + print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}") 211 + plt.close('all') # Force close any lingering plot windows 212 + 213 + if __name__ == "__main__": 214 + main()