Skip to content

Latest commit

 

History

History
186 lines (127 loc) · 7.5 KB

File metadata and controls

186 lines (127 loc) · 7.5 KB

CLAUDE.md

Project Overview

RaspRover-RS is a monorepo for the Waveshare RaspRover robot platform:

  • firmware/ — embedded firmware for the Espressif ESP32 DevKit-C (Xtensa) on the ROS Driver Board, using esp-rtos with Embassy. All esp-hal family crates are consumed directly from crates.io — see firmware/Cargo.toml for pinned versions.
  • robot/ — ROS 2 (Lyrical Luth) workspace for the Raspberry Pi host, built with colcon.
  • data/ — mechanical CAD and reference documents for the RaspRover platform.

Build Commands

Both halves live under one pixi workspace (pixi.toml), with a separate environment each:

pixi run -e firmware build   # cargo build --release
pixi run -e firmware flash   # cargo run --release
pixi run -e firmware lint    # clippy, see below
pixi run -e ros build        # colcon build --symlink-install

The firmware tasks are thin wrappers — plain Cargo from firmware/ works too, provided the esp toolchain and xtensa-esp32-elf-gcc are on PATH (see firmware/esp-activate.sh).

Do not lint with cargo clippy --all-targets: it ignores the test = false flags on [lib] and [[bin]] and links them against a libtest that does not exist for xtensa-esp32-none-elf. The lint task runs cargo clippy --lib --bins --tests -- -D warnings, which covers the same code.

WiFi credentials are configured via environment variables (with fallback defaults in firmware/.cargo/config.toml):

export CONFIG_WIFI_NETWORK="your-ssid"
export CONFIG_WIFI_PASSWORD="your-password"

Toolchain

This project requires the ESP Rust toolchain (not stable/nightly), which supports the Xtensa instruction set used by ESP32:

  • Toolchain: esp (RUSTUP_TOOLCHAIN=esp)
  • Target: xtensa-esp32-none-elf

Rust Analyzer is configured via .vscode/settings.json with the correct RUSTFLAGS and environment variables for the ESP32 target. If using VS Code, these settings are picked up automatically.

Architecture

The codebase is no_std + no_main — standard library and Rust runtime are unavailable. All async code runs in the Embassy executor provided by esp-rtos. However, alloc is available — the heap is configured with esp_alloc::heap_allocator!, so Box, Vec, etc. work fine.

Entry point pattern:

#![no_main]
#![no_std]

extern crate alloc;

esp_bootloader_esp_idf::esp_app_desc!();

#[esp_rtos::main]
async fn main(spawner: Spawner) {
    rtt_target::rtt_init_defmt!();

    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let p = esp_hal::init(config);
    esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 98768);

    let timg0 = TimerGroup::new(p.TIMG0);
    let sw_int = SoftwareInterruptControl::new(p.SW_INTERRUPT);
    esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);

    // application code
}

esp_rtos::start(...) must be called inside the async main function after heap init, and takes a timer plus a software interrupt.

Key dependency structure:

  • esp-rtos — async runtime (Embassy executor + timer integration)
  • esp-radio — WiFi via esp_radio::wifi
  • embassy-net — TCP/IP networking
  • embassy-sync — synchronisation primitives (Watch, Mutex, etc.)
  • All esp-hal family crates are consumed from crates.io at the versions pinned in Cargo.toml

Cargo configuration (firmware/.cargo/config.toml) is self-contained — no external includes. Sets the build target, linker arguments, build-std, and WiFi credential defaults.

Logging

Uses defmt structured logging via RTT. Log level is controlled by the DEFMT_LOG environment variable (default: info in firmware/.cargo/config.toml). Import macros directly from defmt:

use defmt::{debug, error, info, warn};
use panic_rtt_target as _;
info!("message {}", value);

Initialize RTT at the top of main before any logging:

rtt_target::rtt_init_defmt!();

The panic handler is provided by panic-rtt-target. The rtt-target crate (with features = ["defmt"]) provides the transport.

Peripheral Patterns

Peripherals struct (firmware/src/board.rs) is a plain Rust struct. Use concrete peripheral types from esp_hal::peripherals for typed pins and AnyPin<'static> (via .into()) for interchangeable pins:

pub struct Peripherals {
    pub i2c_sda: GPIO32<'static>,
    pub i2c_scl: GPIO33<'static>,
    pub motor_ain1: AnyPin<'static>,  // p.GPIO21.into()
    // ...
}

All peripherals have 'static lifetimeGPIO21<'static>, LEDC<'static>, etc. This propagates through esp-hal types (Output<'static>, Channel<'static, LowSpeed>, etc.).

PWM (LEDC): Use esp_hal::ledc directly with the unstable feature. LEDC source is in the esp-hal crate under src/ledc/ (browse via cargo doc --open or in ~/.cargo/registry/src/).

Self-referential HAL structs: Channel<'a, LowSpeed> holds a &'a dyn TimerIFace so the timer must outlive the channel. Use Box::leak(Box::new(timer)) to get a &'static Timer — this is safe given the always-available heap.

I2C: Use esp_hal::i2c::master::I2c directly:

I2c::new(i2c0, config).unwrap().with_sda(sda).with_scl(scl).into_async()

Wrap with embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice for sharing across tasks.

Rng: Rng::new() — no arguments.

WiFi Pattern

WiFi is initialized in network::init():

let (controller, interfaces) = esp_radio::wifi::new(wifi, Default::default()).unwrap();
let wifi_device: Interface<'static> = interfaces.station;
  • controller.is_connected() — check connection status
  • controller.set_config(&Config::Station(station_config)) — configure, where Config is esp_radio::wifi::Config
  • StationConfig::default().with_ssid(&str).with_password(alloc::string::String) — note the two argument types differ

Module Design Patterns

Encapsulate subsystems as spawned tasks — each major subsystem (network, display, etc.) owns its lifecycle via #[embassy_executor::task] functions and a dedicated src/<subsystem>.rs module. main coordinates but does not inline subsystem logic.

Use Watch for state broadcastingembassy_sync::watch::Watch is the preferred primitive for publishing subsystem state to other tasks:

  • Delivers the current value to late subscribers (no missed events on join)
  • try_get() enables non-blocking polling in tight loops
  • Declare as a pub static in the owning module; consumers call .receiver().unwrap()
  • Size N to the number of concurrent receivers needed
// In the owning module (e.g. src/network.rs):
pub static NET_STATE: Watch<CriticalSectionRawMutex, NetworkState, 2> = Watch::new();

// In a consumer:
let mut net_rx = network::NET_STATE.receiver().unwrap();
if let Some(state) = net_rx.try_get() { /* react */ }

Carry data in enum variants — state enums should embed associated payload directly (e.g. Up(String<18>)) rather than storing it separately.

Track last-seen state to avoid redundant work — consumers that perform side effects should cache the last processed state and only act when it changes:

let mut last_state: Option<NetworkState> = None;
if Some(&state) != last_state.as_ref() {
    // redraw / update
    last_state = Some(state);
}

References