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