diff --git a/scripts/boot_baud_probe.py b/scripts/boot_baud_probe.py new file mode 100644 index 0000000..a2c8695 --- /dev/null +++ b/scripts/boot_baud_probe.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Probe G2 bootloader at different baud rates and try interrupt sequences. + +The bootloader phase is <50ms. This script: +1. Shows the raw hex of the garbage bytes after "Application is running..." +2. Tries rebooting at different baud rates to decode the bootloader's native rate +3. Tries sending interrupt sequences during the bootloader window + +Usage: + uv run scripts/boot_baud_probe.py +""" + +import time + +import serial + +PORT = "/dev/ttyUSB2" +APP_BAUD = 115200 + + +def reboot_and_capture( + port: str, + capture_baud: int, + duration: float = 5.0, + pre_interrupt: bytes | None = None, + interrupt_delay: float = 0.01, +) -> bytes: + """Reboot at app baud, optionally switch to capture_baud, read everything.""" + + # Open at application baud rate to send reboot command + ser = serial.Serial(port, APP_BAUD, timeout=0.5) + ser.reset_input_buffer() + + # Make sure we're at a prompt — send CR + ser.write(b"\r") + time.sleep(0.3) + resp = ser.read(4096).decode("utf-8", errors="replace") + + if "OS>" not in resp: + if "TRK>" in resp: + ser.write(b"os\r") + time.sleep(0.3) + ser.read(4096) + else: + ser.write(b"q\r") + time.sleep(0.3) + ser.read(4096) + ser.write(b"os\r") + time.sleep(0.3) + ser.read(4096) + + ser.reset_input_buffer() + ser.write(b"reboot\r") + + # If we need to switch baud rate, do it immediately + if capture_baud != APP_BAUD: + time.sleep(0.01) # tiny delay for reboot cmd to be sent + ser.baudrate = capture_baud + + # Send interrupt if requested + if pre_interrupt: + time.sleep(interrupt_delay) + if pre_interrupt == b"BREAK": + ser.send_break(duration=0.25) + else: + ser.write(pre_interrupt) + + # Capture everything + buf = bytearray() + start = time.monotonic() + ser.timeout = 0.05 + while time.monotonic() - start < duration: + data = ser.read(4096) + if data: + buf.extend(data) + + ser.close() + return bytes(buf) + + +def hex_dump(data: bytes, prefix: str = " ") -> None: + """Print hex dump with ASCII sidebar.""" + for i in range(0, len(data), 16): + chunk = data[i : i + 16] + hex_part = " ".join(f"{b:02x}" for b in chunk) + ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk) + print(f"{prefix}{i:04x}: {hex_part:<48s} {ascii_part}") + + +def main(): + print("=" * 70) + print("G2 BOOTLOADER BAUD RATE PROBE") + print("=" * 70) + + # --- Phase 1: Capture raw boot at 115200 and show hex --- + print("\n--- Phase 1: Raw boot capture at 115200 (hex dump) ---\n") + data = reboot_and_capture(PORT, APP_BAUD, duration=2.0) + print(f"Captured {len(data)} bytes:\n") + hex_dump(data) + + # Find the garbage bytes between "running..." and "Application Starting" + idx1 = data.find(b"running...") + idx2 = data.find(b"Application Starting") + if idx1 >= 0 and idx2 >= 0: + between = data[idx1 + len(b"running...") : idx2] + print("\nBytes between 'running...' and 'Application Starting':") + print(f" Raw: {between!r}") + print(f" Hex: {between.hex(' ')}") + print(f" Len: {len(between)}") + + # Wait for boot to complete + print("\n Waiting for boot to complete...") + time.sleep(12) + + # --- Phase 2: Try different baud rates during bootloader --- + test_bauds = [9600, 19200, 38400, 57600, 115200, 230400, 460800] + + print("\n--- Phase 2: Bootloader at different baud rates ---\n") + for baud in test_bauds: + print(f"\n Trying {baud} baud...") + data = reboot_and_capture(PORT, baud, duration=2.0) + + # Filter to just the first ~200 bytes (bootloader phase) + preview = data[:200] + # Check if it looks like readable ASCII + ascii_count = sum(1 for b in preview if 32 <= b < 127 or b in (10, 13)) + ratio = ascii_count / max(len(preview), 1) + + print(f" Captured {len(data)} bytes, ASCII ratio: {ratio:.0%}") + if ratio > 0.5: + text = preview.decode("utf-8", errors="replace") + print(f" Preview: {text[:120]!r}") + else: + if preview: + print(f" Hex: {preview[:32].hex(' ')}") + + # Wait for boot + time.sleep(12) + + # --- Phase 3: Try interrupt sequences at 115200 --- + print("\n--- Phase 3: Interrupt sequences at 115200 ---\n") + + interrupts = [ + (0.005, "5ms CR", b"\r\r\r\r\r"), + (0.005, "5ms ESC", b"\x1b\x1b\x1b"), + (0.005, "5ms BREAK", b"BREAK"), + (0.005, "5ms 0x55 autobaud", b"\x55\x55\x55\x55\x55"), + (0.005, "5ms 0x7F (DEL)", b"\x7f\x7f\x7f"), + (0.005, "5ms 'bl'", b"bl\r"), + (0.01, "10ms CR burst", b"\r" * 20), + (0.02, "20ms '?' burst", b"?\r?\r?\r"), + (0.03, "30ms CR", b"\r\r\r"), + ] + + for delay, desc, payload in interrupts: + print(f"\n [{desc}] (delay={delay}s)") + data = reboot_and_capture( + PORT, APP_BAUD, duration=3.0, pre_interrupt=payload, interrupt_delay=delay + ) + + text = data.decode("utf-8", errors="replace") + + # Look for anything unusual (not normal boot) + normal_markers = ["Application Starting", "MotorInit", "BCM4515"] + unusual = False + + # Check if we got a response BEFORE normal boot messages + bl_idx = text.find("Bootloader version") + app_idx = text.find("Application Starting") + + if bl_idx >= 0 and app_idx >= 0: + between = text[bl_idx:app_idx] + if len(between) > 80: # more than just "Bootloader version: 1.01\r\n..." + unusual = True + print(" *** EXTRA DATA in bootloader phase! ***") + print(f" Between BL and App: {between!r}") + + if "BL>" in text or "CMD>" in text or "download" in text.lower(): + unusual = True + print(" *** BOOTLOADER PROMPT DETECTED ***") + + if not unusual: + # Check if boot proceeded normally + if any(m in text for m in normal_markers): + print(" Normal boot (no intercept)") + else: + print(f" Unexpected: {text[:150]!r}") + + time.sleep(12) + + print("\n" + "=" * 70) + print("PROBE COMPLETE") + print("=" * 70) + + +if __name__ == "__main__": + main() diff --git a/scripts/boot_capture.py b/scripts/boot_capture.py new file mode 100644 index 0000000..0a72703 --- /dev/null +++ b/scripts/boot_capture.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""Reboot G2 firmware and capture boot output with timestamps. + +Pass 1: Observe boot sequence timing. +Pass 2: Try interrupt sequences during bootloader phase. + +Usage: + uv run scripts/boot_capture.py [--interrupt] +""" + +import argparse +import time + +import serial + +PORT = "/dev/ttyUSB2" +BAUD = 115200 +BOOT_CAPTURE_SECS = 30 # how long to capture after reboot + + +def timestamp() -> str: + return f"{time.monotonic():.4f}" + + +def capture_boot(ser: serial.Serial, duration: float) -> list[tuple[float, bytes]]: + """Read all data for `duration` seconds, returning (time, data) pairs.""" + chunks = [] + start = time.monotonic() + ser.timeout = 0.05 # 50ms polling + while time.monotonic() - start < duration: + data = ser.read(4096) + if data: + chunks.append((time.monotonic() - start, data)) + return chunks + + +def enter_os_and_reboot(ser: serial.Serial) -> None: + """Navigate to OS menu and send reboot command.""" + ser.reset_input_buffer() + + # We left the port in the OS> menu before closing mcserial. + # But the port close/reopen may have reset state. Try sending + # a bare CR first to see where we are. + ser.write(b"\r") + time.sleep(0.3) + response = ser.read(4096) + decoded = response.decode("utf-8", errors="replace") + print(f" Prompt check: {decoded.strip()!r}") + + if "OS>" in decoded: + print(" Already in OS menu.") + elif "TRK>" in decoded: + print(" At root prompt, entering OS menu...") + ser.write(b"os\r") + time.sleep(0.3) + ser.read(4096) # consume echo + elif "MOT>" in decoded or "DVB>" in decoded: + print(" In a submenu, exiting to root first...") + ser.write(b"q\r") + time.sleep(0.3) + ser.read(4096) + ser.write(b"os\r") + time.sleep(0.3) + ser.read(4096) + else: + # Unknown state — try brute force: q to root, then os + print(" Unknown state, trying q -> os...") + ser.write(b"q\r") + time.sleep(0.3) + ser.read(4096) + ser.write(b"os\r") + time.sleep(0.3) + ser.read(4096) + + # Flush buffer + ser.reset_input_buffer() + + print(" Sending 'reboot' command...") + ser.write(b"reboot\r") + + +def pass1_observe(ser: serial.Serial) -> list[tuple[float, bytes]]: + """Reboot and passively observe boot output.""" + print("\n=== PASS 1: OBSERVE BOOT SEQUENCE ===\n") + enter_os_and_reboot(ser) + + print(f" Capturing for {BOOT_CAPTURE_SECS}s...\n") + chunks = capture_boot(ser, BOOT_CAPTURE_SECS) + + print("--- Boot Output (with timestamps) ---\n") + full_output = bytearray() + for t, data in chunks: + full_output.extend(data) + text = ( + data.decode("utf-8", errors="replace") + .replace("\r\n", "\n") + .replace("\r", "\n") + ) + for line in text.split("\n"): + if line.strip(): + print(f" [{t:7.3f}s] {line}") + + print(f"\n--- Total: {len(full_output)} bytes in {len(chunks)} chunks ---") + return chunks + + +def pass2_interrupt(ser: serial.Serial) -> None: + """Reboot and try interrupt sequences during bootloader phase.""" + print("\n=== PASS 2: INTERRUPT BOOT ===\n") + + # Interrupt sequences to try, with timing (seconds after reboot) + interrupts = [ + # (delay_after_reboot, description, bytes_to_send) + (0.05, "immediate CR", b"\r"), + (0.05, "immediate ESC", b"\x1b"), + (0.05, "immediate BREAK", "BREAK"), # special handling + (0.05, "immediate 0x55 (autobaud)", b"\x55\x55\x55\x55\x55"), + (0.05, "immediate space", b" "), + (0.1, "100ms CR", b"\r"), + (0.1, "100ms ESC", b"\x1b"), + (0.2, "200ms CR", b"\r"), + (0.5, "500ms CR+ESC", b"\r\x1b\r"), + (1.0, "1s CR", b"\r"), + (1.0, "1s 'bl'", b"bl\r"), + (1.0, "1s 'boot'", b"boot\r"), + (2.0, "2s CR", b"\r"), + (2.0, "2s '?'", b"?\r"), + ] + + for delay, desc, payload in interrupts: + print(f"\n--- Trying: {desc} (at +{delay}s) ---") + enter_os_and_reboot(ser) + + # Wait for the specified delay + time.sleep(delay) + + # Send the interrupt + if payload == "BREAK": + ser.send_break(duration=0.25) + print(" Sent BREAK signal (250ms)") + else: + ser.write(payload) + print(f" Sent: {payload!r}") + + # Capture response for 5 seconds + chunks = capture_boot(ser, 8) + + full = bytearray() + for _, data in chunks: + full.extend(data) + text = full.decode("utf-8", errors="replace") + + # Check for interesting responses + interesting = False + for keyword in [ + "boot", + "loader", + "BL>", + "CMD>", + ">>", + "ready", + "download", + "upload", + "flash", + "update", + "xmodem", + "ymodem", + "zmodem", + "srec", + "hex", + "binary", + ]: + if keyword.lower() in text.lower(): + interesting = True + break + + if interesting: + print(" *** INTERESTING RESPONSE ***") + for t, data in chunks: + line = data.decode("utf-8", errors="replace").strip() + if line: + print(f" [{t:7.3f}s] {line}") + else: + # Print condensed summary + lines = [ln.strip() for ln in text.split("\n") if ln.strip()] + if lines: + print(f" Response: {len(full)} bytes, first: {lines[0][:80]!r}") + if len(lines) > 1: + print(f" last: {lines[-1][:80]!r}") + else: + print(f" No response ({len(full)} bytes)") + + # Wait for boot to complete before next attempt + print(" Waiting for full boot...") + time.sleep(max(0, BOOT_CAPTURE_SECS - 8 - delay)) + ser.reset_input_buffer() + + +def main(): + parser = argparse.ArgumentParser(description="G2 bootloader capture") + parser.add_argument( + "--interrupt", action="store_true", help="Pass 2: try interrupt sequences" + ) + parser.add_argument("--port", default=PORT) + parser.add_argument("--baud", type=int, default=BAUD) + args = parser.parse_args() + + print(f"Opening {args.port} @ {args.baud}...") + ser = serial.Serial(args.port, args.baud, timeout=1) + ser.reset_input_buffer() + + try: + pass1_observe(ser) + + if args.interrupt: + # Wait for boot to fully complete + print("\n Waiting for boot to settle...") + time.sleep(5) + ser.reset_input_buffer() + pass2_interrupt(ser) + except KeyboardInterrupt: + print("\n\nInterrupted by user.") + finally: + ser.close() + print("\nPort closed.") + + +if __name__ == "__main__": + main() diff --git a/scripts/ee_dump.py b/scripts/ee_dump.py new file mode 100644 index 0000000..6f3bace --- /dev/null +++ b/scripts/ee_dump.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Dump all EEPROM indices from Winegard G2 firmware via RS-422.""" + +import re +import sys +import time + +import serial + +PORT = "/dev/ttyUSB2" +BAUD = 115200 +PROMPT = b">" +MAX_INDEX = 100 # scan up to this index + + +def send_cmd(ser: serial.Serial, cmd: str) -> str: + """Send command + CR, read until prompt '>'.""" + ser.reset_input_buffer() + ser.write(f"{cmd}\r".encode("ascii")) + buf = bytearray() + while True: + b = ser.read(1) + if len(b) == 0: + break # timeout + buf.append(b[0]) + if b[0] == ord(">"): + break + return buf.decode("utf-8", errors="ignore") + + +def main(): + ser = serial.Serial(PORT, BAUD, timeout=3) + time.sleep(0.1) + + # Navigate to EE submenu + send_cmd(ser, "q") # ensure root + resp = send_cmd(ser, "eeprom") + if "EE>" not in resp: + print(f"Failed to enter EEPROM menu: {resp!r}", file=sys.stderr) + ser.close() + sys.exit(1) + + print(f"{'Index':>5} {'Decimal':>10} {'Hex':>10} Status") + print("-" * 50) + + valid_count = 0 + for idx in range(MAX_INDEX + 1): + resp = send_cmd(ser, f"ee {idx}") + + # Parse response + if "Read value" in resp: + match = re.search(r"Read value = (\d+)", resp) + if match: + val = int(match.group(1)) + print(f"{idx:>5} {val:>10} 0x{val:08X} OK") + valid_count += 1 + elif "Failed to read" in resp: + match = re.search(r"val:(\d+)", resp) + val_str = match.group(1) if match else "?" + val = int(val_str) if val_str != "?" else 0 + print(f"{idx:>5} {val:>10} 0x{val:08X} INVALID") + else: + # Unknown response - might be end of range + clean = resp.strip().replace("\r\n", " | ") + print(f"{idx:>5} {'':>10} {'':>10} ERROR: {clean}") + + print("-" * 50) + print(f"Valid entries: {valid_count} / {MAX_INDEX + 1}") + + # Return to root + send_cmd(ser, "q") + ser.close() + + +if __name__ == "__main__": + main()