"""Extract the CAN-IO board's own digital IO timeline (IN1-4/OUT1-2) from a gateway session log, by pulling out its status frames (arbitration ID 0x100, see can-io/PROTOCOL.md) - the board sends one immediately on any input/output change plus a 1s heartbeat, so this recovers when each channel was high/low over the session (at that resolution - a change that gets undone within a couple of debounce/scheduling ticks might not get its own frame, only the surrounding samples). Usage: python tools/extract_dio_timeline.py captures/gateway_20260829_164540.csv \\ --out captures/dio_20260829.csv """ import argparse import csv from pathlib import Path CAN_ID_STATUS = 0x100 def bits(n: int, count: int) -> str: return "".join(str((n >> i) & 1) for i in range(count)) def main() -> int: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("gateway_log") parser.add_argument("--out", required=True, help="Output CSV path (t, inputs, outputs)") args = parser.parse_args() samples = [] with open(args.gateway_log, newline="") as f: for row in csv.DictReader(f): if int(row["arbitration_id"], 16) != CAN_ID_STATUS: continue data = bytes.fromhex(row["data_hex"]) if len(data) < 2: continue samples.append((float(row["t"]), data[0], data[1])) with open(args.out, "w", newline="") as f: w = csv.writer(f) w.writerow(["t", "inputs", "outputs"]) for t, ins, outs in samples: w.writerow([t, ins, outs]) print(f"Wrote {len(samples)} status samples to {args.out}\n") print("Transitions (IN1-4 / OUT1-2, bit0 first):") prev = None for t, ins, outs in samples: cur = (ins, outs) if cur != prev: print(f" t={t:8.3f}s IN={bits(ins, 4)} OUT={bits(outs, 2)}") prev = cur return 0 if __name__ == "__main__": raise SystemExit(main())