main
John Lauer Component record for ltc2945cms-pbf: files batch 3 16c3b96 23h ago
#!/usr/bin/env python3
"""Write the records of adom/ltc2945cms-pbf from the files in the page tree: lod/manifest.json,
provenance/3d-model.json, provenance/symbol.json, provenance/files.json, models.json, verification.json, LICENSE and
README.md. Every hash is computed from the file as it sits in the tree.
usage: records.py <page dir> <work dir>"""
import sys, os, json, hashlib, re
page, work = sys.argv[1:3]
S = "ltc2945cms-pbf"; MPN = "LTC2945CMS#PBF"; OWNER = "adom"; TODAY = "2026-10-21"
p_ = lambda f: os.path.join(page, f)
def sha(f): return hashlib.sha256(open(p_(f), "rb").read()).hexdigest()
def blob(f):
    b = open(p_(f), "rb").read(); return hashlib.sha1(b"blob %d\0" % len(b) + b).hexdigest()
size = lambda f: os.path.getsize(p_(f))
P = json.load(open(p_("page.json"))); C = P["component"]
pins = {p["number"]: p for p in C["pins"]}
dims = json.load(open(p_("provenance/dims-model.json")))
fpc = json.load(open(p_("provenance/footprint-check.json")))
ov = json.load(open(p_("provenance/overlay-proof.json")))
plan = json.load(open(p_(f"docs/proof/{S}-etched.mark-plan.json")))
chk = json.load(open(p_(f"docs/proof/{S}-etched.check.json")))
ovl = json.load(open(p_(f"{S}-hero.overlay.json")))
schpins = json.load(open(p_("provenance/tools/schlib-pins.json")))
DS = C["provenance"]["datasheet"]
KREF = "provenance/kicad-reference/MSOP-12_3x4.039mm_P0.65mm.step"
KFP = "provenance/kicad-reference/MSOP-12_3x4.039mm_P0.65mm.kicad_mod"
BY = {"name": "Claude (AI agent) for John Lauer, automated checks, not yet reviewed by a person", "handle": "john"}

# ---------------- lod/manifest.json
def glbstats(f):
    b = open(p_(f), "rb").read(); n = int.from_bytes(b[12:16], "little"); j = json.loads(b[20:20 + n])
    tris = 0; prims = 0
    for m in j["meshes"]:
        for pr in m["primitives"]:
            prims += 1; tris += j["accessors"][pr["indices"]]["count"] // 3
    return {"triangles": tris, "primitives": prims, "nodes": [x.get("name") for x in j["nodes"] if "mesh" in x],
            "extensions": j.get("extensionsUsed", []), "materials": [m.get("name") for m in j.get("materials", [])]}
levels = []
LV = [("high", f"{S}-etched.step", True, "the etched GLB itself: etched STEP through step2glb --compress none, physical materials by colour (provenance/tools/materials_bycolour.py)"),
      ("medium", f"{S}-etched.step", True, "the etched STEP through step2glb with its default KHR_mesh_quantization; same tessellation, virtual mark kept"),
      ("low", f"{S}.step", False, "the unmarked STEP re-meshed by OCCT BRepMesh at 0.02 mm linear / 0.9 rad angular deflection (provenance/tools/coarse_glb.py); gull-wing bends kept as chords, pin-1 dimple kept, mark omitted (unreadable at this size)"),
      ("super-low", "provenance/tools/superlow_ms12.py", False, "a box stand-in built from the measured dimensions (provenance/tools/superlow_ms12.py: body box, each lead as foot + riser + shoulder), step2glb quantized; silhouette, 12 leads and seating plane kept; mark and pin-1 dimple omitted")]
for name, src, mark, how in LV:
    f = f"lod/{S}-{name}.glb"; st = glbstats(f)
    levels.append({"level": name, "path": f, "sha256": sha(f), "sizeBytes": size(f), **st, "derivedFrom": src,
                   "markingIncluded": mark, "helperNodes": [], "derivation": how,
                   "review": "candidate: inspected as cached images (depth-buffered render); not qualified in a streaming LOD viewer",
                   "preview": {"path": f"lod/{S}-{name}.png", "sha256": sha(f"lod/{S}-{name}.png"), "view": "iso, same camera on every level"}})
