""" Electronic components """ from dataclasses import dataclass from typing import Optional, Tuple import math 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: 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, ) 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 @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) 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: hole_diam: float = 4.0