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: GRS 3FR-4 speaker fit, L-shaped cable clip

Updated for the actual speaker on the bench (GRS 3FR-4, not the
Adafruit 1314 we initially specced — same 60 mm M3 mounting pattern
but a much deeper magnet). Reference doc renamed and rewritten with
the new specs.

Geometry consequences:

* Speaker posts grow from 22 mm to 44 mm to position the frame plate
with 1 mm of clearance below the magnet (vibration isolation; the
alternative of resting the magnet on the plate would couple bass
into the nightstand surface).
* Chamber inner height grows to 46 mm (44 mm post + 2 mm foam tape
on top of the frame, compressed against the cover underside).
* Speaker frame extent goes from 78 mm to 95 mm (the GRS is wider
across the mounting ears), so the chassis grows to ~107×162×49 mm
assembled.

Cable clip rework:

* Was a closed rectangular tunnel centered on the back wall — over-
engineered (the goal was strain relief, not a zip-tie pass-through)
and conflicted with the centered back snap-fit tab.
* Now an open L-shape (post + cap), positioned ~14 mm from the side
wall to clear the snap tab. The cable drops in from above during
assembly and rests in the L corner — post on the chamber-interior
side, cap extending toward the side wall, cable contained on three
sides with the open side facing the wall.
* Cable notch in the back wall and matching notch in the back edge
of the bottom plate shifted to match the L-clip's X position.

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

