← Commit history

Delete design/gen/export_fab.py

John Lauer ·41599a2672 ·7d ago ·parent 8f60944
1 file changed −105
design/gen/export_fab.pydeleted−105
@@ -1,105 +0,0 @@-#!/usr/bin/env python3-"""Write the manufacturing set from the finished board with kicad-cli, then re-read every file and check it against-the board: 6 copper Gerbers + mask/paste/silk/edge, Excellon drill (PTH and NPTH) whose hit count equals the board's-holes, a CPL whose references and rotations equal the board's placed parts, a BOM whose references cover them.-Writes fab/ (gerbers.zip, the loose files, bom.csv, cpl.csv) and fab/export-check.json; exits 1 on any mismatch."""-import csv, glob, json, os, re, subprocess, sys, zipfile--ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")-K = "/mnt/c/Users/john/AppData/Local/Programs/KiCad/10.0/bin"-BOARD = os.path.join(ROOT, "kicad", "ocp-oculink-riser.kicad_pcb")-FAB = os.path.join(ROOT, "fab")-G = os.path.join(FAB, "gerbers")-os.makedirs(G, exist_ok=True)-for f in glob.glob(os.path.join(G, "*")):-    os.remove(f)---def wp(p):-    return subprocess.run(["wslpath", "-w", p], capture_output=True, text=True).stdout.strip()---def cli(*args):-    r = subprocess.run([f"{K}/kicad-cli.exe", *args], capture_output=True, text=True)-    if r.returncode:-        raise SystemExit(f"kicad-cli {' '.join(args[:3])} failed: {r.stdout}{r.stderr}")-    return r.stdout---LAYERS = "F.Cu,In1.Cu,In2.Cu,In3.Cu,In4.Cu,B.Cu,F.Paste,B.Paste,F.Silkscreen,B.Silkscreen,F.Mask,B.Mask,Edge.Cuts"-cli("pcb", "export", "gerbers", "--output", wp(G) + "\\", "--layers", LAYERS, "--subtract-soldermask",-    "--use-drill-file-origin", wp(BOARD))-cli("pcb", "export", "drill", "--output", wp(G) + "\\", "--format", "excellon", "--excellon-separate-th",-    "--excellon-units", "mm", "--drill-origin", "plot", "--generate-map", "--map-format", "gerberx2", wp(BOARD))-pos = os.path.join(FAB, "pos-kicad.csv")-cli("pcb", "export", "pos", "--output", wp(pos), "--format", "csv", "--units", "mm", "--side", "both",-    "--exclude-dnp", "--use-drill-file-origin", wp(BOARD))--check = {"board": "kicad/ocp-oculink-riser.kicad_pcb", "problems": []}-files = sorted(os.listdir(G))-check["files"] = files-# copper layers: six Gerbers carrying copper-cu = [f for f in files if re.search(r"(F_Cu|In[1-4]_Cu|B_Cu)\.g", f)]-check["copperGerbers"] = len(cu)-if len(cu) != 6:-    check["problems"].append(f"expected 6 copper Gerbers, found {len(cu)}: {cu}")-for f in cu:-    txt = open(os.path.join(G, f), errors="ignore").read()-    if "D03*" not in txt and "D01*" not in txt:-        check["problems"].append(f"{f} has no drawn copper")-# drill hits against the board's holes-txt_b = open(BOARD).read()-vias = len(re.findall(r"^\t\(via\b", txt_b, re.M))-pth_pads = sum(1 for m in re.finditer(r"\(pad \"[^\"]*\" thru_hole", txt_b))-npth_pads = sum(1 for m in re.finditer(r"\(pad \"[^\"]*\" np_thru_hole", txt_b))-hits = {"PTH": 0, "NPTH": 0}-for f in files:-    if f.endswith(".drl"):-        kind = "NPTH" if "NPTH" in f else "PTH"-        hits[kind] += sum(1 for line in open(os.path.join(G, f)) if re.match(r"^X-?[\d.]+Y-?[\d.]+", line.strip()) or re.match(r"^G85", line.strip()))-check["drill"] = {"pthHits": hits["PTH"], "npthHits": hits["NPTH"], "boardVias": vias, "boardPthPads": pth_pads, "boardNpthPads": npth_pads}-if hits["PTH"] != vias + pth_pads:-    check["problems"].append(f"PTH drill hits {hits['PTH']} != vias {vias} + plated pads {pth_pads}")-if hits["NPTH"] != npth_pads:-    check["problems"].append(f"NPTH drill hits {hits['NPTH']} != NPTH pads {npth_pads}")-# CPL: JLC column names, every placed part, rotations as on the board-rows = list(csv.DictReader(open(pos)))-cpl = os.path.join(FAB, "cpl.csv")-with open(cpl, "w", newline="") as fh:-    w = csv.writer(fh)-    w.writerow(["Designator", "Val", "Package", "Mid X", "Mid Y", "Rotation", "Layer"])-    for r in rows:-        w.writerow([r["Ref"], r["Val"], r["Package"], r["PosX"], r["PosY"], r["Rot"], "Top" if r["Side"] == "top" else "Bottom"])-cpl_refs = {r["Ref"] for r in rows}-board_refs = set()-for m in re.finditer(r'\(footprint "([^"]+)".*?\(property "Reference" "([^"]+)"', txt_b, re.S):-    board_refs.add(m.group(2))-excluded = {"J1", "H1", "H2", "JP1"} | {r for r in board_refs if r.startswith("TP")}-placed = board_refs - excluded-check["cpl"] = {"rows": len(rows), "missing": sorted(placed - cpl_refs), "extra": sorted(cpl_refs - placed)}-if placed - cpl_refs or cpl_refs - placed:-    check["problems"].append(f"CPL refs differ from placed parts: missing {sorted(placed - cpl_refs)} extra {sorted(cpl_refs - placed)}")-# BOM: the design BOM, purchased parts only, grouped by MPN-bom_rows = [r for r in csv.DictReader(open(os.path.join(ROOT, "design", "bom.csv"))) if r["mpn"] not in ("no part", "")]-bom_refs = {r["ref"] for r in bom_rows}-check["bom"] = {"lines": len(bom_rows), "missing": sorted(placed - bom_refs), "extra": sorted(bom_refs - placed)}-if placed - bom_refs:-    check["problems"].append(f"BOM lacks placed parts: {sorted(placed - bom_refs)}")-groups = {}-for r in bom_rows:-    g = groups.setdefault(r["mpn"], dict(r, refs=[]))-    g["refs"].append(r["ref"])-with open(os.path.join(FAB, "bom.csv"), "w", newline="") as fh:-    w = csv.writer(fh)-    w.writerow(["Comment", "Designator", "Footprint", "Qty", "Manufacturer", "MPN", "Mouser", "Stock (checked 2026-10-12)"])-    for mpn, g in sorted(groups.items(), key=lambda kv: kv[1]["refs"][0]):-        w.writerow([g["value"], ",".join(sorted(g["refs"], key=lambda s: (s[0], int(re.sub(r'\D', '', s) or 0)))), g["package"], len(g["refs"]),-                    g["manufacturer"], mpn, g["vendor_pn"], g[[k for k in g if k.startswith("stock_checked")][0]]])-# zip the Gerbers and drills for upload-with zipfile.ZipFile(os.path.join(FAB, "gerbers.zip"), "w", zipfile.ZIP_DEFLATED) as z:-    for f in files:-        z.write(os.path.join(G, f), f)-check["ok"] = not check["problems"]-json.dump(check, open(os.path.join(FAB, "export-check.json"), "w"), indent=1)-print(json.dumps({k: check[k] for k in ("copperGerbers", "drill", "cpl", "bom", "problems", "ok")}, indent=1))-sys.exit(0 if check["ok"] else 1)