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.
900 lines
32 KiB
Python
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()
|