main
John Lauer Component record for cre2512-fz-r001e-3: files batch 1 ff9a35c 2d ago
"""Build the CRE2512-FZ-R001E-3 page tree (model, footprint, symbol, checks, GLB, LODs, figures).

usage: build.py            (cadquery venv python, run with -I)
Writes /home/adom/project/uta-dynos/work/pages/cre2512-fz-r001e-3/. Downloads are read from
/home/adom/project/uta-dynos/work/pages/_build/dl (never executed); scripts live in this folder.
"""
import sys, os, json, shutil, subprocess, hashlib, re
from PIL import Image
TOOLS = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, TOOLS)
from parts import P, ROOT, FIGS, TODAY
DL = os.path.join(ROOT, "dl"); WORK = os.path.join(ROOT, "work", "cre"); os.makedirs(WORK, exist_ok=True)
PY = "/home/adom/.venvs/cq/bin/python"
slug, MPN = P["slug"], P["mpn"]
OUT = f"/home/adom/project/uta-dynos/work/pages/{slug}"
sha = lambda f: hashlib.sha256(open(f, "rb").read()).hexdigest()
def run(cmd, **kw):
    r = subprocess.run(cmd, capture_output=True, text=True, **kw)
    if r.returncode != 0:
        print("FAILED", cmd, r.stdout[-2000:], r.stderr[-2000:]); sys.exit(1)
    return r
for d in ("docs/verify", "docs/models", "docs/figures", "lod", "provenance/tools", "provenance/kicad-reference", "land-pattern", "altium"):
    os.makedirs(os.path.join(OUT, d), exist_ok=True)

# ---------- STEP: datasheet-built (no manufacturer STEP reachable by plain HTTP) ----------
step_name, marked_name = f"{MPN}.step", f"{MPN}-marked.step"
step_path, marked_path = os.path.join(OUT, step_name), os.path.join(OUT, marked_name)
run([PY, "-I", os.path.join(TOOLS, "make_step.py"), step_path, marked_path])

# ---------- footprint: Bourns recommended land (KiCad's IPC 2512 differs materially) ----------
L = P["land"]
fp_name = f"R_2512_6432Metric_Bourns_CRE2512_{MPN}"
cx = L["gap_a"] / 2 + L["len_b"] / 2; half_x = L["gap_a"] / 2 + L["len_b"]; half_y = L["wid_c"] / 2
crt_x, crt_y = round(half_x + 0.25, 3), round(half_y + 0.25, 3)
bx, by = P["body_mm"]["x_mm"] / 2, P["body_mm"]["y_mm"] / 2
silk_x = round(L["gap_a"] / 2 - 0.2, 3)  # silk only in the gap, 0.2 mm clear of copper
def line(x0, y0, x1, y1, w, layer):
    return f'\t(fp_line\n\t\t(start {x0} {y0})\n\t\t(end {x1} {y1})\n\t\t(stroke\n\t\t\t(width {w})\n\t\t\t(type solid)\n\t\t)\n\t\t(layer "{layer}")\n\t)\n'
def rect(x0, y0, x1, y1, w, layer):
    return f'\t(fp_rect\n\t\t(start {x0} {y0})\n\t\t(end {x1} {y1})\n\t\t(stroke\n\t\t\t(width {w})\n\t\t\t(type solid)\n\t\t)\n\t\t(fill no)\n\t\t(layer "{layer}")\n\t)\n'
def text_prop(name, val, y, layer, hide=False):
    h = "\t\t(hide yes)\n" if hide else ""
    return f'\t(property "{name}" "{val}"\n\t\t(at 0 {y} 0)\n\t\t(layer "{layer}")\n{h}\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 1 1)\n\t\t\t\t(thickness 0.15)\n\t\t\t)\n\t\t)\n\t)\n'
def pad(n, x):
    return (f'\t(pad "{n}" smd rect\n\t\t(at {x} 0)\n\t\t(size {L["len_b"]} {L["wid_c"]})\n'
            f'\t\t(layers "F.Cu" "F.Mask" "F.Paste")\n\t)\n')
