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.

first-pass 3D-printable enclosure in OpenSCAD

The assembled nightstand unit gets a printable shell. `make enclosure`
renders three STLs to `enclosure/build/`: the chassis (open-bottom shell
with integrated top), a separate bottom plate that screws on with four
M3 self-tap screws, and a translucent snooze cap. The chassis houses
the Atom Echo press-fit onto a daughterboard via M5's 5+4 "Atom Stack"
header pattern, a MAX98357A I2S amp, and the Adafruit 1314 speaker.
Snooze cap on the front-top half presses the Atom's button via a
captive plunger; speaker fires up through a perforated grille on the
rear-top half; USB-C exits a cutout in the back wall.

Layout decisions worth flagging for future archaeology:

* Speaker fires up — less directional, more room-filling for a
nightstand. Snooze cap on the near (front) half of the top, grille on
the far (rear) half.
* No internal divider between the snooze and speaker zones — single
shared chamber so the USB-C cable can route from the front-mounted
Atom straight to the rear cutout. White noise through a 3" driver
doesn't need a sealed speaker chamber.
* Press-fit on the 5+4 side headers as the only Atom mount — no screws
on the Atom itself. M5's mechanical drawing shows two M2 mounting
holes on the bottom face, but they aren't externally accessible on
the actual unit (covered by the label sticker).
* Bottom plate screws into corner bosses with M3 self-tap into PLA;
four register tabs on the plate keep it laterally aligned during
assembly before the screws snug.
* 1.5 mm fillet on visible top + side edges; cap top has a 1.5 mm
spherical dome for tactile cue in the dark.
* Daughterboard is hand-soldered protoboard, not a custom-fab PCB.
Standoffs at the four corners of a 38 × 38 footprint for now; will
re-tune once a real piece of perfboard is on the bench.

The reference docs needed several corrections that fell out of measuring
the actual hardware (photos of the bottom, back, and top-right faces
added under `reference/atom-echo/`):

