mcpositioner/hardware/generate_kicad.py
Ryan Malloy ec3f75e47b ESP32 antenna positioner MCP server: extracted from mcnanovna
5 MCP tools for positioner control (status, move, home, stop, config)
and 3 guided workflow prompts (home_positioner, configure_positioner,
measure_pattern_grid). The measure_pattern_grid prompt orchestrates
cross-server 3D pattern measurement with mcnanovna's VNA scan tools.

httpx HTTP client communicates with ESP32 firmware over WiFi.
Firmware and KiCad hardware schematics moved from mcnanovna.
2026-02-02 21:57:59 -07:00

900 lines
32 KiB
Python

#!/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()