🎯

esp32-rust-embedded

🎯Skill

from psytraxx/esp32-homecontrol-no-std-rs

VibeIndex|
What it does

esp32-rust-embedded skill from psytraxx/esp32-homecontrol-no-std-rs

esp32-rust-embedded

Installation

Install skill:
npx skills add https://github.com/psytraxx/esp32-homecontrol-no-std-rs --skill esp32-rust-embedded
6
Last UpdatedJan 15, 2026

Skill Details

SKILL.md

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.

Overview

# ESP32 Embedded Rust Specialist

Expert guidance for no-std Rust development on ESP32 microcontrollers using the ESP-RS ecosystem and Embassy async framework.

ESP-RS Ecosystem Stack

Core Dependencies

```toml

esp-hal = { version = "1.0.0", features = ["esp32s3", "log-04", "unstable"] }

esp-rtos = { version = "0.2.0", features = ["embassy", "esp-alloc", "esp-radio", "esp32s3", "log-04"] }

esp-radio = { version = "0.17.0", features = ["esp-alloc", "esp32s3", "wifi", "smoltcp"] }

esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32s3", "log-04"] }

```

Embassy Framework

```toml

embassy-executor = { version = "0.9.1", features = ["log"] }

embassy-time = { version = "0.5.0", features = ["log"] }

embassy-net = { version = "0.7.1", features = ["dhcpv4", "tcp", "udp", "dns"] }

embassy-sync = { version = "0.7.2" }

```

Dependency Hierarchy

```

esp-radio (WiFi) -> esp-rtos (scheduler) -> esp-hal (HAL) -> esp-phy (PHY)

embassy-executor -> embassy-time -> embassy-sync -> embassy-net

```

Build & Flash

Environment Setup

```bash

# 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

```

Build Commands

```bash

# Quick build and flash

./run.sh

# Manual release build (recommended)

cargo run --release

# Debug build (slower on device)

cargo run

```

Cargo Profile Optimization

```toml

[profile.dev]

opt-level = "s" # Rust debug too slow for ESP32

[profile.release]

lto = 'fat'

opt-level = 's'

codegen-units = 1

```

Common Build Errors

Linker error: undefined symbol _stack_start

  • Check build.rs has linkall.x configuration
  • Verify esp-hal version compatibility

undefined symbol: esp_rtos_initialized

  • Ensure esp-rtos is started with timer:

```rust

let timg0 = TimerGroup::new(peripherals.TIMG0);

esp_rtos::start(timg0.timer0);

```

Environment variable errors

  • Variables are compile-time via env!() macro
  • Changes require full rebuild

No-Std Patterns

Application Entry

```rust

#![no_std]

#![no_main]

use esp_rtos::main;

#[main]

async fn main(spawner: Spawner) {

// Initialize logger

init_logger(log::LevelFilter::Info);

// Initialize HAL

let peripherals = esp_hal::init(Config::default());

// Setup heap allocator

heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 73744);

// Start RTOS scheduler

let timg0 = TimerGroup::new(peripherals.TIMG0);

esp_rtos::start(timg0.timer0);

}

```

Memory Management

  • Use esp-alloc for dynamic allocation
  • Prefer heapless collections with compile-time capacity
  • Use static_cell::StaticCell for 'static lifetime requirements

String Handling

```rust

use alloc::string::String; // Dynamic strings (heap)

use heapless::String; // Bounded strings (stack)

let s: heapless::String<64> = heapless::String::new();

```

Avoid cloning when possible.

StaticCell Pattern

```rust

static CHANNEL: StaticCell> = StaticCell::new();

// In async function

let channel: &'static mut _ = CHANNEL.init(Channel::new());

let (sender, receiver) = (channel.sender(), channel.receiver());

```

Hardware Patterns

GPIO Configuration

```rust

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();

```

ADC Reading with Calibration

```rust

use esp_hal::analog::adc::{Adc, AdcConfig, AdcCalCurve, Attenuation};

let mut adc_config = AdcConfig::new();

let pin = adc_config.enable_pin_with_cal::<_, AdcCalCurve>(

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))?;

```

Peripheral Bundles Pattern

```rust

pub struct SensorPeripherals {

pub dht11_pin: GPIO1<'static>,

pub moisture_pin: GPIO11<'static>,

pub power_pin: GPIO16<'static>,

pub adc2: ADC2<'static>,

}

```

Async Task Architecture

Task Definition

```rust

#[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;

}

}

```

Task Spawning

