Add G2 bootloader and EEPROM exploration scripts

boot_baud_probe.py: Probe all standard baud rates during G2 power-on
to detect bootloader UART configuration differences.

boot_capture.py: Capture and analyze G2 boot sequence output, with
optional interrupt sequence injection to explore bootloader entry.

ee_dump.py: Dump EEPROM indices from EE> submenu, identifying
initialized vs. uninitialized (0x10101 sentinel) entries.
This commit is contained in:
Ryan Malloy 2026-02-14 18:05:03 -07:00
parent 145763fcfb
commit 13a9d804c6
3 changed files with 502 additions and 0 deletions

197
scripts/boot_baud_probe.py Normal file
View File

@ -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()

229
scripts/boot_capture.py Normal file
View File

@ -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()

76
scripts/ee_dump.py Normal file
View File

@ -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()