component
AS5047P-ATSM
Public Made by Adomby adom
AMS Position Sensors, TSSOP-14. Pinout extracted from the datasheet, KiCad symbol/footprint/3D.
main
John Lauer
KiCad's library model checked against ams' limits and kept (foot 0.529 mm at the gauge plane, in 0.45..0.75); four LODs…
cb10da4
16d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
#!/usr/bin/env python3
"""Add a 3D model record, LODs and model sections to a component page that had none (TMC2226-SA-T,
AS5047P-ATSM). usage: page_new.py <slug> <page dir>
Writes: provenance/3d-model.json, lod/ (4 GLB, 4 previews, manifest), docs/proof/*, README sections;
for a regenerated model also the STEP/GLB/hero files and provenance/previous + tools."""
import sys, os, json, shutil, hashlib, re
from PIL import Image
W = os.path.expanduser("~/project/kicad-gen/work")
slug, page = sys.argv[1], sys.argv[2]
D, O = f"{W}/{slug}", f"{W}/{slug}/out"
def sha(p): return hashlib.sha256(open(p, "rb").read()).hexdigest()
def cp(a, b): os.makedirs(os.path.dirname(b) or ".", exist_ok=True); shutil.copy2(a, b)
METHOD = ("section through one lead's centre line, cut at the 0.25 mm gauge plane; L = lead tip to where the lead's lower outer surface "
"crosses that plane (JEDEC), provenance/tools/kicad-generator/measure.py")
if slug == "tmc2226-sa-t":
regenerated = True
rows = [("D body size X", "9.60 / 9.70 / 9.80", "9.700"), ("E width over pins", "6.20 / 6.40 / 6.60", "6.400"), ("E1 body size Y", "4.30 / 4.40 / 4.50", "4.400"),
("A total thickness", "max 1.20", "1.100"), ("A1 stand off", "0.05 / - / 0.15", "0.100"), ("A2 mold thickness", "0.90 / 1.00 / 1.10", "1.000"),
("b lead width", "0.20 / - / 0.29", "0.245"), ("c lead frame thickness", "0.13 / - / 0.18", "0.155"), ("e lead pitch", "0.55 / 0.65 / 0.75", "0.650"),
("L lead (to the 0.25 mm gauge plane, L2)", "0.45 / 0.60 / 0.75", "0.600 (measured)"), ("R, R1 bend radii", "min 0.09", "0.09, 0.09"),
("D1 x E2 exposed die pad", "6.20 x 2.75 REF", "6.200 x 2.750 (measured)"), ("theta 1..3 body draft", "10 / 12 / 14 deg", "12 deg")]
check = dict(model="the page's previous model (labelled KiCad packages3D HTSSOP-28)", measured="width over pins and lead shape",
datasheet="E 6.20 / 6.40 / 6.60; gull-wing leads", previous="7.20 mm over pins, straight unbent leads, exposed pad 2.85 x 6.70",
regenerated="6.400 mm over pins, radiused gull-wing leads, foot 0.600 mm, exposed pad 6.20 x 2.75",
verdict="The page's previous model was a plain block with straight, unbent leads 0.6 mm over Trinamic's maximum width over pins, with the footprint's 2.85 x 6.7 mm land as its exposed pad. It was regenerated with KiCad's own package generator from the datasheet's p.81 table, with the true 6.20 x 2.75 mm exposed die pad.")
values = ("TMC2226 datasheet Rev. 1.06 p.81 nominal values: D 9.70, E 6.40, E1 4.40, A2 1.00, e 0.65, L 0.60 (gauge plane L2 = 0.25), R = R1 = 0.09, draft 12 deg; "
"midpoints where only limits are printed: A1 0.10, b 0.245, c 0.155; exposed die pad D1 6.20 x E2 2.75 (REF).")
mark = dict(lines=["TMC2226-SA"], source="TMC2226 datasheet p.81, 22.2 Package Codes: Code & marking TMC2226-SA",
note="etched with --over-vendor-mark (the generated body has no printed marking; its faces read as glyphs to the detector) and the real one-line marking; gate WARN size only (one line cannot fill the band as two lines would), every other check PASS")
else:
regenerated = False
rows = [("D body length", "4.90 / 5.00 / 5.10", "5.000"), ("E width over pins", "6.40 BSC", "6.400"), ("E1 body width", "4.30 / 4.40 / 4.50", "4.400"),
("A overall height", "max 1.20", "1.000"), ("A1 standoff", "0.05 / - / 0.15", "0.100"), ("A2 body thickness", "0.80 / 1.00 / 1.05", "0.900"),
("e pitch", "0.65 BSC", "0.650"), ("L foot (to the 0.25 mm gauge plane)", "0.45 / 0.60 / 0.75", "0.529 (measured)"), ("R, R1 bend radii", "min 0.09", "0.10, 0.10 (KiCad)")]
check = dict(model="KiCad packages3D TSSOP-14_4.4x5mm_P0.65mm.step (CC-BY-SA 4.0)", measured="every printed dimension, foot at the gauge plane",
datasheet="ams AS5047P datasheet v2-00 p.31 Figure 40", previous="-", regenerated="not needed",
verdict="KiCad's library model is kept: every printed dimension is inside ams' limits (foot 0.529 mm against 0.45 to 0.75, A2 0.90 against 0.80 to 1.05) and its leads have KiCad's radiused bends. ams' drawing is not to scale (the top view is drawn with a break line), so the proof is this measured table, not an overlay.")
values = "unchanged KiCad library model"
mark = None
# files
stats = {s["file"]: s for s in json.load(open(f"{O}/stats.json"))}
for l in ("high", "medium", "low", "super-low"):
for ext in ("glb", "png"): cp(f"{O}/lod/{slug}-{l}.{ext}", f"{page}/lod/{slug}-{l}.{ext}")
for f in (f"{slug}.glb", f"{slug}-etched.glb"): cp(f"{O}/{f}", f"{page}/{f}")
T = f"{page}/provenance/tools/kicad-generator"
for f in ("materials.py", "render.py", "measure.py"): cp(f"{W}/{f}", f"{T}/{f}")
cp(os.path.expanduser("~/project/kicad-gen/node3/lod.mjs"), f"{T}/lod.mjs"); cp(f"{W}/page_new.py", f"{T}/page_new.py")
if regenerated:
prev = f"{page}/provenance/previous/{slug}-previous.step"
if not os.path.exists(prev): cp(f"{page}/{slug}.step", prev)
prev_hash = sha(prev)
for f in (f"{slug}.step", f"{slug}-etched.step"): cp(f"{D}/{f}", f"{page}/{f}")
for f in (f"{slug}-hero.glb", f"{slug}-hero.overlay.json"): cp(f"{O}/{f}", f"{page}/{f}")
for f in ("kgen.py", "assets.sh", "kicad-generator-A2_b.patch"): cp(f"{W}/{f}", f"{T}/{f}")
cp(f"{W}/tmc.json", f"{T}/params.json"); cp(f"{D}/superlow.json", f"{T}/params-superlow.json"); cp(f"{D}/overlay.json", f"{T}/overlay.json")
for f in (f"{slug}-overlay-tmc2226-ds-p80.png", f"{slug}-leads-before-after.png", f"{slug}-hero-preview.png"): cp(f"{O}/{f}", f"{page}/docs/proof/{f}")
for f in (f"{slug}-etched.check.png", f"{slug}-etched.check.json", f"{slug}-etched.mark-plan.json"): cp(f"{D}/{f}", f"{page}/docs/proof/{f}")
# LOD manifest (schema as on the other pages)
deriv = {"high": "etched STEP, uncompressed; primitives batched per part (lossless)",
"medium": "etched STEP, batched, quantized (KHR_mesh_quantization); marking kept",
"low": "plain STEP, welded and simplified (meshopt, ratio 0.5, error 0.2 %), quantized; marking omitted",
"super-low": ("undrafted, unfilleted generator run, simplified (ratio 0.25, error 0.4 %), quantized; marking omitted" if regenerated
else "plain STEP, simplified (ratio 0.25, error 0.8 %), quantized; terminals and pin-1 cue kept, marking omitted")}
levels = []
for l in ("high", "medium", "low", "super-low"):
f = f"{page}/lod/{slug}-{l}.glb"; s = stats[f"{slug}-{l}.glb"]; png = f"{page}/lod/{slug}-{l}.png"; im = Image.open(png)
levels.append(dict(level=l, path=f"lod/{slug}-{l}.glb", sha256=sha(f), sizeBytes=os.path.getsize(f), decodedBufferBytes=s["decodedBytes"],
triangles=s["triangles"], primitives=s["primitives"], boundsMm=s["boundsMm"], markingIncluded=bool(s["mark"]), mark=s["mark"],
helperNodes=s["helperNodes"], extensions=s["extensions"], derivation=deriv[l],
review="candidate: inspected as cached images (depth-buffered render); not qualified in a streaming LOD viewer",
preview=dict(path=f"lod/{slug}-{l}.png", sha256=sha(png), sizeBytes=os.path.getsize(png), width=im.width, height=im.height,
view="iso (front-left, from above), same camera on every level", sourceLevelSha256=sha(f))))
mpn = json.load(open(f"{page}/page.json"))["component"]["mpn"]
json.dump(dict(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", updated="2026-10-21", source=dict(path=f"{slug}-etched.step", sha256=sha(f"{page}/{slug}-etched.step")),
generator="step2glb 0.13.2 -> glTF Transform 3.10.1 (dedup, join per part, prune; weld + meshopt simplify on low/super-low; quantize) -> materials.py (tin leads); previews by render.py",
hints=["Board-use files: no footprint pad outlines, pad labels, silkscreen or board artwork (helperNodes is empty on every level)."],
levels=levels), open(f"{page}/lod/manifest.json", "w"), indent=1)
# provenance record
rec = dict(schema="adom/3d-model-provenance@1", mpn=mpn, date="2026-10-21",
model=dict(file=f"{slug}.step", sha256=sha(f"{page}/{slug}.step"),
builder=("kicad-packages3D-generator (commit e272fcc, GW_QFP_SOIC_SSOP_TSSOP_SOT/make_gw, CadQuery 2.8.0) via provenance/tools/kicad-generator/kgen.py" if regenerated else "KiCad packages3D library model, unchanged"),
values=values, dimensions_mm=[dict(item=a, datasheet_min_typ_max=b, model=v) for a, b, v in rows],
library_check=dict(check, method=METHOD),
leads="radiused gull-wing leads; GLB material 'Adom.Leads (matte tin)': metallic 1.0, roughness 0.35",
**({"previous": dict(file=f"provenance/previous/{slug}-previous.step", sha256=prev_hash, why="block body with straight, unbent leads, 7.2 mm over pins")} if regenerated else {})),
etched=dict(file=f"{slug}-etched.step", sha256=sha(f"{page}/{slug}-etched.step"), **({"marking": mark} if mark else {"note": "the page's existing MPN etch, unchanged"})),
glb={f: sha(f"{page}/{f}") for f in (f"{slug}.glb", f"{slug}-etched.glb") + ((f"{slug}-hero.glb",) if regenerated else ())},
lod="lod/manifest.json")
os.makedirs(f"{page}/provenance", exist_ok=True)
json.dump(rec, open(f"{page}/provenance/3d-model.json", "w"), indent=1)
# README
R = open(f"{page}/README.md").read()
lodrows = "\n".join(f"| {l['level']} | [{slug}-{l['level']}.glb](lod/{slug}-{l['level']}.glb) | {l['triangles']} | {l['primitives']} | {round(l['sizeBytes']/1024)} KiB | {'yes' if l['markingIncluded'] else 'no'} |" for l in levels)
dims = "\n".join(f"| {a} | {b} | {v} |" for a, b, v in rows)
if regenerated:
R = R.replace("- 3D model: HTSSOP-28 (KiCad packages3D, STEP)", "- 3D model: HTSSOP-28 made with KiCad's own package generator at the datasheet's values, with the true exposed die pad (below)", 1)
R = R.replace("carry the MPN laser-etched on the die", "carry the real marking (TMC2226-SA) laser-etched on the die", 1)
R = R.replace("## CAD (KiCad official)", "## CAD (KiCad footprint and symbol; 3D model from KiCad's package generator)", 1)
block = f"""## 3D model
The model is made with **KiCad's own package generator** (kicad-packages3D-generator, the CadQuery scripts behind KiCad's 3D library), so the leads have real radiused gull-wing bends, run with the datasheet's values. {values} The annotated hero is [{slug}-hero.glb]({slug}-hero.glb) (adom-hero 1.4, display only); the board-use model is [{slug}-etched.glb]({slug}-etched.glb).
**Why it was regenerated:** {check['verdict']}
## Leads: before and after

## Model proof: overlay on the datasheet drawing

| Dimension (mm) | Datasheet min / typ / max | Model |
|---|---|---|
{dims}
Marking: {mark['note']}. Record: [provenance/3d-model.json](provenance/3d-model.json); the previous model is kept in [provenance/previous/](provenance/previous/{slug}-previous.step).
"""
else:
block = f"""## 3D model check
KiCad's library model is kept. {check['verdict']}
| Dimension (mm) | Datasheet min / typ / max | Model |
|---|---|---|
{dims}
Measured {METHOD}. Record: [provenance/3d-model.json](provenance/3d-model.json).
"""
block += f"""## Detail levels for board and digital-twin viewers
::adom-component-lod{{src="/adom/{slug}/render/lod/manifest.json"}}
| Level | File | Triangles | Draw calls | Size | Marking |
|---|---|---|---|---|---|
{lodrows}
All four levels share the Z-up frame, units and seating of the library model, carry no footprint or silkscreen artwork, and keep the real terminals and the pin-1 cue; the leads are matte tin. Low and super-low omit the marking (unreadable at those sizes). Status: candidate; the cached previews were inspected, a streaming LOD viewer has not qualified them.
"""
if "## Detail levels for board" not in R:
R = R.replace("## Provenance", block + "## Provenance", 1)
open(f"{page}/README.md", "w").write(R)
print("OK", slug)