Headless layout / autorouting
Engine: $(kx root)/kicad_lib/layout.py
(python API: PYTHONPATH=; any freerouting-*-linux-x64 in .tools
is auto-detected, newest wins).
Worked end-to-end example: tests/test_layout.py (scratch board with
outline + 2 netted footprints → 9 routed segments).
The chain
layout.autoroute(pcb_in, pcb_out, passes=20)
= dsn_export (pcbnew python in the KiCad flatpak:
pcbnew.ExportSpecctraDSN — headless, verified 10.0)
→ route (.tools/freerouting-2.2.4-linux-x64/bin/freerouting,
BUNDLED JRE — no system java; classic CLI
-de in.dsn -do out.ses -mp N -da)
→ ses_import (pcbnew.ImportSpecctraSES + SaveBoard)
Returns segment/via counts. Bootstrap if .tools missing: download freerouting-X-linux-x64.zip from github freerouting/freerouting releases, unzip into .tools/ (no root needed).
Preconditions (or DSN export is meaningless)
- Edge.Cuts outline present (closed).
- Pads carry nets. Update-PCB-from-Schematic normally fills them; when
building boards programmatically, append
(net "NAME")to each pad — pcb format 20260206 (KiCad 10) keys nets BY NAME, no numeric id (old(net 1 "N1")style is normalized on import, harmless). - Placement matters more than the router: group by schematic function, decoupling caps adjacent, connectors at edges (rule canon).
After routing — never skip
kicad-cli pcb drc --severity-allset-diff vs pre-route baseline.- Render F.Cu/B.Cu/Edge.Cuts → PNG → look (kicad-review pixel diff).
- pcbnew rewrites the file wholesale on SES import — commit the pre-route board first, diff after.