component
Sunon MF40200V1-1000U-G99 · 5 V tachometer fan
Public Made by Adomby adom
Tachometer fan with factory flying leads, an explicit Adom connector harness and gentle GLB state demonstration.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
"""AI-authored lead/harness geometry from stated manufacturer dimensions.
Preserves the existing nominal fan model; cable routing and connector internals
are illustrative. No manufacturer STEP or drawing is redistributed by this tool.
"""
from pathlib import Path
import cadquery as cq
import json,math,struct,hashlib
P=Path(__file__).resolve().parents[1]
OUT=P/'cad/harness';OUT.mkdir(exist_ok=True)
def read_glb(path):
b=path.read_bytes();n=struct.unpack_from('<I',b,12)[0];g=json.loads(b[20:20+n]);p=20+n;length=struct.unpack_from('<I',b,p)[0];return g,bytearray(b[p+8:p+8+length])
def write_glb(path,g,blob):
g['buffers'][0]['byteLength']=len(blob);j=json.dumps(g,separators=(',',':')).encode();j+=b' '*(-len(j)%4);blob+=b'\0'*(-len(blob)%4)
path.write_bytes(struct.pack('<III',0x46546c67,2,28+len(j)+len(blob))+struct.pack('<II',len(j),0x4e4f534a)+j+struct.pack('<II',len(blob),0x004e4942)+blob)
def add_shape(g,blob,shape,name,color,parent,tooltip):
vertices,triangles=shape.tessellate(.08,.18);pts=[[v.x*.001,v.y*.001,v.z*.001]for v in vertices];indices=[i for t in triangles for i in t]
def acc(flat,count,kind,ctype,fmt,bounds=None):
blob.extend(b'\0'*(-len(blob)%4));off=len(blob);blob.extend(struct.pack('<'+fmt*len(flat),*flat));vi=len(g.setdefault('bufferViews',[]));g['bufferViews'].append({'buffer':0,'byteOffset':off,'byteLength':len(blob)-off});ai=len(g['accessors']);x={'bufferView':vi,'componentType':ctype,'count':count,'type':kind}
if bounds:x.update(min=bounds[0],max=bounds[1])
g['accessors'].append(x);return ai
pa=acc([x for p in pts for x in p],len(pts),'VEC3',5126,'f',([min(p[i]for p in pts)for i in range(3)],[max(p[i]for p in pts)for i in range(3)]));ia=acc(indices,len(indices),'SCALAR',5125,'I')
mi=len(g.setdefault('materials',[]));g['materials'].append({'name':name+'.Material','pbrMetallicRoughness':{'baseColorFactor':[*color,1],'metallicFactor':.65 if 'Contact' in name or 'Stripped' in name else 0,'roughnessFactor':.55},'doubleSided':True})
mesh=len(g.setdefault('meshes',[]));g['meshes'].append({'name':name,'primitives':[{'attributes':{'POSITION':pa},'indices':ia,'material':mi}]});node=len(g['nodes']);g['nodes'].append({'name':name,'mesh':mesh,'extras':{'tooltip':tooltip}});g['nodes'][parent].setdefault('children',[]).append(node)
def path(z,tail=0):
V=lambda p:cq.Vector(p[0],p[1],z)
edges=[];cur=(20,15)
def line(p):
nonlocal cur
edges.append(cq.Edge.makeLine(V(cur),V(p)));cur=p
def arc(mid,p):
nonlocal cur
edges.append(cq.Edge.makeThreePointArc(V(cur),V(mid),V(p)));cur=p
d=10/math.sqrt(2)
line((50,15));arc((50+d,5+d),(60,5));line((60,-25));arc((50+d,-25-d),(50,-35));line((30,-35));arc((30-d,-45+d),(20,-45));line((20,-70));arc((30-d,-70-d),(30,-80));line((70,-80));arc((70+d,-70-d),(80,-70));line((80,-20));arc((90-d,-20+d),(90,-10))
remaining=305-sum(e.Length()for e in edges)-tail;line((90+remaining,-10))
return cq.Wire.assembleEdges(edges),cur
def tube(w,z,r):
return cq.Workplane(cq.Plane(origin=(20,15,z),normal=(1,0,0),xDir=(0,1,0))).circle(r).sweep(w,isFrenet=True).val()
report={'sourceFan':'Sunon D04114960G-01 p6','leadSpec':{'awg':26,'style':'UL1007','lengthMm':305,'toleranceMm':10,'stripMm':5,'stripToleranceMm':.3},'wireOutsideDiameterMm':1.2,'wireOutsideDiameterStatus':'illustrative; verify actual insulation OD before crimp qualification','cableRoute':'illustrative uninstalled serpentine; not a minimum bend radius qualification','connector':{'housing':'M20-1060300','contacts':'3 x M20-1180046','matingHeader':'M20-9990346','factorySupplied':False,'polarized':False,'stripPreparation':'Manufacturer contact specifies 3-4 mm strip. Prepare the factory 5 mm ends accordingly; do not blindly crimp the supplied strip length.'},'limitations':['Fan rotor remains illustrative','Connector internal retention details simplified','No mating or crimp qualification','Bare-lead model and added-harness variant are distinct'],'variants':{}}
for harness in [False,True]:
g,blob=read_glb(P/'states/fan-animated.glb');root=len(g['nodes']);g['nodes'].append({'name':'Fan.Connection','children':[],'extras':{'adomLayer':{'key':'connection','label':'Adom harness (added)'if harness else'Factory flying leads','group':'Assembly'},'tooltip':'Added Adom harness, not factory supplied'if harness else'Sunon flying leads, no factory connector'}});g['nodes'][0]['children'].append(root)
assy=cq.Assembly(name='Adom_harness'if harness else'Sunon_flying_leads');lengths=[]
for i,(name,color)in enumerate([('Red_5V',(0.65,.035,.035)),('Black_GND',(.04,.045,.05)),('Yellow_FG',(.85,.65,.02))]):
z=3+2.54*i;w,end=path(z,5 if not harness else 1.5);wire=tube(w,z,.6);lengths.append(w.Length()+(5 if not harness else 0));assy.add(wire,name='Lead_'+name,color=cq.Color(*color));add_shape(g,blob,wire,'Fan.Lead.'+name,color,root,name+'; 26 AWG; cable routing illustrative')
if not harness:
tip=cq.Solid.makeCylinder(.2025,5,cq.Vector(end[0],end[1],z),cq.Vector(1,0,0));assy.add(tip,name='Stripped_'+name,color=cq.Color(.72,.74,.76));add_shape(g,blob,tip,'Fan.Stripped.'+name,(.72,.74,.76),root,'Nominal 5 mm stripped end; no factory connector')
if harness:
# Housing front/rear along X; circuit row along Z. Nominal 7.82 x 2.5 x 14 mm.
rear=end[0];housing=cq.Workplane('XY').box(14,2.5,7.82).translate((rear+7,-10,5.54))
for i in range(3):housing=housing.cut(cq.Workplane('XY').box(14.2,1.0,1.0).translate((rear+7,-10,3+2.54*i)))
housing=housing.val();assy.add(housing,name='M20_1060300_Housing',color=cq.Color(.055,.06,.065));add_shape(g,blob,housing,'Connector.M20-1060300.Housing',(.055,.06,.065),root,'Harwin M20-1060300; added Adom termination; unpolarized')
for i in range(3):
contact=cq.Workplane('XY').box(9,.85,.85).translate((rear+5,-10,3+2.54*i)).cut(cq.Workplane('XY').box(9.2,.65,.65).translate((rear+5,-10,3+2.54*i))).val();assy.add(contact,name=f'Contact_{i+1}',color=cq.Color(.7,.72,.74));add_shape(g,blob,contact,f'Connector.Contact.{i+1}',(.7,.72,.74),root,f'M20-1180046 contact {i+1}; simplified geometry')
mark=cq.Workplane('XZ',origin=(rear+9,-11.27,3)).polyline([(-.65,-.5),(.65,-.5),(0,.6)]).close().extrude(.03).val();assy.add(mark,name='Pin1_display_marker',color=cq.Color(.65,.68,.7));add_shape(g,blob,mark,'Connector.Pin1.Reference',(.65,.68,.7),root,'Circuit 1 reference; confirm molded triangle before assembly')
stem='fan-adom-harness'if harness else'fan-with-flying-leads';write_glb(OUT/(stem+'.glb'),g,blob);assy.export(str(OUT/(stem+'-connection.step')))
report['variants'][stem]={'leadPathLengthsMm':lengths,'glbSha256':hashlib.sha256((OUT/(stem+'.glb')).read_bytes()).hexdigest(),'stepScope':'Connection geometry only; combine with unchanged fan STEP'}
(OUT/'connection-provenance.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report,indent=2))