Add generator for single-row castellated edge footprints
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
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user