Motor assembly

This commit is contained in:
Leni Aniva 2025-05-29 16:21:01 -07:00
parent 6709e4f32e
commit 0f151bd279
Signed by: aniva
GPG Key ID: D5F96287843E8DFB
3 changed files with 312 additions and 42 deletions

View File

@ -34,7 +34,7 @@ class Role(Flag):
STRUCTURE = auto()
DECORATION = auto()
ELECTRONIC = auto()
MOTION = auto()
MOTOR = auto()
# Fasteners, etc.
CONNECTION = auto()
@ -69,7 +69,7 @@ ROLE_COLOR_MAP = {
Role.STRUCTURE: _color('gray', 0.4),
Role.DECORATION: _color('lightseagreen', 0.4),
Role.ELECTRONIC: _color('mediumorchid', 0.7),
Role.MOTION: _color('thistle3', 0.7),
Role.MOTOR: _color('thistle3', 0.7),
Role.CONNECTION: _color('steelblue3', 0.8),
Role.HANDLE: _color('tomato4', 0.8),
}

View File

@ -35,7 +35,7 @@ class FlatHeadBolt(Item):
centered=(True, True, False))
)
rod.faces("<Z").tag("tip")
rod.faces(">Z").tag("root")
rod.tagAbsolute("root", (0, 0, self.height_thread), direction="-Z")
rod = rod.union(head.located(Cq.Location((0, 0, self.height_thread))))
return rod

View File