fp = (f'(footprint "{fp_name}"\n\t(version 20260206)\n\t(generator "adom_cre2512_build")\n\t(layer "F.Cu")\n'
      f'\t(descr "Bourns CRE2512-R001 ~ R004 metal strip current sense resistor, 2512 (6.40 x 3.20 mm), Bourns recommended solder pad layout '
      f'(pads {L["len_b"]} x {L["wid_c"]} mm, gap {L["gap_a"]} mm), datasheet REV. 06/25 p.1, {P["ds"]["url"]}")\n'
      f'\t(tags "resistor current sense shunt 2512 Bourns CRE2512")\n'
      + text_prop("Reference", "REF**", -(crt_y + 0.7), "F.SilkS") + text_prop("Value", fp_name, crt_y + 0.7, "F.Fab")
      + '\t(attr smd)\n\t(duplicate_pad_numbers_are_jumpers no)\n'
      + line(-silk_x, -round(by + 0.11, 3), silk_x, -round(by + 0.11, 3), 0.12, "F.SilkS")
      + line(-silk_x, round(by + 0.11, 3), silk_x, round(by + 0.11, 3), 0.12, "F.SilkS")
      + rect(-crt_x, -crt_y, crt_x, crt_y, 0.05, "F.CrtYd") + rect(-bx, -by, bx, by, 0.1, "F.Fab")
      + '\t(fp_text user "${REFERENCE}"\n\t\t(at 0 0 0)\n\t\t(layer "F.Fab")\n\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 0.8 0.8)\n\t\t\t\t(thickness 0.12)\n\t\t\t)\n\t\t)\n\t)\n'
      + pad(1, -round(cx, 4)) + pad(2, round(cx, 4))
      + f'\t(embedded_fonts no)\n\t(model "${{KIPRJMOD}}/{step_name}"\n\t\t(offset\n\t\t\t(xyz 0 0 0)\n\t\t)\n\t\t(scale\n\t\t\t(xyz 1 1 1)\n\t\t)\n\t\t(rotate\n\t\t\t(xyz 0 0 0)\n\t\t)\n\t)\n)\n')
fp_file = f"{fp_name}.kicad_mod"
open(os.path.join(OUT, fp_file), "w").write(fp)
# KiCad's own 2512 kept for the comparison (unchanged bytes)
for ext in ("kicad_mod", "step"):
    shutil.copyfile(os.path.join(DL, "kicad", f"{P['fp']}.{ext}"), os.path.join(OUT, f"provenance/kicad-reference/{P['fp']}.{ext}"))

# ---------- symbol (standard resistor graphic, pins on the 2.54 mm grid) ----------
d = P["dist"]
def prop(name, val, y, hide=True):
    h = " hide" if hide else ""
    return f'    (property "{name}" "{val}" (at 0 {y} 0) (effects (font (size 1.27 1.27)){h}))\n'
props = (prop("Reference", "R", 0, False).replace("(at 0 0 0)", "(at 2.54 1.27 0)").replace("(font (size 1.27 1.27))", "(font (size 1.27 1.27)) (justify left)")
         + prop("Value", P["value_sym"], 0, False).replace("(at 0 0 0)", "(at 2.54 -1.27 0)").replace("(font (size 1.27 1.27))", "(font (size 1.27 1.27)) (justify left)")
         + prop("Footprint", fp_name, -7.62) + prop("Datasheet", P["ds"]["url"], -10.16) + prop("Description", P["brief"], -12.7)
         + prop("MPN", MPN, -15.24) + prop("Manufacturer", P["mfr"], -17.78)
         + prop("Mouser", d["mouser"][0], -20.32) + prop("DigiKey", d["digikey"][0], -22.86) + prop("LCSC", d["lcsc"][0], -25.4)
         + prop("Adom Wiki", f"https://wiki.adom.inc/adom/{slug}", -27.94))
sym = f'''(kicad_symbol_lib (version 20241209) (generator "adom_passive_symbol")
  (symbol "{MPN}" (pin_numbers (hide yes)) (pin_names (offset 0.254) (hide yes)) (exclude_from_sim no) (in_bom yes) (on_board yes)
{props}    (property "ki_keywords" "resistor current sense shunt 2512 {MPN}" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
    (symbol "{MPN}_0_1"
      (rectangle (start -1.016 -2.54) (end 1.016 2.54) (stroke (width 0.254) (type default)) (fill (type none)))
    )
    (symbol "{MPN}_1_1"
      (pin passive line (at 0 5.08 270) (length 2.54) (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))
      (pin passive line (at 0 -5.08 90) (length 2.54) (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))
    )
  )
)
'''
sym_file = f"{MPN}.kicad_sym"
open(os.path.join(OUT, sym_file), "w").write(sym)

# ---------- datasheet + figure crops (150 dpi) ----------
pdf = os.path.join(DL, P["ds"]["local"])
shutil.copyfile(pdf, os.path.join(OUT, P["ds"]["file"]))
figs = []
for name, page, box, caption in FIGS:
    png = os.path.join(WORK, f"fig150-{page}.png")
    if not os.path.exists(png):
        run(["pdftoppm", "-r", "150", "-png", "-singlefile", "-f", str(page), "-l", str(page), pdf, png[:-4]])
    b = [int(round(v * 1.5)) for v in box]
    out = os.path.join(OUT, "docs/figures", name)
    Image.open(png).convert("RGB").crop(b).save(out, optimize=True)
    figs.append({"file": f"docs/figures/{name}", "document": P["ds"]["title"], "page": page, "crop_px_at_150dpi": b,
                 "caption": caption, "sha256": sha(out)})
json.dump(figs, open(os.path.join(OUT, "docs/figures/figures.json"), "w"), indent=1)

# ---------- overlay proof: our model, and KiCad's 2512 model for the record ----------
crop100 = os.path.join(WORK, "p-2.png")
if not os.path.exists(crop100):
    run(["pdftoppm", "-r", "100", "-png", "-singlefile", "-f", "2", "-l", "2", pdf, crop100[:-4]])
