Back to skills
SKILL.md
Kicad Schematic Generator
ASecurityGenerate complete, ERC-clean KiCad 9/10 schematic projects from Python (hierarchical sheets, custom multi-unit symbols, BGA footprints) and verify them with kicad-cli ERC, netlist checks and PDF review.
- 2 stars
- 0 votes
- 0 copies
- 0 views
- Added October 9, 2026
Works with
Security analysis
100/100Pro scans all 3 files and shows the line behind each finding
npx -y skills add Novin3dp/claude-KiCad-pcb-designer-skill --skill kicad-schematic-generator --agent claude-codeAre you the author of Kicad Schematic Generator?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/novin3dp-kicad-schematic-generator)---
name: "kicad-schematic-generator"
description: "Generate complete, ERC-clean KiCad 9/10 schematic projects from Python (hierarchical sheets, custom multi-unit symbols, BGA footprints) and verify them with kicad-cli ERC, netlist checks and PDF review."
---
# KiCad schematic generator
Use this when the user wants a real KiCad schematic (not just a description) for a board — SBCs, MCU boards,
power supplies, anything. The schematic is produced by a Python script, so it is reproducible, diffable and
easy to change when a part is swapped. The user usually writes in Persian: answer in the user's language,
keep net names, part numbers and pin names in English.
## Principles
1. **Never invent pinouts.** Every IC pin number comes from a datasheet, a vendor/reference schematic, or the
stock KiCad library. For big parts (BGA SoCs, DDR) parse the datasheet ball table with `pdftotext -layout`
and cross-check it against a second independent source (another KiCad symbol on GitHub, the datasheet's
ball-map appendix). Record the source of each custom symbol in the code.
2. **Prefer stock KiCad library symbols/footprints** (`/usr/share/kicad/symbols`, `/usr/share/kicad/footprints`);
their pin numbers already match their footprints. Only draw custom symbols for parts not in the library.
3. **Start from reference designs.** For SoCs look for open schematics (linux-sunxi.org hosts Orange Pi /
Banana Pi / NanoPi PDFs; MediaWiki image path = `/images/<md5[0]>/<md5[0:2]>/<File_Name.pdf>` where md5 is of
the file name with underscores). Render pages with `pdftoppm -r 130..300` and read them visually; crop and
zoom to read pin numbers. Copy the proven support circuitry (strapping, references, bias resistors, power tree).
4. **Local availability matters.** Ask/check where the user buys parts; if a part is unobtainable, swap it for
an available pin-compatible or functionally equivalent one and recompute dividers/values.
5. **Verify, don't trust.** Deliver only after ERC = 0 errors/0 warnings, netcheck = 0 problems, design-specific
assertions pass, and the PDF pages were looked at.
## Setup (Ubuntu 24.04 container)
Install the **same KiCad major version the user has** (ask if unknown). The generator embeds symbols copied
from the installed library, so the output matches that version's libraries; files built with KiCad 10 libraries
contain tokens KiCad 9 may not read. KiCad 7 from the distro has no `kicad-cli sch erc`.
```bash
V=10.0 # or 9.0
curl -s "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x245D5502FAD7A805" | gpg --dearmor > /etc/apt/trusted.gpg.d/kicad.gpg
echo "deb https://ppa.launchpadcontent.net/kicad/kicad-$V-releases/ubuntu noble main" > /etc/apt/sources.list.d/kicad.list
apt-get update -q && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends kicad kicad-symbols kicad-footprints poppler-utils
mkdir -p ~/.config/kicad/$V && cp /usr/share/kicad/template/{sym,fp}-lib-table ~/.config/kicad/$V/
```
Then write `gen/kigen.py` and `gen/netcheck.py` exactly as given at the end of this skill.
## Project layout
```
<project>/
gen/kigen.py generator library (below)
gen/netcheck.py generic netlist checker (below)
gen/symbols.py custom symbols (make_ic) + pin data files (json) with their sources
gen/build.py the design: one section per sheet
gen/verify.py design-specific assertions on the netlist
gen/gen_fp.py custom footprints (e.g. BGA) if the library lacks them
kicad/ generated: <name>.kicad_pro/.kicad_sch, sub-sheets, <lib>.kicad_sym, <lib>.pretty, sym/fp-lib-table
<name>_schematic.pdf, <name>_bom.csv, <name>.net, README.md
```
## How kigen works
- `load_lib(lib, name)` copies a stock symbol (flattens `extends`). Connect pins by **pin number or pin name**;
pins named `~` must use numbers.
- `make_ic(lib_id, units=[{name, left:[(num,name,etype)|None], right:[...]}], footprint=...)` builds a custom
(multi-unit) symbol. Use only left/right pins; `None` inserts a gap. Electrical types: `power_in`, `power_out`,
`input`, `output`, `bidirectional`, `passive`, `open_collector`, `no_connect`.
- `Project` holds sheets; `Sheet(name, file, title)`; `Flow(sheet, x0, y0, width)` auto-places parts in rows:
`fl.section(title, note)`, `fl.add(symdef, ref, value, conns, unit=, rot=, footprint=, fields=, dnp=)`, `fl.newrow()`.
- `conns` must list **every** pin: a net name or `'NC'` (no-connect flag). Missing/unknown pins raise — this is
what guarantees completeness. Every pin gets a wire stub + label; nets used on >1 sheet become global labels
automatically; paper size is picked from content (A4..A0).
- Passives: place with `rot=90` (pin 1 left, pin 2 right) for readable horizontal R/C/L.
- `P.write(outdir)` writes root sheet + sub-sheets + .kicad_pro. Also write the custom symbol lib and
`sym-lib-table`/`fp-lib-table` (see snippet below).
Typical build.py skeleton:
```python
import os, sys; sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from kigen import *
P = Project('myboard')
P.refcount['U'] = 1 # reserve U1 when a multi-unit IC is placed with a fixed ref
R_ = load_lib('Device', 'R'); C_ = load_lib('Device', 'C'); FLAG = load_lib('power', 'PWR_FLAG')
def R(fl, v, a, b): return fl.add(R_, P.ref('R'), v, {'1': a, '2': b}, rot=90, footprint='Resistor_SMD:R_0402_1005Metric')
sh = P.add_sheet(Sheet('Power', 'power.kicad_sch', '12V -> 5V')); fl = Flow(sh, 20, 25, 385)
fl.section('5V BUCK', 'Vout = 0.8 x (1 + 52.3k/10k) = 4.98 V')
... fl.add(...) ...
fl.add(FLAG, '#FLG01', 'PWR_FLAG', {'1': 'VCC_5V'}) # one PWR_FLAG per rail + GND
P.write('../kicad')
lib = ['(kicad_symbol_lib (version 20231120) (generator "kigen") (generator_version "9.0")']
for sd in CUSTOM: lib.append(sd.text.replace('(symbol "mylib:', '(symbol "', 1))
open('../kicad/mylib.kicad_sym', 'w').write('\n'.join(lib + [')']))
open('../kicad/sym-lib-table', 'w').write('(sym_lib_table (version 7)\n (lib (name "mylib")(type "KiCad")(uri "${KIPRJMOD}/mylib.kicad_sym")(options "")(descr ""))\n)\n')
open('../kicad/fp-lib-table', 'w').write('(fp_lib_table (version 7)\n (lib (name "mylib")(type "KiCad")(uri "${KIPRJMOD}/mylib.pretty")(options "")(descr ""))\n)\n')
```
## Workflow
1. Clarify requirements only if expensive to redo (interfaces, counts, voltages, output form, KiCad version). Track steps with a task list.
2. Collect pin data + reference schematics (principles 1 and 3). Save pin tables as json next to the code.
3. Write symbols.py, then build.py sheet by sheet (power input, rails, SoC units, memory, storage, radios,
connectors, IO). Put design equations and assumptions as `fl.section` notes on the sheet itself.
4. Build and verify:
```bash
cd gen && python3 build.py && cd ../kicad
kicad-cli sch erc -o erc.rpt --severity-all <name>.kicad_sch && tail -1 erc.rpt
kicad-cli sch export netlist -o ../<name>.net <name>.kicad_sch
python3 ../gen/netcheck.py ../<name>.net --local-fp mylib=mylib.pretty
python3 ../gen/verify.py
kicad-cli sch export pdf -o ../<name>_schematic.pdf <name>.kicad_sch
kicad-cli sch export bom --fields 'Reference,Value,Footprint,MPN,${QUANTITY}' --labels 'Refs,Value,Footprint,MPN,Qty' --group-by 'Value,Footprint' --exclude-dnp -o ../<name>_bom.csv <name>.kicad_sch
```
5. verify.py (import `netcheck.load`) should assert the things ERC cannot see: SoC pin -> net mapping against
the reference design, connector pinouts pin by pin, memory byte lanes on one chip / address to all chips,
every power ball on the right rail, and **regulator output voltages computed from the actual divider values
in the netlist** (`Vref*(1+Rtop/Rbot)`), current-limit resistor values.
6. Render every PDF page (`pdftoppm -r 55`) and look; zoom crops at 150-200 dpi to check overlaps/readability.
7. Deliver: zip the project + PDF with SendUserFile, README in the user's language listing sheets, what was
verified (and with which KiCad version), and an honest "before fabrication" list (missing footprints,
compensation to confirm, crystal CL, RF tuning, power budget, PCB not done).
## Pitfalls already hit (keep checking for them)
- ERC does **not** reliably catch a reference used twice on different sheets — netcheck/BOM does. Reserve
fixed refs (`P.refcount['U'] = 1`).
- Pin number vs pin name collisions (e.g. DDR3 ball `A7` vs address pin `A7`): map such parts **by number only**.
- Parallel `power_out` pins (multi-LX regulators) or a PWR_FLAG on a net already driven by `power_out` give
pin_to_pin errors: make LX pins `passive`, drop the flag there.
- Nets with only `power_in` pins need a PWR_FLAG (each rail + GND).
- Strings must escape newlines (`q()` does); text lines 2.54 mm apart (grid snapping merges closer lines).
- Stacked hidden pins (e.g. extra GND/EP) share one label automatically; hidden NC pins may be left out.
- Local nets appear in the netlist as `/Sheet Name/NET`; compare on the last path component.
- Library pin types differ from reality sometimes (e.g. TPS3808 CT, TPD4E05U06 NC pass-through pins): read the
symbol's pins with `load_lib(...).pins` before connecting.
- Library pin numbers change between KiCad versions (KiCad 10: USB_A shield `5` -> `SH`, Micro_SD_Card_Det1
shield `10` -> `SH`). Connect by pin **name** where possible and always regenerate + rerun netcheck after a
KiCad upgrade; a schematic built on older libraries shows `lib_symbol_mismatch` and can lose pad links.
- KiCad 10 writes the netlist pretty-printed over many lines; parse it after collapsing whitespace (netcheck does).
- A site that curl/WebFetch cannot reach can often still be listed via `WebSearch` with `site:` queries.
- Skills from third-party repos: read them fully before trusting their output (one "PCB pipeline" skill faked
DRC/"manufacturing ready" reports when KiCad was absent).
## File: gen/kigen.py (write verbatim)
```python
"""kigen - minimal KiCad 8/9 schematic generator (label-connected, ERC-clean output).
Every symbol pin is connected by a short wire stub + a net label (local label if the
net only lives on one sheet, global label otherwise) or marked no-connect.
Library symbols are copied (and flattened) from the stock KiCad libraries; custom
symbols are generated from pin lists.
"""
import re, uuid, math, os, hashlib
KLIB = os.environ.get('KICAD_SYMBOL_DIR', '/usr/share/kicad/symbols')
PROJECT = os.environ.get('KIGEN_PROJECT', 'project')
COMPANY = os.environ.get('KIGEN_COMPANY', '')
DATE = os.environ.get('KIGEN_DATE', '')
GRID = 2.54
def uid(seed=None):
if seed is None:
return str(uuid.uuid4())
h = hashlib.md5(seed.encode()).hexdigest()
return str(uuid.UUID(h))
def q(s):
return '"' + str(s).replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + '"'
def snap(v):
return round(round(v / 1.27) * 1.27, 4)
# ----------------------------------------------------------------------------
# s-expression helpers (only what we need to lift symbols out of .kicad_sym)
def find_block(text, start):
d = 0
instr = False
i = start
while i < len(text):
c = text[i]
if instr:
if c == '\\':
i += 2
continue
if c == '"':
instr = False
else:
if c == '"':
instr = True
elif c == '(':
d += 1
elif c == ')':
d -= 1
if d == 0:
return text[start:i + 1]
i += 1
raise ValueError('unbalanced')
class Pin:
def __init__(self, num, name, x, y, ang, etype, length=2.54, unit=1, hidden=False):
self.num, self.name, self.x, self.y, self.ang = num, name, x, y, ang
self.etype, self.length, self.unit, self.hidden = etype, length, unit, hidden
class SymDef:
"""A symbol definition ready to be embedded into lib_symbols."""
def __init__(self, lib_id, text, pins, units=1, bbox=None, power=False):
self.lib_id = lib_id
self.text = text # full '(symbol "lib_id" ...)' text
self.pins = pins # list[Pin]
self.units = units
self.bbox = bbox # per unit: {unit: (xmin,ymin,xmax,ymax)} lib coords
self.power = power
def unit_pins(self, unit):
return [p for p in self.pins if p.unit in (0, unit)]
_libcache = {}
def _libtext(lib):
if lib not in _libcache:
_libcache[lib] = open(os.path.join(KLIB, lib + '.kicad_sym')).read()
return _libcache[lib]
def _raw_symbol(lib, name):
t = _libtext(lib)
m = re.search(r'\n\t\(symbol ' + re.escape(q(name)), t)
if not m:
raise KeyError(lib + ':' + name)
return find_block(t, m.start() + 2)
def load_lib(lib, name):
"""Return a flattened SymDef for lib:name."""
raw = _raw_symbol(lib, name)
ext = re.search(r'\(extends "([^"]+)"\)', raw)
if ext:
parent = _raw_symbol(lib, ext.group(1))
# child's properties replace parent's
child_props = re.findall(r'\(property "([^"]+)"', raw)
body = parent
# rename top symbol + sub units
pn = ext.group(1)
body = body.replace('(symbol ' + q(pn), '(symbol ' + q(name), 1)
body = re.sub(r'\(symbol "' + re.escape(pn) + r'_(\d+)_(\d+)"', lambda m: '(symbol "%s_%s_%s"' % (name, m.group(1), m.group(2)), body)
# swap property blocks
for pr in child_props:
cm = re.search(r'\(property "' + re.escape(pr) + '"', raw)
cblk = find_block(raw, cm.start())
pm = re.search(r'\(property "' + re.escape(pr) + '"', body)
if pm:
pblk = find_block(body, pm.start())
body = body[:pm.start()] + cblk + body[pm.start() + len(pblk):]
else:
# insert after first line
k = body.find('(property')
body = body[:k] + cblk + '\n\t\t' + body[k:]
raw = body
lib_id = lib + ':' + name
text = raw.replace('(symbol ' + q(name), '(symbol ' + q(lib_id), 1)
pins = []
# units: sub symbols named name_U_S
for sm in re.finditer(r'\(symbol "' + re.escape(name) + r'_(\d+)_(\d+)"', raw):
unit = int(sm.group(1))
blk = find_block(raw, sm.start())
for pm in re.finditer(r'\(pin (\w+) (\w+)', blk):
pb = find_block(blk, pm.start())
at = re.search(r'\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)', pb)
ln = re.search(r'\(length ([-\d.]+)\)', pb)
nm = re.search(r'\(name "([^"]*)"', pb)
nu = re.search(r'\(number "([^"]*)"', pb)
hidden = bool(re.search(r'\(hide yes\)|\bhide\b', pb.split('(name')[0]))
pins.append(Pin(nu.group(1), nm.group(1), float(at.group(1)), float(at.group(2)),
int(float(at.group(3) or 0)), pm.group(1), float(ln.group(1)), unit, hidden))
units = max([p.unit for p in pins] + [1])
power = '(power)' in raw
# bbox from graphics (rectangles/polylines) + pins
bb = {}
for u in range(0, units + 1):
bb[u] = None
return SymDef(lib_id, text, pins, units, None, power)
# ----------------------------------------------------------------------------
# custom symbol builder
def make_ic(lib_id, units, ref='U', value='', footprint='', datasheet='', desc='', pin_len=2.54, min_w=15.24):
"""units: list of dict(name=str, left=[(num,name,etype)|None...], right=[...], top=[...], bottom=[...])
None entries leave a gap. Returns SymDef."""
pins = []
parts = []
bboxes = {}
for ui, u in enumerate(units, start=1):
L, R, T, B = u.get('left', []), u.get('right', []), u.get('top', []), u.get('bottom', [])
maxlen_l = max([len(p[1]) for p in L if p] + [0])
maxlen_r = max([len(p[1]) for p in R if p] + [0])
w = max(min_w, (maxlen_l + maxlen_r) * 1.05 + 5.08, (max(len(T), len(B)) + 1) * GRID)
w = math.ceil(w / (2 * GRID)) * 2 * GRID
rows = max(len(L), len(R), 1)
h = (rows + 2) * GRID
if T:
h += max(len(p[1]) for p in T if p) * 1.05 + 2.54
if B:
h += max(len(p[1]) for p in B if p) * 1.05 + 2.54
h = math.ceil(h / (2 * GRID)) * 2 * GRID
x0, x1 = -w / 2, w / 2
y1 = h / 2
y0 = -h / 2
# left/right pins start from top margin
top_off = (max(len(p[1]) for p in T if p) * 1.05 + 2.54) if T else 0
top_off = math.ceil(top_off / GRID) * GRID
for i, p in enumerate(L):
if p:
pins.append(Pin(p[0], p[1], x0 - pin_len, y1 - top_off - GRID * (i + 1), 0, p[2], pin_len, ui))
for i, p in enumerate(R):
if p:
pins.append(Pin(p[0], p[1], x1 + pin_len, y1 - top_off - GRID * (i + 1), 180, p[2], pin_len, ui))
n = len(T)
for i, p in enumerate(T):
if p:
xx = (i - (n - 1) / 2) * GRID
pins.append(Pin(p[0], p[1], snap(xx), y1 + pin_len, 270, p[2], pin_len, ui))
n = len(B)
for i, p in enumerate(B):
if p:
xx = (i - (n - 1) / 2) * GRID
pins.append(Pin(p[0], p[1], snap(xx), y0 - pin_len, 90, p[2], pin_len, ui))
bboxes[ui] = (x0, y0, x1, y1)
ptxt = []
for p in [pp for pp in pins if pp.unit == ui]:
ptxt.append('(pin %s line (at %.4g %.4g %d) (length %.4g) (name %s (effects (font (size 1.016 1.016)))) (number %s (effects (font (size 1.016 1.016)))))'
% (p.etype, p.x, p.y, p.ang, p.length, q(p.name), q(p.num)))
title = u.get('name', '')
body = '(rectangle (start %.4g %.4g) (end %.4g %.4g) (stroke (width 0.254) (type default)) (fill (type background)))' % (x0, y1, x1, y0)
if title:
body += ' (text %s (at 0 %.4g 0) (effects (font (size 1.27 1.27) bold)))' % (q(title), y0 + 1.905 + (B and (max(len(p[1]) for p in B if p) * 1.05 + 2.54) or 0))
short = lib_id.split(':')[1]
parts.append('(symbol %s %s %s)' % (q('%s_%d_1' % (short, ui)), body, ' '.join(ptxt)))
props = ('(property "Reference" %s (at 0 0 0) (effects (font (size 1.27 1.27))))'
'(property "Value" %s (at 0 0 0) (effects (font (size 1.27 1.27))))'
'(property "Footprint" %s (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))'
'(property "Datasheet" %s (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))'
'(property "Description" %s (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))') % (
q(ref), q(value or lib_id.split(':')[1]), q(footprint), q(datasheet), q(desc))
text = '(symbol %s (pin_names (offset 0.762)) (exclude_from_sim no) (in_bom yes) (on_board yes) %s %s)' % (
q(lib_id), props, ' '.join(parts))
return SymDef(lib_id, text, pins, len(units), bboxes)
# ----------------------------------------------------------------------------
# placement / geometry
def rot_vec(dx, dy_screen, rot):
"""rotate a screen-space vector by rot degrees CCW (screen, y down)."""
t = math.radians(rot)
c, s = round(math.cos(t)), round(math.sin(t))
return dx * c + dy_screen * s, -dx * s + dy_screen * c
class Placed:
pass
class Sheet:
def __init__(self, name, fname, title, paper='A3'):
self.name, self.fname, self.title, self.paper = name, fname, title, paper
self.uuid = uid('sheet:' + fname)
self.items = [] # placed symbols
self.labels = [] # (net, x, y, angle)
self.wires = []
self.ncs = []
self.texts = []
self.junctions = []
self.symdefs = {}
self.nets_here = set()
# -- symbols
def place(self, sd, ref, value, x, y, conns, unit=1, rot=0, footprint=None, fields=None, stub=2.54,
ref_pos=None, val_pos=None, dnp=False, in_bom=True):
x, y = snap(x), snap(y)
self.symdefs[sd.lib_id] = sd
P = Placed()
P.sd, P.ref, P.value, P.x, P.y, P.unit, P.rot = sd, ref, value, x, y, unit, rot
P.footprint = footprint
P.fields = fields or {}
P.dnp, P.in_bom = dnp, in_bom
pins = sd.unit_pins(unit)
conns = dict(conns)
used = set()
# group pins by location (stacked hidden pins share a label)
seen_loc = {}
xs, ys = [], []
for p in pins:
dx, dy = rot_vec(p.x, -p.y, rot)
px, py = snap(x + dx), snap(y + dy)
xs.append(px); ys.append(py)
key = p.num
net = conns.get(key, conns.get(p.name))
if key in conns:
used.add(key)
elif p.name in conns:
used.add(p.name)
if (px, py) in seen_loc:
if net is not None and seen_loc[(px, py)] != net:
raise ValueError('%s: stacked pin %s conflicts' % (ref, key))
continue
if net is None:
if p.hidden:
continue
raise ValueError('%s (%s) pin %s/%s unconnected - specify net or NC' % (ref, sd.lib_id, p.num, p.name))
seen_loc[(px, py)] = net
# outward direction in lib coords = opposite of pin angle
a = math.radians(p.ang)
odx, ody = -round(math.cos(a)), -round(math.sin(a)) # lib (y up)
sdx, sdy = rot_vec(odx, -ody, rot)
if net == 'NC':
self.ncs.append((px, py))
continue
ex, ey = snap(px + sdx * stub), snap(py + sdy * stub)
if stub:
self.wires.append((px, py, ex, ey))
ang = {(1, 0): 0, (-1, 0): 180, (0, -1): 90, (0, 1): 270}[(int(sdx), int(sdy))]
self.labels.append((net, ex, ey, ang))
self.nets_here.add(net)
extra = set(conns) - used
if extra:
raise ValueError('%s: unknown pins %s' % (ref, extra))
P.bbox = (min(xs), min(ys), max(xs), max(ys)) if xs else (x, y, x, y)
# reference / value positions
horiz = (P.bbox[2] - P.bbox[0]) >= (P.bbox[3] - P.bbox[1])
if ref_pos is None:
ref_pos = (x, y - 3.556) if (horiz and len(pins) <= 3) else ((x, P.bbox[1] - 3.81) if rot in (0, 180) else (x, P.bbox[1] - 2.54))
if val_pos is None:
val_pos = (x, y + 3.556) if (horiz and len(pins) <= 3) else ((x, P.bbox[3] + 3.81) if rot in (0, 180) else (x, P.bbox[3] + 2.54))
P.ref_pos, P.val_pos = ref_pos, val_pos
self.items.append(P)
return P
def text(self, s, x, y, size=1.27, bold=False):
self.texts.append((s, snap(x), snap(y), size, bold))
def label(self, net, x, y, ang=0):
self.labels.append((net, snap(x), snap(y), ang))
self.nets_here.add(net)
def content_extent(self):
xs, ys = [0], [0]
for P in self.items:
xs += [P.bbox[0], P.bbox[2], P.ref_pos[0], P.val_pos[0]]
ys += [P.bbox[1], P.bbox[3], P.ref_pos[1], P.val_pos[1]]
for (net, x, y, ang) in self.labels:
L = label_len(net)
dx = {0: L, 180: -L, 90: 0, 270: 0}[ang]
dy = {0: 0, 180: 0, 90: -L, 270: L}[ang]
xs += [x, x + dx]; ys += [y, y + dy]
for (s, x, y, size, bold) in self.texts:
w = max(len(l) for l in s.split('\n')) * size * 0.62
xs += [x, x + w]; ys += [y, y + size * 1.6 * (s.count('\n') + 1)]
return max(xs), max(ys)
def auto_paper(self):
mx, my = self.content_extent()
for name, w, h in (('A4', 297, 210), ('A3', 420, 297), ('A2', 594, 420), ('A1', 841, 594), ('A0', 1189, 841)):
if mx <= w - 12 and (my <= h - 45 or (mx <= w - 125 and my <= h - 12)):
return name
return 'A0'
# -- serialisation
def render(self, global_nets, root_uuid, page, project=PROJECT):
out = []
out.append('(kicad_sch (version 20231120) (generator "kigen") (generator_version "9.0")')
out.append(' (uuid %s)' % q(self.uuid))
self.paper = self.auto_paper()
if isinstance(self.paper, tuple):
out.append(' (paper "User" %.2f %.2f)' % self.paper)
else:
out.append(' (paper %s)' % q(self.paper))
out.append(' (title_block (title %s) (date %s) (rev "A") (company %s) (comment 1 %s))' % (
q(self.title), q(DATE), q(COMPANY), q('Sheet: ' + self.name)))
out.append(' (lib_symbols')
for sd in self.symdefs.values():
out.append(' ' + sd.text)
out.append(' )')
for (x1, y1, x2, y2) in self.wires:
out.append(' (wire (pts (xy %.4f %.4f) (xy %.4f %.4f)) (stroke (width 0) (type default)) (uuid %s))' % (x1, y1, x2, y2, q(uid())))
for (x, y) in self.ncs:
out.append(' (no_connect (at %.4f %.4f) (uuid %s))' % (x, y, q(uid())))
for (x, y) in self.junctions:
out.append(' (junction (at %.4f %.4f) (diameter 0) (color 0 0 0 0) (uuid %s))' % (x, y, q(uid())))
for (net, x, y, ang) in self.labels:
just = {0: 'left', 90: 'left', 180: 'right', 270: 'right'}[ang]
if net in global_nets:
out.append(' (global_label %s (shape bidirectional) (at %.4f %.4f %d) (fields_autoplaced yes) (effects (font (size 1.016 1.016)) (justify %s)) (uuid %s)'
' (property "Intersheetrefs" "${INTERSHEET_REFS}" (at %.4f %.4f 0) (effects (font (size 1.016 1.016)) (hide yes))))' % (q(net), x, y, ang, just, q(uid()), x, y))
else:
out.append(' (label %s (at %.4f %.4f %d) (fields_autoplaced yes) (effects (font (size 1.016 1.016)) (justify %s bottom)) (uuid %s))' % (q(net), x, y, ang, just, q(uid())))
for (s, x, y, size, bold) in self.texts:
out.append(' (text %s (exclude_from_sim no) (at %.4f %.4f 0) (effects (font (size %.3f %.3f)%s) (justify left top)) (uuid %s))' % (
q(s), x, y, size, size, ' bold' if bold else '', q(uid())))
for P in self.items:
sd = P.sd
fp = P.footprint if P.footprint is not None else ''
su = uid('sym:%s:%s:%d' % (self.fname, P.ref, P.unit))
props = []
fa = P.rot % 180
props.append('(property "Reference" %s (at %.4f %.4f %d) (effects (font (size 1.27 1.27))%s))' % (
q(P.ref), P.ref_pos[0], P.ref_pos[1], fa, ' (hide yes)' if sd.power else ''))
props.append('(property "Value" %s (at %.4f %.4f %d) (effects (font (size 1.27 1.27))))' % (q(P.value), P.val_pos[0], P.val_pos[1], fa))
props.append('(property "Footprint" %s (at %.4f %.4f 0) (effects (font (size 1.27 1.27)) (hide yes)))' % (q(fp), P.x, P.y))
props.append('(property "Datasheet" "" (at %.4f %.4f 0) (effects (font (size 1.27 1.27)) (hide yes)))' % (P.x, P.y))
for k, v in P.fields.items():
props.append('(property %s %s (at %.4f %.4f 0) (effects (font (size 1.27 1.27)) (hide yes)))' % (q(k), q(v), P.x, P.y))
pins = ' '.join('(pin %s (uuid %s))' % (q(p.num), q(uid())) for p in sd.unit_pins(P.unit))
out.append(' (symbol (lib_id %s) (at %.4f %.4f %d) (unit %d) (exclude_from_sim no) (in_bom %s) (on_board yes) (dnp %s) (uuid %s) %s %s'
' (instances (project %s (path %s (reference %s) (unit %d)))))' % (
q(sd.lib_id), P.x, P.y, P.rot, P.unit, 'yes' if P.in_bom else 'no', 'yes' if P.dnp else 'no', q(su), ' '.join(props), pins,
q(project), q('/' + root_uuid + '/' + self.uuid), q(P.ref), P.unit))
out.append(')')
return '\n'.join(out)
class Project:
def __init__(self, name=PROJECT):
self.name = name
self.sheets = []
self.root_uuid = uid('root')
self.refcount = {}
def ref(self, prefix):
self.refcount[prefix] = self.refcount.get(prefix, 0) + 1
return '%s%d' % (prefix, self.refcount[prefix])
def add_sheet(self, sh):
self.sheets.append(sh)
return sh
def write(self, outdir, root_title='Root'):
os.makedirs(outdir, exist_ok=True)
count = {}
for sh in self.sheets:
for n in sh.nets_here:
count[n] = count.get(n, 0) + 1
global_nets = {n for n, c in count.items() if c > 1}
self.global_nets = global_nets
for i, sh in enumerate(self.sheets, start=2):
open(os.path.join(outdir, sh.fname), 'w').write(sh.render(global_nets, self.root_uuid, i, self.name))
# root sheet
out = ['(kicad_sch (version 20231120) (generator "kigen") (generator_version "9.0")',
' (uuid %s)' % q(self.root_uuid), ' (paper "A3")',
' (title_block (title %s) (date %s) (rev "A") (company %s))' % (q(root_title), q(DATE), q(COMPANY)),
' (lib_symbols)']
cols = 4
for i, sh in enumerate(self.sheets):
cx, cy = 30 + (i % cols) * 90, 45 + (i // cols) * 45
out.append(' (sheet (at %.2f %.2f) (size 70 25) (fields_autoplaced yes) (stroke (width 0.1524) (type solid)) (fill (color 0 0 0 0.0000)) (uuid %s)'
' (property "Sheetname" %s (at %.2f %.2f 0) (effects (font (size 1.524 1.524)) (justify left bottom)))'
' (property "Sheetfile" %s (at %.2f %.2f 0) (effects (font (size 1.27 1.27)) (justify left top)))'
' (instances (project %s (path %s (page %s)))))' % (
cx, cy, q(sh.uuid), q(sh.name), cx, cy - 0.7, q(sh.fname), cx, cy + 25.6,
q(self.name), q('/' + self.root_uuid), q(str(i + 2))))
self._root_texts(out)
out.append(' (sheet_instances (path "/" (page "1")))')
out.append(')')
open(os.path.join(outdir, self.name + '.kicad_sch'), 'w').write('\n'.join(out))
# project file
open(os.path.join(outdir, self.name + '.kicad_pro'), 'w').write(PRO_TEMPLATE % {'name': self.name})
def _root_texts(self, out):
for (s, x, y, size) in getattr(self, 'root_texts', []):
out.append(' (text %s (exclude_from_sim no) (at %.2f %.2f 0) (effects (font (size %.2f %.2f)) (justify left top)) (uuid %s))' % (
q(s), x, y, size, size, q(uid())))
PRO_TEMPLATE = '''{
"meta": { "filename": "%(name)s.kicad_pro", "version": 1 },
"schematic": { "legacy_lib_dir": "", "legacy_lib_list": [] },
"sheets": [],
"boards": [],
"libraries": { "pinned_footprint_libs": [], "pinned_symbol_libs": [] },
"erc": {
"erc_exclusions": [],
"meta": { "version": 0 },
"pin_map": [],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"extra_units": "error",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"similar_labels": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "warning",
"unannotated": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error",
"footprint_link_issues": "ignore",
"footprint_filter": "ignore"
}
}
}
'''
# ----------------------------------------------------------------------------
# automatic flow layout
def label_len(net):
return 2.54 + len(net) * 0.95 + 3.2
def extent(sd, unit, rot, conns, stub=2.54):
"""bbox (relative to symbol origin, screen coords) including labels & ref/value text."""
xs, ys = [], []
for p in sd.unit_pins(unit):
dx, dy = rot_vec(p.x, -p.y, rot)
a = math.radians(p.ang)
odx, ody = -round(math.cos(a)), -round(math.sin(a))
sdx, sdy = rot_vec(odx, -ody, rot)
# body side: pin extends inward by length
bx, by = dx - sdx * p.length, dy - sdy * p.length
xs += [dx, bx]; ys += [dy, by]
net = conns.get(p.num, conns.get(p.name))
if net and net != 'NC' and not p.hidden:
L = stub + label_len(net)
xs.append(dx + sdx * L); ys.append(dy + sdy * L)
# label text height
if sdx:
ys += [dy - 1.5, dy + 1.5]
else:
xs += [dx - 1.5, dx + 1.5]
if not xs:
xs, ys = [-2.54, 2.54], [-2.54, 2.54]
x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
return (x0 - 1.27, y0 - 5.5, x1 + 1.27, y1 + 5.5)
class Flow:
def __init__(self, sheet, x0, y0, width, gap=3.81):
self.sh, self.x0, self.y0, self.w, self.gap = sheet, x0, y0, width, gap
self.cx, self.cy, self.rowh = x0, y0, 0
def newrow(self, extra=0):
if self.rowh or self.cx > self.x0:
self.cy += self.rowh + self.gap + extra
self.cx, self.rowh = self.x0, 0
def section(self, title, note=None):
self.newrow(2.54)
self.sh.text(title, self.x0, self.cy, size=2.0, bold=True)
self.cy += 4.5
if note:
for ln in note.split('\n'):
self.sh.text(ln, self.x0, self.cy, size=1.27)
self.cy += 2.54
self.cy += 1.0
def add(self, sd, ref, value, conns, unit=1, rot=0, **kw):
e = extent(sd, unit, rot, conns)
w, h = e[2] - e[0], e[3] - e[1]
if self.cx + w > self.x0 + self.w and self.cx > self.x0:
self.newrow()
x = self.cx - e[0]
y = self.cy - e[1]
P = self.sh.place(sd, ref, value, x, y, conns, unit=unit, rot=rot, **kw)
self.cx += w + self.gap
self.rowh = max(self.rowh, h)
return P
def bottom(self):
return self.cy + self.rowh
```
## File: gen/netcheck.py (write verbatim)
```python
#!/usr/bin/env python3
"""netcheck - sanity checks on a KiCad netlist exported with `kicad-cli sch export netlist`.
usage: netcheck.py design.net [--fp-dir /usr/share/kicad/footprints] [--local-fp NAME=DIR ...]
Checks: duplicate refs, single-node nets, symbol pins missing from the assigned footprint,
missing footprints/empty footprint fields. Exit code 1 if anything is wrong.
Also importable: load(netfile) -> (comps, nets, pin2net) for design-specific assertions.
Works with KiCad 9 (one-line) and KiCad 10 (pretty-printed) netlists.
"""
import re, os, sys
def load(path):
t = re.sub(r'\s+', ' ', open(path).read()).replace('( ', '(').replace(' )', ')')
comps = re.findall(r'\(comp \(ref "([^"]+)"\)\s*\(value "([^"]*)"\)(?:\s*\(footprint "([^"]*)"\))?', t)
nets, pin2net = {}, {}
for m in re.finditer(r'\(net \(code "\d+"\) \(name "([^"]*)"\)', t):
s = m.end(); e = t.find('(net (code', s); blk = t[s:e if e > 0 else len(t)]
name = m.group(1).split('/')[-1]
nodes = re.findall(r'\(node \(ref "([^"]+)"\) \(pin "([^"]+)"\)', blk)
nets.setdefault(name, []).extend(nodes)
for n in nodes:
pin2net[n] = name
return comps, nets, pin2net
def main():
args = sys.argv[1:]
net = args[0]
fpdir = '/usr/share/kicad/footprints'
local = {}
i = 1
while i < len(args):
if args[i] == '--fp-dir': fpdir = args[i + 1]; i += 2
elif args[i] == '--local-fp':
k, v = args[i + 1].split('='); local[k] = v; i += 2
else: i += 1
comps, nets, pin2net = load(net)
bad = 0
if not comps:
bad += 1; print('NO COMPONENTS PARSED - unknown netlist format?')
refs = [c[0] for c in comps]
dup = sorted({r for r in refs if refs.count(r) > 1})
if dup: bad += 1; print('DUPLICATE REFS:', dup)
for n, v in nets.items():
if len(v) < 2 and not n.startswith('unconnected'):
bad += 1; print('SINGLE-NODE NET:', n, v)
pins = {}
for (r, p) in pin2net: pins.setdefault(r, set()).add(p)
seen = set()
for ref, val, fp in comps:
if ref.startswith('#') or ref.startswith('H') and not pins.get(ref):
continue
if not fp:
bad += 1; print('NO FOOTPRINT:', ref, val); continue
lib, name = fp.split(':', 1)
d = local.get(lib, os.path.join(fpdir, lib + '.pretty'))
path = os.path.join(d, name + '.kicad_mod')
if not os.path.exists(path):
if fp not in seen: bad += 1; print('MISSING FOOTPRINT FILE:', fp, '(%s)' % ref); seen.add(fp)
continue
pads = set(re.findall(r'\(pad "([^"]*)"', open(path).read()))
miss = pins.get(ref, set()) - pads
if miss: bad += 1; print('PINS NOT IN FOOTPRINT:', ref, val, fp, sorted(miss))
print('components %d, nets %d, problems %d' % (len(comps), len(nets), bad))
sys.exit(1 if bad else 0)
if __name__ == '__main__':
main()
```Files in this skill
- SKILL.md
- gen/kigen.py
- gen/netcheck.py
Attribution
Comments
Loading comments…