@ -31,6 +31,15 @@ BOLT_COMMON = FlatHeadBolt(
height_thread=30.0,
pitch=1.0,
)
BOLT_LONG = FlatHeadBolt(
# FIXME: measure
mass=0.0,
diam_head=12.8,
height_head=2.8,
diam_thread=6.0,
height_thread=50.0,
pitch=1.0,
)
BOLT_BEARING = FlatHeadBolt(
# FIXME: measure
mass=0.0,
@ -41,6 +50,59 @@ BOLT_BEARING = FlatHeadBolt(
pitch=1.0,
)
@dataclass(frozen=True)
class FlangeCoupler(Model):
diam_thread: float = 8.0
diam_inner: float = 10.0
diam_outer: float = 22.0
height: float = 12.0
height_flange: float = 2.0
diam_thread_flange: float = 4.0
n_hole_flange: int = 4
r_hole_flange: float = 8.0 # FIXME: Measure!
def generate(self) -> Cq.Workplane:
result = (
Cq.Workplane()
.cylinder(
radius=self.diam_outer/2,
height=self.height_flange,
centered=(True, True, False),
)
.faces(">Z")
.cylinder(
radius=self.diam_inner/2,
height=self.height - self.height_flange,
centered=(True, True, False),
)
.faces(">Z")
.hole(self.diam_thread)
)
holes = (
Cq.Workplane()
.sketch()
.regularPolygon(
self.r_hole_flange,
self.n_hole_flange,
mode="c",
tag="holes",
)
.vertices(tag="holes")
.circle(self.diam_thread_flange/2)
.finalize()
.extrude(self.height_flange)
)
result -= holes
result.tagAbsolute("top", (0, 0, self.height), direction="+Z")
result.tagAbsolute("bot", (0, 0, 9), direction="-Z")
for i in range(self.n_hole_flange):
loc = Cq.Location.rot2d(i * 360 / self.n_hole_flange) * Cq.Location(self.r_hole_flange, 0)
result.tagAbsolute(f"holeT{i}", loc * Cq.Location(0, 0, self.height_flange), direction="+Z")
result.tagAbsolute(f"holeB{i}", loc, direction="-Z")
return result
@dataclass(frozen=True)
class Motor(Model):
@ -53,13 +115,15 @@ class Motor(Model):
power: float = 30.0 # watts
diam_thread: float = 4.0
diam_shaft: float = 8.0
diam_body: float = 51.0
height_body: float = 83.5
diam_ring: float = 25.93
height_ring: float = 6.55
height_shaft: float = 38.1
height_base_shaft: float = 20.0 # FIXME: Measure
# Distance between anchor and the body
dx_anchor: float = 20.2
dx_anchor: float = 20.2 # FIXME: Measure
height_anchor: float = 10.4
def __post_init__(self):
@ -68,6 +132,13 @@ class Motor(Model):
assert self.dx_anchor < self.diam_body / 2
pass
@property
def dist_mount_rotor(self):
"""
Distance between mount point and shaft
"""
return self.height_base_shaft + self.height_ring
def generate(self) -> Cq.Workplane:
result = (
Cq.Workplane()
@ -83,17 +154,26 @@ class Motor(Model):
centered=(True, True, False)
)
)
base_shaft = Cq.Solid.makeBox(
length=self.diam_shaft,
width=self.diam_shaft,
height=self.height_base_shaft,
).moved(-self.diam_shaft/2, -self.diam_shaft/2, self.height_body)
shaft = Cq.Solid.makeCylinder(
radius=self.diam_thread/2,
radius=self.diam_shaft/2,
height=self.height_shaft,
pnt=(0, 0, self.height_body)
pnt=(0, 0, self.height_body + self.height_base_shaft)
)
z_anchor = self.height_body - self.height_ring
anchor = Cq.Solid.makeCylinder(
radius=self.diam_thread/2,
height=self.height_anchor,
pnt=(0, 0, self.height_body - self.height_ring)
pnt=(0, 0, z_anchor)
)
result = result + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0)
result = result + base_shaft + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0)
result.tagAbsolute("anchor1", (self.dx_anchor, 0, z_anchor), direction="+Z")
result.tagAbsolute("anchor2", (-self.dx_anchor, 0, z_anchor), direction="+Z")
result.tagAbsolute("shaft", (0, 0, self.height_body + self.height_base_shaft), direction="+Z")
return result
@ -172,10 +252,11 @@ class Onbashira(Model):
# Extra bind sites for stator to prevent warping
stator_bind_extra: int = 2
rotor_inner_radius: float = 36.0
rotor_bind_bolt_diam: float = BOLT_COMMON.diam_thread
rotor_bind_bolt_diam: float = BOLT_BEARING.diam_thread
rotor_bind_radius: float = 82.0
rotor_bind_extra: int = 1
rotor_spacer_outer_diam: float = 15.0
stator_spacer_outer_diam: float = 15.0
rotor_spacer_outer_diam: float = 12.0
handle_base_height: float = 10.0
handle_thickness: float = 17.0
@ -183,6 +264,8 @@ class Onbashira(Model):
handle_height: float = 50.0
motor: Motor = Motor()
flange_coupler: FlangeCoupler = FlangeCoupler()
auxiliary_thickness: float = 25.4 / 8
material_side: Material = Material.WOOD_BIRCH
material_bearing: Material = Material.PLASTIC_PLA
@ -191,6 +274,7 @@ class Onbashira(Model):
material_barrel: Material = Material.ACRYLIC_BLACK
material_brace: Material = Material.PLASTIC_PLA
material_fastener: Material = Material.STEEL_STAINLESS
material_auxiliary: Material = Material.WOOD_BIRCH
def __post_init__(self):
assert self.n_side >= 3
@ -235,7 +319,15 @@ class Onbashira(Model):
theta = math.pi / self.n_side
dt = self.angle_joint_thickness * math.tan(theta)
return dt * 2
@property
def angle_joint_bind_pos(self) -> Cq.Location:
"""
Planar position of the joint bind position
"""
th = math.pi / self.n_side
x = self.angle_joint_bind_radius * math.cos(th)
y = self.angle_joint_bind_radius * math.sin(th)
return Cq.Location.from2d(x, y)
@property
def angle_dihedral(self) -> float:
@ -270,6 +362,153 @@ class Onbashira(Model):
.extrude(self.side_width * 1.5)
)
@target(name="motor-driver-shaft")
def motor_driver_shaft(self) -> Cq.Workplane:
"""
Driver shaft which connects to each barrel to move them.
The driver shaft reaches
"""
return (
Cq.Workplane()
.cylinder(
radius=self.barrel_diam/2,
height=20.0
)
)
@target(name="motor-driver-disk", kind=TargetKind.DXF)
def profile_motor_driver_disk(self) -> Cq.Sketch:
"""
A drive disk mounts onto the motor, and extends into gun barrels to turn them.
"""
return (
Cq.Sketch()
.circle(self.rotor_radius)
# Drill out the centre which will accomodate the motor shaft
.circle(self.motor.diam_shaft/2, mode="s")
# Drill out couplers
.reset()
.regularPolygon(
self.flange_coupler.r_hole_flange,
self.flange_coupler.n_hole_flange,
mode="c",
tag="hole",
)
.vertices(tag="hole")
.circle(self.flange_coupler.diam_thread_flange/2, mode="s")
.reset()
.regularPolygon(
self.rotation_radius,
self.n_side,
mode="c",
tag="const",
)
.vertices(tag="const")
.circle(BOLT_COMMON.diam_thread/2, mode="s")
)
def motor_driver_disk(self) -> Cq.Workplane:
result = (
Cq.Workplane()
.placeSketch(self.profile_motor_driver_disk())
.extrude(self.auxiliary_thickness)
)
n = self.flange_coupler.n_hole_flange
for i in range(n):
loc = Cq.Location.rot2d(i * 360 / n) * Cq.Location(self.flange_coupler.r_hole_flange, 0)
result.tagAbsolute(f"holeT{i}", loc * Cq.Location(0, 0, self.auxiliary_thickness), direction="+Z")
result.tagAbsolute(f"holeB{i}", loc, direction="-Z")
return result
@target(name="motor-mount-plate", kind=TargetKind.DXF)
def profile_motor_mount_plate(self) -> Cq.Sketch:
assert self.n_side == 6
bx, by = self.angle_joint_bind_pos.to2d_pos()
gap = 10.0
hole_dx = self.motor.dx_anchor
return (
Cq.Sketch()
.rect((bx + gap) * 2, (by + gap) * 2)
.reset()
.rect(bx * 2, by * 2, mode="c", tag="corner")
.vertices(tag="corner")
.circle(BOLT_COMMON.diam_thread/2, mode="s")
.reset()
.push([
(hole_dx, 0),
(-hole_dx, 0),
])
.circle(self.motor.diam_thread/2, mode="s")
)
def motor_mount_plate(self) -> Cq.Workplane:
result = (
Cq.Workplane()
.placeSketch(self.profile_motor_mount_plate())
.extrude(self.auxiliary_thickness)
)
result.tagAbsolute("anchor1", (self.motor.dx_anchor, 0, 0), direction="-Z")
result.tagAbsolute("anchor2", (-self.motor.dx_anchor, 0, 0), direction="-Z")
bp = self.angle_joint_bind_pos
for i in range(self.n_side):
if i in {1, 4}:
continue
angle = i * 360 / self.n_side
x, y = (Cq.Location.rot2d(angle) * bp).to2d_pos()
result.tagAbsolute(f"holeF{i}", (x, y, self.auxiliary_thickness), direction="+Z")
result.tagAbsolute(f"holeB{i}", (x, -y, 0), direction="-Z")
return result
@assembly()
def assembly_motor(self) -> Cq.Assembly:
a = (
Cq.Assembly()
.addS(
self.motor.generate(),
name="motor",
role=Role.MOTOR,
)
.addS(
self.flange_coupler.generate(),
name="flange_coupler",
role=Role.CONNECTION | Role.STRUCTURE,
material=self.material_fastener,
)
.addS(
self.motor_driver_disk(),
name="driver_disk",
role=Role.CONNECTION | Role.STRUCTURE,
material=self.material_auxiliary,
)
.addS(
self.motor_mount_plate(),
name="mount_plate",
role=Role.CONNECTION | Role.STRUCTURE,
material=self.material_auxiliary,
)
.constrain(
"mount_plate?anchor1",
"motor?anchor1",
"Plane",
)
.constrain(
"mount_plate?anchor2",
"motor?anchor2",
"Plane",
)
.constrain(
"flange_coupler?top",
"motor?shaft",
"Plane"
)
)
for i in range(self.flange_coupler.n_hole_flange):
a = a.constrain(
f"flange_coupler?holeB{i}",
f"driver_disk?holeB{i}",
"Plane",
)
return a.solve()
@target(name="stator-coupler")
def stator_coupler(self) -> Cq.Workplane:
"""
@ -358,13 +597,16 @@ class Onbashira(Model):
br * math.sin(-angle2),
).tagPlane(f"holeE{i}", direction="-Z")
return result
@property
def rotor_radius(self) -> float:
return self.bearing_track_radius - self.bearing_gap/2
@target(name="bearing-rotor", kind=TargetKind.DXF)
def profile_bearing_rotor(self) -> Cq.Sketch:
bolt_angle = (180 / self.n_side) * 1.5
n_binds = 1 + self.rotor_bind_extra
return (
Cq.Sketch()
.circle(self.bearing_track_radius - self.bearing_gap/2)
.circle(self.rotor_radius)
.circle(self.rotor_inner_radius, mode="s")
.reset()
.regularPolygon(
@ -420,7 +662,7 @@ class Onbashira(Model):
@target(name="stator-spacer")
def stator_spacer(self) -> Cq.Solid:
outer = Cq.Solid.makeCylinder(
radius=self.rotor_spacer_outer_diam/2,
radius=self.stator_spacer_outer_diam/2,
height=self.bearing_disk_gap,
)
inner = Cq.Solid.makeCylinder(
@ -533,6 +775,10 @@ class Onbashira(Model):
)
)
z = -self.bearing_disk_gap/2
da_bind_stator = 360 / self.n_side
da_bind_rotor = 360 / self.n_side
da_bind_stator_minor = 360 / self.n_side / (1 + self.stator_bind_extra)
da_bind_rotor_minor = 360 / self.n_side / (1 + self.rotor_bind_extra)
for i in range(self.n_side):
loc_barrel = Cq.Location.rot2d((i+1/2) * 360/self.n_side) * \
Cq.Location(self.rotation_radius, 0, -self.barrel_length/2)
@ -543,22 +789,26 @@ class Onbashira(Model):
role=Role.DECORATION,
loc=loc_barrel,
)
loc = Cq.Location.rot2d(i * 360/self.n_side) * Cq.Location(self.rotor_bind_radius, 0, z)
#a = a.addS(
# self.stator_spacer(),
# name=f"spacerRotor{i}",
# material=self.material_spacer,
# role=Role.STRUCTURE,
# loc=loc
#)
loc = Cq.Location.rot2d((i+0.5) * 360/self.n_side) * Cq.Location(self.angle_joint_bind_radius, 0, z)
#a = a.addS(
# self.rotor_spacer(),
# name=f"spacerStator{i}",
# material=self.material_spacer,
# role=Role.STRUCTURE,
# loc=loc
#)
for j in range(1 + self.rotor_bind_extra):
angle = i * da_bind_rotor + (j+0.5) * da_bind_rotor_minor
loc = Cq.Location.rot2d(angle) * Cq.Location(self.rotor_bind_radius, 0, z)
a = a.addS(
self.rotor_spacer(),
name=f"spacer_rotor{i}_{j}",
material=self.material_spacer,
role=Role.STRUCTURE,
loc=loc
)
for j in range(1 + self.stator_bind_extra):
angle = i * da_bind_stator + (j+0.5) * da_bind_stator_minor
loc = Cq.Location.rot2d(angle) * Cq.Location(self.stator_bind_radius, 0, z)
a = a.addS(
self.stator_spacer(),
name=f"spacer_stator{i}_{j}",
material=self.material_spacer,
role=Role.STRUCTURE,
loc=loc
)
for i in range(self.n_bearing_balls):
ball = self.bearing_spindle()
loc = Cq.Location.rot2d(i * 360/self.n_bearing_balls) * Cq.Location(self.bearing_track_radius, 0, 0)
@ -1393,6 +1643,7 @@ class Onbashira(Model):
material=self.material_brace,
role=Role.HANDLE,
)
.add(self.assembly_motor(), name="motor")
.add(self.assembly_machine(), name="machine")
)
# Add handle
@ -1428,6 +1679,38 @@ class Onbashira(Model):
f"machine/stator2?holeB{ir}",
"Plane",
)
if i not in {1, 4}:
a = a.constrain(
f"motor/mount_plate?holeF{i}",
f"ring2/side{(ir+2)%self.n_side}?holeStatorR",
"Plane",
)
name_bolt =f"stator_outer_bolt{i}"
a = a.addS(
BOLT_LONG.generate(),
name=name_bolt,
material=self.material_fastener,
role=Role.CONNECTION,
)
a = a.constrain(
f"{coupler_name}?holeOF",
f"{name_bolt}?root",
"Plane",
)
name_bolt =f"chamber_back{i}boltFPI{i}"
a = a.addS(
BOLT_COMMON.generate(),
name=name_bolt,
material=self.material_fastener,
role=Role.CONNECTION,
)
a = a.constrain(
f"chamber_back?holeF{i}",
f"{name_bolt}?root",
"Plane",
)
for ih in range(len(self.angle_joint_bolt_position)):
a = a.constrain(
f"chamber/side{i}?holeFPI{ih}",
@ -1452,19 +1735,6 @@ class Onbashira(Model):
#)
# Generate bolts for the chamber back
name_bolt =f"chamber_back{i}boltFPI{ih}"
a = a.addS(
BOLT_COMMON.generate(),
name=name_bolt,
material=self.material_fastener,
role=Role.CONNECTION,
)
a = a.constrain(
f"chamber_back?holeF{i}",
f"{name_bolt}?root",
"Plane",
param=0,
)
for (nl, nc, nr) in [
("section1", "ring1", "section2"),
("section2", "ring2", "section3"),