AnasSarkiz/NEMA14_RP2040
A NEMA motor assembly concept with a controller board, four proposed M3 screws and spacers, motor lead splices, and a JST 4-pin connector harness.
- Version
- 1.13.0
- License
- ISC
- Stars
- 0
scripts/check-exported-harness.py
"""Independently inspect the exported GLB mesh, its units and wire materials."""
import hashlib,json,pathlib,struct
import numpy as np
root=pathlib.Path(__file__).resolve().parents[1]
data=(root/'artifacts/top-assembly/assembled.glb').read_bytes()
assert data[:4]==b'glTF'
json_size=struct.unpack_from('<I',data,12)[0]
gltf=json.loads(data[20:20+json_size]);binary=data[28+json_size:]
nodes=gltf['nodes'];world={}
def 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]);rotation=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)]])
m=np.eye(4);m[:3,:3]=rotation@np.diag(node.get('scale',[1,1,1]));m[:3,3]=node.get('translation',[0,0,0]);return m
def visit(index,parent):
world[index]=parent@matrix(nodes[index])
for child in nodes[index].get('children',[]):visit(child,world[index])
for index in gltf['scenes'][gltf.get('scene',0)]['nodes']:visit(index,np.eye(4))
def geometry(name):
matches=[i for i,n in enumerate(nodes) if n.get('name')==name];assert len(matches)==1,(name,matches)
positions=[];colors=[]
def collect(i):
node=nodes[i]
if 'mesh' in node:
for primitive in gltf['meshes'][node['mesh']]['primitives']:
accessor=gltf['accessors'][primitive['attributes']['POSITION']];assert accessor['type']=='VEC3' and accessor['componentType']==5126
view=gltf['bufferViews'][accessor['bufferView']];offset=view.get('byteOffset',0)+accessor.get('byteOffset',0)
p=np.ndarray((accessor['count'],3),dtype='<f4',buffer=binary,offset=offset,strides=(view.get('byteStride',12),4)).astype(float)
p=np.column_stack([p,np.ones(len(p))])@world[i].T
# The native exporter uses (x,y,z)->(-x,z,y), also visible in the
# unchanged PCB connector node translations. Undo that proper rotation.
positions.append(np.column_stack([-p[:,0],p[:,2],p[:,1]]))
material=gltf['materials'][primitive['material']]
colors.append(material['pbrMetallicRoughness']['baseColorFactor'][:3])
for child in node.get('children',[]):collect(child)
collect(matches[0]);assert positions,name
return np.vstack(positions),colors
harness=json.loads((root/'artifacts/top-assembly/harness-definition.json').read_text())
rows=[]
for wire in harness['wires']:
pin=wire['pin'];points,colors=geometry('MOTOR_LEAD_PIN_'+str(pin))
assert all(np.linalg.norm(np.array(c)-wire['rgb'])<1e-6 for c in colors),(pin,colors,wire['rgb'])
gaps=[]
for i,(a,b) in enumerate(zip(wire['path'],wire['path'][1:])):
aa=np.array([a[k] for k in 'xyz']);ab=np.array([b[k] for k in 'xyz']);delta=ab-aa
t=np.clip(((points-aa)@delta)/np.dot(delta,delta),0,1)
distances=np.linalg.norm(points-aa-t[:,None]*delta,axis=1)
radius=wire['segment_radii_mm'][i]
gaps.append(distances-radius)
maximum=float(np.min(np.vstack(gaps),axis=0).max())
assert maximum<5e-5,('Exported wire escapes its checked path; check rotation units or origin',pin,maximum)
endpoint=np.array([wire['path'][-1][k] for k in 'xyz'])
cap=np.logical_and(np.abs(points[:,2]-endpoint[2])<5e-5,np.abs(np.linalg.norm(points[:,:2]-endpoint[:2],axis=1)-wire['pigtail_radius_mm'])<5e-5)
assert int(cap.sum())>=8,('No rendered terminal ring at its verified PCB contact',pin)
rows.append({'pin':pin,'color':wire['color'],'material_matches_factory_color':True,
'rendered_terminal_ring_vertices':int(cap.sum()),'maximum_mesh_excursion_beyond_checked_wire_mm':maximum})
splice_points,splice_colors=geometry('MOTOR_SPLICE_PIN_'+str(pin))
assert all(np.linalg.norm(np.array(c)-[.06,.06,.06])<1e-6 for c in splice_colors)
splice=wire['splice'];a=np.array([splice['from'][k] for k in 'xyz']);b=np.array([splice['to'][k] for k in 'xyz']);delta=b-a
t=np.clip(((splice_points-a)@delta)/np.dot(delta,delta),0,1)
assert (np.linalg.norm(splice_points-a-t[:,None]*delta,axis=1)-splice['radius']).max()<5e-5
plug,_=geometry('JST_SHR_04V_S_PLUG')
assert abs(float(plug[:,2].min())-harness['header_top_z_mm'])<5e-5
assert abs(float(plug[:,2].max())-harness['housing_top_z_mm'])<5e-5
assert abs(float(plug[:,2].max())-harness['board_surface_z_mm']-6.3)<5e-5
report={'passed':True,'glb_sha256':hashlib.sha256(data).hexdigest(),
'rendered_meshes_follow_verified_conductor_paths':True,'actual_mesh_contact_rings_match_board_pins':True,
'actual_mesh_materials_match_factory_colors':True,'actual_exported_housing_is_seated':True,
'pin_checks':rows,'physical_assembly_tested':False}
(root/'artifacts/top-assembly/harness-mesh-verification.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))