New scripts/generate_castellated_edge.py produces a reusable footprint (round PTH pads + an embedded Edge.Cuts line through their centers) for mounting a module flush onto another board's edge, ESP32-WROOM style -- distinct from the QFP/SSOP breakout-adapter footprints already in footprints/other/castellated_breakouts.pretty/. Default pad size (1.0mm dia / 0.5mm drill) matches the vias used in reference/kicad-castellated-breakouts. Generated CASTELLATED_EDGE_1x20_P1.27MM (20 pins, 1.27mm pitch) into footprints/other/castellated_edge.pretty/, registered as MIKILAB_castellated_edge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5pjBJQ7tf9pwsSs8rG2sm
115 lines
4.4 KiB
Python
115 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
generate_castellated_edge.py
|
|
=============================
|
|
|
|
Generates a generic, reusable KiCad footprint for a single row of
|
|
castellated pads to place on a board's edge (e.g. for a module meant to
|
|
be soldered flush onto a motherboard, the same way ESP32-WROOM-style
|
|
modules are built) -- as opposed to the QFP/SSOP breakout adapters in
|
|
footprints/other/castellated_breakouts.pretty/, which are a different
|
|
thing (a fine-pitch-IC-to-hand-solderable-pad adapter).
|
|
|
|
Technique (same one documented in reference/kicad-castellated-breakouts/
|
|
readme.md): a round plated through-hole pad is placed with its center
|
|
exactly on the board edge; an Edge.Cuts line embedded in the footprint
|
|
runs straight through every pad center. KiCad merges Edge.Cuts graphics
|
|
from every footprint into the final board outline, so once this
|
|
footprint's row lines up with your board's edge, the mill/router cuts
|
|
each hole in half, exposing plated copper on the board edge. Default pad
|
|
size (1.0mm dia / 0.5mm drill) matches the vias used throughout that
|
|
reference project. Remember to NOT tent these pads when plotting.
|
|
|
|
Usage:
|
|
python3 scripts/generate_castellated_edge.py --pins 20 --pitch 1.27
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
import lib_common as lc
|
|
|
|
ROOT = lc.LIBRARY_ROOT
|
|
|
|
|
|
def build_footprint(pins: int, pitch: float, pad_dia: float, drill: float) -> str:
|
|
name = f"CASTELLATED_EDGE_1x{pins}_P{pitch:g}MM"
|
|
half_width = (pins - 1) * pitch / 2
|
|
margin = pad_dia / 2
|
|
|
|
lines = [
|
|
f"(footprint {name} (layer F.Cu) (tedit 00000000)",
|
|
' (descr "Single row of castellated edge pads, '
|
|
f'{pins} pins, {pitch:g}mm pitch. Align this row with the board '
|
|
'outline (do not tent) so the mill/router cuts each pad in half, '
|
|
'exposing plated copper on the board edge.")',
|
|
' (tags "castellated edge module")',
|
|
" (attr through_hole)",
|
|
f" (fp_text reference REF** (at 0 {-half_width - 2}) (layer F.SilkS)",
|
|
" (effects (font (size 1 1) (thickness 0.15)))",
|
|
" )",
|
|
f" (fp_text value {name} (at 0 {half_width + 2}) (layer F.Fab)",
|
|
" (effects (font (size 1 1) (thickness 0.15)))",
|
|
" )",
|
|
]
|
|
|
|
for i in range(pins):
|
|
x = round(-half_width + i * pitch, 4)
|
|
lines.append(
|
|
f" (pad {i + 1} thru_hole circle (at {x} 0) "
|
|
f"(size {pad_dia:g} {pad_dia:g}) (drill {drill:g}) (layers *.Cu *.Mask))"
|
|
)
|
|
|
|
x_start = round(-half_width - margin, 4)
|
|
x_end = round(half_width + margin, 4)
|
|
lines.append(f" (fp_line (start {x_start} 0) (end {x_end} 0) (layer Edge.Cuts) (width 0.1))")
|
|
lines.append(")")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--pins", type=int, required=True, help="Number of pads in the row")
|
|
parser.add_argument("--pitch", type=float, required=True, help="Pad pitch in mm")
|
|
parser.add_argument("--pad-dia", type=float, default=1.0, help="Pad diameter in mm (default 1.0, matches reference project)")
|
|
parser.add_argument("--drill", type=float, default=0.5, help="Drill diameter in mm (default 0.5, matches reference project)")
|
|
parser.add_argument("--category", default="other", choices=lc.CATEGORIES, help="MIKILAB category (default: other)")
|
|
args = parser.parse_args()
|
|
|
|
if args.pins < 2:
|
|
parser.error("--pins must be at least 2")
|
|
|
|
name = f"CASTELLATED_EDGE_1x{args.pins}_P{args.pitch:g}MM"
|
|
pretty_dir = ROOT / "footprints" / args.category / "castellated_edge.pretty"
|
|
pretty_dir.mkdir(parents=True, exist_ok=True)
|
|
dest = pretty_dir / f"{name}.kicad_mod"
|
|
|
|
if dest.exists():
|
|
print(f"error: {dest} already exists", file=sys.stderr)
|
|
return 1
|
|
|
|
dest.write_text(build_footprint(args.pins, args.pitch, args.pad_dia, args.drill), encoding="utf-8")
|
|
|
|
lc.write_fp_lib_table(ROOT)
|
|
lc.write_global_tables(ROOT)
|
|
|
|
digest = lc.sha256_file(dest)
|
|
lc.append_manifest_rows(ROOT, [[
|
|
"footprint", "", str(dest.relative_to(ROOT)), "NEW", digest,
|
|
f"category={args.category};generated by scripts/generate_castellated_edge.py "
|
|
f"--pins {args.pins} --pitch {args.pitch}",
|
|
]])
|
|
|
|
print(f"Wrote {dest.relative_to(ROOT)}")
|
|
print(f"Registered as MIKILAB_castellated_edge:{name}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|