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.

at main 150 lines 5.8 kB view raw
1//! Sound Machine — online-mode firmware v0.2.0. 2//! 3//! Five tasks plus a coordinator in `main`: 4//! - **audio** owns I2S, generates white noise, applies volume cycles, 5//! emits state snapshots to the network task on every change 6//! - **button** polls G39 and emits short / long / double events 7//! - **led** drives the SK6812 RGB LED, composing color from audio + net axes 8//! - **network** owns WiFi + MQTT; gatekeeps online/offline routing for 9//! button events, publishes state, subscribes to inbound commands 10//! - **coordinator** (this main loop) translates button events into audio 11//! commands (offline-only for short) and outbound events (always) 12//! 13//! All cross-task wiring is via FreeRTOS queue–backed channels (see 14//! `channels.rs`) carrying typed 15//! events / commands / signals. The network task is the gatekeeper: it 16//! decides whether short presses toggle audio locally (offline) or wait for 17//! HA to publish back via MQTT (online); long-press cycles volume locally in 18//! both modes; double-press is purely an MQTT gesture. 19//! 20//! See `firmware/README.md` and `reference/operating-modes.md` for the 21//! design and the gotchas. 22 23mod audio; 24mod button; 25mod channels; 26mod discovery; 27mod events; 28mod led; 29mod network; 30mod nvs; 31mod ota; 32mod secrets; 33mod state; 34 35use anyhow::Result; 36use esp_idf_svc::eventloop::EspSystemEventLoop; 37use esp_idf_svc::hal::gpio::PinDriver; 38use esp_idf_svc::hal::peripherals::Peripherals; 39use esp_idf_svc::nvs::{EspNvsPartition, NvsDefault}; 40use events::{AudioCommand, ButtonEvent, LedSignal, NetStatus, OutboundEvent}; 41use log::info; 42use nvs::NvsStore; 43use channels::channel; 44 45const FIRMWARE_VERSION: &str = env!("CARGO_PKG_VERSION"); 46 47fn main() -> Result<()> { 48 esp_idf_svc::sys::link_patches(); 49 esp_idf_svc::log::EspLogger::initialize_default(); 50 51 info!("sound-machine v{FIRMWARE_VERSION} boot"); 52 53 // Take the NVS partition once and clone it for both the audio NvsStore 54 // and the WiFi stack (which uses NVS internally for STA cred caching). 55 let nvs_partition = EspNvsPartition::<NvsDefault>::take()?; 56 let nvs = NvsStore::open_with_partition(nvs_partition.clone())?; 57 let initial_state = nvs.read(); 58 59 // Peripherals split out, owned by the relevant tasks. 60 let peripherals = Peripherals::take()?; 61 let pins = peripherals.pins; 62 let sys_loop = EspSystemEventLoop::take()?; 63 64 // Channels. 65 let (button_tx, button_rx) = channel::<ButtonEvent>(8); 66 let (audio_tx, audio_rx) = channel::<AudioCommand>(16); 67 let (led_tx, led_rx) = channel::<LedSignal>(32); 68 let (out_tx, out_rx) = channel::<OutboundEvent>(16); 69 70 // Tell the LED to pulse cyan while we set things up. The network task 71 // will refresh this once it knows whether we landed online or offline. 72 let _ = led_tx.send(LedSignal::Net(NetStatus::Connecting)); 73 74 let i2s = audio::make_i2s( 75 peripherals.i2s0, 76 pins.gpio26, // BCLK 77 pins.gpio21, // DOUT 78 pins.gpio32, // WS / LRCK 79 )?; 80 let _audio_handle = audio::spawn( 81 i2s, 82 nvs, 83 initial_state, 84 audio_rx, 85 led_tx.clone(), 86 out_tx.clone(), 87 )?; 88 89 // LED task on G27 via RMT channel 0. 90 let _led_handle = led::spawn(peripherals.rmt.channel0, pins.gpio27, led_rx)?; 91 92 // Button task on G39 (input-only; on-board pull-up; active-low). 93 let button = PinDriver::input(pins.gpio39)?; 94 let _button_handle = button::spawn(button, button_tx)?; 95 96 // Network task: WiFi + MQTT, gatekeeper for online/offline routing. 97 // Reads MAC and logs the topic prefix early, even if WiFi is unreachable. 98 let _network_handle = network::spawn( 99 peripherals.modem, 100 sys_loop, 101 nvs_partition, 102 FIRMWARE_VERSION, 103 audio_tx.clone(), 104 led_tx.clone(), 105 out_rx, 106 )?; 107 108 info!("all tasks spawned; entering coordinator loop"); 109 110 // If the device was playing when power was cut, kick the audio task to 111 // resume immediately. The audio task starts in its persisted-state's 112 // `playing` value, so this is technically belt-and-braces — but it also 113 // ensures the LED reflects "Playing" promptly. 114 if initial_state.was_playing { 115 let _ = audio_tx.send(AudioCommand::Play); 116 } 117 118 // Coordinator loop. Receives button events; sends every event to the 119 // network task (which gatekeeps publish-vs-local-fallback by online 120 // state) and, for long-press, also fires the local volume-cycle. 121 loop { 122 let Some(event) = button_rx.recv() else { 123 anyhow::bail!("button channel closed"); 124 }; 125 126 // Every press gets a brief LED flash for tactile feedback. 127 let _ = led_tx.send(LedSignal::PressFlash); 128 129 match event { 130 ButtonEvent::Short => { 131 // Online: network task will publish; HA echoes back via cmd/play. 132 // Offline: network task will translate to AudioCommand::Toggle. 133 info!("coordinator: short → OutboundEvent (network gatekeeps)"); 134 let _ = out_tx.send(OutboundEvent::Button(ButtonEvent::Short)); 135 } 136 ButtonEvent::Long => { 137 // Volume cycles locally in both modes; network task publishes 138 // the event for HA logging when online. 139 info!("coordinator: long → AudioCommand::CycleVolume + OutboundEvent"); 140 let _ = audio_tx.send(AudioCommand::CycleVolume); 141 let _ = out_tx.send(OutboundEvent::Button(ButtonEvent::Long)); 142 } 143 ButtonEvent::Double => { 144 // Pure-MQTT gesture; network task publishes if online, no-op offline. 145 info!("coordinator: double → OutboundEvent"); 146 let _ = out_tx.send(OutboundEvent::Button(ButtonEvent::Double)); 147 } 148 } 149 } 150}