Controller board: ESP32 + 2x StepStick carrier (v0.4), FluidNC config
- KiCad project generated by pcb/tools (design, route, schematic, fab outputs). - v0.4 fixes the v0.3 review findings: low-dropout 3.3 V regulator, OR'd 5 V supply, polyfuse, USB ESD protection, input clamps, ceramic decoupling at the drivers, A4988/DRV8825 jumper, ground stitching. DRC/ERC clean. - v0.3 (as ordered) kept in pcb/archive/v0.3. - FluidNC config.yaml for the board; rho hard limit on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c498882bbb
commit
1400cc8afd
46 files changed
+226470
No files matched your search
@@ -0,0 +1,184 @@
|
||||
"""Load footprints and symbols from the installed KiCad libraries."""
|
||||
import math
|
||||
import os
|
||||
from sexpr import parse, find, findall, QStr
|
||||
|
||||
KICAD = os.environ.get('KICAD_SHARE', '/snap/kicad/current/usr/share/kicad')
|
||||
FP_DIR = os.path.join(KICAD, 'footprints')
|
||||
SYM_DIR = os.path.join(KICAD, 'symbols')
|
||||
|
||||
|
||||
def load_footprint(lib_id, min_drill=None, min_ring=0.2, solid_thermal_pads=False):
|
||||
lib, name = lib_id.split(':', 1)
|
||||
path = os.path.join(FP_DIR, lib + '.pretty', name + '.kicad_mod')
|
||||
with open(path) as f:
|
||||
fp = parse(f.read())
|
||||
if solid_thermal_pads:
|
||||
_solidify_thermal_pads(fp)
|
||||
if min_drill:
|
||||
_enforce_min_drill(fp, min_drill, min_ring)
|
||||
return fp
|
||||
|
||||
|
||||
def _solidify_thermal_pads(fp):
|
||||
"""Remove plated holes that sit inside a cluster of same-numbered SMD pads (thermal vias in an
|
||||
exposed pad) and add one copper-only pad over the cluster, so the copper stays continuous under
|
||||
the solder-mask/paste windows but no hole is left in a pad. Fabs flag holes in pads (solder
|
||||
wicks into them) and charge for resin filling."""
|
||||
import copy
|
||||
pads = findall(fp, 'pad')
|
||||
by_num = {}
|
||||
for p in pads:
|
||||
by_num.setdefault(str(p[1]), []).append(p)
|
||||
for num, group in by_num.items():
|
||||
smd = [p for p in group if str(p[2]) == 'smd']
|
||||
tht = [p for p in group if str(p[2]) == 'thru_hole']
|
||||
if len(smd) < 2 or not tht:
|
||||
continue
|
||||
boxes = []
|
||||
for p in smd:
|
||||
at, size = find(p, 'at'), find(p, 'size')
|
||||
x, y, w, h = float(at[1]), float(at[2]), float(size[1]), float(size[2])
|
||||
boxes.append((x - w / 2, y - h / 2, x + w / 2, y + h / 2))
|
||||
x0, y0 = min(b[0] for b in boxes), min(b[1] for b in boxes)
|
||||
x1, y1 = max(b[2] for b in boxes), max(b[3] for b in boxes)
|
||||
inside = [p for p in tht if x0 - 0.5 <= float(find(p, 'at')[1]) <= x1 + 0.5
|
||||
and y0 - 0.5 <= float(find(p, 'at')[2]) <= y1 + 0.5]
|
||||
if not inside:
|
||||
continue
|
||||
for p in inside:
|
||||
fp.remove(p)
|
||||
solid = copy.deepcopy(smd[0])
|
||||
solid[:] = [v for v in solid if not (isinstance(v, list) and v and v[0] in ('uuid', 'roundrect_rratio'))]
|
||||
solid[3] = 'rect'
|
||||
at, size, layers = find(solid, 'at'), find(solid, 'size'), find(solid, 'layers')
|
||||
at[1:] = [f'{(x0 + x1) / 2:g}', f'{(y0 + y1) / 2:g}']
|
||||
size[1:] = [f'{x1 - x0:g}', f'{y1 - y0:g}']
|
||||
layers[1:] = [QStr('F.Cu')]
|
||||
fp.insert(fp.index(smd[0]), solid)
|
||||
|
||||
|
||||
def _enforce_min_drill(fp, min_drill, min_ring):
|
||||
"""Grow round plated holes below min_drill (and their copper ring) so the board fits the fab's process."""
|
||||
for p in findall(fp, 'pad'):
|
||||
if str(p[2]) != 'thru_hole':
|
||||
continue
|
||||
drill = find(p, 'drill')
|
||||
if not drill or len(drill) < 2 or isinstance(drill[1], list) or str(drill[1]) == 'oval':
|
||||
continue
|
||||
if float(drill[1]) >= min_drill - 1e-9:
|
||||
continue
|
||||
drill[1] = f'{min_drill:g}'
|
||||
size = find(p, 'size')
|
||||
need = min_drill + 2 * min_ring
|
||||
for i in (1, 2):
|
||||
if float(size[i]) < need:
|
||||
size[i] = f'{need:g}'
|
||||
|
||||
|
||||
def fp_pads(fp):
|
||||
"""Pads as dicts in footprint-local coords (KiCad: y down)."""
|
||||
pads = []
|
||||
for p in findall(fp, 'pad'):
|
||||
num = str(p[1])
|
||||
kind = p[2] # smd / thru_hole / np_thru_hole / connect
|
||||
shape = p[3]
|
||||
at = find(p, 'at')
|
||||
x, y = float(at[1]), float(at[2])
|
||||
rot = float(at[3]) if len(at) > 3 else 0.0
|
||||
size = find(p, 'size')
|
||||
w, h = float(size[1]), float(size[2])
|
||||
layers = [str(l) for l in find(p, 'layers')[1:]]
|
||||
drill = find(p, 'drill')
|
||||
d = None
|
||||
if drill:
|
||||
vals = [v for v in drill[1:] if not isinstance(v, list)]
|
||||
if vals and vals[0] == 'oval':
|
||||
d = (float(vals[1]), float(vals[2]) if len(vals) > 2 else float(vals[1]))
|
||||
elif vals:
|
||||
d = (float(vals[0]), float(vals[0]))
|
||||
pads.append(dict(num=num, kind=kind, shape=shape, x=x, y=y, rot=rot,
|
||||
w=w, h=h, layers=layers, drill=d, node=p))
|
||||
return pads
|
||||
|
||||
|
||||
def fp_courtyard(fp):
|
||||
"""Bounding box (x0,y0,x1,y1) of the front/back courtyard lines/rects."""
|
||||
xs, ys = [], []
|
||||
for kind in ('fp_line', 'fp_rect', 'fp_circle', 'fp_arc', 'fp_poly'):
|
||||
for g in findall(fp, kind):
|
||||
layer = find(g, 'layer')
|
||||
if not layer or 'CrtYd' not in str(layer[1]):
|
||||
continue
|
||||
for key in ('start', 'end', 'mid', 'center'):
|
||||
pt = find(g, key)
|
||||
if pt:
|
||||
xs.append(float(pt[1])); ys.append(float(pt[2]))
|
||||
pts = find(g, 'pts')
|
||||
if pts:
|
||||
for xy in findall(pts, 'xy'):
|
||||
xs.append(float(xy[1])); ys.append(float(xy[2]))
|
||||
if kind == 'fp_circle':
|
||||
c = find(g, 'center'); e = find(g, 'end')
|
||||
r = math.hypot(float(e[1]) - float(c[1]), float(e[2]) - float(c[2]))
|
||||
xs += [float(c[1]) - r, float(c[1]) + r]
|
||||
ys += [float(c[2]) - r, float(c[2]) + r]
|
||||
if not xs:
|
||||
return None
|
||||
return (min(xs), min(ys), max(xs), max(ys))
|
||||
|
||||
|
||||
_symlib_cache = {}
|
||||
|
||||
|
||||
def load_symbol(lib_id):
|
||||
"""Return the symbol definition list (with 'extends' resolved)."""
|
||||
lib, name = lib_id.split(':', 1)
|
||||
if lib not in _symlib_cache:
|
||||
with open(os.path.join(SYM_DIR, lib + '.kicad_sym')) as f:
|
||||
_symlib_cache[lib] = parse(f.read())
|
||||
root = _symlib_cache[lib]
|
||||
syms = {str(s[1]): s for s in findall(root, 'symbol')}
|
||||
s = syms[name]
|
||||
ext = find(s, 'extends')
|
||||
if ext:
|
||||
base = load_symbol(lib + ':' + str(ext[1]))
|
||||
# copy base, take name + properties from derived
|
||||
import copy
|
||||
merged = copy.deepcopy(base)
|
||||
merged[1] = QStr(name)
|
||||
# rename sub-units
|
||||
for sub in findall(merged, 'symbol'):
|
||||
sub[1] = QStr(str(sub[1]).replace(str(ext[1]), name, 1))
|
||||
# properties: inherited from the base, overridden/extended by the derived symbol
|
||||
base_props = findall(merged, 'property')
|
||||
merged = [x for x in merged if not (isinstance(x, list) and x and x[0] == 'property')]
|
||||
derived = {str(p[1]): p for p in findall(s, 'property')}
|
||||
props = []
|
||||
for p in base_props:
|
||||
props.append(derived.pop(str(p[1]), p))
|
||||
props += list(derived.values())
|
||||
# insert properties after the symbol-level flags
|
||||
idx = 2
|
||||
while idx < len(merged) and isinstance(merged[idx], list) and merged[idx][0] in ('pin_names', 'pin_numbers', 'exclude_from_sim', 'in_bom', 'on_board'):
|
||||
idx += 1
|
||||
merged = merged[:idx] + props + merged[idx:]
|
||||
return merged
|
||||
return s
|
||||
|
||||
|
||||
def sym_pins(sym):
|
||||
"""Pins (number, name, type, x, y, angle, length) in symbol coords (y up)."""
|
||||
pins = []
|
||||
for unit in findall(sym, 'symbol'):
|
||||
for p in findall(unit, 'pin'):
|
||||
at = find(p, 'at')
|
||||
length = float(find(p, 'length')[1])
|
||||
name = find(p, 'name')
|
||||
number = find(p, 'number')
|
||||
pins.append(dict(type=str(p[1]), style=str(p[2]),
|
||||
x=float(at[1]), y=float(at[2]), angle=float(at[3]) if len(at) > 3 else 0.0,
|
||||
length=length, name=str(name[1]), num=str(number[1]),
|
||||
hidden=('hide' in [str(v) for v in p if not isinstance(v, list)]),
|
||||
unit=str(unit[1])))
|
||||
return pins
|
||||
Reference in new issue
Block a user