diff --git a/.gitignore b/.gitignore index e5fbf74..9440b79 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,15 @@ docker-compose.override.yml # Environment files (may contain secrets) .env + +# Python virtual environments + +.venv/ +venv/ + +# ESP32 build cache (esp-idf-sys managed) + +.embuild/ .env.local .env.*.local diff --git a/Cargo.toml b/Cargo.toml index cafc225..f2c097f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,7 @@ +[workspace] +members = ["."] +resolver = "2" + [package] name = "zeroclaw" version = "0.1.0" diff --git a/firmware/zeroclaw-esp32/.cargo/config.toml b/firmware/zeroclaw-esp32/.cargo/config.toml index 8746ad1..56dd71b 100644 --- a/firmware/zeroclaw-esp32/.cargo/config.toml +++ b/firmware/zeroclaw-esp32/.cargo/config.toml @@ -2,4 +2,10 @@ target = "riscv32imc-esp-espidf" [target.riscv32imc-esp-espidf] +linker = "ldproxy" runner = "espflash flash --monitor" +# ESP-IDF 5.x uses 64-bit time_t +rustflags = ["-C", "default-linker-libraries", "--cfg", "espidf_time64"] + +[unstable] +build-std = ["std", "panic_abort"] diff --git a/firmware/zeroclaw-esp32/Cargo.lock b/firmware/zeroclaw-esp32/Cargo.lock index 2580883..69e989b 100644 --- a/firmware/zeroclaw-esp32/Cargo.lock +++ b/firmware/zeroclaw-esp32/Cargo.lock @@ -58,24 +58,22 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bindgen" -version = "0.63.0" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.0", "cexpr", "clang-sys", - "lazy_static", - "lazycell", + "itertools", "log", - "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 1.0.109", - "which", + "syn 2.0.116", ] [[package]] @@ -374,14 +372,15 @@ checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" [[package]] name = "embassy-sync" -version = "0.5.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd938f25c0798db4280fcd8026bf4c2f48789aebf8f77b6e5cf8a7693ba114ec" +checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" dependencies = [ "cfg-if", "critical-section", "embedded-io-async", - "futures-util", + "futures-core", + "futures-sink", "heapless", ] @@ -446,16 +445,15 @@ dependencies = [ [[package]] name = "embedded-svc" -version = "0.27.1" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6f87e7654f28018340aa55f933803017aefabaa5417820a3b2f808033c7bbc" +checksum = "a7770e30ab55cfbf954c00019522490d6ce26a3334bede05a732ba61010e98e0" dependencies = [ "defmt 0.3.100", "embedded-io", "embedded-io-async", "enumset", "heapless", - "no-std-net", "num_enum", "serde", "strum 0.25.0", @@ -463,9 +461,9 @@ dependencies = [ [[package]] name = "embuild" -version = "0.31.4" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4caa4f198bb9152a55c0103efb83fa4edfcbb8625f4c9e94ae8ec8e23827c563" +checksum = "e188ad2bbe82afa841ea4a29880651e53ab86815db036b2cb9f8de3ac32dad75" dependencies = [ "anyhow", "bindgen", @@ -475,6 +473,7 @@ dependencies = [ "globwalk", "home", "log", + "regex", "remove_dir_all", "serde", "serde_json", @@ -533,9 +532,8 @@ dependencies = [ [[package]] name = "esp-idf-hal" -version = "0.43.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7adf3fb19a9ca016cbea1ab8a7b852ac69df8fcde4923c23d3b155efbc42a74" +version = "0.45.2" +source = "git+https://github.com/esp-rs/esp-idf-hal#bc48639bd626c72afc1e25e5d497b5c639161d30" dependencies = [ "atomic-waker", "embassy-sync", @@ -552,14 +550,12 @@ dependencies = [ "heapless", "log", "nb 1.1.0", - "num_enum", ] [[package]] name = "esp-idf-svc" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2180642ca122a7fec1ec417a9b1a77aa66aaa067fdf1daae683dd8caba84f26b" +version = "0.51.0" +source = "git+https://github.com/esp-rs/esp-idf-svc#dee202f146c7681e54eabbf118a216fc0195d203" dependencies = [ "embassy-futures", "embedded-hal-async", @@ -567,6 +563,7 @@ dependencies = [ "embuild", "enumset", "esp-idf-hal", + "futures-io", "heapless", "log", "num_enum", @@ -575,14 +572,13 @@ dependencies = [ [[package]] name = "esp-idf-sys" -version = "0.34.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e148f97c04ed3e9181a08bcdc9560a515aad939b0ba7f50a0022e294665e0af" +version = "0.36.1" +source = "git+https://github.com/esp-rs/esp-idf-sys#64667a38fb8004e1fc3b032488af6857ca3cd849" dependencies = [ "anyhow", - "bindgen", "build-time", "cargo_metadata", + "cmake", "const_format", "embuild", "envy", @@ -649,21 +645,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] -name = "futures-task" +name = "futures-io" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] -name = "futures-util" +name = "futures-sink" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", -] +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "getrandom" @@ -827,6 +818,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -843,18 +843,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -945,12 +933,6 @@ dependencies = [ "libc", ] -[[package]] -name = "no-std-net" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bcece43b12349917e096cddfa66107277f123e6c96a5aea78711dc601a47152" - [[package]] name = "nom" version = "7.1.3" @@ -1007,18 +989,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - [[package]] name = "prettyplease" version = "0.2.37" @@ -1138,9 +1108,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" diff --git a/firmware/zeroclaw-esp32/Cargo.toml b/firmware/zeroclaw-esp32/Cargo.toml index 70d2611..2ec056f 100644 --- a/firmware/zeroclaw-esp32/Cargo.toml +++ b/firmware/zeroclaw-esp32/Cargo.toml @@ -14,15 +14,21 @@ edition = "2021" license = "MIT" description = "ZeroClaw ESP32 peripheral firmware — GPIO over JSON serial" +[patch.crates-io] +# Use latest esp-rs crates to fix u8/i8 char pointer compatibility with ESP-IDF 5.x +esp-idf-sys = { git = "https://github.com/esp-rs/esp-idf-sys" } +esp-idf-hal = { git = "https://github.com/esp-rs/esp-idf-hal" } +esp-idf-svc = { git = "https://github.com/esp-rs/esp-idf-svc" } + [dependencies] -esp-idf-svc = "0.48" +esp-idf-svc = { git = "https://github.com/esp-rs/esp-idf-svc" } log = "0.4" anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" [build-dependencies] -embuild = "0.31" +embuild = { version = "0.33", features = ["espidf"] } [profile.release] opt-level = "s" diff --git a/firmware/zeroclaw-esp32/README.md b/firmware/zeroclaw-esp32/README.md index 804aaca..f4b2c08 100644 --- a/firmware/zeroclaw-esp32/README.md +++ b/firmware/zeroclaw-esp32/README.md @@ -2,8 +2,11 @@ Peripheral firmware for ESP32 — speaks the same JSON-over-serial protocol as the STM32 firmware. Flash this to your ESP32, then configure ZeroClaw on the host to connect via serial. +**New to this?** See [SETUP.md](SETUP.md) for step-by-step commands and troubleshooting. + ## Protocol + - **Request** (host → ESP32): `{"id":"1","cmd":"gpio_write","args":{"pin":13,"value":1}}\n` - **Response** (ESP32 → host): `{"id":"1","ok":true,"result":"done"}\n` @@ -11,19 +14,44 @@ Commands: `gpio_read`, `gpio_write`. ## Prerequisites -1. **ESP toolchain** (espup): +1. **RISC-V ESP-IDF** (ESP32-C2/C3): Uses nightly Rust with `build-std`. + + **Python**: ESP-IDF requires Python 3.10–3.13 (not 3.14). If you have Python 3.14: + ```sh + brew install python@3.12 + ``` + + **virtualenv** (needed by ESP-IDF tools; PEP 668 workaround on macOS): + ```sh + /opt/homebrew/opt/python@3.12/bin/python3.12 -m pip install virtualenv --break-system-packages + ``` + + **Rust tools**: + ```sh + cargo install espflash ldproxy + ``` + + The project's `rust-toolchain.toml` pins nightly + rust-src. `esp-idf-sys` downloads ESP-IDF automatically on first build. Use Python 3.12 for the build: + ```sh + export PATH="/opt/homebrew/opt/python@3.12/libexec/bin:$PATH" + ``` + +2. **Xtensa targets** (ESP32, ESP32-S2, ESP32-S3): Use espup instead: ```sh cargo install espup espflash espup install - source ~/export-esp.sh # or ~/export-esp.fish for Fish + source ~/export-esp.sh ``` - -2. **Target**: ESP32-C3 (RISC-V) by default. Edit `.cargo/config.toml` for other targets (e.g. `xtensa-esp32-espidf` for original ESP32). + Then edit `.cargo/config.toml` to change the target (e.g. `xtensa-esp32-espidf`). ## Build & Flash ```sh cd firmware/zeroclaw-esp32 +# Use Python 3.12 (required if you have 3.14) +export PATH="/opt/homebrew/opt/python@3.12/libexec/bin:$PATH" +# Optional: pin MCU (esp32c3 or esp32c2) +export MCU=esp32c3 cargo build --release espflash flash target/riscv32imc-esp-espidf/release/zeroclaw-esp32 --monitor ``` diff --git a/firmware/zeroclaw-esp32/SETUP.md b/firmware/zeroclaw-esp32/SETUP.md new file mode 100644 index 0000000..0624f4d --- /dev/null +++ b/firmware/zeroclaw-esp32/SETUP.md @@ -0,0 +1,156 @@ +# ESP32 Firmware Setup Guide + +Step-by-step setup for building the ZeroClaw ESP32 firmware. Follow this if you run into issues. + +## Quick Start (copy-paste) + +```sh +# 1. Install Python 3.12 (ESP-IDF needs 3.10–3.13, not 3.14) +brew install python@3.12 + +# 2. Install virtualenv (PEP 668 workaround on macOS) +/opt/homebrew/opt/python@3.12/bin/python3.12 -m pip install virtualenv --break-system-packages + +# 3. Install Rust tools +cargo install espflash ldproxy + +# 4. Build +cd firmware/zeroclaw-esp32 +export PATH="/opt/homebrew/opt/python@3.12/libexec/bin:$PATH" +cargo build --release + +# 5. Flash (connect ESP32 via USB) +espflash flash target/riscv32imc-esp-espidf/release/zeroclaw-esp32 --monitor +``` + +--- + +## Detailed Steps + +### 1. Python + +ESP-IDF requires Python 3.10–3.13. **Python 3.14 is not supported.** + +```sh +brew install python@3.12 +``` + +### 2. virtualenv + +ESP-IDF tools need `virtualenv`. On macOS with Homebrew Python, PEP 668 blocks `pip install`; use: + +```sh +/opt/homebrew/opt/python@3.12/bin/python3.12 -m pip install virtualenv --break-system-packages +``` + +### 3. Rust Tools + +```sh +cargo install espflash ldproxy +``` + +- **espflash**: flash and monitor +- **ldproxy**: linker for ESP-IDF builds + +### 4. Use Python 3.12 for Builds + +Before every build (or add to `~/.zshrc`): + +```sh +export PATH="/opt/homebrew/opt/python@3.12/libexec/bin:$PATH" +``` + +### 5. Build + +```sh +cd firmware/zeroclaw-esp32 +cargo build --release +``` + +First build downloads and compiles ESP-IDF (~5–15 min). + +### 6. Flash + +```sh +espflash flash target/riscv32imc-esp-espidf/release/zeroclaw-esp32 --monitor +``` + +--- + +## Troubleshooting + +### "No space left on device" + +Free disk space. Common targets: + +```sh +# Cargo cache (often 5–20 GB) +rm -rf ~/.cargo/registry/cache ~/.cargo/registry/src + +# Unused Rust toolchains +rustup toolchain list +rustup toolchain uninstall + +# iOS Simulator runtimes (~35 GB) +xcrun simctl delete unavailable + +# Temp files +rm -rf /var/folders/*/T/cargo-install* +``` + +### "can't find crate for `core`" / "riscv32imc-esp-espidf target may not be installed" + +This project uses **nightly Rust with build-std**, not espup. Ensure: + +- `rust-toolchain.toml` exists (pins nightly + rust-src) +- You are **not** sourcing `~/export-esp.sh` (that's for Xtensa targets) +- Run `cargo build` from `firmware/zeroclaw-esp32` + +### "externally-managed-environment" / "No module named 'virtualenv'" + +Install virtualenv with the PEP 668 workaround: + +```sh +/opt/homebrew/opt/python@3.12/bin/python3.12 -m pip install virtualenv --break-system-packages +``` + +### "expected `i64`, found `i32`" (time_t mismatch) + +Already fixed in `.cargo/config.toml` with `espidf_time64` for ESP-IDF 5.x. If you use ESP-IDF 4.4, switch to `espidf_time32`. + +### "expected `*const u8`, found `*const i8`" (esp-idf-svc) + +Already fixed via `[patch.crates-io]` in `Cargo.toml` using esp-rs crates from git. Do not remove the patch. + +### 10,000+ files in `git status` + +The `.embuild/` directory (ESP-IDF cache) has ~100k+ files. It is in `.gitignore`. If you see them, ensure `.gitignore` contains: + +``` +.embuild/ +``` + +--- + +## Optional: Auto-load Python 3.12 + +Add to `~/.zshrc`: + +```sh +# ESP32 firmware build +export PATH="/opt/homebrew/opt/python@3.12/libexec/bin:$PATH" +``` + +--- + +## Xtensa Targets (ESP32, ESP32-S2, ESP32-S3) + +For non–RISC-V chips, use espup instead: + +```sh +cargo install espup espflash +espup install +source ~/export-esp.sh +``` + +Then edit `.cargo/config.toml` to use `xtensa-esp32-espidf` (or the correct target). diff --git a/firmware/zeroclaw-esp32/rust-toolchain.toml b/firmware/zeroclaw-esp32/rust-toolchain.toml new file mode 100644 index 0000000..f70d225 --- /dev/null +++ b/firmware/zeroclaw-esp32/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src"] diff --git a/firmware/zeroclaw-esp32/src/main.rs b/firmware/zeroclaw-esp32/src/main.rs index b1a487c..a85b67d 100644 --- a/firmware/zeroclaw-esp32/src/main.rs +++ b/firmware/zeroclaw-esp32/src/main.rs @@ -6,8 +6,9 @@ //! Protocol: same as STM32 — see docs/hardware-peripherals-design.md use esp_idf_svc::hal::gpio::PinDriver; -use esp_idf_svc::hal::prelude::*; -use esp_idf_svc::hal::uart::*; +use esp_idf_svc::hal::peripherals::Peripherals; +use esp_idf_svc::hal::uart::{UartConfig, UartDriver}; +use esp_idf_svc::hal::units::Hertz; use log::info; use serde::{Deserialize, Serialize}; @@ -36,9 +37,13 @@ fn main() -> anyhow::Result<()> { let peripherals = Peripherals::take()?; let pins = peripherals.pins; + // Create GPIO output drivers first (they take ownership of pins) + let mut gpio2 = PinDriver::output(pins.gpio2)?; + let mut gpio13 = PinDriver::output(pins.gpio13)?; + // UART0: TX=21, RX=20 (ESP32) — ESP32-C3 may use different pins; adjust for your board let config = UartConfig::new().baudrate(Hertz(115_200)); - let mut uart = UartDriver::new( + let uart = UartDriver::new( peripherals.uart0, pins.gpio21, pins.gpio20, @@ -60,7 +65,8 @@ fn main() -> anyhow::Result<()> { if b == b'\n' { if !line.is_empty() { if let Ok(line_str) = std::str::from_utf8(&line) { - if let Ok(resp) = handle_request(line_str, &peripherals) { + if let Ok(resp) = handle_request(line_str, &mut gpio2, &mut gpio13) + { let out = serde_json::to_string(&resp).unwrap_or_default(); let _ = uart.write(format!("{}\n", out).as_bytes()); } @@ -80,10 +86,15 @@ fn main() -> anyhow::Result<()> { } } -fn handle_request( +fn handle_request( line: &str, - peripherals: &esp_idf_svc::hal::peripherals::Peripherals, -) -> anyhow::Result { + gpio2: &mut PinDriver<'_, G2>, + gpio13: &mut PinDriver<'_, G13>, +) -> anyhow::Result +where + G2: esp_idf_svc::hal::gpio::OutputMode, + G13: esp_idf_svc::hal::gpio::OutputMode, +{ let req: Request = serde_json::from_str(line.trim())?; let id = req.id.clone(); @@ -98,13 +109,13 @@ fn handle_request( } "gpio_read" => { let pin_num = req.args.get("pin").and_then(|v| v.as_u64()).unwrap_or(0) as i32; - let value = gpio_read(peripherals, pin_num)?; + let value = gpio_read(pin_num)?; Ok(value.to_string()) } "gpio_write" => { let pin_num = req.args.get("pin").and_then(|v| v.as_u64()).unwrap_or(0) as i32; let value = req.args.get("value").and_then(|v| v.as_u64()).unwrap_or(0); - gpio_write(peripherals, pin_num, value)?; + gpio_write(gpio2, gpio13, pin_num, value)?; Ok("done".into()) } _ => Err(anyhow::anyhow!("Unknown command: {}", req.cmd)), @@ -126,28 +137,26 @@ fn handle_request( } } -fn gpio_read(_peripherals: &esp_idf_svc::hal::peripherals::Peripherals, _pin: i32) -> anyhow::Result { +fn gpio_read(_pin: i32) -> anyhow::Result { // TODO: implement input pin read — requires storing InputPin drivers per pin Ok(0) } -fn gpio_write( - peripherals: &esp_idf_svc::hal::peripherals::Peripherals, +fn gpio_write( + gpio2: &mut PinDriver<'_, G2>, + gpio13: &mut PinDriver<'_, G13>, pin: i32, value: u64, -) -> anyhow::Result<()> { - let pins = peripherals.pins; - let level = value != 0; +) -> anyhow::Result<()> +where + G2: esp_idf_svc::hal::gpio::OutputMode, + G13: esp_idf_svc::hal::gpio::OutputMode, +{ + let level = esp_idf_svc::hal::gpio::Level::from(value != 0); match pin { - 2 => { - let mut out = PinDriver::output(pins.gpio2)?; - out.set_level(esp_idf_svc::hal::gpio::Level::from(level))?; - } - 13 => { - let mut out = PinDriver::output(pins.gpio13)?; - out.set_level(esp_idf_svc::hal::gpio::Level::from(level))?; - } + 2 => gpio2.set_level(level)?, + 13 => gpio13.set_level(level)?, _ => anyhow::bail!("Pin {} not configured (add to gpio_write)", pin), } Ok(())