Skip to content

Commit 18db2c6

Browse files
cad(enclosure): add bottom standoffs, cam interfaces, spring posts
1 parent 9e1d927 commit 18db2c6

File tree

2 files changed

+155
-16
lines changed

2 files changed

+155
-16
lines changed

cad/column_rod_enclosure.py

Lines changed: 138 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
* In each corner:
1010
* Anchor an M2 or M3 screw through the top and bottom enclosures.
1111
* Use fixed metal rods/bolts in each corner of the enclosure.
12-
* Use springs pushing up on the PCB to keep the dots in the "raised" position for
13-
most CAM positions.
14-
* Use the cams to push the PCB down against the springs (cams push against the top
15-
plate) to lower into the "adjust positions" state.
12+
* Use springs pushing down on the PCB to keep the dots in the "lowered" position for
13+
most CAM positions. Do this because the spring is bulky, and therefore should go
14+
on the top side.
15+
* Use the cams to push the PCB up against the springs (cams push against the bottom
16+
plate) to raise into the "feel the dots now" state.
1617
"""
1718

1819
import copy
1920
import json
21+
import math
2022
from dataclasses import dataclass
2123
from datetime import UTC, datetime
2224
from itertools import product
@@ -42,10 +44,15 @@ class Spec:
4244

4345
pcb_length_x: float = 95
4446
pcb_length_y: float = 99
45-
# PCB + Housing + Screw_Heads -or- PCB + Raiser_Motor_OD + Raiser_Motor_Clip
46-
pcb_and_housing_thickness_z: float = 1.6 + 6.0 + 1.0
47-
pcb_travel_z: float = 2
48-
pcb_extra_z: float = 3 # Extra dist in Z, for housing-to-PCB screw heads.
47+
48+
# Settings controlling the Z travel of the PCB inside in the enclosure.
49+
pcb_thickness: float = 0.8
50+
cam_motor_od: float = 6.0
51+
cam_motor_clamp_top_thickness_plus_bolt_heads: float = 1.5 + 4
52+
pcb_travel_z: float = 1.2
53+
cam_min_od: float = 7.4
54+
cam_max_od: float = 8.6
55+
cam_avg_od: float = 8.0
4956

5057
# PCB layout - corner screws.
5158
pcb_raiser_screw_sep_x: float = 42.6
@@ -65,11 +72,26 @@ class Spec:
6572
rp2040_cutout_x_range: tuple[float, float] = (18, 400)
6673
rp2040_cutout_y_range: tuple[float, float] = (-26, 15)
6774

75+
# PCB layout - where do the parts that interface with the cams go?
76+
# These cam interfaces are drawn on the inside of the bottom wall.
77+
cam_interface_x_sep: float = 64.2 # 163.6 - 99.4
78+
cam_interface_y_sep: float = 79.0 # 164 - 85
79+
cam_interface_width_x: float = 2.0
80+
cam_interface_width_y: float = 8.0
81+
82+
# PCB layout - where do the spring posts go?
83+
# These spring posts are drawn on the inside of the top wall.
84+
spring_post_od = 3.5 # TODO(KS): Check spring ID.
85+
spring_post_margin_from_pcb_edge: float = 3.0 # Dist: PCB edge to center of post.
86+
87+
# Overall enclosure dimensions.
6888
enclosure_total_x: float = 130
6989
enclosure_total_y: float = 125
7090
enclosure_wall_thickness_xy: float = 2.2
7191
enclosure_wall_thickness_bottom: float = 2
7292
enclosure_wall_thickness_top: float = 2
93+
enclosure_bottom_wall_standoff_height: float = 4.0 # Avoid THT solder points, nuts.
94+
enclosure_bottom_wall_standoff_od: float = 6
7395

7496
# Bolts for joining the top/bottom halves of the enclosure securely.
7597
joiner_bolt_d: float = 3 # M3.
@@ -78,27 +100,61 @@ class Spec:
78100

79101
enclosure_fillet_radius_xy: float = 2
80102

103+
bottom_debugging_hole_od: float = 60.0
104+
81105
def __post_init__(self) -> None:
82106
"""Post initialization checks."""
83107
data = {
108+
"pcb_and_housing_thickness_z": self.pcb_and_housing_thickness_z,
84109
"enclosure_total_z": self.enclosure_total_z,
110+
"spring_post_length": self.spring_post_length,
85111
}
86112
logger.info(json.dumps(data, indent=2))
87113

114+
assert math.isclose(
115+
self.cam_max_od - self.cam_min_od,
116+
self.pcb_travel_z,
117+
)
118+
119+
assert math.isclose(
120+
(self.cam_min_od + self.cam_max_od) / 2,
121+
self.cam_avg_od,
122+
)
123+
88124
def deep_copy(self) -> "Spec":
89125
"""Copy the current spec."""
90126
return copy.deepcopy(self)
91127

128+
@property
129+
def pcb_and_housing_thickness_z(self) -> float:
130+
"""Max thickness from the bottom of the PCB to the inside of the enclosure."""
131+
# TODO(KilowattSynthesis): Update to be max(housing_height, raiser_height)
132+
133+
# housing_height = PCB + Housing + Screw_Heads
134+
# raiser_height = PCB + Raiser_Motor_OD + Raiser_Motor_Clip (this one)
135+
raiser_height = (
136+
self.cam_motor_clamp_top_thickness_plus_bolt_heads
137+
+ self.cam_motor_od
138+
+ self.pcb_thickness
139+
)
140+
return raiser_height
141+
92142
@property
93143
def enclosure_total_z(self) -> float:
94144
"""Total thickness of the enclosure."""
95145
return (
96146
self.enclosure_wall_thickness_bottom
147+
+ self.enclosure_bottom_wall_standoff_height
97148
+ self.pcb_and_housing_thickness_z
98149
+ self.pcb_travel_z
99150
+ self.enclosure_wall_thickness_top
100151
)
101152

153+
@property
154+
def spring_post_length(self) -> float:
155+
"""Length of the spring posts."""
156+
return self.pcb_and_housing_thickness_z - self.pcb_thickness
157+
102158
def get_cell_center_x_values(self) -> list[float]:
103159
"""Get the X coordinate of the center of each cell."""
104160
return bde.evenly_space_with_center(
@@ -151,10 +207,11 @@ def make_enclosure_top(spec: Spec) -> bd.Part | bd.Compound:
151207
)
152208

153209
# Remove the inside of the enclosure.
210+
inside_of_box_z_val = spec.enclosure_total_z - spec.enclosure_wall_thickness_top
154211
inner_box = bd.Box(
155212
spec.enclosure_total_x - 2 * spec.enclosure_wall_thickness_xy,
156213
spec.enclosure_total_y - 2 * spec.enclosure_wall_thickness_xy,
157-
spec.enclosure_total_z - spec.enclosure_wall_thickness_top,
214+
inside_of_box_z_val,
158215
align=bde.align.ANCHOR_BOTTOM,
159216
)
160217
p -= bd.fillet(
@@ -217,6 +274,34 @@ def make_enclosure_top(spec: Spec) -> bd.Part | bd.Compound:
217274
range_z=(-50, 50),
218275
)
219276

277+
# Add spring posts.
278+
spring_post_y_max = inside_of_box_z_val
279+
# spring_post_y_min = ( # Old complex logic before simplification by substitution.
280+
# spec.enclosure_wall_thickness_bottom
281+
# + spec.enclosure_bottom_wall_standoff_height
282+
# + spec.pcb_thickness
283+
# + spec.pcb_travel_z
284+
# )
285+
for x_corner, y_corner in product((1, -1), (1, -1)):
286+
for x_small_multiplier, y_small_multiplier in ((1, 0), (0, 1)):
287+
p += bd.Pos(
288+
x_corner
289+
* (
290+
spec.pcb_length_x / 2
291+
- spec.spring_post_margin_from_pcb_edge * x_small_multiplier
292+
),
293+
y_corner
294+
* (
295+
spec.pcb_length_y / 2
296+
- spec.spring_post_margin_from_pcb_edge * y_small_multiplier
297+
),
298+
spring_post_y_max,
299+
) * bd.Cylinder(
300+
radius=spec.spring_post_od / 2,
301+
height=spec.spring_post_length,
302+
align=bde.align.ANCHOR_TOP,
303+
)
304+
220305
return p
221306

222307

@@ -235,7 +320,7 @@ def make_enclosure_bottom(spec: Spec) -> bd.Part | bd.Compound:
235320
inner_box.edges().filter_by(bd.Axis.Z), radius=spec.enclosure_fillet_radius_xy
236321
)
237322

238-
# Add the `joiner_bolt` (top-to-bottom connections).
323+
# Remove the `joiner_bolt` holes (sturdy top-to-bottom connections) in corners.
239324
for x_sign, y_sign in product((1, -1), (1, -1)):
240325
bottom_of_joiner_bolt_pos = bd.Pos(
241326
x_sign * (spec.enclosure_total_x / 2 - spec.joiner_bolt_margin),
@@ -247,6 +332,14 @@ def make_enclosure_bottom(spec: Spec) -> bd.Part | bd.Compound:
247332
align=bde.align.ANCHOR_BOTTOM,
248333
)
249334

335+
# Add the stand offs from the bottom wall.
336+
for x_val, y_val in spec.get_pcb_raiser_screw_coordinates():
337+
p += bd.Pos(x_val, y_val, spec.enclosure_wall_thickness_bottom) * bd.Cylinder(
338+
radius=spec.enclosure_bottom_wall_standoff_od / 2,
339+
height=spec.enclosure_bottom_wall_standoff_height,
340+
align=bde.align.ANCHOR_BOTTOM,
341+
)
342+
250343
# Remove the holes for the `pcb_raiser_screw`.
251344
for x_val, y_val in spec.get_pcb_raiser_screw_coordinates():
252345
p -= bd.Pos(x_val, y_val, 0) * bd.Cylinder(
@@ -255,15 +348,48 @@ def make_enclosure_bottom(spec: Spec) -> bd.Part | bd.Compound:
255348
align=bde.align.ANCHOR_BOTTOM,
256349
)
257350

351+
# Add the cam interfaces.
352+
# Set this height such that, at the chosen cam state, it is statically determinate
353+
# against the standoffs on the bottom.
354+
cam_interface_z_min = spec.enclosure_wall_thickness_bottom
355+
cam_interface_height = (
356+
spec.enclosure_bottom_wall_standoff_height
357+
+ spec.pcb_thickness
358+
+ spec.cam_motor_od / 2
359+
- spec.cam_max_od / 2 # TODO(KS): Not sure what to pick. Min or max for sure.
360+
)
361+
for x_sign, y_sign in product((1, -1), (1, -1)):
362+
p += bd.Pos(
363+
x_sign * spec.cam_interface_x_sep / 2,
364+
y_sign * spec.cam_interface_y_sep / 2,
365+
cam_interface_z_min,
366+
) * bd.Box(
367+
spec.cam_interface_width_x,
368+
spec.cam_interface_width_y,
369+
cam_interface_height,
370+
align=bde.align.ANCHOR_BOTTOM,
371+
)
372+
373+
# Remove giant debugging hole.
374+
if spec.bottom_debugging_hole_od > 0.2: # noqa: PLR2004
375+
p -= bd.Cylinder(
376+
radius=spec.bottom_debugging_hole_od / 2,
377+
height=10,
378+
)
379+
258380
return p
259381

260382

261383
def preview_both_enclosure_parts(spec: Spec) -> bd.Part | bd.Compound:
262384
"""Preview both the top and bottom enclosure parts."""
263385
p = bd.Part(None)
264386

265-
p += make_enclosure_top(spec).translate((0, 0, 3))
266-
p += make_enclosure_bottom(spec).translate((0, 0, -3))
387+
enclosure_top = make_enclosure_top(spec).translate((0, 0, 3))
388+
enclosure_bottom = make_enclosure_bottom(spec).translate((0, 0, -13))
389+
390+
# Debugging: Add a breakpoint on the next line.
391+
p += enclosure_top
392+
p += enclosure_bottom
267393

268394
return p
269395

cad/pcb_raiser_cam.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ class MainSpec:
6060

6161
def __post_init__(self) -> None:
6262
"""Post initialization checks."""
63-
data = {}
63+
data = {
64+
"cam_min_od": self.cam_main_od - self.cam_travel / 2,
65+
"cam_max_od": self.cam_main_od + self.cam_travel / 2,
66+
}
6467
logger.info(json.dumps(data, indent=2))
6568

6669
def deep_copy(self) -> "MainSpec":
@@ -204,15 +207,25 @@ def make_assembly_cam_and_dc_motor(spec: MainSpec) -> bd.Part | bd.Compound:
204207
return p
205208

206209

210+
def preview_all() -> bd.Part | bd.Compound:
211+
"""Preview all the parts defined here."""
212+
p = bd.Part(None)
213+
p += make_cam(MainSpec()).translate((0, 0, 0))
214+
p += make_dc_motor_and_gearbox(MotorMainSpec()).translate((10, 0, 0))
215+
p += make_bushing_block(MainSpec()).translate((20, 0, 0))
216+
return p
217+
218+
207219
if __name__ == "__main__":
208220
start_time = datetime.now(UTC)
209221
py_file_name = Path(__file__).name
210222
logger.info(f"Running {py_file_name}")
211223

212224
parts = {
213-
"cam": show(make_cam(MainSpec())),
214-
"assembly_cam_and_dc_motor": show(make_assembly_cam_and_dc_motor(MainSpec())),
215-
"bushing_block": show(make_bushing_block(MainSpec())),
225+
"preview_all": show(preview_all()),
226+
"cam": (make_cam(MainSpec())),
227+
"assembly_cam_and_dc_motor": (make_assembly_cam_and_dc_motor(MainSpec())),
228+
"bushing_block": (make_bushing_block(MainSpec())),
216229
}
217230

218231
logger.info("Saving CAD model(s)...")

0 commit comments

Comments
 (0)