first commit

This commit is contained in:
2026-07-22 13:48:46 +08:00
commit c87751c3dc
2820 changed files with 726976 additions and 0 deletions
@@ -0,0 +1,139 @@
# Case 20: Integrated 50 mm BLDC Joint Actuator
## Classification
Product-level kinematic assembly. The actuator contains separately manufactured
electrical, magnetic, bearing, gear, housing, and output parts. The fixed housing
is the ground; the rotor, two carriers, and six planets are rotating components.
## Design Intent
- Fit a real inner-rotor brushless motor, a 20:1 reducer, a circular controller
PCB, and serviceable wiring terminals into one coaxial 50 mm package.
- Eliminate the separate motor-to-reducer coupler. The rotor shaft and stage-1
sun are one steel solid.
- Preserve a short load path from the output flange through two adjacent output
bearings into a separate front bearing cap.
- Keep the stator and both ring gears as replaceable press-fit inserts instead of
hiding them inside an impossible one-piece enclosure.
- Reserve rear-facing connector apertures and PCB mounting holes so electronics
are not represented by an empty cosmetic volume.
## Envelope And Performance
| Item | Value |
|---|---:|
| Motor / housing outside diameter | 50.0 mm |
| Structural axial envelope | 75.8 mm |
| Terminal protrusion included | 77.3 mm |
| Motor topology | 12-slot / 14-pole inner rotor |
| Stator active length | 16.0 mm |
| Rotor magnetic length | 17.5 mm |
| Radial air gap | 0.30 mm |
| Stage 1 | 15/15/45 teeth, 4:1 |
| Stage 2 | 18/27/72 teeth, 5:1 |
| Total reduction | 20:1 |
| Reducer housing minimum cylindrical wall | 2.20 mm |
The two stages intentionally use different modules. Stage 1 uses module 0.80 to
leave enough root section around the 8 mm direct-drive shaft. Stage 2 uses module
0.55 to fit the 72-tooth, 5:1 ring inside the 50 mm housing. Both tooth sets obey
the three-planet equal-spacing condition `(sun teeth + ring teeth) mod 3 = 0`.
## Bill Of Materials
| Part / subassembly | Manufacturing intent | Material / connection |
|---|---|---|
| Main reducer housing | One machined fixed part | 6061-T6 aluminum |
| Motor shell | One machined fixed part | 6061-T6, six M3 front screws |
| Rear bearing spider | Separate machined part | 7075-T6, four M2.5 screws |
| Rear electronics cover | Separate machined part with PCB bosses | 6061-T6, four M2.5 screws |
| Output bearing cap | Separate machined part | 7075-T6, six M3 screws |
| Stator core | 12-slot laminated stack | Electrical steel, thermal press fit |
| Windings | Twelve separately represented coil packs | Copper, varnish/potting retained |
| Rotor shaft + stage-1 sun | One integrated machined solid | Hardened alloy steel |
| Rotor magnets | Fourteen bonded inserts | NdFeB |
| PCB | Circular controller board with real holes/notches | FR-4/copper |
| Phase terminal | Three-position rear-access terminal | High-temperature polymer |
| Power/CAN terminal | Four-position rear-access terminal | High-temperature polymer |
| Fixed ring gears | Two replaceable press-fit inserts | Case-hardened steel |
| Planet gears | Six gears with standard bearing seats | Case-hardened steel |
| Stage-1 carrier + stage-2 sun | One integrated interstage part | 7075 carrier / modeled as steel-duty part |
| Output carrier + flange | One integrated output part | 7075-T6 aluminum |
| Motor bearings | 8x16x5 and 8x19x6 | Standard ball bearings |
| Interstage bearing | 5x10x3 | Thin radial ball bearing in fixed divider |
| Planet bearings | Six 3x6x3 bearings | Standard ball bearings |
| Output bearings | Two 16x24x5 bearings | Standard ball bearings |
Fasteners are represented by matching holes and documented interfaces rather
than individual screw solids. This keeps the graph focused while retaining
manufacturable attachment geometry.
## Interface Table
| Constraint / interface | Motion meaning | Real connection | Geometry and clearance |
|---|---|---|---|
| `motor_shell_to_reducer_housing` | Fixed | Six M3 screws | 43.0 mm PCD, 3.2 mm holes |
| `rear_spider_to_motor_shell` | Fixed | Four M2.5 screws | 40.6 mm PCD, 2.7 mm holes; face-to-face column/spider joint |
| `rear_cover_to_motor_shell` | Fixed | Four M2.5 screws | Shared rear columns and holes |
| `stator_to_motor_shell` | Fixed | Thermal press fit + potting | Nominal line-to-line CAD fit; tolerance sets interference |
| `electronics_to_rear_cover` | Fixed | Four M2 PCB screws | 33.0 mm PCD and integrated standoffs |
| `rotor_revolute` | Motor input rotation | Front/rear radial bearings | 8 mm shaft, 0.30 mm magnetic air gap |
| `stage1_ring_fixed` | Fixed ring | Interference fit and axial clamp | 0.04 mm diametral modeled interference |
| `stage1_carrier_revolute` | First reduction output | 5x10x3 radial bearing | Integrated 5 mm stage-2 sun shaft |
| `stage2_ring_fixed` | Fixed ring | Interference fit and axial clamp | 0.04 mm diametral modeled interference |
| `output_carrier_revolute` | Joint output rotation | Paired output bearings | 16 mm shaft in two 16x24x5 bearings |
| Planet revolutes | Planet spin | 3x6x3 bearings on 3 mm pins | 0.05 mm radial gear-seat clearance |
| Output cap to housing | Fixed | Six M3 screws | 43.0 mm PCD, 3.2 mm holes |
| Output link | External fixed attachment | Six M3 screws | 34.0 mm PCD tapped holes, Ø15.96 locating pilot |
## Electronics Packaging
The controller PCB is a 44.4 mm circular board behind the rear motor bearing. It
has a 10 mm center service bore, four M2 mounting holes, four large edge notches
for the rear structural columns, three phase-terminal pin holes, and four
power/CAN pin holes. Two rear-cover apertures expose the terminal bodies without
removing the controller. The board remains removable after the rear cover and
terminal screws are released.
## Assembly Order
1. Press the stator stack into the motor shell and pot the twelve winding packs.
2. Install the rear bearing into the four-arm spider and bolt the spider to the
shell's rear columns.
3. Insert the rotor/shaft from the reducer side and support it with the front
motor bearing in the reducer bulkhead.
4. Bolt the motor shell to the reducer housing with the six-hole front interface.
5. Insert the first fixed ring and planetary stage, the 5x10x3 interstage
bearing, then the second fixed ring and planetary stage from the open front.
6. Install the paired output bearings in the removable output cap, then bolt the
cap to the reducer housing.
7. Install the PCB and terminals on the rear-cover standoffs, connect phases and
sensors, and attach the rear cover.
This sequence avoids the trapped 43 mm ring-gear problem in Case 16: both ring
inserts and carriers enter through the open reducer front before the bearing cap
is installed.
## Strength And Thermal Notes
- The motor shell retains 1.80 mm radial wall around the stator and the reducer
shell retains 2.20 mm around the steel ring inserts.
- The 43 mm PCD case holes pass through 18.5 mm-radius end lands, retaining
1.4 mm of continuous aluminum ligament on the bore side of each M3 clearance
hole instead of clipping only the thin cylindrical shell.
- The front reducer bulkhead is 8 mm long around the 19 mm motor bearing.
- Two adjacent output bearings form a 10 mm stack with 5 mm center spacing to
distribute overturning load.
- The output flange leaves 4.15 mm radial ligament beyond the M3 tapped-hole edges.
- The output face carries a 1.5 mm-high, Ø15.96 locating pilot. Mating links use
the pilot for concentric location and six Ø3.3 clearance holes with Ø6 socket-
head counterbores; the screws provide clamp load rather than radial location.
- External robot structure clamps the continuous Ø50 reducer sleeve at the
`case_clamp_axis` datum (`Z = 20.0`) instead of sharing the internal output-cap
retention screws.
- The stator yoke contacts the aluminum shell over its full active length for a
direct thermal path; controller heat can flow through the rear standoffs and
cover.
- Detailed tooth stress, bearing life, winding thermal limits, rotor retention,
and fastener preload still require engineering calculation and prototype test.
@@ -0,0 +1 @@
"""Reusable implementation package for Example 20."""
@@ -0,0 +1,441 @@
"""Top-level integrated BLDC motor, controller, reducer, and housing assembly."""
from __future__ import annotations
import simplecadapi as scad
try:
from .bearings import (
make_coaxial_bearing_rplacement,
make_main_bearing_rassembly,
make_planet_bearing_rplacement,
make_standard_planet_bearing_rassembly,
)
from .common import connector_ref, ground_constraint_report
from .dimensions import (
FRONT_MOTOR_BEARING,
FRONT_MOTOR_BEARING_CENTER_Z,
INTERSTAGE_BEARING,
INTERSTAGE_BEARING_CENTER_Z,
OUTPUT_BEARING,
OUTPUT_BEARING_1_CENTER_Z,
OUTPUT_BEARING_2_CENTER_Z,
PLANET_BEARING,
PLANET_COUNT,
REAR_BEARING_CENTER_Z,
REAR_MOTOR_BEARING,
STAGE_1,
STAGE_2,
TOTAL_REDUCTION,
StageSpec,
)
from .electronics import make_integrated_controller_rassembly
from .gears import (
make_output_carrier_flange_rpart,
make_planet_rplacement,
make_stage1_carrier_sun_rpart,
make_stage_planet_gear_rpart,
make_stage_ring_gear_rpart,
)
from .housing import (
make_motor_shell_rpart,
make_output_bearing_cap_rpart,
make_rear_bearing_spider_rpart,
make_rear_electronics_cover_rpart,
make_reducer_housing_rpart,
)
from .motor import make_bldc_rotor_rassembly, make_bldc_stator_rassembly
except ImportError: # Support direct execution from this example directory.
from bearings import (
make_coaxial_bearing_rplacement,
make_main_bearing_rassembly,
make_planet_bearing_rplacement,
make_standard_planet_bearing_rassembly,
)
from common import connector_ref, ground_constraint_report
from dimensions import (
FRONT_MOTOR_BEARING,
FRONT_MOTOR_BEARING_CENTER_Z,
INTERSTAGE_BEARING,
INTERSTAGE_BEARING_CENTER_Z,
OUTPUT_BEARING,
OUTPUT_BEARING_1_CENTER_Z,
OUTPUT_BEARING_2_CENTER_Z,
PLANET_BEARING,
PLANET_COUNT,
REAR_BEARING_CENTER_Z,
REAR_MOTOR_BEARING,
STAGE_1,
STAGE_2,
TOTAL_REDUCTION,
StageSpec,
)
from electronics import make_integrated_controller_rassembly
from gears import (
make_output_carrier_flange_rpart,
make_planet_rplacement,
make_stage1_carrier_sun_rpart,
make_stage_planet_gear_rpart,
make_stage_ring_gear_rpart,
)
from housing import (
make_motor_shell_rpart,
make_output_bearing_cap_rpart,
make_rear_bearing_spider_rpart,
make_rear_electronics_cover_rpart,
make_reducer_housing_rpart,
)
from motor import make_bldc_rotor_rassembly, make_bldc_stator_rassembly
def make_integrated_bldc_joint_actuator_rassembly(
*, materials: dict[str, scad.Material]
) -> scad.Assembly:
"""Build and solve the complete compact 50 mm joint actuator."""
component_specs = make_integrated_bldc_joint_actuator_components_rtuple(
materials=materials
)
actuator = scad.make_assembly_rassembly(
assembly_id="integrated_50mm_bldc_joint_actuator",
name="50 mm 12-slot/14-pole BLDC joint actuator with 20:1 reducer and circular ESC",
)
for component_id, item, placement, name in component_specs:
actuator = scad.add_component_rassembly(
assembly=actuator,
item=item,
component_id=component_id,
placement=placement,
name=name,
)
actuator = _add_public_connectors_rassembly(assembly=actuator)
actuator = _add_constraints_rassembly(assembly=actuator)
actuator = scad.solve_assembly_constraints_rassembly(assembly=actuator, strict=True)
ground_constraint_report(label="actuator", assembly=actuator)
return actuator
def make_integrated_bldc_joint_actuator_components_rtuple(
*, materials: dict[str, scad.Material]
) -> tuple[tuple[str, scad.Part | scad.Assembly, scad.Placement, str], ...]:
"""Build the actuator component inventory without creating a parent assembly."""
print(
f"ratio_plan: stage1={STAGE_1.fixed_ring_ratio:.1f}:1 "
f"stage2={STAGE_2.fixed_ring_ratio:.1f}:1 total={TOTAL_REDUCTION:.1f}:1"
)
reducer_housing = make_reducer_housing_rpart(material=materials["housing"])
motor_shell = make_motor_shell_rpart(material=materials["housing"])
rear_spider = make_rear_bearing_spider_rpart(material=materials["carrier"])
rear_cover = make_rear_electronics_cover_rpart(material=materials["housing"])
output_cap = make_output_bearing_cap_rpart(material=materials["carrier"])
stator = make_bldc_stator_rassembly(
steel_material=materials["electrical_steel"],
copper_material=materials["copper"],
)
rotor = make_bldc_rotor_rassembly(
steel_material=materials["gear"],
magnet_material=materials["magnet"],
)
controller = make_integrated_controller_rassembly(
pcb_material=materials["pcb"],
terminal_material=materials["terminal"],
)
stage1_ring = make_stage_ring_gear_rpart(stage=STAGE_1, material=materials["gear"])
stage1_planet = make_stage_planet_gear_rpart(stage=STAGE_1, material=materials["gear"])
stage1_carrier = make_stage1_carrier_sun_rpart(material=materials["gear"])
stage2_ring = make_stage_ring_gear_rpart(stage=STAGE_2, material=materials["gear"])
stage2_planet = make_stage_planet_gear_rpart(stage=STAGE_2, material=materials["gear"])
output_carrier = make_output_carrier_flange_rpart(stage=STAGE_2, material=materials["carrier"])
rear_motor_bearing = make_main_bearing_rassembly(
bearing_id="rear_motor_8x16x5",
spec=REAR_MOTOR_BEARING,
material=materials["gear"],
)
front_motor_bearing = make_main_bearing_rassembly(
bearing_id="front_motor_8x19x6",
spec=FRONT_MOTOR_BEARING,
material=materials["gear"],
)
interstage_bearing = make_main_bearing_rassembly(
bearing_id="interstage_5x10x3",
spec=INTERSTAGE_BEARING,
material=materials["gear"],
)
planet_bearing = make_standard_planet_bearing_rassembly(
bearing_id="planet_3x6x3",
spec=PLANET_BEARING,
material=materials["gear"],
)
output_bearing = make_main_bearing_rassembly(
bearing_id="output_16x24x5",
spec=OUTPUT_BEARING,
material=materials["gear"],
)
fixed_components = (
("reducer_housing", reducer_housing, scad.identity_placement_rplacement(), "Fixed reducer housing"),
("motor_shell", motor_shell, scad.identity_placement_rplacement(), "Fixed BLDC shell"),
("rear_bearing_spider", rear_spider, scad.identity_placement_rplacement(), "Rear motor-bearing spider"),
("rear_electronics_cover", rear_cover, scad.identity_placement_rplacement(), "Rear controller cover"),
("output_bearing_cap", output_cap, scad.identity_placement_rplacement(), "Output bearing cap"),
("stator", stator, scad.identity_placement_rplacement(), "12-slot fixed stator"),
("rotor", rotor, scad.identity_placement_rplacement(), "14-pole rotor and direct sun shaft"),
("controller", controller, scad.identity_placement_rplacement(), "Circular integrated controller"),
("stage1_carrier", stage1_carrier, scad.identity_placement_rplacement(), "Stage 1 carrier and stage 2 sun"),
("output_carrier", output_carrier, scad.identity_placement_rplacement(), "Stage 2 carrier and output flange"),
("stage1_ring", stage1_ring, _stage_rplacement(stage=STAGE_1), "Stage 1 fixed ring insert"),
("stage2_ring", stage2_ring, _stage_rplacement(stage=STAGE_2), "Stage 2 fixed ring insert"),
)
print(f"actuator_base_components: count={len(fixed_components)}")
planet_components = []
for stage, planet in ((STAGE_1, stage1_planet), (STAGE_2, stage2_planet)):
for index in range(PLANET_COUNT):
planet_components.append(
(
f"{stage.stage_id}_planet_{index + 1}",
planet,
make_planet_rplacement(stage=stage, index=index),
f"{stage.label} planet {index + 1}",
)
)
bearing_components = (
(
"rear_motor_bearing",
rear_motor_bearing,
make_coaxial_bearing_rplacement(center_z=REAR_BEARING_CENTER_Z),
"Rear rotor bearing",
),
(
"front_motor_bearing",
front_motor_bearing,
make_coaxial_bearing_rplacement(center_z=FRONT_MOTOR_BEARING_CENTER_Z),
"Front rotor bearing",
),
(
"interstage_bearing",
interstage_bearing,
make_coaxial_bearing_rplacement(center_z=INTERSTAGE_BEARING_CENTER_Z),
"Stage 1 carrier support bearing",
),
(
"output_bearing_1",
output_bearing,
make_coaxial_bearing_rplacement(center_z=OUTPUT_BEARING_1_CENTER_Z),
"Rear output bearing",
),
(
"output_bearing_2",
output_bearing,
make_coaxial_bearing_rplacement(center_z=OUTPUT_BEARING_2_CENTER_Z),
"Front output bearing",
),
)
planet_bearing_components = []
for stage in (STAGE_1, STAGE_2):
for index in range(PLANET_COUNT):
planet_bearing_components.append(
(
f"{stage.stage_id}_planet_bearing_{index + 1}",
planet_bearing,
make_planet_bearing_rplacement(stage=stage, index=index),
f"{stage.label} planet bearing {index + 1}",
)
)
print(f"bearing_components: motor=2 interstage=1 output=2 planet={PLANET_COUNT * 2}")
return tuple(
[
*fixed_components,
*planet_components,
*bearing_components,
*planet_bearing_components,
]
)
def _add_public_connectors_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
forwarded = (
("case_clamp_axis", "reducer_housing", "case_clamp_axis", "External split-clamp datum"),
("case_mount_axis", "output_bearing_cap", "case_mount_axis", "Fixed actuator case datum"),
("output_link_axis", "output_carrier", "output_link_axis", "Rotating six-hole output flange"),
("phase_terminal_access", "controller", "phase_access", "Rear phase-terminal service datum"),
("power_can_terminal_access", "controller", "power_can_access", "Rear power/CAN service datum"),
)
for connector_id, source_component_id, source_connector_id, name in forwarded:
assembly = scad.forward_connector_rassembly(
assembly=assembly,
connector_id=connector_id,
source_component_id=source_component_id,
source_connector_id=source_connector_id,
name=name,
offset=None,
)
print("actuator_public_connectors: " + ",".join(item[0] for item in forwarded))
return assembly
def _add_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="reducer_housing")
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="stage1_ring")
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="stage2_ring")
fixed_pairs = (
("motor_shell_to_reducer_housing", "reducer_housing", "motor_mount_axis", "motor_shell", "reducer_mount_axis"),
("rear_spider_to_motor_shell", "motor_shell", "rear_spider_axis", "rear_bearing_spider", "shell_axis"),
("rear_cover_to_motor_shell", "motor_shell", "rear_cover_axis", "rear_electronics_cover", "shell_axis"),
("stator_to_motor_shell", "motor_shell", "stator_axis", "stator", "shell_axis"),
("controller_to_rear_cover", "rear_electronics_cover", "pcb_axis", "controller", "cover_axis"),
("stage1_ring_fixed", "reducer_housing", "stage1_ring_axis", "stage1_ring", "axis"),
("stage2_ring_fixed", "reducer_housing", "stage2_ring_axis", "stage2_ring", "axis"),
("output_cap_to_reducer_housing", "reducer_housing", "output_cap_axis", "output_bearing_cap", "housing_axis"),
)
for constraint_id, a_component, a_connector, b_component, b_connector in fixed_pairs:
assembly = scad.add_fixed_constraint_rassembly(
assembly=assembly,
constraint_id=constraint_id,
connector_a=connector_ref(component_id=a_component, connector_id=a_connector),
connector_b=connector_ref(component_id=b_component, connector_id=b_connector),
name=constraint_id.replace("_", " "),
)
primary_revolutes = (
("rotor_revolute", "reducer_housing", "front_motor_bearing_axis", "rotor", "front_bearing_axis"),
("stage1_carrier_revolute", "reducer_housing", "stage1_carrier_axis", "stage1_carrier", "carrier_axis"),
("output_carrier_revolute", "reducer_housing", "stage2_carrier_axis", "output_carrier", "carrier_axis"),
)
for constraint_id, a_component, a_connector, b_component, b_connector in primary_revolutes:
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=constraint_id,
connector_a=connector_ref(component_id=a_component, connector_id=a_connector),
connector_b=connector_ref(component_id=b_component, connector_id=b_connector),
drive_angle_degrees=0.0,
angle_limit=None,
name=constraint_id.replace("_", " "),
)
assembly = _add_stage_constraints_rassembly(
assembly=assembly,
stage=STAGE_1,
sun_component="rotor",
sun_connector="front_bearing_axis",
ring_component="stage1_ring",
carrier_component="stage1_carrier",
)
assembly = _add_stage_constraints_rassembly(
assembly=assembly,
stage=STAGE_2,
sun_component="stage1_carrier",
sun_connector="carrier_axis",
ring_component="stage2_ring",
carrier_component="output_carrier",
)
assembly = _add_bearing_constraints_rassembly(assembly=assembly)
print("actuator_constraints: fixed=8 primary_revolute=3 planet_revolute=6 gear=6 internal=6 bearing_interfaces=22")
return assembly
def _add_stage_constraints_rassembly(
*,
assembly: scad.Assembly,
stage: StageSpec,
sun_component: str,
sun_connector: str,
ring_component: str,
carrier_component: str,
) -> scad.Assembly:
for index in range(PLANET_COUNT):
planet_component = f"{stage.stage_id}_planet_{index + 1}"
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=f"{planet_component}_revolute",
connector_a=connector_ref(component_id=carrier_component, connector_id=f"planet_{index + 1}_axis"),
connector_b=connector_ref(component_id=planet_component, connector_id="axis"),
drive_angle_degrees=None,
angle_limit=None,
name=f"{stage.label} planet {index + 1} bearing axis",
)
assembly = scad.add_gear_constraint_rassembly(
assembly=assembly,
constraint_id=f"{stage.stage_id}_sun_planet_{index + 1}_mesh",
connector_a=connector_ref(component_id=sun_component, connector_id=sun_connector),
connector_b=connector_ref(component_id=planet_component, connector_id="axis"),
pitch_radius_a=stage.sun_pitch_radius,
pitch_radius_b=stage.planet_pitch_radius,
phase_offset=None,
name=f"{stage.label} sun to planet {index + 1} external mesh",
)
assembly = scad.add_belt_constraint_rassembly(
assembly=assembly,
constraint_id=f"{stage.stage_id}_ring_planet_{index + 1}_internal_mesh",
connector_a=connector_ref(component_id=ring_component, connector_id="axis"),
connector_b=connector_ref(component_id=planet_component, connector_id="axis"),
pulley_radius_a=stage.ring_pitch_radius,
pulley_radius_b=stage.planet_pitch_radius,
phase_offset=None,
name=f"{stage.label} fixed-ring to planet {index + 1} internal mesh",
)
print(
f"{stage.stage_id}_mesh: sun_r={stage.sun_pitch_radius:.3f} "
f"planet_r={stage.planet_pitch_radius:.3f} center={stage.planet_center_radius:.3f}"
)
return assembly
def _add_bearing_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
interfaces = (
("rear_bearing_outer_to_spider", "rear_bearing_spider", "bearing_axis", "rear_motor_bearing", "outer_axis"),
("rear_bearing_inner_to_rotor", "rotor", "rear_bearing_axis", "rear_motor_bearing", "inner_axis"),
("front_bearing_outer_to_housing", "reducer_housing", "front_motor_bearing_axis", "front_motor_bearing", "outer_axis"),
("front_bearing_inner_to_rotor", "rotor", "front_bearing_axis", "front_motor_bearing", "inner_axis"),
("interstage_bearing_outer_to_housing", "reducer_housing", "interstage_bearing_axis", "interstage_bearing", "outer_axis"),
("interstage_bearing_inner_to_carrier", "stage1_carrier", "interstage_bearing_axis", "interstage_bearing", "inner_axis"),
("output_bearing_1_outer_to_cap", "output_bearing_cap", "bearing_1_axis", "output_bearing_1", "outer_axis"),
("output_bearing_1_inner_to_carrier", "output_carrier", "bearing_1_axis", "output_bearing_1", "inner_axis"),
("output_bearing_2_outer_to_cap", "output_bearing_cap", "bearing_2_axis", "output_bearing_2", "outer_axis"),
("output_bearing_2_inner_to_carrier", "output_carrier", "bearing_2_axis", "output_bearing_2", "inner_axis"),
)
for constraint_id, a_component, a_connector, b_component, b_connector in interfaces:
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=constraint_id,
connector_a=connector_ref(component_id=a_component, connector_id=a_connector),
connector_b=connector_ref(component_id=b_component, connector_id=b_connector),
drive_angle_degrees=None,
angle_limit=None,
name=constraint_id.replace("_", " "),
)
for stage, carrier_component in ((STAGE_1, "stage1_carrier"), (STAGE_2, "output_carrier")):
for index in range(PLANET_COUNT):
planet = f"{stage.stage_id}_planet_{index + 1}"
bearing = f"{stage.stage_id}_planet_bearing_{index + 1}"
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=f"{bearing}_outer_to_planet",
connector_a=connector_ref(component_id=planet, connector_id="bearing_axis"),
connector_b=connector_ref(component_id=bearing, connector_id="outer_axis"),
drive_angle_degrees=None,
angle_limit=None,
name=f"{stage.label} planet {index + 1} bearing outer-ring fit",
)
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=f"{bearing}_inner_to_pin",
connector_a=connector_ref(
component_id=carrier_component,
connector_id=f"planet_{index + 1}_bearing_axis",
),
connector_b=connector_ref(component_id=bearing, connector_id="inner_axis"),
drive_angle_degrees=None,
angle_limit=None,
name=f"{stage.label} planet {index + 1} bearing inner-ring pin fit",
)
return assembly
def _stage_rplacement(*, stage: StageSpec) -> scad.Placement:
return scad.make_placement_rplacement(origin=(0.0, 0.0, stage.bottom_z))
@@ -0,0 +1,174 @@
"""Standard bearing factories and coaxial/planet placements."""
from __future__ import annotations
import simplecadapi as scad
if __package__:
from .common import make_annulus_rsolid, make_axis_part_rpart, make_z_rotation_rplacement
from .dimensions import BearingSpec, PLANET_COUNT, StageSpec
from .gears import planet_center_xy
else:
from common import make_annulus_rsolid, make_axis_part_rpart, make_z_rotation_rplacement
from dimensions import BearingSpec, PLANET_COUNT, StageSpec
from gears import planet_center_xy
def make_standard_planet_bearing_rassembly(
*,
bearing_id: str,
spec: BearingSpec,
material: scad.Material,
) -> scad.Assembly:
"""Create the reused catalog-style planet ball bearing assembly."""
bearing = make_main_bearing_rassembly(
bearing_id=bearing_id,
spec=spec,
material=material,
)
print(
f"bearing_{bearing_id}: bore={spec.bore_diameter:.1f} od={spec.outer_diameter:.1f} "
f"width={spec.width:.1f} balls={spec.ball_count} material={material.material_id}"
)
return bearing
def make_main_bearing_rassembly(
*,
bearing_id: str,
spec: BearingSpec,
material: scad.Material,
) -> scad.Assembly:
"""Create a uniquely identified bearing when several catalog sizes share one graph."""
bore_radius = spec.bore_diameter / 2.0
outer_radius = spec.outer_diameter / 2.0
ball_radius = spec.ball_diameter / 2.0
pitch_radius = (bore_radius + outer_radius) / 2.0
inner_outer_radius = pitch_radius - ball_radius * 0.55
outer_inner_radius = pitch_radius + ball_radius * 0.55
inner_ring = make_annulus_rsolid(
outer_radius=inner_outer_radius,
inner_radius=bore_radius,
bottom_z=-spec.width / 2.0,
height=spec.width,
tags=("role.bearing_inner_ring",),
)
outer_ring = make_annulus_rsolid(
outer_radius=outer_radius,
inner_radius=outer_inner_radius,
bottom_z=-spec.width / 2.0,
height=spec.width,
tags=("role.bearing_outer_ring",),
)
ball = scad.make_sphere_rsolid(
radius=ball_radius,
center=(pitch_radius, 0.0, 0.0),
)
inner_part = make_axis_part_rpart(
part_id=f"{bearing_id}_inner_ring",
body=inner_ring,
name=f"{bearing_id} inner race",
material=material,
connectors=(("axis", (0.0, 0.0, 0.0), "Inner-ring axis"),),
)
outer_part = make_axis_part_rpart(
part_id=f"{bearing_id}_outer_ring",
body=outer_ring,
name=f"{bearing_id} outer race",
material=material,
connectors=(("axis", (0.0, 0.0, 0.0), "Outer-ring axis"),),
)
ball_part = make_axis_part_rpart(
part_id=f"{bearing_id}_reusable_ball",
body=ball,
name=f"{bearing_id} reusable rolling element",
material=material,
connectors=(),
)
bearing = scad.make_assembly_rassembly(
assembly_id=bearing_id,
name=f"Ball bearing {spec.bore_diameter:g}x{spec.outer_diameter:g}x{spec.width:g}",
)
bearing = scad.add_component_rassembly(
assembly=bearing,
item=outer_part,
component_id="outer_ring",
placement=scad.identity_placement_rplacement(),
name="Outer ring",
)
bearing = scad.add_component_rassembly(
assembly=bearing,
item=inner_part,
component_id="inner_ring",
placement=scad.identity_placement_rplacement(),
name="Inner ring",
)
for index in range(spec.ball_count):
angle = 360.0 * index / spec.ball_count
bearing = scad.add_component_rassembly(
assembly=bearing,
item=ball_part,
component_id=f"ball_{index + 1:02d}",
placement=make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle),
name=f"Rolling element {index + 1}",
)
bearing = scad.add_revolute_constraint_rassembly(
assembly=bearing,
constraint_id="inner_outer_revolute",
connector_a=scad.make_connector_ref_rconnectorref(
component_id="outer_ring",
connector_id="axis",
),
connector_b=scad.make_connector_ref_rconnectorref(
component_id="inner_ring",
connector_id="axis",
),
drive_angle_degrees=None,
angle_limit=None,
name="Inner ring rotation in outer ring",
)
for connector_id, component_id in (("outer_axis", "outer_ring"), ("inner_axis", "inner_ring")):
bearing = scad.forward_connector_rassembly(
assembly=bearing,
connector_id=connector_id,
source_component_id=component_id,
source_connector_id="axis",
name=connector_id.replace("_", " "),
offset=None,
)
radial_wall = (spec.outer_diameter - spec.bore_diameter) / 2.0 - spec.ball_diameter
axial_margin = spec.width - spec.ball_diameter
print(
f"bearing_{bearing_id}: bore={spec.bore_diameter:.1f} od={spec.outer_diameter:.1f} "
f"width={spec.width:.1f} balls={spec.ball_count} radial_wall={radial_wall:.2f} "
f"axial_margin={axial_margin:.2f}"
)
return bearing
def make_coaxial_bearing_rplacement(*, center_z: float) -> scad.Placement:
"""Place a standard bearing center plane on the actuator Z axis."""
return make_z_rotation_rplacement(origin=(0.0, 0.0, center_z), angle_degrees=0.0)
def make_planet_bearing_rplacement(
*,
stage: StageSpec,
index: int,
) -> scad.Placement:
"""Place a standard planet bearing at the gear midplane."""
if index < 0 or index >= PLANET_COUNT:
raise ValueError(f"planet bearing index out of range: {index}")
center = planet_center_xy(stage=stage, index=index)
print(
f"{stage.stage_id}_planet_bearing_{index + 1}: "
f"center=({center[0]:.3f},{center[1]:.3f},{stage.mid_z:.3f})"
)
return make_z_rotation_rplacement(
origin=(center[0], center[1], stage.mid_z),
angle_degrees=0.0,
)
@@ -0,0 +1,164 @@
"""Shared construction, tagging, connector, and grounding helpers."""
from __future__ import annotations
import math
from collections.abc import Iterable
import simplecadapi as scad
from simplecadapi import ql
def apply_tags(*, shape: scad.Solid, tags: Iterable[str]) -> scad.Solid:
"""Apply semantic tags through the public functional API."""
tagged = shape
for tag in tags:
tagged = scad.apply_tag(shape=tagged, tag=tag)
return tagged
def make_annulus_rsolid(
*,
outer_radius: float,
inner_radius: float,
bottom_z: float,
height: float,
tags: Iterable[str],
) -> scad.Solid:
"""Create a strict single-solid annular cylinder."""
outer = scad.make_cylinder_rsolid(
radius=outer_radius,
height=height,
bottom_face_center=(0.0, 0.0, bottom_z),
axis=(0.0, 0.0, 1.0),
)
bore = scad.make_cylinder_rsolid(
radius=inner_radius,
height=height + 2.0,
bottom_face_center=(0.0, 0.0, bottom_z - 1.0),
axis=(0.0, 0.0, 1.0),
)
annulus = scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
return apply_tags(shape=annulus, tags=tags)
def make_axis_part_rpart(
*,
part_id: str,
body: scad.Solid,
name: str,
material: scad.Material,
connectors: Iterable[tuple[str, tuple[float, float, float], str]],
) -> scad.Part:
"""Create a single-body part with stable placement-based axis datums."""
part = scad.make_part_rpart(part_id=part_id, body=body, name=name)
part = scad.assign_material_rpart(part=part, material=material)
connector_count = 0
for connector_id, origin, connector_name in connectors:
connector = scad.make_placement_connector_rconnector(
connector_id=connector_id,
placement=scad.make_placement_rplacement(origin=origin),
name=connector_name,
)
part = scad.add_connector_rpart(part=part, connector=connector)
connector_count += 1
ground_solid(label=part_id, solid=body)
print(f"part_{part_id}: connectors={connector_count} material={material.material_id}")
return part
def make_z_rotation_rplacement(
*,
origin: tuple[float, float, float],
angle_degrees: float,
) -> scad.Placement:
"""Return a right-handed placement rotated about Z."""
angle = math.radians(angle_degrees)
return scad.make_placement_rplacement(
origin=origin,
x_axis=(math.cos(angle), math.sin(angle), 0.0),
y_axis=(-math.sin(angle), math.cos(angle), 0.0),
)
def radial_centers(*, count: int, radius: float, angle_offset: float = 0.0):
"""Yield index, angle in degrees, and XY center on a bolt/pole circle."""
for index in range(count):
angle_degrees = angle_offset + 360.0 * index / count
angle = math.radians(angle_degrees)
yield index, angle_degrees, (radius * math.cos(angle), radius * math.sin(angle))
def make_axial_hole_cutters_rsolids(
*,
count: int,
pcd: float,
hole_radius: float,
bottom_z: float,
height: float,
angle_offset: float = 0.0,
) -> list[scad.Solid]:
"""Create equally spaced axial hole cutters."""
cutters = []
for _index, _angle, center in radial_centers(
count=count,
radius=pcd / 2.0,
angle_offset=angle_offset,
):
cutters.append(
scad.make_cylinder_rsolid(
radius=hole_radius,
height=height,
bottom_face_center=(center[0], center[1], bottom_z),
axis=(0.0, 0.0, 1.0),
)
)
return cutters
def ground_solid(*, label: str, solid: scad.Solid) -> None:
"""Print a concise QL-backed solid summary."""
faces = ql.select(items=solid.get_faces()).all()
role_faces = ql.select(items=faces).where(ql.tag(pattern="role.*")).all()
print(
f"{label}: faces={len(faces)} role_faces={len(role_faces)} "
f"volume={solid.get_volume():.3f} tags={','.join(scad.list_tags(shape=solid))}"
)
def ground_compound(*, label: str, compound: scad.Compound) -> None:
"""Print a concise QL-backed assembly projection summary."""
solids = ql.select(items=compound.get_solids()).all()
faces = sum(len(ql.select(items=solid.get_faces()).all()) for solid in solids)
volume = sum(solid.get_volume() for solid in solids)
print(f"{label}: solids={len(solids)} faces={faces} volume={volume:.3f}")
def connector_ref(*, component_id: str, connector_id: str) -> scad.ConnectorRef:
"""Create a component-scoped connector reference."""
return scad.make_connector_ref_rconnectorref(
component_id=component_id,
connector_id=connector_id,
)
def ground_constraint_report(*, label: str, assembly: scad.Assembly) -> None:
"""Print solved state and only non-zero residual facts."""
report = scad.inspect_assembly_constraints_rconstraintreport(assembly=assembly)
worst_translation = max((item.translation_error for item in report.residuals), default=0.0)
worst_angle = max((item.angular_error_degrees for item in report.residuals), default=0.0)
print(
f"{label}_constraints: solved={report.solved} components={len(assembly.component_ids())} "
f"constraints={len(assembly.constraint_ids())} unsolved={len(report.unsolved_component_ids)} "
f"max_translation={worst_translation:.6g} max_angle={worst_angle:.6g}"
)
@@ -0,0 +1,235 @@
"""Design constants for the integrated 50 mm BLDC joint actuator."""
from __future__ import annotations
from dataclasses import dataclass
PACKAGE_RADIUS = 25.0
PACKAGE_STRUCTURAL_BOTTOM_Z = -37.0
PACKAGE_TOP_Z = 40.3
MOTOR_SLOT_COUNT = 12
MOTOR_POLE_COUNT = 14
MOTOR_SHELL_INNER_RADIUS = 23.20
MOTOR_SHELL_BOTTOM_Z = -34.0
MOTOR_SHELL_TOP_Z = -6.0
MOTOR_STATOR_BOTTOM_Z = -24.5
MOTOR_STATOR_TOP_Z = -8.5
MOTOR_STATOR_OUTER_RADIUS = 23.20
MOTOR_STATOR_YOKE_INNER_RADIUS = 20.20
MOTOR_STATOR_TOOTH_INNER_RADIUS = 15.00
MOTOR_STATOR_TOOTH_WIDTH = 2.60
MOTOR_ROTOR_BOTTOM_Z = -25.0
MOTOR_ROTOR_TOP_Z = -7.5
MOTOR_ROTOR_BACKIRON_RADIUS = 13.50
MOTOR_MAGNET_OUTER_RADIUS = 14.70
MOTOR_MAGNET_TANGENTIAL_WIDTH = 5.0
MOTOR_SHAFT_RADIUS = 4.0
MOTOR_AIR_GAP = MOTOR_STATOR_TOOTH_INNER_RADIUS - MOTOR_MAGNET_OUTER_RADIUS
REAR_COVER_BOTTOM_Z = -37.0
REAR_COVER_THICKNESS = 3.0
PCB_BOTTOM_Z = -33.6
PCB_THICKNESS = 1.6
PCB_RADIUS = 22.2
PCB_CENTER_BORE_RADIUS = 5.0
PCB_STANDOFF_PCD = 33.0
PCB_MOUNT_HOLE_RADIUS = 1.10
REAR_COLUMN_PCD = 40.6
REAR_COLUMN_RADIUS = 3.2
REAR_SPIDER_BOSS_RADIUS = 2.9
REAR_FASTENER_HOLE_RADIUS = 1.35
REAR_BEARING_CENTER_Z = -29.0
FRONT_MOTOR_BEARING_CENTER_Z = -2.5
REDUCER_HOUSING_BOTTOM_Z = -6.0
REDUCER_HOUSING_FRONT_Z = 24.3
REDUCER_HOUSING_INNER_RADIUS = 22.80
RING_INSERT_OUTER_RADIUS = 22.82
MOTOR_INTERFACE_PCD = 43.0
OUTPUT_CAP_INTERFACE_PCD = 43.0
M3_CLEARANCE_RADIUS = 1.60
HOUSING_INTERFACE_LAND_INNER_RADIUS = 18.50
PLANET_COUNT = 3
PRESSURE_ANGLE = 20.0
HELIX_ANGLE = 24.0
BACKLASH = 0.03
ADDENDUM_FACTOR = 1.0
CLEARANCE_FACTOR = 0.25
RING_RIM_THICKNESS = 1.80
RING_SUPPORT_OVERLAP = 0.30
GEAR_HEIGHT = 5.50
STAGE1_CARRIER_BOTTOM_Z = 8.20
STAGE1_CARRIER_THICKNESS = 2.50
STAGE1_PIN_RADIUS = 1.48
STAGE1_PIN_BOTTOM_Z = 2.25
STAGE1_HUB_RADIUS = 4.2
STAGE1_PAD_RADIUS = 4.0
STAGE1_ARM_WIDTH = 3.2
INTERSTAGE_SHAFT_RADIUS = 2.50
STAGE2_CARRIER_BOTTOM_Z = 20.20
STAGE2_CARRIER_THICKNESS = 3.0
STAGE2_PIN_RADIUS = 1.48
STAGE2_PIN_BOTTOM_Z = 14.25
STAGE2_HUB_RADIUS = 8.8
STAGE2_PAD_RADIUS = 4.1
STAGE2_ARM_WIDTH = 3.5
OUTPUT_SHAFT_RADIUS = 7.98
OUTPUT_CAP_BOTTOM_Z = 24.3
OUTPUT_CAP_CARTRIDGE_TOP_Z = 34.8
OUTPUT_CAP_TOP_Z = 36.3
OUTPUT_BEARING_1_CENTER_Z = 26.8
OUTPUT_BEARING_2_CENTER_Z = 31.8
OUTPUT_FLANGE_BOTTOM_Z = 35.3
OUTPUT_FLANGE_TOP_Z = 38.8
OUTPUT_FLANGE_RADIUS = 22.4
OUTPUT_LINK_HOLE_PCD = 34.0
OUTPUT_LINK_BOLT_COUNT = 6
OUTPUT_LINK_BOLT_ANGLES_DEGREES = (30.0, 90.0, 150.0, 210.0, 270.0, 330.0)
OUTPUT_LINK_TAP_RADIUS = 1.25
OUTPUT_LINK_THREAD_DEPTH = 3.0
OUTPUT_REGISTER_RADIUS = OUTPUT_SHAFT_RADIUS
OUTPUT_REGISTER_HEIGHT = PACKAGE_TOP_Z - OUTPUT_FLANGE_TOP_Z
OUTPUT_CASE_CLAMP_CENTER_Z = 20.0
@dataclass(frozen=True)
class StageSpec:
"""One fixed-ring planetary stage."""
stage_id: str
label: str
module: float
sun_teeth: int
planet_teeth: int
bottom_z: float
@property
def ring_teeth(self) -> int:
return self.sun_teeth + 2 * self.planet_teeth
@property
def top_z(self) -> float:
return self.bottom_z + GEAR_HEIGHT
@property
def mid_z(self) -> float:
return self.bottom_z + GEAR_HEIGHT / 2.0
@property
def sun_pitch_radius(self) -> float:
return self.module * self.sun_teeth / 2.0
@property
def planet_pitch_radius(self) -> float:
return self.module * self.planet_teeth / 2.0
@property
def ring_pitch_radius(self) -> float:
return self.module * self.ring_teeth / 2.0
@property
def planet_center_radius(self) -> float:
return self.sun_pitch_radius + self.planet_pitch_radius
@property
def fixed_ring_ratio(self) -> float:
return 1.0 + self.ring_teeth / self.sun_teeth
@property
def ring_outer_radius(self) -> float:
return (
self.ring_pitch_radius
+ self.module * (ADDENDUM_FACTOR + CLEARANCE_FACTOR)
+ RING_RIM_THICKNESS
)
@dataclass(frozen=True)
class BearingSpec:
"""Catalog-style radial ball bearing dimensions."""
bore_diameter: float
outer_diameter: float
width: float
ball_diameter: float
ball_count: int
STAGE_1 = StageSpec(
stage_id="stage1",
label="Stage 1",
module=0.80,
sun_teeth=15,
planet_teeth=15,
bottom_z=2.0,
)
STAGE_2 = StageSpec(
stage_id="stage2",
label="Stage 2",
module=0.55,
sun_teeth=18,
planet_teeth=27,
bottom_z=14.0,
)
REAR_MOTOR_BEARING = BearingSpec(8.0, 16.0, 5.0, 2.0, 8)
FRONT_MOTOR_BEARING = BearingSpec(8.0, 19.0, 6.0, 2.4, 9)
INTERSTAGE_BEARING = BearingSpec(5.0, 10.0, 3.0, 1.0, 8)
PLANET_BEARING = BearingSpec(3.0, 6.0, 3.0, 0.7, 8)
OUTPUT_BEARING = BearingSpec(16.0, 24.0, 5.0, 2.5, 12)
REAR_SPIDER_BOTTOM_Z = REAR_BEARING_CENTER_Z - REAR_MOTOR_BEARING.width / 2.0
INTERSTAGE_BEARING_CENTER_Z = (
STAGE1_CARRIER_BOTTOM_Z + STAGE1_CARRIER_THICKNESS + STAGE_2.bottom_z
) / 2.0
TOTAL_REDUCTION = STAGE_1.fixed_ring_ratio * STAGE_2.fixed_ring_ratio
def validate_design_dimensions() -> None:
"""Fail early if a packaging or minimum-ligament invariant is broken."""
assert MOTOR_AIR_GAP >= 0.30
assert MOTOR_STATOR_OUTER_RADIUS == MOTOR_SHELL_INNER_RADIUS
assert abs(PACKAGE_RADIUS - MOTOR_SHELL_INNER_RADIUS - 1.80) < 1.0e-9
assert abs(PACKAGE_RADIUS - REDUCER_HOUSING_INNER_RADIUS - 2.20) < 1.0e-9
assert max(STAGE_1.ring_outer_radius, STAGE_2.ring_outer_radius) < RING_INSERT_OUTER_RADIUS
assert RING_INSERT_OUTER_RADIUS > REDUCER_HOUSING_INNER_RADIUS
assert (
MOTOR_INTERFACE_PCD / 2.0
- M3_CLEARANCE_RADIUS
- HOUSING_INTERFACE_LAND_INNER_RADIUS
>= 1.40 - 1.0e-9
)
assert REAR_SPIDER_BOSS_RADIUS - REAR_FASTENER_HOLE_RADIUS >= 1.50
assert REAR_COLUMN_RADIUS > REAR_SPIDER_BOSS_RADIUS
assert REAR_SPIDER_BOTTOM_Z > MOTOR_SHELL_BOTTOM_Z
assert OUTPUT_FLANGE_RADIUS <= PACKAGE_RADIUS
assert OUTPUT_LINK_HOLE_PCD / 2.0 + OUTPUT_LINK_TAP_RADIUS < OUTPUT_FLANGE_RADIUS
assert OUTPUT_LINK_THREAD_DEPTH < OUTPUT_FLANGE_TOP_Z - OUTPUT_FLANGE_BOTTOM_Z
assert OUTPUT_REGISTER_HEIGHT >= 1.5
assert OUTPUT_REGISTER_RADIUS < OUTPUT_LINK_HOLE_PCD / 2.0 - OUTPUT_LINK_TAP_RADIUS
assert REDUCER_HOUSING_BOTTOM_Z < OUTPUT_CASE_CLAMP_CENTER_Z < REDUCER_HOUSING_FRONT_Z
assert OUTPUT_BEARING_2_CENTER_Z - OUTPUT_BEARING_1_CENTER_Z == OUTPUT_BEARING.width
assert abs(TOTAL_REDUCTION - 20.0) < 1.0e-9
assert (STAGE_1.sun_teeth + STAGE_1.ring_teeth) % PLANET_COUNT == 0
assert (STAGE_2.sun_teeth + STAGE_2.ring_teeth) % PLANET_COUNT == 0
assert (
INTERSTAGE_BEARING_CENTER_Z - INTERSTAGE_BEARING.width / 2.0
> STAGE1_CARRIER_BOTTOM_Z + STAGE1_CARRIER_THICKNESS
)
assert (
INTERSTAGE_BEARING_CENTER_Z + INTERSTAGE_BEARING.width / 2.0
< STAGE_2.bottom_z
)
print(
"design_dimensions: "
f"diameter={PACKAGE_RADIUS * 2.0:.1f} structural_length="
f"{PACKAGE_TOP_Z - PACKAGE_STRUCTURAL_BOTTOM_Z:.1f} air_gap={MOTOR_AIR_GAP:.2f} "
f"ratio={TOTAL_REDUCTION:.1f}"
)
@@ -0,0 +1,270 @@
"""Circular integrated controller PCB, power stages, and rear terminals."""
from __future__ import annotations
import math
import simplecadapi as scad
try:
from .common import (
apply_tags,
connector_ref,
ground_constraint_report,
make_axial_hole_cutters_rsolids,
make_axis_part_rpart,
radial_centers,
)
from .dimensions import (
PCB_BOTTOM_Z,
PCB_CENTER_BORE_RADIUS,
PCB_MOUNT_HOLE_RADIUS,
PCB_RADIUS,
PCB_STANDOFF_PCD,
PCB_THICKNESS,
REAR_COLUMN_PCD,
)
except ImportError: # Support direct execution from this example directory.
from common import (
apply_tags,
connector_ref,
ground_constraint_report,
make_axial_hole_cutters_rsolids,
make_axis_part_rpart,
radial_centers,
)
from dimensions import (
PCB_BOTTOM_Z,
PCB_CENTER_BORE_RADIUS,
PCB_MOUNT_HOLE_RADIUS,
PCB_RADIUS,
PCB_STANDOFF_PCD,
PCB_THICKNESS,
REAR_COLUMN_PCD,
)
PHASE_TERMINAL_CENTER = (-11.0, 0.0)
POWER_CAN_TERMINAL_CENTER = (11.0, 0.0)
MOSFET_ANGLES = (22.5, 67.5, 112.5, 202.5, 247.5, 292.5)
def make_integrated_controller_rassembly(
*,
pcb_material: scad.Material,
terminal_material: scad.Material,
) -> scad.Assembly:
"""Build the circular ESC with six power devices and two service terminals."""
pcb = _make_controller_pcb_rpart(material=pcb_material)
mosfet = _make_mosfet_package_rpart(material=terminal_material)
phase_terminal = _make_terminal_block_rpart(
part_id="three_phase_terminal",
name="Three-position motor phase terminal",
width=8.0,
pin_count=3,
material=terminal_material,
)
power_terminal = _make_terminal_block_rpart(
part_id="power_can_terminal",
name="Four-position DC power and CAN terminal",
width=8.0,
pin_count=4,
material=terminal_material,
)
controller = scad.make_assembly_rassembly(
assembly_id="integrated_circular_motor_controller",
name="44.4 mm circular integrated BLDC controller",
)
controller = scad.add_component_rassembly(
assembly=controller,
item=pcb,
component_id="pcb",
placement=scad.identity_placement_rplacement(),
name="Circular controller PCB",
)
controller = scad.ground_component_rassembly(assembly=controller, component_id="pcb")
for index, angle in enumerate(MOSFET_ANGLES):
radians = math.radians(angle)
center = (13.2 * math.cos(radians), 13.2 * math.sin(radians))
component_id = f"mosfet_{index + 1}"
z = PCB_BOTTOM_Z + PCB_THICKNESS
controller = scad.add_component_rassembly(
assembly=controller,
item=mosfet,
component_id=component_id,
placement=scad.make_placement_rplacement(origin=(center[0], center[1], z)),
name=f"Power MOSFET package {index + 1}",
)
controller = scad.add_fixed_constraint_rassembly(
assembly=controller,
constraint_id=f"{component_id}_soldered",
connector_a=connector_ref(component_id="pcb", connector_id=component_id),
connector_b=connector_ref(component_id=component_id, connector_id="solder_axis"),
name=f"MOSFET {index + 1} solder attachment",
)
for component_id, item, center, connector_id in (
("phase_terminal", phase_terminal, PHASE_TERMINAL_CENTER, "phase_terminal"),
("power_can_terminal", power_terminal, POWER_CAN_TERMINAL_CENTER, "power_can_terminal"),
):
controller = scad.add_component_rassembly(
assembly=controller,
item=item,
component_id=component_id,
placement=scad.make_placement_rplacement(origin=(center[0], center[1], -38.5)),
name=item.name,
)
controller = scad.add_fixed_constraint_rassembly(
assembly=controller,
constraint_id=f"{component_id}_soldered",
connector_a=connector_ref(component_id="pcb", connector_id=connector_id),
connector_b=connector_ref(component_id=component_id, connector_id="solder_axis"),
name=f"{component_id.replace('_', ' ')} solder and screw retention",
)
for connector_id in ("cover_axis", "phase_access", "power_can_access"):
controller = scad.forward_connector_rassembly(
assembly=controller,
connector_id=connector_id,
source_component_id="pcb",
source_connector_id=connector_id,
name=connector_id.replace("_", " "),
offset=None,
)
controller = scad.solve_assembly_constraints_rassembly(assembly=controller, strict=True)
ground_constraint_report(label="controller", assembly=controller)
print("controller_packaging: pcb_d=44.4 mosfets=6 phase_pins=3 power_can_pins=4")
return controller
def _make_controller_pcb_rpart(*, material: scad.Material) -> scad.Part:
board = scad.make_cylinder_rsolid(
radius=PCB_RADIUS,
height=PCB_THICKNESS,
bottom_face_center=(0.0, 0.0, PCB_BOTTOM_Z),
axis=(0.0, 0.0, 1.0),
)
cutters: list[scad.Solid] = [
scad.make_cylinder_rsolid(
radius=PCB_CENTER_BORE_RADIUS,
height=PCB_THICKNESS + 2.0,
bottom_face_center=(0.0, 0.0, PCB_BOTTOM_Z - 1.0),
axis=(0.0, 0.0, 1.0),
)
]
cutters.extend(
make_axial_hole_cutters_rsolids(
count=4,
pcd=PCB_STANDOFF_PCD,
hole_radius=PCB_MOUNT_HOLE_RADIUS,
bottom_z=PCB_BOTTOM_Z - 1.0,
height=PCB_THICKNESS + 2.0,
angle_offset=45.0,
)
)
cutters.extend(
make_axial_hole_cutters_rsolids(
count=4,
pcd=REAR_COLUMN_PCD,
hole_radius=3.45,
bottom_z=PCB_BOTTOM_Z - 1.0,
height=PCB_THICKNESS + 2.0,
)
)
for x, count in ((PHASE_TERMINAL_CENTER[0], 3), (POWER_CAN_TERMINAL_CENTER[0], 4)):
for pin in range(count):
y = (pin - (count - 1) / 2.0) * 1.8
cutters.append(
scad.make_cylinder_rsolid(
radius=0.65,
height=PCB_THICKNESS + 2.0,
bottom_face_center=(x, y, PCB_BOTTOM_Z - 1.0),
axis=(0.0, 0.0, 1.0),
)
)
board = scad.cut_rsolid(board, cutters, skip_non_intersecting=False)
board = apply_tags(
shape=board,
tags=("role.circular_esc_pcb", "role.controller_mounting_holes", "group.integrated_electronics"),
)
connectors = [
("cover_axis", (0.0, 0.0, PCB_BOTTOM_Z + PCB_THICKNESS / 2.0), "Rear-cover PCB plane"),
("phase_terminal", (*PHASE_TERMINAL_CENTER, PCB_BOTTOM_Z), "Phase terminal solder datum"),
("power_can_terminal", (*POWER_CAN_TERMINAL_CENTER, PCB_BOTTOM_Z), "Power/CAN terminal solder datum"),
("phase_access", (*PHASE_TERMINAL_CENTER, -37.0), "Phase terminal service axis"),
("power_can_access", (*POWER_CAN_TERMINAL_CENTER, -37.0), "Power/CAN service axis"),
]
for index, angle in enumerate(MOSFET_ANGLES):
radians = math.radians(angle)
center = (13.2 * math.cos(radians), 13.2 * math.sin(radians))
connectors.append(
(
f"mosfet_{index + 1}",
(center[0], center[1], PCB_BOTTOM_Z + PCB_THICKNESS),
f"MOSFET {index + 1} solder datum",
)
)
print("pcb_holes: center=10.0 mount=4 column_notches=4 terminal_pins=7")
return make_axis_part_rpart(
part_id="circular_controller_pcb",
body=board,
name="44.4 mm circular ESC PCB with service cutouts",
material=material,
connectors=connectors,
)
def _make_mosfet_package_rpart(*, material: scad.Material) -> scad.Part:
package = scad.make_box_rsolid(
width=4.0,
height=3.0,
depth=1.4,
bottom_face_center=(0.0, 0.0, 0.0),
)
package = apply_tags(shape=package, tags=("role.power_mosfet", "group.three_phase_bridge"))
return make_axis_part_rpart(
part_id="reusable_power_mosfet",
body=package,
name="Reusable power MOSFET package",
material=material,
connectors=(("solder_axis", (0.0, 0.0, 0.0), "PCB solder plane"),),
)
def _make_terminal_block_rpart(
*,
part_id: str,
name: str,
width: float,
pin_count: int,
material: scad.Material,
) -> scad.Part:
body = scad.make_box_rsolid(
width=width,
height=6.0,
depth=4.9,
bottom_face_center=(0.0, 0.0, 0.0),
)
access_cutters = []
for pin in range(pin_count):
y = (pin - (pin_count - 1) / 2.0) * 1.8
access_cutters.append(
scad.make_cylinder_rsolid(
radius=0.75,
height=width + 2.0,
bottom_face_center=(-width / 2.0 - 1.0, y, 2.45),
axis=(1.0, 0.0, 0.0),
)
)
body = scad.cut_rsolid(body, access_cutters, skip_non_intersecting=False)
body = apply_tags(shape=body, tags=("role.rear_wiring_terminal", "role.service_access"))
print(f"terminal_{part_id}: pins={pin_count} access_holes={pin_count} width={width:.1f}")
return make_axis_part_rpart(
part_id=part_id,
body=body,
name=name,
material=material,
connectors=(("solder_axis", (0.0, 0.0, 4.9), "PCB solder and screw datum"),),
)
@@ -0,0 +1,441 @@
"""Herringbone ring, planet, carrier, and output parts for the 20:1 reducer."""
from __future__ import annotations
import math
import simplecadapi as scad
try:
from .common import (
apply_tags,
make_axis_part_rpart,
make_axial_hole_cutters_rsolids,
make_z_rotation_rplacement,
)
from .dimensions import (
ADDENDUM_FACTOR,
BACKLASH,
CLEARANCE_FACTOR,
GEAR_HEIGHT,
HELIX_ANGLE,
INTERSTAGE_BEARING_CENTER_Z,
INTERSTAGE_SHAFT_RADIUS,
OUTPUT_BEARING_1_CENTER_Z,
OUTPUT_BEARING_2_CENTER_Z,
OUTPUT_FLANGE_BOTTOM_Z,
OUTPUT_FLANGE_RADIUS,
OUTPUT_FLANGE_TOP_Z,
OUTPUT_LINK_BOLT_ANGLES_DEGREES,
OUTPUT_LINK_BOLT_COUNT,
OUTPUT_LINK_HOLE_PCD,
OUTPUT_LINK_TAP_RADIUS,
OUTPUT_LINK_THREAD_DEPTH,
OUTPUT_REGISTER_HEIGHT,
OUTPUT_SHAFT_RADIUS,
PLANET_BEARING,
PLANET_COUNT,
PRESSURE_ANGLE,
RING_INSERT_OUTER_RADIUS,
RING_RIM_THICKNESS,
RING_SUPPORT_OVERLAP,
STAGE1_ARM_WIDTH,
STAGE1_CARRIER_BOTTOM_Z,
STAGE1_CARRIER_THICKNESS,
STAGE1_HUB_RADIUS,
STAGE1_PAD_RADIUS,
STAGE1_PIN_BOTTOM_Z,
STAGE1_PIN_RADIUS,
STAGE2_ARM_WIDTH,
STAGE2_CARRIER_BOTTOM_Z,
STAGE2_CARRIER_THICKNESS,
STAGE2_HUB_RADIUS,
STAGE2_PAD_RADIUS,
STAGE2_PIN_BOTTOM_Z,
STAGE2_PIN_RADIUS,
STAGE_1,
STAGE_2,
StageSpec,
)
except ImportError: # Support direct execution from this example directory.
from common import (
apply_tags,
make_axis_part_rpart,
make_axial_hole_cutters_rsolids,
make_z_rotation_rplacement,
)
from dimensions import (
ADDENDUM_FACTOR,
BACKLASH,
CLEARANCE_FACTOR,
GEAR_HEIGHT,
HELIX_ANGLE,
INTERSTAGE_BEARING_CENTER_Z,
INTERSTAGE_SHAFT_RADIUS,
OUTPUT_BEARING_1_CENTER_Z,
OUTPUT_BEARING_2_CENTER_Z,
OUTPUT_FLANGE_BOTTOM_Z,
OUTPUT_FLANGE_RADIUS,
OUTPUT_FLANGE_TOP_Z,
OUTPUT_LINK_BOLT_ANGLES_DEGREES,
OUTPUT_LINK_BOLT_COUNT,
OUTPUT_LINK_HOLE_PCD,
OUTPUT_LINK_TAP_RADIUS,
OUTPUT_LINK_THREAD_DEPTH,
OUTPUT_REGISTER_HEIGHT,
OUTPUT_SHAFT_RADIUS,
PLANET_BEARING,
PLANET_COUNT,
PRESSURE_ANGLE,
RING_INSERT_OUTER_RADIUS,
RING_RIM_THICKNESS,
RING_SUPPORT_OVERLAP,
STAGE1_ARM_WIDTH,
STAGE1_CARRIER_BOTTOM_Z,
STAGE1_CARRIER_THICKNESS,
STAGE1_HUB_RADIUS,
STAGE1_PAD_RADIUS,
STAGE1_PIN_BOTTOM_Z,
STAGE1_PIN_RADIUS,
STAGE2_ARM_WIDTH,
STAGE2_CARRIER_BOTTOM_Z,
STAGE2_CARRIER_THICKNESS,
STAGE2_HUB_RADIUS,
STAGE2_PAD_RADIUS,
STAGE2_PIN_BOTTOM_Z,
STAGE2_PIN_RADIUS,
STAGE_1,
STAGE_2,
StageSpec,
)
def make_stage_ring_gear_rpart(
*,
stage: StageSpec,
material: scad.Material,
) -> scad.Part:
"""Create one herringbone ring insert with a full housing support rim."""
ring = scad.std.gear.make_herringbone_ring_gear_rsolid(
n_teeth=stage.ring_teeth,
module=stage.module,
pressure_angle=PRESSURE_ANGLE,
helix_angle=-HELIX_ANGLE,
gear_height=GEAR_HEIGHT,
rim_thickness=RING_RIM_THICKNESS,
backlash=BACKLASH,
addendum_factor=ADDENDUM_FACTOR,
clearance_factor=CLEARANCE_FACTOR,
)
support = scad.make_cylinder_rsolid(
radius=RING_INSERT_OUTER_RADIUS,
height=GEAR_HEIGHT,
bottom_face_center=(0.0, 0.0, 0.0),
axis=(0.0, 0.0, 1.0),
)
support_bore = scad.make_cylinder_rsolid(
radius=stage.ring_outer_radius - RING_SUPPORT_OVERLAP,
height=GEAR_HEIGHT + 2.0,
bottom_face_center=(0.0, 0.0, -1.0),
axis=(0.0, 0.0, 1.0),
)
support = scad.cut_rsolid(support, support_bore, skip_non_intersecting=False)
ring = scad.union_rsolid(ring, support, glue=False)
ring = apply_tags(
shape=ring,
tags=(f"role.{stage.stage_id}.fixed_ring_gear", "role.ring_gear_press_fit", "group.two_stage_reducer"),
)
print(
f"{stage.stage_id}_ring: teeth={stage.ring_teeth} pitch_r={stage.ring_pitch_radius:.3f} "
f"toothed_outer_r={stage.ring_outer_radius:.3f} insert_d={RING_INSERT_OUTER_RADIUS * 2.0:.2f}"
)
return make_axis_part_rpart(
part_id=f"{stage.stage_id}_fixed_ring",
body=ring,
name=f"{stage.label} replaceable fixed herringbone ring insert",
material=material,
connectors=(("axis", (0.0, 0.0, GEAR_HEIGHT / 2.0), "Fixed ring axis"),),
)
def make_stage_planet_gear_rpart(
*,
stage: StageSpec,
material: scad.Material,
) -> scad.Part:
"""Create one reusable herringbone planet with a standard-bearing seat."""
planet = scad.std.gear.make_herringbone_gear_rsolid(
n_teeth=stage.planet_teeth,
module=stage.module,
pressure_angle=PRESSURE_ANGLE,
helix_angle=-HELIX_ANGLE,
gear_height=GEAR_HEIGHT,
addendum_factor=ADDENDUM_FACTOR,
clearance_factor=CLEARANCE_FACTOR,
backlash=BACKLASH,
)
bearing_seat_radius = PLANET_BEARING.outer_diameter / 2.0 + 0.05
bearing_seat = scad.make_cylinder_rsolid(
radius=bearing_seat_radius,
height=GEAR_HEIGHT + 2.0,
bottom_face_center=(0.0, 0.0, -1.0),
axis=(0.0, 0.0, 1.0),
)
planet = scad.cut_rsolid(planet, bearing_seat, skip_non_intersecting=False)
planet = apply_tags(
shape=planet,
tags=(f"role.{stage.stage_id}.planet_gear", "role.planet_bearing_seat", "group.two_stage_reducer"),
)
root_radius = stage.planet_pitch_radius - stage.module * (ADDENDUM_FACTOR + CLEARANCE_FACTOR)
print(
f"{stage.stage_id}_planet: teeth={stage.planet_teeth} pitch_r={stage.planet_pitch_radius:.3f} "
f"bearing_seat_r={bearing_seat_radius:.3f} root_ligament={root_radius - bearing_seat_radius:.3f}"
)
return make_axis_part_rpart(
part_id=f"{stage.stage_id}_reusable_planet",
body=planet,
name=f"{stage.label} reusable bearing-supported planet",
material=material,
connectors=(
("axis", (0.0, 0.0, GEAR_HEIGHT / 2.0), "Planet spin axis"),
("bearing_axis", (0.0, 0.0, GEAR_HEIGHT / 2.0), "Planet bearing outer-ring axis"),
),
)
def make_stage1_carrier_sun_rpart(*, material: scad.Material) -> scad.Part:
"""Create the first carrier and integral second-stage sun/shaft."""
carrier = _make_carrier_body_rsolid(
stage=STAGE_1,
plate_bottom_z=STAGE1_CARRIER_BOTTOM_Z,
plate_thickness=STAGE1_CARRIER_THICKNESS,
pin_bottom_z=STAGE1_PIN_BOTTOM_Z,
pin_radius=STAGE1_PIN_RADIUS,
hub_radius=STAGE1_HUB_RADIUS,
arm_width=STAGE1_ARM_WIDTH,
pad_radius=STAGE1_PAD_RADIUS,
)
shaft = scad.make_cylinder_rsolid(
radius=INTERSTAGE_SHAFT_RADIUS,
height=STAGE_2.top_z - STAGE1_CARRIER_BOTTOM_Z + 0.1,
bottom_face_center=(0.0, 0.0, STAGE1_CARRIER_BOTTOM_Z - 0.05),
axis=(0.0, 0.0, 1.0),
)
stage2_sun = scad.std.gear.make_herringbone_gear_rsolid(
n_teeth=STAGE_2.sun_teeth,
module=STAGE_2.module,
pressure_angle=PRESSURE_ANGLE,
helix_angle=HELIX_ANGLE,
gear_height=GEAR_HEIGHT,
addendum_factor=ADDENDUM_FACTOR,
clearance_factor=CLEARANCE_FACTOR,
backlash=BACKLASH,
)
stage2_sun = scad.translate_shape(shape=stage2_sun, vector=(0.0, 0.0, STAGE_2.bottom_z))
carrier = scad.union_rsolid(carrier, shaft, stage2_sun, glue=False)
carrier = apply_tags(
shape=carrier,
tags=("role.stage1.planet_carrier", "role.stage2.sun_gear", "role.integral_interstage_drive", "group.two_stage_reducer"),
)
connectors = [
("carrier_axis", (0.0, 0.0, INTERSTAGE_BEARING_CENTER_Z), "Stage 1 carrier bearing axis"),
(
"interstage_bearing_axis",
(0.0, 0.0, INTERSTAGE_BEARING_CENTER_Z),
"Interstage bearing inner-ring seat",
),
("stage2_sun_axis", (0.0, 0.0, STAGE_2.mid_z), "Integral stage 2 sun axis"),
]
for index in range(PLANET_COUNT):
center = planet_center_xy(stage=STAGE_1, index=index)
connectors.extend(
(
(f"planet_{index + 1}_axis", (*center, STAGE_1.mid_z), f"Stage 1 planet {index + 1} axis"),
(f"planet_{index + 1}_bearing_axis", (*center, STAGE_1.mid_z), f"Stage 1 planet {index + 1} bearing pin"),
)
)
print(
f"stage1_carrier_sun: pins={PLANET_COUNT} shaft_d={INTERSTAGE_SHAFT_RADIUS * 2.0:.2f} "
f"stage2_sun_teeth={STAGE_2.sun_teeth}"
)
return make_axis_part_rpart(
part_id="stage1_carrier_integral_stage2_sun",
body=carrier,
name="Stage 1 carrier with integral stage-2 sun shaft",
material=material,
connectors=connectors,
)
def make_output_carrier_flange_rpart(
*,
stage: StageSpec,
material: scad.Material,
) -> scad.Part:
"""Create the second carrier, 16 mm bearing land, and output flange."""
carrier = _make_carrier_body_rsolid(
stage=stage,
plate_bottom_z=STAGE2_CARRIER_BOTTOM_Z,
plate_thickness=STAGE2_CARRIER_THICKNESS,
pin_bottom_z=STAGE2_PIN_BOTTOM_Z,
pin_radius=STAGE2_PIN_RADIUS,
hub_radius=STAGE2_HUB_RADIUS,
arm_width=STAGE2_ARM_WIDTH,
pad_radius=STAGE2_PAD_RADIUS,
)
shaft = scad.make_cylinder_rsolid(
radius=OUTPUT_SHAFT_RADIUS,
height=(
OUTPUT_FLANGE_TOP_Z
+ OUTPUT_REGISTER_HEIGHT
- STAGE2_CARRIER_BOTTOM_Z
+ 0.05
),
bottom_face_center=(0.0, 0.0, STAGE2_CARRIER_BOTTOM_Z - 0.05),
axis=(0.0, 0.0, 1.0),
)
flange = scad.make_cylinder_rsolid(
radius=OUTPUT_FLANGE_RADIUS,
height=OUTPUT_FLANGE_TOP_Z - OUTPUT_FLANGE_BOTTOM_Z,
bottom_face_center=(0.0, 0.0, OUTPUT_FLANGE_BOTTOM_Z),
axis=(0.0, 0.0, 1.0),
)
output = scad.union_rsolid(carrier, shaft, flange, glue=False)
output = scad.cut_rsolid(
output,
make_axial_hole_cutters_rsolids(
count=OUTPUT_LINK_BOLT_COUNT,
pcd=OUTPUT_LINK_HOLE_PCD,
hole_radius=OUTPUT_LINK_TAP_RADIUS,
bottom_z=OUTPUT_FLANGE_TOP_Z - OUTPUT_LINK_THREAD_DEPTH,
height=OUTPUT_LINK_THREAD_DEPTH + 1.0,
angle_offset=OUTPUT_LINK_BOLT_ANGLES_DEGREES[0],
),
skip_non_intersecting=False,
)
output = apply_tags(
shape=output,
tags=("role.stage2.output_carrier", "role.output_bearing_land", "role.output_link_flange", "group.two_stage_reducer"),
)
connectors = [
("carrier_axis", (0.0, 0.0, STAGE2_CARRIER_BOTTOM_Z + STAGE2_CARRIER_THICKNESS / 2.0), "Stage 2 output carrier axis"),
("bearing_1_axis", (0.0, 0.0, OUTPUT_BEARING_1_CENTER_Z), "Rear output bearing inner-ring seat"),
("bearing_2_axis", (0.0, 0.0, OUTPUT_BEARING_2_CENTER_Z), "Front output bearing inner-ring seat"),
("output_link_axis", (0.0, 0.0, OUTPUT_FLANGE_TOP_Z), "Six-hole driven-link flange"),
]
for index in range(PLANET_COUNT):
center = planet_center_xy(stage=stage, index=index)
connectors.extend(
(
(f"planet_{index + 1}_axis", (*center, stage.mid_z), f"Stage 2 planet {index + 1} axis"),
(f"planet_{index + 1}_bearing_axis", (*center, stage.mid_z), f"Stage 2 planet {index + 1} bearing pin"),
)
)
radial_ligament = OUTPUT_FLANGE_RADIUS - (OUTPUT_LINK_HOLE_PCD / 2.0 + OUTPUT_LINK_TAP_RADIUS)
print(
f"output_carrier_flange: shaft_d={OUTPUT_SHAFT_RADIUS * 2.0:.2f} "
f"pilot_h={OUTPUT_REGISTER_HEIGHT:.1f} tapped_holes={OUTPUT_LINK_BOLT_COUNT} "
f"pcd={OUTPUT_LINK_HOLE_PCD:.1f} thread_depth={OUTPUT_LINK_THREAD_DEPTH:.1f} "
f"radial_ligament={radial_ligament:.2f}"
)
return make_axis_part_rpart(
part_id="stage2_output_carrier_flange",
body=output,
name="Stage 2 carrier with paired-bearing shaft and output flange",
material=material,
connectors=connectors,
)
def make_planet_rplacement(*, stage: StageSpec, index: int) -> scad.Placement:
"""Place and visually phase one planet at its pitch center."""
center = planet_center_xy(stage=stage, index=index)
carrier_angle = 360.0 * index / PLANET_COUNT
spin = carrier_angle + 180.0 - 180.0 / stage.planet_teeth
print(
f"{stage.stage_id}_planet_{index + 1}_placement: center="
f"({center[0]:.3f},{center[1]:.3f},{stage.bottom_z:.3f}) spin={spin:.2f}"
)
return make_z_rotation_rplacement(
origin=(center[0], center[1], stage.bottom_z),
angle_degrees=spin,
)
def planet_center_xy(*, stage: StageSpec, index: int) -> tuple[float, float]:
"""Return one equally spaced planet pitch center."""
angle = math.radians(360.0 * index / PLANET_COUNT)
return (
stage.planet_center_radius * math.cos(angle),
stage.planet_center_radius * math.sin(angle),
)
def _make_carrier_body_rsolid(
*,
stage: StageSpec,
plate_bottom_z: float,
plate_thickness: float,
pin_bottom_z: float,
pin_radius: float,
hub_radius: float,
arm_width: float,
pad_radius: float,
) -> scad.Solid:
hub = scad.make_cylinder_rsolid(
radius=hub_radius,
height=plate_thickness,
bottom_face_center=(0.0, 0.0, plate_bottom_z),
axis=(0.0, 0.0, 1.0),
)
solids = [hub]
pin_height = plate_bottom_z + plate_thickness - pin_bottom_z
arm_inner_radius = hub_radius - 1.25
arm_outer_radius = stage.planet_center_radius + pad_radius - 0.25
arm_length = arm_outer_radius - arm_inner_radius
arm_center_radius = (arm_inner_radius + arm_outer_radius) / 2.0
for index in range(PLANET_COUNT):
angle = 360.0 * index / PLANET_COUNT
center = planet_center_xy(stage=stage, index=index)
arm = scad.make_box_rsolid(
width=arm_length,
height=arm_width,
depth=plate_thickness,
bottom_face_center=(arm_center_radius, 0.0, plate_bottom_z),
)
solids.append(
scad.rotate_shape(
shape=arm,
angle=angle,
axis=(0.0, 0.0, 1.0),
origin=(0.0, 0.0, 0.0),
)
)
solids.append(
scad.make_cylinder_rsolid(
radius=pad_radius,
height=plate_thickness,
bottom_face_center=(center[0], center[1], plate_bottom_z),
axis=(0.0, 0.0, 1.0),
)
)
solids.append(
scad.make_cylinder_rsolid(
radius=pin_radius,
height=pin_height,
bottom_face_center=(center[0], center[1], pin_bottom_z),
axis=(0.0, 0.0, 1.0),
)
)
carrier = scad.union_rsolid(solids, glue=False)
print(
f"{stage.stage_id}_carrier_body: arm_length={arm_length:.3f} "
f"hub_embed={hub_radius - arm_inner_radius:.3f} pin_height={pin_height:.3f}"
)
return carrier
@@ -0,0 +1,474 @@
"""Serviceable motor shell, reducer case, bearing caps, and electronics cover."""
from __future__ import annotations
import simplecadapi as scad
try:
from .common import (
apply_tags,
make_annulus_rsolid,
make_axis_part_rpart,
make_axial_hole_cutters_rsolids,
radial_centers,
)
from .dimensions import (
FRONT_MOTOR_BEARING,
FRONT_MOTOR_BEARING_CENTER_Z,
HOUSING_INTERFACE_LAND_INNER_RADIUS,
INTERSTAGE_BEARING,
INTERSTAGE_BEARING_CENTER_Z,
MOTOR_INTERFACE_PCD,
MOTOR_SHELL_BOTTOM_Z,
MOTOR_SHELL_INNER_RADIUS,
MOTOR_SHELL_TOP_Z,
MOTOR_STATOR_BOTTOM_Z,
MOTOR_STATOR_TOP_Z,
M3_CLEARANCE_RADIUS,
OUTPUT_BEARING_1_CENTER_Z,
OUTPUT_BEARING_2_CENTER_Z,
OUTPUT_CAP_BOTTOM_Z,
OUTPUT_CAP_CARTRIDGE_TOP_Z,
OUTPUT_CAP_INTERFACE_PCD,
OUTPUT_CAP_TOP_Z,
OUTPUT_CASE_CLAMP_CENTER_Z,
OUTPUT_FLANGE_RADIUS,
PACKAGE_RADIUS,
PCB_BOTTOM_Z,
PCB_STANDOFF_PCD,
REAR_BEARING_CENTER_Z,
REAR_COLUMN_PCD,
REAR_COLUMN_RADIUS,
REAR_COVER_BOTTOM_Z,
REAR_COVER_THICKNESS,
REAR_FASTENER_HOLE_RADIUS,
REAR_SPIDER_BOSS_RADIUS,
REAR_SPIDER_BOTTOM_Z,
REDUCER_HOUSING_BOTTOM_Z,
REDUCER_HOUSING_FRONT_Z,
REDUCER_HOUSING_INNER_RADIUS,
STAGE1_CARRIER_BOTTOM_Z,
STAGE2_CARRIER_BOTTOM_Z,
STAGE_1,
STAGE_2,
)
except ImportError: # Support direct execution from this example directory.
from common import (
apply_tags,
make_annulus_rsolid,
make_axial_hole_cutters_rsolids,
make_axis_part_rpart,
radial_centers,
)
from dimensions import (
FRONT_MOTOR_BEARING,
FRONT_MOTOR_BEARING_CENTER_Z,
HOUSING_INTERFACE_LAND_INNER_RADIUS,
INTERSTAGE_BEARING,
INTERSTAGE_BEARING_CENTER_Z,
M3_CLEARANCE_RADIUS,
MOTOR_INTERFACE_PCD,
MOTOR_SHELL_BOTTOM_Z,
MOTOR_SHELL_INNER_RADIUS,
MOTOR_SHELL_TOP_Z,
MOTOR_STATOR_BOTTOM_Z,
MOTOR_STATOR_TOP_Z,
OUTPUT_BEARING_1_CENTER_Z,
OUTPUT_BEARING_2_CENTER_Z,
OUTPUT_CAP_BOTTOM_Z,
OUTPUT_CAP_CARTRIDGE_TOP_Z,
OUTPUT_CAP_INTERFACE_PCD,
OUTPUT_CAP_TOP_Z,
OUTPUT_CASE_CLAMP_CENTER_Z,
OUTPUT_FLANGE_RADIUS,
PACKAGE_RADIUS,
PCB_BOTTOM_Z,
PCB_STANDOFF_PCD,
REAR_BEARING_CENTER_Z,
REAR_COLUMN_PCD,
REAR_COLUMN_RADIUS,
REAR_COVER_BOTTOM_Z,
REAR_COVER_THICKNESS,
REAR_FASTENER_HOLE_RADIUS,
REAR_SPIDER_BOSS_RADIUS,
REAR_SPIDER_BOTTOM_Z,
REDUCER_HOUSING_BOTTOM_Z,
REDUCER_HOUSING_FRONT_Z,
REDUCER_HOUSING_INNER_RADIUS,
STAGE1_CARRIER_BOTTOM_Z,
STAGE2_CARRIER_BOTTOM_Z,
STAGE_1,
STAGE_2,
)
def make_motor_shell_rpart(*, material: scad.Material) -> scad.Part:
"""Create the stator sleeve, front attachment land, and rear columns."""
sleeve = make_annulus_rsolid(
outer_radius=PACKAGE_RADIUS,
inner_radius=MOTOR_SHELL_INNER_RADIUS,
bottom_z=MOTOR_SHELL_BOTTOM_Z,
height=MOTOR_SHELL_TOP_Z - MOTOR_SHELL_BOTTOM_Z,
tags=("role.motor_shell", "role.stator_thermal_path"),
)
front_land = make_annulus_rsolid(
outer_radius=PACKAGE_RADIUS,
inner_radius=HOUSING_INTERFACE_LAND_INNER_RADIUS,
bottom_z=MOTOR_SHELL_TOP_Z - 1.4,
height=1.4,
tags=("role.motor_reducer_mount",),
)
columns = []
for _index, _angle, center in radial_centers(count=4, radius=REAR_COLUMN_PCD / 2.0):
columns.append(
scad.make_cylinder_rsolid(
radius=REAR_COLUMN_RADIUS,
height=REAR_SPIDER_BOTTOM_Z - MOTOR_SHELL_BOTTOM_Z,
bottom_face_center=(center[0], center[1], MOTOR_SHELL_BOTTOM_Z),
axis=(0.0, 0.0, 1.0),
)
)
shell = scad.union_rsolid(sleeve, front_land, columns, glue=False)
shell = scad.cut_rsolid(
shell,
make_axial_hole_cutters_rsolids(
count=6,
pcd=MOTOR_INTERFACE_PCD,
hole_radius=M3_CLEARANCE_RADIUS,
bottom_z=MOTOR_SHELL_TOP_Z - 2.8,
height=3.6,
angle_offset=30.0,
),
make_axial_hole_cutters_rsolids(
count=4,
pcd=REAR_COLUMN_PCD,
hole_radius=REAR_FASTENER_HOLE_RADIUS,
bottom_z=MOTOR_SHELL_BOTTOM_Z - 1.0,
height=10.4,
),
skip_non_intersecting=False,
)
shell = apply_tags(
shape=shell,
tags=("role.fixed_motor_housing", "group.integrated_bldc_actuator"),
)
print(
f"motor_shell_interface: stator_d={MOTOR_SHELL_INNER_RADIUS * 2.0:.2f} "
f"front_holes=6 rear_columns=4 wall={PACKAGE_RADIUS - MOTOR_SHELL_INNER_RADIUS:.2f}"
)
return make_axis_part_rpart(
part_id="motor_shell",
body=shell,
name="50 mm BLDC motor shell with rear structural columns",
material=material,
connectors=(
("reducer_mount_axis", (0.0, 0.0, MOTOR_SHELL_TOP_Z), "Six-screw reducer mount"),
(
"stator_axis",
(0.0, 0.0, (MOTOR_STATOR_BOTTOM_Z + MOTOR_STATOR_TOP_Z) / 2.0),
"Stator thermal press-fit axis",
),
("rear_spider_axis", (0.0, 0.0, REAR_SPIDER_BOTTOM_Z), "Rear bearing spider mount"),
("rear_cover_axis", (0.0, 0.0, MOTOR_SHELL_BOTTOM_Z), "Rear electronics cover mount"),
),
)
def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
"""Create the reducer sleeve and front motor-bearing bulkhead."""
sleeve = make_annulus_rsolid(
outer_radius=PACKAGE_RADIUS,
inner_radius=REDUCER_HOUSING_INNER_RADIUS,
bottom_z=REDUCER_HOUSING_BOTTOM_Z,
height=REDUCER_HOUSING_FRONT_Z - REDUCER_HOUSING_BOTTOM_Z,
tags=("role.reducer_housing_sleeve",),
)
bulkhead = make_annulus_rsolid(
outer_radius=23.10,
inner_radius=FRONT_MOTOR_BEARING.outer_diameter / 2.0 + 0.05,
bottom_z=REDUCER_HOUSING_BOTTOM_Z,
height=STAGE_1.bottom_z - REDUCER_HOUSING_BOTTOM_Z,
tags=("role.motor_front_bearing_bulkhead",),
)
interstage_divider = make_annulus_rsolid(
outer_radius=23.10,
inner_radius=INTERSTAGE_BEARING.outer_diameter / 2.0 + 0.05,
bottom_z=INTERSTAGE_BEARING_CENTER_Z - INTERSTAGE_BEARING.width / 2.0,
height=INTERSTAGE_BEARING.width,
tags=("role.interstage_bearing_divider",),
)
output_mount_land = make_annulus_rsolid(
outer_radius=PACKAGE_RADIUS,
inner_radius=HOUSING_INTERFACE_LAND_INNER_RADIUS,
bottom_z=REDUCER_HOUSING_FRONT_Z - 2.2,
height=2.2,
tags=("role.output_cap_mount_land",),
)
housing = scad.union_rsolid(
sleeve,
bulkhead,
interstage_divider,
output_mount_land,
glue=False,
)
housing = scad.cut_rsolid(
housing,
make_axial_hole_cutters_rsolids(
count=6,
pcd=MOTOR_INTERFACE_PCD,
hole_radius=M3_CLEARANCE_RADIUS,
bottom_z=REDUCER_HOUSING_BOTTOM_Z - 1.0,
height=STAGE_1.bottom_z - REDUCER_HOUSING_BOTTOM_Z + 2.0,
angle_offset=30.0,
),
make_axial_hole_cutters_rsolids(
count=6,
pcd=OUTPUT_CAP_INTERFACE_PCD,
hole_radius=M3_CLEARANCE_RADIUS,
bottom_z=REDUCER_HOUSING_FRONT_Z - 2.2,
height=3.2,
),
skip_non_intersecting=False,
)
housing = apply_tags(
shape=housing,
tags=("role.fixed_reducer_housing", "role.ring_gear_press_fit", "group.integrated_bldc_actuator"),
)
print(
f"reducer_housing: bore_d={REDUCER_HOUSING_INNER_RADIUS * 2.0:.2f} "
f"wall={PACKAGE_RADIUS - REDUCER_HOUSING_INNER_RADIUS:.2f} bulkhead=8.00 "
f"interstage_bearing_z={INTERSTAGE_BEARING_CENTER_Z:.2f} "
f"m3_inner_ligament={MOTOR_INTERFACE_PCD / 2.0 - M3_CLEARANCE_RADIUS - HOUSING_INTERFACE_LAND_INNER_RADIUS:.2f}"
)
return make_axis_part_rpart(
part_id="reducer_housing",
body=housing,
name="50 mm reducer housing with motor bearing bulkhead",
material=material,
connectors=(
("motor_mount_axis", (0.0, 0.0, MOTOR_SHELL_TOP_Z), "Motor shell six-screw interface"),
("front_motor_bearing_axis", (0.0, 0.0, FRONT_MOTOR_BEARING_CENTER_Z), "Front motor bearing seat"),
("stage1_ring_axis", (0.0, 0.0, STAGE_1.mid_z), "Stage 1 fixed ring seat"),
("stage1_carrier_axis", (0.0, 0.0, INTERSTAGE_BEARING_CENTER_Z), "Stage 1 carrier axis"),
("interstage_bearing_axis", (0.0, 0.0, INTERSTAGE_BEARING_CENTER_Z), "Interstage bearing outer seat"),
("stage2_ring_axis", (0.0, 0.0, STAGE_2.mid_z), "Stage 2 fixed ring seat"),
("stage2_carrier_axis", (0.0, 0.0, STAGE2_CARRIER_BOTTOM_Z + 1.50), "Output carrier axis"),
(
"case_clamp_axis",
(0.0, 0.0, OUTPUT_CASE_CLAMP_CENTER_Z),
"External split-clamp datum on reducer sleeve",
),
("output_cap_axis", (0.0, 0.0, REDUCER_HOUSING_FRONT_Z), "Output bearing cap interface"),
),
)
def make_rear_bearing_spider_rpart(*, material: scad.Material) -> scad.Part:
"""Create a four-arm removable rear motor-bearing support."""
bottom_z = REAR_SPIDER_BOTTOM_Z
hub = make_annulus_rsolid(
outer_radius=10.5,
inner_radius=8.05,
bottom_z=bottom_z,
height=5.0,
tags=("role.rear_motor_bearing_seat",),
)
solids = [hub]
for _index, angle, center in radial_centers(count=4, radius=REAR_COLUMN_PCD / 2.0):
arm = scad.make_box_rsolid(
width=12.0,
height=3.0,
depth=5.0,
bottom_face_center=(14.5, 0.0, bottom_z),
)
solids.append(
scad.rotate_shape(
shape=arm,
angle=angle,
axis=(0.0, 0.0, 1.0),
origin=(0.0, 0.0, 0.0),
)
)
solids.append(
scad.make_cylinder_rsolid(
radius=REAR_SPIDER_BOSS_RADIUS,
height=5.0,
bottom_face_center=(center[0], center[1], bottom_z),
axis=(0.0, 0.0, 1.0),
)
)
spider = scad.union_rsolid(solids, glue=False)
spider = scad.cut_rsolid(
spider,
make_axial_hole_cutters_rsolids(
count=4,
pcd=REAR_COLUMN_PCD,
hole_radius=REAR_FASTENER_HOLE_RADIUS,
bottom_z=bottom_z - 1.0,
height=7.0,
),
skip_non_intersecting=False,
)
spider = apply_tags(
shape=spider,
tags=("role.removable_rear_bearing_spider", "group.integrated_bldc_actuator"),
)
print("rear_bearing_spider: arms=4 bearing_seat_d=16.10 fasteners=4")
return make_axis_part_rpart(
part_id="rear_bearing_spider",
body=spider,
name="Four-arm removable rear motor-bearing spider",
material=material,
connectors=(
("shell_axis", (0.0, 0.0, REAR_SPIDER_BOTTOM_Z), "Motor shell column interface"),
("bearing_axis", (0.0, 0.0, REAR_BEARING_CENTER_Z), "Rear motor bearing outer seat"),
),
)
def make_rear_electronics_cover_rpart(*, material: scad.Material) -> scad.Part:
"""Create the rear cover with PCB standoffs and terminal apertures."""
cover = scad.make_cylinder_rsolid(
radius=PACKAGE_RADIUS,
height=REAR_COVER_THICKNESS,
bottom_face_center=(0.0, 0.0, REAR_COVER_BOTTOM_Z),
axis=(0.0, 0.0, 1.0),
)
standoffs = []
for _index, _angle, center in radial_centers(count=4, radius=PCB_STANDOFF_PCD / 2.0, angle_offset=45.0):
standoffs.append(
scad.make_cylinder_rsolid(
radius=2.4,
height=PCB_BOTTOM_Z - REAR_COVER_BOTTOM_Z - REAR_COVER_THICKNESS + 0.1,
bottom_face_center=(center[0], center[1], REAR_COVER_BOTTOM_Z + REAR_COVER_THICKNESS - 0.1),
axis=(0.0, 0.0, 1.0),
)
)
cover = scad.union_rsolid(cover, standoffs, glue=False)
phase_aperture = scad.make_box_rsolid(
width=9.2,
height=7.2,
depth=REAR_COVER_THICKNESS + 2.0,
bottom_face_center=(-11.0, 0.0, REAR_COVER_BOTTOM_Z - 1.0),
)
power_aperture = scad.make_box_rsolid(
width=9.2,
height=7.2,
depth=REAR_COVER_THICKNESS + 2.0,
bottom_face_center=(11.0, 0.0, REAR_COVER_BOTTOM_Z - 1.0),
)
center_service = scad.make_cylinder_rsolid(
radius=3.2,
height=REAR_COVER_THICKNESS + 2.0,
bottom_face_center=(0.0, 0.0, REAR_COVER_BOTTOM_Z - 1.0),
axis=(0.0, 0.0, 1.0),
)
cover = scad.cut_rsolid(
cover,
phase_aperture,
power_aperture,
center_service,
make_axial_hole_cutters_rsolids(
count=4,
pcd=REAR_COLUMN_PCD,
hole_radius=REAR_FASTENER_HOLE_RADIUS,
bottom_z=REAR_COVER_BOTTOM_Z - 1.0,
height=REAR_COVER_THICKNESS + 2.0,
),
make_axial_hole_cutters_rsolids(
count=4,
pcd=PCB_STANDOFF_PCD,
hole_radius=1.1,
bottom_z=REAR_COVER_BOTTOM_Z - 1.0,
height=PCB_BOTTOM_Z - REAR_COVER_BOTTOM_Z + 2.0,
angle_offset=45.0,
),
skip_non_intersecting=False,
)
cover = apply_tags(
shape=cover,
tags=("role.rear_electronics_cover", "role.terminal_access", "group.integrated_bldc_actuator"),
)
print("rear_cover_access: phase_opening=9.2x7.2 power_can_opening=9.2x7.2 pcb_holes=4")
return make_axis_part_rpart(
part_id="rear_electronics_cover",
body=cover,
name="Rear electronics cover with terminal access",
material=material,
connectors=(
("shell_axis", (0.0, 0.0, MOTOR_SHELL_BOTTOM_Z), "Four-screw motor shell interface"),
("pcb_axis", (0.0, 0.0, PCB_BOTTOM_Z + 0.8), "Controller PCB mounting plane"),
("phase_access", (-11.0, 0.0, REAR_COVER_BOTTOM_Z), "Three-phase terminal access"),
("power_can_access", (11.0, 0.0, REAR_COVER_BOTTOM_Z), "Power and CAN terminal access"),
),
)
def make_output_bearing_cap_rpart(*, material: scad.Material) -> scad.Part:
"""Create the removable paired-bearing cartridge and front cap."""
bearing_clearance_radius = 12.05
rear_flange = make_annulus_rsolid(
outer_radius=PACKAGE_RADIUS,
inner_radius=bearing_clearance_radius,
bottom_z=OUTPUT_CAP_BOTTOM_Z,
height=3.0,
tags=("role.output_cap_mount_flange",),
)
cartridge = make_annulus_rsolid(
outer_radius=15.0,
inner_radius=bearing_clearance_radius,
bottom_z=OUTPUT_CAP_BOTTOM_Z,
height=OUTPUT_CAP_CARTRIDGE_TOP_Z - OUTPUT_CAP_BOTTOM_Z + 0.1,
tags=("role.paired_output_bearing_seat",),
)
bearing_retainer = make_annulus_rsolid(
outer_radius=PACKAGE_RADIUS,
inner_radius=8.10,
bottom_z=OUTPUT_CAP_CARTRIDGE_TOP_Z - 0.1,
height=0.5,
tags=("role.output_axial_retainer",),
)
outer_lip = make_annulus_rsolid(
outer_radius=PACKAGE_RADIUS,
inner_radius=OUTPUT_FLANGE_RADIUS + 0.30,
bottom_z=OUTPUT_CAP_CARTRIDGE_TOP_Z - 0.1,
height=OUTPUT_CAP_TOP_Z - OUTPUT_CAP_CARTRIDGE_TOP_Z + 0.1,
tags=("role.output_labyrinth_lip",),
)
cap = scad.union_rsolid(rear_flange, cartridge, bearing_retainer, outer_lip, glue=False)
cap = scad.cut_rsolid(
cap,
make_axial_hole_cutters_rsolids(
count=6,
pcd=OUTPUT_CAP_INTERFACE_PCD,
hole_radius=M3_CLEARANCE_RADIUS,
bottom_z=OUTPUT_CAP_BOTTOM_Z - 1.0,
height=OUTPUT_CAP_TOP_Z - OUTPUT_CAP_BOTTOM_Z + 2.0,
),
skip_non_intersecting=False,
)
cap = apply_tags(
shape=cap,
tags=("role.removable_output_bearing_cap", "group.integrated_bldc_actuator"),
)
print(
f"output_bearing_cap: bearing_seat_d={bearing_clearance_radius * 2.0:.2f} "
f"bearing_span={OUTPUT_BEARING_2_CENTER_Z - OUTPUT_BEARING_1_CENTER_Z:.1f} fasteners=6"
)
return make_axis_part_rpart(
part_id="output_bearing_cap",
body=cap,
name="Paired output-bearing cartridge and removable cap",
material=material,
connectors=(
("housing_axis", (0.0, 0.0, OUTPUT_CAP_BOTTOM_Z), "Six-screw housing interface"),
("bearing_1_axis", (0.0, 0.0, OUTPUT_BEARING_1_CENTER_Z), "Rear output bearing seat"),
("bearing_2_axis", (0.0, 0.0, OUTPUT_BEARING_2_CENTER_Z), "Front output bearing seat"),
("case_mount_axis", (0.0, 0.0, OUTPUT_CAP_TOP_Z), "Fixed actuator case datum"),
),
)
@@ -0,0 +1,98 @@
"""Build, validate, replay, and export the integrated BLDC joint actuator."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import simplecadapi as scad
from assembly import make_integrated_bldc_joint_actuator_rassembly
from common import ground_compound
from dimensions import (
MOTOR_AIR_GAP,
MOTOR_POLE_COUNT,
MOTOR_SLOT_COUNT,
PACKAGE_RADIUS,
PACKAGE_STRUCTURAL_BOTTOM_Z,
PACKAGE_TOP_Z,
TOTAL_REDUCTION,
validate_design_dimensions,
)
from materials import make_actuator_materials_rdict
sys.setrecursionlimit(30000)
OUT_DIR = Path("examples/out/integrated_bldc_joint_actuator")
def build_integrated_bldc_joint_actuator():
"""Build the replayable actuator and return product and interchange outputs."""
validate_design_dimensions()
materials = make_actuator_materials_rdict()
with scad.GraphSession(graph_id="integrated_50mm_bldc_joint_actuator") as session:
assembly = make_integrated_bldc_joint_actuator_rassembly(materials=materials)
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
ground_compound(label="integrated_actuator_preview", compound=preview)
session_json = scad.export_session_json(session=session, indent=2)
model_json = scad.export_model_json(session=session, indent=2)
return assembly, preview, model_json, session_json
def main() -> None:
"""Generate canonical model JSON, STEP, and optional FreeCAD output."""
OUT_DIR.mkdir(parents=True, exist_ok=True)
model_path = OUT_DIR / "integrated_bldc_joint_actuator.model.json"
session_path = OUT_DIR / "integrated_bldc_joint_actuator.session.json"
step_path = OUT_DIR / "integrated_bldc_joint_actuator.step"
fcstd_path = OUT_DIR / "integrated_bldc_joint_actuator.FCStd"
if fcstd_path.exists():
fcstd_path.unlink()
assembly, preview, model_json, session_json = build_integrated_bldc_joint_actuator()
model_path.write_text(model_json, encoding="utf-8")
session_path.write_text(session_json, encoding="utf-8")
scad.export_step(shapes=preview, filename=str(step_path))
imported = scad.import_model_json(json_str=model_json)
replayed = scad.replay_model_json(json_str=model_json, strict=True)
payload = json.loads(model_json)
fcstd_status = "not attempted"
try:
scad.translator.freecad_translator.translate_model_json_to_fcstd(
json_str=model_json,
output_path=str(fcstd_path.resolve()),
document_name="Integrated50mmBLDCJointActuator",
freecad_cmd=None,
)
fcstd_status = f"{fcstd_path} ({fcstd_path.stat().st_size} bytes)"
except Exception as exc: # pragma: no cover - depends on local FreeCAD install
fcstd_status = f"skipped ({exc.__class__.__name__}: {exc})"
print(f"envelope_diameter={PACKAGE_RADIUS * 2.0:.1f}")
print(f"structural_length={PACKAGE_TOP_Z - PACKAGE_STRUCTURAL_BOTTOM_Z:.1f}")
print(f"motor_topology={MOTOR_SLOT_COUNT}_slot_{MOTOR_POLE_COUNT}_pole")
print(f"motor_air_gap={MOTOR_AIR_GAP:.2f}")
print(f"total_reduction={TOTAL_REDUCTION:.1f}")
print(f"assembly={assembly.assembly_id}")
print(f"components={len(assembly.component_ids())}")
print(f"constraints={len(assembly.constraint_ids())}")
print(f"preview_solids={len(preview.get_solids())}")
print(f"preview_volume={preview.get_volume():.3f}")
print(f"imported_keys={','.join(sorted(imported.keys()))}")
print(f"replay_outputs={len(replayed)}")
print("replay_types=" + ",".join(type(item).__name__ for item in replayed))
print(f"graph_nodes={len(payload['graph']['nodes'])}")
print(f"model={model_path}")
print(f"session={session_path}")
print(f"step={step_path}")
print(f"fcstd={fcstd_status}")
if __name__ == "__main__":
main()
@@ -0,0 +1,70 @@
"""Material records for structural, magnetic, and electronic parts."""
from __future__ import annotations
import simplecadapi as scad
def make_actuator_materials_rdict() -> dict[str, scad.Material]:
"""Create the materials used by Case 20."""
materials = {
"housing": scad.make_material_rmaterial(
material_id="aluminum_6061_t6",
name="Hard-anodized 6061-T6 aluminum",
density=2.70e-6,
density_unit="kg/mm^3",
color=(0.12, 0.15, 0.18),
),
"carrier": scad.make_material_rmaterial(
material_id="aluminum_7075_t6",
name="7075-T6 aluminum",
density=2.81e-6,
density_unit="kg/mm^3",
color=(0.72, 0.74, 0.78),
),
"gear": scad.make_material_rmaterial(
material_id="case_hardened_gear_steel",
name="Case-hardened alloy gear steel",
density=7.85e-6,
density_unit="kg/mm^3",
color=(0.46, 0.50, 0.56),
),
"electrical_steel": scad.make_material_rmaterial(
material_id="laminated_electrical_steel",
name="Laminated electrical steel",
density=7.65e-6,
density_unit="kg/mm^3",
color=(0.20, 0.27, 0.35),
),
"copper": scad.make_material_rmaterial(
material_id="enameled_copper",
name="Enameled copper winding",
density=8.96e-6,
density_unit="kg/mm^3",
color=(0.78, 0.27, 0.06),
),
"magnet": scad.make_material_rmaterial(
material_id="ndfeb_n42sh",
name="NdFeB N42SH magnet",
density=7.50e-6,
density_unit="kg/mm^3",
color=(0.14, 0.34, 0.75),
),
"pcb": scad.make_material_rmaterial(
material_id="fr4_copper_laminate",
name="FR-4 copper laminate",
density=1.85e-6,
density_unit="kg/mm^3",
color=(0.03, 0.42, 0.16),
),
"terminal": scad.make_material_rmaterial(
material_id="high_temperature_terminal_polymer",
name="High-temperature connector polymer",
density=1.45e-6,
density_unit="kg/mm^3",
color=(0.88, 0.70, 0.16),
),
}
print("materials: " + ",".join(sorted(materials)))
return materials
@@ -0,0 +1,378 @@
"""True 12-slot/14-pole BLDC stator and direct-drive rotor assemblies."""
from __future__ import annotations
import simplecadapi as scad
try:
from .common import (
apply_tags,
connector_ref,
ground_constraint_report,
make_annulus_rsolid,
make_axis_part_rpart,
make_z_rotation_rplacement,
radial_centers,
)
from .dimensions import (
ADDENDUM_FACTOR,
BACKLASH,
CLEARANCE_FACTOR,
GEAR_HEIGHT,
HELIX_ANGLE,
MOTOR_MAGNET_OUTER_RADIUS,
MOTOR_MAGNET_TANGENTIAL_WIDTH,
MOTOR_POLE_COUNT,
MOTOR_ROTOR_BACKIRON_RADIUS,
MOTOR_ROTOR_BOTTOM_Z,
MOTOR_ROTOR_TOP_Z,
MOTOR_SHAFT_RADIUS,
MOTOR_SHELL_INNER_RADIUS,
MOTOR_SLOT_COUNT,
MOTOR_STATOR_BOTTOM_Z,
MOTOR_STATOR_OUTER_RADIUS,
MOTOR_STATOR_TOOTH_INNER_RADIUS,
MOTOR_STATOR_TOOTH_WIDTH,
MOTOR_STATOR_TOP_Z,
MOTOR_STATOR_YOKE_INNER_RADIUS,
PRESSURE_ANGLE,
REAR_BEARING_CENTER_Z,
STAGE_1,
)
except ImportError: # Support direct execution from this example directory.
from common import (
apply_tags,
connector_ref,
ground_constraint_report,
make_annulus_rsolid,
make_axis_part_rpart,
make_z_rotation_rplacement,
radial_centers,
)
from dimensions import (
ADDENDUM_FACTOR,
BACKLASH,
CLEARANCE_FACTOR,
GEAR_HEIGHT,
HELIX_ANGLE,
MOTOR_MAGNET_OUTER_RADIUS,
MOTOR_MAGNET_TANGENTIAL_WIDTH,
MOTOR_POLE_COUNT,
MOTOR_ROTOR_BACKIRON_RADIUS,
MOTOR_ROTOR_BOTTOM_Z,
MOTOR_ROTOR_TOP_Z,
MOTOR_SHAFT_RADIUS,
MOTOR_SHELL_INNER_RADIUS,
MOTOR_SLOT_COUNT,
MOTOR_STATOR_BOTTOM_Z,
MOTOR_STATOR_OUTER_RADIUS,
MOTOR_STATOR_TOOTH_INNER_RADIUS,
MOTOR_STATOR_TOOTH_WIDTH,
MOTOR_STATOR_TOP_Z,
MOTOR_STATOR_YOKE_INNER_RADIUS,
PRESSURE_ANGLE,
REAR_BEARING_CENTER_Z,
STAGE_1,
)
def make_bldc_stator_rassembly(
*,
steel_material: scad.Material,
copper_material: scad.Material,
) -> scad.Assembly:
"""Build a laminated 12-slot stator and twelve fixed winding packs."""
core = _make_stator_core_rpart(material=steel_material)
winding = _make_winding_pack_rpart(material=copper_material)
stator = scad.make_assembly_rassembly(
assembly_id="bldc_12_slot_stator",
name="12-slot laminated stator with discrete copper slot packs",
)
stator = scad.add_component_rassembly(
assembly=stator,
item=core,
component_id="stator_core",
placement=scad.identity_placement_rplacement(),
name="Laminated stator core",
)
stator = scad.ground_component_rassembly(assembly=stator, component_id="stator_core")
for index, angle, _center in radial_centers(
count=MOTOR_SLOT_COUNT,
radius=0.0,
angle_offset=0.0,
):
component_id = f"winding_{index + 1:02d}"
stator = scad.add_component_rassembly(
assembly=stator,
item=winding,
component_id=component_id,
placement=make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle),
name=f"Slot winding pack {index + 1}",
)
stator = scad.add_fixed_constraint_rassembly(
assembly=stator,
constraint_id=f"{component_id}_potted_to_core",
connector_a=connector_ref(component_id="stator_core", connector_id=component_id),
connector_b=connector_ref(component_id=component_id, connector_id="mount_axis"),
name=f"Winding {index + 1} varnish and potting retention",
)
stator = scad.forward_connector_rassembly(
assembly=stator,
connector_id="shell_axis",
source_component_id="stator_core",
source_connector_id="shell_axis",
name="Stator press-fit axis",
offset=None,
)
stator = scad.solve_assembly_constraints_rassembly(assembly=stator, strict=True)
ground_constraint_report(label="stator", assembly=stator)
return stator
def make_bldc_rotor_rassembly(
*,
steel_material: scad.Material,
magnet_material: scad.Material,
) -> scad.Assembly:
"""Build the rotor, bonded magnets, shaft, and integrated stage-1 sun."""
core = _make_rotor_shaft_sun_rpart(material=steel_material)
magnet = _make_rotor_magnet_rpart(material=magnet_material)
rotor = scad.make_assembly_rassembly(
assembly_id="direct_coupled_bldc_rotor",
name="14-pole BLDC rotor with integrated stage-1 sun shaft",
)
rotor = scad.add_component_rassembly(
assembly=rotor,
item=core,
component_id="rotor_core_shaft_sun",
placement=scad.identity_placement_rplacement(),
name="Rotor back iron, shaft, and stage-1 sun",
)
rotor = scad.ground_component_rassembly(assembly=rotor, component_id="rotor_core_shaft_sun")
for index, angle, _center in radial_centers(count=MOTOR_POLE_COUNT, radius=0.0):
component_id = f"magnet_{index + 1:02d}"
rotor = scad.add_component_rassembly(
assembly=rotor,
item=magnet,
component_id=component_id,
placement=make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle),
name=f"Bonded rotor magnet {index + 1}",
)
rotor = scad.add_fixed_constraint_rassembly(
assembly=rotor,
constraint_id=f"{component_id}_bonded_to_rotor",
connector_a=connector_ref(component_id="rotor_core_shaft_sun", connector_id=component_id),
connector_b=connector_ref(component_id=component_id, connector_id="bond_axis"),
name=f"Magnet {index + 1} adhesive and sleeve retention",
)
for connector_id in (
"rotor_axis",
"rear_bearing_axis",
"front_bearing_axis",
"stage1_sun_axis",
):
rotor = scad.forward_connector_rassembly(
assembly=rotor,
connector_id=connector_id,
source_component_id="rotor_core_shaft_sun",
source_connector_id=connector_id,
name=connector_id.replace("_", " "),
offset=None,
)
rotor = scad.solve_assembly_constraints_rassembly(assembly=rotor, strict=True)
ground_constraint_report(label="rotor", assembly=rotor)
print(
f"rotor_direct_coupling: shaft_d={MOTOR_SHAFT_RADIUS * 2.0:.1f} "
f"magnets={MOTOR_POLE_COUNT} stage1_sun_teeth={STAGE_1.sun_teeth}"
)
return rotor
def _make_stator_core_rpart(*, material: scad.Material) -> scad.Part:
yoke = make_annulus_rsolid(
outer_radius=MOTOR_STATOR_OUTER_RADIUS,
inner_radius=MOTOR_STATOR_YOKE_INNER_RADIUS,
bottom_z=MOTOR_STATOR_BOTTOM_Z,
height=MOTOR_STATOR_TOP_Z - MOTOR_STATOR_BOTTOM_Z,
tags=("role.stator_back_iron",),
)
tooth_length = MOTOR_STATOR_YOKE_INNER_RADIUS - MOTOR_STATOR_TOOTH_INNER_RADIUS + 0.40
tooth_center_radius = MOTOR_STATOR_TOOTH_INNER_RADIUS + tooth_length / 2.0
teeth = []
for _index, angle, _center in radial_centers(count=MOTOR_SLOT_COUNT, radius=0.0):
tooth = scad.make_box_rsolid(
width=tooth_length,
height=MOTOR_STATOR_TOOTH_WIDTH,
depth=MOTOR_STATOR_TOP_Z - MOTOR_STATOR_BOTTOM_Z,
bottom_face_center=(tooth_center_radius, 0.0, MOTOR_STATOR_BOTTOM_Z),
)
teeth.append(
scad.rotate_shape(
shape=tooth,
angle=angle,
axis=(0.0, 0.0, 1.0),
origin=(0.0, 0.0, 0.0),
)
)
core = scad.union_rsolid(yoke, teeth, glue=False)
core = apply_tags(
shape=core,
tags=("role.stator_core", "role.stator_thermal_path", "group.bldc_motor"),
)
connectors = [
(
"shell_axis",
(0.0, 0.0, (MOTOR_STATOR_BOTTOM_Z + MOTOR_STATOR_TOP_Z) / 2.0),
"Stator press-fit axis",
)
]
part = make_axis_part_rpart(
part_id="stator_core",
body=core,
name="12-slot laminated electrical-steel stator stack",
material=material,
connectors=connectors,
)
for index, angle, _center in radial_centers(
count=MOTOR_SLOT_COUNT,
radius=0.0,
angle_offset=0.0,
):
rotation = make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle)
connector = scad.make_placement_connector_rconnector(
connector_id=f"winding_{index + 1:02d}",
placement=rotation,
name=f"Slot winding {index + 1} retention datum",
)
part = scad.add_connector_rpart(part=part, connector=connector)
print(
f"stator_core_geometry: slots={MOTOR_SLOT_COUNT} active_length="
f"{MOTOR_STATOR_TOP_Z - MOTOR_STATOR_BOTTOM_Z:.1f} radial_fit_clearance="
f"{MOTOR_SHELL_INNER_RADIUS - MOTOR_STATOR_OUTER_RADIUS:.2f}"
)
return part
def _make_winding_pack_rpart(*, material: scad.Material) -> scad.Part:
side_depth = MOTOR_STATOR_TOP_Z - MOTOR_STATOR_BOTTOM_Z + 0.4
side_bottom_z = MOTOR_STATOR_BOTTOM_Z - 0.2
side_positive = scad.make_box_rsolid(
width=4.2,
height=1.2,
depth=side_depth,
bottom_face_center=(17.55, 2.1, side_bottom_z),
)
side_negative = scad.make_box_rsolid(
width=4.2,
height=1.2,
depth=side_depth,
bottom_face_center=(17.55, -2.1, side_bottom_z),
)
rear_end_turn = scad.make_box_rsolid(
width=4.2,
height=5.4,
depth=0.9,
bottom_face_center=(17.55, 0.0, MOTOR_STATOR_BOTTOM_Z - 1.0),
)
front_end_turn = scad.make_box_rsolid(
width=4.2,
height=5.4,
depth=0.9,
bottom_face_center=(17.55, 0.0, MOTOR_STATOR_TOP_Z + 0.1),
)
winding = scad.union_rsolid(
side_positive,
side_negative,
rear_end_turn,
front_end_turn,
glue=False,
)
winding = apply_tags(
shape=winding,
tags=("role.copper_slot_winding", "group.three_phase_windings"),
)
return make_axis_part_rpart(
part_id="reusable_slot_winding",
body=winding,
name="Reusable four-segment copper tooth winding pack",
material=material,
connectors=(("mount_axis", (0.0, 0.0, 0.0), "Core potting datum"),),
)
def _make_rotor_shaft_sun_rpart(*, material: scad.Material) -> scad.Part:
shaft_bottom_z = REAR_BEARING_CENTER_Z - 3.0
shaft = scad.make_cylinder_rsolid(
radius=MOTOR_SHAFT_RADIUS,
height=STAGE_1.top_z - shaft_bottom_z,
bottom_face_center=(0.0, 0.0, shaft_bottom_z),
axis=(0.0, 0.0, 1.0),
)
back_iron = scad.make_cylinder_rsolid(
radius=MOTOR_ROTOR_BACKIRON_RADIUS,
height=MOTOR_ROTOR_TOP_Z - MOTOR_ROTOR_BOTTOM_Z,
bottom_face_center=(0.0, 0.0, MOTOR_ROTOR_BOTTOM_Z),
axis=(0.0, 0.0, 1.0),
)
sun = scad.std.gear.make_herringbone_gear_rsolid(
n_teeth=STAGE_1.sun_teeth,
module=STAGE_1.module,
pressure_angle=PRESSURE_ANGLE,
helix_angle=HELIX_ANGLE,
gear_height=GEAR_HEIGHT,
addendum_factor=ADDENDUM_FACTOR,
clearance_factor=CLEARANCE_FACTOR,
backlash=BACKLASH,
)
sun = scad.translate_shape(shape=sun, vector=(0.0, 0.0, STAGE_1.bottom_z))
rotor = scad.union_rsolid(shaft, back_iron, sun, glue=False)
rotor = apply_tags(
shape=rotor,
tags=("role.rotor_back_iron", "role.direct_drive_shaft", "role.stage1.sun_gear", "group.bldc_motor"),
)
part = make_axis_part_rpart(
part_id="rotor_core_shaft_sun",
body=rotor,
name="Integrated rotor back iron, 8 mm shaft, and stage-1 sun",
material=material,
connectors=(
("rotor_axis", (0.0, 0.0, -2.5), "Motor rotation axis"),
("rear_bearing_axis", (0.0, 0.0, REAR_BEARING_CENTER_Z), "Rear motor bearing shaft seat"),
("front_bearing_axis", (0.0, 0.0, -2.5), "Front motor bearing shaft seat"),
("stage1_sun_axis", (0.0, 0.0, STAGE_1.mid_z), "Integrated stage-1 sun axis"),
),
)
for index, angle, _center in radial_centers(count=MOTOR_POLE_COUNT, radius=0.0):
connector = scad.make_placement_connector_rconnector(
connector_id=f"magnet_{index + 1:02d}",
placement=make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle),
name=f"Magnet {index + 1} bond datum",
)
part = scad.add_connector_rpart(part=part, connector=connector)
return part
def _make_rotor_magnet_rpart(*, material: scad.Material) -> scad.Part:
half_width = MOTOR_MAGNET_TANGENTIAL_WIDTH / 2.0
outer_x = (MOTOR_MAGNET_OUTER_RADIUS**2 - half_width**2) ** 0.5
radial_depth = outer_x - MOTOR_ROTOR_BACKIRON_RADIUS + 0.05
magnet = scad.make_box_rsolid(
width=radial_depth,
height=MOTOR_MAGNET_TANGENTIAL_WIDTH,
depth=MOTOR_ROTOR_TOP_Z - MOTOR_ROTOR_BOTTOM_Z,
bottom_face_center=(outer_x - radial_depth / 2.0, 0.0, MOTOR_ROTOR_BOTTOM_Z),
)
magnet = apply_tags(shape=magnet, tags=("role.rotor_magnet", "group.rotor_magnets"))
print(
f"rotor_magnet_envelope: corner_radius={MOTOR_MAGNET_OUTER_RADIUS:.2f} "
f"air_gap={MOTOR_STATOR_TOOTH_INNER_RADIUS - MOTOR_MAGNET_OUTER_RADIUS:.2f}"
)
return make_axis_part_rpart(
part_id="reusable_rotor_magnet",
body=magnet,
name="Reusable bonded NdFeB rotor magnet",
material=material,
connectors=(("bond_axis", (0.0, 0.0, 0.0), "Rotor bond datum"),),
)