diff --git a/nhf/parts/electronics.py b/nhf/parts/electronics.py index b20e8e8..0a145b3 100644 --- a/nhf/parts/electronics.py +++ b/nhf/parts/electronics.py @@ -21,7 +21,7 @@ class ArduinoUnoR3(Item): # This is labeled in mirrored coordinates from top down (i.e. unmirrored from bottom up) holes: list[Tuple[float, float]] = field(default_factory=lambda: [ (15.24, 2.54), - (13.74, 50.80), # x coordinate not labeled on schematic + (15.24 - 1.270, 50.80), # x coordinate not labeled on schematic (66.04, 17.78), (66.04, 45.72), ]) @@ -70,3 +70,66 @@ class ArduinoUnoR3(Item): for i, (x, y) in enumerate(self.holes): plane.moveTo(x, self.width - y).tagPlane(f"conn{i}", direction='-Z') 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, + ) + ) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 35e74d3..b3d6724 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -11,7 +11,7 @@ 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 +from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650 from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON import nhf.utils @@ -210,68 +210,6 @@ class MountingBracket(Item): 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=40.8, @@ -345,6 +283,15 @@ ELECTRONIC_MOUNT_HEXNUT = HexNut( 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: """ @@ -529,17 +476,10 @@ class ElectronicBoard(Model): role=Role.ELECTRONIC | Role.STRUCTURE, material=self.material) ) for hole in self.mount_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}", @@ -551,6 +491,7 @@ class ElectronicBoard(Model): @dataclass class ElectronicBoardBattery(ElectronicBoard): name: str = "electronic-board-battery" + battery_box: BatteryBox18650 = BATTERY_BOX @submodel(name="panel") def panel_out(self) -> MountingBox: diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 40f8e02..9804a21 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -10,7 +10,7 @@ from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob, Washer from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes from nhf.touhou.houjuu_nue.electronics import ( - Flexor, LinearActuator, LINEAR_ACTUATOR_21, + Winch, Flexor, LinearActuator, LINEAR_ACTUATOR_21, ) import nhf.geometry import nhf.utils @@ -390,7 +390,7 @@ class ShoulderJoint(Model): parent_arm_width: float = 25.0 parent_arm_height: float = 12.0 # remove a bit of material from the base so it does not interfere with gluing - parent_arm_base_shift: float = 2.0 + parent_arm_base_shift: float = 1.0 # Generates a child guard which covers up the internals. The lip length is # relative to the +X surface of the guard. @@ -423,6 +423,7 @@ class ShoulderJoint(Model): spool_groove_radius: float = 10.0 flip: bool = False + winch: Optional[Winch] = None # Initialized later actuator: LinearActuator = LINEAR_ACTUATOR_21 def __post_init__(self): @@ -431,6 +432,10 @@ class ShoulderJoint(Model): assert self.spool_groove_radius < self.spool_inner_radius < self.spool_outer_radius assert self.child_lip_height < self.height assert self.draft_length <= self.actuator.stroke_length + self.winch = Winch( + actuator=self.actuator, + linear_motion_span=self.draft_length, + ) @property def spool_outer_radius(self): @@ -457,6 +462,10 @@ class ShoulderJoint(Model): """ return 0 + @property + def parent_lip_gap(self): + return self.height - self.parent_lip_length * 2 + def parent_lip_loc(self, left: bool=True) -> Cq.Location: """ 2d location of the arm surface on the parent side, relative to axle diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index c72ebdf..81831eb 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -21,6 +21,7 @@ from nhf.touhou.houjuu_nue.electronics import ( ElectronicBoard, ElectronicBoardBattery, LightStrip, + ElectronicBoard, ELECTRONIC_MOUNT_HEXNUT, ) import nhf.utils @@ -287,7 +288,7 @@ class WingProfile(Model): plane.moveTo(t, self.root_height + t*2).tagPlane("top") return result - @submodel(name="spacer-s0-shoulder") + @submodel(name="spacer-s0-shoulder-inner") def spacer_s0_shoulder(self, left: bool=True) -> MountingBox: """ Shoulder side serves double purpose for mounting shoulder joint and @@ -302,6 +303,21 @@ class WingProfile(Model): Hole(x=-x, y=sign * y, tag=f"conn_bot{i}"), ] ] + def post(sketch: Cq.Sketch) -> Cq.Sketch: + """ + Carve out the middle if this is closer to the front + """ + if left: + return sketch + return ( + sketch + .push([(0,0)]) + .rect( + w=self.shoulder_joint.parent_lip_gap, + h=self.shoulder_joint.parent_lip_width, + mode='s' + ) + ) return MountingBox( length=self.shoulder_joint.height, width=self.shoulder_joint.parent_lip_width, @@ -311,7 +327,12 @@ class WingProfile(Model): centred=(True, True), flip_y=self.flip, centre_bot_top_tags=True, + profile_callback=post, ) + @submodel(name="spacer-s0-shoulder-outer") + def spacer_s0_shoulder_outer(self) -> MountingBox: + return self.spacer_s0_shoulder_inner(left=False) + @submodel(name="spacer-s0-base") def spacer_s0_base(self) -> MountingBox: """ @@ -341,11 +362,15 @@ class WingProfile(Model): @submodel(name="spacer-s0-electronic") def spacer_s0_electronic_mount(self) -> MountingBox: """ - This one has circular holes for the screws + This one has hexagonal holes """ + face = ELECTRONIC_MOUNT_HEXNUT.cutting_face() + holes = [ + Hole(x=h.x, y=h.y, face=face, tag=h.tag) + for h in self.electronic_board.mount_holes + ] return MountingBox( - holes=self.electronic_board.mount_holes, - hole_diam=self.electronic_board.mount_hole_diam, + holes=holes, length=self.root_height, width=self.electronic_board.width, centred=(True, True), @@ -356,13 +381,8 @@ class WingProfile(Model): @submodel(name="spacer-s0-electronic2") def spacer_s0_electronic_mount2(self) -> MountingBox: """ - This one has hexagonal holes + This one has circular holes """ - face = ELECTRONIC_MOUNT_HEXNUT.cutting_face() - holes = [ - Hole(x=h.x, y=h.y, face=face, tag=h.tag) - for h in self.electronic_board.mount_holes - ] def post(sketch: Cq.Sketch) -> Cq.Sketch: return ( sketch @@ -370,7 +390,7 @@ class WingProfile(Model): .rect(70, 130, mode='s') ) return MountingBox( - holes=holes, + holes=self.electronic_board.mount_holes, hole_diam=self.electronic_board.mount_hole_diam, length=self.root_height, width=self.electronic_board.width, @@ -408,7 +428,7 @@ class WingProfile(Model): ("shoulder_right", self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), ("shoulder_act", - self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(100, -20)), + self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(120, -40)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("electronic_mount", Cq.Location.from2d(-35, 65, 60)), ] @@ -474,17 +494,26 @@ class WingProfile(Model): for hole in self.electronic_board.mount_holes: result.constrain( f"electronic_mount?{hole.tag}", - f"electronic_mount2?{hole.tag}_rev", + f"electronic_mount2?{hole.rev_tag}", "Plane") if not ignore_electronics: result.add(self.electronic_board.assembly(), name="electronic_board") for hole in self.electronic_board.mount_holes: assert hole.tag + nut_name = f"{hole.tag}_nut" + ( + result + .add(self.electronic_board.nut.assembly(), name=nut_name) + .constrain( + f"{nut_name}?top", + f"electronic_mount?{hole.tag}", + "Plane", param=0 + ) + ) result.constrain( f"electronic_mount2?{hole.tag}", - f'electronic_board/{hole.tag}_spacer?top', + f'electronic_board/panel?{hole.rev_tag}', "Plane", - param=0 ) return result.solve() @@ -1073,6 +1102,27 @@ class WingProfile(Model): result.add(self.assembly_s0( ignore_electronics=ignore_electronics ), name="s0") + if not ignore_electronics: + tag_act = "shoulder_act" + tag_bolt = "shoulder_act_bolt" + tag_nut = "shoulder_act_nut" + tag_bracket = "shoulder_act_bracket" + winch = self.shoulder_joint.winch + ( + result + .add(winch.actuator.assembly(pos=0), name=tag_act) + .add(winch.bracket.assembly(), name=tag_bracket) + .add(winch.bolt.assembly(), name=tag_bolt) + .add(winch.nut.assembly(), name=tag_nut) + .constrain(f"{tag_bolt}?root", f"{tag_bracket}?conn_top", + "Plane", param=0) + .constrain(f"{tag_nut}?bot", f"{tag_bracket}?conn_bot", + "Plane") + .constrain(f"{tag_act}/back?conn", f"{tag_bracket}?conn_mid", + "Plane", param=0) + .constrain("s0/shoulder_act?conn0", f"{tag_bracket}?conn_side", + "Plane") + ) if "root" in parts: result.addS(self.root_joint.assembly( offset=root_offset,