#!/usr/bin/env bash # Route one KiCad board with freeroute and judge the result with KiCad's DRC. # # bench/drc_check.sh [--keep] [freeroute args...] # # Copies the board (and its .kicad_pro) into , strips its tracks and # vias unless --keep is given (then they reach the router as fixed wiring), # exports the DSN through mckicad, routes, applies the session, and runs DRC # with zones refilled at every severity. Prints one line: time, copper added, # shorts, opens, hole problems (whatever severity the board gives them) and # the other errors. The source board is never touched. # # FREEROUTE (default: this checkout's .venv/bin/freeroute) and MCK (default # ~/claude/mckicad) pick the builds to judge. set -euo pipefail keep=0 if [[ "${1:-}" == "--keep" ]]; then keep=1; shift; fi src="$1"; out="$2"; shift 2 here="$(cd "$(dirname "$0")/.." && pwd)" FREEROUTE="${FREEROUTE:-$here/.venv/bin/freeroute}" MCK="${MCK:-$HOME/claude/mckicad}" rm -rf "$out"; mkdir -p "$out" cp "${src%.kicad_pcb}.kicad_pro" "$out/board.kicad_pro" 2>/dev/null || true if [[ $keep == 1 ]]; then cp "$src" "$out/board.kicad_pcb" else (cd "$MCK" && uv run --no-sync python - "$src" "$out/board.kicad_pcb" <<'EOF' import sys from mckicad.utils.sexp_tree import parse text = open(sys.argv[1]).read() spans = sorted((c.span for c in parse(text).children if c.tag in ("segment", "arc", "via")), reverse=True) for a, b in spans: text = text[:a] + text[b:] open(sys.argv[2], "w").write(text) EOF ) fi (cd "$MCK" && uv run --no-sync python -c " from mckicad.utils.freerouting import FreeRoutingEngine assert FreeRoutingEngine().export_dsn_from_kicad('$out/board.kicad_pcb', '$out/board.dsn', {})") \ >/dev/null 2>&1 || { echo "$out: DSN export failed"; exit 1; } start=$(date +%s.%N) "$FREEROUTE" "$out/board.dsn" -do "$out/board.ses" "$@" >"$out/route.log" 2>&1 || true secs=$(python3 -c "print(round($(date +%s.%N)-$start,1))") [[ -s "$out/board.ses" ]] || { echo "$out: no session ($(tail -1 "$out/route.log"))"; exit 1; } (cd "$MCK" && uv run --no-sync python -c " from mckicad.utils.ses_apply import apply_ses_to_board as a r = a('$out/board.kicad_pcb', '$out/board.ses', '$out/board.kicad_pcb') print(r['segments_added'], r['vias_added'])" 2>/dev/null) >"$out/apply.txt" kicad-cli pcb drc --refill-zones --format json --severity-all \ -o "$out/drc.json" "$out/board.kicad_pcb" >/dev/null 2>&1 || true python3 - "$out" "$secs" <<'EOF' import collections, json, sys out, secs = sys.argv[1:] seg, via = open(f"{out}/apply.txt").read().split() d = json.load(open(f"{out}/drc.json")) errors = collections.Counter(v["type"] for v in d["violations"] if v.get("severity") == "error") other = collections.Counter(v["type"] for v in d["violations"] if v.get("severity") != "error") holes = {k: errors.pop(k, 0) + other.get(k, 0) for k in ("holes_co_located", "hole_to_hole")} print(f"{out}: {secs}s {seg} seg {via} via shorts {errors.pop('shorting_items', 0)} " f"opens {len(d.get('unconnected_items', []))} holes {holes} errors {dict(errors)}") EOF