A nightstand noise generator based on M5Stack Atom Echo and integrating with Home Assistant
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

enclosure: all hardware on the baseplate, snap-fit cover

Reorganized so the bottom plate is the structural foundation and the
chassis ("cover") is purely cosmetic. Cover comes off without tools,
exposing all internals at once.

* Speaker mounts to four tall posts on the bottom plate (60 mm M3
pattern), with self-tap pilots at the post tops. No more screw holes
through the cover's top panel. Foam sealing, if it turns out to be
needed, becomes a foam donut on top of the speaker frame compressed
against the cover underside during assembly.
* USB cable routes internally. The connector lives on the Atom inside
the chamber; cable threads through a printed strain-relief tunnel
a few cm in front of the back wall and exits through a small notch
at the bottom of the back wall, matched by a notch in the plate's
back edge so the exit sits flush with the bottom of the unit.
* Cover retention is snap-fit. Four tabs on the plate carry hemi-
spherical bumps that catch in detent recesses on the chassis inner
walls. M3 screws and bosses are gone.

Post height, cable diameter, and snap-bump diameter are param-tunable
guesses to be dialed in after a test print.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+253 -162
+86 -67
enclosure/lib/params.scad
··· 1 1 // Sound machine enclosure — shared dimensions. 2 2 // Units: mm. Edit here; everything else derives. 3 + // 4 + // Design principle: all internal hardware mounts to the bottom plate. 5 + // The chassis ("cover") just drops on top, captured by snap-fit tabs. 6 + // No tools required to open or close the unit. 3 7 4 8 // ============================================================ 5 9 // Atom Echo (M5Stack C008-C) ··· 16 20 atom_usb_h_native = 7; 17 21 18 22 // Bottom-face male HY2.0-4P GROVE connector. 19 - // Sticks down from the body; sits between the press-fit headers. 20 23 grove_body_w = 8; 21 24 grove_body_d = 4.5; 22 25 grove_body_h = 2.5; 23 26 24 27 // Bottom-face press-fit headers (the "Atom Stack" interface). 25 - // 5-pin on one edge, 4-pin on the opposite edge. Pitch 2.54 mm. 26 28 header_pitch = 2.54; 27 29 header_left_pin_count = 5; 28 30 header_right_pin_count = 4; 29 - header_edge_offset = 2.0; // body-edge to pin-row centerline (estimate) 31 + header_edge_offset = 2.0; 30 32 31 33 // ============================================================ 32 34 // MAX98357A I2S amp breakout (Adafruit 3006 / generic clone) ··· 50 52 spk_magnet_clearance = 30; // air below baffle for magnet + breathing 51 53 52 54 // ============================================================ 53 - // Daughterboard — the small PCB the Atom presses onto. 54 - // Carries the MAX98357A and the speaker terminals; sits on 55 - // standoffs molded into the bottom plate. 55 + // Daughterboard — protoboard the Atom presses onto. 56 56 // ============================================================ 57 57 58 58 dboard_t = 1.6; ··· 65 65 // Snooze cap + well 66 66 // ============================================================ 67 67 68 - // The cap is a wide pad covering most of the snooze zone — easy to 69 - // find in the dark. The cap body passes through a through-hole in 70 - // the top panel; a flange on its underside (wider than the hole) 71 - // catches on the underside of the top panel as the up-stop. Down- 72 - // stop is the Atom's tactile dome, which also acts as the return 73 - // spring. 74 68 snooze_cap_w = 70; 75 69 snooze_cap_d = 28; 76 - snooze_cap_h_above = 6; // how far the cap protrudes above the top surface 77 - snooze_cap_wall_t = 1.5; // thin so the SK6812 LED diffuses through 78 - snooze_cap_clearance = 0.4; // play between cap body and well walls 79 - snooze_cap_travel = 1.5; // total cap travel from rest to fully pressed 70 + snooze_cap_h_above = 6; 71 + snooze_cap_wall_t = 1.5; 72 + snooze_cap_clearance = 0.4; 73 + snooze_cap_travel = 1.5; 80 74 81 - // Captive flange on the underside of the cap — must be wider than 82 - // the well opening so it catches on the underside of the top panel. 83 75 snooze_cap_flange_overhang = 3; 84 76 snooze_cap_flange_h = 2; 85 77 86 - // Well opening in the top panel — derived from the cap. 87 78 snooze_well_w = snooze_cap_w + 2 * snooze_cap_clearance; 88 79 snooze_well_d = snooze_cap_d + 2 * snooze_cap_clearance; 89 80 ··· 91 82 // Speaker grille (in the rear portion of the top panel) 92 83 // ============================================================ 93 84 94 - grille_hole_d = 2.5; 95 - grille_pitch = 5; 96 - // Constrain the grille to the speaker's cone diameter — holes outside 97 - // that radius hit the speaker's rigid frame and don't pass sound. 85 + grille_hole_d = 2.5; 86 + grille_pitch = 5; 98 87 grille_diameter = spk_cone_cutout; 99 88 100 89 // ============================================================ ··· 102 91 // ============================================================ 103 92 104 93 wall_t = 3; 105 - top_t = 4; // a touch thicker for rigidity around the speaker mount 94 + top_t = 4; 106 95 bottom_t = 3; 107 96 108 - // Subtle rounding on the chassis outer top + vertical edges. Bottom edge 109 - // stays sharp so the part sits flat on the print bed (chassis prints 110 - // open-bottom-down). 111 - chassis_round_r = 1.5; 97 + chassis_round_r = 1.5; 98 + cap_round_r = 1.5; 99 + cap_dome_h = 1.5; 100 + snooze_well_corner_r = 1.5; 112 101 113 - // Slight rounding on the visible (above-the-top-panel) portion of the 114 - // snooze cap. Hidden portions stay sharp. 115 - cap_round_r = 1.5; 116 - 117 - // Spherical dome on top of the visible cap — center rises this far above 118 - // the corners, so a sleepy hand finds the high point first. 119 - cap_dome_h = 1.5; 120 - 121 - // Rounded corner radius on the snooze well opening, matching the cap. 122 - snooze_well_corner_r = 1.5; 123 - 124 - // Top-panel layout (front to back, looking down at the unit): 102 + // Top-panel layout (front to back, looking down): 125 103 // front_margin | snooze well | zone_gap | speaker grille | back_margin 126 104 front_margin = 8; 127 105 zone_gap = 6; 128 106 back_margin = 6; 129 107 130 - // Internal volume. 131 - inner_w = max(dboard_w + 8, spk_frame + 6); // whichever is wider 108 + inner_w = max(dboard_w + 8, spk_frame + 6); 132 109 inner_d = front_margin + snooze_well_d + zone_gap + spk_frame + back_margin; 133 110 inner_h = max(spk_magnet_clearance + 4, 134 111 dboard_standoff_h + dboard_t + dboard_pin_h + atom_body_h + 3); 135 112 136 - // Outer envelope. 137 113 outer_w = inner_w + 2 * wall_t; 138 114 outer_d = inner_d + 2 * wall_t; 139 - outer_h = inner_h + top_t; // bottom is a separate plate; chassis is open-bottom 115 + outer_h = inner_h + top_t; 116 + 117 + // ============================================================ 118 + // Speaker mounting posts on the bottom plate. 119 + // Speaker rests on top of the posts, M3 self-tap screws come down 120 + // through the speaker's mounting tabs into the post tops. With the 121 + // speaker thus mounted to the base, the cover panel above is purely 122 + // cosmetic — no fasteners pass through it. 123 + // 124 + // Post height targets the speaker frame plate at ~22 mm above the 125 + // bottom plate, leaving ~3 mm clearance below the cover for a foam 126 + // donut between frame and cover underside. Re-tune `spk_post_h` once 127 + // you've measured the actual cone protrusion / magnet depth on a 128 + // physical unit. 129 + // ============================================================ 130 + 131 + spk_post_h = 22; 132 + spk_post_d = 6; 133 + spk_post_pilot_d = 2.5; 140 134 141 135 // ============================================================ 142 - // USB-C cutout in the back wall. 143 - // Aligned with the Atom Echo's USB-C port: vertically centered on the 144 - // Atom body, which sits atop the daughterboard's pin headers. 145 - // Slightly larger than the Atom's own cutout for plug clearance. 136 + // USB-C cable: routed internally, exits through a small notch at the 137 + // bottom of the back wall. A printed strain-relief tunnel on the 138 + // bottom plate grips the cable so a tug doesn't transfer to the 139 + // connector. 146 140 // ============================================================ 147 141 148 - usb_hole_w = 14; 149 - usb_hole_h = 9; 142 + cable_diameter = 4; // typical USB-C cable jacket OD 150 143 151 - // Z of the bottom of the Atom body (measured from the inside floor of the 152 - // chassis, which sits atop the bottom plate). 153 - atom_z_bottom = dboard_standoff_h + dboard_t + dboard_pin_h; 154 - // USB-C port centerline is vertically centered on the Atom body height. 155 - usb_hole_cz = atom_z_bottom + atom_body_h / 2; 144 + cable_notch_w = cable_diameter + 2; 145 + cable_notch_h = cable_diameter + 2; 146 + 147 + cable_clip_inner_w = cable_diameter + 0.4; // snug horizontal fit 148 + cable_clip_inner_h = cable_diameter + 0.4; // snug vertical fit 149 + cable_clip_wall_t = 1.5; 150 + cable_clip_l = 8; // length along cable axis 151 + cable_clip_y_from_back = 12; // from the back wall inside face 156 152 157 153 // ============================================================ 158 - // Bottom plate — separate part, screws onto the chassis from below. 154 + // Snap-fit cover retention. 155 + // 156 + // Four tabs rise from the bottom plate's top face, one centered on 157 + // each side. Each tab carries a small hemispherical bump near its 158 + // top, which catches in a matching detent recess on the chassis 159 + // inner wall. Drop the chassis on, hear the snap; pull straight up 160 + // firmly to remove. 159 161 // ============================================================ 160 162 161 - bottom_plate_t = 3; 162 - bottom_plate_screw_d = 3.4; // M3 clearance through the plate 163 - bottom_plate_pilot_d = 2.5; // M3 self-tap pilot in the chassis boss (PLA) 164 - bottom_plate_screw_inset = 5; // edge to screw centerline 165 - bottom_plate_boss_d = 8; // OD of the screw bosses inside the chassis 166 - bottom_plate_boss_h = 8; // height of bosses above the chassis open bottom 163 + snap_tab_w = 14; 164 + snap_tab_t = 2; 165 + snap_tab_h = 8; 166 + snap_tab_clearance = 0.3; // gap between tab and chassis inner wall 167 + 168 + snap_bump_d = 2; // hemisphere diameter 169 + snap_bump_inset_from_top = 1.5; // bump center below tab top 170 + 171 + // Recess on the chassis inner wall — slightly larger than the bump 172 + // for a positive click without binding. 173 + snap_recess_w = snap_tab_w + 2; 174 + snap_recess_h = snap_bump_d + 1; 175 + snap_recess_d = snap_bump_d / 2 + 0.4; 176 + // Vertical center of the recess in chassis coords (chassis open bottom = z=0). 177 + snap_recess_cz = snap_tab_h - snap_bump_inset_from_top; 178 + 179 + // ============================================================ 180 + // Bottom plate 181 + // ============================================================ 182 + 183 + bottom_plate_t = 3; 184 + 185 + // ============================================================ 186 + // Z reference for the snooze-cap plunger sizing. 187 + // ============================================================ 188 + 189 + atom_z_bottom = dboard_standoff_h + dboard_t + dboard_pin_h; 167 190 168 191 // ============================================================ 169 192 // Component placement (X centered; Y measured from the front wall inside) 170 193 // ============================================================ 171 194 172 - // Snooze well center, in chassis-internal coordinates (origin at front-left 173 - // inside corner of the chamber). 174 195 snooze_well_cy = front_margin + snooze_well_d / 2; 175 - 176 - // Speaker grille center. 177 - spk_grille_cy = front_margin + snooze_well_d + zone_gap + spk_frame / 2; 196 + spk_grille_cy = front_margin + snooze_well_d + zone_gap + spk_frame / 2;
+125 -45
enclosure/parts/bottom-plate.scad
··· 1 - // Bottom plate — closes the open-bottom chassis from below and carries 2 - // the standoffs that hold the daughterboard. 1 + // Bottom plate — the structural foundation of the unit. All internal 2 + // hardware mounts here: 3 + // * Daughterboard sits on 4 standoffs (front section). 4 + // * Speaker rests on 4 tall posts at the 60 mm M3 pattern (rear 5 + // section), with M3 self-tap screws coming down through its 6 + // mounting tabs into the post tops. 7 + // * USB cable threads through a strain-relief tunnel on its way out 8 + // the back-bottom notch. 9 + // The chassis ("cover") drops straight on top, captured by 4 snap-fit 10 + // tabs around the perimeter. 3 11 // 4 - // Coordinate system: same as the chassis. Origin at the bottom-front-left 5 - // outer corner. The plate's top face sits at z=0 (i.e., flush with the 6 - // chassis's open-bottom edge); the plate body extends downward to 7 - // z=-bottom_plate_t. 12 + // Coordinate system: same as the chassis. Origin at the bottom-front- 13 + // left outer corner. The plate's top face sits at z=0; the plate body 14 + // extends downward to z = -bottom_plate_t. 8 15 9 16 include <../lib/params.scad> 10 17 include <../lib/util.scad> ··· 14 21 15 22 EPS = 0.05; 16 23 17 - // Register tab dimensions — small bumps on the plate's top face that 18 - // slide into the chassis cavity, locating the plate laterally during 19 - // assembly before the screws are run home. 20 - tab_w = 12; // length along the wall direction 21 - tab_t = 2; // thickness perpendicular to the wall 22 - tab_h = 2; // height above the plate top face 23 - tab_clear = 0.3; // gap between the tab and the chassis inner wall 24 - 25 - // Daughterboard sits in the front portion of the chamber, directly under 26 - // the snooze well, with its center aligned to the snooze well center. 24 + // Daughterboard sits in the front portion of the chamber, directly 25 + // under the snooze well, with its center aligned to the well center. 27 26 dboard_cx = outer_w / 2; 28 27 dboard_cy = wall_t + snooze_well_cy; 29 28 29 + // Speaker sits in the rear portion, aligned with the grille. 30 + spk_cx = outer_w / 2; 31 + spk_cy = wall_t + spk_grille_cy; 32 + 30 33 module bottom_plate() { 31 34 difference() { 32 35 // Plate body — rounded vertical corners to match the chassis, ··· 34 37 translate([0, 0, -bottom_plate_t]) 35 38 rounded_prism([outer_w, outer_d, bottom_plate_t], chassis_round_r); 36 39 37 - // Screw clearance holes — 4 corners, matching the chassis bosses. 38 - for (cx = [wall_t + bottom_plate_screw_inset, 39 - outer_w - wall_t - bottom_plate_screw_inset]) 40 - for (cy = [wall_t + bottom_plate_screw_inset, 41 - outer_d - wall_t - bottom_plate_screw_inset]) 42 - translate([cx, cy, -bottom_plate_t - EPS]) 43 - cylinder(h = bottom_plate_t + 2 * EPS, 44 - d = bottom_plate_screw_d); 40 + // Cable notch in the back edge of the plate, aligning with the 41 + // chassis cable notch in the back wall — together they form a 42 + // small rectangular hole flush with the bottom of the unit. 43 + translate([(outer_w - cable_notch_w) / 2, 44 + outer_d - wall_t - EPS, 45 + -bottom_plate_t - EPS]) 46 + cube([cable_notch_w, wall_t + 2 * EPS, bottom_plate_t + 2 * EPS]); 45 47 } 46 48 47 49 // Daughterboard standoffs — 4 corners of the daughterboard footprint. 48 - // Self-tapping into the daughterboard with M2 (preferred) or M2.5, 49 - // depending on what holes get drilled in the daughterboard. 50 50 daughterboard_standoffs(); 51 51 52 - // Register tabs — locate the plate laterally inside the chassis 53 - // cavity during assembly. 54 - register_tabs(); 55 - } 52 + // Speaker mounting posts — 4 at the 60 mm M3 pattern. 53 + speaker_posts(); 56 54 57 - module register_tabs() { 58 - inset = wall_t + tab_clear; 59 - 60 - // Front + back tabs run parallel to the X axis. 61 - for (cy = [inset, outer_d - inset - tab_t]) 62 - translate([(outer_w - tab_w) / 2, cy, 0]) 63 - cube([tab_w, tab_t, tab_h]); 55 + // USB cable strain-relief tunnel near the back wall. 56 + cable_clip(); 64 57 65 - // Left + right tabs run parallel to the Y axis. 66 - for (cx = [inset, outer_w - inset - tab_t]) 67 - translate([cx, (outer_d - tab_w) / 2, 0]) 68 - cube([tab_t, tab_w, tab_h]); 58 + // Snap-fit tabs that retain the chassis cover. 59 + snap_tabs(); 69 60 } 70 61 71 62 module daughterboard_standoffs() { 72 63 standoff_d = 5; 73 - standoff_id = 1.6; // pilot for self-tapping M2 74 - inset = 3; // distance from daughterboard edge to standoff center 64 + standoff_id = 1.6; 65 + inset = 3; 75 66 76 - // Standoff top sits at z = dboard_standoff_h (so the daughterboard 77 - // bottom face rests there). 78 67 for (sx = [dboard_cx - dboard_w / 2 + inset, 79 68 dboard_cx + dboard_w / 2 - inset]) 80 69 for (sy = [dboard_cy - dboard_d / 2 + inset, ··· 85 74 translate([sx, sy, -EPS]) 86 75 cylinder(h = dboard_standoff_h + 2 * EPS, d = standoff_id); 87 76 } 77 + } 78 + 79 + module speaker_posts() { 80 + p = spk_mount_pattern / 2; 81 + 82 + for (sx = [-p, p]) 83 + for (sy = [-p, p]) 84 + difference() { 85 + translate([spk_cx + sx, spk_cy + sy, 0]) 86 + cylinder(h = spk_post_h, d = spk_post_d); 87 + translate([spk_cx + sx, spk_cy + sy, -EPS]) 88 + cylinder(h = spk_post_h + 2 * EPS, d = spk_post_pilot_d); 89 + } 90 + } 91 + 92 + // A short rectangular tunnel for the USB cable. The cable threads 93 + // through it during assembly; pulls on the cable transfer to the 94 + // tunnel walls (and thus the bottom plate) instead of the connector. 95 + module cable_clip() { 96 + cy_center = outer_d - wall_t - cable_clip_y_from_back; 97 + outer_w_l = cable_clip_inner_w + 2 * cable_clip_wall_t; 98 + outer_h_l = cable_clip_inner_h + cable_clip_wall_t; // open at the bottom 99 + 100 + translate([(outer_w - outer_w_l) / 2, 101 + cy_center - cable_clip_l / 2, 102 + 0]) 103 + difference() { 104 + cube([outer_w_l, cable_clip_l, outer_h_l]); 105 + 106 + // Hollow the tunnel along the cable axis. 107 + translate([cable_clip_wall_t, 108 + -EPS, 109 + 0]) // open at z=0 so the cable can drop in from above 110 + cube([cable_clip_inner_w, 111 + cable_clip_l + 2 * EPS, 112 + cable_clip_inner_h]); 113 + } 114 + } 115 + 116 + // Four snap-fit tabs centered on each side of the cavity. Each carries 117 + // a small hemispherical bump near its top that catches in a recess on 118 + // the chassis inner wall. 119 + module snap_tabs() { 120 + inset = wall_t + snap_tab_clearance; 121 + 122 + // Front (along front wall): tab oriented with its width along X, 123 + // thickness along Y, bump on the +Y face? No — the chassis wall is 124 + // at y < wall_t, so the bump should face -Y (toward the front 125 + // wall's interior face, which is at y = wall_t). 126 + // Actually: the front wall's INTERIOR face is at y = wall_t, on 127 + // the +Y side from the wall. The tab sits inside the cavity at y = 128 + // wall_t + snap_tab_clearance. The bump faces the front wall's 129 + // interior, i.e., -Y. So bump goes on the -Y face of the tab. 130 + snap_tab_oriented( 131 + cx = (outer_w - snap_tab_w) / 2, 132 + cy = inset, 133 + size = [snap_tab_w, snap_tab_t, snap_tab_h], 134 + bump_offset = [snap_tab_w / 2, 0, snap_tab_h - snap_bump_inset_from_top] 135 + ); 136 + 137 + // Back wall. 138 + snap_tab_oriented( 139 + cx = (outer_w - snap_tab_w) / 2, 140 + cy = outer_d - inset - snap_tab_t, 141 + size = [snap_tab_w, snap_tab_t, snap_tab_h], 142 + bump_offset = [snap_tab_w / 2, snap_tab_t, snap_tab_h - snap_bump_inset_from_top] 143 + ); 144 + 145 + // Left wall. 146 + snap_tab_oriented( 147 + cx = inset, 148 + cy = (outer_d - snap_tab_w) / 2, 149 + size = [snap_tab_t, snap_tab_w, snap_tab_h], 150 + bump_offset = [0, snap_tab_w / 2, snap_tab_h - snap_bump_inset_from_top] 151 + ); 152 + 153 + // Right wall. 154 + snap_tab_oriented( 155 + cx = outer_w - inset - snap_tab_t, 156 + cy = (outer_d - snap_tab_w) / 2, 157 + size = [snap_tab_t, snap_tab_w, snap_tab_h], 158 + bump_offset = [snap_tab_t, snap_tab_w / 2, snap_tab_h - snap_bump_inset_from_top] 159 + ); 160 + } 161 + 162 + module snap_tab_oriented(cx, cy, size, bump_offset) { 163 + translate([cx, cy, 0]) { 164 + cube(size); 165 + translate(bump_offset) 166 + sphere(d = snap_bump_d); 167 + } 88 168 } 89 169 90 170 bottom_plate();
+42 -50
enclosure/parts/chassis.scad
··· 1 - // Main printed chassis — 4 walls + integrated top panel, open at the bottom. 2 - // The bottom plate (separate part) screws on from below and carries the 3 - // daughterboard standoffs. 1 + // Main printed chassis — 4 walls + integrated top panel, open at the 2 + // bottom. Drops onto the bottom plate (where all hardware lives) and is 3 + // retained by 4 snap-fit tabs that catch in the recesses on the inner 4 + // walls. No fasteners; pull straight up firmly to remove. 4 5 // 5 6 // Coordinate system: origin at the bottom-front-left outer corner. 6 7 // +X = right, +Y = back, +Z = up. ··· 19 20 // bottom so the part sits flat on the build plate. 20 21 rounded_box_top([outer_w, outer_d, outer_h], chassis_round_r); 21 22 22 - // Inner cavity — open-bottom, so subtract a cube that extends 23 - // through z=0 and stops at the underside of the top panel. 23 + // Inner cavity — open-bottom. 24 24 translate([wall_t, wall_t, -EPS]) 25 25 cube([inner_w, inner_d, inner_h + EPS]); 26 26 ··· 31 31 rounded_prism([snooze_well_w, snooze_well_d, top_t + 2 * EPS], 32 32 snooze_well_corner_r); 33 33 34 - // Speaker grille — a circular field of small holes in the rear 35 - // portion of the top panel, constrained to the cone diameter. 34 + // Speaker grille — circular field of small holes, constrained 35 + // to the cone diameter. Speaker now mounts to bottom-plate 36 + // posts, so this is the only feature the cover contributes to 37 + // the speaker; no M3 holes anywhere on the top panel. 36 38 speaker_grille(); 37 39 38 - // Speaker mounting holes — 4× M3 clearance, 60 mm square pattern, 39 - // through the top panel. 40 - speaker_mount_holes(); 41 - 42 - // USB-C cutout in the back wall. 43 - translate([(outer_w - usb_hole_w) / 2, 40 + // Cable notch in the back wall, at the bottom edge. The USB 41 + // cable plugs into the Atom internally and exits here. 42 + translate([(outer_w - cable_notch_w) / 2, 44 43 outer_d - wall_t - EPS, 45 - usb_hole_cz - usb_hole_h / 2]) 46 - cube([usb_hole_w, wall_t + 2 * EPS, usb_hole_h]); 44 + 0]) 45 + cube([cable_notch_w, wall_t + 2 * EPS, cable_notch_h]); 47 46 48 - // Bottom-plate screw holes — clearance through the chassis wall 49 - // is unnecessary because the bottom plate screws into bosses 50 - // (added below); subtract from those bosses instead. 51 - bottom_plate_screw_holes(); 47 + // Snap-fit detent recesses on the inside of the four walls, 48 + // matching the tabs on the bottom plate. 49 + snap_recesses(); 52 50 } 53 - 54 - // Bottom-plate screw bosses, 4 corners inside the chassis. 55 - bottom_plate_screw_bosses(); 56 51 } 57 52 58 53 module speaker_grille() { ··· 60 55 cy = wall_t + spk_grille_cy; 61 56 r = grille_diameter / 2; 62 57 63 - // Hexagonal-ish grid clipped to a circle. 64 58 intersection() { 65 59 translate([cx - r, cy - r, outer_h - top_t - EPS]) 66 60 cube([2 * r, 2 * r, top_t + 2 * EPS]); ··· 68 62 union() { 69 63 for (xi = [-r : grille_pitch : r]) 70 64 for (yi = [-r : grille_pitch : r]) { 71 - // Stagger every other row by half-pitch for a nicer pattern. 72 65 xoff = (round(yi / grille_pitch) % 2 == 0) ? 0 : grille_pitch / 2; 73 66 px = cx + xi + xoff; 74 67 py = cy + yi; ··· 80 73 } 81 74 } 82 75 83 - module speaker_mount_holes() { 84 - cx = outer_w / 2; 85 - cy = wall_t + spk_grille_cy; 86 - p = spk_mount_pattern / 2; 76 + // Detent recesses on each inner wall — small horizontal pockets where 77 + // the bottom-plate snap tab's bumps catch when the cover is fully 78 + // seated. 79 + module snap_recesses() { 80 + cz = snap_recess_cz; 87 81 88 - for (sx = [-p, p]) 89 - for (sy = [-p, p]) 90 - translate([cx + sx, cy + sy, outer_h - top_t - EPS]) 91 - cylinder(h = top_t + 2 * EPS, d = spk_mount_hole_d); 92 - } 82 + // Front wall (interior face at y = wall_t). 83 + translate([(outer_w - snap_recess_w) / 2, 84 + wall_t - EPS, 85 + cz - snap_recess_h / 2]) 86 + cube([snap_recess_w, snap_recess_d + EPS, snap_recess_h]); 93 87 94 - // 4 corner bosses inside the chassis. Each has a self-tap pilot hole 95 - // for an M3 screw coming up from the bottom plate; the screw cuts its 96 - // own threads in the PLA on first assembly. 97 - module bottom_plate_screw_bosses() { 98 - inset = bottom_plate_screw_inset; 88 + // Back wall (interior face at y = outer_d - wall_t). 89 + translate([(outer_w - snap_recess_w) / 2, 90 + outer_d - wall_t - snap_recess_d, 91 + cz - snap_recess_h / 2]) 92 + cube([snap_recess_w, snap_recess_d + EPS, snap_recess_h]); 99 93 100 - for (cx = [wall_t + inset, outer_w - wall_t - inset]) 101 - for (cy = [wall_t + inset, outer_d - wall_t - inset]) 102 - difference() { 103 - translate([cx, cy, 0]) 104 - cylinder(h = bottom_plate_boss_h, d = bottom_plate_boss_d); 105 - translate([cx, cy, -EPS]) 106 - cylinder(h = bottom_plate_boss_h + 2 * EPS, d = bottom_plate_pilot_d); 107 - } 108 - } 94 + // Left wall (interior face at x = wall_t). 95 + translate([wall_t - EPS, 96 + (outer_d - snap_recess_w) / 2, 97 + cz - snap_recess_h / 2]) 98 + cube([snap_recess_d + EPS, snap_recess_w, snap_recess_h]); 109 99 110 - module bottom_plate_screw_holes() { 111 - // No-op placeholder kept inside the chassis difference() block 112 - // for symmetry. Holes are produced by bottom_plate_screw_bosses(). 100 + // Right wall (interior face at x = outer_w - wall_t). 101 + translate([outer_w - wall_t - snap_recess_d, 102 + (outer_d - snap_recess_w) / 2, 103 + cz - snap_recess_h / 2]) 104 + cube([snap_recess_d + EPS, snap_recess_w, snap_recess_h]); 113 105 } 114 106 115 107 chassis();