"""Capture CAN frames to a log file, with start/stop control. Usage: python log_can.py [--bitrate 500000] [--port /dev/cu.usbserial-DNBJV4F5] [--output capture.csv] [--duration 30] Prints each frame as it arrives and appends it to a CSV log file. Without --duration it runs until you press Ctrl+C; with --duration it stops itself after that many seconds. The CSV columns are: timestamp_ms, arbitration_id (hex), is_extended, dlc, data_hex timestamp_ms is the adapter's own millisecond counter (wraps every 60000ms), so it's only useful for relative timing within one capture, not wall-clock time. """ import argparse import csv import sys import time from collections import Counter from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from adapters.candapter import Candapter # noqa: E402 CAPTURES_DIR = Path(__file__).resolve().parents[1] / "captures" DEFAULT_PORT = "/dev/cu.usbserial-DNBJV4F5" DEFAULT_BITRATE = 500000 def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--port", default=DEFAULT_PORT, help="Serial device path") parser.add_argument("--bitrate", type=int, default=DEFAULT_BITRATE, help="CAN bus bitrate") parser.add_argument("--output", default=None, help="CSV log file (default: capture_.csv)") parser.add_argument("--duration", type=float, default=None, help="Stop automatically after N seconds") args = parser.parse_args() output = args.output or str(CAPTURES_DIR / time.strftime("capture_%Y%m%d_%H%M%S.csv")) try: adapter = Candapter(args.port, args.bitrate, timestamps=True) except (OSError, ValueError) as exc: print(f"Failed to open adapter on {args.port}: {exc}", file=sys.stderr) return 1 ids = Counter() frame_count = 0 start_monotonic = time.monotonic() start_wall = time.time() # frame.recv_time uses time.time(), not monotonic() deadline = None if args.duration is None else start_monotonic + args.duration stop_msg = f"for {args.duration:.0f}s" if args.duration else "until Ctrl+C" print(f"Capturing {stop_msg} -> {output}\n") print(f"{'timestamp':>14} {'id':>10} {'dlc':>3} data (hex)") try: with adapter, open(output, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["timestamp_ms", "arbitration_id", "is_extended", "dlc", "data_hex"]) while deadline is None or time.monotonic() < deadline: frame = adapter.read_frame(timeout=0.5) if frame is None: continue frame_count += 1 ids[frame.arbitration_id] += 1 id_str = f"{frame.arbitration_id:08X}x" if frame.is_extended else f"{frame.arbitration_id:03X}" data_hex = " ".join(f"{b:02X}" for b in frame.data) print(f"{frame.recv_time - start_wall:14.3f} {id_str:>10} {len(frame.data):>3} {data_hex}") writer.writerow( [ frame.timestamp_ms if frame.timestamp_ms is not None else "", f"{frame.arbitration_id:X}", int(frame.is_extended), len(frame.data), frame.data.hex().upper(), ] ) except KeyboardInterrupt: pass elapsed = time.monotonic() - start_monotonic print(f"\nStopped after {elapsed:.1f}s: {frame_count} frames, {len(ids)} unique IDs.") print("Frames per ID:", ", ".join(f"0x{i:X}={c}" for i, c in ids.most_common())) print(f"Log written to {output}") return 0 if __name__ == "__main__": raise SystemExit(main())