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.

firmware: drive external MAX98357A on G21/G26/G32

Now that the external amps are wired up and the audio chain is
validated end-to-end on a breadboard, switch the I2S driver off the
onboard NS4168 (G19 BCLK / G22 DOUT / G33 LRCK) and onto the planned
external pins (G26 BCLK / G21 DOUT / G32 LRCK). The onboard amp
receives no I2S now, so its tiny speaker stays silent regardless.

Renamed `make_onboard_i2s` → `make_i2s` since there's only one I2S
driver in this firmware. README updates: status table, removal of the
"prototype-only path" gotcha that's no longer relevant, and the pinout
cheat sheet rewritten around the new active pins (G19/G22/G33 listed
as reserved-but-driven-silent, since they still feed the onboard amp
internally — we just don't send them anything).

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

+23 -28
+11 -15
firmware/README.md
··· 12 12 | --- | --- | 13 13 | Cargo build + flash | ✅ working | 14 14 | GPIO input (button G39) | ✅ working — debounced, active-low | 15 - | I2S TX (16-bit / 44.1 kHz / stereo, Philips) | ✅ working into onboard NS4168 | 15 + | I2S TX (16-bit / 44.1 kHz / stereo, Philips) | ✅ working into external MAX98357A | 16 16 | Continuous pink noise generator (Paul Kellet IIR) | ✅ xorshift32 white → pink filter, volume-scaled | 17 17 | Button state machine (short / long / double) | ✅ working | 18 18 | NVS persistence (volume + direction + playing) | ✅ working | ··· 20 20 | WiFi | ✅ working — STA, hostname `nightstand-<mac_hex>`, auto-reconnect | 21 21 | MQTT client + HA discovery | ✅ working — LWT, retained discovery on (re)connect, `cmd/play`/`cmd/volume`/`cmd/update` subscribed | 22 22 | OTA updates | ✅ working — `esp_https_ota` chunked download with HA progress bar, two-slot rollback | 23 - | Hardware: external MAX98357A + 1314 speaker | ❌ amps in transit; using onboard for now | 23 + | Hardware: external MAX98357A + GRS 3FR-4 speaker | ✅ assembled on breadboard | 24 24 25 25 ## Module layout 26 26 ··· 195 195 That's **`dout` before `ws`**, not `bclk → ws → dout` as you'd guess from reading "Bclk, Ws, Data" in audio convention. Getting them swapped produces: 196 196 - "Static" instead of a tone (data on the LRCK line, clock on the data line) 197 197 - A persistent quiet squeal during silence (WS clock ticking on the data input) 198 - - The NS4168 amp running warm (sustained switching, no real audio output) 198 + - The amp running warm (sustained switching, no real audio output) 199 199 200 200 The compiler accepts both orderings since the pin types are generic. Always check the signature against the version of `esp-idf-hal` you're on. Symptom: garbage output where you'd expect tone. 201 201 ··· 222 222 ### 7. `esp_https_ota` rejects plain HTTP unless an opt-in flag is set 223 223 224 224 Despite working fine at the protocol level, `esp_https_ota` validates the URL scheme and refuses plain HTTP unless `CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y` is in sdkconfig. With the flag missing, `esp_https_ota_begin` returns immediately with an error and never opens a socket — webserver logs show no GET requests. The flag is set in `sdkconfig.defaults`. 225 - 226 - ### 8. The onboard NS4168 + tiny speaker is a prototype-only path 227 - 228 - We're using the I2S pins that drive the built-in amp (G19 BCLK, G22 DOUT, G33 LRCK) for hello-world. The plan ([signal-chain.md](../reference/signal-chain.md)) is to switch to an external MAX98357A on G21/G26/G32 once amps arrive. Don't run sustained white noise on the built-in speaker — short test bursts only. The thermal warning on this is real (we briefly demonstrated it via the swapped-pins bug above). 229 225 230 226 ## Layout 231 227 ··· 248 244 └── .embuild/ # embuild-managed ESP-IDF clone (gitignored) 249 245 ``` 250 246 251 - ## Hardware-pinout cheat sheet (Atom Echo, prototype config) 247 + ## Hardware-pinout cheat sheet (Atom Echo) 252 248 253 249 | Pin | Function (this firmware) | Notes | 254 250 | --- | --- | --- | 255 251 | G39 | Button input | Active-low, on-board pull-up, input-only pin | 256 - | G19 | I2S BCLK → NS4168 | Onboard amp, prototype-only | 257 - | G22 | I2S DOUT → NS4168 | Onboard amp data | 258 - | G33 | I2S WS/LRCK → NS4168 | Onboard amp word select | 259 - | G27 | SK6812 RGB LED data | Not used yet | 260 - | G26 | GROVE yellow / future I2S BCLK to MAX98357A | Free | 261 - | G32 | GROVE white / future I2S WS to MAX98357A | Free | 262 - | G21 | Side header / future I2S DOUT to MAX98357A | Free | 252 + | G26 | I2S BCLK → MAX98357A | GROVE yellow | 253 + | G21 | I2S DOUT → MAX98357A | Side header | 254 + | G32 | I2S WS/LRCK → MAX98357A | GROVE white | 255 + | G27 | SK6812 RGB LED data | On-board, top-face button cap | 256 + | G19 | (reserved — onboard NS4168 BCLK) | Driven silent: no I2S routed here | 257 + | G22 | (reserved — onboard NS4168 DOUT) | Driven silent: no I2S routed here | 258 + | G33 | (reserved — onboard NS4168 LRCK / mic DATA) | Driven silent: no I2S routed here | 263 259 | G25 | Side header / spare | Free | 264 260 265 261 Authoritative pinout: [`../reference/atom-echo/pinmap.md`](../reference/atom-echo/pinmap.md).
+8 -7
firmware/src/audio.rs
··· 36 36 const FADE_DURATION_S: f32 = 0.7; 37 37 const FADE_STEP_PER_SAMPLE: f32 = 100.0 / (FADE_DURATION_S * SAMPLE_RATE as f32); 38 38 39 - /// Construct the I2S driver for the onboard NS4168 amp. 39 + /// Construct the I2S driver for the external MAX98357A amp. 40 40 /// 41 - /// Pin assignment matches the M5Stack Atom Echo schematic: 42 - /// BCLK = G19, DOUT = G22, WS/LRCK = G33, no MCLK. 41 + /// Pin assignment per `reference/signal-chain.md`: 42 + /// BCLK = G26 (GROVE yellow), DOUT = G21 (side header), 43 + /// WS/LRCK = G32 (GROVE white), no MCLK. 43 44 /// 44 45 /// **Argument order to `new_std_tx` is `(bclk, dout, mclk, ws)`** — not 45 46 /// the audio-convention `(bclk, ws, dout)` you'd guess. Getting it wrong 46 47 /// produces static instead of sound; see firmware/README.md gotcha #3. 47 - pub fn make_onboard_i2s( 48 + pub fn make_i2s( 48 49 i2s0: esp_idf_svc::hal::i2s::I2S0, 49 - bclk: esp_idf_svc::hal::gpio::Gpio19, 50 - dout: esp_idf_svc::hal::gpio::Gpio22, 51 - ws: esp_idf_svc::hal::gpio::Gpio33, 50 + bclk: esp_idf_svc::hal::gpio::Gpio26, 51 + dout: esp_idf_svc::hal::gpio::Gpio21, 52 + ws: esp_idf_svc::hal::gpio::Gpio32, 52 53 ) -> Result<I2sDriver<'static, I2sTx>> { 53 54 let config = StdConfig::new( 54 55 Config::default(),
+4 -6
firmware/src/main.rs
··· 71 71 // will refresh this once it knows whether we landed online or offline. 72 72 let _ = led_tx.send(LedSignal::Net(NetStatus::Connecting)); 73 73 74 - // Audio task: I2S to the onboard NS4168 (prototype). Will move to the 75 - // external MAX98357A on G21/G26/G32 once amps arrive. 76 - let i2s = audio::make_onboard_i2s( 74 + let i2s = audio::make_i2s( 77 75 peripherals.i2s0, 78 - pins.gpio19, // BCLK 79 - pins.gpio22, // DOUT 80 - pins.gpio33, // WS / LRCK 76 + pins.gpio26, // BCLK 77 + pins.gpio21, // DOUT 78 + pins.gpio32, // WS / LRCK 81 79 )?; 82 80 let _audio_handle = audio::spawn( 83 81 i2s,