imrishabh18/corne-keyboard
The code defines and renders two surface-mount chip components with SMT pads, silkscreen outlines, and 3D CAD models (OBJ and STEP) for PCB assembly.
- Version
- 2.0.21
- License
- unset
- Stars
- 1
scripts/verify-jlc-footprints.py
"""Compare installed imports and each built supplier footprint to exact references.
Run after building scripts/footprint-references.tsx and the panel. Copper/drill
geometry is checked up to rigid placement and layer transforms at 1 um tolerance.
DNP hot-swap switch bodies and bare PCB features are reported separately.
"""
import ast,json,re,hashlib,sys,math
from pathlib import Path
from collections import Counter
from shapely.geometry import Point,Polygon,LineString,box
from shapely.affinity import rotate,translate,scale
from shapely.ops import unary_union
project=Path(__file__).resolve().parent.parent
for node in ast.parse((project/'scripts/audit-copper.py').read_text()).body:
if isinstance(node,ast.FunctionDef) and node.name in ['ring','pill','rect','copper']:exec(compile(ast.Module(body=[node],type_ignores=[]),'geometry','exec'))
manifest=json.load(open(project/'lib/parts/jlc-import-manifest.json'));parts={p['lcsc']:p for p in manifest['parts']}
for p in parts.values():
reference_file=project/'vendor/jlc-exact-imports'/(p['lcsc']+'.tsx.txt')
assert hashlib.sha256(reference_file.read_bytes()).hexdigest()==p['exact_import_sha256'],p['name']+' reference changed'
text=(project/p['import_file']).read_text();fp=text.split('footprint={',1)[1].split('</footprint>}',1)[0]+'</footprint>'
assert hashlib.sha256(re.sub(r'\s+','',fp).encode()).hexdigest()==p['footprint_sha256'],p['name']+' changed from exact import'
reference_path=next(p for p in (project/'dist').rglob('circuit.json') if 'footprint-references' in str(p))
reference=json.load(open(reference_path));input_path=Path(sys.argv[1]) if len(sys.argv)>1 else project/'dist/index/circuit.json';actual=json.load(open(input_path))
def component_records(j):
sc={e['source_component_id']:e for e in j if e['type']=='source_component'}
return [(e,sc[e['source_component_id']]) for e in j if e['type']=='pcb_component']
def footprint(j,c):
ports={e['pcb_port_id']:e for e in j if e['type']=='pcb_port'};sp={e['source_port_id']:e for e in j if e['type']=='source_port'}
result=[]
for e in j:
if e.get('pcb_component_id')!=c['pcb_component_id'] or e['type'] not in ['pcb_smtpad','pcb_plated_hole','pcb_hole']:continue
if e['type']=='pcb_hole':
assert e['hole_shape']=='circle';g=translate(Point(0,0).buffer(e['hole_diameter']/2,quad_segs=64),e['x'],e['y'])
else:g=copper(e)
g=rotate(translate(g,-c['center']['x'],-c['center']['y']),-c.get('rotation',0),origin=(0,0))
if c['layer']=='bottom':g=scale(g,xfact=-1,yfact=1,origin=(0,0))
port=sp.get(ports.get(e.get('pcb_port_id'),{}).get('source_port_id'),{})
label=str(port.get('pin_number',port.get('name','')))
result.append((e['type'],label,g))
bounds=unary_union([g for _,_,g in result]).bounds;x=(bounds[0]+bounds[2])/2;y=(bounds[1]+bounds[3])/2
return [(t,p,translate(g,-x,-y)) for t,p,g in result]
refs={s['name']:footprint(reference,c) for c,s in component_records(reference)}
results=[];features=[];switches=[]
for c,s in component_records(actual):
code=(s.get('supplier_part_numbers') or {}).get('jlcpcb',[None])[0];name=s['name']
if name.endswith('_KEY'):
assert c.get('do_not_place') and code=='C400230'
holes=[r for r in actual if r.get('pcb_component_id')==c['pcb_component_id'] and r['type']=='pcb_hole']
assert len(holes)==3 and sorted(round(r['hole_diameter'],6) for r in holes)==[1.999996,1.999996,3.500018],name
assert not any(r['type'] in ['pcb_smtpad','pcb_plated_hole'] for r in actual if r.get('pcb_component_id')==c['pcb_component_id']),name
switches.append(name);continue
if not code:
assert c.get('do_not_place'), 'Unsourced assembled part: '+name
assert name.endswith('_ENCODER_PADS') or re.match(r'^r?JP1#39;,name) or name.startswith('LOGO_'),name
features.append(name);continue
assert code in parts,(name,code)
patterns=footprint(actual,c);remaining=list(refs[code]);assert len(patterns)==len(remaining),(name,len(patterns),len(remaining))
maximum=0
for kind,pin,g in patterns:
candidates=[(i,g.hausdorff_distance(h)) for i,(t,p,h) in enumerate(remaining) if kind==t and pin==p]
assert candidates,(name,kind,pin)
i,distance=min(candidates,key=lambda q:q[1]);maximum=max(maximum,distance);assert distance<.001,(name,kind,pin,distance)
remaining.pop(i)
results.append({'designator':name,'jlcpcb':code,'do_not_place':bool(c.get('do_not_place')),'features_checked':len(patterns),'max_geometry_difference_mm':maximum})
assert len(switches)==42
counts=Counter(r['jlcpcb'] for r in results if not r['do_not_place']);assert counts['C5333465']==46
report={'input_sha256':hashlib.sha256(input_path.read_bytes()).hexdigest(),'import_types_verified':len(parts),'supplier_placements_verified':len(results),'assembled_placements':sum(counts.values()),'assembled_sockets':counts['C5333465'],'dnp_removable_switches':switches,'unpopulated_board_features':features,'placements':results,'tolerance_mm':.001}
output=Path(sys.argv[2]) if len(sys.argv)>2 else project/'dist/jlc-footprint-verification.json';output.write_text(json.dumps(report,indent=2)+'\n')
print(f"Verified {len(parts)} exact imports, {len(results)} placed supplier footprints, {sum(counts.values())} assembled parts and {counts['C5333465']} sockets. All 42 removable switches remain DNP.")