component
TMC2262
Public Made by Adomby adom
Analog Devices TMC2262 65 V, 4 A RMS smart stepper driver with step/dir, SPI and encoder input, 30-pin FC2QFN 6x6. KiCad, Fusion and Altium libraries, datasheet-built 3D model, every pin cited to the data sheet.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
"""Build the component page folders pages/<slug>/ for the LAN8770M and the TMC2262 from pages_def.py.
python3 build.py [slug ...] [--stage cad|hero|libs|art|docs|meta|all]
Inputs: datasheets/ (manufacturer PDFs), build/models (CadQuery STEPs, tools/build_models.py), build/fp (footprints,
tools/footprints.py), build/sym (adom-symbol), the chip-fetcher library (adom-chip-thumbnailer: etched STEP, outlines).
"""
import datetime, glob, hashlib, json, os, re, shutil, subprocess, sys
H = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, H); sys.path.insert(0, os.path.join(H, 'tools'))
import pages_def as PD
OWNER = 'adom'
LIB = '/home/adom/project/chip-fetcher/library'
HERO_GEN = os.path.expanduser('~/.claude/skills/component-hero/scripts/hero_glb.py')
CW = '/tmp/claude-1001/-home-adom-project/4adf5409-bffa-49ac-86cd-045d126c5cb0/scratchpad/component-widgets/tools'
TODAY = datetime.date.today().isoformat()
VERSION = '0.1.0'
def run(cmd, cwd=None, ok=(0,)):
r = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
if r.returncode not in ok:
raise SystemExit('%s failed (%d): %s %s' % (' '.join(cmd[:3]), r.returncode, r.stdout[-1500:], r.stderr[-1500:]))
return r.stdout + r.stderr
def sha(p):
return hashlib.sha256(open(p, 'rb').read()).hexdigest()
def art(d):
"""Artwork file stem: the chip-fetcher library name, '+' spelled out so URLs stay plain."""
return d['lib_mpn'].replace('+', '-PLUS')
def pdir(d, *p):
return os.path.join(H, 'pages', d['slug'], *p)
def sym_name(d):
return 'LAN8770M' if d['slug'] == 'lan8770m' else 'TMC2262'
# ------------------------------------------------------------------------------------------------ CAD
def stage_cad(d):
s = d['slug']; P = pdir(d)
for sub in ('', 'docs', 'docs/figures', 'viewers', 'altium', 'artwork', 'provenance', 'provenance/tools'):
os.makedirs(pdir(d, sub), exist_ok=True)
shutil.copy(os.path.join(H, 'build', 'fp', s, s + '.kicad_mod'), pdir(d, s + '.kicad_mod'))
shutil.copy(os.path.join(H, 'build', 'sym', s, sym_name(d) + '.kicad_sym'), pdir(d, s + '.kicad_sym'))
shutil.copy(os.path.join(H, 'build', 'models', d['lib_mpn'] + '.step'), pdir(d, s + '.step'))
shutil.copy(os.path.join(LIB, d['lib_mpn'], d['lib_mpn'] + '-etched.step'), pdir(d, s + '-etched.step'))
for st in (s, s + '-etched'):
print(run(['step2glb', 'convert', pdir(d, st + '.step'), '-o', pdir(d, st + '.glb')]).strip().splitlines()[-1][:200])
print('cad', s, 'ok')
# ------------------------------------------------------------------------------------------------ page.json
def page_json(d, extra_parts=None):
s = d['slug']
dist = {}
srch = {}
for q in ('TMC2262', 'LAN8770M', 'LAN8770'):
p = os.path.join(H, 'data', 'parts-search-%s.json' % q)
if os.path.exists(p):
raw = open(p).read(); raw = raw[raw.index('{'):]
js = json.JSONDecoder().raw_decode(raw)[0]
for v in ('mouser', 'digikey', 'jlcpcb'):
for h in (js.get(v) or {}).get('hits', []):
srch[(v, h.get('vendor_pn'))] = h
for key, vend in (('mouser', 'mouser'), ('digikey', 'digikey'), ('lcsc', 'jlcpcb')):
pn = d['distributors'].get(key)
if not pn:
dist[key] = False
continue
h = srch.get((vend, pn), {})
url = h.get('product_url') or ('https://www.lcsc.com/product-detail/%s.html' % pn if key == 'lcsc' else '')
dist[key] = dict(part=pn, url=url)
if d.get('lcsc_note'):
dist['lcsc_note'] = d['lcsc_note']
parts = dict(symbol=s + '.kicad_sym', footprint=s + '.kicad_mod', model_3d=s + '-hero.glb', model_3d_plain=s + '-etched.glb',
model_3d_step=s + '-etched.step', model_3d_step_plain=s + '.step', lbr=s + '.lbr',
datasheet='docs/%s-datasheet.pdf' % s)
comp = dict(mpn=d['mpn'], manufacturer=d['manufacturer'], package=d['package'], pin_count=d['pin_count'],
category=d['category'], subcategory=d['subcategory'], body_size=d['body'], parts=parts,
distributor_links=dist, wiki_url='https://wiki.adom.inc/%s/%s' % (OWNER, s),
datasheet_url=d['ds']['url'], product_url=d['product_url'],
manufacturer_links=[dict(label=a, url=b) for a, b, c in d['resources']],
pins=[dict(number=n, name=nm, type=t, group=g, description=desc,
datasheet_page=pg) for n, nm, t, g, desc, pg in d['pins']],
ordering_variants=[dict(mpn=a, packing=b, temperature=c, note=e, source=f) for a, b, c, e, f in d['ordering']],
models_3d=[
dict(file=s + '-etched.step', role='library model: datasheet-built body with a virtual laser-etched part number (Adom.LaserEtch)', up_axis='Z'),
dict(file=s + '.step', role='the same datasheet-built body without the mark', up_axis='Z'),
dict(file=s + '-hero.glb', role='page hero only: etched chip + teal dashed pad outlines (50 %), silkscreen (30 %), pad signal labels (50 %), heatsink vias. Not for boards.', up_axis='Z'),
],
states=None, viewer_3d=dict(ground=True),
provenance=dict(datasheet=dict(id=d['ds']['id'], rev=d['ds']['rev'], url=d['ds']['url'], file=parts['datasheet']),
model_3d='datasheet-built (CadQuery, tools/build_models.py); no manufacturer STEP was downloadable without a CAD-portal account',
footprint='manufacturer land pattern, pad for pad (provenance/footprint-vs-datasheet.json)',
symbol='adom-symbol 1.2.6, pins and groups from the datasheet pin tables'))
comp.pop('states')
pj = dict(slug=s, title=d['title'], brief=d['brief'], version=VERSION, type='hardware', kind='component',
license='CC-BY-4.0',
hero=dict(type='image', path='docs/hero.png', headline=d['title'], subhead=d['hero_subhead'], screenshot='docs/hero.png', chrome=False),
tags=d['tags'], discovery_triggers=d['triggers'], component=comp,
visibility=dict(public=d['visibility_public']))
pj['source'] = dict(tool=d['manufacturer_key'], url=d['product_url'])
pj['eda_assets'] = dict(kicad=dict(symbol=s + '.kicad_sym', footprint=s + '.kicad_mod', model=s + '-etched.step', provenance='authored'),
fusion=dict(lbr=s + '.lbr', provenance='authored'),
altium=dict(schlib='altium/Adom_%s.SchLib' % s, pcblib='altium/Adom_%s.PcbLib' % s, provenance='authored'))
if extra_parts:
comp['parts'].update(extra_parts)
return pj
# ------------------------------------------------------------------------------------------------ hero GLB
def stage_hero(d):
s = d['slug']
json.dump(page_json(d), open(pdir(d, 'page.json'), 'w'), indent=1)
out = run([sys.executable, HERO_GEN, pdir(d, s + '-etched.glb'), pdir(d, s + '.kicad_mod'), pdir(d, 'page.json'), pdir(d, s + '-hero.glb'),
'--png', os.path.join(H, 'build', s + '-hero-preview.png')])
print(out.strip().splitlines()[-1])
out = run([sys.executable, os.path.join(H, 'tools', 'hero_post_vias.py'), pdir(d, s + '-hero.glb'), pdir(d, s + '.kicad_mod')])
print(out.strip().splitlines()[-1])
# ------------------------------------------------------------------------------------------------ libraries
def lanes_footprint(d):
"""The footprint as adom-lbr's Fusion and Altium lanes can carry it (2.47.5 drops KiCad custom-pad primitives and
turns paste-only pads into unnumbered copper): each L-shaped corner pad becomes its vertical and horizontal legs as
two overlapping rectangles with the same number (exact copper, without the R0.150 inner corner), and the exposed
pad takes full paste instead of the split windows. KiCad keeps the exact pads."""
s = d['slug']
t = open(pdir(d, s + '.kicad_mod')).read()
t = re.sub(r' \(pad "" smd rect \(at [^)]*\) \(size [^)]*\)\n \(layers "F.Paste"\)\)\n', '', t)
t = re.sub(r'(\(pad "EP" smd rect \(at 0 0\) \(size [^)]*\)\n \(layers "F.Cu") ("F.Mask"\))', r'\1 "F.Paste" \2', t)
def legs(m):
num, ax, ay = m.group(1), float(m.group(2)), float(m.group(3))
sx, sy = (1 if ax > 0 else -1), (1 if ay > 0 else -1)
v = (sx * 2.525, sy * 2.8125, 0.45, 0.975) # vertical leg: x 2.30..2.75, y 2.325..3.30 from the centre
h = (sx * 2.80, sy * 2.55, 1.0, 0.45) # horizontal leg: x 2.30..3.30, y 2.325..2.775
return ''.join(' (pad "%s" smd rect (at %s %s) (size %s %s)\n (layers "F.Cu" "F.Paste" "F.Mask"))\n' % (num, x, y, w, hh) for x, y, w, hh in (v, h))
t = re.sub(r' \(pad "([^"]+)" smd custom \(at ([-\d.]+) ([-\d.]+)\).*?\n \)\n', legs, t, flags=re.S)
out = os.path.join(H, 'build', 'lanes', s + '.kicad_mod')
os.makedirs(os.path.dirname(out), exist_ok=True)
open(out, 'w').write(t)
return out
def stage_libs(d):
s = d['slug']; P = pdir(d)
lanes = lanes_footprint(d)
if not os.path.exists(pdir(d, 'page.json')):
json.dump(page_json(d), open(pdir(d, 'page.json'), 'w'), indent=1)
print(run(['adom-symbol', 'embed', '--file', pdir(d, s + '.kicad_sym'), '--name', sym_name(d), '--out', pdir(d, 'viewers', 'symbol.html')]).strip().splitlines()[-1][:160])
print(run(['adom-footprint', 'embed', '--file', pdir(d, s + '.kicad_mod'), '--out', pdir(d, 'viewers', 'footprint.html')]).strip().splitlines()[-1][:160])
for v in ('symbol.html', 'footprint.html'): # wiki content rule: no em or en dashes, even in tool output
vp = pdir(d, 'viewers', v); vt = open(vp, encoding='utf-8').read()
open(vp, 'w', encoding='utf-8').write(vt.replace(' \u2014 ', ', ').replace('\u2014', '-').replace('\u2013', '-'))
o = run(['adom-lbr', 'generate', '--sym', pdir(d, s + '.kicad_sym'), '--fp', lanes, '--part-metadata', pdir(d, 'page.json'),
'--name', sym_name(d), '-o', pdir(d, s + '.lbr')])
print('\n'.join(l[:220] for l in o.splitlines() if l.startswith(('OK', 'WARN', 'LOST', 'HINT', 'ERROR')))[:2500])
# Altium: hub JSON with the etched library model beside it, then the native pair
hub = pdir(d, 'altium', art(d) + '.adom-lbr.json')
o = run(['adom-lbr', 'import-kicad', '--sym', pdir(d, s + '.kicad_sym'), '--fp', lanes, '--mpn', d['mpn'], '--out', hub])
print('\n'.join(l[:220] for l in o.splitlines() if l.startswith(('OK', 'WARN', 'LOST', 'HINT', 'ERROR')))[:2500])
hj = json.load(open(hub)); hj['model_3d'] = '../%s-etched.step' % s; json.dump(hj, open(hub, 'w'), indent=1)
step_beside = os.path.join(os.path.dirname(hub), d['mpn'].replace('/', '_') + '.step')
shutil.copy(pdir(d, s + '-etched.step'), step_beside)
o = run(['adom-lbr', 'assemble-altium', '--no-dedupe', '--out', pdir(d, 'altium', 'Adom_' + s), hub], ok=(0, 2))
print(o[-1500:])
if os.path.exists(step_beside):
os.remove(step_beside)
# ------------------------------------------------------------------------------------------------ artwork
def stage_art(d):
s = d['slug']; L = os.path.join(LIB, d['lib_mpn']); A = art(d)
styles = ['heavy', 'teal', 'dark', 'hairline', 'dashed', 'blueprint', 'bold', 'thin', 'ink-heavy', 'neon', 'xray']
assets = []
for st in styles:
for named in (('-named', '') if st == 'heavy' else ('-named',)):
src = os.path.join(L, '%s-3d-outline-%s%s.svg' % (d['lib_mpn'], st, named))
if not os.path.exists(src):
continue
dst = pdir(d, 'artwork', '%s-3d-outline-%s%s.svg' % (A, st, named))
shutil.copy(src, dst)
if named:
assets.append(dict(kind='chip-outline', path='artwork/' + os.path.basename(dst), format='svg', sizeBytes=os.path.getsize(dst),
sha256=sha(dst), style='symbol' if st == 'heavy' else st,
label={'heavy': 'Schematic ink (in the symbol)', 'teal': 'Teal', 'dark': 'Dark ink', 'hairline': 'Hairline',
'dashed': 'Dashed', 'blueprint': 'Blueprint'}.get(st, st.replace('-', ' ').capitalize()),
width=320, height=320))
# shaded iso of the etched model and a transparent outline preview
shutil.copy(os.path.join(L, d['lib_mpn'] + '-3d-iso-named.png'), pdir(d, 'artwork', A + '-3d-iso.png'))
prev = pdir(d, 'artwork', A + '-chip-outline-preview.png')
run(['cairosvg', pdir(d, 'artwork', A + '-3d-outline-heavy-named.svg'), '-o', prev, '--output-width', '960'])
from PIL import Image
im = Image.open(prev)
assets.insert(0, dict(kind='chip-outline-preview', path='artwork/' + os.path.basename(prev), format='png', sizeBytes=os.path.getsize(prev),
sha256=sha(prev), label='Chip outline', width=im.width, height=im.height, transparent=True))
iso = pdir(d, 'artwork', A + '-3d-iso.png'); im = Image.open(iso)
assets.insert(0, dict(kind='chip-shaded', path='artwork/' + os.path.basename(iso), format='png', sizeBytes=os.path.getsize(iso), sha256=sha(iso),
label='3D chip (etched, iso)', width=im.width, height=im.height))
assets.append(dict(kind='mark-source', path=s + '-etched.step', format='step', sizeBytes=os.path.getsize(pdir(d, s + '-etched.step')), sha256=sha(pdir(d, s + '-etched.step'))))
assets.append(dict(kind='artwork-step', path=s + '.step', format='step', sizeBytes=os.path.getsize(pdir(d, s + '.step')), sha256=sha(pdir(d, s + '.step'))))
plan = json.load(open(os.path.join(L, d['lib_mpn'] + '.mark-plan.json')))
shutil.copy(os.path.join(L, d['lib_mpn'] + '.mark-plan.json'), pdir(d, 'artwork', A + '.mark-plan.json'))
shutil.copy(os.path.join(L, d['lib_mpn'] + '-etched.check.png'), pdir(d, 'artwork', A + '-etched.check.png'))
key = hashlib.sha256((sha(pdir(d, s + '.step')) + sha(pdir(d, s + '-etched.step')) + json.dumps(plan, sort_keys=True)).encode()).hexdigest()
ident = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
man = dict(schema='adom/chip-artwork-cache@1', mpn=d['mpn'],
generatedBy='adom-chip-thumbnailer 1.2.1 (OCCT HLR and mark planner via service-step2glb) on the datasheet-built STEP (tools/build_models.py)',
generatedAtIso=datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), cacheKey=key,
recipe=dict(schema='adom/chip-artwork-cache@1', sourceStepSha256=sha(pdir(d, s + '.step')), displayGlbSha256=sha(pdir(d, s + '-etched.glb')),
markStepSha256=sha(pdir(d, s + '-etched.step')), sourceToDisplayMm=ident, markToDisplayMm=ident,
view='iso (adom-chip-thumbnailer standard pose: pin 1 toward the viewer)', upAxis='Z', units='mm',
fovHelperSolidsDropped=[], generator='tools/build_models.py + adom-chip-thumbnailer once ' + d['lib_mpn']),
source=dict(step=s + '.step', displayGlb=s + '-etched.glb', markStep=s + '-etched.step'),
marking=dict(text=d['mark'], placement='Top face, along the package, upright with pin 1 lower left (mark planner, legibility gate PASS)',
manufacturerMarking=False,
etch=dict(text=d['mark'], surface='top', plan='artwork/%s.mark-plan.json' % A, check='artwork/%s-etched.check.png' % A,
note='virtual identification aid, NOT the manufacturer marking (the real top marks are described in the data sheet)')),
review=dict(orientation='Z-up, seated on z = 0, pin 1 at the footprint pad 1 corner: checked by AI in the iso, underside and hero renders; awaiting a person',
markingPlacement='Mark planner PASS; visually checked by AI in the etched top view and the named outline (%s)' % TODAY,
projectedArtwork='Outline checked by AI at 320 px and in the baked symbol; awaiting a person'),
assets=assets,
hints=['Reuse these cached assets before generating anything for this part.',
'The top carries no features, so the virtual part number is on the top face; the named outlines show it.',
'adom-symbol: bake the named heavy outline (%s-3d-outline-heavy-named.svg).' % A,
'Hashes were matched at publication; compare before reuse.'],
verification=dict(status='matched-at-publication', checkedAtIso=datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
checks=['datasheet-built STEP hash', 'etched STEP hash', 'etched GLB hash', 'all cached asset hashes'], notContinuous=True))
json.dump(man, open(pdir(d, 'artwork', A + '-thumbs.json'), 'w'), indent=1)
# heatsink vias, read back from the footprint
t = open(pdir(d, s + '.kicad_mod')).read()
vias = [(float(x), float(y)) for x, y in re.findall(r'\(pad "%s" thru_hole circle \(at ([-\d.]+) ([-\d.]+)\)' % re.escape(d['ep']['number']), t)]
ep = d['ep']
hv = dict(mpn=d['mpn'], package=d['package_short'], ep=dict(w_mm=ep['w'], h_mm=ep['h'], number=ep['number'], name=ep['name']),
via=dict(drill_mm=ep['drill'], pitch_mm=ep['pitch'], ring_mm=ep['ring'], tented=True, count=len(vias),
positions=[dict(x_mm=x + 0.0, y_mm=-y + 0.0, dia_mm=round(ep['drill'] + 2 * ep['ring'], 3), drill_mm=ep['drill']) for x, y in vias],
in_footprint='plated thru_hole pads numbered %s (KiCad pad_prop_heatsink); adom-lbr carries them to Altium and Fusion' % ep['number'],
hero_glb=dict(layer='vias', node='Adom.HeatsinkVias', board_mm=1.6, alpha=0.35)),
note=ep['note'])
json.dump(hv, open(pdir(d, 'artwork', A + '-heatsink-via.json'), 'w'), indent=1)
print('art', s, len(assets), 'assets,', len(vias), 'vias')
# ------------------------------------------------------------------------------------------------ docs
def stage_docs(d):
s = d['slug']
ds = os.path.join(H, d['ds']['file'])
shutil.copy(ds, pdir(d, 'docs', s + '-datasheet.pdf'))
if s == 'tmc2262':
for f in ('21-100708a.pdf', '90-100245b.pdf'):
shutil.copy(os.path.join(H, 'datasheets', f), pdir(d, 'docs', f))
run(['pdftoppm', '-r', '120', '-f', '2', '-l', '2', '-png', '-singlefile', os.path.join(H, 'datasheets', '21-100708a.pdf'), pdir(d, 'docs', 'figures', 'tmc2262-pkg-21-100708a')])
run(['pdftoppm', '-r', '120', '-png', '-singlefile', os.path.join(H, 'datasheets', '90-100245b.pdf'), pdir(d, 'docs', 'figures', 'tmc2262-land-90-100245b')])
for pg, what in d['figures']:
run(['pdftoppm', '-r', '110', '-f', str(pg), '-l', str(pg), '-png', '-singlefile', ds, pdir(d, 'docs', 'figures', '%s-ds-p%d' % (s, pg))])
# hero card: the etched model, iso
rpng = os.path.join(H, 'build', s + '-render.png')
run(['step2glb', 'thumbnail', pdir(d, s + '-etched.step'), '-o', rpng, '--width', '2000', '--height', '1600', '--pose', d.get('hero_pose', 'iso2')])
import hero_card
hero_card.make(rpng, pdir(d, 'docs', 'hero.png'), d['title'], d['one_liner'].split(' with ')[0] if len(d['one_liner']) > 60 else d['one_liner'],
'%s · %s' % (d['manufacturer'], d['package_short']),
footer='Manufacturer datasheet · Datasheet-built 3D · KiCad / Fusion / Altium · Adom Wiki')
# model-vs-drawing proof
run([os.path.join(H, '..', '.venv-cq', 'bin', 'python'), os.path.join(H, 'tools', 'proof.py'), s, pdir(d, s + '.kicad_mod'), pdir(d, s + '.step'), pdir(d, 'docs', 'figures', s + '-model-vs-land-pattern.png')])
print('docs', s, 'ok')
# ------------------------------------------------------------------------------------------------ provenance
def fp_pads(path):
t = open(path).read(); out = []
for m in re.finditer(r'\(pad "([^"]*)" (smd|thru_hole) (\w+) \(at ([-\d.]+) ([-\d.]+)\) \(size ([\d.]+) ([\d.]+)\)', t):
n, kind, shp = m.group(1, 2, 3); x, y, w, h = map(float, m.group(4, 5, 6, 7))
poly = None
if shp == 'custom':
q = re.search(r'\(gr_poly \(pts (.*?)\) \(width', t[m.start():m.start() + 2000], re.S)
poly = [(x + float(a), y + float(b)) for a, b in re.findall(r'\(xy ([-\d.]+) ([-\d.]+)\)', q.group(1))]
out.append(dict(number=n, kind=kind, shape=shp, x=x, y=y, w=w, h=h, poly=poly))
return out
def expected_lands(d):
"""Every land from the manufacturer drawing, computed here independently of tools/footprints.py."""
E = []
if d['slug'] == 'lan8770m':
src = 'C04-3268 Rev A (DS00002550C Figure 3-3, page 18): E 0.50, C1 = C2 4.90, X1 0.30, Y1 0.85, X2 = Y2 3.20, V 0.33, EV 1.20'
for i in range(8):
o = -1.75 + 0.5 * i
E += [('%d' % (1 + i), -2.45, o, 0.85, 0.30), ('%d' % (9 + i), o, 2.45, 0.30, 0.85),
('%d' % (17 + i), 2.45, -o, 0.85, 0.30), ('%d' % (25 + i), -o, -2.45, 0.30, 0.85)]
E.append(('EP', 0, 0, 3.20, 3.20))
vias = [(x, y, 0.33) for x in (-1.2, 0, 1.2) for y in (-1.2, 0, 1.2)]
else:
src = '90-100245 Rev B: extent 6.600, bars 2.275 x 0.590 on 0.875/0.855/0.855/0.875 from 1.570 above the bottom edge, small pads 0.300 x 0.725 at 0.500 pitch (1.400 from the edge, 0.800 across the centre), corner L 0.450 legs (0.550 / 0.525 offsets, R0.150), exposed pad 1.450 x 4.050'
bars = [-1.73, -0.855, 0.0, 0.855, 1.73]
for i, y in enumerate(bars):
E += [('%d' % (1 + i), -2.1625, y, 2.275, 0.59), ('%d' % (20 - i), 2.1625, y, 2.275, 0.59)]
for i, x in enumerate([-1.9, -1.4, -0.9, -0.4, 0.4, 0.9, 1.4, 1.9]):
E += [('%d' % (7 + i), x, 2.9375, 0.30, 0.725), ('%d' % (29 - i), x, -2.9375, 0.30, 0.725)]
E.append(('EP', 0, 0, 1.45, 4.05))
vias = None
return src, E, vias
def stage_prov(d):
s = d['slug']
pads = fp_pads(pdir(d, s + '.kicad_mod'))
src, E, vias = expected_lands(d)
rows, ok = [], True
smd = {p['number']: p for p in pads if p['kind'] == 'smd' and p['number'] and p['shape'] != 'custom'}
for n, x, y, w, h in E:
p = smd.get(n)
good = bool(p) and all(abs(a - b) < 1e-3 for a, b in ((p['x'], x), (p['y'], y), (p['w'], w), (p['h'], h)))
ok &= good
rows.append(dict(pad=n, drawing=dict(x=x, y=y, w=w, h=h), footprint=dict(x=p['x'], y=p['y'], w=p['w'], h=p['h']) if p else None, match=good))
for p in pads:
if p['shape'] == 'custom':
xs = [q[0] for q in p['poly']]; ys = [q[1] for q in p['poly']]
bbox = [round(min(xs), 4), round(min(ys), 4), round(max(xs), 4), round(max(ys), 4)]
ax, ay = abs(bbox[0]) if bbox[0] < 0 else bbox[0], 0
want = sorted([2.30, 3.30]); got = sorted([abs(bbox[0]), abs(bbox[2])])
good = abs(got[0] - 2.30) < 1e-3 and abs(got[1] - 3.30) < 1e-3 and sorted([abs(bbox[1]), abs(bbox[3])]) == [2.325, 3.3]
ok &= good
rows.append(dict(pad=p['number'], drawing='L pad: legs 0.450 wide, x 2.300..3.300, y 2.325..3.300 from the centre, R0.150 inner corner',
footprint=dict(polygon_bbox=bbox, vertices=len(p['poly'])), match=good))
if vias is not None:
got = sorted((round(p['x'], 3), round(p['y'], 3), p.get('w')) for p in pads if p['kind'] == 'thru_hole')
want_xy = sorted((x, y) for x, y, _ in vias)
good = [(a, b) for a, b, _ in got] == want_xy
ok &= good
rows.append(dict(pad='thermal vias', drawing='9 x 0.33 mm on a 1.20 mm grid (V, EV)', footprint=[dict(x=a, y=b) for a, b, _ in got], match=good))
rep = dict(part=d['mpn'], footprint=s + '.kicad_mod', footprint_sha256=sha(pdir(d, s + '.kicad_mod')), source=src,
frame='KiCad, top view, mm, Y down', all_match=ok, lands=rows,
not_from_the_drawing=['paste windows on the exposed pad (usual practice; the drawing gives no stencil)',
'silkscreen, courtyard and fab outline'] + ([] if s == 'lan8770m' else ['heatsink vias (Adom rule, 0.3 mm drill on a 0.7 mm grid; the data sheet asks for as many as possible)']))
json.dump(rep, open(pdir(d, 'provenance', 'footprint-vs-datasheet.json'), 'w'), indent=1)
mp = json.load(open(os.path.join(H, 'build', 'models', 'models.provenance.json')))[d['lib_mpn'] + '.step']
json.dump(dict(file=s + '.step', sha256=sha(pdir(d, s + '.step')), **mp,
colours=dict(body='mould compound, rgb 0.10 0.10 0.11', terminals='matte tin, rgb 0.80 0.80 0.82'),
note='AI-built from the manufacturer drawing; not a manufacturer model. Proof: docs/figures/%s-model-vs-land-pattern.png' % s),
open(pdir(d, 'provenance', s + '.step.json'), 'w'), indent=1)
plan = json.load(open(pdir(d, 'artwork', art(d) + '.mark-plan.json')))
json.dump(dict(file=s + '-etched.step', sha256=sha(pdir(d, s + '-etched.step')), derived_from=s + '.step', derived_sha256=sha(pdir(d, s + '.step')),
tool='adom-chip-thumbnailer 1.2.1 once (mark planner + service-step2glb /laser-etch, mode flat)', mark=d['mark'],
mark_plan='artwork/%s.mark-plan.json' % art(d), gate='PASS', group='Adom.LaserEtch',
note='Virtual identification aid, NOT the manufacturer marking.'),
open(pdir(d, 'provenance', s + '-etched.step.json'), 'w'), indent=1)
ov = json.load(open(pdir(d, s + '-hero.overlay.json')))
json.dump(dict(file=s + '-hero.glb', sha256=sha(pdir(d, s + '-hero.glb')), derived_from=s + '-etched.glb',
tool='component-hero scripts/hero_glb.py (adom-hero standard) + tools/hero_post_vias.py', align=ov.get('align'),
labels=len(ov.get('labels', [])), layers=ov.get('layers'),
heatsink_vias='Adom.HeatsinkVias (a switchable group in the viewer), 65 % transparent copper through a 1.6 mm board'),
open(pdir(d, 'provenance', s + '-hero.json'), 'w'), indent=1)
json.dump(dict(part=d['mpn'], tool='adom-lbr 2.47.5',
kicad=s + '.kicad_mod: the exact manufacturer land pattern (custom L pads with the R0.150 corner, split paste windows)',
fusion_and_altium=('%s.lbr and altium/Adom_%s.PcbLib are built from build/lanes/%s.kicad_mod, which differs from the KiCad footprint only where adom-lbr 2.47.5 '
'cannot carry a KiCad feature: paste-only windows (dropped from Fusion, turned into unnumbered copper for Altium) are replaced by full paste on the exposed pad'
% (s, s, s)) + ('' if s == 'lan8770m' else ', and each L-shaped corner pad (custom pad, primitives dropped) becomes its two legs as overlapping rectangles with the same number (identical copper except the R0.150 inner corner)'),
reported='adom-lbr printed no LOST line for either loss.'),
open(pdir(d, 'provenance', 'eda-lanes.json'), 'w'), indent=1)
for f in ('tools/build_models.py', 'tools/footprints.py', 'tools/hero_post_vias.py', 'tools/proof.py', 'tools/hero_card.py', 'build.py', 'pages_def.py'):
shutil.copy(os.path.join(H, f), pdir(d, 'provenance', 'tools', os.path.basename(f)))
print('prov', s, 'footprint matches drawing:', ok)
return ok
def stage_models(d):
s = d['slug']; P = pdir(d); tool = os.path.join(CW, 'models_json.py')
drawing = 'C04-1268 Rev A' if s == 'lan8770m' else '21-100708 Rev A'
run([sys.executable, tool, 'scan'], cwd=P)
sets = {
s + '.step': ['title=Datasheet-built model, unmarked (STEP)', 'role=datasheet-built', 'variant=plain', 'use=board-cad,render', 'colours=true',
'slot=model_3d_step_plain', 'marking=none',
'unique=The body and terminals built from the package drawing %s with no part number on the top' % drawing,
'source=tools/build_models.py (CadQuery) from the manufacturer package drawing %s' % drawing],
s + '-etched.step': ['title=Library model with the virtual part number (STEP)', 'role=derived', 'variant=etched', 'use=board-cad', 'colours=true',
'slot=model_3d_step', 'marking.text=' + d['mark'], 'marking.surface=top', 'marking.colour=light grey inlay',
'unique=The same body plus a flat Adom.LaserEtch inlay reading %s on the top: the model the footprint links' % d['mark'],
'derived_from=' + s + '.step', 'source=adom-chip-thumbnailer 1.2.1 mark planner + service-step2glb laser-etch'],
s + '.glb': ['title=Unmarked model (GLB)', 'role=derived', 'variant=plain', 'use=render', 'colours=true', 'marking=none',
'unique=Web copy of the unmarked STEP', 'derived_from=' + s + '.step', 'source=step2glb'],
s + '-etched.glb': ['title=Library model with the virtual part number (GLB)', 'role=derived', 'variant=etched', 'use=render,board-insertion', 'colours=true',
'slot=model_3d_plain', 'marking.text=' + d['mark'], 'marking.surface=top', 'marking.colour=light grey inlay',
'unique=Clean board-use web copy of the etched STEP, no footprint artwork', 'derived_from=' + s + '-etched.step', 'source=step2glb'],
s + '-hero.glb': ['title=Page hero (GLB)', 'role=derived', 'variant=hero', 'use=wiki-hero', 'colours=true', 'slot=model_3d',
'marking.text=' + d['mark'], 'marking.surface=top', 'marking.colour=light grey inlay',
'unique=Etched chip plus teal dashed pad outlines, silkscreen, pad signal names and heatsink vias as toggleable layers; page hero only, never on a board',
'derived_from=' + s + '-etched.glb', 'source=component-hero hero_glb.py + tools/hero_post_vias.py'],
}
for f, kv in sets.items():
run([sys.executable, tool, 'set', f] + kv, cwd=P)
m = json.load(open(pdir(d, 'models.json')))
m['front'] = [0, -1, 0]
json.dump(m, open(pdir(d, 'models.json'), 'w'), indent=1)
out = run([sys.executable, tool, 'thumbs'], cwd=P)
print('models', s, out.strip()[-400:])
def sourcing_rows(d):
rows = []
for q in ('TMC2262', 'LAN8770M', 'LAN8770'):
p = os.path.join(H, 'data', 'parts-search-%s.json' % q)
if not os.path.exists(p): continue
raw = open(p).read(); js = json.JSONDecoder().raw_decode(raw[raw.index('{'):])[0]
for v, label in (('mouser', 'Mouser'), ('digikey', 'DigiKey'), ('jlcpcb', 'LCSC / JLCPCB')):
for h in (js.get(v) or {}).get('hits', []):
if h.get('mpn') in [o[0] for o in d['ordering']]:
url = h.get('product_url') or ('https://jlcpcb.com/partdetail/%s' % h.get('vendor_pn') if v == 'jlcpcb' else '')
rows.append((label, h['mpn'], h.get('vendor_pn'), h.get('unit_price') or 'see link', h.get('stock'), url))
seen, out = set(), []
for r in rows:
if (r[0], r[2]) not in seen:
seen.add((r[0], r[2])); out.append(r)
order = {'Mouser': 0, 'DigiKey': 1, 'LCSC / JLCPCB': 2}
return sorted(out, key=lambda r: ([o[0] for o in d['ordering']].index(r[1]), order[r[0]]))
def readme(d):
s = d['slug']; A = art(d); ds = d['ds']
L = ['# %s' % d['title'], '',
'**%s** · %s · %s' % (d['manufacturer'], d['one_liner'][0].upper() + d['one_liner'][1:], d['package_short']), '',
]
live = d['visibility_public'] # the registered widgets fetch their JSON from a sandboxed frame without a session,
if live: # so on a private page they would come up empty: show the same data statically there
L += ['::adom-verified{src="/%s/%s/render/verification.json"}' % (OWNER, s), '']
else:
L += ['> **Verification: open.** Built from the manufacturer documents and checked automatically (every land matches the drawing, the hero aligns with the footprint, the Fusion library passes lint); not yet reviewed by a person. The log is `verification.json`.', '']
L += [x + '\n' for x in d['intro']]
L += ['Order code **%s** (%s). Datasheet: [%s](%s) (local copy: `docs/%s-datasheet.pdf`). Product page: [%s](%s).' %
(d['mpn'], d['ordering'][0][1].lower(), ds['title'] + ', ' + ds['rev'], ds['url'], s,
d['product_url'].split('/')[2], d['product_url']), '']
if d.get('doc_note'):
L += ['> ' + d['doc_note'], '']
L += ['## Manufacturer and resources', '', 'Everything on this page traces back to %s. Start here for the original documents and design resources.' % d['manufacturer'], '',
'| Resource | Link | Why you need it |', '|---|---|---|']
L += ['| %s | [open](%s) | %s |' % (a, b, c) for a, b, c in d['resources']]
L += ['', '## Ordering variants', '', '| Part number | Packing | Temperature | Note | Source |', '|---|---|---|---|---|']
L += ['| %s | %s | %s | %s | %s |' % o for o in d['ordering']]
L += ['', d['ordering_note'], '']
L += ['## Pinout', '', 'Source: %s, %s. Pin 1 is the top-left pad in the top view; the footprint marks it with a filled silkscreen dot and the 3D model with a dimple on the top.' % (ds['id'], d['pin_table_pages']), '',
'| Pin | Name | Type | Group | Function | Page |', '|---|---|---|---|---|---|']
for n, nm, ty, g, desc, pg in d['pins']:
L.append('| %s | %s | %s | %s | %s | %d |' % (n, nm, ty, g, desc, pg))
L += ['', '### Pin groups', '', 'The KiCad, Fusion and Altium symbols group the pins the same way:', '']
L += ['- **%s:** %s' % (g, d['group_desc'][g]) for g in d['groups_order']]
L += ['', '## Key specifications', '', '| Parameter | Value | Source |', '|---|---|---|']
L += ['| %s | %s | %s |' % sp for sp in d['specs']]
rows = sourcing_rows(d)
L += ['', '## Sourcing', '', 'Snapshot %s (single-unit price and stock at that moment; check before ordering).' % PD.SNAPSHOT, '',
'| Distributor | Part number | Distributor number | Price @1 | Stock | Link |', '|---|---|---|---|---|---|']
L += ['| %s | %s | %s | %s | %s | [open](%s) |' % (a, b, c, pr, '{:,}'.format(st) if isinstance(st, int) else st, u) for a, b, c, pr, st, u in rows]
if d.get('lcsc_note'):
L += ['', d['lcsc_note']]
L += ['', '## Packaging, handling and marking', '']
if s == 'lan8770m':
L += ['- Tray (no suffix) or tape and reel (`T` after the device name); the `T` is never printed on the part (%s page 23).' % ds['id'],
'- Top marking: `8770Xe`, `V-LANy e3`, country of origin and the `YYWWNNN` date and traceability code, with the pin-1 mark at the top left (%s Section 3.1, page 15).' % ds['id'],
'- Matte tin (e3) terminals with wettable flanks for automated optical inspection of the solder fillet (%s pages 1 and 15 to 17).' % ds['id'],
'- Thermal vias under the exposed pad should be filled or tented to avoid solder loss during reflow (%s Figure 3-3 note 2, page 18).' % ds['id']]
else:
L += ['- `TMC2262AFV+` and `TMC2262AFV+T` (tape and reel); `+` marks a lead-free, RoHS-compliant package (Rev. 0 page 187).',
'- Top marking: ADI logo, part number (`TMC` + `2262`), test flow and suffix, `YYWW` date code, assembly and fab code, and the lead-free `+` near pin 1 (Rev. 0 Figure 1 and Table 1, page 15).',
'- Absolute maximum: VS 70 V, VCC_IO 5.5 V, junction 160 C, reflow 260 C (Rev. 0 page 10).',
'- theta-JA 23 C/W on a four-layer board with 4 W dissipated: copper area and exposed-pad vias set how much current the part can run continuously (Rev. 0 page 15).']
L += ['', '## CAD', '', 'Adom-authored files are licensed CC-BY-4.0; the manufacturer documents in `docs/` keep %s\'s terms.' % d['manufacturer'], '',
'| File | What it is |', '|---|---|',
'| `%s.kicad_sym` | KiCad symbol: two columns with intelligent pin groups (%s) and the outline of this exact chip baked in the body (adom-symbol 1.2.6). MPN, manufacturer, datasheet, Mouser, DigiKey%s and the Adom Wiki link are symbol properties, so they reach the BOM. |' %
(s, ', '.join(d['groups_order']), ', LCSC' if d['distributors'].get('lcsc') else ''),
'| `%s.kicad_mod` | KiCad footprint, pad for pad from the manufacturer land pattern, with heatsink vias in the exposed pad. See *Footprint* below. |' % s,
'| `%s.lbr` | Fusion 360 / EAGLE library (symbol, package, device, with the MPN and distributor attributes). |' % s,
'| `altium/Adom_%s.SchLib`, `altium/Adom_%s.PcbLib` | Native Altium Designer libraries with the 3D body embedded; `altium/%s.adom-lbr.json` is the canonical adom-lbr hub record. |' % (s, s, A),
'| `%s-etched.step` / `.glb` | **Library model** (the one the footprint links): the datasheet-built body with a virtual laser-etched `%s` on the top as the toggleable `Adom.LaserEtch` group. |' % (s, d['mark']),
'| `%s.step` / `.glb` | The same body, unmarked. |' % s,
'| `%s-hero.glb` | Page hero: the etched chip plus teal dashed copper-pad outlines (50 %% opacity), silkscreen with pin 1 (30 %%), pad signal labels (50 %%) and the heatsink vias through a 1.6 mm board. The chip, the laser etch and the vias switch on and off in the viewer\'s Layers panel. Reference artwork only: never put it on a board. |' % s,
'| `viewers/` | Self-contained interactive symbol and footprint viewers (adom-symbol embed, adom-footprint embed). |',
'| `artwork/` | Chip-outline cache from adom-chip-thumbnailer (vector outlines with the part number, shaded iso, mark plan and legibility check); `%s-thumbs.json` is the manifest. |' % A,
'| `provenance/` | How every file was made: model dimensions and source drawing, etch plan, hero build, the pad-by-pad footprint comparison, EDA-lane notes, file hashes and the build scripts. |', '']
L += ['## 3D models', '', d['model_note'], '']
if live:
L += ['::adom-models{src="/%s/%s/render/models.json"}' % (OWNER, s), '']
else:
mj = json.load(open(pdir(d, 'models.json')))
L += ['| Preview | File | What it is | What makes it different |', '|---|---|---|---|']
for m in mj['models']:
L.append('| <img src="%s" width="96" alt="%s"> | `%s` | %s | %s |' % (m.get('thumb', ''), m['title'], m['path'], m['title'], m.get('unique', '')))
L += ['', 'Every file is described in `models.json` (schema adom/models@1) with its hashes and what it was derived from.', '']
L += ['## Footprint', '', d['footprint_note'], '',
'' % (d['title'], s), '',
'Grey: the solderable faces of every terminal, read straight from the STEP. Teal dashed: every copper land of the footprint. Each terminal sits inside its land with the toe and heel the drawing gives, and pad 1 and the exposed-pad chamfer are top left. The full pad-by-pad comparison against the drawing is `provenance/footprint-vs-datasheet.json` (all lands match).', '']
if s == 'tmc2262':
L += ['**Fusion 360 and Altium:** adom-lbr 2.47.5 cannot carry KiCad custom pads or paste-only windows yet, so the `.lbr` and the PcbLib draw each L-shaped corner pad as its two legs (two overlapping pads with the same number: identical copper except the R0.150 inner corner) and put paste over the whole exposed pad. Trim the exposed-pad paste in your stencil. Details in `provenance/eda-lanes.json`.', '']
else:
L += ['**Fusion 360 and Altium:** adom-lbr 2.47.5 cannot carry paste-only windows yet, so the `.lbr` and the PcbLib put paste over the whole exposed pad. Trim it in your stencil. Details in `provenance/eda-lanes.json`.', '']
L += ['## Thermal pad', '', d['ep']['note'], '']
if live:
L += ['::adom-heatsink-via{src="/%s/%s/render/artwork/%s-heatsink-via.json"}' % (OWNER, s, A), '']
else:
hv = json.load(open(pdir(d, 'artwork', A + '-heatsink-via.json')))['via']
L += ['| Exposed pad | Vias | Drill | Pad | Pitch | Positions (mm, top view) |', '|---|---|---|---|---|---|',
'| %s x %s mm, pad %s | %d, tented | %s mm | %s mm | %s mm | %s |' % (d['ep']['w'], d['ep']['h'], d['ep']['number'], hv['count'], hv['drill_mm'],
round(hv['drill_mm'] + 2 * hv['ring_mm'], 2), hv['pitch_mm'], ', '.join('(%g, %g)' % (q['x_mm'], q['y_mm']) for q in hv['positions'])), '',
'The vias are plated holes numbered like the exposed pad in the footprint, the Fusion and Altium libraries, and the `Adom.HeatsinkVias` layer of the hero (`artwork/%s-heatsink-via.json`).' % A, '']
L += ['## Chip outline cache', '']
if live:
L += ['::adom-chip-thumbnail{src="/%s/%s/render/artwork/%s-thumbs.json" mpn="%s" package="%s"}' % (OWNER, s, A, d['mpn'], d['package_short']), '']
else:
L += ['| Outline (the one baked into the symbol) | Shaded |', '|---|---|',
'| <img src="artwork/%s-chip-outline-preview.png" width="320" alt="%s chip outline"> | <img src="artwork/%s-3d-iso.png" width="320" alt="%s shaded"> |' % (A, d['title'], A, d['title']), '',
'Eleven vector outline styles with the part number (`artwork/%s-3d-outline-*-named.svg`), the mark plan and its legibility check are cached in `artwork/`; `%s-thumbs.json` (schema adom/chip-artwork-cache@1) lists them with their hashes.' % (A, A), '']
L += ['## Provenance', '', '| Data | Source | Reference | Evidence |', '|---|---|---|---|',
'| Identity, features | %s %s | page 1 | datasheet copy in `docs/` |' % (d['manufacturer'], ds['id']),
'| Pin numbers and functions | %s | %s | table above quotes the datasheet, page per pin |' % (ds['id'], d['pin_table_pages']),
'| Package dimensions | %s | %s | `provenance/%s.step.json`, proof image above |' % ('Microchip drawing C04-1268 Rev A' if s == 'lan8770m' else 'ADI package outline 21-100708 Rev A', 'DS00002550C pages 16 and 17' if s == 'lan8770m' else '`docs/21-100708a.pdf`', s),
'| Land pattern | %s | %s | `provenance/footprint-vs-datasheet.json` |' % ('Microchip drawing C04-3268 Rev A' if s == 'lan8770m' else 'ADI land pattern 90-100245 Rev B', 'DS00002550C page 18' if s == 'lan8770m' else '`docs/90-100245b.pdf`'),
'| 3D model | built from the package drawing (CadQuery), no manufacturer STEP | %s | `provenance/%s.step.json`, `models.json` |' % (TODAY, s),
'| Virtual part number | adom-chip-thumbnailer mark planner | legibility gate PASS | `artwork/%s-etched.check.png`, `provenance/%s-etched.step.json` |' % (A, s),
'| Price and stock | Mouser, DigiKey%s | %s | Sourcing table above |' % (', LCSC' if d['distributors'].get('lcsc') else '', PD.SNAPSHOT), '',
'### Cited datasheet pages', '', 'Page images rendered from the %s copy in `docs/` (click to enlarge).' % ds['id'], '', '| Page | What it shows |', '|---|---|']
L += ['| [page %d](docs/figures/%s-ds-p%d.png) | %s |' % (pg, s, pg, what) for pg, what in d['figures']]
if s == 'tmc2262':
L += ['| [21-100708 Rev A](docs/figures/tmc2262-pkg-21-100708a.png) | Package outline, 30L FC2QFN 6 x 6 x 0.75 mm |',
'| [90-100245 Rev B](docs/figures/tmc2262-land-90-100245b.png) | Recommended land pattern and package overlay |']
L += ['', '## Talk to the AI', '', 'Paste one of these into an Adom AI thread:', '',
'- "Add the %s from the Adom wiki to my KiCad project and place it on the schematic."' % d['title'],
'- "Send the %s Fusion 360 library from the wiki to my desktop and open it."' % d['title'],
'- "Install the %s Altium library from adom/%s into my Altium project."' % (d['title'], s),
('- "Draw the RMII connection from an STM32H563 to the LAN8770M, with the 25 MHz crystal and the 100BASE-T1 line network."' if s == 'lan8770m'
else '- "Lay out a TMC2262 stepper stage for a NEMA 17 at 24 V with the charge pump, VDD and IREF parts from the data sheet."'), '']
L += ['## Related', '']
L += ['- [%s](%s) - %s.' % r for r in d['related']]
L += ['', '## License', '', 'Adom-authored symbol, footprint, 3D models, artwork and scripts: CC-BY-4.0 (`LICENSE`). The datasheet%s in `docs/` %s copyright %s and included for reference.' %
('s and package drawings' if s == 'tmc2262' else '', 'are' if s == 'tmc2262' else 'is', d['manufacturer']), '']
txt = '\n'.join(L)
bad = [c for c in ('\u2014', '\u2013') if c in txt]
if bad: raise SystemExit('dash in README: %r' % bad)
return txt
def stage_meta(d):
s = d['slug']; P = pdir(d); tool = os.path.join(CW, 'verify_log.py')
if os.path.exists(pdir(d, 'verification.json')): os.remove(pdir(d, 'verification.json'))
json.dump(page_json(d), open(pdir(d, 'page.json'), 'w'), indent=1)
stocked = [r[1] for r in sourcing_rows(d) if isinstance(r[4], int) and r[4] > 0]
run([sys.executable, tool, 'variants'] + sum([['--set', '%s=%s, %s' % (o[0], o[1], o[2])] for o in d['ordering']], []) +
[], cwd=P) # stocked = an Adom reel on hand: none for these parts
fv = json.load(open(pdir(d, 'provenance', 'footprint-vs-datasheet.json')))
checks = ['Every pin number, name and function taken from %s %s, with the page per pin' % (d['ds']['id'], d['pin_table_pages']),
'Footprint lands compared pad by pad with the manufacturer land pattern: %s' % ('all match' if fv['all_match'] else 'MISMATCH'),
'Model terminals sit inside their lands (proof image), body %s x %s x %s mm as drawn' % (d['body']['x_mm'], d['body']['y_mm'], d['body']['z_mm']),
'Pin 1: footprint pad 1, silkscreen dot, model dimple and exposed-pad chamfer on the same corner',
'Hero GLB aligned with the footprint (component-hero align OK, offset 0.0 mm)',
'Fusion .lbr lint passed; Altium libraries assembled with the 3D body embedded',
'note:3D model is datasheet-built, not a manufacturer model',
'note:Checked by the AI that built the page; a person has not reviewed it yet']
args = [sys.executable, tool, 'add', '--by', 'Claude (AI build checks)', '--handle', 'claude', '--status', 'open',
'--title', 'Built from the manufacturer documents; awaiting review by a person',
'--scope', 'symbol', '--scope', 'footprint', '--scope', '3d', '--scope', 'pinout', '--variant', '*']
for c in checks: args += ['--check', c]
args += ['--evidence', 'model vs land pattern=docs/figures/%s-model-vs-land-pattern.png' % s,
'--evidence', 'pad-by-pad comparison=provenance/footprint-vs-datasheet.json',
'--evidence', 'pin table page=docs/figures/%s-ds-p%d.png' % (s, d['figures'][1][0] if s == 'lan8770m' else 16)]
for f in (s + '.kicad_sym', s + '.kicad_mod', s + '.step', s + '-etched.step', s + '.lbr'):
args += ['--file', f]
run(args, cwd=P)
open(pdir(d, 'README.md'), 'w').write(readme(d))
lic = ('Creative Commons Attribution 4.0 International (CC-BY-4.0)\n\nThe Adom-authored files on this page (symbol, footprint, Fusion and Altium libraries, '
'the datasheet-built 3D models, artwork, viewers, provenance and scripts) are licensed under CC-BY-4.0: https://creativecommons.org/licenses/by/4.0/\n'
'Attribution: Adom (wiki.adom.inc/%s/%s).\n\nThird-party material keeps its own terms: the documents in docs/ (%s) are copyright %s, '
'included unmodified for reference.\n') % (OWNER, s, ', '.join(sorted(os.path.basename(f) for f in glob.glob(pdir(d, 'docs', '*.pdf')))), d['manufacturer'])
open(pdir(d, 'LICENSE'), 'w').write(lic)
files = sorted(os.path.relpath(f, P) for f in glob.glob(P + '/**/*', recursive=True) if os.path.isfile(f) and not f.endswith('package.json'))
hashes = {f: sha(os.path.join(P, f)) for f in files if not f.startswith('provenance/file-hashes') and f != 'page.json'} # the server rewrites page.json on push
json.dump(dict(generated=TODAY, note='sha256 of every file at publication', files=hashes), open(pdir(d, 'provenance', 'file-hashes.json'), 'w'), indent=1)
files = sorted(os.path.relpath(f, P) for f in glob.glob(P + '/**/*', recursive=True) if os.path.isfile(f))
pk = dict(name=s, title=d['title'], version=VERSION, description=d['brief'], type='hardware', kind='component', license='CC-BY-4.0',
files=sorted(set(files + ['package.json'])))
json.dump(pk, open(pdir(d, 'package.json'), 'w'), indent=1)
txt = open(pdir(d, 'README.md')).read() + open(pdir(d, 'page.json')).read()
assert '\u2014' not in txt and '\u2013' not in txt
print('meta', s, len(files), 'files', sum(os.path.getsize(os.path.join(P, f)) for f in files) // 1024, 'KiB')
if __name__ == '__main__':
args = [a for a in sys.argv[1:] if not a.startswith('--')]
stage = 'all'
if '--stage' in sys.argv:
stage = sys.argv[sys.argv.index('--stage') + 1]
args = [a for a in args if a != stage]
parts = [p for p in PD.PARTS if not args or p['slug'] in args]
for d in parts:
for st, fn in (('cad', stage_cad), ('hero', stage_hero), ('libs', stage_libs), ('art', stage_art), ('docs', stage_docs), ('prov', stage_prov), ('models', stage_models), ('meta', stage_meta)):
if stage in (st, 'all'):
fn(d)