skill
KiCad Board Audit
Public Made by Adomby adom
Agent skill for auditing any KiCad board before fab: real ERC/DRC, pad-level netlist extraction from the .kicad_pcb, and a checklist for the bugs ERC can't see — floating mandatory pins, supply pins on GND, wrong straps, wrong-variant feedback dividers, missing pull-ups, impedance class misassignment — verified against datasheets and reported by severity.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
#!/usr/bin/env python3
"""Pad-level netlist + layout stats from a .kicad_pcb (KiCad 7-10, stdlib only).
Usage: kicad_netlist.py <board.kicad_pcb>
Prints: components (ref/value/footprint/layer/DNP-attrs), per-net pin lists
with pin functions, pads with no net, per-net routed track length + via count.
Handles both pad-net forms: (net N "name") pre-v10, (net "name") v10+.
"""
import math, re, sys
from collections import defaultdict
def parse(text):
# quote-aware tokenizer: pin names may contain parentheses
toks = re.findall(r'"(?:[^"\\]|\\.)*"|\(|\)|[^\s()"]+', text)
stack = [[]]
for t in toks:
if t == '(':
new = []
stack[-1].append(new)
stack.append(new)
elif t == ')':
stack.pop()
else:
if t.startswith('"'):
t = t[1:-1].replace('\\"', '"').replace('\\\\', '\\')
stack[-1].append(t)
return stack[0]
def first(node, tag):
for c in node:
if isinstance(c, list) and c and c[0] == tag:
return c
return None
def atom(node, tag, idx=1):
n = first(node, tag)
return n[idx] if n and len(n) > idx and not isinstance(n[idx], list) else None
def pad_net(pad):
n = first(pad, 'net')
if not n:
return None
return n[2] if len(n) >= 3 else n[1] # (net N "name") | (net "name")
def refkey(ref):
m = re.match(r'([A-Za-z_#]+)(\d*)', ref)
return (m.group(1), int(m.group(2) or 0))
def main(path):
root = parse(open(path, encoding='utf-8').read())[0]
comps, net_pins, no_net = [], defaultdict(list), []
for fp in (c for c in root if isinstance(c, list) and c and c[0] == 'footprint'):
props = {p[1]: p[2] for p in fp
if isinstance(p, list) and p and p[0] == 'property'
and len(p) >= 3 and not isinstance(p[2], list)}
ref = props.get('Reference', '?')
attrs = first(fp, 'attr')
flags = [a for a in (attrs[1:] if attrs else []) if not isinstance(a, list)]
comps.append((ref, props.get('Value', '?'), fp[1], atom(fp, 'layer') or '?', ','.join(flags)))
for pad in (c for c in fp if isinstance(c, list) and c and c[0] == 'pad'):
net, fn = pad_net(pad), atom(pad, 'pinfunction')
tag = f"{ref}.{pad[1]}" + (f"({fn})" if fn else "")
if net:
net_pins[net].append(tag)
elif pad[2] in ('smd', 'thru_hole'):
no_net.append(tag)
netlen, vias = defaultdict(float), defaultdict(int)
for c in root:
if isinstance(c, list) and c and c[0] == 'segment':
s, e = first(c, 'start'), first(c, 'end')
netlen[atom(c, 'net')] += math.hypot(
float(e[1]) - float(s[1]), float(e[2]) - float(s[2]))
elif isinstance(c, list) and c and c[0] == 'via':
vias[atom(c, 'net')] += 1
print(f"== COMPONENTS ({len(comps)}) ==")
for ref, val, fpname, layer, flags in sorted(comps, key=lambda x: refkey(x[0])):
print(f"{ref:10s} {val[:38]:38s} {fpname[:52]:52s} {layer} {flags}")
print(f"\n== NETS ({len(net_pins)}): pins | routed mm | vias ==")
for net in sorted(net_pins):
print(f"{net} [{len(net_pins[net])} pins, {netlen.get(net, 0):.1f}mm, {vias.get(net, 0)} vias]")
print(" " + " ".join(sorted(net_pins[net])))
print(f"\n== PADS WITH NO NET ({len(no_net)}) ==")
for t in no_net:
print(" " + t)
if __name__ == '__main__':
if len(sys.argv) != 2:
sys.exit(__doc__)
main(sys.argv[1])