Cosplay/nhf/touhou/houjuu_nue/electronics.py

539 lines
17 KiB
Python
Raw Normal View History

2024-07-19 21:00:10 -07:00
"""
Electronic components
"""
2024-07-22 15:02:26 -07:00
from dataclasses import dataclass, field
from typing import Optional, Tuple
import math
2024-07-19 21:00:10 -07:00
import cadquery as Cq
2024-07-22 15:02:26 -07:00
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.materials import Role, Material
from nhf.parts.box import MountingBox, Hole
2024-07-25 00:09:16 -07:00
from nhf.parts.fibre import tension_fibre
2024-07-19 21:00:10 -07:00
from nhf.parts.item import Item
from nhf.parts.fasteners import FlatHeadBolt, HexNut
2024-07-22 15:02:26 -07:00
from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON
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.54
2024-07-19 21:00:10 -07:00
segment1_width: float = 15.95
segment1_height: float = 11.94
segment2_length: float = 37.37
2024-07-19 21:00:10 -07:00
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, f"Illegal position: {pos}"
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.25
2024-07-21 05:46:18 -07:00
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,
)
)
LINEAR_ACTUATOR_50 = LinearActuator(
mass=40.8,
stroke_length=50,
shaft_diam=9.05,
front_hole_ext=4.32,
back_hole_ext=4.54,
segment1_length=57.35,
segment1_width=15.97,
segment1_height=11.95,
segment2_length=37.69,
segment2_width=19.97,
segment2_height=14.96,
front_length=9.40,
front_width=9.17,
front_height=6.12,
back_length=9.18,
back_width=10.07,
back_height=8.06,
)
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-22 09:49:16 -07:00
segment1_width=15.0,
segment2_width=21.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()
2024-07-25 00:09:16 -07:00
@dataclass(kw_only=True)
class Flexor:
"""
Actuator assembly which flexes, similar to biceps
"""
motion_span: float
arm_radius: Optional[float] = None
pos_smaller: bool = True
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
2024-07-25 00:09:16 -07:00
# Length of line attached to the flexor
line_length: float = 0.0
line_thickness: float = 0.5
# By how much is the line permitted to slack. This reduces the effective stroke length
line_slack: float = 0.0
2024-07-21 05:46:18 -07:00
2024-07-25 00:09:16 -07:00
def __post_init__(self):
assert self.line_slack <= self.line_length < self.actuator.stroke_length
@property
def mount_height(self):
return self.bracket.hole_to_side_ext
2024-07-25 00:09:16 -07:00
@property
def d_open(self):
return self.actuator.conn_length + self.actuator.stroke_length + self.line_length - self.line_slack
@property
def d_closed(self):
return self.actuator.conn_length + self.line_length
def open_pos(self) -> Tuple[float, float, float]:
r, phi, r_ = nhf.geometry.contraction_span_pos_from_radius(
2024-07-25 00:09:16 -07:00
d_open=self.d_open,
d_closed=self.d_closed,
theta=math.radians(self.motion_span),
r=self.arm_radius,
smaller=self.pos_smaller,
)
return r, math.degrees(phi), r_
def target_length_at_angle(
self,
angle: float = 0.0
) -> float:
"""
Length of the actuator at some angle
"""
assert 0 <= angle <= self.motion_span
r, phi, rp = self.open_pos()
th = math.radians(phi - angle)
result = math.sqrt(r * r + rp * rp - 2 * r * rp * math.cos(th))
#result = math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2)
2024-07-25 00:09:16 -07:00
assert self.d_closed -1e-6 <= result <= self.d_open + 1e-6,\
f"Illegal length: {result} not in [{self.d_closed}, {self.d_open}]"
return result
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()`.
"""
2024-07-25 00:09:16 -07:00
draft = max(0, target_length - self.d_closed - self.line_length)
pos = draft / self.actuator.stroke_length
line_l = target_length - draft - self.actuator.conn_length
2024-07-21 05:46:18 -07:00
if tag_prefix:
tag_prefix = tag_prefix + "_"
2024-07-23 22:40:49 -07:00
else:
tag_prefix = ""
2024-07-21 05:46:18 -07:00
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_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")
)
2024-07-25 00:09:16 -07:00
if self.line_length == 0.0:
a.constrain(
f"{name_actuator}/front?conn",
f"{name_bracket_front}?conn_mid",
"Plane", param=0)
else:
(
a
.addS(tension_fibre(
length=line_l,
hole_diam=self.nut.diam_thread,
thickness=self.line_thickness,
), name="fibre", role=Role.CONNECTION)
.constrain(
f"{name_actuator}/front?conn",
"fibre?male",
"Plane"
)
.constrain(
f"{name_bracket_front}?conn_mid",
"fibre?female",
"Plane"
)
)
2024-07-21 05:46:18 -07:00
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
2024-07-22 15:02:26 -07:00
class ElectronicBoard(Model):
name: str = "electronic-board"
nut: HexNut = NUT_COMMON
bolt: FlatHeadBolt = BOLT_COMMON
length: float = 70.0
width: float = 170.0
mount_holes: list[Hole] = field(default_factory=lambda: [
Hole(x=30, y=80),
Hole(x=30, y=-80),
Hole(x=-30, y=80),
Hole(x=-30, y=-80),
])
panel_thickness: float = 25.4 / 16
mount_panel_thickness: float = 25.4 / 4
material: Material = Material.WOOD_BIRCH
2024-07-22 15:02:26 -07:00
@property
def mount_hole_diam(self) -> float:
return self.bolt.diam_thread
def __post_init__(self):
super().__init__(name=self.name)
@submodel(name="panel")
def panel(self) -> MountingBox:
return MountingBox(
holes=self.mount_holes,
hole_diam=self.mount_hole_diam,
length=self.length,
width=self.width,
centred=(True, True),
thickness=self.panel_thickness,
generate_reverse_tags=True,
)
def assembly(self) -> Cq.Assembly:
panel = self.panel()
result = (
Cq.Assembly()
.addS(panel.generate(), name="panel",
role=Role.STRUCTURE, material=self.material)
)
for hole in panel.holes:
spacer_name = f"{hole.tag}_spacer"
bolt_name = f"{hole.tag}_bolt"
(
result
.add(self.nut.assembly(), name=spacer_name)
.add(self.bolt.assembly(), name=bolt_name)
.constrain(
f"{spacer_name}?top",
f"panel?{hole.rev_tag}",
"Plane"
)
.constrain(
f"{bolt_name}?root",
f"panel?{hole.tag}",
"Plane", param=0
)
)
return result.solve()