+139 -129
+57 -53
enclosure/lib/params.scad
··· 15 15 atom_body_h = 16.8; 16 16 atom_corner_r = 3; 17 17 18 - // USB-C cutout in the Atom's own back face (vertically centered on body). 19 18 atom_usb_w_native = 11.6; 20 19 atom_usb_h_native = 7; 21 20 22 - // Bottom-face male HY2.0-4P GROVE connector. 23 21 grove_body_w = 8; 24 22 grove_body_d = 4.5; 25 23 grove_body_h = 2.5; 26 24 27 - // Bottom-face press-fit headers (the "Atom Stack" interface). 28 25 header_pitch = 2.54; 29 26 header_left_pin_count = 5; 30 27 header_right_pin_count = 4; ··· 40 37 max_pcb_t = 1.6; 41 38 42 39 // ============================================================ 43 - // Speaker — Adafruit 1314 (3" full-range, 4Ω 3W) 44 - // See: reference/speakers/adafruit-1314.md 40 + // Speaker — GRS 3FR-4 (3" full-range, 4Ω, 25W max). 41 + // See: reference/speakers/grs-3fr-4.md 42 + // 43 + // Listed dimensions (Amazon / Parts Express): 3.2" × 3.7" × 2". 44 + // Treating 3.7" (94 mm) as the largest lateral extent (across the 45 + // mounting ears), and the magnet hangs ~43 mm below the frame plate 46 + // based on user measurement. spk_post_h is set to give 1 mm of 47 + // clearance between magnet bottom and bottom plate. 45 48 // ============================================================ 46 49 47 - spk_frame = 77.8; // square frame outer 48 - spk_total_d = 25.49; // total depth incl. magnet 49 - spk_mount_pattern = 60; // square M3 hole pattern (centers) 50 - spk_mount_hole_d = 3.4; // M3 clearance 51 - spk_cone_cutout = 67; // baffle opening; estimate, measure & update 52 - spk_magnet_clearance = 30; // air below baffle for magnet + breathing 50 + spk_frame = 95; // largest lateral extent (mounting-ear to mounting-ear) 51 + spk_mount_pattern = 60; // square M3 hole pattern (centers) 52 + spk_mount_hole_d = 3.4; // M3 clearance 53 + spk_cone_cutout = 67; // baffle opening (estimate; measure & update) 53 54 54 55 // ============================================================ 55 56 // Daughterboard — protoboard the Atom presses onto. ··· 58 59 dboard_t = 1.6; 59 60 dboard_w = atom_w + 14; 60 61 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 62 + dboard_pin_h = 6; 63 + dboard_standoff_h = 6; 63 64 64 65 // ============================================================ 65 66 // Snooze cap + well ··· 87 88 grille_diameter = spk_cone_cutout; 88 89 89 90 // ============================================================ 91 + // Speaker mounting posts on the bottom plate. 92 + // 93 + // Speaker rests on top of the posts; M3 self-tap screws come down 94 + // through the speaker's mounting tabs into the post tops. With the 95 + // speaker thus mounted to the base, the cover above is purely 96 + // cosmetic — no fasteners pass through it. 97 + // 98 + // Post height: 44 mm puts the magnet 1 mm above the bottom plate 99 + // (verified against the GRS 3FR-4's ~43 mm magnet protrusion). Bump 100 + // up if you want more vibration isolation; drop to 42 mm if you want 101 + // the magnet to seat on the plate (not recommended — couples magnet 102 + // vibration to the plate and onward to the nightstand surface). 103 + // ============================================================ 104 + 105 + spk_post_h = 44; 106 + spk_post_d = 6; 107 + spk_post_pilot_d = 2.5; 108 + 109 + // Foam tape on top of the speaker frame, compressed against the cover 110 + // underside when assembled. Adds vibration damping + a light air seal. 111 + foam_tape_t = 2; 112 + 113 + // ============================================================ 90 114 // Enclosure — overall printed shell 91 115 // ============================================================ 92 116 ··· 107 131 108 132 inner_w = max(dboard_w + 8, spk_frame + 6); 109 133 inner_d = front_margin + snooze_well_d + zone_gap + spk_frame + back_margin; 110 - inner_h = max(spk_magnet_clearance + 4, 134 + inner_h = max(spk_post_h + foam_tape_t, 111 135 dboard_standoff_h + dboard_t + dboard_pin_h + atom_body_h + 3); 112 136 113 137 outer_w = inner_w + 2 * wall_t; ··· 115 139 outer_h = inner_h + top_t; 116 140 117 141 // ============================================================ 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; 134 - 135 - // ============================================================ 136 142 // 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. 143 + // bottom of the back wall. The exit is offset to ~1/4 of the way 144 + // across the back so it clears the centered snap-fit tab. An L-shaped 145 + // clip on the bottom plate at the same X position routes and tensions 146 + // the cable. 140 147 // ============================================================ 141 148 142 - cable_diameter = 4; // typical USB-C cable jacket OD 149 + cable_diameter = 4; 150 + 151 + cable_x_offset = 14; // X position of the cable centerline (close to the side wall) 143 152 144 153 cable_notch_w = cable_diameter + 2; 145 154 cable_notch_h = cable_diameter + 2; 146 155 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 + // L-clip dimensions. Post on one side of the cable, cap above 157 + // extending laterally over the cable. The cable presses sideways into 158 + // the open side of the L, and the post + cap + bottom plate contain 159 + // it on three sides. 160 + clip_post_w = 2; 161 + clip_cap_h = 1.5; 162 + clip_cap_overhang = cable_diameter + 1; 163 + clip_l = 6; // length along the cable axis 164 + clip_y_from_back = 14; // distance from the back wall to clip center 152 165 153 166 // ============================================================ 154 167 // 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. 161 168 // ============================================================ 162 169 163 170 snap_tab_w = 14; 164 171 snap_tab_t = 2; 165 172 snap_tab_h = 8; 166 - snap_tab_clearance = 0.3; // gap between tab and chassis inner wall 173 + snap_tab_clearance = 0.3; 167 174 168 - snap_bump_d = 2; // hemisphere diameter 169 - snap_bump_inset_from_top = 1.5; // bump center below tab top 175 + snap_bump_d = 2; 176 + snap_bump_inset_from_top = 1.5; 170 177 171 - // Recess on the chassis inner wall — slightly larger than the bump 172 - // for a positive click without binding. 173 178 snap_recess_w = snap_tab_w + 2; 174 179 snap_recess_h = snap_bump_d + 1; 175 180 snap_recess_d = snap_bump_d / 2 + 0.4; 176 - // Vertical center of the recess in chassis coords (chassis open bottom = z=0). 177 181 snap_recess_cz = snap_tab_h - snap_bump_inset_from_top; 178 182 179 183 // ============================================================
+26 -24
enclosure/parts/bottom-plate.scad
··· 37 37 translate([0, 0, -bottom_plate_t]) 38 38 rounded_prism([outer_w, outer_d, bottom_plate_t], chassis_round_r); 39 39 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, 40 + // Cable notch in the back edge of the plate, aligned with 41 + // the chassis notch — together they form a small rectangular 42 + // hole flush with the bottom of the unit. 43 + translate([cable_x_offset - cable_notch_w / 2, 44 44 outer_d - wall_t - EPS, 45 45 -bottom_plate_t - EPS]) 46 46 cube([cable_notch_w, wall_t + 2 * EPS, bottom_plate_t + 2 * EPS]); ··· 89 89 } 90 90 } 91 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. 92 + // L-shaped cable clip: a short vertical post on the chamber-interior 93 + // side of the cable with a horizontal cap above, extending toward the 94 + // side wall. Cable rests in the inside corner of the L; cable is 95 + // dropped in from above during assembly. Position matches the back- 96 + // wall cable notch (offset horizontally) so the cable runs straight 97 + // from the clip to the notch. 95 98 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]); 99 + post_h = cable_diameter + clip_cap_h; 100 + cap_w = clip_post_w + clip_cap_overhang; 101 + cap_z = post_h - clip_cap_h; 102 + // Post sits on the chamber-interior side of the cable; cap extends 103 + // outward (toward the side wall). 104 + post_x = cable_x_offset + cable_diameter / 2; 105 + cap_x = post_x + clip_post_w - cap_w; 106 + clip_y = outer_d - wall_t - clip_y_from_back; 105 107 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 - } 108 + translate([0, clip_y - clip_l / 2, 0]) { 109 + // Vertical post (right leg of the L, on the chamber-interior side). 110 + translate([post_x, 0, 0]) 111 + cube([clip_post_w, clip_l, post_h]); 112 + // Horizontal cap (top leg, extending toward the side wall). 113 + translate([cap_x, 0, cap_z]) 114 + cube([cap_w, clip_l, clip_cap_h]); 115 + } 114 116 } 115 117 116 118 // Four snap-fit tabs centered on each side of the cavity. Each carries
+3 -2
enclosure/parts/chassis.scad
··· 37 37 // the speaker; no M3 holes anywhere on the top panel. 38 38 speaker_grille(); 39 39 40 - // Cable notch in the back wall, at the bottom edge. The USB 40 + // Cable notch in the back wall, at the bottom edge. Offset 41 + // horizontally to clear the centered snap-fit tab. The USB 41 42 // cable plugs into the Atom internally and exits here. 42 - translate([(outer_w - cable_notch_w) / 2, 43 + translate([cable_x_offset - cable_notch_w / 2, 43 44 outer_d - wall_t - EPS, 44 45 0]) 45 46 cube([cable_notch_w, wall_t + 2 * EPS, cable_notch_h]);
+2 -2
reference/signal-chain.md
··· 29 29 │ bridge-tied speaker output (V+ / V−) 30 30 │ no common ground to the speaker 31 31 32 - Adafruit 1314 (4Ω 3W) 32 + GRS 3FR-4 (4Ω 25W max) 33 33 ``` 34 34 35 35 ## MAX98357A specs ··· 112 112 | ---- | ------ | ----- | 113 113 | M5Stack Atom Echo | owned | | 114 114 | MAX98357A breakout | [Adafruit 3006][adafruit-3006] (~$6) or generic Amazon/AliExpress (~$3) | need 2 total for both units | 115 - | Adafruit 1314 speaker | owned | | 115 + | GRS 3FR-4 speaker | owned | see [reference/speakers/grs-3fr-4.md](./speakers/grs-3fr-4.md) | 116 116 | 5V 2A USB wall adapter + USB-C cable | off-the-shelf | UL-listed brick | 117 117 | Dupont wires / hookup wire | stock | | 118 118
-48
reference/speakers/adafruit-1314.md
··· 1 - # Adafruit 1314 — 3" speaker, 4Ω 3W 2 - 3 - Full-range driver, generic Chinese OEM, sold by Adafruit as ["Speaker - 3" Diameter - 4 Ohm 3 Watt"][adafruit-1314]. Chris already has two. 4 - 5 - ## Published specs 6 - 7 - | Spec | Value | 8 - | ----------- | ----- | 9 - | Impedance | 4 Ω | 10 - | Max power | 3 W | 11 - | Cone diameter | 3" (76 mm nominal) | 12 - | Frame (L×W×D) | 77.8 × 77.8 × 25.49 mm | 13 - | Weight | 50.48 g | 14 - | Mounting | 4 tabs, 60 mm apart | 15 - 16 - ## Unknown / to measure from physical unit 17 - 18 - These aren't in the Adafruit listing and the Verical-hosted datasheet was unreachable at the time of this research. We'll measure when we have a unit on the bench: 19 - 20 - - **Cone cutout diameter** (baffle hole) — estimate ~65–70 mm based on standard 3" driver dimensions 21 - - **Mounting hole diameter** (what screw fits the tabs) — likely M3 or similar 22 - - **Magnet diameter and depth** — sets rear clearance needed inside the enclosure 23 - - **Basket depth above / below the mounting flange** 24 - - **Frequency response** — not published; irrelevant for white noise but would matter if we wanted music quality 25 - - **Thiele-Small parameters** (Fs, Qts, Vas) — not published; fine, we're using a sealed box and not tuning 26 - 27 - ## Why this is fine for our use 28 - 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 - - **Size match**: 78 mm baffle + a few mm per side for the enclosure wall gives a face ~90 mm square. Reasonable nightstand footprint. 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 - 34 - ## Enclosure design notes 35 - 36 - - Baffle opening will need to be cut to match the cone cutout (TBD from measurement). 37 - - Four M3 (assumed) mounting holes in a 60 mm square pattern on the inside of the baffle. 38 - - Magnet clearance at the rear: leave at least 30 mm behind the mounting flange until we measure. 39 - - The 25.49 mm total depth means the whole driver (including magnet) fits within ~26 mm of baffle depth. 40 - - Seal the baffle-to-frame joint (gasket, foam, or printed lip) to avoid air leaks that kill bass coupling. 41 - 42 - ## Sources 43 - 44 - - [Adafruit product page for 1314][adafruit-1314] 45 - - [Verical datasheet link (currently unreachable)][verical-1314] — referenced here for future attempts 46 - 47 - [adafruit-1314]: https://www.adafruit.com/product/1314 48 - [verical-1314]: https://www.verical.com/datasheet/adafruit-speakers-1314-5770604.pdf
+51
reference/speakers/grs-3fr-4.md
··· 1 + # GRS 3FR-4 — 3" full-range driver, 4 Ω, 25 W max 2 + 3 + Full-range driver from GRS, sold via Amazon and Parts Express. Chris has two. 4 + 5 + ## Published specs 6 + 7 + | Spec | Value | 8 + | ---- | ----- | 9 + | Impedance | 4 Ω | 10 + | Max power | 25 W | 11 + | Frequency response | up to ~20 kHz (no low-end roll-off published) | 12 + | Cone diameter | 3" (76 mm nominal) | 13 + | Item dimensions (D × W × H) | 3.2 × 3.7 × 2 in (≈ 81 × 94 × 51 mm) | 14 + | Mounting | 4 tabs at 60 mm pattern | 15 + 16 + ## Measured / inferred from the unit on the bench 17 + 18 + These came out of sizing the enclosure to the actual hardware: 19 + 20 + - **Magnet protrusion below the frame plate**: ≈ 43 mm. The bottom-plate posts in the enclosure are sized 44 mm tall to give 1 mm of magnet-to-plate clearance. 21 + - **Cone protrusion above the frame plate**: ≈ 8 mm (= total depth 51 − magnet 43, approximately). 22 + - **Frame outer extent**: ≈ 94 mm at the largest lateral dimension (likely tip-to-tip across the mounting ears). Reflected in `spk_frame = 95` in the enclosure params. 23 + 24 + ## Unknown / to measure when convenient 25 + 26 + - **Cone cutout diameter** (the baffle hole that needs to clear the moving cone) — estimated at ~67 mm in the params; verify with a caliper and update. 27 + - **Mounting hole diameter** — assumed M3 clearance. Verify against the actual ears. 28 + - **Frequency response curve** — not published. Irrelevant for white noise. 29 + - **Thiele-Small parameters** (Fs, Qts, Vas) — not published. Fine, our enclosure isn't tuned. 30 + 31 + ## Why this driver is fine for our use 32 + 33 + - **Power match**: 25 W handling vs. MAX98357A delivering ~3.2 W into 4 Ω at 5 V. Massive headroom — the amp is the limit, never the driver. 34 + - **Impedance match**: MAX98357A is rated for 4 Ω loads. Exact match. 35 + - **Size**: 3" full-range, ~95 mm frame, sits comfortably on a nightstand. The ~50 mm depth (deeper magnet than a 3 W full-range driver would have) drives a slightly taller enclosure but nothing dramatic. 36 + - **White noise doesn't care about bass extension**, so a non-sealed printed box stuffed with polyfill (or not) will sound fine. 37 + 38 + ## Enclosure design notes 39 + 40 + - Baffle opening (grille pattern in the cover) is sized to ~67 mm — tune to the actual cone cutout once measured. 41 + - Four M3 mounting holes in a 60 mm square pattern. Speaker mounts to four 44 mm tall posts on the bottom plate, M3 self-tap into the post tops; no screws or holes through the cover top panel. 42 + - Magnet hangs ~43 mm below the frame; with 44 mm posts the magnet bottom sits 1 mm above the bottom plate (vibration isolation; not resting on the plate). 43 + - Foam tape on top of the frame compresses against the cover underside when the cover snaps on, giving a light air seal + damping any frame-to-cover rattle. 44 + 45 + ## Sources 46 + 47 + - [GRS 3FR-4 product listing on Amazon][amazon-grs-3fr-4] 48 + - [GRS speakers on Parts Express][parts-express-grs] — search "3FR-4" 49 + 50 + [amazon-grs-3fr-4]: https://www.amazon.com/dp/B00K2ESJZ2 51 + [parts-express-grs]: https://www.parts-express.com/cat/grs-speakers/178