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