* Bottom face has the 5+4 press-fit headers + a male HY2.0-4P GROVE in
the middle.
* USB-C is on the back face, not the top.
* The onboard NS4168 speaker grille shares the top face with the
button, not the front.
* Planned MAX98357A wiring: I2S DIN moves from G21 (which isn't exposed
on the Atom's external pins) to G25.
* Stale PAM8302 references in the Adafruit 1314 doc updated to
MAX98357A — that path was the rejected analog-amp architecture.

References:

* https://docs.m5stack.com/en/atom/atomecho
* https://www.adafruit.com/product/3006 (MAX98357A breakout)
* https://www.adafruit.com/product/1314 (3" 4Ω speaker)

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

+618 -19
+1
.gitignore
··· 3 3 *.swp 4 4 *.swo 5 5 .DS_Store 6 + enclosure/build/
+14 -4
Makefile
··· 6 6 7 7 MAKEFLAGS += --no-print-directory 8 8 9 - .PHONY: all firmware firmware-check firmware-flash firmware-flash-monitor firmware-monitor firmware-ota-publish firmware-clean clean help 9 + .PHONY: all firmware firmware-check firmware-flash firmware-flash-monitor firmware-monitor firmware-ota-publish firmware-clean enclosure enclosure-preview enclosure-clean clean help 10 10 11 11 all: firmware 12 12 ··· 31 31 firmware-clean: 32 32 $(MAKE) -C firmware clean 33 33 34 - clean: firmware-clean 34 + enclosure: 35 + $(MAKE) -C enclosure all 36 + 37 + enclosure-preview: 38 + $(MAKE) -C enclosure preview 39 + 40 + enclosure-clean: 41 + $(MAKE) -C enclosure clean 42 + 43 + clean: firmware-clean enclosure-clean 35 44 36 45 help: 37 46 @echo "sound-machine targets:" ··· 43 52 @echo " firmware-monitor serial monitor only" 44 53 @echo " firmware-ota-publish save .bin to OTA_LOCAL_DIR + publish latest_version" 45 54 @echo " firmware-clean cargo clean" 55 + @echo " enclosure render printable STLs from OpenSCAD into enclosure/build/" 56 + @echo " enclosure-preview open the enclosure assembly view in OpenSCAD" 57 + @echo " enclosure-clean remove enclosure/build/" 46 58 @echo " clean clean everything" 47 - @echo "" 48 - @echo "Future: 3D model rendering, etc., will live as sibling targets."
+40
enclosure/Makefile
··· 1 + # Enclosure build — render OpenSCAD parts to printable STLs. 2 + # 3 + # Run from the project root via `make enclosure`, or directly here. 4 + # Renders into build/ (gitignored). 5 + 6 + OPENSCAD ?= openscad-nightly 7 + BUILD = build 8 + PARTS_DIR = parts 9 + LIB_DIR = lib 10 + 11 + PARTS = chassis snooze-cap bottom-plate 12 + STLS = $(PARTS:%=$(BUILD)/%.stl) 13 + 14 + LIB_FILES = $(wildcard $(LIB_DIR)/*.scad) 15 + 16 + .PHONY: all preview clean help 17 + 18 + all: $(STLS) 19 + 20 + $(BUILD)/%.stl: $(PARTS_DIR)/%.scad $(LIB_FILES) | $(BUILD) 21 + $(OPENSCAD) -o $@ $< 22 + 23 + $(BUILD): 24 + mkdir -p $(BUILD) 25 + 26 + # Open the assembly view in OpenSCAD for visual inspection. 27 + preview: 28 + $(OPENSCAD) enclosure.scad & 29 + 30 + clean: 31 + rm -rf $(BUILD) 32 + 33 + help: 34 + @echo "enclosure targets:" 35 + @echo " all (default) render all parts to build/*.stl" 36 + @echo " preview open enclosure.scad in OpenSCAD for visual review" 37 + @echo " clean remove build/" 38 + @echo "" 39 + @echo " Override OPENSCAD=... if you have a different binary" 40 + @echo " (default: openscad-nightly)."
+44
enclosure/enclosure.scad
··· 1 + // Top-level assembly view. Shows all printed parts in their final 2 + // positions for visual sanity-checking. Open this file in OpenSCAD's 3 + // preview to inspect the design; do NOT export this as STL — render 4 + // each individual part file (parts/*.scad) for printable STLs. 5 + 6 + include <lib/params.scad> 7 + 8 + $fa = 2; 9 + $fs = 0.4; 10 + 11 + // Suppress the trailing chassis(); / snooze_cap(); / bottom_plate(); 12 + // calls in each part file by using `use <>` instead of `include <>`. 13 + use <parts/chassis.scad> 14 + use <parts/snooze-cap.scad> 15 + use <parts/bottom-plate.scad> 16 + 17 + // Printed shell. 18 + chassis(); 19 + 20 + // Bottom plate, attached from below. 21 + bottom_plate(); 22 + 23 + // Snooze cap at rest position (flange touching the underside of the top 24 + // panel). The cap origin sits with its flange's bottom-front-left at 25 + // z = outer_h - top_t - snooze_cap_flange_h, x/y centered on the well. 26 + flange_w_local = snooze_well_w + 2 * snooze_cap_flange_overhang; 27 + flange_d_local = snooze_well_d + 2 * snooze_cap_flange_overhang; 28 + translate([(outer_w - flange_w_local) / 2, 29 + wall_t + snooze_well_cy - flange_d_local / 2, 30 + outer_h - top_t - snooze_cap_flange_h - (inner_h - (atom_z_bottom + atom_body_h))]) 31 + color("white", 0.6) snooze_cap(); 32 + 33 + // Reference geometry — a translucent box where the Atom Echo lives, 34 + // for visual verification of clearances. Comment out for a clean view. 35 + %translate([(outer_w - atom_w) / 2, 36 + wall_t + snooze_well_cy - atom_d / 2, 37 + atom_z_bottom]) 38 + cube([atom_w, atom_d, atom_body_h]); 39 + 40 + // Reference geometry — speaker frame footprint hanging from the top panel. 41 + %translate([(outer_w - spk_frame) / 2, 42 + wall_t + spk_grille_cy - spk_frame / 2, 43 + outer_h - top_t - spk_total_d]) 44 + cube([spk_frame, spk_frame, spk_total_d]);
+177
enclosure/lib/params.scad
··· 1 + // Sound machine enclosure — shared dimensions. 2 + // Units: mm. Edit here; everything else derives. 3 + 4 + // ============================================================ 5 + // Atom Echo (M5Stack C008-C) 6 + // See: reference/atom-echo/dimensions.md 7 + // ============================================================ 8 + 9 + atom_w = 24; 10 + atom_d = 24; 11 + atom_body_h = 16.8; 12 + atom_corner_r = 3; 13 + 14 + // USB-C cutout in the Atom's own back face (vertically centered on body). 15 + atom_usb_w_native = 11.6; 16 + atom_usb_h_native = 7; 17 + 18 + // Bottom-face male HY2.0-4P GROVE connector. 19 + // Sticks down from the body; sits between the press-fit headers. 20 + grove_body_w = 8; 21 + grove_body_d = 4.5; 22 + grove_body_h = 2.5; 23 + 24 + // 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 + header_pitch = 2.54; 27 + header_left_pin_count = 5; 28 + header_right_pin_count = 4; 29 + header_edge_offset = 2.0; // body-edge to pin-row centerline (estimate) 30 + 31 + // ============================================================ 32 + // MAX98357A I2S amp breakout (Adafruit 3006 / generic clone) 33 + // See: reference/signal-chain.md 34 + // ============================================================ 35 + 36 + max_w = 21; 37 + max_d = 16; 38 + max_pcb_t = 1.6; 39 + 40 + // ============================================================ 41 + // Speaker — Adafruit 1314 (3" full-range, 4Ω 3W) 42 + // See: reference/speakers/adafruit-1314.md 43 + // ============================================================ 44 + 45 + spk_frame = 77.8; // square frame outer 46 + spk_total_d = 25.49; // total depth incl. magnet 47 + spk_mount_pattern = 60; // square M3 hole pattern (centers) 48 + spk_mount_hole_d = 3.4; // M3 clearance 49 + spk_cone_cutout = 67; // baffle opening; estimate, measure & update 50 + spk_magnet_clearance = 30; // air below baffle for magnet + breathing 51 + 52 + // ============================================================ 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. 56 + // ============================================================ 57 + 58 + dboard_t = 1.6; 59 + dboard_w = atom_w + 14; 60 + dboard_d = atom_d + 14; 61 + dboard_pin_h = 6; // male pin height above the daughterboard 62 + dboard_standoff_h = 6; // air below the daughterboard 63 + 64 + // ============================================================ 65 + // Snooze cap + well 66 + // ============================================================ 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 + snooze_cap_w = 70; 75 + 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 80 + 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 + snooze_cap_flange_overhang = 3; 84 + snooze_cap_flange_h = 2; 85 + 86 + // Well opening in the top panel — derived from the cap. 87 + snooze_well_w = snooze_cap_w + 2 * snooze_cap_clearance; 88 + snooze_well_d = snooze_cap_d + 2 * snooze_cap_clearance; 89 + 90 + // ============================================================ 91 + // Speaker grille (in the rear portion of the top panel) 92 + // ============================================================ 93 + 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. 98 + grille_diameter = spk_cone_cutout; 99 + 100 + // ============================================================ 101 + // Enclosure — overall printed shell 102 + // ============================================================ 103 + 104 + wall_t = 3; 105 + top_t = 4; // a touch thicker for rigidity around the speaker mount 106 + bottom_t = 3; 107 + 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; 112 + 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): 125 + // front_margin | snooze well | zone_gap | speaker grille | back_margin 126 + front_margin = 8; 127 + zone_gap = 6; 128 + back_margin = 6; 129 + 130 + // Internal volume. 131 + inner_w = max(dboard_w + 8, spk_frame + 6); // whichever is wider 132 + inner_d = front_margin + snooze_well_d + zone_gap + spk_frame + back_margin; 133 + inner_h = max(spk_magnet_clearance + 4, 134 + dboard_standoff_h + dboard_t + dboard_pin_h + atom_body_h + 3); 135 + 136 + // Outer envelope. 137 + outer_w = inner_w + 2 * wall_t; 138 + outer_d = inner_d + 2 * wall_t; 139 + outer_h = inner_h + top_t; // bottom is a separate plate; chassis is open-bottom 140 + 141 + // ============================================================ 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. 146 + // ============================================================ 147 + 148 + usb_hole_w = 14; 149 + usb_hole_h = 9; 150 + 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; 156 + 157 + // ============================================================ 158 + // Bottom plate — separate part, screws onto the chassis from below. 159 + // ============================================================ 160 + 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 167 + 168 + // ============================================================ 169 + // Component placement (X centered; Y measured from the front wall inside) 170 + // ============================================================ 171 + 172 + // Snooze well center, in chassis-internal coordinates (origin at front-left 173 + // inside corner of the chamber). 174 + 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;
+49
enclosure/lib/util.scad
··· 1 + // Shared modules / helpers for the enclosure parts. 2 + 3 + // A rectangular prism with the four vertical edges filleted by `r`. 4 + // Top and bottom are sharp; the part still sits flat on the print bed. 5 + // Bounding box is identical to a plain cube of the same size — corners 6 + // round inward. 7 + module rounded_prism(size, r) { 8 + linear_extrude(height = size[2]) 9 + offset(r = r) 10 + offset(r = -r) 11 + square([size[0], size[1]]); 12 + } 13 + 14 + // A rectangular prism with rounded vertical edges AND a filleted top — 15 + // the "softened brick" look. Bottom edges stay sharp so the part can sit 16 + // flat on the build plate (chassis prints open-bottom-down). Built as 17 + // the convex hull of cylinder feet at the bottom and a sphere at each 18 + // top corner, so the top edge is a true fillet. 19 + module rounded_box_top(size, r) { 20 + hull() { 21 + for (x = [r, size[0] - r]) 22 + for (y = [r, size[1] - r]) { 23 + translate([x, y, 0]) cylinder(h = 0.01, r = r); 24 + translate([x, y, size[2] - r]) sphere(r); 25 + } 26 + } 27 + } 28 + 29 + // A rectangular prism with rounded vertical edges and a gentle spherical 30 + // dome on top. `dome_h` is the rise of the center above the corners, and 31 + // the dome surface is a spherical section sized so it lands flat at the 32 + // bounding-box diagonal corners. Bottom stays sharp. 33 + module domed_box_top(size, r, dome_h) { 34 + // Spherical cap geometry: dome reaches z = size[2] at the diagonal 35 + // corners and z = size[2] + dome_h at the center. 36 + diag_half = sqrt(size[0] * size[0] + size[1] * size[1]) / 2; 37 + dome_r = (diag_half * diag_half + dome_h * dome_h) / (2 * dome_h); 38 + 39 + intersection() { 40 + // Tall prism — slightly taller than the dome peak, gets carved by 41 + // the sphere underside. 42 + rounded_prism([size[0], size[1], size[2] + dome_h + 0.5], r); 43 + 44 + // Big sphere whose upper surface is the dome. Local $fa keeps the 45 + // tessellation reasonable on a very large radius. 46 + translate([size[0] / 2, size[1] / 2, size[2] + dome_h - dome_r]) 47 + sphere(dome_r, $fa = 4); 48 + } 49 + }
+90
enclosure/parts/bottom-plate.scad
··· 1 + // Bottom plate — closes the open-bottom chassis from below and carries 2 + // the standoffs that hold the daughterboard. 3 + // 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. 8 + 9 + include <../lib/params.scad> 10 + include <../lib/util.scad> 11 + 12 + $fa = 2; 13 + $fs = 0.4; 14 + 15 + EPS = 0.05; 16 + 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. 27 + dboard_cx = outer_w / 2; 28 + dboard_cy = wall_t + snooze_well_cy; 29 + 30 + module bottom_plate() { 31 + difference() { 32 + // Plate body — rounded vertical corners to match the chassis, 33 + // sharp top and bottom edges (sits flat on the print bed). 34 + translate([0, 0, -bottom_plate_t]) 35 + rounded_prism([outer_w, outer_d, bottom_plate_t], chassis_round_r); 36 + 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); 45 + } 46 + 47 + // 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 + daughterboard_standoffs(); 51 + 52 + // Register tabs — locate the plate laterally inside the chassis 53 + // cavity during assembly. 54 + register_tabs(); 55 + } 56 + 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]); 64 + 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]); 69 + } 70 + 71 + module daughterboard_standoffs() { 72 + standoff_d = 5; 73 + standoff_id = 1.6; // pilot for self-tapping M2 74 + inset = 3; // distance from daughterboard edge to standoff center 75 + 76 + // Standoff top sits at z = dboard_standoff_h (so the daughterboard 77 + // bottom face rests there). 78 + for (sx = [dboard_cx - dboard_w / 2 + inset, 79 + dboard_cx + dboard_w / 2 - inset]) 80 + for (sy = [dboard_cy - dboard_d / 2 + inset, 81 + dboard_cy + dboard_d / 2 - inset]) 82 + difference() { 83 + translate([sx, sy, 0]) 84 + cylinder(h = dboard_standoff_h, d = standoff_d); 85 + translate([sx, sy, -EPS]) 86 + cylinder(h = dboard_standoff_h + 2 * EPS, d = standoff_id); 87 + } 88 + } 89 + 90 + bottom_plate();
+115
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. 4 + // 5 + // Coordinate system: origin at the bottom-front-left outer corner. 6 + // +X = right, +Y = back, +Z = up. 7 + 8 + include <../lib/params.scad> 9 + include <../lib/util.scad> 10 + 11 + $fa = 2; 12 + $fs = 0.4; 13 + 14 + EPS = 0.05; 15 + 16 + module chassis() { 17 + difference() { 18 + // Outer envelope — rounded vertical edges + filleted top, sharp 19 + // bottom so the part sits flat on the build plate. 20 + rounded_box_top([outer_w, outer_d, outer_h], chassis_round_r); 21 + 22 + // Inner cavity — open-bottom, so subtract a cube that extends 23 + // through z=0 and stops at the underside of the top panel. 24 + translate([wall_t, wall_t, -EPS]) 25 + cube([inner_w, inner_d, inner_h + EPS]); 26 + 27 + // Snooze well — rounded-corner through-hole in the top panel. 28 + translate([(outer_w - snooze_well_w) / 2, 29 + wall_t + snooze_well_cy - snooze_well_d / 2, 30 + outer_h - top_t - EPS]) 31 + rounded_prism([snooze_well_w, snooze_well_d, top_t + 2 * EPS], 32 + snooze_well_corner_r); 33 + 34 + // Speaker grille — a circular field of small holes in the rear 35 + // portion of the top panel, constrained to the cone diameter. 36 + speaker_grille(); 37 + 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, 44 + 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]); 47 + 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(); 52 + } 53 + 54 + // Bottom-plate screw bosses, 4 corners inside the chassis. 55 + bottom_plate_screw_bosses(); 56 + } 57 + 58 + module speaker_grille() { 59 + cx = outer_w / 2; 60 + cy = wall_t + spk_grille_cy; 61 + r = grille_diameter / 2; 62 + 63 + // Hexagonal-ish grid clipped to a circle. 64 + intersection() { 65 + translate([cx - r, cy - r, outer_h - top_t - EPS]) 66 + cube([2 * r, 2 * r, top_t + 2 * EPS]); 67 + 68 + union() { 69 + for (xi = [-r : grille_pitch : r]) 70 + for (yi = [-r : grille_pitch : r]) { 71 + // Stagger every other row by half-pitch for a nicer pattern. 72 + xoff = (round(yi / grille_pitch) % 2 == 0) ? 0 : grille_pitch / 2; 73 + px = cx + xi + xoff; 74 + py = cy + yi; 75 + if ((px - cx) * (px - cx) + (py - cy) * (py - cy) <= r * r) 76 + translate([px, py, outer_h - top_t - EPS]) 77 + cylinder(h = top_t + 2 * EPS, d = grille_hole_d); 78 + } 79 + } 80 + } 81 + } 82 + 83 + module speaker_mount_holes() { 84 + cx = outer_w / 2; 85 + cy = wall_t + spk_grille_cy; 86 + p = spk_mount_pattern / 2; 87 + 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 + } 93 + 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; 99 + 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 + } 109 + 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(). 113 + } 114 + 115 + chassis();
+72
enclosure/parts/snooze-cap.scad
··· 1 + // Snooze cap — translucent press button. Drops into the well in the top 2 + // panel from below; the flange catches on the underside of the top panel 3 + // as the up-stop. The Atom Echo's tactile dome under the plunger acts as 4 + // the return spring. 5 + // 6 + // Print in translucent or white PLA so the SK6812 LED diffuses through. 7 + // Print upside-down (cap top on the build plate) for the cleanest first 8 + // layer on the visible surface — no supports needed. 9 + // 10 + // Coordinate system: origin at the bottom-front-left of the flange. 11 + // The cap is rendered "right side up" — the flange is at z=0, the 12 + // visible top is at z=cap_total_h. 13 + 14 + include <../lib/params.scad> 15 + include <../lib/util.scad> 16 + 17 + $fa = 2; 18 + $fs = 0.4; 19 + 20 + EPS = 0.05; 21 + 22 + // Distance the plunger has to descend below the flange to reach the Atom 23 + // button. Derived from the chamber height and the Atom-on-daughterboard 24 + // stack — when the flange top touches the underside of the top panel, 25 + // the plunger bottom should just kiss the Atom button. 26 + atom_button_top_z = atom_z_bottom + atom_body_h; 27 + plunger_h = inner_h - atom_button_top_z; 28 + 29 + // Z layout of the cap, bottom-up: 30 + z_plunger_bot = 0; 31 + z_plunger_top = plunger_h; 32 + z_flange_bot = z_plunger_top; 33 + z_flange_top = z_flange_bot + snooze_cap_flange_h; 34 + z_body_bot = z_flange_top; 35 + z_body_top = z_body_bot + top_t + snooze_cap_h_above; 36 + 37 + // Outer flange XY footprint (also the cap origin reference). 38 + flange_w = snooze_well_w + 2 * snooze_cap_flange_overhang; 39 + flange_d = snooze_well_d + 2 * snooze_cap_flange_overhang; 40 + 41 + module snooze_cap() { 42 + // Plunger — centered over the Atom Echo footprint. 43 + translate([(flange_w - atom_w) / 2, 44 + (flange_d - atom_d) / 2, 45 + z_plunger_bot]) 46 + cube([atom_w, atom_d, plunger_h]); 47 + 48 + // Flange — wider than the well opening, catches the underside of 49 + // the top panel. 50 + translate([0, 0, z_flange_bot]) 51 + cube([flange_w, flange_d, snooze_cap_flange_h]); 52 + 53 + // Body — split into two stacked sections so we can round only the 54 + // visible top edges, while still having consistent rounded vertical 55 + // edges throughout (so the joint at z = top_t is seamless). 56 + body_x = (flange_w - snooze_cap_w) / 2; 57 + body_y = (flange_d - snooze_cap_d) / 2; 58 + 59 + // Lower body — passes through the well, hidden. Rounded vertical 60 + // edges, flat top/bottom. 61 + translate([body_x, body_y, z_body_bot]) 62 + rounded_prism([snooze_cap_w, snooze_cap_d, top_t], cap_round_r); 63 + 64 + // Upper body — visible above the top panel. Rounded vertical edges 65 + // and a gentle spherical dome on top. 66 + translate([body_x, body_y, z_body_bot + top_t]) 67 + domed_box_top([snooze_cap_w, snooze_cap_d, snooze_cap_h_above], 68 + cap_round_r, 69 + cap_dome_h); 70 + } 71 + 72 + snooze_cap();
reference/atom-echo/atom-back.jpg

This is a binary file and will not be displayed.

reference/atom-echo/atom-bottom.jpg

This is a binary file and will not be displayed.

reference/atom-echo/atom-top-right.jpg

This is a binary file and will not be displayed.

+12 -10
reference/atom-echo/dimensions.md
··· 2 2 3 3 - **Body**: 24 × 24 × 16.8 mm (near-cube) 4 4 - **Corner radius**: R3 (body), R0.5 (USB-C cutout) 5 - - **USB-C port**: on top face, ~11.6 mm wide 6 - - **Speaker grille**: front face 7 - - **Button**: top face (through the RGB LED button cap) 8 - - **Mounting**: 2 holes on bottom face, 12 mm apart, centered — M2 screws 9 - - **GROVE connector**: back face (HY2.0-4P) 5 + - **Top face**: shares the button (curved cap with embedded SK6812 RGB LED) on one side and the onboard NS4168 speaker grille (grid of small holes) on the other. Both occupy the same 24 × 24 mm face. 6 + - **USB-C port**: back face, ~11.6 mm wide cutout, vertically centered on the 16.8 mm body height. The HY2.0-4P GROVE connector body protrudes downward from the bottom face right at the rear edge, so its opening is also visible from this back angle — leave clearance for it when sizing the rear cutout. 7 + - **Side faces**: no connectors or features; just regulatory markings (CE / FCC) on the label. 8 + - **Front face**: small mic port (SPM1423), no other features. 9 + - **Bottom face**: female pin headers for press-fit "Atom Stack" mounting — 5-pin header on one side, 4-pin header on the other, plus a male HY2.0-4P GROVE connector centered between them. M5's mechanical drawing also shows two M2 screw holes on this face but they are not externally accessible on the actual unit (covered by the label sticker), so press-fit via the headers is the practical mount. 10 10 11 11 ## Enclosure implications 12 12 13 13 The Atom Echo itself will be the "compute module" inside a larger nightstand enclosure. Need to account for: 14 14 15 - - Cable exits: USB-C (power) and GROVE (if we use it) — both on opposite faces 16 - - Button access: the top-face button is the whole top surface, so the enclosure needs to either expose it or transmit the press mechanically 17 - - LED visibility: the SK6812 glows through the button — same access concern 18 - - Mic port: there's a small hole on the body for the mic (visible in dimension drawing); probably won't matter since we're only using it for output, but if we ever want wake-word later we'd need an acoustic path 19 - - Mounting: two M2 screw posts inside the enclosure, 12 mm apart 15 + - **Mounting**: a daughterboard sitting below the Atom with male pin headers in a matching 5+4 footprint (and optionally a female HY2.0-4P in the middle) friction-fits the Atom in place. Same daughterboard carries electrical signals to the MAX98357A. Atom Echo lifts straight up for service. This is M5's intended "Atom Stack" pattern. 16 + - **USB-C cable exit**: rear face of the Atom is flush against the rear wall of the enclosure, with a small rectangular cutout (≥12 × 7 mm to clear the port + plug shell) so any USB-C cable plugs in from outside — no hand reach into the enclosure required. 17 + - **Button + LED**: top face points up at the underside of the snooze cap. The cap covers the full 24 × 24 mm top face (the silent onboard NS4168 speaker grille shares this face but is unused), transmits press downward onto the Atom's button, and the SK6812 LED shines up through the (translucent) cap. 18 + - **Mic port**: small hole on the front face; output-only design ignores it. If wake-word ever becomes a goal we'd need an acoustic path through the enclosure. 20 19 21 20 ## Sources 22 21 23 22 - [Atom Echo product docs (M5Stack)](https://docs.m5stack.com/en/atom/atomecho) — dimensions section 24 23 - [module-size.jpg](./module-size.jpg) — mechanical drawing, downloaded from M5Stack docs 24 + - [atom-bottom.jpg](./atom-bottom.jpg) — photo of the actual unit's bottom face, showing the 5+4 press-fit headers and the central HY2.0-4P 25 + - [atom-back.jpg](./atom-back.jpg) — photo of the back face: USB-C port and the rear-edge view of the GROVE connector 26 + - [atom-top-right.jpg](./atom-top-right.jpg) — photo of the top + side faces: button outline + onboard speaker grille on top, regulatory label on the side
+2 -3
reference/signal-chain.md
··· 54 54 | GND | GND | | 55 55 | G26 (GROVE yellow) | BCLK | bit clock | 56 56 | G32 (GROVE white) | LRC | word select (LRCK) | 57 - | G21 (side header) | DIN | I2S serial data | 57 + | G25 (side header) | DIN | I2S serial data — only safely-available external GPIO (G19/G22/G23/G33 are reserved for the onboard NS4168, G27 is the RGB LED, G39 is the button) | 58 58 | — | SD | leave floating = mono (L+R summed) | 59 59 | — | GAIN | leave floating = 9 dB default | 60 60 ··· 104 104 105 105 - G26 (GROVE yellow) → I2S BCLK 106 106 - G32 (GROVE white) → I2S LRC 107 - - G21 (side header) → I2S DIN 108 - - G25 (side header) → free spare (future button, rotary encoder, IR, etc.) 107 + - G25 (side header) → I2S DIN 109 108 110 109 ## Parts list for this chain (per unit) 111 110
+2 -2
reference/speakers/adafruit-1314.md
··· 26 26 27 27 ## Why this is fine for our use 28 28 29 - - **Power match**: 3 W handling vs. PAM8302 delivering ~2.5 W into 4 Ω at 5V. Comfortable headroom; we'll never run the amp at max anyway. 30 - - **Impedance match**: PAM8302 is rated for 4Ω loads. Exact match. 29 + - **Power match**: 3 W handling vs. MAX98357A delivering ~3.2 W into 4 Ω at 5V. Essentially matched; white noise never runs near max anyway. 30 + - **Impedance match**: MAX98357A is rated for 4Ω loads. Exact match. 31 31 - **Size match**: 78 mm baffle + a few mm per side for the enclosure wall gives a face ~90 mm square. Reasonable nightstand footprint. 32 32 - **White noise doesn't care about bass extension**, so a sealed box with any sensible internal volume (~0.3 L+) will sound fine. Fill with polyfill / pillow stuffing to damp standing waves. 33 33