Cosplay/nhf/touhou/houjuu_nue/electronics.py

421 lines
13 KiB
Python
Raw Normal View History

2024-07-19 21:00:10 -07:00
"""
Electronic components
"""
from dataclasses import dataclass
from typing import Optional, Tuple
import math
2024-07-19 21:00:10 -07:00
import cadquery as Cq
from nhf.materials import Role
from nhf.parts.item import Item
from nhf.parts.fasteners import FlatHeadBolt, HexNut
2024-07-21 05:46:18 -07:00
import nhf.utils
2024-07-19 21:00:10 -07:00
@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:
assert -1e-6 <= pos <= 1 + 1e-6
2024-07-19 21:00:10 -07:00
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',
)
)
2024-07-21 05:46:18 -07:00
front.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
2024-07-19 21:00:10 -07:00
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',
)
)
2024-07-21 05:46:18 -07:00
back.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
2024-07-19 21:00:10 -07:00
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
2024-07-21 05:46:18 -07:00
@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
2024-07-21 22:34:19 -07:00
@dataclass(frozen=True)
class BatteryBox18650(Item):
"""
A number of 18650 batteries in series
"""
mass: float = 17.4 + 68.80 * 3
length: float = 75.70
width_base: float = 61.46 - 18.48 - 20.18 * 2
battery_dist: float = 20.18
height: float = 19.66
# space from bottom to battery begin
thickness: float = 1.66
battery_diam: float = 18.48
battery_height: float = 68.80
n_batteries: int = 3
def __post_init__(self):
assert 2 * self.thickness < min(self.length, self.height)
@property
def name(self) -> str:
return f"BatteryBox 18650*{self.n_batteries}"
@property
def role(self) -> Role:
return Role.ELECTRONIC
def generate(self) -> Cq.Workplane:
width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam
return (
Cq.Workplane('XY')
.box(
length=self.length,
width=width,
height=self.height,
centered=(True, True, False),
)
.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.thickness)))
.box(
length=self.length - self.thickness*2,
width=width - self.thickness*2,
height=self.height - self.thickness,
centered=(True, True, False),
combine='cut',
)
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2)))
.rarray(
xSpacing=1,
ySpacing=self.battery_dist,
xCount=1,
yCount=self.n_batteries,
center=True,
)
.cylinder(
radius=self.battery_diam/2,
height=self.battery_height,
direct=(1, 0, 0),
centered=(True, True, False),
combine=True,
)
)
2024-07-19 21:00:10 -07:00
LINEAR_ACTUATOR_50 = LinearActuator(
mass=34.0,
stroke_length=50,
# FIXME: Measure
front_hole_ext=6,
back_hole_ext=6,
segment1_length=50,
segment2_length=50,
)
LINEAR_ACTUATOR_30 = LinearActuator(
2024-07-19 21:00:10 -07:00
mass=34.0,
stroke_length=30,
)
LINEAR_ACTUATOR_21 = LinearActuator(
# FIXME: Measure
mass=0.0,
stroke_length=21,
front_hole_ext=4,
back_hole_ext=4,
segment1_length=75/2,
segment2_length=75/2,
)
LINEAR_ACTUATOR_10 = LinearActuator(
# FIXME: Measure
mass=0.0,
stroke_length=10,
front_hole_ext=4.5/2,
back_hole_ext=4.5/2,
segment1_length=30.0,
segment2_length=30.0,
)
2024-07-19 21:00:10 -07:00
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,
2024-07-21 05:46:18 -07:00
diam_head=6.68,
2024-07-19 21:00:10 -07:00
height_head=2.98,
diam_thread=4.0,
height_thread=15.83,
)
2024-07-21 05:46:18 -07:00
LINEAR_ACTUATOR_BRACKET = MountingBracket()
2024-07-21 22:34:19 -07:00
BATTERY_BOX = BatteryBox18650()
@dataclass
class Flexor:
"""
Actuator assembly which flexes, similar to biceps
"""
motion_span: float
2024-07-21 05:46:18 -07:00
actuator: LinearActuator = LINEAR_ACTUATOR_50
2024-07-21 05:46:18 -07:00
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
# FIXME: Add a compression spring so the serviceable distances are not as fixed
@property
def mount_height(self):
return self.bracket.hole_to_side_ext
@property
def min_serviceable_distance(self):
return self.bracket.hole_to_side_ext * 2 + self.actuator.conn_length
@property
def max_serviceable_distance(self):
return self.min_serviceable_distance + self.actuator.stroke_length
def open_pos(self) -> Tuple[float, float]:
r, phi = nhf.geometry.contraction_actuator_span_pos(
d_open=self.actuator.conn_length + self.actuator.stroke_length,
d_closed=self.actuator.conn_length,
theta=math.radians(self.motion_span)
)
return r, math.degrees(phi)
def target_length_at_angle(
self,
angle: float = 0.0
) -> float:
# law of cosines
r, phi = self.open_pos()
th = math.radians(phi - angle)
d2 = 2 * r * r * (1 - math.cos(th))
return math.sqrt(d2)
2024-07-21 05:46:18 -07:00
def add_to(
self,
a: Cq.Assembly,
target_length: float,
2024-07-21 05:46:18 -07:00
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()`.
"""
pos = (target_length - self.actuator.conn_length) / self.actuator.stroke_length
2024-07-21 05:46:18 -07:00
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(pos=pos), name=name_actuator)
2024-07-21 05:46:18 -07:00
.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)
@dataclass
class ElectronicBoard:
hole_diam: float = 4.0