"""Scan common automotive CAN bitrates to find one that yields valid frames. Usage: python scan_bitrate.py [--port /dev/cu.usbserial-DNBJV4F5] [--seconds 2] Tries each candidate bitrate in turn, listens briefly, and reports how many frames came back and whether they look well-formed (DLC 0-8, plausible IDs). The bitrate with the most valid frames is your best bet. Note on wiring: CAN H and L being swapped is NOT usually something a bitrate scan can "see through" - CAN transceivers work on the differential voltage (H minus L), so swapping the two wires inverts dominant/recessive and corrupts every bit, at every bitrate. If this scan finds zero valid frames at all common bitrates, that's a strong signal to double check H/L wiring (and termination - a CAN bus needs ~120 ohm resistors at each end) rather than trying more bitrates. """ import argparse 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 DEFAULT_PORT = "/dev/cu.usbserial-DNBJV4F5" # Common automotive bitrates, most-likely-first for a steering/body bus. CANDIDATE_BITRATES = [500_000, 125_000, 250_000, 1_000_000, 100_000, 50_000, 20_000, 10_000] def scan_one(port: str, bitrate: int, seconds: float) -> tuple[int, int, Counter]: total = 0 valid = 0 ids = Counter() with Candapter(port, bitrate) as adapter: deadline = time.monotonic() + seconds while time.monotonic() < deadline: frame = adapter.read_frame(timeout=0.2) if frame is None: continue total += 1 if 0 <= len(frame.data) <= 8: valid += 1 ids[frame.arbitration_id] += 1 return total, valid, ids def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--port", default=DEFAULT_PORT, help="Serial device path") parser.add_argument("--seconds", type=float, default=2.0, help="Listen time per bitrate") args = parser.parse_args() print(f"Scanning {args.port} across {len(CANDIDATE_BITRATES)} bitrates ({args.seconds}s each)...\n") print(f"{'bitrate':>10} {'frames':>7} {'valid':>6} {'unique ids':>10}") results = [] for bitrate in CANDIDATE_BITRATES: try: total, valid, ids = scan_one(args.port, bitrate, args.seconds) except OSError as exc: print(f"{bitrate:>10} error opening port: {exc}") continue print(f"{bitrate:>10} {total:>7} {valid:>6} {len(ids):>10}") results.append((bitrate, valid, ids)) results.sort(key=lambda r: r[1], reverse=True) best = results[0] if results else None if best and best[1] > 0: bitrate, valid, ids = best print(f"\nBest match: {bitrate} bps ({valid} valid frames, {len(ids)} unique IDs).") print("Top IDs seen:", ", ".join(f"0x{i:X}x{c}" for i, c in ids.most_common(10))) else: print( "\nNo valid frames at any bitrate. Since this affects every bitrate equally, " "check the physical connection first: CAN H/L polarity and bus termination " "(~120 ohm at each end) before trying more bitrates." ) return 0 if __name__ == "__main__": raise SystemExit(main())