lod = {"schema": "adom/component-lod@1", "mpn": MPN, "units": "metres (glTF)", "upAxis": "Z (Adom/KiCad frame; seated on z = 0, centred on the footprint origin)",
       "status": "candidate", "purpose": "clean board-use models: no footprint pads, outlines, labels or silkscreen (those are only in the page hero)",
       "source": {"path": f"{S}-etched.step", "sha256": sha(f"{S}-etched.step")},
       "sourceToDisplay": "uniform scale 0.001 (mm to m); no rotation, no translation; origin = footprint origin, seat z = 0",
       "generator": "step2glb (service) + provenance/tools/materials_bycolour.py, coarse_glb.py, superlow_ms12.py; previews provenance/tools/render_glb.py",
       "removedHelpers": [], "levels": levels,
       "hints": ["All four levels share pose and units. high and medium carry the virtual MPN (Adom.LaserEtch); low and super-low omit it."]}
json.dump(lod, open(p_("lod/manifest.json"), "w"), indent=1)

# ---------------- provenance/3d-model.json
m3d = {
 "schema": "adom/3d-model-provenance@1", "mpn": MPN, "date": TODAY,
 "classification": "KiCad library model used as is (method 1 of the wiki-component ladder): every printed dimension of ADI's MS12 drawing is met and the leads have radiused gull-wing bends. Not manufacturer CAD.",
 "method_chosen": "1 (KiCad's library model, unchanged geometry)",
 "manufacturer_step": {"tried": ["https://www.analog.com/en/products/ltc2945.html (product page; ADI's symbols, footprints and 3D models are served through a third-party CAD portal)"],
                       "result": "HTTP 403 to a plain request on 2026-10-21 (the datasheet and the package drawing PDFs downloaded fine). No browser session was used, so ADI's own STEP was not obtained.",
                       "status": "not reachable"},
 "kicad_model": {"source": "KiCad packages3D Package_SO.3dshapes/MSOP-12_3x4.039mm_P0.65mm.step (gitlab.com/kicad/libraries/kicad-packages3D, master, fetched 2026-10-21), CC-BY-SA 4.0 with the KiCad library exception",
                 "file": KREF, "sha256": sha(KREF), "footprint_descr": "KiCad's footprint cites the same LTC drawing 05-08-1668 (ADI ltc-legacy-msop/05081668_a_ms12.pdf)"},
 "page_model": {"file": f"{S}.step", "sha256": sha(f"{S}.step"), "identical_to_kicad_bytes": sha(f"{S}.step") == sha(KREF)},
 "check_against_drawing": {
  "drawing": "LTC DWG 05-08-1668 Rev A (datasheet Rev. D p.32; docs/05081668_a_ms12.pdf)",
  "method": "provenance/tools/dims.py: plane sections of the STEP (body at y = 0 and x = 0; lead profile through pin 1's centre line y = +1.625; JEDEC foot L where the lower outer surface crosses z = 0.25)",
  "rows": [
   {"dim": "D, body length (pin-row direction)", "printed": "4.039 +-0.102", "model": dims["D_body_length"], "pass": abs(dims["D_body_length"] - 4.039) <= 0.102},
   {"dim": "E1, body width", "printed": "3.00 +-0.102", "model": dims["E1_body_width"], "pass": abs(dims["E1_body_width"] - 3.0) <= 0.102},
   {"dim": "E, lead tip to tip", "printed": "4.90 +-0.152", "model": dims["E_tip_to_tip"], "pass": abs(dims["E_tip_to_tip"] - 4.9) <= 0.152},
   {"dim": "A, overall height", "printed": "1.10 max", "model": dims["A_overall_height"], "pass": dims["A_overall_height"] <= 1.10},
   {"dim": "A2, body thickness", "printed": "0.86 REF", "model": dims["A2_molded_thickness"], "pass": abs(dims["A2_molded_thickness"] - 0.86) <= 0.05},
   {"dim": "A1, standoff", "printed": "0.1016 +-0.0508", "model": dims["A1_standoff"], "pass": abs(dims["A1_standoff"] - 0.1016) <= 0.0508},
   {"dim": "b, lead width", "printed": "0.22 to 0.38", "model": dims["b_lead_width"][0], "pass": 0.22 <= dims["b_lead_width"][0] <= 0.38},
   {"dim": "c, lead thickness", "printed": "0.18", "model": dims["c_lead_thickness"], "pass": abs(dims["c_lead_thickness"] - 0.18) <= 0.05},
   {"dim": "e, pitch", "printed": "0.65 BSC", "model": dims["pitch"][0], "pass": all(abs(x - 0.65) < 1e-3 for x in dims["pitch"])},
   {"dim": "L, foot at the gauge plane", "printed": "0.53 +-0.152 (gauge plane 0.254)", "model": dims["L_gauge"], "pass": abs(dims["L_gauge"] - 0.53) <= 0.152}],
  "bends": "radiused gull-wing bends (KiCad generator), not square folds",
  "overlay": {"image": f"docs/proof/{S}-overlay.png", "record": "provenance/overlay-proof.json", "note": ov["drawing_note"]}},
 "frame": "Z up, millimetres, seated on z = 0, centred on the footprint origin; pin 1 at (-x, +y) = footprint pad 1 at (-2.107, -1.625) in KiCad's Y-down footprint frame; binds with zero offset and rotation",
 "pin1_mark": "0.6 mm round dimple in the top face near pin 1 (KiCad model); ADI draws a dot at the pin-1 corner of the top view (p.32)",
 "etch": {"file": f"{S}-etched.step", "sha256": sha(f"{S}-etched.step"), "tool": "adom-aiflow-etch (planner) on adom-chip-laser / service-occt /laser-etch, flat mode",
          "text": ["LTC2945", "CMS#PBF"], "cap_height_mm": 0.370, "fit": "2 lines, 2.11 x 0.87 mm block in a 2.78 x 2.71 mm band (adom-aiflow-etch output)",
          "gate": chk.get("status", "PASS"), "plan": f"docs/proof/{S}-etched.mark-plan.json", "check": f"docs/proof/{S}-etched.check.png",
          "virtual": True, "note": "Virtual identification mark (the orderable MPN), not ADI's real marking (2945, datasheet p.4)."},
 "glb": {"plain": {"file": f"{S}.glb", "sha256": sha(f"{S}.glb")}, "etched": {"file": f"{S}-etched.glb", "sha256": sha(f"{S}-etched.glb")},
         "how": "step2glb convert --compress none (service), then provenance/tools/materials_bycolour.py (epoxy metallic 0 roughness 0.55, matte tin leads, pin-1 dimple, virtual mark)"},
 "hero": {"file": f"{S}-hero.glb", "sha256": sha(f"{S}-hero.glb"), "generator": "adom/component-hero 0.1.2 scripts/hero_glb.py (adom-hero standard)",
          "inputs": [f"{S}-etched.glb", f"{S}.kicad_mod", "page.json component.pins"], "result": "pads 12, silk items 3, pin1 added dot (AI), labels 12; align OK (offset 0.0 mm)",
          "pin1_note": "The generator did not take the footprint's pin-1 silk tick as a pin-1 mark and added a dot beside pad 1 (recorded as AI-added in the hero metadata)."},
}
json.dump(m3d, open(p_("provenance/3d-model.json"), "w"), indent=1)

