Loading...
Loading...
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 skill4agent add psytraxx/esp32-homecontrol-no-std-rs esp32-rust-embeddedesp-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-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" }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 = 1_stack_startbuild.rsesp_rtos_initializedlet timg0 = TimerGroup::new(peripherals.TIMG0);
esp_rtos::start(timg0.timer0);env!()#![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);
}esp-allocheaplessstatic_cell::StaticCelluse 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);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?;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 STOP_WIFI_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
// In connection task
STOP_WIFI_SIGNAL.wait().await;
controller.stop_async().await?;
// Before deep sleep
STOP_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) }
}#[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
}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-netuse esp_println::println;
init_logger(log::LevelFilter::Info);
println!("Debug: value = {}", value);use esp_hal::system::software_reset;
software_reset(); // Clean restart on unrecoverable error