Skip to main content

dmx_interface/
main.rs

1//! DMX interface firmware for ESP32-S2 (Lolin S2 Mini).
2//!
3//! Current scope: status LED on GPIO7 (LEDC PWM, own embassy task), user
4//! button on GPIO5 (power-up factory-reset hold plus click actions),
5//! USB-CDC with esptool auto-reset, ring-buffer logging over the CDC port,
6//! panic reports persisted across resets, and a `WiFi` network stack (AP
7//! with DHCP server or station mode, AP fallback).
8
9#![no_std]
10#![no_main]
11// In this binary crate every module is private, so `missing_docs` only
12// gates crate-level items (the `//!` header above). Module and API doc
13// coverage is enforced separately by scripts/check-rust-docs.sh plus the
14// warning-denied rustdoc build (`inv docs:rustdoc`).
15#![deny(missing_docs)]
16#![allow(dead_code)]
17
18extern crate alloc;
19
20// Module docs live as `//!` at the top of each module file. Do NOT also put
21// `///` docs on these declarations: rustdoc then resolves the module's
22// intra-doc links in this (parent) module's scope and every unqualified link
23// breaks — see AGENTS.md "//!" only.
24mod boards;
25mod config;
26mod hardware;
27mod logging;
28mod net;
29mod panic_report;
30mod storage;
31
32use embassy_executor::Spawner;
33use embassy_time::{Duration, Timer};
34use esp_alloc::HeapRegion;
35use esp_hal::{
36    clock::CpuClock, interrupt::software::SoftwareInterruptControl, timer::timg::TimerGroup,
37};
38
39use hardware::led::LedStatus;
40
41esp_bootloader_esp_idf::esp_app_desc!();
42
43/// Clears the panic boot-loop counter once the firmware ran stably for 30 s.
44#[embassy_executor::task]
45async fn stable_marker() {
46    Timer::after(Duration::from_secs(30)).await;
47    panic_report::mark_stable();
48}
49
50/// Entry point.
51#[esp_rtos::main]
52async fn main(spawner: Spawner) -> ! {
53    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::max()));
54
55    // ── Heap allocator ─────────────────────────────────────────────────────
56    {
57        /// Heap size in bytes (internal RAM — the `WiFi` driver and smoltcp
58        /// need ~100 KiB per esp-radio's examples, plus panic-report replay
59        /// and NVS reads).
60        const HEAP_SIZE: usize = 128 * 1024;
61        /// Backing memory for the heap allocator, placed in `.dram2_uninit`
62        /// (the 136 KiB region above the stack top that nothing else uses —
63        /// see esp-hal's `dram2.x`), keeping the main `dram_seg` free for
64        /// the `WiFi` driver's statics and the stack.
65        #[unsafe(link_section = ".dram2_uninit")]
66        static mut HEAP_MEM: [u8; HEAP_SIZE] = [0u8; HEAP_SIZE];
67        let heap_ptr = core::ptr::addr_of_mut!(HEAP_MEM).cast::<u8>();
68        // SAFETY: `heap_ptr` points to the whole `HEAP_MEM` array, which is
69        // valid for `'static`, exclusively owned by the allocator afterwards,
70        // handed over exactly once, and `size > 0`.
71        let region = unsafe {
72            HeapRegion::new(
73                heap_ptr,
74                HEAP_SIZE,
75                esp_alloc::MemoryCapability::Internal.into(),
76            )
77        };
78        // SAFETY: the heap region is registered exactly once at init time,
79        // before anything allocates.
80        unsafe {
81            esp_alloc::HEAP.add_region(region);
82        }
83    }
84
85    // ── Embassy scheduler ──────────────────────────────────────────────────
86    let timg0 = TimerGroup::new(peripherals.TIMG0);
87    let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
88    esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);
89
90    // ── Logging + last-panic report ────────────────────────────────────────
91    // Must follow esp_rtos::start (uptime timestamps need the time driver)
92    // and precede the USB spawn, so a stored panic report is already in the
93    // ring when the first host connects.
94    logging::init();
95    storage::init(peripherals.FLASH);
96    // Load the persisted config and push it into every consumer (LED
97    // brightness today, the network stack below).
98    let config = storage::load();
99    config.apply();
100    panic_report::report_last_panic();
101
102    // ── USB-CDC (esptool auto-reset + log drain) ───────────────────────────
103    spawner.spawn(
104        hardware::usb::task(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19)
105            .expect("USB task spawn failed"),
106    );
107    spawner.spawn(stable_marker().expect("stable marker spawn failed"));
108
109    // ── Status LED (LEDC PWM, own task) ─────────────────────────────────────
110    hardware::led::set(LedStatus::Startup);
111    spawner.spawn(
112        hardware::led::task(peripherals.LEDC, peripherals.GPIO7).expect("LED task spawn failed"),
113    );
114
115    // ── User button (GPIO5): power-up factory reset, then click actions ────
116    // The hold check must finish before the network starts, so a triggered
117    // reset never brings the radio up just to reboot again.
118    let button = hardware::button::input(peripherals.GPIO5);
119    hardware::button::boot_factory_reset(&button).await;
120    spawner.spawn(hardware::button::task(button).expect("button task spawn failed"));
121
122    // ── WiFi (AP/STA bring-up + embassy-net stack) ─────────────────────────
123    spawner.spawn(net::task(peripherals.WIFI, config, spawner).expect("network task spawn failed"));
124
125    log::info!("Init complete");
126
127    // TODO(phase 6): switch to Ok only once the network is up — the old
128    // firmware waited for NETWORK_READY + 2 s before LED_MODE_NORMAL.
129    // Until then, keep the Startup breathing visible for 3 s (three full
130    // breaths at the 1 s period) before settling into the steady Ok state.
131    Timer::after(Duration::from_secs(3)).await;
132    hardware::led::set(LedStatus::Ok);
133
134    // No main-loop duties yet — Phase 8 moves system work into tasks.
135    loop {
136        core::future::pending::<()>().await;
137    }
138}