"""Work out the message order that precedes the EPS coming online. Takes a gateway session log where the EPS *did* wake up, finds the moment it first transmits (its own IDs, not relayed car traffic), and reports what the car sent before that - first-seen times, and how the CAS terminal message 0x130 progressed. That progression is the thing a synthetic transmit set has to reproduce: a static payload can't walk the EPS through the same states the car does. Usage: python tools/analyze_startup_order.py captures/gateway_20260829_164540.csv """ import argparse import csv from collections import defaultdict from pathlib import Path # The EPS's own transmissions, per eps-comms/findings.md. 0x100 is our # CAN-IO board's status frame, so it is deliberately not in this set. EPS_IDS = {0x1FB, 0x4B0, 0x5B0} TERMINAL = {0x00: "off", 0x40: "terminal_R", 0x41: "terminal_15", 0x45: "engine_running", 0x55: "cranking", 0x80: "wake_up"} def main() -> int: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("gateway_log") parser.add_argument("--window", type=float, default=None, help="Only consider car frames within N seconds before the EPS wakes") args = parser.parse_args() rows = [] with open(args.gateway_log, newline="") as f: for row in csv.DictReader(f): rows.append((float(row["t"]), row["direction"], int(row["arbitration_id"], 16), bytes.fromhex(row["data_hex"]))) rows.sort(key=lambda r: r[0]) wake = next((t for t, d, aid, _ in rows if d == "eps->car" and aid in EPS_IDS), None) if wake is None: print("The EPS never transmitted in this log - pick one where it came online.") return 1 print(f"EPS first transmits at t={wake:.3f}s\n") first_seen = {} counts = defaultdict(int) for t, direction, aid, _ in rows: if direction != "car->eps": continue counts[aid] += 1 first_seen.setdefault(aid, t) before = {aid: t for aid, t in first_seen.items() if t < wake} after = {aid: t for aid, t in first_seen.items() if t >= wake} if args.window is not None: before = {aid: t for aid, t in before.items() if t >= wake - args.window} print(f"Car IDs present BEFORE the EPS woke ({len(before)}):") for aid, t in sorted(before.items(), key=lambda kv: kv[1]): print(f" t={t:7.3f}s 0x{aid:03X} ({counts[aid]} frames total)") if after: print(f"\nCar IDs that only appeared AFTER ({len(after)}) - not needed to wake it:") for aid, t in sorted(after.items(), key=lambda kv: kv[1]): print(f" t={t:7.3f}s 0x{aid:03X}") print("\n0x130 CAS terminal progression (the state walk to reproduce):") prev = None for t, direction, aid, data in rows: if direction != "car->eps" or aid != 0x130 or not data: continue if data[0] != prev: marker = " <-- EPS wakes around here" if prev is not None and t >= wake > 0 and abs(t - wake) < 1.5 else "" print(f" t={t:7.3f}s b0=0x{data[0]:02X} {TERMINAL.get(data[0], '?'):15s}{marker}") prev = data[0] print("\nPayload variety in the pre-wake window (how much each ID actually moves):") seen_payloads = defaultdict(set) for t, direction, aid, data in rows: if direction == "car->eps" and t < wake: seen_payloads[aid].add(data) static = [aid for aid, s in seen_payloads.items() if len(s) == 1] varying = sorted(((len(s), aid) for aid, s in seen_payloads.items() if len(s) > 1), reverse=True) print(f" {len(static)} IDs sent one fixed payload: " + ", ".join(f"0x{a:03X}" for a in sorted(static))) print(" IDs whose payload changed (these need live counters/values, not a frozen capture):") for n, aid in varying[:15]: print(f" 0x{aid:03X}: {n} distinct payloads") return 0 if __name__ == "__main__": raise SystemExit(main())