def overlay(step, out_png, label):
    spec = {"label": label, "mpn": MPN, **P["overlay"], "crops": [[crop100, [0, 230, 825, 430]]],
            "marking": "datasheet code R001 on the marked display copy; this check measures the unmarked STEP" if step == step_path else "n/a"}
    sp = os.path.join(WORK, os.path.basename(out_png) + ".spec.json"); json.dump(spec, open(sp, "w"))
    r = run([PY, "-I", os.path.join(TOOLS, "overlay_chip.py"), sp, step, out_png], cwd=TOOLS)
    pub = dict(spec); pub["crops"] = [["datasheet p.2 render at 100 dpi", [0, 230, 825, 430]]]
    return json.loads(r.stdout.strip().splitlines()[-1]), pub
check, spec_pub = overlay(step_path, os.path.join(OUT, f"docs/verify/{slug}-overlay.png"), MPN)
check["step"] = step_name
json.dump(check, open(os.path.join(OUT, f"docs/verify/{slug}-check.json"), "w"), indent=1)
json.dump(spec_pub, open(os.path.join(OUT, f"provenance/tools/{slug}-overlay-spec.json"), "w"), indent=1)
kcheck, _ = overlay(os.path.join(DL, "kicad", P["fp"] + ".step"), os.path.join(OUT, "docs/verify/kicad-2512-model-check.png"), "KiCad R_2512_6332Metric.step")
kcheck["step"] = f"provenance/kicad-reference/{P['fp']}.step"
json.dump(kcheck, open(os.path.join(OUT, "docs/verify/kicad-2512-model-check.json"), "w"), indent=1)
print("overlay all_pass:", check["all_pass"], "kicad model all_pass:", kcheck["all_pass"])
if not check["all_pass"]:
    print(json.dumps(check, indent=1)); sys.exit(2)

# ---------- land: KiCad IPC 2512 pads vs Bourns land ----------
r = run([PY, "-I", os.path.join(TOOLS, "landcheck.py"), slug, os.path.join(OUT, "land-pattern"),
         os.path.join(OUT, f"provenance/kicad-reference/{P['fp']}.kicad_mod")], cwd=TOOLS)
land = json.loads(r.stdout.strip().splitlines()[-1])
json.dump(land, open(os.path.join(OUT, "land-pattern/land-check.json"), "w"), indent=1)
pads = [tuple(map(float, m)) for m in re.findall(r'\(pad "\d" smd rect\s*\(at ([-\d.]+) ([-\d.]+)\)\s*\(size ([\d.]+) ([\d.]+)\)', fp)]
assert len(pads) == 2 and abs(pads[1][0] - cx) < 1e-6 and pads[1][2] == L["len_b"] and pads[1][3] == L["wid_c"], pads

# ---------- GLB (step2glb service) + thumbnails ----------
glb_path = os.path.join(OUT, "package-reference.glb")
g = subprocess.run(["step2glb", "convert", marked_path, "-o", glb_path], capture_output=True, text=True)
print("step2glb exit", g.returncode)
assert g.returncode == 0 and os.path.getsize(glb_path) > 1000
def thumb(src, out, wh=None):
    extra = ["--width", str(wh[0]), "--height", str(wh[1])] if wh else []
    t = subprocess.run(["step2glb", "thumbnail", src, "-o", out] + extra, capture_output=True, text=True)
    return t.returncode
th = [thumb(marked_path, os.path.join(OUT, "docs/hero.png"), (1600, 1000)),
      thumb(marked_path, os.path.join(OUT, "docs/models/package-reference-step.png")),
      thumb(step_path, os.path.join(OUT, "docs/models/step-plain.png"))]
print("thumbs", th)

# ---------- LODs ----------
run([PY, "-I", os.path.join(TOOLS, "lod.py"), marked_path, MPN, OUT, marked_name, step_path], cwd=TOOLS)
man = json.load(open(os.path.join(OUT, "lod/manifest.json")))
man["purpose"] = ("clean board-use component models: no footprint pads, outlines, labels or silkscreen; the datasheet marking code "
                  "R001 on the top overcoat in high and medium, omitted in low and super-low")
man["source"]["unmarked"] = {"path": step_name, "sha256": sha(step_path)}
man["generator"] = "lod.py (OCCT via cadquery-ocp)"
json.dump(man, open(os.path.join(OUT, "lod/manifest.json"), "w"), indent=1)

for t in ("build.py", "parts.py", "make_step.py", "ortho.py", "overlay_chip.py", "landcheck.py", "lod.py", "records.py", "finish.py", "push.sh", "verify_live.py"):
    if os.path.exists(os.path.join(TOOLS, t)):
        shutil.copyfile(os.path.join(TOOLS, t), os.path.join(OUT, "provenance/tools", t))
json.dump({"step": step_name, "marked": marked_name, "fp": fp_file, "sym": sym_file, "check": check, "kicad_check": kcheck,
           "land": land, "thumbs": th, "glb_stderr": g.stderr[-300:], "date": TODAY},
          open(os.path.join(ROOT, "work", f"{slug}-build.json"), "w"), indent=1)
print("built", OUT)