#!/usr/bin/env python3 """Generate KiCad 9 project files for ESP32 + TMC2209 antenna positioner wiring. Pin assignments sourced from firmware/include/config.h: Theta: STEP=25, DIR=26, EN=27 (TMC addr 0) Phi: STEP=32, DIR=33, EN=14 (TMC addr 1) UART: TX=17, RX=16 Run: python hardware/generate_kicad.py """ import json import os import uuid # Deterministic UUIDs seeded from names for reproducible output _UUID_NS = uuid.UUID("a1b2c3d4-e5f6-7890-abcd-ef1234567890") def uid(name=""): if name: return str(uuid.uuid5(_UUID_NS, name)) return str(uuid.uuid4()) # ── S-expression formatting helpers ────────────────────────────────── def _pin(num, name, ptype, x, y, angle, length=2.54): """KiCad symbol pin S-expression.""" return ( f" (pin {ptype} line\n" f" (at {x:.2f} {y:.2f} {angle})\n" f" (length {length:.2f})\n" f' (name "{name}" (effects (font (size 1.27 1.27))))\n' f' (number "{num}" (effects (font (size 1.27 1.27))))\n' f" )" ) def _rect(x1, y1, x2, y2, width=0.254, fill="background"): return ( f" (rectangle\n" f" (start {x1:.2f} {y1:.2f})\n" f" (end {x2:.2f} {y2:.2f})\n" f" (stroke (width {width}) (type default))\n" f" (fill (type {fill}))\n" f" )" ) def _polyline(pts, width=0.254, fill="none"): pts_str = " ".join(f"(xy {x:.2f} {y:.2f})" for x, y in pts) return ( f" (polyline\n" f" (pts {pts_str})\n" f" (stroke (width {width}) (type default))\n" f" (fill (type {fill}))\n" f" )" ) def _arc(start, mid, end, width=0.254): return ( f" (arc\n" f" (start {start[0]:.2f} {start[1]:.2f})\n" f" (mid {mid[0]:.2f} {mid[1]:.2f})\n" f" (end {end[0]:.2f} {end[1]:.2f})\n" f" (stroke (width {width}) (type default))\n" f" (fill (type none))\n" f" )" ) def _text(txt, x, y, size=1.27): return ( f' (text "{txt}"\n' f" (at {x:.2f} {y:.2f} 0)\n" f" (effects (font (size {size} {size})))\n" f" )" ) # ── Symbol builders ────────────────────────────────────────────────── # Each returns the full (symbol "Name" ...) S-expression string. # prefix="" for .kicad_sym, prefix="positioner:" for embedded lib_symbols. def _build_dip_symbol(name, ref_prefix, description, left_pins, right_pins, body_w=10.16, pin_len=2.54, extra_graphics="", prefix=""): """Generic DIP-style symbol with pins on left and right.""" n_left = len(left_pins) n_right = len(right_pins) n_max = max(n_left, n_right) body_h = (n_max - 1) * 2.54 + 5.08 # padding top+bottom hw = body_w / 2 hh = body_h / 2 # Pin Y positions (symbol coords: Y up) def pin_ys(count): top = (count - 1) * 2.54 / 2 return [top - i * 2.54 for i in range(count)] left_ys = pin_ys(n_left) right_ys = pin_ys(n_right) pins_sexp = [] for i, (num, pname, ptype) in enumerate(left_pins): pins_sexp.append(_pin(num, pname, ptype, -(hw + pin_len), left_ys[i], 0, pin_len)) for i, (num, pname, ptype) in enumerate(right_pins): pins_sexp.append(_pin(num, pname, ptype, (hw + pin_len), right_ys[i], 180, pin_len)) full_name = f"{prefix}{name}" # Sub-symbol names use just the symbol name, never the library prefix return ( f' (symbol "{full_name}"\n' f" (exclude_from_sim no)\n" f" (in_bom yes)\n" f" (on_board yes)\n" f' (property "Reference" "{ref_prefix}" (at 0 {hh + 2.54:.2f} 0)\n' f" (effects (font (size 1.27 1.27))))\n" f' (property "Value" "{name}" (at 0 {-(hh + 2.54):.2f} 0)\n' f" (effects (font (size 1.27 1.27))))\n" f' (property "Footprint" "" (at 0 0 0)\n' f" (effects (font (size 1.27 1.27)) hide))\n" f' (property "Datasheet" "" (at 0 0 0)\n' f" (effects (font (size 1.27 1.27)) hide))\n" f' (property "Description" "{description}" (at 0 0 0)\n' f" (effects (font (size 1.27 1.27)) hide))\n" f' (symbol "{name}_0_1"\n' f"{_rect(-hw, hh, hw, -hh)}\n" f"{extra_graphics}" f" )\n" f' (symbol "{name}_1_1"\n' f"{chr(10).join(pins_sexp)}\n" f" )\n" f" )" ) def sym_esp32(prefix=""): """ESP32 DevKit V1 38-pin module symbol. Left side matches physical left header (top to bottom). Right side matches physical right header (top to bottom). Used pins have function annotations. """ left = [ ("1", "3V3", "power_out"), ("2", "EN", "input"), ("3", "VP/IO36", "passive"), ("4", "VN/IO39", "passive"), ("5", "IO34", "passive"), ("6", "IO35", "passive"), ("7", "IO32/\u03c6_STEP", "output"), ("8", "IO33/\u03c6_DIR", "output"), ("9", "IO25/\u03b8_STEP", "output"), ("10", "IO26/\u03b8_DIR", "output"), ("11", "IO27/\u03b8_EN", "output"), ("12", "IO14/\u03c6_EN", "output"), ("13", "IO12", "passive"), ("14", "GND", "power_in"), ("15", "IO13", "passive"), ("16", "SD2", "passive"), ("17", "SD3", "passive"), ("18", "CMD", "passive"), ("19", "5V", "power_in"), ] right = [ ("38", "GND", "power_in"), ("37", "IO23", "passive"), ("36", "IO22", "passive"), ("35", "TX0/IO1", "passive"), ("34", "RX0/IO3", "passive"), ("33", "IO21", "passive"), ("32", "GND", "power_in"), ("31", "IO19", "passive"), ("30", "IO18", "passive"), ("29", "IO5", "passive"), ("28", "IO17/TMC_TX", "output"), ("27", "IO16/TMC_RX", "input"), ("26", "IO4", "passive"), ("25", "IO0", "passive"), ("24", "IO2", "passive"), ("23", "IO15", "passive"), ("22", "SD1", "passive"), ("21", "SD0", "passive"), ("20", "CLK", "passive"), ] return _build_dip_symbol( "ESP32_DevKit_38pin", "U", "ESP32 DevKit V1 38-pin module", left, right, body_w=30.0, pin_len=3.81, prefix=prefix, ) def sym_tmc2209(prefix=""): """TMC2209 SilentStepStick breakout board symbol.""" left = [ ("1", "VM", "power_in"), ("2", "GND", "power_in"), ("3", "2B", "output"), ("4", "2A", "output"), ("5", "1A", "output"), ("6", "1B", "output"), ("7", "VIO", "power_in"), ("8", "GND", "power_in"), ] right = [ ("9", "EN", "input"), ("10", "MS1", "input"), ("11", "MS2", "input"), ("12", "PDN_UART", "bidirectional"), ("13", "STEP", "input"), ("14", "DIR", "input"), ("15", "DIAG", "output"), ("16", "CLK", "input"), ] return _build_dip_symbol( "TMC2209_SilentStepStick", "U", "TMC2209 stepper driver breakout board", left, right, body_w=18.0, prefix=prefix, ) def sym_nema17(prefix=""): """NEMA 17 stepper motor — 4 pins with coil graphic.""" left = [ ("1", "A1", "passive"), ("2", "A2", "passive"), ("3", "B1", "passive"), ("4", "B2", "passive"), ] # Coil graphics inside body: two zigzag coils coil_a = _polyline([(-2, 3.0), (-2, 1.5), (-1, 1.2), (-3, 0.6), (-1, 0.0), (-2, -0.3)], width=0.254) coil_b = _polyline([(2, 3.0), (2, 1.5), (1, 1.2), (3, 0.6), (1, 0.0), (2, -0.3)], width=0.254) label = _text("M", 0, -3.5, 2.0) gfx = f"{coil_a}\n{coil_b}\n{label}\n" return _build_dip_symbol( "NEMA17_Motor", "J", "NEMA 17 stepper motor connector", left, [], body_w=12.0, pin_len=2.54, extra_graphics=gfx, prefix=prefix, ) def sym_barrel_jack(prefix=""): """DC barrel jack — 3 pins.""" left = [ ("1", "+12V", "passive"), ("2", "GND", "passive"), ("3", "Shield", "passive"), ] return _build_dip_symbol( "Barrel_Jack_DC", "J", "DC barrel jack power input", left, [], body_w=10.0, pin_len=2.54, prefix=prefix, ) def sym_resistor(prefix=""): """Simple 2-pin resistor (vertical, pins top/bottom).""" name = "R" full_name = f"{prefix}{name}" return ( f' (symbol "{full_name}"\n' f" (exclude_from_sim no)\n" f" (in_bom yes)\n" f" (on_board yes)\n" f' (property "Reference" "R" (at 2.54 0 90)\n' f" (effects (font (size 1.27 1.27))))\n" f' (property "Value" "R" (at -2.54 0 90)\n' f" (effects (font (size 1.27 1.27))))\n" f' (property "Footprint" "" (at 0 0 0)\n' f" (effects (font (size 1.27 1.27)) hide))\n" f' (property "Datasheet" "" (at 0 0 0)\n' f" (effects (font (size 1.27 1.27)) hide))\n" f' (symbol "{name}_0_1"\n' f"{_rect(-1.016, 3.81, 1.016, -3.81, width=0.254)}\n" f" )\n" f' (symbol "{name}_1_1"\n' f"{_pin('1', '~', 'passive', 0, 6.35, 270, 2.54)}\n" f"{_pin('2', '~', 'passive', 0, -6.35, 90, 2.54)}\n" f" )\n" f" )" ) def sym_cap_pol(prefix=""): """Polarized capacitor (vertical, pin 1 = +).""" name = "C_Polarized" full_name = f"{prefix}{name}" # Two horizontal lines for plates, + marker plate_top = _polyline([(-2.0, 1.0), (2.0, 1.0)], width=0.508) plate_bot = _polyline([(-2.0, -1.0), (2.0, -1.0)], width=0.508) plus_h = _polyline([(-1.0, 2.5), (1.0, 2.5)], width=0.254) plus_v = _polyline([(0.0, 1.5), (0.0, 3.5)], width=0.254) # Curved bottom plate arc_gfx = _arc((-2.0, -1.0), (0.0, -2.0), (2.0, -1.0), width=0.508) return ( f' (symbol "{full_name}"\n' f" (exclude_from_sim no)\n" f" (in_bom yes)\n" f" (on_board yes)\n" f' (property "Reference" "C" (at 2.54 0 0)\n' f" (effects (font (size 1.27 1.27))))\n" f' (property "Value" "C_Polarized" (at -2.54 0 0)\n' f" (effects (font (size 1.27 1.27))))\n" f' (property "Footprint" "" (at 0 0 0)\n' f" (effects (font (size 1.27 1.27)) hide))\n" f' (property "Datasheet" "" (at 0 0 0)\n' f" (effects (font (size 1.27 1.27)) hide))\n" f' (symbol "{name}_0_1"\n' f"{plate_top}\n" f"{arc_gfx}\n" f"{plus_h}\n" f"{plus_v}\n" f" )\n" f' (symbol "{name}_1_1"\n' f"{_pin('1', '+', 'passive', 0, 3.81, 270, 2.54)}\n" f"{_pin('2', '-', 'passive', 0, -3.81, 90, 2.54)}\n" f" )\n" f" )" ) ALL_SYMBOLS = [sym_esp32, sym_tmc2209, sym_nema17, sym_barrel_jack, sym_resistor, sym_cap_pol] # ── Symbol library (.kicad_sym) ────────────────────────────────────── def gen_sym_lib(): syms = "\n".join(fn(prefix="") for fn in ALL_SYMBOLS) return ( "(kicad_symbol_lib\n" " (version 20231120)\n" ' (generator "mcnanovna_gen")\n' ' (generator_version "1.0")\n' f"{syms}\n" ")\n" ) # ── Schematic (.kicad_sch) ─────────────────────────────────────────── def gen_schematic(): root_uuid = uid("root") # Embedded lib_symbols (with "positioner:" prefix) lib_syms = "\n".join(fn(prefix="positioner:") for fn in ALL_SYMBOLS) # ── Component placement ── # Schematic coords: X right, Y down. All on 2.54mm grid. # # Layout (left to right): # J1(barrel) C1(cap) ... U1(ESP32) ... R1 ... U2(TMC θ) J2(motor θ) # ... U3(TMC φ) J3(motor φ) components = [] # (symbol ...) blocks wires = [] # (wire ...) blocks labels = [] # (label ...) blocks no_connects = [] # (no_connect ...) blocks texts = [] # (text ...) blocks sym_instances = [] # for symbol_instances section def add_symbol(lib_name, ref, value, x, y, angle, pin_numbers, extra_props=""): """Add a schematic symbol instance.""" inst_uuid = uid(f"inst-{ref}") pin_lines = "\n".join( f' (pin "{p}" (uuid "{uid(f"pin-{ref}-{p}")}"))' for p in pin_numbers ) prop_ref_x = x # Place reference above, value below (adjust for symbol size) components.append( f" (symbol\n" f' (lib_id "positioner:{lib_name}")\n' f" (at {x:.2f} {y:.2f} {angle})\n" f" (unit 1)\n" f" (exclude_from_sim no)\n" f" (in_bom yes)\n" f" (on_board yes)\n" f" (dnp no)\n" f' (uuid "{inst_uuid}")\n' f' (property "Reference" "{ref}"\n' f" (at {prop_ref_x:.2f} {y - 3:.2f} 0)\n" f" (effects (font (size 1.27 1.27))))\n" f' (property "Value" "{value}"\n' f" (at {prop_ref_x:.2f} {y + 3:.2f} 0)\n" f" (effects (font (size 1.27 1.27))))\n" f"{extra_props}" f"{pin_lines}\n" f" )" ) sym_instances.append( f' (path "/{inst_uuid}"\n' f' (reference "{ref}")\n' f" (unit 1)\n" f' (value "{value}")\n' f' (footprint ""))' ) return inst_uuid def add_wire(x1, y1, x2, y2): wires.append( f" (wire\n" f" (pts (xy {x1:.2f} {y1:.2f}) (xy {x2:.2f} {y2:.2f}))\n" f" (stroke (width 0) (type default))\n" f' (uuid "{uid()}")\n' f" )" ) def add_label(name, x, y, angle=0): labels.append( f' (label "{name}"\n' f" (at {x:.2f} {y:.2f} {angle})\n" f" (effects (font (size 1.27 1.27)) (justify left))\n" f' (uuid "{uid()}")\n' f" )" ) def add_no_connect(x, y): no_connects.append( f' (no_connect (at {x:.2f} {y:.2f}) (uuid "{uid()}"))' ) def add_text(txt, x, y, size=2.54): texts.append( f' (text "{txt}"\n' f" (at {x:.2f} {y:.2f} 0)\n" f" (effects (font (size {size} {size})))\n" f' (uuid "{uid()}")\n' f" )" ) junctions = [] # (junction ...) blocks def add_junction(x, y): junctions.append( f' (junction (at {x:.2f} {y:.2f}) (diameter 0) (color 0 0 0 0)\n' f' (uuid "{uid()}")\n' f" )" ) # ── Pin position calculators ── # For DIP symbols placed at (cx, cy) with body_w and n pins per side: # Left pin i wire-connect at: (cx - body_w/2 - pin_len, cy - top_offset + i*2.54) # Right pin i wire-connect at: (cx + body_w/2 + pin_len, cy - top_offset + i*2.54) # NOTE: In schematic coords, Y is inverted from symbol coords. # Symbol pin at local (x, y) → schematic (cx + x, cy - y) def dip_pin_pos(cx, cy, side, index, n_pins, body_w=10.16, pin_len=2.54): """Get schematic (x, y) of pin wire-connect point.""" # Symbol-local Y of pin i (0-indexed): top_y - i * 2.54 # where top_y = (n_pins - 1) * 2.54 / 2 top_y = (n_pins - 1) * 2.54 / 2.0 local_y = top_y - index * 2.54 # Transform to schematic coords (Y inverted) if side == "left": sx = cx - (body_w / 2.0 + pin_len) sy = cy - local_y else: # right sx = cx + (body_w / 2.0 + pin_len) sy = cy - local_y return sx, sy # ── Place ESP32 (U1) ── U1_X, U1_Y = 101.6, 104.14 esp32_left_pins = [str(i) for i in range(1, 20)] esp32_right_pins = [str(i) for i in range(38, 19, -1)] add_symbol("ESP32_DevKit_38pin", "U1", "ESP32 DevKit", U1_X, U1_Y, 0, esp32_left_pins + esp32_right_pins) # ESP32 pin positions helper def esp_pin(side, index): return dip_pin_pos(U1_X, U1_Y, side, index, 19, body_w=30.0, pin_len=3.81) # ── Place TMC2209 Theta (U2) ── U2_X, U2_Y = 190.5, 68.58 tmc_pins = [str(i) for i in range(1, 17)] add_symbol("TMC2209_SilentStepStick", "U2", "TMC2209 \u03b8", U2_X, U2_Y, 0, tmc_pins) def tmc_theta_pin(side, index): return dip_pin_pos(U2_X, U2_Y, side, index, 8, body_w=24.0) # ── Place TMC2209 Phi (U3) ── U3_X, U3_Y = 190.5, 139.7 add_symbol("TMC2209_SilentStepStick", "U3", "TMC2209 \u03c6", U3_X, U3_Y, 0, tmc_pins) def tmc_phi_pin(side, index): return dip_pin_pos(U3_X, U3_Y, side, index, 8, body_w=24.0) # ── Place NEMA17 Theta Motor (J2) ── J2_X, J2_Y = 243.84, 68.58 motor_pins = ["1", "2", "3", "4"] add_symbol("NEMA17_Motor", "J2", "Motor \u03b8", J2_X, J2_Y, 0, motor_pins) def motor_theta_pin(index): return dip_pin_pos(J2_X, J2_Y, "left", index, 4, body_w=18.0) # ── Place NEMA17 Phi Motor (J3) ── J3_X, J3_Y = 243.84, 139.7 add_symbol("NEMA17_Motor", "J3", "Motor \u03c6", J3_X, J3_Y, 0, motor_pins) def motor_phi_pin(index): return dip_pin_pos(J3_X, J3_Y, "left", index, 4, body_w=18.0) # ── Place Barrel Jack (J1) ── J1_X, J1_Y = 30.48, 104.14 add_symbol("Barrel_Jack_DC", "J1", "12V DC", J1_X, J1_Y, 0, ["1", "2", "3"]) def jack_pin(index): return dip_pin_pos(J1_X, J1_Y, "left", index, 3, body_w=14.0) # ── Place Resistor R1 (1k UART) — horizontal ── # Placed between ESP32 TX and TMC UART bus # Rotated 90 degrees (angle=90): pin 1 on left, pin 2 on right R1_X, R1_Y = 154.94, 109.22 add_symbol("R", "R1", "1k\u03a9", R1_X, R1_Y, 90, ["1", "2"]) # When rotated 90: pin 1 at (x, y-6.35) → (x-6.35, y) in schematic? No... # With angle=90: symbol rotated CCW 90 deg # Pin 1 (at 0, 6.35, 270 in symbol) → after 90 CCW rotation → (6.35, 0, 0) # So pin 1 wire-connect at (R1_X + 6.35, R1_Y) and pin 2 at (R1_X - 6.35, R1_Y) # Wait, the rotation transforms (x,y) → (-y, x) for CCW 90 # Pin 1 symbol pos: (0, 6.35) → (-6.35, 0) → schematic (R1_X - 6.35, R1_Y - 0) # Pin 2 symbol pos: (0, -6.35) → (6.35, 0) → schematic (R1_X + 6.35, R1_Y) R1_PIN1 = (R1_X - 6.35, R1_Y) # left end R1_PIN2 = (R1_X + 6.35, R1_Y) # right end # ── Place Capacitor C1 (100uF) — vertical ── C1_X, C1_Y = 48.26, 104.14 add_symbol("C_Polarized", "C1", "100\u00b5F", C1_X, C1_Y, 0, ["1", "2"]) # Pin 1 (+) at top: (C1_X, C1_Y - 3.81) # Pin 2 (-) at bottom: (C1_X, C1_Y + 3.81) C1_PIN1 = (C1_X, C1_Y - 3.81) C1_PIN2 = (C1_X, C1_Y + 3.81) # ────────────────────────────────────────────────────────────────── # WIRING — using net labels for all connections # ────────────────────────────────────────────────────────────────── STUB = 5.08 # wire stub length for net labels # --- Power: +12V rail --- # Barrel jack pin 1 (+12V) → stub left → label "+12V" jx, jy = jack_pin(0) add_wire(jx, jy, jx - STUB, jy) add_label("+12V", jx - STUB, jy, 180) # C1 pin 1 (+) → label "+12V" add_wire(C1_PIN1[0], C1_PIN1[1], C1_PIN1[0], C1_PIN1[1] - STUB) add_label("+12V", C1_PIN1[0], C1_PIN1[1] - STUB, 90) # TMC theta VM (left pin 0) → label "+12V" tx, ty = tmc_theta_pin("left", 0) add_wire(tx, ty, tx - STUB, ty) add_label("+12V", tx - STUB, ty, 180) # TMC phi VM (left pin 0) → label "+12V" px, py = tmc_phi_pin("left", 0) add_wire(px, py, px - STUB, py) add_label("+12V", px - STUB, py, 180) # --- Power: GND rail --- # Barrel jack pin 2 (GND) jx, jy = jack_pin(1) add_wire(jx, jy, jx - STUB, jy) add_label("GND", jx - STUB, jy, 180) # C1 pin 2 (-) → GND add_wire(C1_PIN2[0], C1_PIN2[1], C1_PIN2[0], C1_PIN2[1] + STUB) add_label("GND", C1_PIN2[0], C1_PIN2[1] + STUB, 270) # ESP32 pin 14 (GND, left index 13) and pin 38 (GND, right index 0) ex, ey = esp_pin("left", 13) add_wire(ex, ey, ex - STUB, ey) add_label("GND", ex - STUB, ey, 180) ex, ey = esp_pin("right", 0) add_wire(ex, ey, ex + STUB, ey) add_label("GND", ex + STUB, ey, 0) # TMC theta GND (left pins 1 and 7) for idx in [1, 7]: tx, ty = tmc_theta_pin("left", idx) add_wire(tx, ty, tx - STUB, ty) add_label("GND", tx - STUB, ty, 180) # TMC phi GND for idx in [1, 7]: px, py = tmc_phi_pin("left", idx) add_wire(px, py, px - STUB, py) add_label("GND", px - STUB, py, 180) # --- Power: +5V rail (ESP32 5V → VIO on both TMC2209s) --- # ESP32 pin 19 (5V, left index 18) ex, ey = esp_pin("left", 18) add_wire(ex, ey, ex - STUB, ey) add_label("+5V", ex - STUB, ey, 180) # TMC theta VIO (left pin 6) tx, ty = tmc_theta_pin("left", 6) add_wire(tx, ty, tx - STUB, ty) add_label("+5V", tx - STUB, ty, 180) # TMC phi VIO (left pin 6) px, py = tmc_phi_pin("left", 6) add_wire(px, py, px - STUB, py) add_label("+5V", px - STUB, py, 180) # --- Theta axis: ESP32 → TMC2209 θ --- # Stagger ESP32 left-side labels: alternating short/long stubs to prevent overlap # Labels are ~15mm wide, so we need >15mm separation between columns STUB_SHORT = STUB STUB_LONG = STUB * 5 # ~25mm stub creates clear two-column layout # GPIO25 (θ_STEP) — ESP32 left pin index 8 ex, ey = esp_pin("left", 8) add_wire(ex, ey, ex - STUB_LONG, ey) add_label("\u03b8_STEP", ex - STUB_LONG, ey, 180) # TMC theta STEP (right pin index 4) tx, ty = tmc_theta_pin("right", 4) add_wire(tx, ty, tx + STUB, ty) add_label("\u03b8_STEP", tx + STUB, ty, 0) # GPIO26 (θ_DIR) — ESP32 left pin index 9 ex, ey = esp_pin("left", 9) add_wire(ex, ey, ex - STUB_SHORT, ey) add_label("\u03b8_DIR", ex - STUB_SHORT, ey, 180) tx, ty = tmc_theta_pin("right", 5) add_wire(tx, ty, tx + STUB, ty) add_label("\u03b8_DIR", tx + STUB, ty, 0) # GPIO27 (θ_EN) — ESP32 left pin index 10 ex, ey = esp_pin("left", 10) add_wire(ex, ey, ex - STUB_LONG, ey) add_label("\u03b8_EN", ex - STUB_LONG, ey, 180) tx, ty = tmc_theta_pin("right", 0) add_wire(tx, ty, tx + STUB, ty) add_label("\u03b8_EN", tx + STUB, ty, 0) # --- Phi axis: ESP32 → TMC2209 φ --- # GPIO32 (φ_STEP) — ESP32 left pin index 6 ex, ey = esp_pin("left", 6) add_wire(ex, ey, ex - STUB_LONG, ey) add_label("\u03c6_STEP", ex - STUB_LONG, ey, 180) px, py = tmc_phi_pin("right", 4) add_wire(px, py, px + STUB, py) add_label("\u03c6_STEP", px + STUB, py, 0) # GPIO33 (φ_DIR) — ESP32 left pin index 7 ex, ey = esp_pin("left", 7) add_wire(ex, ey, ex - STUB_SHORT, ey) add_label("\u03c6_DIR", ex - STUB_SHORT, ey, 180) px, py = tmc_phi_pin("right", 5) add_wire(px, py, px + STUB, py) add_label("\u03c6_DIR", px + STUB, py, 0) # GPIO14 (φ_EN) — ESP32 left pin index 11 ex, ey = esp_pin("left", 11) add_wire(ex, ey, ex - STUB_SHORT, ey) add_label("\u03c6_EN", ex - STUB_SHORT, ey, 180) px, py = tmc_phi_pin("right", 0) add_wire(px, py, px + STUB, py) add_label("\u03c6_EN", px + STUB, py, 0) # --- UART bus (half-duplex) --- # GPIO17 (TMC_TX, ESP32 right pin index 10) → R1 pin 1 # Route with L-shape: horizontal to R1's X, then vertical to R1's Y ex, ey = esp_pin("right", 10) corner_x = R1_PIN1[0] add_wire(ex, ey, corner_x, ey) # horizontal segment add_wire(corner_x, ey, corner_x, R1_PIN1[1]) # vertical segment add_junction(corner_x, ey) # junction at corner # R1 pin 2 → net label "PDN_UART" add_wire(R1_PIN2[0], R1_PIN2[1], R1_PIN2[0] + STUB, R1_PIN2[1]) add_label("PDN_UART", R1_PIN2[0] + STUB, R1_PIN2[1], 0) # GPIO16 (TMC_RX, ESP32 right pin index 11) → "PDN_UART" ex, ey = esp_pin("right", 11) add_wire(ex, ey, ex + STUB, ey) add_label("PDN_UART", ex + STUB, ey, 0) # TMC theta PDN_UART (right pin index 3) tx, ty = tmc_theta_pin("right", 3) add_wire(tx, ty, tx + STUB, ty) add_label("PDN_UART", tx + STUB, ty, 0) # TMC phi PDN_UART (right pin index 3) px, py = tmc_phi_pin("right", 3) add_wire(px, py, px + STUB, py) add_label("PDN_UART", px + STUB, py, 0) # --- TMC2209 address selection --- # Theta (addr 0): MS1=GND, MS2=GND tx, ty = tmc_theta_pin("right", 1) # MS1 add_wire(tx, ty, tx + STUB, ty) add_label("GND", tx + STUB, ty, 0) tx, ty = tmc_theta_pin("right", 2) # MS2 add_wire(tx, ty, tx + STUB, ty) add_label("GND", tx + STUB, ty, 0) # Phi (addr 1): MS1=VIO(+5V), MS2=GND px, py = tmc_phi_pin("right", 1) # MS1 add_wire(px, py, px + STUB, py) add_label("+5V", px + STUB, py, 0) px, py = tmc_phi_pin("right", 2) # MS2 add_wire(px, py, px + STUB, py) add_label("GND", px + STUB, py, 0) # --- Motor wiring: TMC2209 → NEMA17 --- # TMC outputs (left side: 2B=2, 2A=3, 1A=4, 1B=5) → Motor (A1, A2, B1, B2) # Standard mapping: 1A→A1, 1B→A2, 2A→B1, 2B→B2 motor_nets_theta = ["M\u03b8_2B", "M\u03b8_2A", "M\u03b8_1A", "M\u03b8_1B"] motor_nets_phi = ["M\u03c6_2B", "M\u03c6_2A", "M\u03c6_1A", "M\u03c6_1B"] # Theta motor: TMC left pins 2-5 → J2 pins 0-3 for i, net in enumerate(motor_nets_theta): tx, ty = tmc_theta_pin("left", i + 2) add_wire(tx, ty, tx - STUB, ty) add_label(net, tx - STUB, ty, 180) mx, my = motor_theta_pin(i) add_wire(mx, my, mx - STUB, my) add_label(net, mx - STUB, my, 180) # Phi motor for i, net in enumerate(motor_nets_phi): px, py = tmc_phi_pin("left", i + 2) add_wire(px, py, px - STUB, py) add_label(net, px - STUB, py, 180) mx, my = motor_phi_pin(i) add_wire(mx, my, mx - STUB, my) add_label(net, mx - STUB, my, 180) # --- No-connect flags on unused TMC2209 pins --- # DIAG (index 6) and CLK (index 7) on both TMCs for pin_fn in [tmc_theta_pin, tmc_phi_pin]: for idx in [6, 7]: # DIAG, CLK nx, ny = pin_fn("right", idx) add_no_connect(nx, ny) # --- No-connect flags on unused ESP32 pins --- # Left side: skip connected power pins (13=GND, 18=5V) # and used signal pins (6=IO32, 7=IO33, 8=IO25, 9=IO26, 10=IO27, 11=IO14) # Include 0=3V3 (unused power output) to silence ERC unused_left_idx = [0, 1, 2, 3, 4, 5, 12, 14, 15, 16, 17] for idx in unused_left_idx: nx, ny = esp_pin("left", idx) add_no_connect(nx, ny) # Right side: skip GND connected at 0 (pin 38), used (10=IO17, 11=IO16) # Include index 6 (pin 32, second GND) for no_connect to silence ERC unused_right_idx = [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16, 17, 18] for idx in unused_right_idx: nx, ny = esp_pin("right", idx) add_no_connect(nx, ny) # Barrel jack shield pin sx, sy = jack_pin(2) add_no_connect(sx, sy) # --- Text annotations --- add_text("12V DC\\nMotor Supply", J1_X, J1_Y - 15, 1.5) add_text("100\u00b5F\\nBulk Decoupling", C1_X, C1_Y - 12, 1.27) add_text("1k\u03a9 prevents\\nUART bus contention", R1_X, R1_Y - 8, 1.27) add_text("TMC addr 0\\nMS1=GND MS2=GND", U2_X, U2_Y - 18, 1.27) add_text("TMC addr 1\\nMS1=VIO MS2=GND", U3_X, U3_Y - 18, 1.27) add_text("Half-duplex UART\\nshared bus", 155, R1_Y + 6, 1.27) # ── Assemble schematic ── all_components = "\n".join(components) all_wires = "\n".join(wires) all_junctions = "\n".join(junctions) all_labels = "\n".join(labels) all_nc = "\n".join(no_connects) all_texts = "\n".join(texts) all_sym_inst = "\n".join(sym_instances) return ( "(kicad_sch\n" " (version 20231120)\n" ' (generator "mcnanovna_gen")\n' ' (generator_version "1.0")\n' f' (uuid "{root_uuid}")\n' ' (paper "A3")\n' " (title_block\n" ' (title "ESP32 + TMC2209 Antenna Positioner Wiring")\n' ' (date "2026-02-01")\n' ' (rev "1")\n' ' (company "mcnanovna")\n' ' (comment 1 "Pin assignments from firmware/include/config.h")\n' ' (comment 2 "Module-level wiring diagram for breadboard/perfboard")\n' " )\n" " (lib_symbols\n" f"{lib_syms}\n" " )\n" f"{all_components}\n" f"{all_wires}\n" f"{all_junctions}\n" f"{all_labels}\n" f"{all_nc}\n" f"{all_texts}\n" " (sheet_instances\n" ' (path "/"\n' ' (page "1")\n' " )\n" " )\n" " (symbol_instances\n" f"{all_sym_inst}\n" " )\n" ")\n" ) # ── Project file (.kicad_pro) ──────────────────────────────────────── def gen_project(): return json.dumps( { "meta": { "filename": "positioner.kicad_pro", "version": 1, }, "schematic": { "meta": {"version": 1}, "drawing": {"default_line_thickness": 6.0}, "page_layout_descr_file": "", }, "libraries": { "pinned_symbol_libs": [], "pinned_footprint_libs": [], }, "text_variables": {}, }, indent=2, ) + "\n" # ── Symbol library table (sym-lib-table) ───────────────────────────── def gen_sym_lib_table(): return ( "(sym_lib_table\n" " (version 7)\n" ' (lib (name "positioner")(type "KiCad")' '(uri "${KIPRJMOD}/positioner.kicad_sym")' '(options "")(descr "Module symbols for ESP32 antenna positioner"))\n' ")\n" ) # ── Main ───────────────────────────────────────────────────────────── def main(): outdir = os.path.dirname(os.path.abspath(__file__)) files = [ ("positioner.kicad_sym", gen_sym_lib()), ("positioner.kicad_sch", gen_schematic()), ("positioner.kicad_pro", gen_project()), ("sym-lib-table", gen_sym_lib_table()), ] for name, content in files: path = os.path.join(outdir, name) with open(path, "w") as f: f.write(content) print(f" wrote {name} ({len(content)} bytes)") print(f"\nDone. Open {os.path.join(outdir, 'positioner.kicad_pro')} in KiCad 9.") if __name__ == "__main__": main()