"""Generate the KiCad schematic for the Plasma DMD PSU carrier board. The AliExpress modules (Mini560, 2x NCH6100HV, isolated DC-DC, relay) sit off-board and connect through screw terminals. Symbols are copied from the installed KiCad standard libraries. Every pin gets a short wire stub and a net label; connectivity is defined in PARTS below. python hardware/kicad/gen_kicad.py [path/to/kicad/share] """ import re import sys import uuid from pathlib import Path HERE = Path(__file__).parent PROJECT = "plasma_dmd_psu" SHARE = Path(sys.argv[1] if len(sys.argv) > 1 else "C:/Users/patri/AppData/Local/Programs/KiCad/10.0/share/kicad") SYMDIR = SHARE / "symbols" FP_R = "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal" FP_R1W = "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P15.24mm_Horizontal" FP_R5W = "Resistor_THT:R_Axial_Power_L25.0mm_W6.4mm_P30.48mm" FP_D35 = "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal" FP_D41 = "Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal" FP_D15 = "Diode_THT:D_DO-15_P12.70mm_Horizontal" FP_LED = "LED_THT:LED_D5.0mm" FP_TO92 = "Package_TO_SOT_THT:TO-92_Inline" FP_TO220 = "Package_TO_SOT_THT:TO-220-3_Vertical" FP_FUSE = "Fuse:Fuseholder_Cylinder-5x20mm_Schurter_0031_8201_Horizontal_Open" FP_CFILM = "Capacitor_THT:C_Rect_L18.0mm_W5.0mm_P15.00mm_FKS3_FKP3" FP_CDISC = "Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P5.00mm" FP_CP5 = "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm" FP_CP63 = "Capacitor_THT:CP_Radial_D6.3mm_P2.50mm" FP_CP10 = "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm" FP_CP125 = "Capacitor_THT:CP_Radial_D12.5mm_P5.00mm" FP_HDR2 = "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical" FP_KK8 = "Connector_Molex:Molex_KK-396_A-41791-0008_1x08_P3.96mm_Vertical" FP_HOLE = "MountingHole:MountingHole_3.2mm_M3" def tb(n): return f"TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-{n}-5.08_1x{n:02d}_P5.08mm_Horizontal" # (ref, lib_id, value, footprint, {pin number or name: net}, group) PARTS = [ # 1. Input and +5 V ("J1", "Connector:Screw_Terminal_01x02", "12V DC in", tb(2), {"1": "+12V_IN", "2": "GND"}, 0), ("F1", "Device:Fuse", "3A T", FP_FUSE, {"1": "+12V_IN", "2": "+12V"}, 0), ("D1", "Device:D_TVS", "P6KE15A", FP_D15, {"1": "+12V", "2": "GND"}, 0), ("C1", "Device:C_Polarized", "470uF 25V", FP_CP10, {"1": "+12V", "2": "GND"}, 0), ("J3", "Connector:Screw_Terminal_01x04", "Mini560 5V: IN+ IN- OUT+ OUT-", tb(4), {"1": "+12V", "2": "GND", "3": "+5V", "4": "GND"}, 0), ("C2", "Device:C_Polarized", "100uF 10V", FP_CP63, {"1": "+5V", "2": "GND"}, 0), # 2. HV enable + watchdog ("J8", "Connector:Screw_Terminal_01x03", "ESP32: 5V GND GPIO21", tb(3), {"1": "+5V", "2": "GND", "3": "WDOG_IN"}, 1), ("C3", "Device:C", "100nF", FP_CDISC, {"1": "WDOG_IN", "2": "WDOG_A"}, 1), ("D3", "Device:D", "1N4148", FP_D35, {"K": "WDOG_A", "A": "GND"}, 1), ("D2", "Device:D", "1N4148", FP_D35, {"A": "WDOG_A", "K": "WDOG_B"}, 1), ("C4", "Device:C_Polarized", "10uF 16V", FP_CP5, {"1": "WDOG_B", "2": "GND"}, 1), ("R1", "Device:R", "100k", FP_R, {"1": "WDOG_B", "2": "GND"}, 1), ("Q1", "Transistor_FET:2N7000", "2N7000", FP_TO92, {"G": "WDOG_B", "S": "GND", "D": "RELAY_IN"}, 1), ("J4", "Connector:Screw_Terminal_01x05", "Relay module: VCC GND IN COM NO", tb(5), {"1": "+5V", "2": "GND", "3": "RELAY_IN", "4": "+12V", "5": "+12V_HV"}, 1), ("F2", "Device:Fuse", "1A T", FP_FUSE, {"1": "+12V_HV", "2": "+12V_HVP"}, 1), ("F3", "Device:Fuse", "1A T", FP_FUSE, {"1": "+12V_HV", "2": "+12V_HVN"}, 1), # 3. Positive HV ("J5", "Connector:Screw_Terminal_01x03", "NCH6100HV #1 (90V): VIN GND HV+", tb(3), {"1": "+12V_HVP", "2": "GND", "3": "HVP_MOD"}, 2), ("F4", "Device:Fuse", "100mA F", FP_FUSE, {"1": "HVP_MOD", "2": "+90V_RAW"}, 2), ("C5", "Device:C_Polarized", "22uF 250V", FP_CP125, {"1": "+90V_RAW", "2": "GND"}, 2), ("R2", "Device:R", "330k", FP_R, {"1": "+90V_RAW", "2": "GND"}, 2), ("R3", "Device:R", "100k 1W", FP_R1W, {"1": "+90V_RAW", "2": "N_LED1"}, 2), ("LED1", "Device:LED", "HV on", FP_LED, {"A": "N_LED1", "K": "GND"}, 2), ("D4", "Device:D_Zener", "1N5228 3.9V", FP_D35, {"K": "+90V_RAW", "A": "REF_P"}, 2), ("R5", "Device:R", "47k 1W", FP_R1W, {"1": "REF_P", "2": "GND"}, 2), ("R4", "Device:R", "1.5k", FP_R, {"1": "+90V_RAW", "2": "Q2_E"}, 2), ("Q2", "Transistor_BJT:Q_PNP_EBC", "MPSA92", FP_TO92, {"E": "Q2_E", "B": "REF_P", "C": "Q3_B"}, 2), ("D5", "Device:D_Zener", "1N4760A 68V", FP_D41, {"K": "Q3_B", "A": "GND"}, 2), ("Q3", "Transistor_BJT:Q_NPN_BCE", "MJE15030", FP_TO220, {"C": "+90V_RAW", "B": "Q3_B", "E": "+65V"}, 2), ("C6", "Device:C", "0.1uF 500V", FP_CFILM, {"1": "+65V", "2": "GND"}, 2), ("R6", "Device:R", "330k", FP_R, {"1": "+65V", "2": "GND"}, 2), # 4. Negative HV ("J6", "Connector:Screw_Terminal_01x02", "Isolated DC-DC in: VIN GND", tb(2), {"1": "+12V_HVN", "2": "GND"}, 3), ("J7", "Connector:Screw_Terminal_01x02", "NCH6100HV #2 (130V) out: HV+ GND", tb(2), {"1": "GND", "2": "HVN_MOD"}, 3), ("F5", "Device:Fuse", "100mA F", FP_FUSE, {"1": "HVN_MOD", "2": "-130V_RAW"}, 3), ("C7", "Device:C_Polarized", "22uF 250V", FP_CP125, {"1": "GND", "2": "-130V_RAW"}, 3), ("R7", "Device:R", "330k", FP_R, {"1": "GND", "2": "-130V_RAW"}, 3), ("R8", "Device:R", "100k 1W", FP_R1W, {"1": "N_LED2", "2": "-130V_RAW"}, 3), ("LED2", "Device:LED", "HV on", FP_LED, {"A": "GND", "K": "N_LED2"}, 3), ("D6", "Device:D_Zener", "1N5228 3.9V", FP_D35, {"K": "REF_N", "A": "-130V_RAW"}, 3), ("R10", "Device:R", "47k 1W", FP_R1W, {"1": "GND", "2": "REF_N"}, 3), ("R9", "Device:R", "1.5k", FP_R, {"1": "Q4_E", "2": "-130V_RAW"}, 3), ("Q4", "Transistor_BJT:Q_NPN_EBC", "MPSA42", FP_TO92, {"E": "Q4_E", "B": "REF_N", "C": "Q5_B"}, 3), ("D7", "Device:D_Zener", "1N4743 13V", FP_D41, {"K": "REF_M", "A": "Q5_B"}, 3), ("D8", "Device:D_Zener", "1N4764A 100V", FP_D41, {"K": "GND", "A": "REF_M"}, 3), ("Q5", "Transistor_BJT:Q_PNP_BCE", "MJE15031", FP_TO220, {"B": "Q5_B", "C": "-130V_RAW", "E": "-112V"}, 3), ("C8", "Device:C", "0.1uF 500V", FP_CFILM, {"1": "GND", "2": "-112V"}, 3), ("R11", "Device:R", "330k", FP_R, {"1": "GND", "2": "-112V"}, 3), ("R12", "Device:R", "1.5k", FP_R, {"1": "REF_M", "2": "Q6_B"}, 3), ("Q6", "Transistor_BJT:Q_NPN_BCE", "MJE15030", FP_TO220, {"B": "Q6_B", "C": "Q6_C", "E": "-100V"}, 3), ("R13", "Device:R", "2k 5W", FP_R5W, {"1": "GND", "2": "Q6_C"}, 3), ("C9", "Device:C", "0.1uF 500V", FP_CFILM, {"1": "GND", "2": "-100V"}, 3), # 5. Panel connector (Stern 520-5138 CN2 order; verify against your panel!) ("J2", "Connector_Generic:Conn_01x08", "Panel power (Stern CN2 order)", FP_KK8, {"1": "-112V", "2": "-100V", "3": None, "4": "GND", "5": "GND", "6": "+5V", "7": "+12V_PANEL", "8": "+65V"}, 4), ("JP1", "Connector_Generic:Conn_01x02", "Fit if panel uses +12V", FP_HDR2, {"1": "+12V", "2": "+12V_PANEL"}, 4), ("H1", "Mechanical:MountingHole", "M3", FP_HOLE, {}, 4), ("H2", "Mechanical:MountingHole", "M3", FP_HOLE, {}, 4), ("H3", "Mechanical:MountingHole", "M3", FP_HOLE, {}, 4), ("H4", "Mechanical:MountingHole", "M3", FP_HOLE, {}, 4), ] GROUP_TITLES = [ "1. 12 V input and +5 V (Mini560 off-board)", "2. HV enable relay + hardware watchdog (relay module off-board)", "3. Positive HV: NCH6100HV #1 off-board + original regulator -> +65 V", "4. Negative HV: isolated DC-DC + NCH6100HV #2 off-board (FLOATING at -130 V) + regulator -> -112 / -100 V", "5. Panel connector and mounting", ] COLS = 7 # parts per row inside a group DX, DY = 50.8, 27.94 GROUP_GAP = 12.7 def group_origins(): """Stack the groups vertically, each as tall as its number of part rows.""" y, out = 33.02, [] for g in range(len(GROUP_TITLES)): n = sum(1 for p in PARTS if p[5] == g) out.append((25.4, y)) y += 12.7 + -(-n // COLS) * DY + GROUP_GAP return out GROUPS = list(zip(GROUP_TITLES, group_origins())) NOTES = """Plasma DMD - 12 V DC PSU carrier board (rev 0.1, unverified) Connectivity is by net labels: every pin has a stub + label. Off-board modules connect via J3-J7. Panel voltages are set by zeners D5 (68 V -> about +65 V) and D7+D8 (113 V -> about -112 / -100 V), copied from Stern 520-5138-00. SAFETY: keep >= 3 mm clearance (more is better) between HV nets (+90V_RAW, +65V, -130V_RAW, -112V, -100V, HVN_MOD, HVP_MOD) and low-voltage nets. J2 pin order follows the Stern CN2 connector: map it to YOUR panel before use. Pin 3 is the key position.""" # --------------------------------------------------------------------------- def uid(): return str(uuid.uuid4()) def read_sexpr_block(text, start): depth, i = 0, start in_str = False while True: ch = text[i] if in_str: if ch == "\\": i += 1 elif ch == '"': in_str = False elif ch == '"': in_str = True elif ch == "(": depth += 1 elif ch == ")": depth -= 1 if depth == 0: return text[start:i + 1] i += 1 _libcache = {} def lib_symbol(lib_id): lib, name = lib_id.split(":") if lib not in _libcache: _libcache[lib] = (SYMDIR / f"{lib}.kicad_sym").read_text(encoding="utf-8") text = _libcache[lib] m = re.search(r'\(symbol "' + re.escape(name) + r'"', text) if not m: raise KeyError(lib_id) block = read_sexpr_block(text, m.start()) ext = re.search(r'\(extends "([^"]+)"\)', block) if ext: raise ValueError(f"{lib_id} extends {ext.group(1)}; pick a non-derived symbol") # top-level name gets the library prefix; sub-units keep the bare name return block.replace(f'(symbol "{name}"', f'(symbol "{lib_id}"', 1) def pins_of(block): """Return list of (number, name, x, y, angle) from a symbol block (symbol coords, y up).""" out = [] for m in re.finditer(r'\(pin \w+ \w+\s*\(at ([-\d.]+) ([-\d.]+) ([-\d.]+)\)', block): pb = read_sexpr_block(block, m.start()) name = re.search(r'\(name "([^"]*)"', pb).group(1) num = re.search(r'\(number "([^"]*)"', pb).group(1) out.append((num, name, float(m.group(1)), float(m.group(2)), float(m.group(3)))) return out def pin_cache_for(lib_id): return pins_of(lib_symbol(lib_id)) def q(s): return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"' def snap(v): return round(round(v / 1.27) * 1.27, 2) HV_NETS = ["+90V_RAW", "+65V", "HVP_MOD", "Q2_E", "REF_P", "Q3_B", "N_LED1", "-130V_RAW", "-112V", "-100V", "HVN_MOD", "Q4_E", "REF_N", "Q5_B", "REF_M", "Q6_B", "Q6_C", "N_LED2"] POWER_NETS = ["GND", "+12V", "+12V_IN", "+12V_HV", "+12V_HVP", "+12V_HVN", "+5V", "+12V_PANEL"] def netclass(name, clearance, track, priority): return {"bus_width": 12, "clearance": clearance, "diff_pair_gap": 0.25, "diff_pair_via_gap": 0.25, "diff_pair_width": 0.2, "line_style": 0, "microvia_diameter": 0.3, "microvia_drill": 0.1, "name": name, "pcb_color": "rgba(0, 0, 0, 0.000)", "priority": priority, "schematic_color": "rgba(0, 0, 0, 0.000)", "track_width": track, "tuning_profile": "", "via_diameter": 1.0, "via_drill": 0.5, "wire_width": 6} def write_project(): """Create/update the .kicad_pro with net classes. HV nets get 3 mm clearance to everything.""" import json pro = HERE / f"{PROJECT}.kicad_pro" data = json.loads(pro.read_text(encoding="utf-8")) if pro.exists() else {} data.setdefault("meta", {"filename": pro.name, "version": 3}) data["net_settings"] = { "classes": [ netclass("Default", 0.25, 0.4, 2147483647), netclass("Power", 0.3, 1.0, 1), netclass("HV", 3.0, 0.6, 0), ], "meta": {"version": 5}, "net_colors": None, "netclass_assignments": None, "netclass_patterns": [{"netclass": "HV", "pattern": "/" + n} for n in HV_NETS] + [{"netclass": "Power", "pattern": "/" + n} for n in POWER_NETS], } pro.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") def main(): root = uid() lib_blocks, pin_cache = {}, {} for p in PARTS: if p[1] not in lib_blocks: blk = lib_symbol(p[1]) lib_blocks[p[1]] = blk pin_cache[p[1]] = pins_of(blk) items = [] counts = {} for ref, lib_id, value, fp, conns, g in PARTS: i = counts.get(g, 0) counts[g] = i + 1 gx, gy = GROUPS[g][1] x = snap(gx + 12.7 + (i % COLS) * DX) y = snap(gy + 12.7 + (i // COLS) * DY) pins = pin_cache[lib_id] # Ref/value go beside the part: above it when a net label sticks out to the right # (horizontal two-pin parts), otherwise to the right of the body. if any(int(a) % 360 == 180 for *_, a in pins): rx, ry, vx, vy, just = x, snap(y - 6.35), x, snap(y - 4.445), "" else: rx, ry, vx, vy, just = snap(x + 5.08), snap(y - 1.27), snap(x + 5.08), snap(y + 1.27), " (justify left)" pin_uuid = "\n".join(f' (pin {q(num)} (uuid {q(uid())}))' for num, *_ in pins) items.append(f""" (symbol (lib_id {q(lib_id)}) (at {x} {y} 0) (unit 1) (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid {q(uid())}) (property "Reference" {q(ref)} (at {rx} {ry} 0) (effects (font (size 1.27 1.27)){just})) (property "Value" {q(value)} (at {vx} {vy} 0) (effects (font (size 1.27 1.27)){just})) (property "Footprint" {q(fp)} (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes))) {pin_uuid} (instances (project {q(PROJECT)} (path {q('/' + root)} (reference {q(ref)}) (unit 1)))) )""") used = set() for num, name, px, py, ang in pins: key = num if num in conns else (name if name in conns else None) if key is None: if conns: raise KeyError(f"{ref}: pin {num}/{name} not connected in PARTS") continue used.add(key) net = conns[key] cx, cy = round(x + px, 2), round(y - py, 2) # schematic y is down # stub points away from the body (pin angle points from connection point toward body) dx, dy = {0: (-1, 0), 90: (0, 1), 180: (1, 0), 270: (0, -1)}[int(ang) % 360] ex, ey = round(cx + dx * 2.54, 2), round(cy + dy * 2.54, 2) if net is None: items.append(f' (no_connect (at {cx} {cy}) (uuid {q(uid())}))') continue items.append(f' (wire (pts (xy {cx} {cy}) (xy {ex} {ey})) (stroke (width 0) (type default)) (uuid {q(uid())}))') lang, just = {(-1, 0): (180, "right"), (1, 0): (0, "left"), (0, -1): (90, "left"), (0, 1): (270, "right")}[(dx, dy)] items.append(f' (label {q(net)} (at {ex} {ey} {lang}) (fields_autoplaced yes)' f' (effects (font (size 1.27 1.27)) (justify {just} bottom)) (uuid {q(uid())}))') missing = set(conns) - used if missing: raise KeyError(f"{ref}: unknown pins {missing}") for title, (gx, gy) in GROUPS: items.append(f' (text {q(title)} (exclude_from_sim no) (at {gx} {gy} 0)' f' (effects (font (size 2.54 2.54) (bold yes)) (justify left bottom)) (uuid {q(uid())}))') items.append(f' (text {q(NOTES)} (exclude_from_sim no) (at 25.4 12.7 0)' f' (effects (font (size 1.778 1.778)) (justify left top)) (uuid {q(uid())}))') libs = "\n".join(" " + b.replace("\n", "\n ") for b in lib_blocks.values()) sch = f"""(kicad_sch (version 20260101) (generator "plasma_dmd_gen") (generator_version "1.0") (uuid {q(root)}) (paper "A2") (title_block (title "Plasma DMD 12 V DC PSU") (rev "0.1") (comment 1 "Unverified - see notes")) (lib_symbols {libs} ) {chr(10).join(items)} (sheet_instances (path "/" (page "1"))) ) """ (HERE / f"{PROJECT}.kicad_sch").write_text(sch, encoding="utf-8") write_project() print(f"wrote {PROJECT}.kicad_sch with {len(PARTS)} parts") if __name__ == "__main__": main()