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
1819import copy
1920import json
21+ import math
2022from dataclasses import dataclass
2123from datetime import UTC , datetime
2224from 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
261383def 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
0 commit comments