# ---------------- provenance/symbol.json
orig = open(os.path.join(work, "sym-original-pins.txt")).read().strip().splitlines()
symrec = {"schema": "adom/symbol-provenance@1", "mpn": MPN, "date": TODAY,
          "tool": "adom-symbol 1.2.6 create (provenance/tools/symbol-create.json), chip outline baked from artwork/LTC2945CMS-PBF-3d-outline-dark-named.svg (adom-aiflow-etch outline of the etched model)",
          "grid_fix": {"problem": "adom-symbol 1.2.6 put the first pin of each column at y = -2.00 and spaced groups 8.35 mm apart, so 10 of the 12 pin ends were off the 2.54 mm grid (known bug)",
                       "fix": "provenance/tools/snap_symbol_groups.py: per column, first pin at -2.54, 2.54 within a group, 7.62 between groups; captions moved with their first pin; body bottom and the field column moved by the same amount; Value set to the MPN, Footprint to ltc2945cms-pbf:ltc2945cms-pbf (1.2.6 wrote the package text there)",
                       "original_pins": orig},
          "groups": {"left": ["I2C: SCL 7, SDAI 8, SDAO 9, ALERT 10", "Address: ADR1 3, ADR0 4"], "right": ["Power: VDD 1, INTVCC 2", "Ground: GND 6", "Current sense: SENSE+ 12, SENSE- 11", "ADC input: ADIN 5"]},
          "types": "input: SCL, SDAI, ADR0, ADR1, SENSE+, SENSE-, ADIN; open_collector: SDAO, ALERT (open drain, p.9); power_in: VDD, INTVCC, GND. INTVCC is typed power_in although it can also be the 5 V regulator output (p.9): add a PWR_FLAG or retype in the schematic when it supplies other parts.",
          "unconnected_pins": "none: all 12 MS12 pins have functions (p.9), so no pin is typed unconnected",
          "fusion_lbr": {"file": f"{S}.lbr", "sha256": sha(f"{S}.lbr"), "tool": "adom-lbr 2.47.5 generate --part-metadata page.json",
                         "check": "12 pins; directions in (7), oc (2: SDAO, ALERT), pwr (3); no pin carries a visible= attribute, so EAGLE's default 'both' shows pin names and pad numbers; connects pin n to pad n; lint PASS. The adom-lbr 2.47.5 nc-typed-as-io and hidden-name bugs were checked for and not triggered (this part has no NC pins); no hand fix was needed."},
          "altium": {"schlib": "altium/LTC2945CMS-PBF.SchLib", "schlib_sha256": sha("altium/LTC2945CMS-PBF.SchLib"),
                     "pcblib": "altium/LTC2945CMS-PBF.PcbLib", "pcblib_sha256": sha("altium/LTC2945CMS-PBF.PcbLib"),
                     "tool": "adom-lbr 2.47.5 import-kicad + assemble-altium --no-dedupe, with the etched STEP embedded in the PcbLib (models.embedded 1)",
                     "check": "pin records read straight from the SchLib binary by provenance/tools/schlib_pins.py (independent of adom-lbr's reader): 12 pins, electrical types input/open_collector/power as in the KiCad symbol, name and designator visible on every pin (conglomerate 0x38 / 0x3a), none hidden; no hand fix was needed",
                     "pins": schpins}}
