913 lines
38 KiB
Python
913 lines
38 KiB
Python
"""Compact quasi-direct-drive (QDD) actuator concept for a humanoid robot joint.
|
|
|
|
Functional concept layout, manufacturable industrial design:
|
|
- high-torque-density outrunner BLDC motor (24-slot stator, 28-magnet rotor)
|
|
- low-ratio single-stage planetary reduction (sun 24T / planets 30T / ring 84T,
|
|
module 1.0, fixed ring -> 4.5:1)
|
|
- integrated rear magnetic encoder (rotor magnet ring + encoder PCB)
|
|
- output torque sensing (4-spoke strain-gauge flexure between carrier and flange)
|
|
- cross-roller output bearing (alternating +/-45 deg rollers in V-groove races)
|
|
- hollow shaft cable routing (Ø14/Ø11 static tube through the full stack)
|
|
- compact annular motor-driver PCB (FETs, bus capacitors, MCU)
|
|
- aluminum housing with axial heat fins, bolted front retainer ring
|
|
- sealed rear circular connectors (power + signal, o-ring glands)
|
|
|
|
Coordinate convention:
|
|
- Units mm. Origin on the actuator centerline (Z = rotation axis).
|
|
- +Z is the output direction; the gear stage and output flange are at +Z,
|
|
the driver electronics and sealed connectors are at -Z.
|
|
|
|
The same parameterized builder produces the assembled model (explode=0) and an
|
|
exploded technical layout (explode=1) used by qdd_actuator_exploded.py. The
|
|
exploded translations are documented per-part static Locations along +/-Z.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from build123d import * # noqa: F401,F403
|
|
from cadpy.assembly import AssemblyHelper
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gear stage parameters (module-1.0 concept teeth, trapezoidal with backlash)
|
|
# ---------------------------------------------------------------------------
|
|
GEAR_MODULE = 1.0
|
|
SUN_TEETH = 24
|
|
PLANET_TEETH = 30
|
|
RING_TEETH = 84 # = SUN_TEETH + 2 * PLANET_TEETH
|
|
PLANET_COUNT = 3
|
|
REDUCTION_RATIO = 1.0 + RING_TEETH / SUN_TEETH # 4.5:1, fixed ring gear
|
|
|
|
SUN_PITCH_R = GEAR_MODULE * SUN_TEETH / 2 # 12.0
|
|
PLANET_PITCH_R = GEAR_MODULE * PLANET_TEETH / 2 # 15.0
|
|
RING_PITCH_R = GEAR_MODULE * RING_TEETH / 2 # 42.0
|
|
PLANET_CENTER_R = SUN_PITCH_R + PLANET_PITCH_R # 27.0
|
|
PLANET_ANGLES_DEG = (90.0, 210.0, 330.0)
|
|
|
|
ADDENDUM = 1.0 * GEAR_MODULE
|
|
DEDENDUM = 1.25 * GEAR_MODULE
|
|
GEAR_WIDTH = 12.0
|
|
GEAR_Z = (8.5, 20.5)
|
|
|
|
SUN_ROOT_R = SUN_PITCH_R - DEDENDUM # 10.75
|
|
SUN_TIP_R = SUN_PITCH_R + ADDENDUM # 13.0
|
|
PLANET_ROOT_R = PLANET_PITCH_R - DEDENDUM # 13.75
|
|
PLANET_TIP_R = PLANET_PITCH_R + ADDENDUM # 16.0
|
|
RING_TIP_R = RING_PITCH_R - ADDENDUM # 41.0 (internal teeth point inward)
|
|
RING_ROOT_R = RING_PITCH_R + DEDENDUM # 43.25
|
|
RING_RIM_R = 46.0
|
|
PLANET_BORE_R = 4.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hollow shaft cable routing
|
|
# ---------------------------------------------------------------------------
|
|
TUBE_OUTER_R = 7.0 # Ø14 conduit
|
|
TUBE_INNER_R = 5.5 # Ø11 cable bore
|
|
# The static tube protrudes past the rear cover and the output flange so the
|
|
# hollow-shaft cable path visibly enters and exits the actuator.
|
|
TUBE_Z = (-36.5, 35.5)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Outrunner BLDC motor
|
|
# ---------------------------------------------------------------------------
|
|
STATOR_SLOTS = 24
|
|
STATOR_YOKE_IR = 28.0 # seats on the housing center sleeve
|
|
STATOR_YOKE_OR = 33.0
|
|
STATOR_TOOTH_TIP_R = 40.0
|
|
STATOR_TOOTH_W = 5.0 # tangential tooth width
|
|
STATOR_SHOE_W = 7.6 # tangential pole-shoe width
|
|
STATOR_SHOE_IR = 38.8
|
|
STATOR_STACK_Z = (-13.0, 1.0)
|
|
|
|
WINDING_RADIAL = (33.4, 38.6) # clears yoke OD and pole shoes
|
|
WINDING_TANGENTIAL = 8.0
|
|
WINDING_AXIAL = 17.0 # end turns overhang the stack
|
|
WINDING_HOLE_TANGENTIAL = 5.1
|
|
WINDING_HOLE_AXIAL = 14.1
|
|
|
|
MAGNET_COUNT = 28
|
|
MAGNET_IR = 40.75 # 0.75 mm airgap over the stator tooth tips
|
|
MAGNET_OR = 43.75
|
|
MAGNET_W = 8.0
|
|
MAGNET_Z = (-13.0, 1.0)
|
|
BACK_IRON_R = (43.75, 45.5)
|
|
BACK_IRON_Z = (-13.5, 2.5)
|
|
|
|
ROTOR_HUB_R = (7.5, 11.0) # hollow hub over the cable tube
|
|
ROTOR_HUB_Z = (-20.0, 5.5)
|
|
ROTOR_WEB_Z = (2.5, 5.5) # front web (output-side bell)
|
|
ROTOR_FLANGE_R = (7.5, 17.0) # sun-gear coupling flange
|
|
ROTOR_FLANGE_Z = (5.5, 6.8)
|
|
COUPLING_HOLE_R = 11.25 # 4x M3 rotor/sun coupling circle
|
|
AIRGAP = MAGNET_IR - STATOR_TOOTH_TIP_R
|
|
|
|
# Rotor support ball bearings (inner race on hub, outer race in sleeve bore)
|
|
ROTOR_BRG_IR = (11.0, 12.4)
|
|
ROTOR_BRG_OR = (13.6, 15.0)
|
|
ROTOR_BRG_BALL_R = 1.25
|
|
ROTOR_BRG_BALL_COUNT = 10
|
|
ROTOR_BRG_REAR_Z = (-17.0, -12.0)
|
|
ROTOR_BRG_FRONT_Z = (-3.5, 1.5)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Housing (aluminum, axial heat fins) and front retainer
|
|
# ---------------------------------------------------------------------------
|
|
BARREL_R = (46.0, 50.0)
|
|
BARREL_Z = (-31.0, 29.0)
|
|
# Dense thin-fin array over the motor/electronics zone; deliberately many,
|
|
# thin, and shallow so the array reads as heat-sink fins, not gear teeth.
|
|
# Fins stop short of BOTH end faces so each rim keeps a machined solid band.
|
|
FIN_COUNT = 64
|
|
FIN_RADIAL = (49.5, 52.5)
|
|
FIN_W = 1.4
|
|
FIN_Z = (-26.5, 12.0)
|
|
REAR_WALL_R = (13.0, 46.0)
|
|
REAR_WALL_Z = (-22.0, -18.0)
|
|
SLEEVE_R = (15.0, 28.0)
|
|
SLEEVE_Z = (-18.0, 1.5)
|
|
RETAINER_R = (41.5, 50.0)
|
|
RETAINER_Z = (29.0, 31.0)
|
|
FRONT_SCREW_CIRCLE_R = 48.0 # 8x M3 retainer screws into the barrel face
|
|
FRONT_SCREW_COUNT = 8
|
|
RETAINER_SCREW_HEAD_R = 2.75 # visible M3 socket-head fasteners in the ring
|
|
RETAINER_SCREW_HEAD_H = 2.2
|
|
RETAINER_SCREW_SOCKET_R = 1.45
|
|
REAR_SCREW_CIRCLE_R = 48.0 # 8x M3 rear cover screws
|
|
REAR_SCREW_COUNT = 8
|
|
PHASE_WIRE_HOLE_R = 2.5 # 3x stator phase-wire passages through the rear wall
|
|
PHASE_WIRE_CIRCLE_R = 34.0
|
|
PHASE_WIRE_ANGLES = (15.0, 135.0, 255.0)
|
|
|
|
ENC_STANDOFF_R = 2.5
|
|
ENC_STANDOFF_CIRCLE_R = 17.5
|
|
ENC_STANDOFF_Z = (-24.4, -22.0)
|
|
ENC_STANDOFF_ANGLES = (90.0, 210.0, 330.0)
|
|
DRV_STANDOFF_R = 3.0
|
|
DRV_STANDOFF_CIRCLE_R = 40.0
|
|
DRV_STANDOFF_Z = (-28.8, -22.0)
|
|
DRV_STANDOFF_ANGLES = (30.0, 150.0, 270.0)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Rear cover and sealed connectors
|
|
# ---------------------------------------------------------------------------
|
|
COVER_PLATE_Z = (-34.0, -31.0)
|
|
COVER_R = 50.0
|
|
COVER_LIP_R = (43.8, 45.8)
|
|
COVER_LIP_Z = (-31.0, -29.5)
|
|
COVER_BOSS_R = (7.05, 8.5)
|
|
COVER_BOSS_Z = (-31.0, -29.0)
|
|
CONNECTOR_CIRCLE_R = 28.0
|
|
CONNECTOR_ANGLES = {"power": 135.0, "signal": 225.0}
|
|
CONNECTOR_SCALE = {"power": 1.0, "signal": 0.82}
|
|
ORING_MAJOR_R = 8.2 # scaled per connector
|
|
ORING_MINOR_R = 0.6
|
|
ORING_GLAND_MINOR_R = 0.7
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Electronics
|
|
# ---------------------------------------------------------------------------
|
|
DRIVER_BOARD_R = (9.0, 43.0)
|
|
DRIVER_BOARD_Z = (-30.4, -28.8)
|
|
FET_COUNT = 6
|
|
FET_SIZE = (9.0, 7.0, 2.5) # radial, tangential, height
|
|
FET_CIRCLE_R = 38.0
|
|
CAP_COUNT = 6
|
|
CAP_R = 4.0
|
|
CAP_H = 5.0
|
|
CAP_CIRCLE_R = 33.0
|
|
MCU_SIZE = (10.0, 10.0, 1.5)
|
|
MCU_CENTER = (0.0, 22.0) # az 90 deg
|
|
|
|
ENCODER_BOARD_R = (8.0, 20.0)
|
|
ENCODER_BOARD_Z = (-26.0, -24.4)
|
|
ENCODER_CHIP_SIZE = (5.0, 5.0, 1.0)
|
|
ENCODER_CHIP_CENTER = (0.0, -10.0) # az 270 deg, faces the rotor magnet ring
|
|
ENC_MAGNET_R = (7.5, 12.0)
|
|
ENC_MAGNET_Z = (-22.0, -20.0)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Output: carrier, cross-roller bearing, torque sensor, flange
|
|
# ---------------------------------------------------------------------------
|
|
CARRIER_PIN_R = 3.75
|
|
CARRIER_PIN_Z = (8.5, 21.0)
|
|
CARRIER_PLATE_R = (8.0, 31.0)
|
|
CARRIER_PLATE_Z = (21.0, 24.5)
|
|
CARRIER_HUB_R = (8.0, 25.0)
|
|
CARRIER_HUB_Z = (24.5, 26.0)
|
|
CARRIER_LIGHTEN_HOLE_R = 4.0
|
|
CARRIER_LIGHTEN_CIRCLE_R = 19.0
|
|
CARRIER_LIGHTEN_ANGLES = (30.0, 150.0, 270.0)
|
|
|
|
XROLLER_INNER_R = (33.0, 37.5)
|
|
XROLLER_OUTER_R = (41.5, 46.0)
|
|
XROLLER_Z = (21.0, 29.0)
|
|
XROLLER_GROOVE_SQ = 5.65 # rotated-45deg square raceway profile
|
|
XROLLER_PITCH_R = 39.5
|
|
XROLLER_ROLLER_R = 2.75
|
|
XROLLER_ROLLER_L = 5.5
|
|
XROLLER_COUNT = 16
|
|
|
|
FLEXURE_HUB_R = (8.0, 14.0)
|
|
FLEXURE_HUB_Z = (26.0, 28.5)
|
|
FLEXURE_SPOKE_COUNT = 4
|
|
FLEXURE_SPOKE_L = 13.0
|
|
FLEXURE_SPOKE_W = 6.0
|
|
FLEXURE_SPOKE_Z = (26.5, 28.5)
|
|
FLEXURE_RIM_R = (26.0, 32.0)
|
|
FLEXURE_RIM_Z = (26.5, 29.0)
|
|
GAUGE_SIZE = (8.0, 4.0, 0.3)
|
|
GAUGE_Z = 28.5
|
|
|
|
FLANGE_PLATE_R = (7.6, 36.0)
|
|
FLANGE_PLATE_Z = (29.0, 33.0)
|
|
FLANGE_BOSS_R = (14.0, 20.0)
|
|
FLANGE_BOSS_Z = (33.0, 34.5)
|
|
FLANGE_BOLT_CIRCLE_R = 28.0 # 6x M5 output bolt circle
|
|
FLANGE_BOLT_R = 2.75
|
|
FLANGE_BOLT_COUNT = 6
|
|
FLANGE_PILOT_HOLE_R = 1.7 # 4x M3 on the pilot boss
|
|
FLANGE_PILOT_CIRCLE_R = 17.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Exploded-view static translations (mm at explode=1.0), documented per part.
|
|
# Pure +/-Z technical explosion with two-tier spacing rhythm: ~10-17 mm air
|
|
# gaps between parts inside a subsystem and ~20-25 mm gaps at subsystem
|
|
# boundaries (electronics | housing | motor | gear stage | output stage), so
|
|
# every part owns a readable station. Verified pairwise non-overlapping at
|
|
# factor 1.0.
|
|
# ---------------------------------------------------------------------------
|
|
EXPLODE_OFFSETS = {
|
|
# rear: sealed connectors / cover / electronics (~12-18 mm internal gaps)
|
|
"connector_power": -82.0,
|
|
"connector_signal": -82.0,
|
|
"rear_cover": -64.0,
|
|
"driver_pcb": -46.0,
|
|
"encoder_pcb": -32.0,
|
|
# fixed root (housing | electronics boundary ~24 mm)
|
|
"housing": 0.0,
|
|
# motor group (housing | motor boundary ~19 mm, ~11-16 mm internal)
|
|
"encoder_magnet_ring": 70.0,
|
|
"rotor_bearing_rear": 78.0,
|
|
"stator": 94.0,
|
|
"rotor": 128.0,
|
|
"rotor_bearing_front": 154.0,
|
|
# gear stage (motor | gears boundary ~21 mm, ~14-16 mm internal)
|
|
"ring_gear": 168.0,
|
|
"sun_gear": 196.0,
|
|
"planet_gear": 222.0,
|
|
"planet_carrier": 250.0,
|
|
# output stage (gears | output boundary ~25 mm, ~11-18 mm internal)
|
|
"cross_roller_bearing": 280.0,
|
|
"front_retainer": 298.0,
|
|
"retainer_screws": 312.0,
|
|
"torque_sensor": 330.0,
|
|
"output_flange": 348.0,
|
|
"cable_tube": 360.0,
|
|
}
|
|
# Planets additionally translate radially outward so the three gears separate
|
|
# in projection instead of stacking behind each other.
|
|
PLANET_EXPLODE_RADIAL = 14.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Concept material colors (production-actuator palette: dark anodized body,
|
|
# machined-aluminum output hardware, one anodized accent ring)
|
|
# ---------------------------------------------------------------------------
|
|
AL_BODY = Color(0.13, 0.135, 0.15) # black-anodized housing
|
|
AL_COVER = Color(0.115, 0.12, 0.13) # black-anodized rear cover
|
|
AL_BRIGHT = Color(0.78, 0.79, 0.81) # machined aluminum (flange, carrier)
|
|
AL_RETAINER = Color(0.85, 0.20, 0.01) # saturated anodized-orange accent ring
|
|
STEEL = Color(0.62, 0.63, 0.66)
|
|
STEEL_POLISHED = Color(0.76, 0.77, 0.79)
|
|
STEEL_RACE = Color(0.24, 0.25, 0.29) # dark bearing-steel races vs aluminum
|
|
STEEL_ROLLER = Color(0.85, 0.86, 0.88)
|
|
LAMINATION = Color(0.30, 0.31, 0.34)
|
|
COPPER = Color(0.80, 0.45, 0.18)
|
|
ROTOR_DARK = Color(0.24, 0.25, 0.28)
|
|
MAGNET_N = Color(0.10, 0.10, 0.12) # alternating pole colors: dark / nickel
|
|
MAGNET_S = Color(0.68, 0.69, 0.71)
|
|
FERRITE = Color(0.25, 0.08, 0.10)
|
|
PCB_GREEN = Color(0.05, 0.30, 0.14)
|
|
IC_BLACK = Color(0.08, 0.08, 0.09)
|
|
CAP_GRAY = Color(0.22, 0.24, 0.30)
|
|
PLASTIC_BLACK = Color(0.07, 0.07, 0.08)
|
|
GUNMETAL = Color(0.34, 0.35, 0.38) # connector coupling nuts
|
|
PIN_GOLD = Color(0.80, 0.64, 0.25) # connector contacts
|
|
BLACK_OXIDE = Color(0.20, 0.20, 0.22) # socket-head fasteners
|
|
RUBBER = Color(0.04, 0.04, 0.04)
|
|
TITANIUM = Color(0.60, 0.59, 0.64)
|
|
GAUGE_GOLD = Color(0.85, 0.72, 0.35)
|
|
TUBE_BLACK = Color(0.32, 0.33, 0.35) # dark gunmetal, reads against the cover
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Consistency checks (concept-level fit, not tolerance engineering)
|
|
# ---------------------------------------------------------------------------
|
|
assert RING_TEETH == SUN_TEETH + 2 * PLANET_TEETH, "planetary tooth-count identity"
|
|
assert (SUN_TEETH + RING_TEETH) % PLANET_COUNT == 0, "equal planet spacing condition"
|
|
assert AIRGAP >= 0.5, "outrunner magnetic airgap"
|
|
assert SUN_ROOT_R - TUBE_OUTER_R - 0.5 >= 2.5, "sun gear wall over hollow tube"
|
|
assert MAGNET_OR <= BACK_IRON_R[0] + 1e-9, "magnets sit inside the back iron"
|
|
assert BACK_IRON_R[1] < BARREL_R[0], "rotor bell clears the housing bore"
|
|
assert XROLLER_INNER_R[1] < XROLLER_PITCH_R - 0.5, "roller pitch sits in the race gap"
|
|
assert XROLLER_OUTER_R[0] > XROLLER_PITCH_R + 0.5, "roller pitch sits in the race gap"
|
|
assert RING_RIM_R == BARREL_R[0], "ring gear rim seats in the housing bore"
|
|
assert FLEXURE_RIM_R[1] < XROLLER_INNER_R[0], "torque flexure clears the bearing"
|
|
|
|
|
|
def _tube_z(outer_r: float, inner_r: float, z: tuple[float, float]) -> Part:
|
|
"""Annular ring solid between z[0]..z[1]."""
|
|
return Pos(0, 0, z[0]) * extrude(Circle(outer_r) - Circle(inner_r), amount=z[1] - z[0])
|
|
|
|
|
|
def _cyl_z(r: float, z: tuple[float, float]) -> Part:
|
|
return Pos(0, 0, z[0]) * extrude(Circle(r), amount=z[1] - z[0])
|
|
|
|
|
|
def _external_gear(teeth: int, root_r: float, tip_r: float, bore_r: float) -> Part:
|
|
"""Concept gear: root cylinder + trapezoidal teeth with visible backlash."""
|
|
half_arc = math.pi * GEAR_MODULE / 2
|
|
wt = half_arc * 0.34 # half-width at tip (thinned for visible backlash)
|
|
wr = half_arc * 0.55 # half-width at root
|
|
h = tip_r - root_r
|
|
rm = (tip_r + root_r) / 2
|
|
tooth = Polygon(
|
|
(-h / 2 - 0.4, -wr), (h / 2, -wt), (h / 2, wt), (-h / 2 - 0.4, wr), align=None
|
|
)
|
|
teeth_sk = [loc * tooth for loc in PolarLocations(rm, teeth)]
|
|
sketch = Circle(root_r) + teeth_sk - Circle(bore_r)
|
|
return extrude(sketch, amount=GEAR_WIDTH)
|
|
|
|
|
|
def _internal_ring_gear() -> Part:
|
|
"""Internal ring gear with inward trapezoidal teeth and press-in rim."""
|
|
half_arc = math.pi * GEAR_MODULE / 2
|
|
wt = half_arc * 0.34
|
|
wr = half_arc * 0.55
|
|
h = RING_ROOT_R - RING_TIP_R
|
|
rm = (RING_ROOT_R + RING_TIP_R) / 2
|
|
# counter-clockwise point order so the face normal is +Z and the teeth
|
|
# extrude in the same direction as the rim
|
|
tooth = Polygon(
|
|
(h / 2 + 0.4, -wr), (h / 2 + 0.4, wr), (-h / 2, wt), (-h / 2, -wt), align=None
|
|
)
|
|
teeth_sk = [loc * tooth for loc in PolarLocations(rm, RING_TEETH)]
|
|
sketch = Circle(RING_RIM_R) - Circle(RING_ROOT_R) + teeth_sk
|
|
return extrude(sketch, amount=GEAR_WIDTH)
|
|
|
|
|
|
def make_housing() -> Part:
|
|
"""Finned aluminum housing: barrel, rear wall, stator sleeve, standoffs."""
|
|
barrel = _tube_z(BARREL_R[1], BARREL_R[0], BARREL_Z)
|
|
rear_wall = _tube_z(REAR_WALL_R[1], REAR_WALL_R[0], REAR_WALL_Z)
|
|
sleeve = _tube_z(SLEEVE_R[1], SLEEVE_R[0], SLEEVE_Z)
|
|
|
|
fin = Box(
|
|
FIN_RADIAL[1] - FIN_RADIAL[0] + 0.5, # overlap into the barrel for fusing
|
|
FIN_W,
|
|
FIN_Z[1] - FIN_Z[0],
|
|
)
|
|
fin_loc = Pos((FIN_RADIAL[0] - 0.5 + FIN_RADIAL[1]) / 2, 0, (FIN_Z[0] + FIN_Z[1]) / 2)
|
|
fins = [rot * (fin_loc * fin) for rot in PolarLocations(0, FIN_COUNT)]
|
|
|
|
standoffs = []
|
|
for az in ENC_STANDOFF_ANGLES:
|
|
standoffs.append(
|
|
Rotation(0, 0, az)
|
|
* Pos(ENC_STANDOFF_CIRCLE_R, 0, 0)
|
|
* _cyl_z(ENC_STANDOFF_R, ENC_STANDOFF_Z)
|
|
)
|
|
for az in DRV_STANDOFF_ANGLES:
|
|
standoffs.append(
|
|
Rotation(0, 0, az)
|
|
* Pos(DRV_STANDOFF_CIRCLE_R, 0, 0)
|
|
* _cyl_z(DRV_STANDOFF_R, DRV_STANDOFF_Z)
|
|
)
|
|
|
|
housing = barrel + [rear_wall, sleeve, *fins, *standoffs]
|
|
|
|
cuts = []
|
|
# 8x M3 retainer screw holes in the front barrel face
|
|
for loc in PolarLocations(FRONT_SCREW_CIRCLE_R, FRONT_SCREW_COUNT, start_angle=22.5):
|
|
cuts.append(loc * _cyl_z(1.7, (BARREL_Z[1] - 8.0, BARREL_Z[1] + 0.1)))
|
|
# 8x M3 rear cover screw holes in the rear barrel face
|
|
for loc in PolarLocations(REAR_SCREW_CIRCLE_R, REAR_SCREW_COUNT, start_angle=22.5):
|
|
cuts.append(loc * _cyl_z(1.4, (BARREL_Z[0] - 0.1, BARREL_Z[0] + 5.0)))
|
|
# 3x stator phase-wire passages through the rear wall
|
|
for az in PHASE_WIRE_ANGLES:
|
|
cuts.append(
|
|
Rotation(0, 0, az)
|
|
* Pos(PHASE_WIRE_CIRCLE_R, 0, 0)
|
|
* _cyl_z(PHASE_WIRE_HOLE_R, (REAR_WALL_Z[0] - 0.1, REAR_WALL_Z[1] + 0.1))
|
|
)
|
|
return housing - cuts
|
|
|
|
|
|
def make_front_retainer() -> Part:
|
|
"""Bolted bearing retainer ring (makes the front stack assemblable)."""
|
|
ring = _tube_z(RETAINER_R[1], RETAINER_R[0], RETAINER_Z)
|
|
holes = [
|
|
loc * _cyl_z(1.7, (RETAINER_Z[0] - 0.1, RETAINER_Z[1] + 0.1))
|
|
for loc in PolarLocations(FRONT_SCREW_CIRCLE_R, FRONT_SCREW_COUNT, start_angle=22.5)
|
|
]
|
|
return ring - holes
|
|
|
|
|
|
def make_retainer_screws() -> list[Part]:
|
|
"""8x M3 socket-head fasteners populating the retainer screw circle."""
|
|
screws = []
|
|
socket = extrude(RegularPolygon(RETAINER_SCREW_SOCKET_R, 6), amount=1.2)
|
|
for loc in PolarLocations(FRONT_SCREW_CIRCLE_R, FRONT_SCREW_COUNT, start_angle=22.5):
|
|
head = _cyl_z(
|
|
RETAINER_SCREW_HEAD_R, (RETAINER_Z[1], RETAINER_Z[1] + RETAINER_SCREW_HEAD_H)
|
|
)
|
|
shank = _cyl_z(1.55, (RETAINER_Z[0] + 0.5, RETAINER_Z[1]))
|
|
screw = (head + shank) - Pos(
|
|
0, 0, RETAINER_Z[1] + RETAINER_SCREW_HEAD_H - 1.1
|
|
) * socket
|
|
screws.append(loc * screw)
|
|
return screws
|
|
|
|
|
|
def make_rear_cover() -> Part:
|
|
"""Sealed rear cover: plate, locating lip, tube boss, o-ring glands."""
|
|
plate = _cyl_z(COVER_R, COVER_PLATE_Z)
|
|
lip = _tube_z(COVER_LIP_R[1], COVER_LIP_R[0], COVER_LIP_Z)
|
|
boss = _tube_z(COVER_BOSS_R[1], COVER_BOSS_R[0], COVER_BOSS_Z)
|
|
cover = plate + [lip, boss]
|
|
|
|
cuts = [_cyl_z(COVER_BOSS_R[0], (COVER_PLATE_Z[0] - 0.1, COVER_PLATE_Z[1] + 0.1))]
|
|
# connector pass-throughs + o-ring glands on the rear face
|
|
for name, az in CONNECTOR_ANGLES.items():
|
|
hole_r = 5.0 if name == "power" else 4.0
|
|
conn_loc = Rotation(0, 0, az) * Pos(CONNECTOR_CIRCLE_R, 0, 0)
|
|
cuts.append(conn_loc * _cyl_z(hole_r, (COVER_PLATE_Z[0] - 0.1, COVER_PLATE_Z[1] + 0.1)))
|
|
cuts.append(
|
|
conn_loc
|
|
* Pos(0, 0, COVER_PLATE_Z[0])
|
|
* Torus(ORING_MAJOR_R * CONNECTOR_SCALE[name], ORING_GLAND_MINOR_R)
|
|
)
|
|
# 8x M3 counterbored cover screws
|
|
for loc in PolarLocations(REAR_SCREW_CIRCLE_R, REAR_SCREW_COUNT, start_angle=22.5):
|
|
cuts.append(loc * _cyl_z(1.7, (COVER_PLATE_Z[0] - 0.1, COVER_PLATE_Z[1] + 0.1)))
|
|
cuts.append(loc * _cyl_z(3.0, (COVER_PLATE_Z[0] - 0.1, COVER_PLATE_Z[0] + 1.8)))
|
|
return cover - cuts
|
|
|
|
|
|
def make_connector(kind: str) -> list[Part]:
|
|
"""Sealed circular connector at the cover face.
|
|
|
|
Body with shrouded keying cup, 12-flat coupling nut (knurl read), recessed
|
|
insert floor with a gold pin array, and an o-ring seal at the flange.
|
|
"""
|
|
az = CONNECTOR_ANGLES[kind]
|
|
scale = CONNECTOR_SCALE[kind]
|
|
z0 = COVER_PLATE_Z[0] # mounting face, body protrudes in -Z
|
|
flange = _cyl_z(9.0 * scale, (z0 - 1.5, z0))
|
|
barrel = _cyl_z(7.5 * scale, (z0 - 6.5, z0 - 1.5))
|
|
core = _cyl_z(5.5 * scale, (z0 - 7.8, z0 - 1.5))
|
|
shroud = _tube_z(6.0 * scale, 5.0 * scale, (z0 - 10.0, z0 - 7.5))
|
|
insert_floor = _cyl_z(5.0 * scale, (z0 - 8.4, z0 - 7.5))
|
|
body = flange + [barrel, core, shroud, insert_floor]
|
|
# cut the gland into the connector flange as well so the o-ring seats cleanly
|
|
body = body - Pos(0, 0, z0) * Torus(ORING_MAJOR_R * scale, ORING_GLAND_MINOR_R)
|
|
collar = Pos(0, 0, z0 - 9.0) * extrude(
|
|
RegularPolygon(8.5 * scale, 12) - Circle(6.2 * scale), amount=2.5
|
|
)
|
|
o_ring = Pos(0, 0, z0) * Torus(ORING_MAJOR_R * scale, ORING_MINOR_R)
|
|
pin_count = 4 if kind == "power" else 6
|
|
pin_r = 0.8 * scale if kind == "power" else 0.55
|
|
pins = Compound(
|
|
children=[
|
|
loc * _cyl_z(pin_r, (z0 - 9.7, z0 - 8.4))
|
|
for loc in PolarLocations(2.6 * scale, pin_count)
|
|
]
|
|
)
|
|
loc = Rotation(0, 0, az) * Pos(CONNECTOR_CIRCLE_R, 0, 0)
|
|
return [loc * body, loc * collar, loc * o_ring, loc * pins]
|
|
|
|
|
|
def make_driver_pcb() -> dict[str, object]:
|
|
"""Annular motor-driver PCB with FETs, bus capacitors, and MCU."""
|
|
board = _tube_z(DRIVER_BOARD_R[1], DRIVER_BOARD_R[0], DRIVER_BOARD_Z)
|
|
z_face = DRIVER_BOARD_Z[1]
|
|
fets = []
|
|
for i, loc in enumerate(PolarLocations(FET_CIRCLE_R, FET_COUNT, start_angle=15.0)):
|
|
fets.append(
|
|
loc * Pos(0, 0, z_face + FET_SIZE[2] / 2) * Box(FET_SIZE[0], FET_SIZE[1], FET_SIZE[2])
|
|
)
|
|
caps = [
|
|
loc * Pos(0, 0, z_face + CAP_H / 2) * Cylinder(CAP_R, CAP_H)
|
|
for loc in PolarLocations(CAP_CIRCLE_R, CAP_COUNT, start_angle=0.0)
|
|
]
|
|
mcu = Pos(MCU_CENTER[0], MCU_CENTER[1], z_face + MCU_SIZE[2] / 2) * Box(*MCU_SIZE)
|
|
return {"board": board, "fets": fets, "caps": caps, "mcu": mcu}
|
|
|
|
|
|
def make_encoder_pcb() -> dict[str, object]:
|
|
"""Encoder PCB behind the rotor magnet ring."""
|
|
board = _tube_z(ENCODER_BOARD_R[1], ENCODER_BOARD_R[0], ENCODER_BOARD_Z)
|
|
chip = Pos(
|
|
ENCODER_CHIP_CENTER[0], ENCODER_CHIP_CENTER[1], ENCODER_BOARD_Z[1] + ENCODER_CHIP_SIZE[2] / 2
|
|
) * Box(*ENCODER_CHIP_SIZE)
|
|
return {"board": board, "chip": chip}
|
|
|
|
|
|
def make_stator() -> dict[str, object]:
|
|
"""24-slot outrunner stator: laminated yoke/teeth and copper windings."""
|
|
yoke = _tube_z(STATOR_YOKE_OR, STATOR_YOKE_IR, STATOR_STACK_Z)
|
|
z_mid = (STATOR_STACK_Z[0] + STATOR_STACK_Z[1]) / 2
|
|
stack_h = STATOR_STACK_Z[1] - STATOR_STACK_Z[0]
|
|
|
|
tooth = Pos((STATOR_YOKE_OR - 0.5 + STATOR_TOOTH_TIP_R) / 2, 0, z_mid) * Box(
|
|
STATOR_TOOTH_TIP_R - STATOR_YOKE_OR + 0.5, STATOR_TOOTH_W, stack_h
|
|
)
|
|
shoe = Pos((STATOR_SHOE_IR + STATOR_TOOTH_TIP_R) / 2, 0, z_mid) * Box(
|
|
STATOR_TOOTH_TIP_R - STATOR_SHOE_IR, STATOR_SHOE_W, stack_h
|
|
)
|
|
tooth_unit = tooth + shoe
|
|
teeth = [rot * tooth_unit for rot in PolarLocations(0, STATOR_SLOTS)]
|
|
stack = yoke + teeth
|
|
|
|
# one rectangular coil loop around a tooth, then a polar pattern of copies
|
|
wind_r_mid = (WINDING_RADIAL[0] + WINDING_RADIAL[1]) / 2
|
|
coil_outer = Pos(wind_r_mid, 0, z_mid) * Box(
|
|
WINDING_RADIAL[1] - WINDING_RADIAL[0], WINDING_TANGENTIAL, WINDING_AXIAL
|
|
)
|
|
coil_hole = Pos(wind_r_mid, 0, z_mid) * Box(
|
|
WINDING_RADIAL[1] - WINDING_RADIAL[0] + 1.0, WINDING_HOLE_TANGENTIAL, WINDING_HOLE_AXIAL
|
|
)
|
|
coil = coil_outer - coil_hole
|
|
windings = [rot * coil for rot in PolarLocations(0, STATOR_SLOTS)]
|
|
return {"stack": stack, "windings": windings}
|
|
|
|
|
|
def make_rotor() -> dict[str, object]:
|
|
"""Outrunner rotor bell: hollow hub, front web, back iron, magnet ring."""
|
|
hub = _tube_z(ROTOR_HUB_R[1], ROTOR_HUB_R[0], ROTOR_HUB_Z)
|
|
web = _tube_z(BACK_IRON_R[1], ROTOR_HUB_R[0], ROTOR_WEB_Z)
|
|
iron = _tube_z(BACK_IRON_R[1], BACK_IRON_R[0], BACK_IRON_Z)
|
|
flange = _tube_z(ROTOR_FLANGE_R[1], ROTOR_FLANGE_R[0], ROTOR_FLANGE_Z)
|
|
body = hub + [web, iron, flange]
|
|
|
|
cuts = []
|
|
# 6x web lightening holes
|
|
for loc in PolarLocations(28.0, 6, start_angle=30.0):
|
|
cuts.append(loc * _cyl_z(5.0, (ROTOR_WEB_Z[0] - 0.1, ROTOR_WEB_Z[1] + 0.1)))
|
|
# 4x M3 sun-gear coupling holes
|
|
for loc in PolarLocations(COUPLING_HOLE_R, 4, start_angle=45.0):
|
|
cuts.append(loc * _cyl_z(1.6, (ROTOR_FLANGE_Z[0] - 0.1, ROTOR_FLANGE_Z[1] + 0.1)))
|
|
body = body - cuts
|
|
|
|
z_mid = (MAGNET_Z[0] + MAGNET_Z[1]) / 2
|
|
|
|
# Two independent prototypes so the alternating pole groups keep distinct
|
|
# exported materials (copies of one shape share a single STEP style).
|
|
def magnet_proto() -> Part:
|
|
return Pos((MAGNET_IR + MAGNET_OR) / 2, 0, z_mid) * Box(
|
|
MAGNET_OR - MAGNET_IR, MAGNET_W, MAGNET_Z[1] - MAGNET_Z[0]
|
|
)
|
|
|
|
north_proto, south_proto = magnet_proto(), magnet_proto()
|
|
locs = list(PolarLocations(0, MAGNET_COUNT))
|
|
magnets_north = [locs[i] * north_proto for i in range(0, MAGNET_COUNT, 2)]
|
|
magnets_south = [locs[i] * south_proto for i in range(1, MAGNET_COUNT, 2)]
|
|
return {"body": body, "magnets_north": magnets_north, "magnets_south": magnets_south}
|
|
|
|
|
|
def make_ball_bearing(z: tuple[float, float]) -> dict[str, object]:
|
|
"""Simplified deep-groove rotor support bearing: races + ball ring."""
|
|
z_mid = (z[0] + z[1]) / 2
|
|
ball_pitch_r = (ROTOR_BRG_IR[1] + ROTOR_BRG_OR[0]) / 2
|
|
groove = Pos(0, 0, z_mid) * Torus(ball_pitch_r, ROTOR_BRG_BALL_R + 0.05)
|
|
inner = _tube_z(ROTOR_BRG_IR[1], ROTOR_BRG_IR[0], z) - groove
|
|
outer = _tube_z(ROTOR_BRG_OR[1], ROTOR_BRG_OR[0], z) - groove
|
|
balls = [
|
|
loc * Pos(0, 0, z_mid) * Sphere(ROTOR_BRG_BALL_R)
|
|
for loc in PolarLocations(ball_pitch_r, ROTOR_BRG_BALL_COUNT)
|
|
]
|
|
return {"inner": inner, "outer": outer, "balls": balls}
|
|
|
|
|
|
def make_sun_gear() -> Part:
|
|
"""Sun gear with hollow bore and rotor coupling flange."""
|
|
gear = Pos(0, 0, GEAR_Z[0]) * _external_gear(SUN_TEETH, SUN_ROOT_R, SUN_TIP_R, TUBE_OUTER_R + 0.5)
|
|
neck = _tube_z(SUN_ROOT_R, TUBE_OUTER_R + 0.5, (ROTOR_FLANGE_Z[1], GEAR_Z[0]))
|
|
flange = _tube_z(15.0, TUBE_OUTER_R + 0.5, (ROTOR_FLANGE_Z[1], ROTOR_FLANGE_Z[1] + 1.5))
|
|
sun = gear + [neck, flange]
|
|
holes = [
|
|
loc * _cyl_z(1.6, (ROTOR_FLANGE_Z[1] - 0.1, ROTOR_FLANGE_Z[1] + 1.6))
|
|
for loc in PolarLocations(COUPLING_HOLE_R, 4, start_angle=45.0)
|
|
]
|
|
return sun - holes
|
|
|
|
|
|
def make_planet_gear() -> Part:
|
|
"""One planet gear at the origin; placed with mesh-phase rotation."""
|
|
return _external_gear(PLANET_TEETH, PLANET_ROOT_R, PLANET_TIP_R, PLANET_BORE_R)
|
|
|
|
|
|
def planet_location(psi_deg: float) -> Location:
|
|
"""Planet placement: position on the carrier circle plus mesh phasing.
|
|
|
|
With sun/ring tooth patterns starting at azimuth 0, a tooth of each faces
|
|
the planet center whenever psi is a multiple of the sun/ring angular pitch
|
|
(true for 90/210/330 with 24 and 84 teeth). Rotating the planet by
|
|
psi + half a planet tooth pitch puts a tooth gap on the line of centers.
|
|
"""
|
|
phase = psi_deg + 180.0 / PLANET_TEETH
|
|
px = PLANET_CENTER_R * math.cos(math.radians(psi_deg))
|
|
py = PLANET_CENTER_R * math.sin(math.radians(psi_deg))
|
|
return Pos(px, py, GEAR_Z[0]) * Rotation(0, 0, phase)
|
|
|
|
|
|
def make_carrier() -> Part:
|
|
"""Single-sided planet carrier: pins, plate, and torque-sensor hub."""
|
|
plate = _tube_z(CARRIER_PLATE_R[1], CARRIER_PLATE_R[0], CARRIER_PLATE_Z)
|
|
hub = _tube_z(CARRIER_HUB_R[1], CARRIER_HUB_R[0], CARRIER_HUB_Z)
|
|
pins = [
|
|
loc * _cyl_z(CARRIER_PIN_R, CARRIER_PIN_Z)
|
|
for loc in PolarLocations(PLANET_CENTER_R, PLANET_COUNT, start_angle=90.0)
|
|
]
|
|
carrier = plate + [hub, *pins]
|
|
lighten = [
|
|
Rotation(0, 0, az)
|
|
* Pos(CARRIER_LIGHTEN_CIRCLE_R, 0, 0)
|
|
* _cyl_z(CARRIER_LIGHTEN_HOLE_R, (CARRIER_PLATE_Z[0] - 0.1, CARRIER_PLATE_Z[1] + 0.1))
|
|
for az in CARRIER_LIGHTEN_ANGLES
|
|
]
|
|
return carrier - lighten
|
|
|
|
|
|
def make_cross_roller_bearing() -> dict[str, object]:
|
|
"""Cross-roller output bearing: V-groove races, alternating 45-deg rollers."""
|
|
z_mid = (XROLLER_Z[0] + XROLLER_Z[1]) / 2
|
|
groove_profile = Plane.XZ * (
|
|
Pos(XROLLER_PITCH_R, z_mid) * Rotation(0, 0, 45) * Rectangle(XROLLER_GROOVE_SQ, XROLLER_GROOVE_SQ)
|
|
)
|
|
groove = revolve(groove_profile, axis=Axis.Z)
|
|
inner = _tube_z(XROLLER_INNER_R[1], XROLLER_INNER_R[0], XROLLER_Z) - groove
|
|
outer = _tube_z(XROLLER_OUTER_R[1], XROLLER_OUTER_R[0], XROLLER_Z) - groove
|
|
|
|
rollers = []
|
|
for i in range(XROLLER_COUNT):
|
|
az = 360.0 * i / XROLLER_COUNT
|
|
tilt = 45.0 if i % 2 == 0 else -45.0
|
|
rollers.append(
|
|
Rotation(0, 0, az)
|
|
* Pos(XROLLER_PITCH_R, 0, z_mid)
|
|
* Rotation(0, tilt, 0)
|
|
* Cylinder(XROLLER_ROLLER_R, XROLLER_ROLLER_L)
|
|
)
|
|
return {"inner": inner, "outer": outer, "rollers": rollers}
|
|
|
|
|
|
def make_torque_sensor() -> dict[str, object]:
|
|
"""Spoked torque-sensing flexure with strain-gauge pads."""
|
|
hub = _tube_z(FLEXURE_HUB_R[1], FLEXURE_HUB_R[0], FLEXURE_HUB_Z)
|
|
rim = _tube_z(FLEXURE_RIM_R[1], FLEXURE_RIM_R[0], FLEXURE_RIM_Z)
|
|
spoke_mid_r = (FLEXURE_HUB_R[1] + FLEXURE_RIM_R[0]) / 2
|
|
spoke = Pos(spoke_mid_r, 0, (FLEXURE_SPOKE_Z[0] + FLEXURE_SPOKE_Z[1]) / 2) * Box(
|
|
FLEXURE_SPOKE_L, FLEXURE_SPOKE_W, FLEXURE_SPOKE_Z[1] - FLEXURE_SPOKE_Z[0]
|
|
)
|
|
spokes = [rot * spoke for rot in PolarLocations(0, FLEXURE_SPOKE_COUNT)]
|
|
flexure = hub + [rim, *spokes]
|
|
gauges = [
|
|
rot * Pos(spoke_mid_r, 0, GAUGE_Z + GAUGE_SIZE[2] / 2) * Box(*GAUGE_SIZE)
|
|
for rot in PolarLocations(0, FLEXURE_SPOKE_COUNT)
|
|
]
|
|
return {"flexure": flexure, "gauges": gauges}
|
|
|
|
|
|
def make_output_flange() -> Part:
|
|
"""Output flange: bolt circle, pilot boss, hollow bore."""
|
|
plate = _tube_z(FLANGE_PLATE_R[1], FLANGE_PLATE_R[0], FLANGE_PLATE_Z)
|
|
boss = _tube_z(FLANGE_BOSS_R[1], FLANGE_BOSS_R[0], FLANGE_BOSS_Z)
|
|
flange = plate + boss
|
|
cuts = [
|
|
loc * _cyl_z(FLANGE_BOLT_R, (FLANGE_PLATE_Z[0] - 0.1, FLANGE_PLATE_Z[1] + 0.1))
|
|
for loc in PolarLocations(FLANGE_BOLT_CIRCLE_R, FLANGE_BOLT_COUNT)
|
|
]
|
|
cuts += [
|
|
loc * _cyl_z(FLANGE_PILOT_HOLE_R, (FLANGE_PLATE_Z[0] + 0.5, FLANGE_BOSS_Z[1] + 0.1))
|
|
for loc in PolarLocations(FLANGE_PILOT_CIRCLE_R, 4, start_angle=45.0)
|
|
]
|
|
return flange - cuts
|
|
|
|
|
|
def make_cable_tube() -> Part:
|
|
"""Static hollow cable-routing tube through the full actuator stack."""
|
|
return _tube_z(TUBE_OUTER_R, TUBE_INNER_R, TUBE_Z)
|
|
|
|
|
|
def build_actuator(explode: float = 0.0) -> Compound:
|
|
"""Build the labeled actuator assembly.
|
|
|
|
explode=0 returns the assembled model with source mate frames/joints.
|
|
explode>0 translates each part along Z by EXPLODE_OFFSETS * explode as a
|
|
documented static exploded layout (joints are omitted in that mode).
|
|
"""
|
|
asm = AssemblyHelper("qdd_actuator")
|
|
|
|
housing = asm.add(make_housing(), "housing", color=AL_BODY)
|
|
retainer = asm.add(make_front_retainer(), "front_retainer", color=AL_RETAINER)
|
|
cover = asm.add(make_rear_cover(), "rear_cover", color=AL_COVER)
|
|
|
|
screws = make_retainer_screws()
|
|
retainer_screws = asm.add(
|
|
Compound(children=screws), "retainer_screws", color=BLACK_OXIDE
|
|
)
|
|
|
|
connectors = {}
|
|
for kind in CONNECTOR_ANGLES:
|
|
body, collar, o_ring, pins = make_connector(kind)
|
|
asm.feature(body, "connector_body", kind, color=PLASTIC_BLACK)
|
|
asm.feature(collar, "connector_collar", kind, color=GUNMETAL)
|
|
asm.feature(o_ring, "connector_o_ring", kind, color=RUBBER)
|
|
asm.feature(pins, "connector_pins", kind, color=PIN_GOLD)
|
|
connectors[kind] = asm.add_module("connector", [body, collar, o_ring, pins], kind)
|
|
|
|
drv = make_driver_pcb()
|
|
asm.feature(drv["board"], "driver_board", color=PCB_GREEN)
|
|
for i, fet in enumerate(drv["fets"], start=1):
|
|
asm.feature(fet, "fet", f"q{i}", color=IC_BLACK)
|
|
for i, cap in enumerate(drv["caps"], start=1):
|
|
asm.feature(cap, "bus_capacitor", f"c{i}", color=CAP_GRAY)
|
|
asm.feature(drv["mcu"], "mcu", color=IC_BLACK)
|
|
driver_pcb = asm.add_module(
|
|
"driver_pcb", [drv["board"], *drv["fets"], *drv["caps"], drv["mcu"]]
|
|
)
|
|
|
|
enc = make_encoder_pcb()
|
|
asm.feature(enc["board"], "encoder_board", color=PCB_GREEN)
|
|
asm.feature(enc["chip"], "encoder_ic", color=IC_BLACK)
|
|
encoder_pcb = asm.add_module("encoder_pcb", [enc["board"], enc["chip"]])
|
|
|
|
enc_magnet = asm.add(
|
|
_tube_z(ENC_MAGNET_R[1], ENC_MAGNET_R[0], ENC_MAGNET_Z),
|
|
"encoder_magnet_ring",
|
|
color=FERRITE,
|
|
)
|
|
|
|
st = make_stator()
|
|
asm.feature(st["stack"], "lamination_stack", color=LAMINATION)
|
|
winding_compound = asm.feature(Compound(children=st["windings"]), "windings", color=COPPER)
|
|
stator = asm.add_module("stator", [st["stack"], winding_compound])
|
|
|
|
rt = make_rotor()
|
|
asm.feature(rt["body"], "rotor_bell", color=ROTOR_DARK)
|
|
mag_n = asm.feature(Compound(children=rt["magnets_north"]), "magnets_north", color=MAGNET_N)
|
|
mag_s = asm.feature(Compound(children=rt["magnets_south"]), "magnets_south", color=MAGNET_S)
|
|
rotor = asm.add_module("rotor", [rt["body"], mag_n, mag_s])
|
|
|
|
bearings = {}
|
|
for name, z in (("rear", ROTOR_BRG_REAR_Z), ("front", ROTOR_BRG_FRONT_Z)):
|
|
brg = make_ball_bearing(z)
|
|
asm.feature(brg["inner"], "inner_race", name, color=STEEL_POLISHED)
|
|
asm.feature(brg["outer"], "outer_race", name, color=STEEL_POLISHED)
|
|
balls = asm.feature(Compound(children=brg["balls"]), "balls", name, color=STEEL_ROLLER)
|
|
bearings[name] = asm.add_module(
|
|
"rotor_bearing", [brg["inner"], brg["outer"], balls], name
|
|
)
|
|
|
|
sun = asm.add(make_sun_gear(), "sun_gear", color=STEEL)
|
|
ring = asm.add(Pos(0, 0, GEAR_Z[0]) * _internal_ring_gear(), "ring_gear", color=STEEL)
|
|
|
|
planet_proto = make_planet_gear()
|
|
planets = []
|
|
for i, psi in enumerate(PLANET_ANGLES_DEG, start=1):
|
|
planet = planet_location(psi) * planet_proto
|
|
planets.append(asm.add(planet, "planet_gear", f"p{i}", color=STEEL))
|
|
|
|
carrier = asm.add(make_carrier(), "planet_carrier", color=AL_BRIGHT)
|
|
|
|
xr = make_cross_roller_bearing()
|
|
asm.feature(xr["inner"], "inner_ring", color=STEEL_RACE)
|
|
asm.feature(xr["outer"], "outer_ring", color=STEEL_RACE)
|
|
rollers = asm.feature(Compound(children=xr["rollers"]), "rollers", color=STEEL_ROLLER)
|
|
xroller = asm.add_module("cross_roller_bearing", [xr["inner"], xr["outer"], rollers])
|
|
|
|
ts = make_torque_sensor()
|
|
asm.feature(ts["flexure"], "flexure_disc", color=TITANIUM)
|
|
for i, gauge in enumerate(ts["gauges"], start=1):
|
|
asm.feature(gauge, "strain_gauge", f"g{i}", color=GAUGE_GOLD)
|
|
torque_sensor = asm.add_module("torque_sensor", [ts["flexure"], *ts["gauges"]])
|
|
|
|
flange = asm.add(make_output_flange(), "output_flange", color=AL_BRIGHT)
|
|
tube = asm.add(make_cable_tube(), "cable_tube", color=TUBE_BLACK)
|
|
|
|
by_name = {
|
|
"housing": housing,
|
|
"front_retainer": retainer,
|
|
"retainer_screws": retainer_screws,
|
|
"rear_cover": cover,
|
|
"connector_power": connectors["power"],
|
|
"connector_signal": connectors["signal"],
|
|
"driver_pcb": driver_pcb,
|
|
"encoder_pcb": encoder_pcb,
|
|
"encoder_magnet_ring": enc_magnet,
|
|
"stator": stator,
|
|
"rotor": rotor,
|
|
"rotor_bearing_rear": bearings["rear"],
|
|
"rotor_bearing_front": bearings["front"],
|
|
"sun_gear": sun,
|
|
"ring_gear": ring,
|
|
"planet_carrier": carrier,
|
|
"cross_roller_bearing": xroller,
|
|
"torque_sensor": torque_sensor,
|
|
"output_flange": flange,
|
|
"cable_tube": tube,
|
|
}
|
|
|
|
if explode:
|
|
for name, part in by_name.items():
|
|
offset = EXPLODE_OFFSETS.get(name, 0.0) * explode
|
|
if offset:
|
|
part.move(Location((0, 0, offset)))
|
|
for psi, planet in zip(PLANET_ANGLES_DEG, planets):
|
|
dx = PLANET_EXPLODE_RADIAL * explode * math.cos(math.radians(psi))
|
|
dy = PLANET_EXPLODE_RADIAL * explode * math.sin(math.radians(psi))
|
|
planet.move(Location((dx, dy, EXPLODE_OFFSETS["planet_gear"] * explode)))
|
|
else:
|
|
# Source mate frames + rigid connections (all identity by construction;
|
|
# connect() re-derives placement from the named datums and records the
|
|
# relationships for the exported topology).
|
|
seat = asm.rigid_frame(housing, "stator_seat", Location((0, 0, STATOR_STACK_Z[0])))
|
|
asm.face_to_face(seat, asm.rigid_frame(stator, "stack_rear", Location((0, 0, STATOR_STACK_Z[0]))))
|
|
|
|
ring_seat = asm.rigid_frame(housing, "ring_gear_seat", Location((0, 0, GEAR_Z[0])))
|
|
asm.face_to_face(ring_seat, asm.rigid_frame(ring, "rim_rear", Location((0, 0, GEAR_Z[0]))))
|
|
|
|
cover_seat = asm.rigid_frame(housing, "cover_seat", Location((0, 0, BARREL_Z[0])))
|
|
asm.face_to_face(cover_seat, asm.rigid_frame(cover, "lip_face", Location((0, 0, BARREL_Z[0]))))
|
|
|
|
retainer_seat = asm.rigid_frame(housing, "retainer_seat", Location((0, 0, RETAINER_Z[0])))
|
|
asm.face_to_face(
|
|
retainer_seat, asm.rigid_frame(retainer, "ring_rear", Location((0, 0, RETAINER_Z[0])))
|
|
)
|
|
screw_seat = asm.rigid_frame(retainer, "screw_seats", Location((0, 0, RETAINER_Z[1])))
|
|
asm.face_to_face(
|
|
screw_seat,
|
|
asm.rigid_frame(retainer_screws, "head_seats", Location((0, 0, RETAINER_Z[1]))),
|
|
)
|
|
|
|
for kind, module in connectors.items():
|
|
az = math.radians(CONNECTOR_ANGLES[kind])
|
|
mount = Location(
|
|
(
|
|
CONNECTOR_CIRCLE_R * math.cos(az),
|
|
CONNECTOR_CIRCLE_R * math.sin(az),
|
|
COVER_PLATE_Z[0],
|
|
)
|
|
)
|
|
pad = asm.rigid_frame(cover, f"{kind}_pad", mount)
|
|
asm.face_to_face(pad, asm.rigid_frame(module, "mount_face", mount))
|
|
|
|
for name, module, z_seat in (
|
|
("driver_pcb", driver_pcb, DRV_STANDOFF_Z[0]),
|
|
("encoder_pcb", encoder_pcb, ENC_STANDOFF_Z[0]),
|
|
):
|
|
seat_frame = asm.rigid_frame(housing, f"{name}_seat", Location((0, 0, z_seat)))
|
|
asm.face_to_face(
|
|
seat_frame, asm.rigid_frame(module, "mount_plane", Location((0, 0, z_seat)))
|
|
)
|
|
|
|
coupling = asm.rigid_frame(rotor, "sun_coupling", Location((0, 0, ROTOR_FLANGE_Z[1])))
|
|
asm.face_to_face(
|
|
coupling, asm.rigid_frame(sun, "flange_rear", Location((0, 0, ROTOR_FLANGE_Z[1])))
|
|
)
|
|
magnet_seat = asm.rigid_frame(rotor, "encoder_magnet_seat", Location((0, 0, ENC_MAGNET_Z[0])))
|
|
asm.face_to_face(
|
|
magnet_seat,
|
|
asm.rigid_frame(enc_magnet, "ring_rear", Location((0, 0, ENC_MAGNET_Z[0]))),
|
|
)
|
|
|
|
sensor_seat = asm.rigid_frame(carrier, "sensor_seat", Location((0, 0, FLEXURE_HUB_Z[0])))
|
|
asm.face_to_face(
|
|
sensor_seat,
|
|
asm.rigid_frame(torque_sensor, "hub_rear", Location((0, 0, FLEXURE_HUB_Z[0]))),
|
|
)
|
|
rim_face = asm.rigid_frame(torque_sensor, "rim_face", Location((0, 0, FLEXURE_RIM_Z[1])))
|
|
asm.face_to_face(
|
|
rim_face, asm.rigid_frame(flange, "mount_rear", Location((0, 0, FLEXURE_RIM_Z[1])))
|
|
)
|
|
|
|
xr_seat = asm.rigid_frame(housing, "bearing_seat", Location((0, 0, XROLLER_Z[0])))
|
|
asm.face_to_face(
|
|
xr_seat, asm.rigid_frame(xroller, "outer_ring_rear", Location((0, 0, XROLLER_Z[0])))
|
|
)
|
|
|
|
tube_bore = asm.rigid_frame(cover, "tube_bore", Location((0, 0, COVER_PLATE_Z[0])))
|
|
asm.face_to_face(
|
|
tube_bore, asm.rigid_frame(tube, "rear_end", Location((0, 0, COVER_PLATE_Z[0])))
|
|
)
|
|
|
|
# Motion datums (joint frames only; static pose is authored explicitly):
|
|
asm.revolute_frame(housing, "rotor_axis", Axis((0, 0, 0), (0, 0, 1)))
|
|
asm.revolute_frame(housing, "output_axis", Axis((0, 0, XROLLER_Z[1]), (0, 0, 1)))
|
|
|
|
return asm.build()
|
|
|
|
|
|
def gen_step():
|
|
return build_actuator(explode=0.0)
|