master
Name Last commit message Last updated
fable ESC demo: routing round, Freerouting and Fable takes, video and stats; Astra pending · John Lauer 25d ago
freerouting ESC demo: routing round, Freerouting and Fable takes, video and stats; Astra pending · John Lauer 25d ago
loop ESC routing to 100%: the place-route loop (smaller signal vias, 13 parts moved, 114 commits, 0 unconnected), kicad-place-route-loop skill, video and stats; Astra pending · John Lauer 24d ago
adjust_live.py ESC routing to 100%: the place-route loop (smaller signal vias, 13 parts moved, 114 commits, 0 unconnected), kicad-place-route-loop skill, video and stats; Astra pending · John Lauer 24d ago
ai_router.py ESC routing to 100%: the place-route loop (smaller signal vias, 13 parts moved, 114 commits, 0 unconnected), kicad-place-route-loop skill, video and stats; Astra pending · John Lauer 24d ago
esc-g431-fable-placed-planes.kicad_pcb ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
esc-g431-fable-placed.kicad_pcb ESC routing round: Fable's placed board (no copper, no zones) as the routing fixture · John Lauer 25d ago
escboard.py ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
fable-esc-routing-plan.json ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
fable-esc-routing-report.json ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
offline-drc-report.json ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
plan-preview.png ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
README.md ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
render_plan.py ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
route_live.py ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago
routing-stats.json ESC routing to 100%: the place-route loop (smaller signal vias, 13 parts moved, 114 commits, 0 unconnected), kicad-place-route-loop skill, video and stats; Astra pending · John Lauer 24d ago
write_copper.py ESC routing round: planes fixture (GND In1, +3V3 In2), Fable's grid router, offline DRC gate, live replay · John Lauer 25d ago

ESC G431 routing round: Claude Fable 5.1's router

The placement round left esc-g431-fable-placed.kicad_pcb: 149 footprints inside a 64 x 74 mm outline, four copper layers, 365 pads, 59 nets with two or more pads, no copper. This folder is the routing round: a real grid maze router written for the board, an offline DRC gate that proves its plan before anything touches a desktop, and the live replay through the bridge.

Everything here is Python 3 with numpy. Nothing in the Rust workspace changed.

The four scripts

Script What it does
tools/add_planes.py Adds two full-outline copper zones to a copy of the board: GND on In1.Cu and +3V3 on In2.Cu. Output esc-g431-fable-placed-planes.kicad_pcb. --drc runs the shared headless KiCad DRC on it.
demo/routing/esc/ai_router.py The router. Reads the planes board, writes the plan JSON (fable-esc-routing-plan.json) and a report (fable-esc-routing-report.json).
demo/routing/esc/write_copper.py Applies a plan offline into a copy of the board (segments, vias, uuids, net references, widths, layers), adds a raster fill to the plane zones so connectivity through the planes counts, runs service-kicad pcb drc and compares the errors with the unrouted board. This is the gate.
demo/routing/esc/route_live.py Replays the plan through the bridge: kicad_routing_state, one kicad_route_net per plan entry with expectedRevision, dryRun support, a timestamped JSON log, stops on drc_rejected or stale_board, ends with kicad_routing_validate.

