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:
2026-09-08 00:07:33 +02:00
co-authored by Claude Sonnet 5
parent 25b09cf48a
commit 804e2a2cb8
5 changed files with 149 additions and 0 deletions
+114
View File
@@ -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())