""" Electronic components """ from dataclasses import dataclass, field from typing import Optional, Tuple import math import cadquery as Cq from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.materials import Role, Material from nhf.parts.box import MountingBox, Hole from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON 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: assert -1e-6 <= pos <= 1 + 1e-6 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 @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=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( 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, segment1_width=15.0, segment2_width=21.0, ) 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() BATTERY_BOX = BatteryBox18650() @dataclass class Flexor: """ Actuator assembly which flexes, similar to biceps """ motion_span: float actuator: LinearActuator = LINEAR_ACTUATOR_50 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 def open_pos(self) -> Tuple[float, float, float]: r, phi, r_ = nhf.geometry.min_tangent_contraction_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), r_ #r, phi = nhf.geometry.min_radius_contraction_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), r r, phi, r_ = nhf.geometry.min_tangent_contraction_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), r_ def target_length_at_angle( self, angle: float = 0.0 ) -> float: """ Length of the actuator at some angle """ r, phi, rp = self.open_pos() th = math.radians(phi - angle) return math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2) # Law of cosines d2 = r * r + rp * rp - 2 * r * rp * math.cos(th) return math.sqrt(d2) def add_to( self, a: Cq.Assembly, target_length: float, 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 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) .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(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 @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()