""" 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.fibre import tension_fibre from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650 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.54 segment1_width: float = 15.95 segment1_height: float = 11.94 segment2_length: float = 37.37 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}" 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.faces(">X").tag("dir") 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.25 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_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( 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=34, segment2_length=34, ) LINEAR_ACTUATOR_10 = LinearActuator( mass=41.3, stroke_length=10, front_hole_ext=4.02, back_hole_ext=4.67, segment1_length=13.29, segment1_width=15.88, segment1_height=12.07, segment2_length=42.52, segment2_width=20.98, segment2_height=14.84, ) 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() # Acrylic hex nut ELECTRONIC_MOUNT_HEXNUT = HexNut( mass=0.8, diam_thread=4, pitch=0.7, thickness=3.57, width=6.81, ) @dataclass(kw_only=True, frozen=True) class Winch: linear_motion_span: float actuator: LinearActuator = LINEAR_ACTUATOR_21 nut: HexNut = LINEAR_ACTUATOR_HEX_NUT bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET @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 actuator: LinearActuator = LINEAR_ACTUATOR_50 nut: HexNut = LINEAR_ACTUATOR_HEX_NUT bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET # 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 def __post_init__(self): assert self.line_slack <= self.line_length, f"Insufficient length: {self.line_slack} >= {self.line_length}" assert self.line_slack < self.actuator.stroke_length @property def mount_height(self): return self.bracket.hole_to_side_ext @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( 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) 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 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()`. """ 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 if tag_prefix: tag_prefix = tag_prefix + "_" else: 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_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 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" ) ) 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=25, y=75), Hole(x=25, y=-75), Hole(x=-25, y=75), Hole(x=-25, y=-75), ]) 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) 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.ELECTRONIC | Role.STRUCTURE, material=self.material) ) for hole in self.mount_holes: bolt_name = f"{hole.tag}_bolt" ( result .add(self.bolt.assembly(), name=bolt_name) .constrain( f"{bolt_name}?root", f"panel?{hole.tag}", "Plane", param=0 ) ) return result.solve() @dataclass class ElectronicBoardBattery(ElectronicBoard): name: str = "electronic-board-battery" battery_box: BatteryBox18650 = BATTERY_BOX @submodel(name="panel") def panel_out(self) -> MountingBox: return self.panel() @dataclass class ElectronicBoardControl(ElectronicBoard): name: str = "electronic-board-control" controller_datum: Cq.Location = Cq.Location.from2d(-25,10, -90) controller: ArduinoUnoR3 = ArduinoUnoR3() def panel(self) -> MountingBox: box = super().panel() def transform(i, x, y): pos = self.controller_datum * Cq.Location.from2d(x, self.controller.width - y) x, y = pos.to2d_pos() return Hole( x=x, y=y, diam=self.controller.hole_diam, tag=f"controller_conn{i}", ) box.holes = box.holes.copy() + [ transform(i, x, y) for i, (x, y) in enumerate(self.controller.holes) ] return box @submodel(name="panel") def panel_out(self) -> MountingBox: return self.panel() def assembly(self) -> Cq.Assembly: result = super().assembly() result.add(self.controller.assembly(), name="controller") for i in range(len(self.controller.holes)): result.constrain(f"controller?conn{i}", f"panel?controller_conn{i}", "Plane") return result.solve() @dataclass(frozen=True) class LightStrip: width: float = 10.0 height: float = 4.5