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:
parent
145763fcfb
commit
13a9d804c6
3 changed files with 502 additions and 0 deletions
229
scripts/boot_capture.py
Normal file
229
scripts/boot_capture.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue