KiCad schematic generation
Engine: $(kx root)/kicad_lib/ — lossless sexp
parser (sexp.py, token-equal round-trip proven on 2.3 MB boards; byte-identity only for .kicad_mod — sch re-serializes with ~0.5% whitespace drift) and the
kx CLI (python3 -m kicad_lib.cli probe|check FILE from the repo root).
Prefer kx probe over ad-hoc grep for inventory; prefer sexp.py over
regex surgery for edits (kx is on PATH via ~/.local/bin). Persist with
sexp.save_file(path, root) — PATH FIRST. Python imports need
PYTHONPATH= or cwd=; the package is not pip-installed. Sibling skills: kicad-project (state machine),
kicad-component (find/fetch parts), kicad-review (diff/ERC), kicad-improve
(self-improvement loop). Design: doc/DESIGN.md in that repo.
Battle-tested workflow for writing .kicad_sch/.kicad_pcb files from
Python without KiCad GUI. The engine and worked examples ship in this repo
(kx root):
kicad_lib/ops.py — standalone sheet, scaffold symbols, full-wire
layout, driven by the built-in geometric verifier (kicad_lib/verify.py).
kicad_lib/live_ops.py — inject a block into an EXISTING schematic using
real library symbols (extends-flattening, multi-unit parts, mirrors),
label-stitched islands.
kicad_lib/pcb.py — stage footprints in the PCB, path-linked to
schematic symbols so Update-PCB adopts them. See tests/test_*.py for
runnable end-to-end usage against tests/fixtures/.
0. The loop (never skip a stage)
- Probe target file + libraries (uuids, pin geometry, occupancy, refs).
- Generate with explicit coordinates; run the geometric verifier.
- ERC headless; compare against a baseline run as a violation set.
- Render SVG → PNG → look at it. Geometry that passes checks can still
be unreadable (text collisions, symbol overlap). Iterate.
- Commit per slice. Checkpoint-commit the project BEFORE first modification.
1. Environment
KiCad 10 is a flatpak (org.kicad.KiCad); there is no host kicad-cli:
flatpak run --command=kicad-cli org.kicad.KiCad sch erc --output erc.rpt --severity-error FILE.kicad_sch
flatpak run --command=kicad-cli org.kicad.KiCad sch export svg --output OUTDIR --no-background-color FILE.kicad_sch
flatpak run --command=kicad-cli org.kicad.KiCad sch export netlist --output out.net FILE.kicad_sch
flatpak run --command=kicad-cli org.kicad.KiCad pcb export svg --output out.svg --layers F.Cu,B.Cu,Edge.Cuts,F.SilkS FILE.kicad_pcb
- Flatpak
/tmp is sandboxed — outputs must land under $HOME.
- Official libs:
~/.local/share/flatpak/runtime/org.kicad.KiCad.Library.Symbols/x86_64/stable/active/files/symbols/*.kicad_sym
~/.local/share/flatpak/runtime/org.kicad.KiCad.Library.Footprints/x86_64/stable/active/files/footprints/*.pretty/*.kicad_mod
- Rasterize SVG with
cairosvg (installed), crop with PIL, then Read the PNG.
- If a project lock file (
~NAME.kicad_pro.lck) exists, KiCad is open —
warn the user to close/reload before and after edits.
2. File format essentials (.kicad_sch, format 20260306, generator "eeschema" 10.0)
Top-level order: (kicad_sch (version) (generator) (generator_version) (uuid) (paper) (lib_symbols ...) <junctions/wires/labels/symbols/sheets> (sheet_instances ...) (embedded_fonts no)). Order of body items is loose.
- Preserve the existing file uuid (it's the root sheet uuid; instance
paths reference it). For a sub-sheet keep the uuid KiCad created.
- Every placed symbol needs
(instances (project "NAME" (path "/<ROOT_UUID>" (reference "R1") (unit 1)))). Sub-sheet instance paths are
/<root_uuid>/<sheet_symbol_uuid>.
lib_symbols is a cache of full symbol definitions named "Lib:Name".
Wires connect ONLY at exact pin endpoints — geometry comes from the cache.
- Wire:
(wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width 0) (type default)) (uuid ...)). Junction: (junction (at x y) (diameter 0) (color 0 0 0 0) (uuid ...)).
- Labels: local
(label "net" (at x y rot) ...), global (global_label "net" (shape input|output) ...), hierarchical (hierarchical_label ...)
(sub-sheets only; on a root sheet use local/global).
- Paren-balance check before writing; strings may contain parens — the
balanced extractor must be quote-aware.
3. Coordinates, rotation, mirror
World coords: mm, +Y down. Library pin coords: +Y up. Everything
(symbol origins AND wire endpoints) must sit on the 1.27 mm grid or
wires will not connect.
Pin world position for a symbol at (x, y, rot), lib pin offset (u, v):
def rot_xy(u, v, rot, mirror=""):
if mirror == "x": v = -v # use mirror only with rot 0 (unambiguous)
if rot == 0: return (u, -v)
if rot == 90: return (-v, -u)
if rot == 180: return (-u, v)
if rot == 270: return (v, u)
# world = (x + dx, y + dy)
Device:R is vertical at rot 0 (pin1 top); rot 90 → pin1 left.
- KiCad PNPs draw emitter at bottom;
(mirror x) flips it up (idiomatic).
- Property text angle is RELATIVE to symbol rotation — for a rot-90/270
symbol set property angle 90 so the text renders horizontal.
- Real symbol heights differ from scratch-built ones — transistor circles
are ~11 mm; totem pairs need ≥15.24 mm center spacing.
4. Sourcing symbols
Real library symbols (preferred — "select real components"):
- Extract
(symbol "NAME" ...) blocks from the lib with a balanced parser.
- Resolve
(extends "BASE"): take the base body, apply the derived
symbol's property overrides, rename BASE_X_Y sub-units to NAME_X_Y,
then prefix the top name to "Lib:NAME" for the cache.
- Cache
Value property must equal the bare symbol name, else ERC reports
lib_symbol_mismatch against the configured library.
- Parse pin positions per unit from
(pin ... (at x y a) ... (number "N")).
- Multi-unit parts (LM393 = unit1 A, unit2 B, unit3 power): one reference,
several
(symbol ...) instances each with its own (unit N); all units
carry identical properties. One dual comparator serves two channels.
- Watch coincident pins (e.g. DMP3013SFV has 3 drain pins at one point —
all get the same net, emit all pin uuid entries).
Scaffold symbols (standalone sheets / quick drafts): define your own
minimal R/C/Q/M/opamp boxes in lib_symbols so pin geometry is fully under
your control. Sub-unit names inside a definition are bare ("R_0_1", never
"lib:R_0_1"). Power symbol header is (power global). If KiCad will open
the project standalone, also emit scaffold.kicad_sym + a project
sym-lib-table entry (URI ${KIPRJMOD}/scaffold.kicad_sym) or ERC warns.
5. Placement and routing
- Describe nets as polylines of waypoints, where a waypoint is either a
literal point or
("ref","pin"); consecutive points become axis-parallel
wire segments. Skip zero-length segments.
- Full wires for a self-contained sheet; label-stitched islands
(driver island / power island / comparator island joined by short local
labels) when injecting into a crowded page — labels merge nets with the
existing drawing (
out1, Vin_p, VCC...) for free.
- Probe free space BEFORE placing: collect
(at ...)/(xy ...) coords into
a coarse occupancy grid — but remember it misses label TEXT extents and
sheet boxes; always confirm against a render. Probe (sheet (at)(size))
blocks explicitly. Title block ≈ bottom-right 110×30 mm — keep clear.
- If the page is full, grow the paper (A4 → A3); existing content keeps
coordinates.
- T-junctions: a wire endpoint or pin touching another wire's interior DOES
connect; emit a
(junction ...) there. Pins may sit pin-on-pin or
pin-on-wire-interior — both connect.
- GND/VCC: one
power:GND/power:VCC symbol per drop, short stub wire to
the pin. Standalone sheets need one PWR_FLAG (power_out pin) on each
passive-driven rail or ERC raises power_pin_not_driven.
6. Geometric verifier (run before every write)
Implemented: kicad_lib/verify.py (Seg/Pin/verify()) +
kicad_lib/geom.py (rot_xy, pin_world, snap) — mutation-tested by
tests/test_verify.py. Build your net model with these; do NOT rewrite
the verifier per-generator.
Mandatory checks over all generated segments + pins:
- Everything on the 1.27 mm grid; no diagonal segments.
- No two segments of DIFFERENT nets may touch, cross, T-touch, or overlap
collinearly (treat all
GND*/VCC* stub nets as one net each).
- Same-net collinear overlap (double-draw) and same-net crossing without a
shared endpoint are errors too.
- No pin of net A lying on a wire of net B.
- Every declared pin claimed by exactly one net; every label anchor on a
segment of its net.
- (Scaffold symbols) no wire through a symbol body bbox.
- Junction emission: ≥3 connection items at a point, or endpoint/pin on a
same-net segment interior.
Float hygiene: round(coord, 3) everywhere — 160.02 + 7.62 produces
167.64000000000001 which the clash checks will flag against 167.64.
Mutation-test the verifier once (plant a crossing, expect failure).
7. References and ERC
- References are global across ALL sheets of a project. Collision-check
against every
.kicad_sch in the project dir, not just the target file.
Duplicate refs silently cross-merge multi-unit symbols and corrupt ERC.
- GOTCHA:
kicad-cli sch erc does NOT catch duplicate designators or
unannotated symbols (R?) — even at --severity-all. They are
Annotation-tool checks (SCH_REFERENCE_LIST), not ERC violations, so a
headless ERC pass stays 0/0 green while two R1s cross-merge. Verify refs
with kx ref-audit FILE (multi-unit aware: U1A/U1B/U1C sharing U1 is
fine; a repeated unit or two lib_ids on one ref is the real dup) or the
GUI Annotate dialog — never trust headless ERC for annotation.
#PWR0NNN refs: pick an unused high block.
- ERC totals are noisy. Always: run ERC on the pre-edit baseline
(
git show HEAD:file > baseline.kicad_sch), normalize each violation to
type: @locations, and set-diff new vs baseline. Judge only the NEW
entries; report regrouping moves pre-existing items around.
- Expected-benign classes:
isolated_pin_label on block I/O, undriven
inputs awaiting later wiring, pre-existing unwired-MCU noise.
missing_unit/missing_input_pin: EVERY unit of a multi-unit part
(dual/quad opamp, gate pack) must be placed, AND a spare you don't use
still goes on a sheet + tied off (opamp: in+ → GND, in− → out; logic:
inputs to a defined level) or its inputs throw missing_input_pin.
kx unit-audit FILE lists placed-vs-expected units per ref (reads the
count from lib_symbols) and front-runs ERC; ERC is the authority on
tie-off. (kicad.info unused-pin threads; KLC S4.5)
- No-connect discipline: a genuinely unused pin needs a
(no_connect …)
flag to silence pin_not_connected — DOCUMENT the intent, never lower
severity. Inverse trap no_connect_connected: a NC flag on a pin/node
that IS wired (flag and wiring contradict) — delete the flag OR the wire,
not both. NC means "nothing else attaches here"; one ERC entry lands at
the flag, one at the connected pin. (kicad.info t/46229, t/21294)
- The exported netlist is the ground truth for connectivity: parse
(net (name)(node (ref)(pin))) blocks and assert each designed net has
exactly the expected members. Do this at least once per block.
power_pin_not_driven ("Input power pin not driven by output power
pins") is a missing DRIVER DECLARATION, not a wiring fault. Each
power/ground rail needs one driver: a power_out pin (regulator) OR a
power:PWR_FLAG. Fix by adding PWR_FLAG — NOT a ground symbol like
PWRGND (name trap: PWRGND is just a GND graphic; PWR_FLAG declares the
net driven) — at the rail's PASSIVE source (connector / battery /
regulator INPUT). Never flag a regulator OUTPUT (already power_out;
flagging it can trip "two outputs"). One flag per rail.
- GOTCHA:
kicad-cli sch export netlist drops PWR_FLAG and power-symbol
nodes, so a flagged and a flag-stripped schematic have IDENTICAL
netlist driver content (both zero power_out) — the netlist cannot see
this class of error. NEVER audit power drivers from the netlist; use
kx power-audit FILE (reads PWR_FLAG instances from the schematic) plus
kicad-cli ERC (the authority). (kicad.info t/35552, t/57016)
8. Visual inspection (non-negotiable)
flatpak run --command=kicad-cli org.kicad.KiCad sch export svg --output svg_out --no-background-color FILE.kicad_sch
python3 -c "import cairosvg; cairosvg.svg2png(url='svg_out/FILE.svg', write_to='out.png', output_width=2600)"
# crop: px = x_mm/paper_w*img_w (A4 297x210, A3 420x297), then Read the PNG
Look for: symbol body overlap, value/ref text collisions (move per-ref via
a TEXT_POS override table), labels overlapping existing drawing text,
junction dots present, pins facing their wires. Fix and re-render until
clean. Matplotlib net-colored previews of the segment model help debug the
generator, but only the KiCad render proves the file.
9. Injecting into an existing schematic
- Checkpoint-commit first; idempotency =
git checkout -- file then rerun
the generator (uuids regenerate — re-derive anything uuid-dependent after).
- Splice lib symbols right after
(lib_symbols, body before
(sheet_instances; skip lib defs already cached.
- Reuse existing nets by label name (
out1, Vin_p, VCC) and existing
cached symbols (e.g. the project's own cap symbol) where possible.
- Bind MCU pins by adding a 5.08 mm stub wire + local label at the pin
endpoint (get pin world pos from the cached MCU symbol + instance
(at)).
Only bind pins the design doc justifies; leave the rest to the user.
10. PCB footprint staging (.kicad_pcb)
Goal: place footprints so KiCad's Update PCB from Schematic adopts them
(no duplicates) and fills nets:
- Export the netlist; for each ref take
(tstamps "<uuid>") from its
(comp ...) block — that uuid IS the link.
- Load
.kicad_mod, take the balanced (footprint ...) block, rename to
"Lib:Name", strip (version)/(generator*) headers, insert after the
layer clause: (uuid), (at X Y), (path "/<tstamp-uuid>"),
(sheetname "/"), (sheetfile "..."); set Reference/Value properties;
refresh all item uuids. Leave pads netless — sync fills them.
- Stage in a grid clear of the board outline (probe Edge.Cuts extent).
- Parse-check via
pcb export svg, render, eyeball, commit.
11. Live IPC editing (KiCad v11 nightly, GUI open)
When kx env reports ipc_alive: true + scope pcb+sch, edit the
schematic INSIDE the running eeschema instead of the file (the file is
LOCKED then — never write it). kipy master via repo .venv (bootstrap:
tools/bootstrap_kipy.sh); worked example tests/test_live_ipc.py:
from kipy import KiCad
import kipy.schematic_types as st
from kipy.geometry import Vector2 # nanometers: mm * 1e6
k = KiCad(socket_path="ipc:///tmp/kicad/api.sock")
sch = k.get_schematic() # the OPEN document
refs = [s.reference_field.text.value for s in sch.get_symbols()]
t = st.SchematicText(); t.value = "note"
t.position = Vector2.from_xy(50_800_000, 25_400_000)
c = sch.begin_commit(); sch.create_items(t); sch.push_commit(c, "msg")
- Every push_commit is a single undo step in the GUI — small definitive
steps map 1:1 onto user-visible, user-revertable edits.
- remove_items() deletes live. get_lines/get_labels/get_text mirror reads.
- PERSISTENCE GAP (nightly 10.99 standalone): save/revert are unhandled
over IPC — the USER saves. Verify expected state on disk afterwards
with kx probe / kx check. Full handler map: kicad-project skill.
kx live wraps this as VERIFIED atomic ops (kicad_lib/live_ops.py,
worked example tests/test_live_ops.py — re-execs the .venv itself):
kx live snap inventory + violations (mm, with ids)
kx live check geometric verifier on the live model
kx live wire X1 Y1 X2 Y2 verify -> push or refuse (exit 1+JSON)
kx live junction X Y | label X Y TEXT | text X Y TEXT
kx live rm ID... remove by KIID (from snap)
Net-blind gate: live backend sees wires not nets — grid/diagonal/zero/
overlap/T-junction checked; cross-net + pin rules only at file level
after save. A refusal sends NOTHING (GUI never sees invalid state);
a push is exactly one GUI undo step.
12. Done checklist
1---2name: kicad-schematic3description: Generate and modify KiCad schematics (.kicad_sch) and PCBs (.kicad_pcb) programmatically, with geometric verification, headless ERC, and rendered visual inspection. Use when asked to draw/implement/extend a schematic, inject circuit blocks into an existing KiCad project, stage footprints on a PCB, or verify schematic wiring/rotation. Trigger phrases: "draw in kicad", "create schematic", "implement schematic", "add to the schematic", "kicad_sch", "footprints on the pcb".4---56# KiCad schematic generation78**Engine:** `$(kx root)/kicad_lib/` — lossless sexp9parser (`sexp.py`, token-equal round-trip proven on 2.3 MB boards; byte-identity only for .kicad_mod — sch re-serializes with ~0.5% whitespace drift) and the10`kx` CLI (`python3 -m kicad_lib.cli probe|check FILE` from the repo root).11Prefer `kx probe` over ad-hoc grep for inventory; prefer `sexp.py` over12regex surgery for edits (`kx` is on PATH via ~/.local/bin). Persist with13`sexp.save_file(path, root)` — PATH FIRST. Python imports need14PYTHONPATH=<repo> or cwd=<repo>; the package is not pip-installed. Sibling skills: kicad-project (state machine),15kicad-component (find/fetch parts), kicad-review (diff/ERC), kicad-improve16(self-improvement loop). Design: `doc/DESIGN.md` in that repo.1718Battle-tested workflow for writing `.kicad_sch`/`.kicad_pcb` files from19Python without KiCad GUI. The engine and worked examples ship in this repo20(`kx root`):2122- `kicad_lib/ops.py` — standalone sheet, scaffold symbols, full-wire23 layout, driven by the built-in geometric verifier (`kicad_lib/verify.py`).24- `kicad_lib/live_ops.py` — inject a block into an EXISTING schematic using25 real library symbols (extends-flattening, multi-unit parts, mirrors),26 label-stitched islands.27- `kicad_lib/pcb.py` — stage footprints in the PCB, path-linked to28 schematic symbols so Update-PCB adopts them. See `tests/test_*.py` for29 runnable end-to-end usage against `tests/fixtures/`.3031## 0. The loop (never skip a stage)32331. **Probe** target file + libraries (uuids, pin geometry, occupancy, refs).342. **Generate** with explicit coordinates; run the **geometric verifier**.353. **ERC headless**; compare against a **baseline run as a violation set**.364. **Render SVG → PNG → look at it.** Geometry that passes checks can still37 be unreadable (text collisions, symbol overlap). Iterate.385. Commit per slice. Checkpoint-commit the project BEFORE first modification.3940## 1. Environment4142KiCad 10 is a flatpak (`org.kicad.KiCad`); there is no host `kicad-cli`:4344```bash45flatpak run --command=kicad-cli org.kicad.KiCad sch erc --output erc.rpt --severity-error FILE.kicad_sch46flatpak run --command=kicad-cli org.kicad.KiCad sch export svg --output OUTDIR --no-background-color FILE.kicad_sch47flatpak run --command=kicad-cli org.kicad.KiCad sch export netlist --output out.net FILE.kicad_sch48flatpak run --command=kicad-cli org.kicad.KiCad pcb export svg --output out.svg --layers F.Cu,B.Cu,Edge.Cuts,F.SilkS FILE.kicad_pcb49```5051- Flatpak `/tmp` is sandboxed — outputs must land under `$HOME`.52- Official libs:53 `~/.local/share/flatpak/runtime/org.kicad.KiCad.Library.Symbols/x86_64/stable/active/files/symbols/*.kicad_sym`54 `~/.local/share/flatpak/runtime/org.kicad.KiCad.Library.Footprints/x86_64/stable/active/files/footprints/*.pretty/*.kicad_mod`55- Rasterize SVG with `cairosvg` (installed), crop with PIL, then Read the PNG.56- If a project lock file (`~NAME.kicad_pro.lck`) exists, KiCad is open —57 warn the user to close/reload before and after edits.5859## 2. File format essentials (.kicad_sch, format 20260306, generator "eeschema" 10.0)6061Top-level order: `(kicad_sch (version) (generator) (generator_version)62(uuid) (paper) (lib_symbols ...) <junctions/wires/labels/symbols/sheets>63(sheet_instances ...) (embedded_fonts no))`. Order of body items is loose.6465- **Preserve the existing file uuid** (it's the root sheet uuid; instance66 paths reference it). For a sub-sheet keep the uuid KiCad created.67- Every placed symbol needs `(instances (project "NAME" (path "/<ROOT_UUID>"68 (reference "R1") (unit 1))))`. Sub-sheet instance paths are69 `/<root_uuid>/<sheet_symbol_uuid>`.70- `lib_symbols` is a cache of full symbol definitions named `"Lib:Name"`.71 Wires connect ONLY at exact pin endpoints — geometry comes from the cache.72- Wire: `(wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width 0) (type default))73 (uuid ...))`. Junction: `(junction (at x y) (diameter 0) (color 0 0 0 0)74 (uuid ...))`.75- Labels: local `(label "net" (at x y rot) ...)`, global `(global_label76 "net" (shape input|output) ...)`, hierarchical `(hierarchical_label ...)`77 (sub-sheets only; on a root sheet use local/global).78- Paren-balance check before writing; strings may contain parens — the79 balanced extractor must be quote-aware.8081## 3. Coordinates, rotation, mirror8283World coords: mm, **+Y down**. Library pin coords: **+Y up**. Everything84(symbol origins AND wire endpoints) must sit on the **1.27 mm grid** or85wires will not connect.8687Pin world position for a symbol at `(x, y, rot)`, lib pin offset `(u, v)`:8889```python90def rot_xy(u, v, rot, mirror=""):91 if mirror == "x": v = -v # use mirror only with rot 0 (unambiguous)92 if rot == 0: return (u, -v)93 if rot == 90: return (-v, -u)94 if rot == 180: return (-u, v)95 if rot == 270: return (v, u)96# world = (x + dx, y + dy)97```9899- `Device:R` is **vertical** at rot 0 (pin1 top); rot 90 → pin1 left.100- KiCad PNPs draw emitter at bottom; `(mirror x)` flips it up (idiomatic).101- **Property text angle is RELATIVE to symbol rotation** — for a rot-90/270102 symbol set property angle 90 so the text renders horizontal.103- Real symbol heights differ from scratch-built ones — transistor circles104 are ~11 mm; totem pairs need ≥15.24 mm center spacing.105106## 4. Sourcing symbols107108**Real library symbols (preferred — "select real components"):**109- Extract `(symbol "NAME" ...)` blocks from the lib with a balanced parser.110- Resolve `(extends "BASE")`: take the base body, apply the derived111 symbol's property overrides, rename `BASE_X_Y` sub-units to `NAME_X_Y`,112 then prefix the top name to `"Lib:NAME"` for the cache.113- Cache `Value` property must equal the bare symbol name, else ERC reports114 `lib_symbol_mismatch` against the configured library.115- Parse pin positions per unit from `(pin ... (at x y a) ... (number "N"))`.116- Multi-unit parts (LM393 = unit1 A, unit2 B, unit3 power): one reference,117 several `(symbol ...)` instances each with its own `(unit N)`; all units118 carry identical properties. One dual comparator serves two channels.119- Watch coincident pins (e.g. DMP3013SFV has 3 drain pins at one point —120 all get the same net, emit all pin uuid entries).121122**Scaffold symbols (standalone sheets / quick drafts):** define your own123minimal R/C/Q/M/opamp boxes in `lib_symbols` so pin geometry is fully under124your control. Sub-unit names inside a definition are bare (`"R_0_1"`, never125`"lib:R_0_1"`). Power symbol header is `(power global)`. If KiCad will open126the project standalone, also emit `scaffold.kicad_sym` + a project127`sym-lib-table` entry (URI `${KIPRJMOD}/scaffold.kicad_sym`) or ERC warns.128129## 5. Placement and routing130131- Describe nets as **polylines of waypoints**, where a waypoint is either a132 literal point or `("ref","pin")`; consecutive points become axis-parallel133 wire segments. Skip zero-length segments.134- **Full wires** for a self-contained sheet; **label-stitched islands**135 (driver island / power island / comparator island joined by short local136 labels) when injecting into a crowded page — labels merge nets with the137 existing drawing (`out1`, `Vin_p`, `VCC`...) for free.138- Probe free space BEFORE placing: collect `(at ...)`/`(xy ...)` coords into139 a coarse occupancy grid — but remember it misses label TEXT extents and140 sheet boxes; always confirm against a render. Probe `(sheet (at)(size))`141 blocks explicitly. Title block ≈ bottom-right 110×30 mm — keep clear.142- If the page is full, **grow the paper** (A4 → A3); existing content keeps143 coordinates.144- T-junctions: a wire endpoint or pin touching another wire's interior DOES145 connect; emit a `(junction ...)` there. Pins may sit pin-on-pin or146 pin-on-wire-interior — both connect.147- GND/VCC: one `power:GND`/`power:VCC` symbol per drop, short stub wire to148 the pin. Standalone sheets need one `PWR_FLAG` (power_out pin) on each149 passive-driven rail or ERC raises `power_pin_not_driven`.150151## 6. Geometric verifier (run before every write)152153**Implemented:** `kicad_lib/verify.py` (`Seg`/`Pin`/`verify()`) +154`kicad_lib/geom.py` (`rot_xy`, `pin_world`, `snap`) — mutation-tested by155`tests/test_verify.py`. Build your net model with these; do NOT rewrite156the verifier per-generator.157158Mandatory checks over all generated segments + pins:1591601. Everything on the 1.27 mm grid; no diagonal segments.1612. No two segments of DIFFERENT nets may touch, cross, T-touch, or overlap162 collinearly (treat all `GND*`/`VCC*` stub nets as one net each).1633. Same-net collinear overlap (double-draw) and same-net crossing without a164 shared endpoint are errors too.1654. No pin of net A lying on a wire of net B.1665. Every declared pin claimed by exactly one net; every label anchor on a167 segment of its net.1686. (Scaffold symbols) no wire through a symbol body bbox.1697. Junction emission: ≥3 connection items at a point, or endpoint/pin on a170 same-net segment interior.171172Float hygiene: `round(coord, 3)` everywhere — `160.02 + 7.62` produces173`167.64000000000001` which the clash checks will flag against `167.64`.174**Mutation-test the verifier once** (plant a crossing, expect failure).175176## 7. References and ERC177178- **References are global across ALL sheets of a project.** Collision-check179 against every `.kicad_sch` in the project dir, not just the target file.180 Duplicate refs silently cross-merge multi-unit symbols and corrupt ERC.181- GOTCHA: `kicad-cli sch erc` does **NOT** catch duplicate designators or182 unannotated symbols (`R?`) — even at `--severity-all`. They are183 Annotation-tool checks (`SCH_REFERENCE_LIST`), not ERC violations, so a184 headless ERC pass stays 0/0 green while two `R1`s cross-merge. Verify refs185 with `kx ref-audit FILE` (multi-unit aware: U1A/U1B/U1C sharing `U1` is186 fine; a repeated unit or two lib_ids on one ref is the real dup) or the187 GUI Annotate dialog — never trust headless ERC for annotation.188- `#PWR0NNN` refs: pick an unused high block.189- ERC totals are noisy. Always: run ERC on the **pre-edit baseline**190 (`git show HEAD:file > baseline.kicad_sch`), normalize each violation to191 `type: @locations`, and **set-diff** new vs baseline. Judge only the NEW192 entries; report regrouping moves pre-existing items around.193- Expected-benign classes: `isolated_pin_label` on block I/O, undriven194 inputs awaiting later wiring, pre-existing unwired-MCU noise.195- `missing_unit`/`missing_input_pin`: EVERY unit of a multi-unit part196 (dual/quad opamp, gate pack) must be placed, AND a spare you don't use197 still goes on a sheet + tied off (opamp: in+ → GND, in− → out; logic:198 inputs to a defined level) or its inputs throw `missing_input_pin`.199 `kx unit-audit FILE` lists placed-vs-expected units per ref (reads the200 count from lib_symbols) and front-runs ERC; ERC is the authority on201 tie-off. (kicad.info unused-pin threads; KLC S4.5)202- No-connect discipline: a genuinely unused pin needs a `(no_connect …)`203 flag to silence `pin_not_connected` — DOCUMENT the intent, never lower204 severity. Inverse trap `no_connect_connected`: a NC flag on a pin/node205 that IS wired (flag and wiring contradict) — delete the flag OR the wire,206 not both. NC means "nothing else attaches here"; one ERC entry lands at207 the flag, one at the connected pin. (kicad.info t/46229, t/21294)208- The exported **netlist is the ground truth** for connectivity: parse209 `(net (name)(node (ref)(pin)))` blocks and assert each designed net has210 exactly the expected members. Do this at least once per block.211- **`power_pin_not_driven` ("Input power pin not driven by output power212 pins") is a missing DRIVER DECLARATION, not a wiring fault.** Each213 power/ground rail needs one driver: a `power_out` pin (regulator) OR a214 `power:PWR_FLAG`. Fix by adding PWR_FLAG — NOT a ground symbol like215 PWRGND (name trap: PWRGND is just a GND graphic; PWR_FLAG declares the216 net driven) — at the rail's PASSIVE source (connector / battery /217 regulator INPUT). Never flag a regulator OUTPUT (already `power_out`;218 flagging it can trip "two outputs"). One flag per rail.219- GOTCHA: `kicad-cli sch export netlist` **drops PWR_FLAG and power-symbol220 nodes**, so a flagged and a flag-stripped schematic have IDENTICAL221 netlist driver content (both zero `power_out`) — the netlist cannot see222 this class of error. NEVER audit power drivers from the netlist; use223 `kx power-audit FILE` (reads PWR_FLAG instances from the schematic) plus224 kicad-cli ERC (the authority). (kicad.info t/35552, t/57016)225226## 8. Visual inspection (non-negotiable)227228```bash229flatpak run --command=kicad-cli org.kicad.KiCad sch export svg --output svg_out --no-background-color FILE.kicad_sch230python3 -c "import cairosvg; cairosvg.svg2png(url='svg_out/FILE.svg', write_to='out.png', output_width=2600)"231# crop: px = x_mm/paper_w*img_w (A4 297x210, A3 420x297), then Read the PNG232```233234Look for: symbol body overlap, value/ref text collisions (move per-ref via235a TEXT_POS override table), labels overlapping existing drawing text,236junction dots present, pins facing their wires. Fix and re-render until237clean. Matplotlib net-colored previews of the segment model help debug the238generator, but only the KiCad render proves the file.239240## 9. Injecting into an existing schematic241242- Checkpoint-commit first; idempotency = `git checkout -- file` then rerun243 the generator (uuids regenerate — re-derive anything uuid-dependent after).244- Splice lib symbols right after `(lib_symbols`, body before245 `(sheet_instances`; skip lib defs already cached.246- Reuse existing nets by label name (`out1`, `Vin_p`, `VCC`) and existing247 cached symbols (e.g. the project's own cap symbol) where possible.248- Bind MCU pins by adding a 5.08 mm stub wire + local label at the pin249 endpoint (get pin world pos from the cached MCU symbol + instance `(at)`).250 Only bind pins the design doc justifies; leave the rest to the user.251252## 10. PCB footprint staging (.kicad_pcb)253254Goal: place footprints so KiCad's **Update PCB from Schematic** adopts them255(no duplicates) and fills nets:2562571. Export the netlist; for each ref take `(tstamps "<uuid>")` from its258 `(comp ...)` block — that uuid IS the link.2592. Load `.kicad_mod`, take the balanced `(footprint ...)` block, rename to260 `"Lib:Name"`, strip `(version)/(generator*)` headers, insert after the261 layer clause: `(uuid)`, `(at X Y)`, `(path "/<tstamp-uuid>")`,262 `(sheetname "/")`, `(sheetfile "...")`; set Reference/Value properties;263 refresh all item uuids. Leave pads netless — sync fills them.2643. Stage in a grid clear of the board outline (probe Edge.Cuts extent).2654. Parse-check via `pcb export svg`, render, eyeball, commit.266267## 11. Live IPC editing (KiCad v11 nightly, GUI open)268269When `kx env` reports `ipc_alive: true` + scope `pcb+sch`, edit the270schematic INSIDE the running eeschema instead of the file (the file is271LOCKED then — never write it). kipy master via repo `.venv` (bootstrap:272`tools/bootstrap_kipy.sh`); worked example `tests/test_live_ipc.py`:273274 from kipy import KiCad275 import kipy.schematic_types as st276 from kipy.geometry import Vector2 # nanometers: mm * 1e6277 k = KiCad(socket_path="ipc:///tmp/kicad/api.sock")278 sch = k.get_schematic() # the OPEN document279 refs = [s.reference_field.text.value for s in sch.get_symbols()]280 t = st.SchematicText(); t.value = "note"281 t.position = Vector2.from_xy(50_800_000, 25_400_000)282 c = sch.begin_commit(); sch.create_items(t); sch.push_commit(c, "msg")283284- Every push_commit is a single undo step in the GUI — small definitive285 steps map 1:1 onto user-visible, user-revertable edits.286- remove_items() deletes live. get_lines/get_labels/get_text mirror reads.287- PERSISTENCE GAP (nightly 10.99 standalone): save/revert are unhandled288 over IPC — the USER saves. Verify expected state on disk afterwards289 with kx probe / kx check. Full handler map: kicad-project skill.290291`kx live` wraps this as VERIFIED atomic ops (kicad_lib/live_ops.py,292worked example tests/test_live_ops.py — re-execs the .venv itself):293294 kx live snap inventory + violations (mm, with ids)295 kx live check geometric verifier on the live model296 kx live wire X1 Y1 X2 Y2 verify -> push or refuse (exit 1+JSON)297 kx live junction X Y | label X Y TEXT | text X Y TEXT298 kx live rm ID... remove by KIID (from snap)299300Net-blind gate: live backend sees wires not nets — grid/diagonal/zero/301overlap/T-junction checked; cross-net + pin rules only at file level302after save. A refusal sends NOTHING (GUI never sees invalid state);303a push is exactly one GUI undo step.304305## 12. Done checklist306307- [ ] Geometric verifier: 0 violations (and it failed when mutation-tested).308- [ ] ERC set-diff vs baseline: no unexplained NEW entries.309- [ ] Netlist spot-check: designed nets have exactly the expected members.310- [ ] Rendered PNG inspected at readable zoom; no overlaps.311- [ ] Refs unique across every sheet file in the project.312- [ ] Paren balance + file written with original uuid/paper preserved.313- [ ] Checkpoint commit before, one commit per slice after; scratch outputs314 (svg_out/, *.rpt, *.net, *.png) cleaned or ignored, never committed.