From ddbf904f5842bc6955e9548237e74f2f9d13af15 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 22 Jul 2024 15:02:26 -0700 Subject: [PATCH] feat: Electronic board assembly --- nhf/parts/box.py | 25 ++++++---- nhf/touhou/houjuu_nue/common.py | 18 +++++++ nhf/touhou/houjuu_nue/electronics.py | 72 +++++++++++++++++++++++++--- nhf/touhou/houjuu_nue/wing.py | 52 +++++++++++++------- 4 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 nhf/touhou/houjuu_nue/common.py diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 3498f6d..36828fe 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -34,6 +34,11 @@ class Hole: diam: Optional[float] = None tag: Optional[str] = None + @property + def rev_tag(self) -> str: + assert self.tag is not None + return self.tag + "_rev" + @dataclass class MountingBox(Model): """ @@ -56,6 +61,11 @@ class MountingBox(Model): # Determines the position of side tags flip_y: bool = False + def __post_init__(self): + for i, hole in enumerate(self.holes): + if hole.tag is None: + hole.tag = f"conn{i}" + @target(kind=TargetKind.DXF) def profile(self) -> Cq.Sketch: bx, by = 0, 0 @@ -84,12 +94,11 @@ class MountingBox(Model): ) plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness) reverse_plane = result.copyWorkplane(Cq.Workplane('XY')) - for i, hole in enumerate(self.holes): - tag = hole.tag if hole.tag else f"conn{i}" - plane.moveTo(hole.x, hole.y).tagPlane(tag) + for hole in self.holes: + assert hole.tag + plane.moveTo(hole.x, hole.y).tagPlane(hole.tag) if self.generate_reverse_tags: - rev_tag = hole.tag + "_rev" if hole.tag else f"conn{i}_rev" - reverse_plane.moveTo(hole.x, hole.y).tagPlane(rev_tag, '-Z') + reverse_plane.moveTo(hole.x, hole.y).tagPlane(hole.rev_tag, '-Z') if self.generate_side_tags: result.faces("Z").val().Center()).tagPlane("left") @@ -106,10 +115,10 @@ class MountingBox(Model): Cq.Assembly() .add(self.generate(), name="box") ) - for i in range(len(self.holes)): - result.markPlane(f"box?conn{i}") + for hole in self.holes: + result.markPlane(f"box?{hole.tag}") if self.generate_reverse_tags: - result.markPlane(f"box?conn{i}_rev") + result.markPlane(f"box?{hole.rev_tag}") if self.generate_side_tags: ( result diff --git a/nhf/touhou/houjuu_nue/common.py b/nhf/touhou/houjuu_nue/common.py new file mode 100644 index 0000000..5d3d0a0 --- /dev/null +++ b/nhf/touhou/houjuu_nue/common.py @@ -0,0 +1,18 @@ +from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob + +NUT_COMMON = HexNut( + # FIXME: measure + mass=0.0, + diam_thread=4.0, + pitch=0.7, + thickness=3.2, + width=7.0, +) +BOLT_COMMON = FlatHeadBolt( + # FIXME: measure + mass=0.0, + diam_head=8.0, + height_head=2.0, + diam_thread=4.0, + height_thread=20.0, +) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 02468ec..ae114e1 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -1,13 +1,16 @@ """ Electronic components """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional, Tuple import math import cadquery as Cq -from nhf.materials import Role +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) @@ -268,8 +271,6 @@ class BatteryBox18650(Item): ) - - LINEAR_ACTUATOR_50 = LinearActuator( mass=34.0, stroke_length=50, @@ -427,6 +428,65 @@ class Flexor: @dataclass -class ElectronicBoard: +class ElectronicBoard(Model): - hole_diam: float = 4.0 + 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() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index ce3fe7b..bc1c690 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -90,7 +90,7 @@ class WingProfile(Model): elbow_rotate: float = -5.0 wrist_rotate: float = -30.0 # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) - elbow_axle_pos: float = 0.3 + elbow_axle_pos: float = 0.5 wrist_axle_pos: float = 0.0 # False for the right side, True for the left side @@ -99,6 +99,8 @@ class WingProfile(Model): def __post_init__(self): super().__init__(name=self.name) + assert self.electronic_board.length == self.shoulder_height + self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) if self.flip: @@ -114,6 +116,7 @@ class WingProfile(Model): self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, self.shoulder_angle_bias) self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2 + assert self.spacer_thickness == self.root_joint.child_mount_thickness @property @@ -278,21 +281,16 @@ class WingProfile(Model): flip_y=self.flip, ) @submodel(name="spacer-s0-electronic") - def spacer_s0_electronic(self) -> MountingBox: - holes = [ - Hole(x=30, y=80), - Hole(x=30, y=-80), - Hole(x=-30, y=80), - Hole(x=-30, y=-80), - ] + def spacer_s0_electronic_mount(self) -> MountingBox: return MountingBox( - holes=holes, - hole_diam=self.electronic_board.hole_diam, + holes=self.electronic_board.mount_holes, + hole_diam=self.electronic_board.mount_hole_diam, length=self.root_height, - width=170, + width=self.electronic_board.width, centred=(True, True), thickness=self.spacer_thickness, - flip_y=self.flip + flip_y=self.flip, + generate_reverse_tags=True, ) def surface_s0(self, top: bool = False) -> Cq.Workplane: @@ -310,7 +308,7 @@ class WingProfile(Model): self.shoulder_joint.parent_arm_loc() * loc_tip), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), - ("electronic", Cq.Location.from2d(-55, 75, 64)), + ("electronic_mount", Cq.Location.from2d(-55, 75, 64)), ] result = extrude_with_markers( self.profile_s0(top=top), @@ -323,7 +321,7 @@ class WingProfile(Model): return result @assembly() - def assembly_s0(self) -> Cq.Assembly: + def assembly_s0(self, ignore_detail: bool=False) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s0(top=True), name="bot", @@ -343,7 +341,7 @@ class WingProfile(Model): for o, tag in [ (self.spacer_s0_shoulder().generate(), "shoulder"), (self.spacer_s0_base().generate(), "base"), - (self.spacer_s0_electronic().generate(), "electronic"), + (self.spacer_s0_electronic_mount().generate(), "electronic_mount"), ]: top_tag, bot_tag = "top", "bot" if self.flip: @@ -357,6 +355,27 @@ class WingProfile(Model): .constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane") .constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis") ) + if not ignore_detail: + result.add(self.electronic_board.assembly(), name="electronic_board") + for hole in self.electronic_board.mount_holes: + assert hole.tag + nut_name = f"electronic_board_{hole.tag}_nut" + ( + result + .addS( + self.electronic_board.nut.assembly(), + name=nut_name) + .constrain( + f"electronic_mount?{hole.rev_tag}", + f'{nut_name}?top', + "Plane" + ) + .constrain( + f"electronic_mount?{hole.tag}", + f'electronic_board/{hole.tag}_spacer?bot', + "Plane" + ) + ) return result.solve() @@ -804,6 +823,7 @@ class WingProfile(Model): elbow_wrist_deflection: float = 0.0, root_offset: int = 5, fastener_pos: float = 0.0, + ignore_detail: bool = False, ) -> Cq.Assembly(): if parts is None: parts = [ @@ -824,7 +844,7 @@ class WingProfile(Model): tag_top, tag_bot = tag_bot, tag_top if "s0" in parts: - result.add(self.assembly_s0(), name="s0") + result.add(self.assembly_s0(ignore_detail=ignore_detail), name="s0") if "root" in parts: result.addS(self.root_joint.assembly( offset=root_offset,