//! Sound Machine — online-mode firmware v0.2.0. //! //! Five tasks plus a coordinator in `main`: //! - **audio** owns I2S, generates white noise, applies volume cycles, //! emits state snapshots to the network task on every change //! - **button** polls G39 and emits short / long / double events //! - **led** drives the SK6812 RGB LED, composing color from audio + net axes //! - **network** owns WiFi + MQTT; gatekeeps online/offline routing for //! button events, publishes state, subscribes to inbound commands //! - **coordinator** (this main loop) translates button events into audio //! commands (offline-only for short) and outbound events (always) //! //! All cross-task wiring is via FreeRTOS queue–backed channels (see //! `channels.rs`) carrying typed //! events / commands / signals. The network task is the gatekeeper: it //! decides whether short presses toggle audio locally (offline) or wait for //! HA to publish back via MQTT (online); long-press cycles volume locally in //! both modes; double-press is purely an MQTT gesture. //! //! See `firmware/README.md` and `reference/operating-modes.md` for the //! design and the gotchas. mod audio; mod button; mod channels; mod discovery; mod events; mod led; mod network; mod nvs; mod ota; mod secrets; mod state; use anyhow::Result; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::hal::gpio::PinDriver; use esp_idf_svc::hal::peripherals::Peripherals; use esp_idf_svc::nvs::{EspNvsPartition, NvsDefault}; use events::{AudioCommand, ButtonEvent, LedSignal, NetStatus, OutboundEvent}; use log::info; use nvs::NvsStore; use channels::channel; const FIRMWARE_VERSION: &str = env!("CARGO_PKG_VERSION"); fn main() -> Result<()> { esp_idf_svc::sys::link_patches(); esp_idf_svc::log::EspLogger::initialize_default(); info!("sound-machine v{FIRMWARE_VERSION} boot"); // Take the NVS partition once and clone it for both the audio NvsStore // and the WiFi stack (which uses NVS internally for STA cred caching). let nvs_partition = EspNvsPartition::::take()?; let nvs = NvsStore::open_with_partition(nvs_partition.clone())?; let initial_state = nvs.read(); // Peripherals split out, owned by the relevant tasks. let peripherals = Peripherals::take()?; let pins = peripherals.pins; let sys_loop = EspSystemEventLoop::take()?; // Channels. let (button_tx, button_rx) = channel::(8); let (audio_tx, audio_rx) = channel::(16); let (led_tx, led_rx) = channel::(32); let (out_tx, out_rx) = channel::(16); // Tell the LED to pulse cyan while we set things up. The network task // will refresh this once it knows whether we landed online or offline. let _ = led_tx.send(LedSignal::Net(NetStatus::Connecting)); let i2s = audio::make_i2s( peripherals.i2s0, pins.gpio26, // BCLK pins.gpio21, // DOUT pins.gpio32, // WS / LRCK )?; let _audio_handle = audio::spawn( i2s, nvs, initial_state, audio_rx, led_tx.clone(), out_tx.clone(), )?; // LED task on G27 via RMT channel 0. let _led_handle = led::spawn(peripherals.rmt.channel0, pins.gpio27, led_rx)?; // Button task on G39 (input-only; on-board pull-up; active-low). let button = PinDriver::input(pins.gpio39)?; let _button_handle = button::spawn(button, button_tx)?; // Network task: WiFi + MQTT, gatekeeper for online/offline routing. // Reads MAC and logs the topic prefix early, even if WiFi is unreachable. let _network_handle = network::spawn( peripherals.modem, sys_loop, nvs_partition, FIRMWARE_VERSION, audio_tx.clone(), led_tx.clone(), out_rx, )?; info!("all tasks spawned; entering coordinator loop"); // If the device was playing when power was cut, kick the audio task to // resume immediately. The audio task starts in its persisted-state's // `playing` value, so this is technically belt-and-braces — but it also // ensures the LED reflects "Playing" promptly. if initial_state.was_playing { let _ = audio_tx.send(AudioCommand::Play); } // Coordinator loop. Receives button events; sends every event to the // network task (which gatekeeps publish-vs-local-fallback by online // state) and, for long-press, also fires the local volume-cycle. loop { let Some(event) = button_rx.recv() else { anyhow::bail!("button channel closed"); }; // Every press gets a brief LED flash for tactile feedback. let _ = led_tx.send(LedSignal::PressFlash); match event { ButtonEvent::Short => { // Online: network task will publish; HA echoes back via cmd/play. // Offline: network task will translate to AudioCommand::Toggle. info!("coordinator: short → OutboundEvent (network gatekeeps)"); let _ = out_tx.send(OutboundEvent::Button(ButtonEvent::Short)); } ButtonEvent::Long => { // Volume cycles locally in both modes; network task publishes // the event for HA logging when online. info!("coordinator: long → AudioCommand::CycleVolume + OutboundEvent"); let _ = audio_tx.send(AudioCommand::CycleVolume); let _ = out_tx.send(OutboundEvent::Button(ButtonEvent::Long)); } ButtonEvent::Double => { // Pure-MQTT gesture; network task publishes if online, no-op offline. info!("coordinator: double → OutboundEvent"); let _ = out_tx.send(OutboundEvent::Button(ButtonEvent::Double)); } } } }