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)