AnasSarkiz/magnetic-shutter-remote-r8
A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.
- Version
- 0.4.0-prototype
- License
- unset
- Stars
- 0
scripts/review-native-product-poses.py
"""Read back actual native assembly GLBs, independently of their source placements.
GLB renderer scene(-product X,product Z,product Y) -> product XYZ millimetres.
Checks every electronics fragment, the native supplier OBJ, and all mechanics.
"""
import json,struct,hashlib,itertools,argparse,math
from pathlib import Path
import numpy as np
ROOT=Path(__file__).resolve().parents[1]
def glb_document(path):
with path.open('rb') as stream:
header=stream.read(20);return json.loads(stream.read(struct.unpack_from('<I',header,12)[0]))
def node_matrix(node):
if 'matrix' in node:return np.array(node['matrix']).reshape((4,4),order='F')
x,y,z,w=node.get('rotation',[0,0,0,1]);matrix=np.eye(4)
matrix[:3,:3]=np.array([[1-2*(y*y+z*z),2*(x*y-z*w),2*(x*z+y*w)],
[2*(x*y+z*w),1-2*(x*x+z*z),2*(y*z-x*w)],
[2*(x*z-y*w),2*(y*z+x*w),1-2*(x*x+y*y)]])@np.diag(node.get('scale',[1,1,1]))
matrix[:3,3]=node.get('translation',[0,0,0]);return matrix
def emitted_bounds(document):
result={}
def visit(index,parent):
node=document['nodes'][index];world=parent@node_matrix(node)
if 'mesh' in node:
points=[];triangles=0
for primitive in document['meshes'][node['mesh']]['primitives']:
accessor=document['accessors'][primitive['attributes']['POSITION']]
for corner in itertools.product(*zip(accessor['min'],accessor['max'])):
point=world@np.array([*corner,1]);points.append([-point[0],point[2],point[1]])
triangles+=(document['accessors'][primitive['indices']]['count'] if 'indices' in primitive else accessor['count'])//3
points=np.array(points);result[node['name']]={'bounds':[points.min(axis=0).tolist(),points.max(axis=0).tolist()],'triangles':triangles}
for child in node.get('children',[]):visit(child,world)
for root in document['scenes'][document.get('scene',0)]['nodes']:visit(root,np.eye(4))
return result
def pcb_bounds(rows,z_offset):
points=[]
for row in rows:
for x,y,z in itertools.product(*zip(*row['boundsMm'])):points.append([-15.2-y,-9+x,9.5+z+z_offset])
points=np.array(points);return [points.min(axis=0).tolist(),points.max(axis=0).tolist()]
def review(output):
pcb=json.load(open(ROOT/'product/pcb-model-review.json'));geometry=json.load(open(ROOT/'product/geometry-review.json'))
partitions=json.load(open(ROOT/'product/pcb-fragment-partition.json'))['partitions'];c=json.load(open(ROOT/'dist/index/circuit.json'))
names={e['source_component_id']:e['name'] for e in c if e['type']=='source_component'}
cad_names={e['cad_component_id']:names[e['source_component_id']] for e in c if e['type']=='cad_component'}
checks=[]
for entry,z_offset in [('enclosure',0),('product.exploded',26)]:
path=ROOT/'dist'/entry/'3d.glb';actual=emitted_bounds(glb_document(path));expected={}
for index,ids in enumerate(partitions):
selected={cad_names[identity] for identity in ids}
if index==0:selected.add('Box0')
rows=[row for row in pcb['meshes'] if row['name'] in selected]
if len(rows)!=len(selected):raise ValueError('Missing fitted mesh in fragment')
name='R9PCBAndFittedParts1' if index==0 else f'R9FittedParts{index+1}'
expected[name]={'bounds':pcb_bounds(rows,z_offset),'triangles':sum(row['triangles'] for row in rows)}
l3=next(row for row in pcb['meshes'] if row['name']=='L3');expected['L3']={'bounds':pcb_bounds([l3],z_offset),'triangles':l3['triangles']}
native_c=json.load(open(ROOT/'dist'/entry/'circuit.json'));native_names={e['source_component_id']:e['name'] for e in native_c if e['type']=='source_component'}
for part in geometry['parts']:
cad=next(e for e in native_c if e['type']=='cad_component' and native_names[e['source_component_id']]==part['name'])
lower,upper=part['localBoundsMm'];shift=[cad['position'][axis]-(lower[i]+upper[i])/2 for i,axis in enumerate(['x','y','z'])]
expected[part['name']]={'bounds':[[point[i]+shift[i] for i in range(3)] for point in part['boundsMm']]}
if set(actual)!=set(expected) or len(actual)!=19:raise ValueError('Actual native assembly model coverage changed')
for name,wanted in expected.items():
got=actual[name];delta=float(np.max(np.abs(np.array(got['bounds'])-np.array(wanted['bounds']))))
if delta>1e-4:raise ValueError(f'Actual assembly pose differs: {entry}/{name}/{delta}mm')
if 'triangles' in wanted and got['triangles']!=wanted['triangles']:raise ValueError(f'Actual electronics triangles changed: {entry}/{name}')
checks.append({'view':entry,'model':name,'maximum_bounds_delta_mm':delta,'triangles':got['triangles'],'passes':True})
output.write_text(json.dumps({'actual_native_assembly_glb_readback':True,'checks':checks,'passes':True,'source_circuit_sha256':hashlib.sha256((ROOT/'dist/index/circuit.json').read_bytes()).hexdigest()},indent=2)+'\n')
print('Actual closed/exploded native GLBs:38 model pose checks and all electronics triangle counts PASS')
if __name__=='__main__':
parser=argparse.ArgumentParser();parser.add_argument('--output',type=Path,required=True);review(parser.parse_args().output)