```rust

spawner.spawn(sensor_task(sender, peripherals)).ok();

spawner.spawn(update_task(stack, display, receiver)).ok();

```

Inter-Task Communication

Channel (multiple values)

```rust

use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel};

static CHANNEL: StaticCell> = StaticCell::new();

// sender.send(data).await / receiver.receive().await

```

Signal (single notification)

```rust

use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};

static SIGNAL: Signal = Signal::new();

// SIGNAL.signal(()) / SIGNAL.wait().await

```

Reconnection Loop Pattern

```rust

'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

}

}

}

}

```

Power Management

Deep Sleep Configuration

```rust

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!();

}

```

RTC Fast Memory Persistence

```rust

use esp_hal::ram;

#[ram(unstable(rtc_fast))]

pub static BOOT_COUNT: RtcCell = RtcCell::new(0);

// Survives deep sleep - read/write with .get()/.set()

let count = BOOT_COUNT.get();

BOOT_COUNT.set(count + 1);

```

Power Optimization

  • Toggle sensor power pins only during reads
  • Use power save mode on displays
  • Gracefully disconnect WiFi before sleep
  • Keep awake duration minimal

WiFi Networking

Connection Setup

```rust

use esp_radio::wifi::{self, ClientConfig, ModeConfig, WifiController};

let init = esp_radio::init().unwrap();

let (controller, interfaces) = wifi::new(&init, wifi_peripheral, Default::default()).unwrap();

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?;

```

Embassy-Net Stack

```rust

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;

}

```

Graceful WiFi Shutdown

```rust

pub static STOP_WIFI_SIGNAL: Signal = Signal::new();

// In connection task

STOP_WIFI_SIGNAL.wait().await;

controller.stop_async().await?;

// Before deep sleep

STOP_WIFI_SIGNAL.signal(());

```

Sensor Patterns

ADC Sampling with Warmup

```rust

async fn sample_adc_with_warmup(

adc: &mut Adc,

pin: &mut AdcPin,

warmup_ms: u64,

) -> Option {

Timer::after(Duration::from_millis(warmup_ms)).await;

nb::block!(adc.read_oneshot(pin)).ok()

}

```

Power-Controlled Sensor Read

```rust

async fn read_sensor(adc: &mut Adc, pin: &mut AdcPin, power: &mut Output) -> Option {

power.set_high();

let result = sample_adc_with_warmup(adc, pin, 50).await;

power.set_low();

result

}

```

Outlier-Resistant Averaging

```rust

fn calculate_average>(samples: &mut [T]) -> Option {

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()

}

```

Display Integration

ST7789 Parallel Interface

```rust

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)?;

```

Power Save Mode

```rust

display.set_display_on(false)?; // Enter power save

// Before deep sleep

power_pin.set_low();

```

Error Handling

Module Error Pattern

```rust

#[derive(Debug)]

pub enum Error {

Wifi(WifiError),

Display(display::Error),

Mqtt(MqttError),

}

impl From for Error {

fn from(e: WifiError) -> Self { Self::Wifi(e) }

}

```

Fallible Main Pattern

```rust

#[main]

async fn main(spawner: Spawner) {

if let Err(error) = main_fallible(spawner).await {

println!("Error: {:?}", error);

software_reset();

}

}

async fn main_fallible(spawner: Spawner) -> Result<(), Error> {

// Application logic with ? operator

}

```

Dependency Updates

Safe Update Process

```bash

cargo outdated

cargo update -p esp-hal

cargo build --release

cargo clippy -- -D warnings

```

Breaking Change Patterns

  • GPIO API changes frequently (OutputConfig)
  • Timer initialization changes
  • Feature flag renames
  • Always check esp-hal release notes

Version Alignment

Update Embassy crates together:

```bash

cargo update -p embassy-executor -p embassy-time -p embassy-sync -p embassy-net

```

Debugging

Serial Logging

```rust

use esp_println::println;

init_logger(log::LevelFilter::Info);

println!("Debug: value = {}", value);

```

Common Runtime Issues

  • WiFi fails: Check 2.4GHz network, signal strength
  • MQTT fails: Verify DNS resolution, broker credentials
  • Sensors fail: Check warmup delays, power pin toggling
  • Display blank: Ensure GPIO15 is HIGH (power enable)
  • Sleep wake fails: Verify RTC fast memory config

Software Reset

```rust

use esp_hal::system::software_reset;

software_reset(); // Clean restart on unrecoverable error

```