app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay
f564403
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
#!/usr/bin/env python3
"""Add full-outline copper planes to a copy of a KiCad 10 board.
python3 tools/add_planes.py BOARD.kicad_pcb [--out FILE] [--plane GND:In1.Cu] [--plane +3V3:In2.Cu]
[--clearance 0.2] [--min-thickness 0.25] [--drc]
Default planes: GND on In1.Cu and +3V3 on In2.Cu. Each plane is one zone whose polygon is the
Edge.Cuts outline (lines as they are, arcs sampled to short chords), thermal-relief pad
connection with the given clearance, priority 0 (KiCad's default, so the line is not written),
fill enabled, `(filled_areas_thickness no)` and NO filled_polygon: KiCad fills it on load or
when a DRC refills zones. The net is written the way this board writes nets: KiCad 10 boards
carry `(net "NAME")` on every pad and no net table, so the zone gets `(net "NAME")`; a board
with a `(net N "NAME")` table gets `(net N)` plus `(net_name "NAME")`.
The zones are spliced in before the closing paren; every other byte of the file is untouched.
--drc runs `service-kicad pcb drc` (the shared headless KiCad, which refills zones) on the
output and prints the error, warning and unconnected counts.
"""
import argparse
import json
import os
import subprocess
import sys
import tempfile
import uuid
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "demo", "routing", "esc"))
from make_placement_fixture import num # noqa: E402
from escboard import Board, polygon_area # noqa: E402
def zone_sexpr(board, net_name, layer, outline, clearance, min_thickness):
if board.net_format == "number":
number = next((n for n, name in board.net_table.items() if name == net_name), None)
if number is None:
raise KeyError("net %r is not in the net table" % net_name)
net_lines = "\t\t(net %d)\n\t\t(net_name \"%s\")\n" % (number, net_name.replace('"', '\\"'))
else:
net_lines = board.net_ref_sexpr(net_name)
pts = " ".join("(xy %s %s)" % (num(x), num(y)) for x, y in outline)
return (
"\t(zone\n"
+ net_lines
+ "\t\t(layer \"%s\")\n" % layer
+ "\t\t(uuid \"%s\")\n" % uuid.uuid4()
+ "\t\t(name \"%s plane\")\n" % net_name.replace('"', '\\"')
+ "\t\t(hatch edge 0.5)\n"
+ "\t\t(connect_pads\n\t\t\t(clearance %s)\n\t\t)\n" % num(clearance)
+ "\t\t(min_thickness %s)\n" % num(min_thickness)
+ "\t\t(filled_areas_thickness no)\n"
+ "\t\t(fill yes\n\t\t\t(thermal_gap 0.5)\n\t\t\t(thermal_bridge_width 0.5)\n\t\t)\n"
+ "\t\t(polygon\n\t\t\t(pts\n\t\t\t\t%s\n\t\t\t)\n\t\t)\n" % pts
+ "\t)"
)
def splice(text, blocks):
"""Insert blocks before the closing paren of the kicad_pcb list, like the bridge's pcb::splice."""
trimmed = text.rstrip()
idx = trimmed.rfind(")")
body = text[:idx].rstrip()
nl = "\r\n" if "\r\n" in text[:2000] else "\n"
out = body + nl + nl.join(b.replace("\n", nl) for b in blocks) + nl + ")" + nl
return out
def run_drc(path):
"""service-kicad pcb drc as JSON: (errors, warnings, unconnected, report dict)."""
with tempfile.TemporaryDirectory() as tmp:
report = Path(tmp) / "drc.json"
proc = subprocess.run(["service-kicad", "pcb", "drc", "--format", "json", "--out", str(report), str(path)],
capture_output=True, text=True, timeout=600)
if proc.returncode or not report.is_file():
raise RuntimeError("service-kicad drc failed: %s %s" % (proc.stdout[-400:], proc.stderr[-400:]))
data = json.loads(report.read_text(encoding="utf-8"))
viol = data.get("violations", [])
errors = sum(1 for v in viol if v.get("severity") == "error")
warnings = sum(1 for v in viol if v.get("severity") == "warning")
return errors, warnings, len(data.get("unconnected_items", [])), data
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("board")
ap.add_argument("--out", help="output path (default: <board>-planes.kicad_pcb beside the input)")
ap.add_argument("--plane", action="append", help="NET:LAYER, repeatable (default GND:In1.Cu and +3V3:In2.Cu)")
ap.add_argument("--clearance", type=float, default=0.2)
ap.add_argument("--min-thickness", type=float, default=0.25)
ap.add_argument("--drc", action="store_true", help="run service-kicad pcb drc on the output and print the counts")
a = ap.parse_args(argv)
planes = [p.split(":", 1) for p in (a.plane or ["GND:In1.Cu", "+3V3:In2.Cu"])]
src = Path(a.board)
board = Board(str(src))
for net, layer in planes:
if net not in board.nets:
raise SystemExit("net %r has no pads on this board" % net)
if layer not in board.copper_layers:
raise SystemExit("layer %r is not a copper layer of this board (%s)" % (layer, board.copper_layers))
outline = list(board.outline)
if polygon_area(outline) < 0:
outline.reverse()
blocks = [zone_sexpr(board, net, layer, outline, a.clearance, a.min_thickness) for net, layer in planes]
out = Path(a.out) if a.out else src.with_name(src.name[: -len(".kicad_pcb")] + "-planes.kicad_pcb")
text = splice(board.text, blocks)
out.write_text(text, encoding="utf-8", newline="")
Board(str(out)) # must still parse as one balanced document
print("%s: %d zones (%s), %d outline points, net format %s, %d bytes" % (
out, len(planes), ", ".join("%s on %s" % (n, l) for n, l in planes), len(outline), board.net_format, len(text.encode("utf-8"))))
if a.drc:
errors, warnings, unconnected, data = run_drc(out)
by_type = {}
for v in data.get("violations", []):
by_type[v.get("type")] = by_type.get(v.get("type"), 0) + 1
print("drc: %d errors, %d warnings, %d unconnected items; by type %s (KiCad %s)" % (
errors, warnings, unconnected, json.dumps(by_type, sort_keys=True), data.get("kicad_version")))
return 0
if __name__ == "__main__":
sys.exit(main())