#!/usr/bin/env python3 """Capture a line-oriented bare-metal UART stream with host timestamps.""" from __future__ import annotations import argparse import csv import time from pathlib import Path import serial def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--port", default="/dev/ttyUSB2") parser.add_argument("--baud", type=int, default=115200) parser.add_argument("--duration", type=float, default=60.0) parser.add_argument("--output", type=Path, required=True) parser.add_argument( "--stop-text", help="stop early after a line containing this text (duration remains a limit)", ) return parser.parse_args() def main() -> int: args = parse_args() args.output.parent.mkdir(parents=True, exist_ok=True) line_count = 0 byte_count = 0 start_wall_ns = time.time_ns() start_mono_ns = time.monotonic_ns() deadline_ns = start_mono_ns + int(args.duration * 1e9) with serial.Serial(args.port, args.baud, timeout=0.1) as uart, args.output.open( "w", newline="", encoding="utf-8" ) as stream: writer = csv.writer(stream) writer.writerow(["host_monotonic_s", "host_unix_s", "uart_line"]) uart.reset_input_buffer() while time.monotonic_ns() < deadline_ns: raw = uart.readline() if not raw: continue now_mono_ns = time.monotonic_ns() now_wall_ns = time.time_ns() byte_count += len(raw) line = raw.decode("utf-8", errors="replace").rstrip("\r\n") writer.writerow( [ f"{(now_mono_ns - start_mono_ns) / 1e9:.9f}", f"{now_wall_ns / 1e9:.9f}", line, ] ) line_count += 1 if args.stop_text and args.stop_text in line: break elapsed_s = (time.monotonic_ns() - start_mono_ns) / 1e9 print( f"captured lines={line_count} bytes={byte_count} " f"elapsed={elapsed_s:.3f}s output={args.output} " f"start_unix={start_wall_ns / 1e9:.6f}" ) return 0 if __name__ == "__main__": raise SystemExit(main())