"""Place footprints, draw the outline and silkscreen for the PSU carrier board. Run with KiCad's bundled Python (KiCad must not have the board open): "C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/python.exe" hardware/kicad/layout_pcb.py Coordinates below are board-local mm (origin = top-left corner of the board, y down). Positions are the footprint origin (pad 1). Axial parts run left to right, pad 1 on the left. """ from pathlib import Path import pcbnew HERE = Path(__file__).parent BOARD = HERE / "plasma_dmd_psu.kicad_pcb" OX, OY = 100.0, 50.0 # board origin on the page W, H = 170.0, 125.0 # board size HV_X = 61.0 # LV | HV boundary (silkscreen) # ref: (x, y, rotation_deg) PLACE = { # Phoenix MKDS terminals: wire entry is on the footprint's +y side, so top-edge blocks are # rotated 180 (entry up) and bottom-edge blocks stay at 0 (entry down). # ---- low-voltage side (left) ------------------------------------------------- "J1": (16.08, 6.5, 180), # 12 V in "J3": (39.24, 6.5, 180), # Mini560 "J8": (57.16, 6.5, 180), # ESP32 "F1": (8.0, 22.0, 0), "D1": (8.0, 31.0, 0), "C1": (28.0, 35.0, 0), "C2": (40.0, 22.0, 0), "C3": (37.0, 34.0, 0), "D3": (37.0, 41.0, 0), "D2": (37.0, 47.0, 0), "C4": (50.0, 41.0, 0), "R1": (37.0, 53.0, 0), "Q1": (44.0, 60.0, 0), "JP1": (50.0, 72.0, 0), "F2": (8.0, 76.0, 0), "F3": (8.0, 90.0, 0), "J4": (11.0, 118.5, 0), # relay module "J6": (40.0, 118.5, 0), # isolated DC-DC input # ---- positive HV (top right) ------------------------------------------------- "J5": (76.16, 6.5, 180), # NCH6100HV #1 "F4": (66.0, 20.0, 0), "C5": (96.0, 20.0, 0), "R2": (110.0, 20.0, 0), "R3": (127.0, 20.0, 0), "LED1": (148.0, 20.0, 0), "D4": (68.0, 32.0, 0), "R5": (80.0, 32.0, 0), "R4": (102.0, 32.0, 0), "Q2": (117.0, 33.0, 0), "D5": (127.0, 32.0, 0), "Q3": (81.0, 52.0, 0), # heatsink space above (tab side) "C6": (98.0, 50.0, 0), "R6": (119.0, 50.0, 0), # ---- panel connector: on the LV/HV boundary, LV pins 4-7 face left, HV pins 1,2,8 right "J2": (61.0, 78.0, 90), # ---- negative HV (bottom right) ---------------------------------------------- "Q5": (81.0, 70.0, 0), # heatsink space above (tab side) "Q6": (99.0, 70.0, 0), "R13": (114.0, 62.0, 0), # 5 W, runs warm: keep clear of caps "D7": (74.0, 83.5, 0), "D8": (90.0, 83.0, 0), "R12": (106.0, 83.0, 0), "C8": (122.0, 83.0, 0), "D6": (68.0, 93.0, 0), "R10": (82.0, 93.0, 0), "R9": (105.0, 93.0, 0), "Q4": (120.0, 94.0, 0), "R11": (130.0, 93.0, 0), "C9": (68.0, 103.0, 0), "C7": (92.0, 105.0, 0), "R7": (106.0, 103.0, 0), "R8": (122.0, 103.0, 0), "LED2": (144.0, 103.0, 0), "F5": (120.0, 113.5, 0), "J7": (70.0, 118.5, 0), # NCH6100HV #2 output # ---- heatsinks: see heatsink_for() ------------------------------------------------ # ---- mounting holes ------------------------------------------------------------ "H1": (4.0, 4.0, 0), "H2": (W - 4.0, 4.0, 0), "H3": (4.0, H - 4.0, 0), "H4": (W - 4.0, H - 4.0, 0), } HEATSINKS = {"HS1": "Q3", "HS2": "Q5", "HS3": "Q6"} def heatsink_for(q): """Aavid 576802B03900G on a TO-220-3_Vertical at rotation 0 (tab towards -y).""" x, y, _ = PLACE[q] return (x + 2.54, y - 3.6, 270) for _hs, _q in HEATSINKS.items(): PLACE[_hs] = heatsink_for(_q) SILK = [ # (text, x, y, size, layer) ("PLASMA DMD PSU rev 0.1", 24.0, 102.0, 1.5, "F.SilkS"), ("J1 12V IN", 13.5, 13.6, 1.0, "F.SilkS"), ("J3 MINI560", 31.6, 13.6, 1.0, "F.SilkS"), ("J8 ESP32", 52.1, 13.6, 1.0, "F.SilkS"), ("J5 HV MOD 1", 68.0, 13.6, 1.0, "F.SilkS"), ("J4 RELAY MODULE", 21.2, 111.2, 1.0, "F.SilkS"), ("J6 ISO DC-DC IN", 41.5, 111.2, 1.0, "F.SilkS"), ("J7 HV MOD 2 OUT", 72.5, 111.2, 1.0, "F.SilkS"), ("J2 PANEL", 61.0, 44.0, 1.0, "F.SilkS"), ("DANGER", 158.0, 38.0, 2.0, "F.SilkS"), ("HIGH VOLTAGE", 158.0, 42.0, 1.6, "F.SilkS"), ("up to 220V", 158.0, 45.5, 1.2, "F.SilkS"), ("DANGER HIGH VOLTAGE", 152.0, 70.0, 1.6, "B.SilkS"), ] def mm(v): return pcbnew.FromMM(v) def pt(x, y): return pcbnew.VECTOR2I(mm(OX + x), mm(OY + y)) def lib_dir(lib): """Project library (plasma_dmd.pretty next to the board) or a KiCad standard library.""" local = HERE / f"{lib}.pretty" return local if local.exists() else FP_DIR / f"{lib}.pretty" FP_DIR = Path(pcbnew.__file__).parents[3] / "share/kicad/footprints" # /10.0/share/... def schematic_footprints(): """ref -> footprint lib id, read from the schematic.""" import re text = (HERE / "plasma_dmd_psu.kicad_sch").read_text(encoding="utf-8") out = {} for m in re.finditer(r'\(property "Reference" "([^"]+)".*?\(property "Footprint" "([^"]*)"', text, re.S): out[m.group(1)] = m.group(2) return out def sync_footprints(board): """Swap footprints whose library id no longer matches the schematic (keeps ref, value, nets, link).""" wanted = schematic_footprints() for old in list(board.GetFootprints()): ref = old.GetReference() fpid = wanted.get(ref) if not fpid or fpid == old.GetFPIDAsString(): continue lib, name = fpid.split(":") new = pcbnew.FootprintLoad(str(lib_dir(lib)), name) new.SetFPID(pcbnew.LIB_ID(lib, name)) new.SetReference(ref) new.SetValue(old.GetValue()) new.SetPath(old.GetPath()) new.SetSheetname(old.GetSheetname()) new.SetSheetfile(old.GetSheetfile()) nets = {p.GetNumber(): p.GetNet() for p in old.Pads()} board.Add(new) for p in new.Pads(): if p.GetNumber() in nets: p.SetNet(nets[p.GetNumber()]) new.SetPosition(old.GetPosition()) board.Delete(old) print("swapped", ref, "->", fpid) def add_missing_footprints(board): """Add footprints for schematic symbols that are not on the board yet (like Update PCB, F8).""" import gen_kicad _, uuids = gen_kicad.existing_uuids() present = {f.GetReference() for f in board.GetFootprints()} for ref, lib_id, value, fpid, conns, _g in gen_kicad.PARTS: if ref in present: continue lib, name = fpid.split(":") fp = pcbnew.FootprintLoad(str(lib_dir(lib)), name) fp.SetFPID(pcbnew.LIB_ID(lib, name)) fp.SetReference(ref) fp.SetValue(value) fp.SetPath(pcbnew.KIID_PATH("/" + uuids[ref])) fp.SetSheetname("/") fp.SetSheetfile("plasma_dmd_psu.kicad_sch") board.Add(fp) for pad in fp.Pads(): net = conns.get(pad.GetNumber()) if net: pad.SetNet(board.FindNet("/" + net)) print("added", ref, "->", fpid) def main(): board = pcbnew.LoadBoard(str(BOARD)) add_missing_footprints(board) sync_footprints(board) # clean previous layout output (outline, our silkscreen, tracks, vias, zones) for d in list(board.GetDrawings()): if d.GetLayer() in (pcbnew.Edge_Cuts, pcbnew.F_SilkS, pcbnew.B_SilkS): board.Delete(d) for getter in (board.GetTracks, board.Zones): try: items = list(getter()) except TypeError: # SWIG returns a bare object when the list is empty items = [] for it in items: board.Delete(it) fps = {f.GetReference(): f for f in board.GetFootprints()} missing = set(fps) - set(PLACE) extra = set(PLACE) - set(fps) if missing or extra: raise SystemExit(f"placement table mismatch: missing {missing}, unknown {extra}") for ref, (x, y, rot) in PLACE.items(): f = fps[ref] if f.IsFlipped(): f.Flip(f.GetPosition(), False) f.SetOrientationDegrees(rot) f.SetPosition(pt(x, y)) fixed = ref.startswith("J") or (ref.startswith("H") and ref[1:].isdigit()) f.SetLocked(fixed) if fixed: f.Reference().SetVisible(False) # connectors are labelled by the SILK texts if ref in HEATSINKS.values(): f.Reference().SetPosition(pt(x + 2.54, y + 3.2)) # below the pins, clear of the heatsink pin if ref in HEATSINKS: f.Reference().SetPosition(pt(x + 9.5, y + 2.0)) # beside the heatsink # board outline with 2 mm corner radius r = 2.0 def seg(x1, y1, x2, y2): s = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_SEGMENT) s.SetStart(pt(x1, y1)); s.SetEnd(pt(x2, y2)) s.SetLayer(pcbnew.Edge_Cuts); s.SetWidth(mm(0.1)); board.Add(s) def arc(cx, cy, sx, sy, ang): a = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_ARC) a.SetCenter(pt(cx, cy)); a.SetStart(pt(sx, sy)); a.SetArcAngleAndEnd(pcbnew.EDA_ANGLE(ang, pcbnew.DEGREES_T)) a.SetLayer(pcbnew.Edge_Cuts); a.SetWidth(mm(0.1)); board.Add(a) seg(r, 0, W - r, 0); seg(W, r, W, H - r); seg(W - r, H, r, H); seg(0, H - r, 0, r) arc(r, r, 0, r, 90); arc(W - r, r, W - r, 0, 90); arc(W - r, H - r, W, H - r, 90); arc(r, H - r, r, H, 90) # HV boundary: dashed line on silkscreen (gap where the panel connector J2 straddles it) y = 15.0 while y < H - 3: if 42.0 < y < 84.0: y += 3.5 continue s = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_SEGMENT) s.SetStart(pt(HV_X, y)); s.SetEnd(pt(HV_X, min(y + 2.0, H - 3))) s.SetLayer(pcbnew.F_SilkS); s.SetWidth(mm(0.3)); board.Add(s) y += 3.5 for text, x, ty, size, layer in SILK: t = pcbnew.PCB_TEXT(board) t.SetText(text); t.SetPosition(pt(x, ty)) t.SetTextSize(pcbnew.VECTOR2I(mm(size), mm(size))); t.SetTextThickness(mm(size * 0.15)) t.SetLayer(board.GetLayerID(layer)) if layer.startswith("B."): t.SetMirrored(True) board.Add(t) board.Save(str(BOARD)) # report pad positions of rotated connectors so orientation can be checked for ref in ("J2", "J4", "J6", "J7"): f = fps[ref] print(ref, [(p.GetNumber(), round(pcbnew.ToMM(p.GetPosition().x) - OX, 2), round(pcbnew.ToMM(p.GetPosition().y) - OY, 2)) for p in f.Pads()]) print("placed", len(PLACE), "footprints") if __name__ == "__main__": main()