128 files · 3.7 MB at 05eecdc Download ZIP
.dockerignore 72 B
.gitignore 170 B
CLAUDE.md 11.0 KB
Dockerfile 3.2 KB
fp-lib-table 152 B
pyproject.toml 497 B
QFP58-feed-RHCP-drc.rpt 553 B
QFP58-feed-RHCP.kicad_pcb 108 KB
QFP58-feed-RHCP.kicad_pro 9.6 KB
README.md 15.5 KB
run.sh 4.2 KB
uv.lock 189 KB
docs/
design_report.json 5.3 KB
preview_LHCP.png 195 KB
preview_RHCP.png 195 KB
docs/svg/
QFP58-feed-LHCP-B_Cu.svg 134 KB
QFP58-feed-LHCP-Edge_Cuts.svg 11.5 KB
QFP58-feed-LHCP-F_Cu.svg 61.9 KB
QFP58-feed-RHCP-B_Cu.svg 131 KB
QFP58-feed-RHCP-Edge_Cuts.svg 11.6 KB
QFP58-feed-RHCP-F_Cu.svg 61.9 KB
QFP58-patch-B_Cu.svg 37.2 KB
QFP58-patch-Edge_Cuts.svg 11.9 KB
QFP58-patch-F_Cu.svg 16.3 KB
gerbers/
QFP58-feed-LHCP-fab.zip 31.5 KB
QFP58-feed-RHCP-fab.zip 31.6 KB
QFP58-patch-d23.06-fab.zip 12.0 KB
QFP58-patch-d23.36-fab.zip 12.0 KB
QFP58-patch-fab.zip 11.7 KB
gerbers/QFP58-feed-LHCP/
QFP58-feed-LHCP-B_Cu.gbl 4.2 KB
QFP58-feed-LHCP-B_Mask.gbs 1.6 KB
QFP58-feed-LHCP-B_Silkscreen.gbo 41.5 KB
QFP58-feed-LHCP-Edge_Cuts.gm1 17.0 KB
QFP58-feed-LHCP-F_Cu.gtl 47.5 KB
QFP58-feed-LHCP-F_Mask.gts 1.2 KB
QFP58-feed-LHCP-F_Silkscreen.gto 1.2 KB
QFP58-feed-LHCP-job.gbrjob 2.4 KB
QFP58-feed-LHCP-NPTH.drl 383 B
QFP58-feed-LHCP-PTH.drl 530 B
gerbers/QFP58-feed-RHCP/
QFP58-feed-RHCP-B_Cu.gbl 4.2 KB
QFP58-feed-RHCP-B_Mask.gbs 1.6 KB
QFP58-feed-RHCP-B_Silkscreen.gbo 40.6 KB
QFP58-feed-RHCP-Edge_Cuts.gm1 17.0 KB
QFP58-feed-RHCP-F_Cu.gtl 47.5 KB
QFP58-feed-RHCP-F_Mask.gts 1.2 KB
QFP58-feed-RHCP-F_Silkscreen.gto 1.2 KB
QFP58-feed-RHCP-job.gbrjob 2.4 KB
QFP58-feed-RHCP-NPTH.drl 383 B
QFP58-feed-RHCP-PTH.drl 536 B
gerbers/QFP58-patch/
QFP58-patch-B_Cu.gbl 21.0 KB
QFP58-patch-B_Mask.gbs 653 B
QFP58-patch-B_Silkscreen.gbo 663 B
QFP58-patch-Edge_Cuts.gm1 640 B
QFP58-patch-F_Cu.gtl 937 B
QFP58-patch-F_Mask.gts 810 B
QFP58-patch-F_Silkscreen.gto 9.4 KB
QFP58-patch-job.gbrjob 2.4 KB
QFP58-patch-NPTH.drl 264 B
QFP58-patch-PTH.drl 375 B
gerbers/QFP58-patch-d23.06/
QFP58-patch-d23.06-B_Cu.gbl 21.0 KB
QFP58-patch-d23.06-B_Mask.gbs 660 B
QFP58-patch-d23.06-B_Silkscreen.gbo 670 B
QFP58-patch-d23.06-Edge_Cuts.gm1 647 B
QFP58-patch-d23.06-F_Cu.gtl 944 B
QFP58-patch-d23.06-F_Mask.gts 817 B
QFP58-patch-d23.06-F_Silkscreen.gto 10.5 KB
QFP58-patch-d23.06-job.gbrjob 2.4 KB
gerbers/QFP58-patch-d23.36/
QFP58-patch-d23.36-B_Cu.gbl 21.1 KB
QFP58-patch-d23.36-B_Mask.gbs 660 B
QFP58-patch-d23.36-B_Silkscreen.gbo 670 B
QFP58-patch-d23.36-Edge_Cuts.gm1 647 B
QFP58-patch-d23.36-F_Cu.gtl 944 B
QFP58-patch-d23.36-F_Mask.gts 817 B
QFP58-patch-d23.36-F_Silkscreen.gto 10.1 KB
QFP58-patch-d23.36-job.gbrjob 2.4 KB
kicad/QFP58-feed-LHCP/
fp-lib-table 149 B
QFP58-feed-LHCP-drc.rpt 553 B
QFP58-feed-LHCP.kicad_pcb 108 KB
QFP58-feed-LHCP.kicad_pro 9.6 KB
kicad/QFP58-patch/
fp-lib-table 149 B
QFP58-patch-drc.rpt 576 B
QFP58-patch.kicad_pcb 36.3 KB
QFP58-patch.kicad_pro 9.6 KB
kicad/QFP58-patch-d23.06/
fp-lib-table 149 B
QFP58-patch-d23.06-drc.rpt 528 B
QFP58-patch-d23.06.kicad_pcb 36.3 KB
QFP58-patch-d23.06.kicad_pro 9.5 KB
kicad/QFP58-patch-d23.36/
fp-lib-table 149 B
QFP58-patch-d23.36-drc.rpt 528 B
QFP58-patch-d23.36.kicad_pcb 36.4 KB
QFP58-patch-d23.36.kicad_pro 9.5 KB
kicad/QFP58.pretty/
CouplingDisk_3.6mm.kicad_mod 1.2 KB
FeedPin_1.1mm.kicad_mod 1.0 KB
MountingHole_2.2mm_M2.kicad_mod 1.0 KB
R_0603_Wilkinson.kicad_mod 1.2 KB
SMA_EdgeLaunch_1.0mm_generic.kicad_mod 1.7 KB
sim/full/
full_cuts_5.65GHz.png 86.6 KB
full_cuts_5.80GHz.png 87.0 KB
full_cuts_5.95GHz.png 88.6 KB
full_farfield_5.65GHz.npz 147 KB
full_farfield_5.80GHz.npz 147 KB
full_farfield_5.95GHz.npz 147 KB
full_pattern_5.65GHz.png 79.2 KB
full_pattern_5.80GHz.png 80.5 KB
full_pattern_5.95GHz.png 83.6 KB
full_pattern3d_5.65GHz.png 154 KB
full_pattern3d_5.80GHz.png 156 KB
full_pattern3d_5.95GHz.png 155 KB
full_sparams.csv 3.2 KB
full_sparams.png 30.3 KB
summary.json 1.5 KB
sim/network/
network_sparams.csv 12.9 KB
network_sparams.png 45.0 KB
summary.json 286 B
sim/patch/
patch_ideal_feed_pattern_5.65GHz.png 69.2 KB
patch_ideal_feed_pattern_5.80GHz.png 69.5 KB
patch_ideal_feed_pattern_5.95GHz.png 70.5 KB
patch_sparams.png 48.0 KB
summary.json 1.4 KB
src/
generate.py 1.9 KB
kicad_build.py 13.9 KB
qfp_geom.py 11.6 KB
qfp_layers.py 6.9 KB
qfp_sim.py 27.4 KB
#!/usr/bin/env python3
"""
Build native KiCad boards for QFP-58 from the qfp_geom primitives (runs on KiCad 7-10; saves in that version's format).
feed board : B.Cu network as real tracks/arcs (net RF), F.Cu GND zone, 0603 resistors, edge SMA, pins, M2 holes
patch board: B.Cu patch zone, top coupling-disk pads, plated pin holes; one per entry of patch_boards()
"""
import os, sys, math
import numpy as np
import pcbnew
from pcbnew import FromMM, VECTOR2I
from qfp_geom import P, build, u
from qfp_layers import outline_feed
X0, Y0 = 150.0, 100.0 # board centre on the KiCad page (mm)
REV = 'C'
def V(p): # top-view mm (y up) -> KiCad nm (y down)
return VECTOR2I(FromMM(X0 + float(p[0])), FromMM(Y0 - float(p[1])))
def kdeg(a): # math angle (rad, CCW, y up) -> KiCad orientation (deg, CCW on screen)
return math.degrees(a)
def new_board(path, name):
b = pcbnew.NewBoard(path)
ds = b.GetDesignSettings()
ds.SetBoardThickness(FromMM(P['sub_h']))
ds.m_CopperEdgeClearance = FromMM(0.2)
ds.m_MinClearance = FromMM(0.15)
b.SetCopperLayerCount(2)
tb = b.GetTitleBlock()
tb.SetTitle(f'QFP-58 {name}'); tb.SetRevision(REV)
tb.SetComment(0, '5.8 GHz quad-feed sequential-rotation CP patch. 1.0 mm FR4, 1 oz, 2 layers.')
tb.SetComment(1, 'Concept after the Triple Feed Patch (M. Baert, R. Theunis, CC BY-SA 4.0).')
return b
def net(b, name):
n = pcbnew.NETINFO_ITEM(b, name); b.Add(n); return n
def poly_shape(b, pts, layer, width=0.1, filled=False):
s = pcbnew.PCB_SHAPE(b)
s.SetShape(pcbnew.SHAPE_T_POLY)
s.SetPolyPoints([V(p) for p in pts])
s.SetLayer(layer); s.SetWidth(FromMM(width)); s.SetFilled(filled)
b.Add(s); return s
def circle_shape(b, c, r, layer, width=0.1, filled=False):
s = pcbnew.PCB_SHAPE(b)
s.SetShape(pcbnew.SHAPE_T_CIRCLE)
s.SetCenter(V(c)); s.SetEnd(V((c[0] + r, c[1])))
s.SetLayer(layer); s.SetWidth(FromMM(width)); s.SetFilled(filled)
b.Add(s); return s
def text(b, s, pos, layer, h=0.8, mirror=False):
t = pcbnew.PCB_TEXT(b)
t.SetText(s); t.SetPosition(V(pos)); t.SetLayer(layer)
t.SetTextSize(VECTOR2I(FromMM(h), FromMM(h))); t.SetTextThickness(FromMM(h*0.15))
t.SetMirrored(mirror)
b.Add(t); return t
def zone(b, pts, layer, n, clearance, name, priority=0):
z = pcbnew.ZONE(b)
z.SetLayer(layer); z.SetNet(n); z.SetZoneName(name)
o = z.Outline(); o.NewOutline()
for p in pts:
q = V(p); o.Append(q.x, q.y)
z.SetLocalClearance(FromMM(clearance))
z.SetMinThickness(FromMM(0.2))
z.SetPadConnection(pcbnew.ZONE_CONNECTION_FULL)
z.SetAssignedPriority(priority)
z.SetIslandRemovalMode(pcbnew.ISLAND_REMOVAL_MODE_NEVER)
b.Add(z); return z
# ----------------------------------------------------------------------------- footprints
def fp_new(b, ref, value, lib_name):
fp = pcbnew.FOOTPRINT(b)
fp.SetFPID(pcbnew.LIB_ID('QFP58', lib_name))
fp.SetReference(ref); fp.SetValue(value)
fp.Reference().SetTextSize(VECTOR2I(FromMM(0.8), FromMM(0.8)))
fp.Reference().SetTextThickness(FromMM(0.12))
fp.Value().SetLayer(pcbnew.F_Fab)
return fp
def pad_smd(fp, num, pos, size, shape=pcbnew.PAD_SHAPE_RECT, n=None, layers='F'):
p = pcbnew.PAD(fp)
p.SetNumber(num); p.SetAttribute(pcbnew.PAD_ATTRIB_SMD); p.SetShape(shape)
p.SetSize(VECTOR2I(FromMM(size[0]), FromMM(size[1])))
ls = pcbnew.LSET()
for L in ((pcbnew.F_Cu, pcbnew.F_Mask) if layers == 'F' else (pcbnew.B_Cu, pcbnew.B_Mask)):
ls.AddLayer(L)
p.SetLayerSet(ls)
q = VECTOR2I(FromMM(pos[0]), FromMM(pos[1]))
if hasattr(p, 'SetPos0'): p.SetPos0(q) # KiCad 7 only
p.SetPosition(q)
if n: p.SetNet(n)
fp.Add(p); return p
def pad_tht(fp, num, pos, size, drill, n=None, clearance=None, plated=True):
p = pcbnew.PAD(fp)
p.SetNumber(num)
p.SetAttribute(pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH)
p.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
p.SetSize(VECTOR2I(FromMM(size), FromMM(size)))
p.SetDrillSize(VECTOR2I(FromMM(drill), FromMM(drill)))
p.SetLayerSet(p.PTHMask() if plated else p.UnplatedHoleMask())
q = VECTOR2I(FromMM(pos[0]), FromMM(pos[1]))
if hasattr(p, 'SetPos0'): p.SetPos0(q) # KiCad 7 only
p.SetPosition(q)
if clearance is not None: p.SetLocalClearance(FromMM(clearance))
if n: p.SetNet(n)
fp.Add(p); return p
def place(b, fp, pos, ang, back=False):
"""footprint built at origin, x-axis = local axis; place with local x-axis along math angle `ang`."""
b.Add(fp)
if back:
# mirror top<->bottom (y -> -y), layers F<->B; KiCad 9+ takes a FLIP_DIRECTION instead of a bool
fp.Flip(VECTOR2I(0, 0), getattr(pcbnew, 'FLIP_DIRECTION_TOP_BOTTOM', False))
fp.SetOrientationDegrees(round(kdeg(ang), 1))
fp.SetPosition(V(pos))
return fp
def check_pads(fp, expected, tol=0.01):
got = sorted([(pcbnew.ToMM(p.GetPosition().x), pcbnew.ToMM(p.GetPosition().y)) for p in fp.Pads()])
exp = sorted([(X0 + e[0], Y0 - e[1]) for e in expected])
for g, e in zip(got, exp):
assert abs(g[0]-e[0]) < tol and abs(g[1]-e[1]) < tol, f'{fp.GetReference()}: pad {g} != {e}'
# ----------------------------------------------------------------------------- footprint builders
def fp_resistor(b, ref='R', val='R', n=None):
d = P['res_pitch']/2
fp = fp_new(b, ref, val, 'R_0603_Wilkinson')
for k, num in ((-1, '1'), (1, '2')):
pad_smd(fp, num, (k*d, 0), P['res_pad'], n=n)
fp.Reference().SetPosition(VECTOR2I(0, FromMM(-1.3)))
return fp
def fp_feedpin(b, ref='P', val='feed pin', n=None):
fp = fp_new(b, ref, val, 'FeedPin_1.1mm')
pad_tht(fp, '1', (0, 0), P['pin_pad'], P['pin_drill'], n=n, clearance=(P['gnd_antipad'] - P['pin_pad'])/2)
fp.Reference().SetVisible(False)
return fp
def fp_sma(b, w_in, gx, ref='J1', n_sig=None, n_gnd=None):
fp = fp_new(b, ref, 'SMA edge-launch, 1.0 mm PCB', 'SMA_EdgeLaunch_1.0mm_generic')
pad_smd(fp, '1', (0, 0), (P['sma_sig_len'], w_in), n=n_sig)
for side in ('F', 'B'):
for k in (-1, 1):
pad_smd(fp, '2', (gx, k*P['sma_gnd_off']), (P['sma_gnd_len'], P['sma_gnd_w']), n=n_gnd, layers=side)
fp.Reference().SetPosition(VECTOR2I(FromMM(-3.0), 0))
return fp
def fp_mhole(b, ref='H'):
fp = fp_new(b, ref, 'M2 NPTH', 'MountingHole_2.2mm_M2')
pad_tht(fp, '', (0, 0), P['mh_drill'], P['mh_drill'], clearance=(P['mh_clear'] - P['mh_drill'])/2, plated=False)
fp.Reference().SetVisible(False)
return fp
def fp_disk(b, ref='C', n=None):
fp = fp_new(b, ref, 'coupling disk + pin', 'CouplingDisk_3.6mm')
pad_tht(fp, '1', (0, 0), P['patch_pin_ring'], P['pin_drill'], n=n,
clearance=(P['patch_antipad'] - P['patch_pin_ring'])/2)
pad_smd(fp, '1', (0, 0), (P['disk_d'], P['disk_d']), shape=pcbnew.PAD_SHAPE_CIRCLE, n=n)
fp.Reference().SetVisible(False)
return fp
def write_library(libdir, w_in, gx):
import shutil
if os.path.isdir(libdir): shutil.rmtree(libdir)
# a .pretty library is just a directory; KiCad 10's FootprintLibCreate/CreateLibrary bindings are broken
os.makedirs(libdir)
if hasattr(pcbnew, 'PCB_IO_MGR'): # KiCad 8+
io = pcbnew.PCB_IO_MGR.FindPlugin(pcbnew.PCB_IO_MGR.KICAD_SEXP)
else: # KiCad 7
io = pcbnew.IO_MGR.PluginFind(pcbnew.IO_MGR.KICAD_SEXP)
b = pcbnew.BOARD()
for fp in (fp_resistor(b, 'R', '0603'), fp_feedpin(b), fp_sma(b, w_in, gx), fp_mhole(b), fp_disk(b)):
io.FootprintSave(libdir, fp)
def write_lib_table(projdir, libdir):
rel = os.path.relpath(libdir, projdir)
open(os.path.join(projdir, 'fp-lib-table'), 'w').write(
'(fp_lib_table\n (version 7)\n (lib (name "QFP58")(type "KiCad")(uri "${KIPRJMOD}/' + rel + '")'
'(options "")(descr "QFP-58 antenna footprints"))\n)\n')
# ----------------------------------------------------------------------------- project layout
SHOWCASE = 'QFP58-feed-RHCP' # the one board in the repo root (the PCB-hosting repo renders a single root PCB)
def board_path(out, name):
"""Showcase board in the root, every other board in kicad/<name>/ (one PCB per folder)."""
d = out if name == SHOWCASE else os.path.join(out, 'kicad', name)
return os.path.abspath(os.path.join(d, f'{name}.kicad_pcb'))
def lib_path(out):
return os.path.abspath(os.path.join(out, 'kicad', 'QFP58.pretty'))
def board_names():
return ['QFP58-feed-RHCP', 'QFP58-feed-LHCP'] + [name for _, name in patch_boards()]
# ----------------------------------------------------------------------------- feed board
def feed_board(path, hand):
f, info = build(hand)
b = new_board(path, f'feed board {hand}')
RF, GND = net(b, 'RF'), net(b, 'GND')
# outline
outline = outline_feed(f)
poly_shape(b, list(outline.exterior.coords)[:-1], pcbnew.Edge_Cuts, 0.05)
# network: native tracks + arcs
for p in f['prims']:
if p['kind'] == 'seg':
t = pcbnew.PCB_TRACK(b); t.SetStart(V(p['p0'])); t.SetEnd(V(p['p1']))
else:
t = pcbnew.PCB_ARC(b)
am = (p['a0'] + p['a1'])/2
t.SetStart(V(p['c'] + p['r']*u(p['a0']))); t.SetMid(V(p['c'] + p['r']*u(am)))
t.SetEnd(V(p['c'] + p['r']*u(p['a1'])))
t.SetWidth(FromMM(p['w'])); t.SetLayer(pcbnew.B_Cu); t.SetNet(RF)
b.Add(t)
# isolation resistors (bottom side)
d = P['res_pitch']/2
for ref, c, a, val in f['res']:
fp = fp_resistor(b, ref, f'{val}R 0603 1%', RF)
place(b, fp, c, a, back=True)
check_pads(fp, [c + k*d*u(a) for k in (-1, 1)])
# feed pins (THT, 1.0 mm Cu wire): ground antipad from pad clearance
labels = ['0°', '−90°', '−180°', '−270°']
for k, p in enumerate(f['pins']):
fp = fp_feedpin(b, f'P{k+1}', f'feed pin {labels[k]}', RF)
place(b, fp, p, 0.0, back=True)
# edge SMA J1: local x-axis points to the board edge
s = f['sma']
ang = math.atan2(s['u'][1], s['u'][0])
w_in = f['prims'][-1]['w']
gx = float(np.dot(s['gnd_c'][0] - s['sig_c'], s['u']))
fp = fp_sma(b, w_in, gx, 'J1', RF, GND) # signal pad -> B.Cu after flip
f['_sma_lib'] = (w_in, gx)
place(b, fp, s['sig_c'], ang, back=True)
# stitching vias for the SMA ground legs
for v in f['vias']:
via = pcbnew.PCB_VIA(b); via.SetPosition(V(v)); via.SetViaType(pcbnew.VIATYPE_THROUGH)
via.SetDrill(FromMM(P['via_drill'])); via.SetWidth(FromMM(P['via_pad'])); via.SetNet(GND)
via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
b.Add(via)
# M2 mounting holes (NPTH)
for k, p in enumerate(f['mh']):
fp = fp_mhole(b, f'H{k+1}')
place(b, fp, p, 0.0)
# F.Cu ground plane (faces the patch)
plane = outline.buffer(-P['edge_pullback']) # as in qfp_layers/qfp_sim; keeps the fill off the edge
zone(b, list(plane.exterior.coords)[:-1], pcbnew.F_Cu, GND, 0.4, 'GND_plane')
# bottom silkscreen notes (mirrored, bottom side)
ty = 21.0 if hand == 'RHCP' else -21.0
rv = {r[0]: r[3] for r in f['res']}
lines = [f'QFP-58 {hand} 5.8 GHz rev {REV}', 'FR4 1.0 mm | gap 4.00 mm Cu-Cu',
f"R1 {rv['R1']}R R2,R3 {rv['R2']}R 0603 1%"]
for i, ln in enumerate(lines):
text(b, ln, (0, ty + 1.6 - 1.6*i), pcbnew.B_SilkS, 0.9, mirror=True)
return b, f, info
# ----------------------------------------------------------------------------- patch board
def patch_board(path):
b = new_board(path, 'patch board')
PATCH = net(b, 'PATCH')
a = P['patch_d']/2
circle_shape(b, (0, 0), a + P['patch_margin'], pcbnew.Edge_Cuts, 0.05)
circ = [(a*math.cos(t), a*math.sin(t)) for t in np.linspace(0, 2*math.pi, 256, endpoint=False)]
zone(b, circ, pcbnew.B_Cu, PATCH, (P['patch_antipad'] - P['patch_pin_ring'])/2, 'patch')
for k in range(4):
p = P['rp']*u(k*math.pi/2)
n = net(b, f'C{k+1}')
fp = fp_disk(b, f'C{k+1}', n)
place(b, fp, p, 0.0)
text(b, f"QFP-58 d{P['patch_d']:.2f} TOP", (0, -8.2), pcbnew.F_SilkS, 0.8)
return b
def patch_boards():
"""(patch_d, board name): the design value plus the bracketing variants, which carry d in the name."""
return [(P['patch_d'], 'QFP58-patch')] + [(d, f'QFP58-patch-d{d:.2f}') for d in P['patch_variants']]
def fill_save_drc(b, path, libdir, ignore=()):
import json
pcbnew.ZONE_FILLER(b).Fill(b.Zones())
pcbnew.SaveBoard(path, b)
write_lib_table(os.path.dirname(path), libdir)
pro = path.replace('.kicad_pcb', '.kicad_pro')
j = json.load(open(pro))
for k in ignore:
j['board']['design_settings']['rule_severities'][k] = 'ignore'
json.dump(j, open(pro, 'w'), indent=2)
rpt = path.replace('.kicad_pcb', '-drc.rpt')
import shutil, subprocess
if shutil.which('kicad-cli'): # KiCad 8+; WriteDRCReport aborts outside the GUI on KiCad 10
subprocess.run(['kicad-cli', 'pcb', 'drc', '--all-track-errors', '--units', 'mm', '-o', rpt, path],
check=True, capture_output=True)
else:
b2 = pcbnew.LoadBoard(path)
pcbnew.WriteDRCReport(b2, rpt, pcbnew.EDA_UNITS_MILLIMETRES, True)
return rpt
if __name__ == '__main__':
out = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
lib = lib_path(out)
for name in board_names():
os.makedirs(os.path.dirname(board_path(out, name)), exist_ok=True)
write_library(lib, 1.883, 0.0) # placeholder, rewritten below
for hand in ('RHCP', 'LHCP'):
pth = board_path(out, f'QFP58-feed-{hand}')
b, f, info = feed_board(pth, hand)
if hand == 'RHCP':
write_library(lib, *f['_sma_lib'])
print(hand, '->', fill_save_drc(b, pth, lib))
d0 = P['patch_d']
for d, name in patch_boards():
P['patch_d'] = d
pth = board_path(out, name)
print(f'patch d{d:.2f} ->', fill_save_drc(patch_board(pth), pth, lib, ignore=('isolated_copper',)))
P['patch_d'] = d0