json.dump(symrec, open(p_("provenance/symbol.json"), "w"), indent=1)

# ---------------- models.json
mark = {"text": "LTC2945 / CMS#PBF (two lines)", "surface": "top, along the pin rows, upright with pin 1 lower left", "colour": "light grey on black epoxy",
        "virtual": True, "note": "virtual identification mark (the orderable MPN) by adom-chip-laser; ADI's real marking is 2945 (datasheet p.4)"}
def rec(path, fmt, title, slot, variant, role, use, marking, derived, unique, source, thumb=None, status="current"):
    r = {"path": path, "format": fmt, "sha256": sha(path), "git_blob": blob(path), "bytes": size(path), "title": title, "status": status,
         "slot": slot, "variant": variant, "role": role, "use": use, "marking": marking, "colours": True, "derived_from": derived,
         "unique": unique, "source": source}
    if thumb: r["thumb"] = thumb
    return r
models = {"schema": "adom/models@1", "part": MPN, "updated": TODAY, "models": [
 rec(f"{S}-etched.step", "step", f"{MPN}, virtual MPN etch (library model)", "model_3d_step", "etched", "derived", ["board-cad"], mark, f"{S}.step",
     "The board-CAD model: KiCad's MSOP-12 model with the MPN etched on top in the Adom.LaserEtch group; bound by the KiCad footprint, the Fusion .lbr and the Altium PcbLib",
     "KiCad packages3D MSOP-12_3x4.039mm_P0.65mm + adom-chip-laser (service-occt etch, placed by adom-aiflow-etch)", f"docs/models/{S}-etched-iso.png"),
 rec(f"{S}.step", "step", f"{MPN}, unmarked", "model_3d_step_plain", "plain", "derived", ["board-cad", "archive"], None, KREF,
     "The same package without any marking; byte-identical to KiCad's library model", "KiCad packages3D (CC-BY-SA 4.0 with the library exception)", f"docs/models/{S}-glb-top.png"),
 rec(f"{S}-hero.glb", "glb", f"{MPN} hero (page 3D viewer)", "model_3d", "hero", "derived", ["wiki-hero"], mark, f"{S}-etched.glb",
     "Adds the footprint's teal dashed pad outlines, pin names beside the pads and the silkscreen with a pin-1 dot under the chip; for the page only, never for boards",
     "adom/component-hero hero_glb.py"),
 rec(f"{S}-etched.glb", "glb", f"{MPN}, virtual MPN etch", "model_3d_plain", "etched", "derived", ["render", "board-insertion"], mark, f"{S}-etched.step",
     "Clean GLB of the library model with physical materials (epoxy, matte tin); what boards and digital twins should place", "step2glb + materials_bycolour.py", f"docs/models/{S}-etched-top.png"),
 rec(f"{S}.glb", "glb", f"{MPN}, unmarked", None, "plain", "derived", ["render"], None, f"{S}.step",
     "Unmarked GLB with physical materials", "step2glb + materials_bycolour.py", f"docs/models/{S}-glb-top.png"),
 rec(KREF, "step", "KiCad library model MSOP-12_3x4.039mm_P0.65mm (as downloaded)", None, "original", "derived", ["archive"], None,
     "gitlab.com/kicad/libraries/kicad-packages3D", "The upstream file, kept for comparison; same bytes as the unmarked page model", "KiCad packages3D master, 2026-10-21")]}
