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-product-browser.py

"""Check read-only native browser mesh bounds against qualified source poses."""
import argparse
import hashlib
import itertools
import json
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]


def collect_groups(meshes, names):
    groups = {name: [] for name in names}
    for mesh in meshes:
        key = mesh['name'] if mesh['name'] in groups else mesh['parent']
        if key in groups:
            groups[key].append(mesh)
    if any(not rows for rows in groups.values()):
        raise ValueError('Native browser omitted a required physical model')
    return groups


def verify_view(options, context):
    view, path = options
    pcb, geometry, models = context
    circuit_path = ROOT/'dist'/('enclosure' if view == 'closed' else 'product.exploded')/'circuit.json'
    circuit = json.loads(circuit_path.read_text())
    source_names = {row['source_component_id']: row['name'] for row in circuit if row['type']=='source_component'}
    cad_by_name = {source_names[row['source_component_id']]:row for row in circuit if row['type']=='cad_component'}
    meshes = json.loads(path.read_text())
    groups = collect_groups(meshes, [row['name'] for row in pcb['meshes']]+[row['name'] for row in geometry['parts']])
    board = cad_by_name['R9PCBAndFittedParts1']['position']
    if pcb['assetRotationDegrees'] != 90:
        raise ValueError('Browser review currently requires the qualified 90 degree pose')
    checks = []
    for name, rows in groups.items():
        original = next((row for row in pcb['meshes'] if row['name']==name), None)
        if original:
            lower, upper = original['boundsMm']
            # Independent qualified PCB XY -> product XY: (-Y,+X), preserving +Z.
            points = [[board['x']-y, board['y']+x, board['z']+z]
                      for x,y,z in itertools.product(*zip(lower,upper))]
            expected = [[min(point[axis] for point in points) for axis in range(3)],
                        [max(point[axis] for point in points) for axis in range(3)]]
            if name=='L3':
                if sum(row['triangles'] for row in rows)!=original['triangles']:
                    raise ValueError(f'Native supplier OBJ triangle coverage changed: {view}/{name}')
            elif sum(row['vertices'] for row in rows) != original['vertices']:
                raise ValueError(f'Browser changed original PCB vertices: {view}/{name}')
        else:
            part = next(row for row in geometry['parts'] if row['name']==name)
            model = next(row for row in models['models'] if row['part']==name)
            if sum(row['triangles'] for row in rows) != model['triangles']:
                raise ValueError(f'Browser changed mechanical triangles: {view}/{name}')
            expected = part['boundsMm']
            lower, upper = part['localBoundsMm']
            shift = [cad_by_name[name]['position'][axis]-(lower[i]+upper[i])/2
                     for i,axis in enumerate(['x','y','z'])]
            expected = [[point[i]+shift[i] for i in range(3)] for point in expected]
        actual = [[min(row['bounds'][0][axis] for row in rows) for axis in range(3)],
                  [max(row['bounds'][1][axis] for row in rows) for axis in range(3)]]
        delta = max(abs(expected[i][axis]-actual[i][axis]) for i in range(2) for axis in range(3))
        if delta > 1e-4:
            raise ValueError(f'Browser model pose differs from qualified source: {view}/{name}: {delta} mm')
        checks.append({'view':view,'model':name,'maximum_bounds_delta_mm':delta,'vertices':sum(row['vertices'] for row in rows),'triangles':sum(row['triangles'] for row in rows),'passes':True})
    return checks


def review(options):
    pcb = json.loads((ROOT/'product/pcb-model-review.json').read_text())
    geometry = json.loads((ROOT/'product/geometry-review.json').read_text())
    models = json.loads((ROOT/'product/mechanical-model-review.json').read_text())
    browser = json.loads((options.browser_output/'review.json').read_text())
    if browser['browser_errors'] or not browser['passes']:
        raise ValueError('Native browser rendering did not pass')
    checks = []
    for view in ['closed','exploded']:
        checks.extend(verify_view((view, options.browser_output/(view+'-browser-meshes.json')), (pcb,geometry,models)))
    source_hashes = {str(path.relative_to(ROOT)):hashlib.sha256(path.read_bytes()).hexdigest()
                     for path in (ROOT/'product/models').rglob('*.glb')}
    obj_path=ROOT/'imports/AHW2012C_24NJTF/AHW2012C_24NJTF.obj'
    source_hashes[str(obj_path.relative_to(ROOT))]=hashlib.sha256(obj_path.read_bytes()).hexdigest()
    for model in browser['models_requested']:
        if not model['matches'] or source_hashes[model['path']] != model['sha256']:
            raise ValueError('Browser loaded a stale or changed asset')
    if set(row['path'] for row in browser['models_requested']) != set(source_hashes):
        raise ValueError('Browser did not load all 19 actual native assets')
    report = {'browser_render_review':True,'viewer_version':browser['viewer_version'],'browser_version':browser['browser_version'],
              'models_checked':checks,'geometry_checks':len(checks),'all_19_assets_match':True,'model_sha256':source_hashes,
              'circuit_sha256':hashlib.sha256((ROOT/'dist/index/circuit.json').read_bytes()).hexdigest(),
              'view_circuit_sha256':{view:hashlib.sha256((ROOT/'dist'/view/'circuit.json').read_bytes()).hexdigest() for view in ['enclosure','product.exploded']},
              'browser_errors':browser['browser_errors'],'orbit_gesture_executed':browser['orbit_gesture_executed'],
              'physical_fit_testing_claimed':False,'separate_native_gltf_export':'failed in initial test; Maximum call stack size exceeded',
              'passes':True}
    options.output.parent.mkdir(parents=True,exist_ok=True)
    options.output.write_text(json.dumps(report,indent=2)+'\n')
    print(f'Actual native browser PCB and mechanical poses: {len(checks)} checks PASS; 19 exact asset loads')


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--browser-output',type=Path,required=True)
    parser.add_argument('--output',type=Path,required=True)
    review(parser.parse_args())