284 lines
9.1 KiB
Python
284 lines
9.1 KiB
Python
"""
|
|
Electronic components
|
|
"""
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
import cadquery as Cq
|
|
from nhf.materials import Role
|
|
from nhf.parts.item import Item
|
|
from nhf.parts.fasteners import FlatHeadBolt, HexNut
|
|
import nhf.utils
|
|
|
|
@dataclass(frozen=True)
|
|
class LinearActuator(Item):
|
|
stroke_length: float
|
|
shaft_diam: float = 9.04
|
|
|
|
front_hole_ext: float = 4.41
|
|
front_hole_diam: float = 4.41
|
|
front_length: float = 9.55
|
|
front_width: float = 9.24
|
|
front_height: float = 5.98
|
|
|
|
segment1_length: float = 37.55
|
|
segment1_width: float = 15.95
|
|
segment1_height: float = 11.94
|
|
|
|
segment2_length: float = 37.47
|
|
segment2_width: float = 20.03
|
|
segment2_height: float = 15.03
|
|
|
|
back_hole_ext: float = 4.58
|
|
back_hole_diam: float = 4.18
|
|
back_length: float = 9.27
|
|
back_width: float = 10.16
|
|
back_height: float = 8.12
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return f"LinearActuator {self.stroke_length}mm"
|
|
|
|
@property
|
|
def role(self) -> Role:
|
|
return Role.MOTION
|
|
|
|
@property
|
|
def conn_length(self):
|
|
return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext
|
|
|
|
def generate(self, pos: float=0) -> Cq.Assembly:
|
|
stroke_x = pos * self.stroke_length
|
|
front = (
|
|
Cq.Workplane('XZ')
|
|
.cylinder(
|
|
radius=self.front_width / 2,
|
|
height=self.front_height,
|
|
centered=True,
|
|
)
|
|
.box(
|
|
length=self.front_hole_ext,
|
|
width=self.front_width,
|
|
height=self.front_height,
|
|
combine=True,
|
|
centered=(False, True, True)
|
|
)
|
|
.copyWorkplane(Cq.Workplane('XZ'))
|
|
.cylinder(
|
|
radius=self.front_hole_diam / 2,
|
|
height=self.front_height,
|
|
centered=True,
|
|
combine='cut',
|
|
)
|
|
)
|
|
front.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
|
|
if stroke_x > 0:
|
|
shaft = (
|
|
Cq.Workplane('YZ')
|
|
.cylinder(
|
|
radius=self.shaft_diam / 2,
|
|
height=stroke_x,
|
|
centered=(True, True, False)
|
|
)
|
|
)
|
|
else:
|
|
shaft = None
|
|
segment1 = (
|
|
Cq.Workplane()
|
|
.box(
|
|
length=self.segment1_length,
|
|
height=self.segment1_width,
|
|
width=self.segment1_height,
|
|
centered=(False, True, True),
|
|
)
|
|
)
|
|
segment2 = (
|
|
Cq.Workplane()
|
|
.box(
|
|
length=self.segment2_length,
|
|
height=self.segment2_width,
|
|
width=self.segment2_height,
|
|
centered=(False, True, True),
|
|
)
|
|
)
|
|
back = (
|
|
Cq.Workplane('XZ')
|
|
.cylinder(
|
|
radius=self.back_width / 2,
|
|
height=self.back_height,
|
|
centered=True,
|
|
)
|
|
.box(
|
|
length=self.back_hole_ext,
|
|
width=self.back_width,
|
|
height=self.back_height,
|
|
combine=True,
|
|
centered=(False, True, True)
|
|
)
|
|
.copyWorkplane(Cq.Workplane('XZ'))
|
|
.cylinder(
|
|
radius=self.back_hole_diam / 2,
|
|
height=self.back_height,
|
|
centered=True,
|
|
combine='cut',
|
|
)
|
|
)
|
|
back.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
|
|
result = (
|
|
Cq.Assembly()
|
|
.add(front, name="front",
|
|
loc=Cq.Location((-self.front_hole_ext, 0, 0)))
|
|
.add(segment1, name="segment1",
|
|
loc=Cq.Location((stroke_x, 0, 0)))
|
|
.add(segment2, name="segment2",
|
|
loc=Cq.Location((stroke_x + self.segment1_length, 0, 0)))
|
|
.add(back, name="back",
|
|
loc=Cq.Location((stroke_x + self.segment1_length + self.segment2_length + self.back_hole_ext, 0, 0), (0, 1, 0), 180))
|
|
)
|
|
if shaft:
|
|
result.add(shaft, name="shaft")
|
|
return result
|
|
|
|
@dataclass(frozen=True)
|
|
class MountingBracket(Item):
|
|
"""
|
|
Mounting bracket for a linear actuator
|
|
"""
|
|
mass: float = 1.6
|
|
hole_diam: float = 4.0
|
|
width: float = 8.0
|
|
height: float = 12.20
|
|
thickness: float = 0.98
|
|
length: float = 13.00
|
|
hole_to_side_ext: float = 8.10
|
|
|
|
def __post_init__(self):
|
|
assert self.hole_to_side_ext - self.hole_diam / 2 > 0
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return f"MountingBracket M{int(self.hole_diam)}"
|
|
|
|
@property
|
|
def role(self) -> Role:
|
|
return Role.MOTION
|
|
|
|
def generate(self) -> Cq.Workplane:
|
|
result = (
|
|
Cq.Workplane('XY')
|
|
.box(
|
|
length=self.hole_to_side_ext,
|
|
width=self.width,
|
|
height=self.height,
|
|
centered=(False, True, True)
|
|
)
|
|
.copyWorkplane(Cq.Workplane('XY'))
|
|
.cylinder(
|
|
height=self.height,
|
|
radius=self.width / 2,
|
|
combine=True,
|
|
)
|
|
.copyWorkplane(Cq.Workplane('XY'))
|
|
.box(
|
|
length=2 * (self.hole_to_side_ext - self.thickness),
|
|
width=self.width,
|
|
height=self.height - self.thickness * 2,
|
|
combine='cut',
|
|
)
|
|
.copyWorkplane(Cq.Workplane('XY'))
|
|
.cylinder(
|
|
height=self.height,
|
|
radius=self.hole_diam / 2,
|
|
combine='cut'
|
|
)
|
|
.copyWorkplane(Cq.Workplane('YZ'))
|
|
.cylinder(
|
|
height=self.hole_to_side_ext * 2,
|
|
radius=self.hole_diam / 2,
|
|
combine='cut'
|
|
)
|
|
)
|
|
result.copyWorkplane(Cq.Workplane('YZ', origin=(self.hole_to_side_ext, 0, 0))).tagPlane("conn_side")
|
|
result.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.height/2))).tagPlane("conn_top")
|
|
result.copyWorkplane(Cq.Workplane('YX', origin=(0, 0, -self.height/2))).tagPlane("conn_bot")
|
|
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid")
|
|
return result
|
|
|
|
|
|
LINEAR_ACTUATOR_SHOULDER = LinearActuator(
|
|
mass=34.0,
|
|
stroke_length=30,
|
|
)
|
|
LINEAR_ACTUATOR_HEX_NUT = HexNut(
|
|
mass=0.8,
|
|
diam_thread=4,
|
|
pitch=0.7,
|
|
thickness=4.16,
|
|
width=6.79,
|
|
)
|
|
LINEAR_ACTUATOR_BOLT = FlatHeadBolt(
|
|
mass=1.7,
|
|
diam_head=6.68,
|
|
height_head=2.98,
|
|
diam_thread=4.0,
|
|
height_thread=15.83,
|
|
)
|
|
LINEAR_ACTUATOR_BRACKET = MountingBracket()
|
|
|
|
@dataclass(frozen=True)
|
|
class LinearActuatorAssembly:
|
|
|
|
# FIXME: Measure
|
|
actuator: LinearActuator = LINEAR_ACTUATOR_SHOULDER
|
|
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
|
|
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
|
|
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
|
|
|
|
def add_to(
|
|
self,
|
|
a: Cq.Assembly,
|
|
tag_prefix: Optional[str] = None,
|
|
tag_hole_front: Optional[str] = None,
|
|
tag_hole_back: Optional[str] = None,
|
|
tag_dir: Optional[str] = None):
|
|
"""
|
|
Adds the necessary mechanical components to this assembly. Does not
|
|
invoke `a.solve()`.
|
|
"""
|
|
if tag_prefix:
|
|
tag_prefix = tag_prefix + "_"
|
|
name_actuator = f"{tag_prefix}actuator"
|
|
name_bracket_front = f"{tag_prefix}bracket_front"
|
|
name_bracket_back = f"{tag_prefix}bracket_back"
|
|
name_bolt_front = f"{tag_prefix}front_bolt"
|
|
name_bolt_back = f"{tag_prefix}back_bolt"
|
|
name_nut_front = f"{tag_prefix}front_nut"
|
|
name_nut_back = f"{tag_prefix}back_nut"
|
|
(
|
|
a
|
|
.add(self.actuator.assembly(), name=name_actuator)
|
|
.add(self.bracket.assembly(), name=name_bracket_front)
|
|
.add(self.bolt.assembly(), name=name_bolt_front)
|
|
.add(self.nut.assembly(), name=name_nut_front)
|
|
.constrain(f"{name_actuator}/front?conn", f"{name_bracket_front}?conn_mid",
|
|
"Plane", param=0)
|
|
.constrain(f"{name_bolt_front}?root", f"{name_bracket_front}?conn_top",
|
|
"Plane", param=0)
|
|
.constrain(f"{name_nut_front}?bot", f"{name_bracket_front}?conn_bot",
|
|
"Plane")
|
|
.add(self.bracket.assembly(), name=name_bracket_back)
|
|
.add(self.bolt.assembly(), name=name_bolt_back)
|
|
.add(self.nut.assembly(), name=name_nut_back)
|
|
.constrain(f"{name_actuator}/back?conn", f"{name_bracket_back}?conn_mid",
|
|
"Plane", param=0)
|
|
.constrain(f"{name_bolt_back}?root", f"{name_bracket_back}?conn_top",
|
|
"Plane", param=0)
|
|
.constrain(f"{name_nut_back}?bot", f"{name_bracket_back}?conn_bot",
|
|
"Plane")
|
|
)
|
|
if tag_hole_front:
|
|
a.constrain(tag_hole_front, f"{name_bracket_front}?conn_side", "Plane")
|
|
if tag_hole_back:
|
|
a.constrain(tag_hole_back, f"{name_bracket_back}?conn_side", "Plane")
|
|
if tag_dir:
|
|
a.constrain(tag_dir, f"{name_bracket_front}?conn_mid", "Axis", param=0)
|