json.dump(models, open(p_("models.json"), "w"), indent=1)

# ---------------- verification.json
def ent(title, scope, checks, evidence, files, status="verified"):
    return {"date": TODAY, "by": BY, "status": status, "title": title, "scope": scope, "variants": ["*"],
            "checks": [{"text": t, "result": r} for t, r in checks], "evidence": [{"label": os.path.basename(e), "href": e} for e in evidence],
            "files": {f: sha(f) for f in files}}
pinlist = ", ".join(f"{n} {pins[n]['name']}" for n in sorted(pins, key=int))
ver = {"schema": "adom/verification@1", "part": MPN,
       "variants": [{"mpn": MPN, "note": "0 C to 70 C grade, MS12; LTC2945CMS#TRPBF is the same part on tape and reel (datasheet p.4)", "stocked": False}],
       "entries": [
 ent("Pinout matches the datasheet", ["symbol"], [
     (f"Symbol pins match the MS package pin configuration (Rev. D p.3) and pin functions (p.9): {pinlist}", "pass"),
     ("Electrical types: SCL, SDAI, ADR0, ADR1, SENSE+, SENSE-, ADIN input; SDAO, ALERT open collector (open drain); VDD, INTVCC, GND power input", "pass"),
     ("No unconnected pins on this package, so no pin is typed nc/unconnected in any library", "pass"),
     ("Pin n maps to pad n, read back from the Fusion .lbr connects and from the Altium SchLib pin records", "pass"),
     ("Every pin end on the 2.54 mm grid (adom-symbol 1.2.6 put 10 of 12 off grid; fixed by provenance/tools/snap_symbol_groups.py)", "note"),
     ("Altium SchLib: names and designators visible on all 12 pins, types as in KiCad (read from the binary by schlib_pins.py); Fusion .lbr: pin names shown (default visible=both). No hand fix was needed", "pass")],
     [f"docs/figures/{S}-ds-p3.png", f"docs/figures/{S}-ds-p9.png", f"docs/proof/{S}-symbol.png"], [f"{S}.kicad_sym", f"{S}.lbr", "altium/LTC2945CMS-PBF.SchLib"]),
 ent("Footprint equals ADI's recommended solder pad layout", ["footprint"], [
     ("All 12 pads equal LTC DWG 05-08-1668 Rev A: 0.42 x 0.889 mm rectangles, pitch 0.65, pad centres at x = +-2.107 (inner gap 3.325 mm, the middle of the printed 3.20 to 3.45; outer extent 5.103 mm, printed 5.10 min)", "pass" if fpc["all_pads_match"] else "fail"),
     ("KiCad's MSOP-12_3x4.039mm_P0.65mm differs (1.45 x 0.40 roundrect at +-2.15: 0.2375 mm further under the body, 0.3235 mm further out); recorded in provenance/footprint-check.json", "note"),
     ("Every 3D-model lead contact (section at z = 0.01 mm) lies inside its pad", "pass" if fpc["model_contact"]["all_inside"] else "fail")],
     [f"docs/figures/{S}-ds-p32-ms12-crop.png", f"docs/proof/{S}-footprint-check.png"], [f"{S}.kicad_mod", "altium/LTC2945CMS-PBF.PcbLib"]),
 ent("Pin 1 orientation agrees across datasheet, footprint, 3D model and symbol", ["footprint", "model", "symbol"], [
     ("Datasheet: MS package top view (p.3) has pin 1 (VDD) top left with 1 to 6 down the left side and 7 to 12 up the right; the package drawing (p.32) marks pin 1 with a dot", "pass"),
     ("Footprint: pad 1 at (-2.107, -1.625), top left in KiCad's Y-down frame, pads counter-clockwise seen from the top (the datasheet top view, not mirrored); silkscreen pin-1 tick above the pad-1 end", "pass"),
     ("3D model: pin-1 dimple at the (-x, +y) corner of the top face = pad 1's corner; the hero generator found the model aligned with the footprint (offset 0.0 mm)", "pass"),
     ("Symbol: pin numbers carry the datasheet names (1 VDD ... 12 SENSE+); the etched name reads upright with pin 1 lower left (etch gate PASS)", "pass")],
     [f"docs/figures/{S}-ds-p3.png", f"docs/figures/{S}-ds-p32-ms12-crop.png", f"docs/proof/{S}-hero-preview.png", f"docs/proof/{S}-etched.check.png"],
     [f"{S}.kicad_mod", f"{S}-etched.step", f"{S}.kicad_sym"]),
 ent("3D model inside ADI's package drawing", ["model"], [
     (f"Dimensions measured on the STEP: D {dims['D_body_length']}, E1 {dims['E1_body_width']}, E {dims['E_tip_to_tip']}, A {dims['A_overall_height']}, A1 {dims['A1_standoff']}, b {dims['b_lead_width'][0]}, c {dims['c_lead_thickness']}, e 0.65, L {dims['L_gauge']} mm; all inside 05-08-1668 Rev A", "pass"),
     ("Overlay on the drawing (top and side, each axis calibrated on printed extension lines because the drawing is not to scale): body, leads and pin-1 mark line up", "pass"),
     ("Model source: KiCad's library model, used unchanged (ADI's own STEP was behind a CAD portal that refused a plain request)", "note")],
     [f"docs/proof/{S}-overlay.png", f"docs/figures/{S}-ds-p32.png"], [f"{S}.step", f"{S}-etched.step"]),
 ent("Still to do", ["*"], [
     ("Not yet reviewed by a person", "open"),
     ("ADI's own STEP not compared (not reachable without a browser session)", "open"),
     ("Fusion, KiCad and Altium imports not opened in the desktop applications for this page (files checked by parsing only)", "open")],
     [], [], status="open")]}
json.dump(ver, open(p_("verification.json"), "w"), indent=1)

# ---------------- provenance/files.json
allf = sorted(os.path.relpath(os.path.join(d, f), page) for d, _, fs in os.walk(page) for f in fs if f != "page.json")
json.dump({"schema": "adom/files@1", "date": TODAY, "files": {f: sha(f) for f in allf if not f.startswith("provenance/files.json")}},
          open(p_("provenance/files.json"), "w"), indent=1)
print("OK records", len(levels), "lods", len(models["models"]), "models", len(ver["entries"]), "entries")