建立方向盘项目初始版本

This commit is contained in:
2026-10-29 22:04:55 +08:00
commit 9962cbfbce
834 changed files with 1519203 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
"""Create first-fit printing parts for M9C12 + MT6826S in FreeCAD GUI.
Dimension assumptions are documented in the associated user instructions.
Run using rebuild_encoder_print_parts.FCMacro. Does not change motor source.
"""
import math
from pathlib import Path
import FreeCAD as App
import FreeCADGui as Gui
import Part, Mesh, Import
from pivy import coin
root = Path(__file__).resolve().parent.parent
out = root / 'models' / 'encoder_print_v1'
out.mkdir(parents=True, exist_ok=True)
V = App.Vector
doc = App.newDocument('EncoderPrintV1')
def cyl(r, h, x=0, y=0, z=0):
return Part.makeCylinder(r, h, V(x,y,z))
def obj(name, label, shape, color):
o=doc.addObject('PartDesign::Feature', name); o.Label=label; o.Shape=shape
o.ViewObject.ShapeColor=color; o.ViewObject.LineColor=(0.1,0.1,0.1)
return o
def check(name, shape):
if shape.isNull() or not shape.isValid() or len(shape.Solids)!=1 or shape.Volume<0.01:
raise RuntimeError('Invalid or non-single solid '+name)
# rear motor face z=0; positive z is away from motor.
# Base and tower remain clear of three PCB screw heads when rotating.
holder = cyl(13.5,3).fuse(cyl(10,4.5,z=3)).removeSplitter()
holder = holder.cut(cyl(1.65,7.6,z=-0.05))
# Screw head pocket from z=1.9 to 4.5; nominal M3 head 6 mm assumed.
holder = holder.cut(cyl(3.6,2.6,z=1.9))
# Magnet pocket: 10.3 mm diameter nominal, 3 mm deep.
holder = holder.cut(cyl(5.15,3.05,z=4.5))
# An edge notch allows gentle magnet removal. Never pry on magnet face.
notch=Part.makeBox(6,2.5,2.4,V(4.5,-1.25,5.1))
holder=holder.cut(notch).removeSplitter()
check('holder',holder)
# Gap at no shim 0.5 mm. Magnet face z=7.5, chip face z=8.0;
# chip is assumed 0.8 mm high, so board near face z=8.8 and far face 10.4.
# Add equal 0.5 mm under feet for initial nominal 1 mm gap.
board_near=8.8
board_far=10.4
ring_z=board_far
ring_h=3.2
bracket=cyl(20.5,ring_h,z=ring_z).cut(cyl(16.8,ring_h+0.02,z=ring_z-0.01))
pcb_centres=[(0,13.49),(-11.71,-6.76),(11.71,-6.76)]
# Three local bosses/bridges; PCB rests only locally instead of an unbroken ring.
for x,y in pcb_centres:
boss=cyl(3.2,ring_h,x,y,ring_z)
a=math.degrees(math.atan2(y,x))
bridge=Part.makeBox(6.0,4.0,ring_h,V(13,-2,ring_z))
bridge.rotate(V(0,0,0),V(0,0,1),a)
bracket=bracket.fuse(boss).fuse(bridge)
for i in range(4):
angle=45+i*90
a=math.radians(angle); ux,uy=math.cos(a),math.sin(a)
# Post nearest face at r=16.8; PCB nominal radius 16.5.
# M4 head has 0.7 mm radial clearance if its diameter is 7 mm.
post=cyl(2.0,ring_z-2,18.8*ux,18.8*uy,2)
foot=cyl(4.5,2,25*ux,25*uy,0)
bridge=Part.makeBox(10,4,2,V(18,-2,0)); bridge.rotate(V(0,0,0),V(0,0,1),angle)
bracket=bracket.fuse(post).fuse(foot).fuse(bridge)
bracket=bracket.cut(cyl(2.2,2.05,25*ux,25*uy,-0.02))
# Ø1.8 pilot bores for M2 self-tapping screws, through 3.2 mm support thickness.
for x,y in pcb_centres: bracket=bracket.cut(cyl(0.9,ring_h+0.04,x,y,ring_z-0.02))
bracket=bracket.removeSplitter()
check('bracket',bracket)
shim05=cyl(4.5,.5).cut(cyl(2.2,.52,z=-.01))
shim10=cyl(4.5,1).cut(cyl(2.2,1.02,z=-.01))
check('shim05',shim05); check('shim10',shim10)
# Export all printable parts at z=0 in an independent print-parts document.
parts=App.newDocument('EncoderPartsPrint')
def pobj(name,label,shape,color):
o=parts.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=shape
o.ViewObject.ShapeColor=color;return o
ph=pobj('MagnetHolder','Magnet holder - print motor face down',holder,(0.96,.55,.12))
print_bracket=bracket.copy();print_bracket.translate(V(0,0,-(ring_z+ring_h)))
print_bracket.rotate(V(0,0,0),V(1,0,0),180)
pb=pobj('EncoderBracket','Bracket - print PCB ring face down',print_bracket,(.2,.48,.8))
s05=pobj('Shim05','M4 shim 0.5mm - print 4',shim05,(.7,.7,.7))
s10=pobj('Shim10','M4 shim 1.0mm - print 4 optional',shim10,(.7,.7,.7))
parts.recompute()
for o in [ph,pb,s05,s10]:
check(o.Name,o.Shape)
Mesh.export([o], str(out/(o.Name+'.stl')))
Import.export([o], str(out/(o.Name+'.step')))
mesh_results=[]
for name in ['MagnetHolder','EncoderBracket','Shim05','Shim10']:
mesh=Mesh.Mesh(str(out/(name+'.stl')))
solid=mesh.isSolid(); bounds=mesh.BoundBox
mesh_results.append('%s solid=%s facets=%d volume=%.5f ZMin=%.5f ZMax=%.5f' %
(name,solid,mesh.CountFacets,mesh.Volume,bounds.ZMin,bounds.ZMax))
if not solid or mesh.Volume <= 0 or abs(bounds.ZMin)>0.001:
raise RuntimeError('STL verification failed: '+mesh_results[-1])
out.joinpath('mesh_check.txt').write_text('\n'.join(mesh_results)+'\n',encoding='utf-8')
parts.saveAs(str(out/'EncoderParts_Print.FCStd'))
# Assembly with nominal 0.5 mm shim under all four feet (1 mm sensor gap).
App.setActiveDocument(doc.Name)
motor=Part.read(str(root/'models'/'MAD_M9C12.step'))
motor.rotate(V(0,0,0),V(1,0,0),180)
mo=obj('Motor','Original approximate M9C12',motor,(.34,.36,.38));mo.ViewObject.Transparency=70
ho=obj('Holder','Print magnet holder',holder,(.96,.55,.12))
mag=cyl(5,3,z=4.5);mg=obj('Magnet','10 x 3 mm diametric magnet',mag,(.78,.78,.8))
br=bracket.copy();br.translate(V(0,0,.5));bo=obj('Bracket','Print bracket with 0.5mm shims',br,(.2,.48,.8))
pcb=cyl(16.5,1.6,z=board_near+.5)
for x,y in pcb_centres: pcb=pcb.cut(cyl(1.5,1.62,x,y,board_near+.49))
pc=obj('PCB','PCB approximate',pcb,(.06,.6,.22))
chip=Part.makeBox(4,5,.8,V(-2,-2.5,board_near+.5-.8))
ch=obj('Chip','Chip approximate',chip,(.1,.1,.1))
for i in range(4):
a=math.radians(45+i*90); x,y=25*math.cos(a),25*math.sin(a)
so=obj('Shim%d'%i,'0.5mm shim %d'%(i+1),cyl(4.5,.5,x,y,0).cut(cyl(2.2,.52,x,y,-.01)),(.8,.8,.8))
# Inspect interferences in the simplified assembly. Include nominal M2
# screw heads 4.0 mm dia, 1.5 mm high on the motor-facing PCB side.
m2heads=[]
for x,y in pcb_centres:
head=cyl(2,1.5,x,y,board_near+.5-1.5)
m2heads.append(head)
obj('PCBHead%d'%len(m2heads),'M2 head envelope 4x1.5 assumed',head,(.8,.8,.8))
checks=[('magnet_holder',mag,holder),('holder_bracket',holder,br),('magnet_bracket',mag,br),('chip_magnet',chip,mag),('board_bracket',pcb,br)]
checks += [('holder_M2head_%d'%i,holder,h) for i,h in enumerate(m2heads,1)]
results=[]
for name,a,b in checks:
vol=a.common(b).Volume
results.append('%s %.6f mm3'%(name,vol))
if vol>0.001: raise RuntimeError('Interference '+results[-1])
doc.recompute()
for o in [mo,ho,mg,bo,pc,ch]:
if o.Shape.isNull() or not o.Shape.isValid(): raise RuntimeError('Invalid assembly '+o.Name)
doc.saveAs(str(out/'EncoderAssembly_Fit.FCStd'))
view=Gui.activeDocument().activeView()
view.getCameraNode().orientation.setValue(coin.SbRotation(0.880476, 0.115917, -0.279848, -0.364705))
view.fitAll();Gui.updateGui();view.saveImage(str(out/'assembly.png'),1600,1200,'White')
out.joinpath('geometry_check.txt').write_text('\n'.join(results)+'\nNominal chip-face-to-magnet-face gap: 1 mm\n',encoding='utf-8')
out.joinpath('BUILD_OK.txt').write_text('success\n',encoding='utf-8')
print('PRINT_PARTS_OK')