npx skills add ...
npx skills add psytraxx/esp32-homecontrol-no-std-rs --skill esp32-rust-embedded
Expert embedded Rust development for ESP32 microcontrollers using no-std, Embassy async framework, and the ESP-RS ecosystem (esp-hal, esp-rtos, esp-radio). Use when building, debugging, flashing, or adding features to ESP32 projects. Covers sensor integration (ADC, GPIO, I2C, SPI), power management (deep sleep, RTC memory), WiFi networking, MQTT clients, display drivers, async task patterns, memory allocation, error handling, and dependency management. Ideal for LilyGO boards, Espressif chips (ESP32, ESP32-S3, ESP32-C3), and any no-std Xtensa/RISC-V embedded development.
npx skills add psytraxx/esp32-homecontrol-no-std-rs --skill esp32-rust-embedded
Expert guidance for no-std Rust development on ESP32 microcontrollers using the ESP-RS ecosystem and Embassy async framework.
edition = "2024" in Cargo.toml)rust-version = "1.88")src/bin/main.rs (not src/main.rs) — [[bin]] entry required in Cargo.tomlLinker error: undefined symbol _stack_start
build.rs has linkall.x configurationundefined symbol: esp_rtos_* ("esp-radio has no scheduler enabled")
Environment variable errors
env!() macroesp-alloc for dynamic allocationheapless collections with compile-time capacitystatic_cell::StaticCell for 'static lifetime requirementsAvoid cloning when possible.
Channel (multiple values)
Signal (single notification)
esp_rtos::start() now requires 2 args (timer + software interrupt)esp_radio::init() removed; call esp_radio::wifi::new() directly#[esp_hal::ram(reclaimed)] instead of #[unsafe(link_section = ".dram2_uninit")]Update Embassy crates together:
Note: wifi option now requires both unstable-hal and alloc.
Note: embassy option requires unstable-hal.
embassy-executor = { version = "0.10.0", features = ["log"] }
embassy-time = { version = "0.5.0", features = ["log"] }
embassy-net = { version = "0.9.1", features = ["dhcpv4", "log", "medium-ethernet", "tcp", "udp"] }
embassy-sync = { version = "0.7.2" }esp-radio (WiFi) -> esp-rtos (scheduler) -> esp-hal (HAL) -> esp-phy (PHY)
embassy-executor -> embassy-time -> embassy-sync -> embassy-net# Install ESP toolchain (one-time)
espup install
source $HOME/export-esp.sh
# Configure credentials (.env file)
cp .env.dist .env
# Edit: WIFI_SSID, WIFI_PSK, MQTT_HOSTNAME, MQTT_USERNAME, MQTT_PASSWORD# Quick build and flash
./run.sh
# Manual release build (recommended)
cargo run --release
# Debug build (slower on device)
cargo run[profile.dev]
opt-level = "s" # Rust debug too slow for ESP32
[profile.release]
lto = 'fat'
opt-level = 's'
codegen-units = 1let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt = esp_hal::interrupt::software::SoftwareInterruptControl::new(
peripherals.SW_INTERRUPT
);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);#![no_std]
#![no_main]
// Recommended lints (now standard in esp-generate templates)
#![deny(clippy::mem_forget)] // esp-hal types must not be mem::forgotten
#![deny(clippy::large_stack_frames)]
use embassy_executor::Spawner;
use esp_hal::{clock::CpuClock, timer::timg::TimerGroup};
esp_bootloader_esp_idf::esp_app_desc!();
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
// Initialize logger (reads RUST_LOG env var at compile time via esp-config)
esp_println::logger::init_logger_from_env();
// Initialize HAL with max CPU clock
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
// Setup heap allocator using reclaimed memory (replaces dram2_uninit approach)
esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 73744);
// Start RTOS scheduler — now requires BOTH a timer AND a software interrupt
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt = esp_hal::interrupt::software::SoftwareInterruptControl::new(
peripherals.SW_INTERRUPT
);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
// Spawn tasks...
let _ = spawner;
loop {}
}use alloc::string::String; // Dynamic strings (heap)
use heapless::String; // Bounded strings (stack)
let s: heapless::String<64> = heapless::String::new();static CHANNEL: StaticCell<Channel<NoopRawMutex, Data, 3>> = StaticCell::new();
// In async function
let channel: &'static mut _ = CHANNEL.init(Channel::new());
let (sender, receiver) = (channel.sender(), channel.receiver());use esp_hal::gpio::{Level, Output, OutputConfig, Pull, DriveMode};
// Standard output
let pin = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
// Open-drain for sensors like DHT11
let pin = Output::new(
peripherals.GPIO1,
Level::High,
OutputConfig::default()
.with_drive_mode(DriveMode::OpenDrain)
.with_pull(Pull::None),
).into_flex();use esp_hal::analog::adc::{Adc, AdcConfig, AdcCalCurve, Attenuation};
let mut adc_config = AdcConfig::new();
let pin = adc_config.enable_pin_with_cal::<_, AdcCalCurve<ADC2>>(
peripherals.GPIO11,
Attenuation::_11dB // 0-3.3V range
);
let adc = Adc::new(peripherals.ADC2, adc_config);
// Read with nb::block!
let value = nb::block!(adc.read_oneshot(&mut pin))?;pub struct SensorPeripherals {
pub dht11_pin: GPIO1<'static>,
pub moisture_pin: GPIO11<'static>,
pub power_pin: GPIO16<'static>,
pub adc2: ADC2<'static>,
}#[embassy_executor::task]
pub async fn my_task(sender: Sender<'static, NoopRawMutex, Data, 3>) {
loop {
// Do work
sender.send(data).await;
Timer::after(Duration::from_secs(5)).await;
}
}spawner.spawn(sensor_task(sender, peripherals)).ok();
spawner.spawn(update_task(stack, display, receiver)).ok();use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel};
static CHANNEL: StaticCell<Channel<NoopRawMutex, Data, 3>> = StaticCell::new();
// sender.send(data).await / receiver.receive().awaituse embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
static SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
// SIGNAL.signal(()) / SIGNAL.wait().await'reconnect: loop {
let mut client = initialize_client().await?;
loop {
match client.process().await {
Ok(_) => { /* handle messages */ }
Err(e) => {
println!("Error: {:?}", e);
continue 'reconnect; // Reconnect on error
}
}
}
}use esp_hal::rtc_cntl::{Rtc, sleep::{RtcSleepConfig, TimerWakeupSource, RtcioWakeupSource, WakeupLevel}};
pub fn enter_deep(wakeup_pin: &mut dyn RtcPin, rtc_cntl: LPWR, duration: Duration) -> ! {
// GPIO wake source
let wakeup_pins: &mut [(&mut dyn RtcPin, WakeupLevel)] = &mut [(wakeup_pin, WakeupLevel::Low)];
let ext0 = RtcioWakeupSource::new(wakeup_pins);
// Timer wake source
let timer = TimerWakeupSource::new(duration.into());
let mut rtc = Rtc::new(rtc_cntl);
let mut config = RtcSleepConfig::deep();
config.set_rtc_fastmem_pd_en(false); // Keep RTC fast memory powered
rtc.sleep(&config, &[&ext0, &timer]);
unreachable!();
}use esp_hal::ram;
#[ram(unstable(rtc_fast))]
pub static BOOT_COUNT: RtcCell<u32> = RtcCell::new(0);
// Survives deep sleep - read/write with .get()/.set()
let count = BOOT_COUNT.get();
BOOT_COUNT.set(count + 1);// esp_radio::init() is GONE — wifi::new() is called directly (as of esp-radio 0.18+)
use esp_radio::wifi::{self, ClientConfig, ModeConfig, WifiController};
let (mut controller, interfaces) =
esp_radio::wifi::new(peripherals.WIFI, Default::default())
.expect("Failed to initialize Wi-Fi controller");
let client_config = ModeConfig::Client(
ClientConfig::default()
.with_ssid(env!("WIFI_SSID").try_into().unwrap())
.with_password(env!("WIFI_PSK").try_into().unwrap()),
);
controller.set_config(&client_config)?;
controller.start_async().await?;
controller.connect_async().await?;use embassy_net::{Config, Stack, StackResources};
let config = Config::dhcpv4(DhcpConfig::default());
let (stack, runner) = embassy_net::new(wifi_interface, config, stack_resources, seed);
// Wait for link and IP
loop {
if stack.is_link_up() { break; }
Timer::after(Duration::from_millis(500)).await;
}
loop {
if let Some(config) = stack.config_v4() {
println!("IP: {}", config.address);
break;
}
Timer::after(Duration::from_millis(500)).await;
}pub static WIFI_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
// In connection task
WIFI_SIGNAL.wait().await;
controller.stop_async().await?;
// Before deep sleep
WIFI_SIGNAL.signal(());async fn sample_adc_with_warmup<PIN, ADC>(
adc: &mut Adc<ADC, Blocking>,
pin: &mut AdcPin<PIN, ADC>,
warmup_ms: u64,
) -> Option<u16> {
Timer::after(Duration::from_millis(warmup_ms)).await;
nb::block!(adc.read_oneshot(pin)).ok()
}async fn read_sensor(adc: &mut Adc, pin: &mut AdcPin, power: &mut Output) -> Option<u16> {
power.set_high();
let result = sample_adc_with_warmup(adc, pin, 50).await;
power.set_low();
result
}fn calculate_average<T: Copy + Ord + Into<u32>>(samples: &mut [T]) -> Option<T> {
if samples.len() <= 2 { return None; }
samples.sort_unstable();
let trimmed = &samples[1..samples.len() - 1]; // Remove min/max
let sum: u32 = trimmed.iter().map(|&x| x.into()).sum();
(sum / trimmed.len() as u32).try_into().ok()
}use mipidsi::{Builder, options::ColorInversion};
let di = display_interface_parallel_gpio::Generic8BitBus::new(/*pins*/);
let mut display = Builder::new(ST7789, di)
.display_size(320, 170)
.invert_colors(ColorInversion::Inverted)
.init(&mut delay)?;display.set_display_on(false)?; // Enter power save
// Before deep sleep
power_pin.set_low();#[derive(Debug)]
pub enum Error {
Wifi(WifiError),
Display(display::Error),
Mqtt(MqttError),
}
impl From<WifiError> for Error {
fn from(e: WifiError) -> Self { Self::Wifi(e) }
}// Note: #[esp_rtos::main] (not use esp_rtos::main; #[main])
// Note: main now returns `-> !` (diverging)
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
if let Err(error) = main_fallible(spawner).await {
log::error!("Fatal: {:?}", error);
esp_hal::system::software_reset();
}
unreachable!()
}
async fn main_fallible(spawner: Spawner) -> Result<(), Error> {
// Application logic with ? operator
}cargo outdated
cargo update -p esp-hal
cargo build --release
cargo clippy -- -D warningscargo update -p embassy-executor -p embassy-time -p embassy-sync -p embassy-net# Install
cargo install esp-generate --locked
# Generate ESP32-S3 project with Embassy + WiFi + alloc
esp-generate --chip esp32s3 -o unstable-hal -o embassy -o alloc -o wifi -o log my-project
# Available options: unstable-hal, alloc, wifi, ble-trouble, embassy,
# probe-rs, defmt, log, esp-backtrace, embedded-test, wokwi, ci,
# vscode, neovim, helix, zeduse esp_println::println;
// New: reads RUST_LOG at compile time via esp-config (.cargo/esp-config.toml)
esp_println::logger::init_logger_from_env();
// Or explicitly set a level:
// esp_println::logger::init_logger(log::LevelFilter::Info);
log::info!("Debug: value = {}", value);
println!("Debug: value = {}", value); // also worksuse esp_hal::system::software_reset;
software_reset(); // Clean restart on unrecoverable error