escboard.py is the shared board model (pads in board coordinates with KiCad's rotation convention, custom pad polygons, the outline, the 0.1 mm grid rasterisers). render_plan.py draws a plan over the pads as a PNG for a quick look.

How to run

# 1. planes (writes demo/routing/esc/esc-g431-fable-placed-planes.kicad_pcb, prints the DRC counts)
python3 tools/add_planes.py demo/routing/esc/esc-g431-fable-placed.kicad_pcb --drc

# 2. route (about ten minutes on the container; writes the plan and the report)
python3 demo/routing/esc/ai_router.py demo/routing/esc/esc-g431-fable-placed-planes.kicad_pcb \
    --out demo/routing/esc/fable-esc-routing-plan.json --report demo/routing/esc/fable-esc-routing-report.json

# 3. offline gate (writes the routed copy and the DRC summary; exit 0 only at zero new errors and zero unconnected)
python3 demo/routing/esc/write_copper.py demo/routing/esc/esc-g431-fable-placed-planes.kicad_pcb \
    demo/routing/esc/fable-esc-routing-plan.json --out demo/routing/esc/esc-g431-fable-routed-offline.kicad_pcb \
    --drc --report demo/routing/esc/offline-drc-report.json

# 4. live replay (John runs this one; the planes board must be the file open in KiCad)
python3 demo/routing/esc/route_live.py --target arav-rog \
    --board "C:/Users/arav/Documents/esc/esc-g431-fable-placed-planes.kicad_pcb" \
    --plan demo/routing/esc/fable-esc-routing-plan.json --log demo/routing/esc/route-log.json [--dry-run]

# a picture of the plan
python3 demo/routing/esc/render_plan.py demo/routing/esc/esc-g431-fable-placed-planes.kicad_pcb \
    demo/routing/esc/fable-esc-routing-plan.json demo/routing/esc/plan-preview.png --labels

What the router does

Grid and layers. A 0.1 mm cell grid over the outline (641 x 741 cells), two signal layers (F.Cu and B.Cu) joined by through vias, 0.8 mm with a 0.4 mm drill. The planes on In1.Cu and In2.Cu are never routed on.

Rules. This board has no project file, so KiCad's defaults apply and the router uses them: 0.25 mm track, 0.2 mm clearance, 0.5 mm copper to board edge, plus the local (clearance 0.6) overrides on the phase contacts MC5, MC9, MC11 and the machine pin MP3. The bridge's kicad_route_net gets viaSize 0.8 and viaDrill 0.4 on every call.

Search. A* per connection, eight neighbours (octilinear), costs 10 per orthogonal cell and 14 per diagonal, times 1.1 on B.Cu (prefer F.Cu), plus 250 per via (heavily penalised) and 4 per direction change (a little). Collinear cells merge into one segment. A diagonal step is allowed only when one of its ends also clears a larger dilation covering the midpoint of the step, so a 45-degree segment never clips a corner of a pad.

Obstacles. For the net being routed, every pad and every committed segment and via of any other net is dilated by its clearance plus half the track width plus half a cell diagonal (pads sit off-grid). Through pads exist on both layers. The outline is inset by the edge clearance. Own-net copper is free space, so a branch can end on the net's own track.

Fine-pitch pins. A pad narrower than 0.5 mm (the LQFP-48, the DRV8300 and the buck module pins) is entered on its long axis: a 0.2 mm stub from the exact pad centre to an on-grid escape point 0.45 mm beyond either pad end, and the route starts there. The stub and a short corridor beyond it are reserved for the pin until it is routed, an IC body is a keepout on F.Cu for nets with no pin on that IC, a band around the IC carries a small cost so routes do not run along a pin row, and a via pays extra for every unrouted neighbour pin whose straight path it would wall in. Pads that overlap on the same net (the MOSFET source pads) form one cluster and are hit once.

Nets. A net with N clusters is a minimum spanning tree: Prim's order from the biggest pad, each new cluster routed to ANY copper the net already has (pads, tracks, vias, escape points). A branch that lands mid-segment splits that segment at the on-grid junction so KiCad sees connected copper. Nets are routed shortest MST first, then longest.

Widths. +VBAT and the three phase nets /DRV_SHA, /DRV_SHB and /DRV_SHC (these are also the MOSFET drain and source nets) try 1.0 mm, then 0.5, then 0.25; +5V and +12V try 0.5 then 0.25; everything else is 0.25 mm. A pad narrower than the width caps it (a 1.0 mm track never enters a 0.35 mm pin), and the fallback happens per connection when the wider search finds no path.

Planes. GND and +3V3 pads are not routed as traces. Every SMD pad on those nets gets the shortest stub (Dijkstra) to the first cell where a 0.8 mm via fits, and the via reaches the plane; through pads on those nets already reach the plane and get nothing. When no site fits within 6 mm the via goes in the pad (exact geometry check; reported). The plane stubs are routed first because they are the shortest connections on the board and their vias then shape the signal routing rather than hunting for holes in it afterwards.

Rip-up and retry. When a connection fails on every width and window, a second search treats other nets' tracks as expensive rather than solid; the nets it crosses are ripped up (most recently routed first, two at a time), the failing net is routed again, and the ripped nets are re-queued. Bounded to three rip-ups per net and 120 in total.

How the plan maps to kicad_route_net

The plan is {"engine", "board", "rules", "nets": [entry, ...]} and every entry is exactly one kicad_route_net call: {"net", "width", "viaSize", "viaDrill", "paths": [[...], ...]}. A waypoint is "REF.PAD" (the bridge resolves it to the pad centre), [x, y], or {"x": .., "y": .., "layer": "B.Cu"}; a layer marker means a through via at that point and every following segment on the new layer, which is the grammar in rust/crates/kicad-core/src/pcb.rs plan and resolve_point. A path that starts on B.Cu at a through pad uses {"pad": "MC1.1", "layer": "B.Cu"}. A plane stub is ["C7.2", ..., {"x", "y", "layer": "B.Cu"}]: the stub on F.Cu, then the via. A net appears in several entries when it uses several widths (the 0.2 mm pin stubs are their own entry), so route_live.py sends the entries in order and feeds each reply's revision to the next call.

The offline gate

service-kicad pcb drc is the shared headless KiCad 10.0.2. It does not refill zones, so the gate copy carries a raster fill for each plane: overlapping strips over the outline (0.5 mm from the edge) minus every other-net through pad, hole and via expanded by its clearance. KiCad's own fill on the live board replaces it. Two consequences are reported and excluded: the strips are "isolated copper" islands to KiCad (warnings), and the gate copy connects pads solidly instead of with thermal reliefs because a raster fill has no spokes to check.

Known and excluded on this board before any routing: 13 inherited errors (six malformed MOSFET courtyards on Q1 to Q6, seven pad clearances inside U2's own footprint) and 149 library warnings (lib_footprint_issues) plus 23 silk_over_copper warnings. The gate compares error fingerprints (type, description, item positions) with the unrouted board the way the bridge's route_net compares before and after, and passes only at zero NEW errors and zero unconnected.

Gate result (the delivered plan, fable-esc-routing-plan.json)

Command 3 above on the delivered plan, KiCad 10.0.2 through service-kicad:

Measure Value
DRC errors 13, all inherited (0 new)
DRC warnings 172 inherited (149 library, 23 silk over copper) plus 34 raster-fill island warnings from the gate copy's own fill
Unconnected items 7 (before routing: 271)
Segments 1795 (0.2 mm: 57, 0.25 mm: 1453, 0.5 mm: 148, 1.0 mm: 137)
Vias 255, all 0.8 mm / 0.4 mm drill, none inside a pad
Copper length 1908.3 mm (F.Cu 1010.4 mm, B.Cu 897.9 mm)
Plan entries 105 kicad_route_net calls for 55 nets, 308 paths, 2267 waypoints
Nets routed 50 of 57 signal nets complete, GND (90 clusters) and +3V3 (21 clusters) complete through the planes

The gate is therefore zero new errors but NOT zero unconnected: seven connections in six nets could not be routed under these rules, and the router says so rather than leaving them out quietly (fable-esc-routing-report.json unrouted, and the router's exit code is 3). The delivered plan is the best of the runs made while tuning (the router keeps the best intermediate state of a pass, and the multi-pass mode keeps the best pass); the file is the one the gate numbers above were measured on.

Net Connection left open Why
/TIM1_CH2N_INLB U5.28 to U4.5 LED1 sits 1.0 mm from the ends of U5 pins 26 to 28, so those three adjacent 0.5 mm pins can only leave inward under the package with vias; two adjacent pins cannot both drop a 0.8 mm via in a 0.5 mm pitch channel, and the one in the middle loses.
/HSE_IN U5.5 to Y1.1 and C35.1 U5 pins 5 to 12 (crystal, reset, the BEMF comparator inputs) all leave the left column into the same 3 mm strip between U5 and the R26 to R34 network; every F.Cu corridor there is taken by nets routed earlier and no via site is left within reach.
/PB6 U5.43 to R17.2 Same congestion on the top row: PB6, PB7, MCU_LED, SWCLK, SWDIO and BOOT0 escape upward into the LED2, LED3, R16 group.
+5V U2.9 to the rest of +5V U2.9 is a 0.35 mm pin of the buck module whose only escape pocket holds the GND stub of C10 and the +3V3 stub next to it.
/COMP2_INM_IO1_BEMF_A R27.2 to the branch R27's second pad is walled by the +3V3 stub and via of C29 and R27's own GND neighbour.
Net-(LED2-A) R11.1 to LED2.2 R11 sits at the far left of the board and LED2 next to U5's top row; the 17 mm run has to cross the SWDIO, SWCLK and nRST bundle and the GND vias of J1.

In every case the router tried: 0.25 mm on F.Cu and B.Cu, a via in the pad when the pad allows it, a soft search to name the blockers, and up to eight rip-up rounds per net (the plane stubs included when they were the only blocker). What would fix them is placement, not routing: LED1 and the resistor network are too close to the QFP pin rows for 0.25 mm tracks with 0.2 mm clearance and 0.8 mm vias. On the live board these seven show up as ratsnest lines after the replay, and kicad_routing_validate reports them as unconnected; the AI or the user can finish them by hand or after nudging LED1 and R26 to R34 away from U5.

Per-net widths, segments, vias and copper length are in the table at the end of this file.

Reproducibility: the router is deterministic for a given code state, but which seven connections lose depends on the rip-up order, and the rip-up rules were still being tuned while the delivered plan was produced (it came from the run with the fewest open connections; every later rule change also ended at seven, in a different mix around U5). A fresh ai_router.py run on the planes board takes about ten minutes and reports its own open connections on stdout and in the report JSON; gate whatever it produces with write_copper.py --drc before replaying it.

Files

  • esc-g431-fable-placed.kicad_pcb: the input (placement round output).
  • esc-g431-fable-placed-planes.kicad_pcb: the input plus the two planes (what the desktop opens).
  • fable-esc-routing-plan.json: the plan, one entry per kicad_route_net call.
  • fable-esc-routing-report.json: per-net clusters, routes, widths, rip-ups, failures.
  • esc-g431-fable-routed-offline.kicad_pcb: the plan applied offline with raster-filled planes (the gate copy).
  • offline-drc-report.json: the gate's DRC summary.
  • plan-preview.png: the plan drawn over the pads (F.Cu red, B.Cu blue, vias black).

Per-net result (delivered plan)

Widths are the track widths the net ended up with (the 0.2 mm entries are the fine-pitch pin stubs). Clusters are pad groups; a failed count is a connection left open.

Net widths (mm) segments vias copper mm clusters open
+VBAT 1/0.5/0.25/0.2 129 16 224.0 28 0
/COMP1_INM_IO2_BEMF_C 0.25/0.2 100 14 100.1 6 0
/COMP2_INM_IO1_BEMF_A 0.25/0.2 44 5 93.2 6 1
/COMP1_INM_IO1_BEMF_B 0.25/0.2 86 14 93.0 5 0
GND_OUT 0.25 22 5 92.1 3 0
/SWCLK 0.25/0.2 92 8 91.0 4 0
GND 0.25/0.2 110 73 85.8 90 0
/PB12 0.25/0.2 93 4 78.2 4 0
/DRV_SHA 1/0.5/0.25/0.2 48 9 75.4 8 0
/DRV_SHC 1/0.5/0.25/0.2 60 5 68.1 8 0
/DRV_SHB 1/0.5/0.25/0.2 77 10 67.1 8 0
/SWDIO 0.25/0.2 68 9 59.7 4 0
/nRST 0.25/0.2 94 6 58.3 6 0
/TIM15_CH1_DSHOT 0.25/0.2 60 5 56.7 3 0
/NEUTRAL 0.25/0.2 75 4 45.5 6 0
/VBAT_SENSE 0.25/0.2 40 4 41.0 4 0
/IBAT_SENSE 0.25/0.2 10 2 34.4 2 0
+12V 0.5/0.25/0.2 38 4 31.5 7 0
+5V 0.5 41 4 31.3 9 1
/HSE_OUT 0.25/0.2 48 3 31.0 3 0
/GLC 0.25 20 2 30.8 3 0
/GHC 0.25 20 0 30.7 3 0
/5V_EXT 0.25 6 1 27.5 2 0
/TIM1_CH3N_INLA 0.25/0.2 38 4 24.2 2 0
/USART1_TX 0.25 38 1 23.8 4 0
/GHA 0.25 10 2 22.9 3 0
+3V3 0.25/0.2 38 19 22.8 21 0
/USART1_RX 0.25 10 1 21.6 4 0
/TIM1_CH2_INHB 0.25/0.2 21 2 19.9 2 0
/GLA 0.25 7 2 19.9 3 0
/TIM1_CH1N_INLC 0.25/0.2 13 2 19.5 2 0
/PB7 0.25/0.2 26 2 19.1 2 0
/VDDA 0.25/0.2 32 0 16.0 6 0
/TIM1_CH3_INHA 0.25/0.2 14 2 15.4 3 0
/5V_EN 0.25/0.2 11 1 13.9 3 0
/TIM1_CH1_INHC 0.25/0.2 20 2 13.4 2 0
/GLB 0.25 5 0 12.2 3 0
/MCU_LED 0.25/0.2 16 2 11.3 2 0
Net-(LED1-A) 0.25 18 2 10.0 2 0
/DRV_GHB 0.25/0.2 10 2 8.5 2 0
/DRV_BSTB 0.25/0.2 7 2 7.7 2 0
/DRV_BSTC 0.25/0.2 10 0 7.4 2 0
/DRV_GHA 0.25/0.2 6 0 7.0 3 0
/GHB 0.25 5 0 6.6 3 0
/BOOT0 0.25/0.2 14 0 5.8 3 0
/DRV_GLB 0.25/0.2 7 0 5.3 2 0
/5V_VCC 0.25/0.2 8 0 4.8 2 0
/DRV_BSTA 0.25/0.2 4 0 4.3 2 0
/DRV_GLA 0.25/0.2 3 0 3.5 2 0
Net-(LED3-A) 0.25 8 0 3.1 2 0
/SW 0.25/0.2 3 0 3.1 2 0
/RPM 0.25/0.2 3 0 2.6 2 0
/DRV_GHC 0.25/0.2 4 0 2.2 2 0
/DRV_GLC 0.25/0.2 3 0 2.1 2 0
/E_STOP 0.25 2 0 2.1 2 0
total 1795 255 1908.3 7