From 844db18b4b2f5af1314b958d119d08d373c2d253 Mon Sep 17 00:00:00 2001 From: ruvnet Date: Thu, 30 Apr 2026 13:06:22 -0400 Subject: [PATCH 1/4] feat(ruvllm-esp32): tiny RuvLLM agents on heterogeneous ESP32 SoCs (ADR-165, closes #409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframes `examples/ruvLLM/esp32-flash` from a single-chip "tiny LLM" skeleton (which had drifted out of sync with `lib.rs` and was reported as broken in #409) into a fleet of tiny ruvLLM/ruvector agents. Each ESP32 chip runs ONE role drawn from the canonical primitive surface defined in ADR-002, ADR-074, ADR-084. Roles (one binary, one chip, one role): HnswIndexer — MicroHNSW kNN + HashEmbedder (ESP32-C3 default) RagRetriever — MicroRAG retrieval (ESP32 default) AnomalySentinel — AnomalyDetector (ESP32-S2 default) MemoryArchivist — SemanticMemory type-tagged (ESP32-C6 default) LoraAdapter — MicroLoRA rank 1-2 (ESP32-S3 SIMD) SpeculativeDrafter — SpeculativeDecoder (ESP32-S3 default) PipelineRelay — PipelineNode head/middle/tail Verified end-to-end: cargo build --no-default-features --features host-test → green; all 5 variants boot to correct default role; smoke tests confirm RagRetriever recall, MemoryArchivist recall by type, AnomalySentinel learn+check. cargo +esp build --release --target xtensa-esp32s3-espidf → green; 858 KB ELF. espflash flash --chip esp32s3 /dev/ttyACM0 … → 451 KB programmed; chip boots; Rust main entered; TinyAgent constructed with HNSW capacity 32; banner + stats reach the host on /dev/ttyACM0: === ruvllm-esp32 tiny-agent (ADR-165) === variant=esp32s3 role=SpeculativeDrafter chip_id=0 sram_kb=512 [ready] type 'help' for commands role=SpeculativeDrafter variant=esp32s3 sram_kb=512 ops=0 hnsw=0 Issues solved while wiring up the cross-compile and on-device path: - build.rs cfg(target_os) evaluated against the host, not the cargo target. Switched to env::var("CARGO_CFG_TARGET_OS") so embuild's espidf::sysenv::output() runs only when actually cross-compiling to *-espidf — required for ldproxy's --ldproxy-linker arg to propagate into the link line. - embuild now needs `features = ["espidf"]` in build-dependencies. - esp-idf-svc 0.49.1 / esp-idf-hal 0.46.2 had a *const i8 / *const u8 bindgen regression and a broken TransmitConfig field; pinned the trio to 0.51.0 / 0.45.2 / 0.36.1. - The host's RUSTFLAGS=-C link-arg=-fuse-ld=mold breaks Xtensa link (mold doesn't speak Xtensa). CI invocation in the workflow uses `env -u RUSTFLAGS` and the README documents the local override. - `.cargo/config.toml` only declared xtensa-esp32-espidf — added blocks for esp32s2, esp32s3, esp32c3, esp32c6 with linker = "ldproxy". - ESP32-S3 dev board exposes USB-Serial/JTAG, not the UART0 GPIO pins my prior main was driving. Switched the device main path to `usb_serial_jtag_write_bytes` / `_read_bytes` directly so I/O actually reaches /dev/ttyACM0. - `sdkconfig.defaults` was per-variant inconsistent (ESP32 keys on an S3 build). Split into a chip-agnostic base + per-variant `sdkconfig.defaults.` files (`sdkconfig.defaults.esp32s3` is the first; CI matrix will add the others). - Bumped main task stack to 96 KB and dropped HNSW capacity to 32 so TinyAgent fits without overflowing on Xtensa stack growth. Files: ADR-165 — formal decision record (context, role catalog, per-variant assignment, embedder choice, federation bus, build/release plan, acceptance gates G1–G6, out-of-scope, roadmap). build.rs — cfg-via-env-var fix. Cargo.toml — pinned trio + binstart + native + embuild espidf. .cargo/config.toml — ldproxy linker for all 5 ESP32 variants. sdkconfig.defaults + sdkconfig.defaults.esp32s3 — split base / S3. src/main.rs — full rewrite as TinyAgent role engine; HashEmbedder per ADR-074 Tier 1; UART CLI on host-test; usb_serial_jtag CLI on esp32; WASM shim untouched. README.md — top-of-file rewrite with the ADR-165 framing, role matrix, primitive surface, and explicit "honest scope" disclaimer pointing at #409 + ADR-090 for the PSRAM big-model path. .github/workflows/ruvllm-esp32-firmware.yml — three-job CI: host-test smoke (G1–G3), matrix cross-compile via `espup install --targets $variant` + `cargo +esp build --release` + `espflash save-image --merge`, attach `ruvllm-esp32-${target}.bin` assets matching the URL pattern in `npm/web-flasher/index.html`. .gitignore — exclude target/, .embuild/, *.bin from the example dir. Closes #409 observations 1a, 1b, 3 in this commit. Observation 2 (no firmware in releases) closes when CI runs against the next ruvllm-esp32 tag. Co-Authored-By: claude-flow --- .github/workflows/ruvllm-esp32-firmware.yml | 139 ++ ...DR-165-tiny-ruvllm-agents-on-esp32-soCs.md | 182 +++ .../ruvLLM/esp32-flash/.cargo/config.toml | 22 +- examples/ruvLLM/esp32-flash/.gitignore | 4 + examples/ruvLLM/esp32-flash/Cargo.lock | 779 +++++++---- examples/ruvLLM/esp32-flash/Cargo.toml | 12 +- examples/ruvLLM/esp32-flash/README.md | 56 +- examples/ruvLLM/esp32-flash/build.rs | 10 +- .../ruvLLM/esp32-flash/sdkconfig.defaults | 17 +- .../esp32-flash/sdkconfig.defaults.esp32s3 | 30 + examples/ruvLLM/esp32-flash/src/main.rs | 1142 ++++++++--------- 11 files changed, 1506 insertions(+), 887 deletions(-) create mode 100644 .github/workflows/ruvllm-esp32-firmware.yml create mode 100644 docs/adr/ADR-165-tiny-ruvllm-agents-on-esp32-soCs.md create mode 100644 examples/ruvLLM/esp32-flash/.gitignore create mode 100644 examples/ruvLLM/esp32-flash/sdkconfig.defaults.esp32s3 diff --git a/.github/workflows/ruvllm-esp32-firmware.yml b/.github/workflows/ruvllm-esp32-firmware.yml new file mode 100644 index 000000000..5c777fc1c --- /dev/null +++ b/.github/workflows/ruvllm-esp32-firmware.yml @@ -0,0 +1,139 @@ +name: ruvllm-esp32 firmware (ADR-165) + +# Builds the tiny-agent firmware for every ESP32 variant and attaches the +# merged .bin assets to a GitHub release. Asset names match the URL pattern +# already hardcoded in `examples/ruvLLM/esp32-flash/npm/web-flasher/index.html` +# (`${FIRMWARE_BASE_URL}/ruvllm-esp32-${target}`), closing issue #409 obs 2. +# +# Modeled on RuView's `firmware-ci.yml` pattern: espup install → cargo +esp +# build → espflash save-image → upload to release. +# +# This workflow is the implementation of ADR-165 §2.5 + §7 step 5. + +on: + push: + tags: + - 'ruvllm-esp32-v*' + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag to attach .bin assets to (e.g. v2.3.0)' + required: true + type: string + +permissions: + contents: write + +jobs: + host-test: + name: host-test smoke (G1–G3) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: build host-test + working-directory: examples/ruvLLM/esp32-flash + run: cargo build --no-default-features --features host-test --target x86_64-unknown-linux-gnu + - name: smoke each role + working-directory: examples/ruvLLM/esp32-flash + shell: bash + run: | + set -euo pipefail + BIN=target/x86_64-unknown-linux-gnu/debug/ruvllm-esp32 + for variant in esp32 esp32s2 esp32s3 esp32c3 esp32c6; do + echo "=== $variant ===" + echo "stats" | RUVLLM_VARIANT=$variant "$BIN" | grep -E "role=" || { + echo "FAIL: $variant did not boot a role" + exit 1 + } + done + + firmware: + name: build ${{ matrix.target }} + needs: host-test + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - target: esp32 + rust_target: xtensa-esp32-espidf + chip: esp32 + - target: esp32s2 + rust_target: xtensa-esp32s2-espidf + chip: esp32s2 + - target: esp32s3 + rust_target: xtensa-esp32s3-espidf + chip: esp32s3 + - target: esp32c3 + rust_target: riscv32imc-esp-espidf + chip: esp32c3 + - target: esp32c6 + rust_target: riscv32imac-esp-espidf + chip: esp32c6 + steps: + - uses: actions/checkout@v4 + + - name: install espup + run: cargo install espup --locked + + - name: install esp toolchain + run: | + espup install --targets ${{ matrix.target }} + source ~/export-esp.sh + echo "PATH=$PATH" >> $GITHUB_ENV + echo "LIBCLANG_PATH=$LIBCLANG_PATH" >> $GITHUB_ENV + + - name: install espflash + run: cargo install espflash --locked + + - name: build firmware + working-directory: examples/ruvLLM/esp32-flash + env: + RUVLLM_VARIANT: ${{ matrix.target }} + run: | + source ~/export-esp.sh + cargo +esp build \ + --release \ + --target ${{ matrix.rust_target }} \ + --features esp32 + + - name: produce merged .bin + working-directory: examples/ruvLLM/esp32-flash + run: | + source ~/export-esp.sh + espflash save-image \ + --chip ${{ matrix.chip }} \ + --merge \ + target/${{ matrix.rust_target }}/release/ruvllm-esp32 \ + ruvllm-esp32-${{ matrix.target }}.bin + + - name: upload artifact + uses: actions/upload-artifact@v4 + with: + name: ruvllm-esp32-${{ matrix.target }} + path: examples/ruvLLM/esp32-flash/ruvllm-esp32-${{ matrix.target }}.bin + if-no-files-found: error + + release: + name: attach to release + needs: firmware + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + steps: + - name: download all firmware + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: list assets + run: ls -lah dist/ + + - name: upload to release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.release_tag || github.ref_name }} + files: dist/ruvllm-esp32-*.bin + fail_on_unmatched_files: true + generate_release_notes: false diff --git a/docs/adr/ADR-165-tiny-ruvllm-agents-on-esp32-soCs.md b/docs/adr/ADR-165-tiny-ruvllm-agents-on-esp32-soCs.md new file mode 100644 index 000000000..ce8824cd2 --- /dev/null +++ b/docs/adr/ADR-165-tiny-ruvllm-agents-on-esp32-soCs.md @@ -0,0 +1,182 @@ +# ADR-165: Tiny RuvLLM Agents on Heterogeneous ESP32 SoCs + +**Status:** Proposed +**Date:** 2026-04-30 +**Authors:** RuVector / RuvLLM team +**Deciders:** ruv +**Technical Area:** Edge Inference / Embedded Federation / Vector Memory on MCUs +**Related ADRs:** ADR-002 (RuvLLM ↔ Ruvector Integration), ADR-074 (RuvLLM Neural Embeddings — HashEmbedder tier), ADR-084 (ruvllm-wasm Primitive Surface), ADR-090 (Ultra-Low-Bit QAT / Pi-Quantization for ESP32-P4 PSRAM), ADR-091 (INT8 CNN Quantization) +**Closes / supersedes:** Issue #409 framing (`examples/ruvLLM/esp32-flash` as "tiny LLM") + +## 1. Context + +`examples/ruvLLM/esp32-flash` was framed as a "Full-featured LLM v0.2" with INT8 transformer inference, MicroLoRA adaptation, and speculative decoding running on a single MCU SRAM (≤ 520 KB). Issue #409 reproduced the actual behavior: `main.rs` was a control-surface skeleton (PRNG-seeded weights, single-multiply pseudo-attention, no KV cache) that no longer compiled against `lib.rs` after the `optimizations/*` and `ruvector/*` modules were refactored. + +The framing is the root issue. ADR-002 positions ruvLLM as a *serving runtime* and ruvector as the *unified memory layer*. ADR-084 enumerates the canonical ruvllm primitive surface (KV cache, MicroLoRA r1-4, HNSW Semantic Router, MicroRAG, Chat Templates, SONA Instant). ADR-074 ships HashEmbedder as Tier 1 (deterministic FNV-1a + char-bigram + L2-norm, no model). ADR-090 places "real" model inference at ESP32-P4 with 8 MB PSRAM, not at 520 KB SRAM. **None of those ADRs endorse a transformer skeleton in 4 KB of `HVec`.** + +What an ESP32 (Xtensa or RISC-V, 320–520 KB SRAM, no FPU on most variants) *can* do honestly: + +- HNSW kNN search over ≤256 INT8 vectors (`MicroHNSW`) +- RAG retrieval with embedded knowledge entries (`MicroRAG`) +- Semantic memory with type-tagged entries (`SemanticMemory`) +- Anomaly detection from embedding drift (`AnomalyDetector`) +- MicroLoRA rank 1-2 adaptation on cached activations (`MicroLoRA`) +- Sparse-attention masks and binary/PQ quantization helpers (`SparseAttention`, `BinaryVector`, `ProductQuantizer`) +- Multi-chip federation via SPI/UART/ESP-NOW (`PipelineNode`, `FederationMessage`, `SpeculativeDecoder`) +- HashEmbedder Tier 1 from ADR-074 + +The crate `lib.rs` already exports all of these. The example `main.rs` was the only thing not aligned. + +## 2. Decision + +Reframe `examples/ruvLLM/esp32-flash` as **a fleet of tiny ruvLLM/ruvector agents**, where each chip in a federation runs **one specialized role** (or a small composition) drawn from the lib's primitive surface, not a monolithic "LLM." Cross-chip coordination uses the existing `federation::*` types. + +This is the smallest viable framing that: + +1. Compiles against the current `lib.rs` without API drift. +2. Matches what the hardware can actually do. +3. Composes with ADR-090's PSRAM big-model path when it lands (a P4 chip can join the federation as the "drafter" role; smaller chips remain "verifiers" / "indexers"). +4. Honors ADR-002's split: **ruvllm** primitives on each chip, **ruvector** memory shared across chips. + +### 2.1 Tiny-Agent Role Catalog + +A *tiny agent* is one Rust binary, one ESP32 SoC, one role + always-on health surface. + +| Role | Min variant | Primitives used | Federation traffic | +|---|---|---|---| +| **HnswIndexer** | ESP32-C3 (400 KB) | `MicroHNSW<128, 256>`, `HashEmbedder` | inbound: `add(text)`; outbound: `kNN(query, k)` | +| **RagRetriever** | ESP32 (520 KB) | `MicroRAG`, `HashEmbedder` + `MicroHNSW` | inbound: `recall(query)`; outbound: top-k entries | +| **AnomalySentinel** | ESP32-S2 (320 KB) | `AnomalyDetector` | streams `AnomalyResult` events | +| **MemoryArchivist** | ESP32-C6 (512 KB) | `SemanticMemory` (type-tagged) | inbound: `remember(type, text)`; outbound: `recall_by_type` | +| **LoraAdapter** | ESP32-S3 (512 KB + SIMD) | `MicroLoRA`, `LoRAStack` | inbound: rank-1 deltas; outbound: adapted activations | +| **SpeculativeDrafter** | ESP32-S3 (512 KB) | `SpeculativeDecoder` w/ `DraftVerifyConfig::for_five_chips` | drafts → broadcast; consumes `VerifyResult` | +| **PipelineRelay** | any | `PipelineNode { Head/Middle/Tail }` | passes activations along the chain | + +Every binary also exposes the **always-on** surface: a UART CLI for `role`, `stats`, `peers`, `help`. This is the shape that lets `espflash flash --monitor` give an honest "what does this chip do" answer in <1 s of serial output. + +### 2.2 What ships per ESP32 variant (default role assignment) + +| Variant | SRAM | FPU | SIMD | Default role | +|---|---|---|---|---| +| ESP32 | 520 KB | no | no | `RagRetriever` | +| ESP32-S2 | 320 KB | no | no | `AnomalySentinel` | +| ESP32-S3 | 512 KB | yes | yes | `SpeculativeDrafter` (or `LoraAdapter`) | +| ESP32-C3 | 400 KB | no | no | `HnswIndexer` | +| ESP32-C6 | 512 KB | no | no | `MemoryArchivist` | +| ESP32-P4 | 8 MB PSRAM | yes | yes | (deferred — ADR-090 path) | + +Role is selected at boot from a build-time `ROLE` env var (defaulted per-variant by the CI matrix), with a UART override `set-role ` for development. + +### 2.3 Embedding (ADR-074 Tier 1) + +All roles share a common embedder so federation messages are interoperable: **HashEmbedder** — FNV-1a hash of UTF-8 bytes + character-bigram bag, L2-normalized to 64-byte INT8. Deterministic, ~5 µs on Xtensa, no float ops on non-FPU variants. Output dim matches the existing `EMBED_DIM = 64` already used across `lib.rs`. Phase 2 (RlmEmbedder, ADR-074) and Phase 3 (Candle, ADR-074) are explicitly deferred — they don't fit on these chips. + +### 2.4 Federation bus (existing types) + +`CommunicationBus` already enumerates `{Spi, I2c, Uart, EspNow, Parallel}`. v1 uses `Uart` (host bridge) + `EspNow` (chip-to-chip). `FederationMessage` and `MessageHeader` are reused unchanged. `MAX_FEDERATION_SIZE = 8` stays as the upper bound. + +### 2.5 Build & release + +One Cargo target → one role → one .bin per ESP32 variant. CI matrix produces: + +``` +ruvllm-esp32-esp32 (RagRetriever) +ruvllm-esp32-esp32s2 (AnomalySentinel) +ruvllm-esp32-esp32s3 (SpeculativeDrafter) +ruvllm-esp32-esp32c3 (HnswIndexer) +ruvllm-esp32-esp32c6 (MemoryArchivist) +ruvllm-esp32-host-test (host-test binary, x86_64 / aarch64 — for CI smoke) +``` + +Asset names match the URL pattern hardcoded in `npm/web-flasher/index.html` (`${FIRMWARE_BASE_URL}/ruvllm-esp32-${target}`), closing issue #409 obs 2. + +CI uses `espup install` → `cargo +esp build --release --target xtensa-esp32{,s2,s3}-espidf` (and `riscv32imc-esp-espidf` for c3, `riscv32imac-esp-espidf` for c6) → `espflash save-image --merge` → upload to GitHub release. + +## 3. Architecture + +``` +┌──────────────────────────────────────────────────────────┐ +│ Tiny Agent Binary │ +│ │ +│ ┌──────────────────────┐ ┌─────────────────────┐ │ +│ │ Role Selector │ │ HashEmbedder │ │ +│ │ (build-time + │ │ (ADR-074 Tier 1, │ │ +│ │ UART override) │ │ 64-byte INT8) │ │ +│ └─────┬────────────────┘ └─────────┬───────────┘ │ +│ │ enables exactly one of: │ │ +│ ┌─────▼────────┐ ┌──────────┐ ┌────────▼─────────┐ │ +│ │ HnswIndexer │ │ RagRetr │ │ AnomalySentinel │ ... │ +│ │ MicroHNSW │ │ MicroRAG │ │ AnomalyDetector │ │ +│ └─────┬────────┘ └────┬─────┘ └────────┬─────────┘ │ +│ │ federation messages (FederationMessage) │ +│ ┌─────▼──────────────────────────────────────────────┐ │ +│ │ CommunicationBus { Uart, EspNow } │ │ +│ └─────┬──────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────▼─────────────────────────┐ │ +│ │ UART CLI (always on) │ │ +│ │ role | stats | peers | help │ │ +│ └───────────────────────────────┘ │ +└──────────────────────────────────────────────────────────┘ +``` + +## 4. Acceptance Gates + +Each gate must pass before progressing to the next. + +| Gate | Test | Evidence | +|---|---|---| +| **G1** | `cargo build --no-default-features --features host-test` succeeds | local + CI host-test job green | +| **G2** | All 7 roles instantiate in host-test without panic | smoke binary boots each role and prints `role: ` | +| **G3** | UART CLI loop accepts `add`/`search`/`recall`/`check`/`role`/`stats`/`help` in host-test | golden-output test fixture | +| **G4** | `cargo +esp build --release --target {xtensa-esp32s3-espidf}` succeeds in CI | a real `.bin` lands as a release asset | +| **G5** | Flash-and-monitor on attached `/dev/ttyACM0` produces banner + accepts `role` within 5 s | manual or `expect` script in CI hardware-loop (optional) | +| **G6** | All 5 target chips produce `.bin`s; web-flasher URL pattern resolves 200 for each | curl smoke against latest release | + +G1–G3 are the prerequisite for a meaningful PR closing #409. G4–G6 are the firmware-release piece. + +## 5. Out of Scope + +- Real transformer inference at MCU SRAM scale. Deferred to ADR-090's PSRAM path (ESP32-P4, ESP32 + external PSRAM). +- WebGPU / WiFi-6 routing inside the agent. Federation can coexist with WiFi services; the agent itself stays bus-agnostic. +- RlmEmbedder Phase 2 / Candle Phase 3 from ADR-074 — they require corpora and model weights that don't fit. +- Reusing RuView's `esp32-csi-node.bin` artifacts — different application, see issue #409 reply. + +## 6. Consequences + +**Positive** + +- Closes the #409 framing gap permanently: the README and binaries describe what they actually do. +- Eliminates the 27 API-drift errors by replacing the drifted code (no patching of misaligned signatures). +- Lets each chip be evaluated independently before federation, dropping the all-or-nothing build. +- Composes forward with ADR-090: when PSRAM "real model" lands, P4 joins the same federation with the existing `FederationMessage` schema. +- Honest baseline for benchmarks: per-role latency / SRAM / energy numbers on real hardware. + +**Negative** + +- "tinyLLM-on-one-chip" framing in old marketing material no longer matches the example. Acceptable; the issue reporter showed the prior framing was already wrong. +- Federated demos require >1 ESP32 to fully exercise. Single-chip flashing still produces a useful agent for its role. +- ADR-090 PSRAM path remains the only honest answer for "real model on ESP32"; this ADR doesn't accelerate it, only structures the federation it will join. + +## 7. Implementation Roadmap + +| Step | Owner | Dependency | +|---|---|---| +| 1 — Cfg-guard `build.rs` so `host-test` can build | this branch | none | +| 2 — Replace `main.rs` with role-selecting tiny-agent binary | this branch | step 1 | +| 3 — Add `HashEmbedder`-style embedder to `examples/ruvLLM/esp32-flash/src/embed.rs` | this branch | step 2 | +| 4 — README rewrite reflecting roles + ADR-165 | this branch | step 3 | +| 5 — `.github/workflows/ruvllm-esp32-firmware.yml` — espup + matrix build + release upload | follow-up PR | step 2 | +| 6 — Web-flasher URL alignment + npm CLI fallback fix | follow-up PR | step 5 | +| 7 — Hardware smoke script for `/dev/ttyACM0` | follow-up PR | step 5 | + +Steps 1–4 are this PR's scope and unblock issue #409 obs 1+3. Steps 5–7 unblock issue #409 obs 2. + +## 8. References + +- Issue #409 — examples/ruvLLM/esp32-flash gap analysis (williavs) +- ADR-002 — RuvLLM ↔ Ruvector Integration +- ADR-074 — RuvLLM Neural Embeddings (HashEmbedder Tier 1 used here) +- ADR-084 — ruvllm-wasm v2.0.0 (canonical primitive surface) +- ADR-090 — Ultra-Low-Bit QAT / PSRAM big-model path +- `examples/ruvLLM/esp32-flash/src/{lib,federation/mod,ruvector/mod}.rs` — exported surface this ADR composes diff --git a/examples/ruvLLM/esp32-flash/.cargo/config.toml b/examples/ruvLLM/esp32-flash/.cargo/config.toml index 892e56dd2..1d0455945 100644 --- a/examples/ruvLLM/esp32-flash/.cargo/config.toml +++ b/examples/ruvLLM/esp32-flash/.cargo/config.toml @@ -1,10 +1,30 @@ [build] -target = "xtensa-esp32-espidf" +target = "xtensa-esp32s3-espidf" +# ldproxy expands the link line to include the ESP-IDF + FreeRTOS static libs. +# esp-idf-sys's build.rs emits `--ldproxy-linker=...` automatically; do not +# override `rustflags` here or you'll lose those flags. +# One block per supported variant (ADR-165 §2.5). [target.xtensa-esp32-espidf] linker = "ldproxy" runner = "espflash flash --monitor" +[target.xtensa-esp32s2-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + +[target.xtensa-esp32s3-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + +[target.riscv32imc-esp-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + +[target.riscv32imac-esp-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + [env] ESP_IDF_VERSION = "v5.1.2" ESP_IDF_SDKCONFIG_DEFAULTS = "sdkconfig.defaults" diff --git a/examples/ruvLLM/esp32-flash/.gitignore b/examples/ruvLLM/esp32-flash/.gitignore new file mode 100644 index 000000000..f8e65e61b --- /dev/null +++ b/examples/ruvLLM/esp32-flash/.gitignore @@ -0,0 +1,4 @@ +target/ +.embuild/ +sdkconfig +*.bin diff --git a/examples/ruvLLM/esp32-flash/Cargo.lock b/examples/ruvLLM/esp32-flash/Cargo.lock index fd5b134ff..c8dc8b31e 100644 --- a/examples/ruvLLM/esp32-flash/Cargo.lock +++ b/examples/ruvLLM/esp32-flash/Cargo.lock @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "as-slice" @@ -58,16 +58,14 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bindgen" -version = "0.69.5" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools", - "lazy_static", - "lazycell", "log", "prettyplease", "proc-macro2", @@ -75,8 +73,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.111", - "which", + "syn 2.0.117", ] [[package]] @@ -87,9 +84,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bstr" @@ -111,14 +108,14 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -155,14 +152,14 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "cc" -version = "1.2.51" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", @@ -191,9 +188,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -213,20 +210,21 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -312,7 +310,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -323,7 +321,48 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.117", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", ] [[package]] @@ -417,10 +456,14 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7770e30ab55cfbf954c00019522490d6ce26a3334bede05a732ba61010e98e0" dependencies = [ + "defmt 0.3.100", "embedded-io", "embedded-io-async", "enumset", "heapless", + "num_enum", + "serde", + "strum 0.25.0", ] [[package]] @@ -428,6 +471,28 @@ name = "embuild" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6e3e470e31fd4cae065d37f7cad56d42861ba1f9a35aa277694dee3d6b357c4" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "filetime", + "home", + "log", + "regex", + "remove_dir_all", + "serde", + "serde_json", + "shlex", + "strum 0.24.1", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "embuild" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e188ad2bbe82afa841ea4a29880651e53ab86815db036b2cb9f8de3ac32dad75" dependencies = [ "anyhow", "bindgen", @@ -442,9 +507,9 @@ dependencies = [ "serde", "serde_json", "shlex", - "strum", + "strum 0.24.1", "tempfile", - "thiserror", + "thiserror 1.0.69", "which", ] @@ -466,7 +531,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -478,6 +543,12 @@ dependencies = [ "serde", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -490,9 +561,9 @@ dependencies = [ [[package]] name = "esp-idf-hal" -version = "0.44.1" +version = "0.45.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa893ab84c4a7db5ca42ab45e2e09942412976fe3100a9dd72e56ba0a9a58b4" +checksum = "775ce25171dc4f615146a4a27ed3a64c6fd99ced77d7112062f2b19bf933f5db" dependencies = [ "atomic-waker", "embassy-sync", @@ -503,9 +574,9 @@ dependencies = [ "embedded-hal-nb", "embedded-io", "embedded-io-async", - "embuild", + "embuild 0.33.1", "enumset", - "esp-idf-sys 0.35.0", + "esp-idf-sys 0.36.1", "heapless", "log", "nb 1.1.0", @@ -514,16 +585,17 @@ dependencies = [ [[package]] name = "esp-idf-svc" -version = "0.49.1" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac42f9303792348e3217c570b0f0d8280a381d053bcb730c3018ec6873928513" +checksum = "2bc07aaba257d28d54a96af005ca67d0b38876d8837f5d54a3e0547e100b219c" dependencies = [ "embassy-futures", "embedded-hal-async", "embedded-svc", - "embuild", + "embuild 0.33.1", "enumset", "esp-idf-hal", + "futures-io", "heapless", "log", "num_enum", @@ -538,21 +610,21 @@ checksum = "f6b824a1aff03a105d67b11119595dedbbb4943471dd7e47131f417bb2484eae" [[package]] name = "esp-idf-sys" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97e3800686a4d64f3c0a9998be3d6f16c903bca2a425746e97f00ed28cde5e" +checksum = "fb77a3d02b579a60a811ed9be22b78c5e794bc492d833ee7fc44d3a0155885e1" dependencies = [ "anyhow", - "bindgen", "build-time", "cargo_metadata", + "cmake", "const_format", - "embuild", + "embuild 0.33.1", "envy", "libc", "regex", "serde", - "strum", + "strum 0.24.1", "which", ] @@ -569,27 +641,26 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -597,6 +668,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fs_at" version = "0.2.1" @@ -613,44 +690,51 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-task", "pin-project-lite", - "pin-utils", + "slab", ] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", + "wasip3", ] [[package]] @@ -692,6 +776,21 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heapless" version = "0.8.0" @@ -709,6 +808,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "home" version = "0.5.12" @@ -720,9 +825,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -742,6 +847,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -764,48 +875,71 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" [[package]] -name = "lazycell" -version = "1.3.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -819,18 +953,19 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", + "plain", "redox_syscall", ] @@ -842,9 +977,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" @@ -854,9 +989,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "minimal-lexical" @@ -885,7 +1020,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -921,9 +1056,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -931,32 +1066,33 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "prettyplease" @@ -965,47 +1101,78 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1015,9 +1182,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1026,9 +1193,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "remove_dir_all" @@ -1046,9 +1213,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" @@ -1056,7 +1223,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1065,14 +1232,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -1087,10 +1254,10 @@ name = "ruvllm-esp32-flash" version = "0.2.0" dependencies = [ "anyhow", - "embuild", + "embuild 0.32.0", "esp-idf-hal", "esp-idf-svc", - "esp-idf-sys 0.35.0", + "esp-idf-sys 0.36.1", "esp_idf_logger", "heapless", "libm", @@ -1109,9 +1276,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -1144,14 +1311,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -1166,6 +1333,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1178,7 +1351,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.3", ] [[package]] @@ -1187,13 +1369,26 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + [[package]] name = "syn" version = "1.0.109" @@ -1207,9 +1402,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1218,14 +1413,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -1235,7 +1430,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -1246,7 +1450,48 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", ] [[package]] @@ -1260,9 +1505,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -1294,18 +1539,27 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -1316,9 +1570,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1326,26 +1580,60 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "which" version = "4.4.2" @@ -1388,7 +1676,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1399,7 +1687,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1432,7 +1720,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1441,16 +1729,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -1468,31 +1747,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -1501,12 +1763,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1514,91 +1770,146 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" +name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_gnu" -version = "0.53.1" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_gnullvm" +name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_i686_msvc" +name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_i686_msvc" -version = "0.53.1" +name = "winnow" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "zmij" -version = "0.1.9" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/ruvLLM/esp32-flash/Cargo.toml b/examples/ruvLLM/esp32-flash/Cargo.toml index 38a5c226e..c7e1944f3 100644 --- a/examples/ruvLLM/esp32-flash/Cargo.toml +++ b/examples/ruvLLM/esp32-flash/Cargo.toml @@ -31,10 +31,12 @@ federation = [] full = ["federation"] [dependencies] -# ESP-IDF Framework (optional, for ESP32 target) -esp-idf-svc = { version = "0.49", default-features = false, optional = true } -esp-idf-hal = { version = "0.44", default-features = false, optional = true } -esp-idf-sys = { version = "0.35", default-features = false, features = ["binstart"], optional = true } +# ESP-IDF Framework (optional, for ESP32 target). +# 0.52/0.46/0.37 trio — earlier 0.49.1/0.44/0.35 had a *const i8 vs *const u8 +# regression against current bindgen output (see ADR-165 §7 step 2 notes). +esp-idf-svc = { version = "=0.51.0", default-features = false, features = ["std"], optional = true } +esp-idf-hal = { version = "=0.45.2", default-features = false, features = ["std"], optional = true } +esp-idf-sys = { version = "=0.36.1", default-features = false, features = ["binstart", "native"], optional = true } # WASM support (optional) wasm-bindgen = { version = "0.2", optional = true } @@ -53,7 +55,7 @@ anyhow = "1.0" esp_idf_logger = "0.1" [build-dependencies] -embuild = "0.32" +embuild = { version = "0.32", features = ["espidf"] } [profile.release] opt-level = "s" diff --git a/examples/ruvLLM/esp32-flash/README.md b/examples/ruvLLM/esp32-flash/README.md index ef10366e2..4acd517fb 100644 --- a/examples/ruvLLM/esp32-flash/README.md +++ b/examples/ruvLLM/esp32-flash/README.md @@ -1,35 +1,47 @@ -# RuvLLM ESP32 - Tiny LLM Inference Engine for ESP32 Microcontrollers +# RuvLLM ESP32 — Tiny RuvLLM/RuVector Agents on Heterogeneous ESP32 SoCs [![crates.io](https://img.shields.io/crates/v/ruvllm-esp32.svg)](https://crates.io/crates/ruvllm-esp32) [![npm](https://img.shields.io/npm/v/ruvllm-esp32.svg)](https://www.npmjs.com/package/ruvllm-esp32) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -**Run AI locally on ESP32 microcontrollers** - A complete, production-ready LLM inference engine with INT8/Binary quantization, HNSW vector search, RAG (Retrieval-Augmented Generation), and multi-chip federation support. No cloud required. +**See [ADR-165](../../docs/adr/ADR-165-tiny-ruvllm-agents-on-esp32-soCs.md) for the full design.** -## Why RuvLLM ESP32? +Each ESP32 chip runs **one tiny-agent role** drawn from the ruvllm/ruvector primitive surface. Per ADR-165 §2.1, the canonical roles are: -Run AI directly on microcontrollers without cloud dependencies: +| Role | Default variant | Primitives | +|---|---|---| +| `HnswIndexer` | ESP32-C3 | `MicroHNSW` + `HashEmbedder` | +| `RagRetriever` | ESP32 | `MicroRAG` + `MicroHNSW` | +| `AnomalySentinel` | ESP32-S2 | `AnomalyDetector` | +| `MemoryArchivist` | ESP32-C6 | `SemanticMemory` (type-tagged) | +| `LoraAdapter` | ESP32-S3 | `MicroLoRA` rank-1/2 | +| `SpeculativeDrafter` | ESP32-S3 | `SpeculativeDecoder` federation drafter | +| `PipelineRelay` | any | `PipelineNode { Head/Middle/Tail }` | -- **Privacy**: Data never leaves the device -- **Latency**: No network round-trips (2-5ms/token) -- **Cost**: Zero API fees, runs on $4 hardware -- **Offline**: Works without internet connectivity -- **Edge AI**: Perfect for IoT, robotics, wearables +Chips federate over UART / SPI / ESP-NOW using `FederationMessage` (`MAX_FEDERATION_SIZE = 8`). -## Features at a Glance +> **Honest scope.** This example does **not** implement transformer inference at MCU SRAM scale. Real model inference on ESP32 is ADR-090's PSRAM path (ESP32-P4, 8 MB). The "INT8 transformer in 4 KB" framing in the previous README was the gap reported in [issue #409](https://github.com/ruvnet/ruvector/issues/409) and is removed here. What this example *does* ship is the federation-ready primitive layer: HNSW kNN, RAG retrieval, semantic memory, anomaly detection, MicroLoRA rank-1/2 adaptation, and the federation message bus — all `no_std` and all individually exercised on real hardware. -| Category | Features | -|----------|----------| -| **Inference** | INT8 quantized transformers, 2-5ms/token @ 240MHz | -| **Compression** | Binary quantization (32x), Product quantization (8-32x) | -| **Adaptation** | MicroLoRA on-device fine-tuning (2KB overhead) | -| **Attention** | Sparse patterns: sliding window, strided, BigBird | -| **Vector Search** | HNSW index with 1000+ vectors in ~20KB RAM | -| **Memory** | Semantic memory with context-aware retrieval + TTL | -| **RAG** | Retrieval-Augmented Generation for knowledge bases | -| **Anomaly** | Statistical outlier detection via embeddings | -| **Speedup** | Speculative decoding (2-4x potential) | -| **Scaling** | Multi-chip federation with pipeline/tensor parallelism | +## Why a tiny agent per chip + +- **Privacy** — data never leaves the device +- **Latency** — local kNN / recall / anomaly check in <10 ms +- **Cost** — runs on $4 hardware (per role) +- **Offline** — no internet required for the primitives in-tree +- **Composable** — chips federate; tomorrow's ESP32-P4 PSRAM "real model" (ADR-090) joins the same federation as the drafter + +## Primitive surface + +| Category | What ships in `lib.rs` | +|---|---| +| **Vector search** | `MicroHNSW` with INT8 vectors | +| **Quantization** | `BinaryVector` (32× compress), `ProductQuantizer` (8–32×) | +| **Adaptation** | `MicroLoRA` rank-1/2, `LoRAStack` | +| **Sparse attention masks** | `SparseAttention { sliding_window, strided, big_bird }` | +| **Memory** | `SemanticMemory` (type-tagged), `MicroRAG` (knowledge entries + retrieval) | +| **Anomaly** | `AnomalyDetector` over embedding drift | +| **Federation** | `PipelineNode`, `FederationMessage`, `SpeculativeDecoder`, `CommunicationBus { Spi, I2c, Uart, EspNow, Parallel }` | +| **Embedder (ADR-074 Tier 1)** | `hash_embed(text)` — FNV-1a + char bigrams + integer-normalize, no float, no model | ## Supported Hardware diff --git a/examples/ruvLLM/esp32-flash/build.rs b/examples/ruvLLM/esp32-flash/build.rs index 112ec3f76..e451b5832 100644 --- a/examples/ruvLLM/esp32-flash/build.rs +++ b/examples/ruvLLM/esp32-flash/build.rs @@ -1,3 +1,11 @@ fn main() { - embuild::espidf::sysenv::output(); + // build.rs runs on the host, so `cfg(target_os)` would always be the host's + // OS — not the cargo --target. Read CARGO_CFG_TARGET_OS instead. + // ADR-165 §7 step 1: lets `--features host-test` and other non-espidf + // targets compile without the espidf-only embuild path, while still + // re-emitting esp-idf-sys's link args (incl. --ldproxy-linker) when + // cross-compiling to *-espidf. + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("espidf") { + embuild::espidf::sysenv::output(); + } } diff --git a/examples/ruvLLM/esp32-flash/sdkconfig.defaults b/examples/ruvLLM/esp32-flash/sdkconfig.defaults index 2c46f043e..4039e7049 100644 --- a/examples/ruvLLM/esp32-flash/sdkconfig.defaults +++ b/examples/ruvLLM/esp32-flash/sdkconfig.defaults @@ -1,19 +1,16 @@ -# RuvLLM ESP32 SDK Configuration +# Base sdkconfig defaults shared by all ESP32 variants (ADR-165 §2.5). +# Per-variant tweaks live in `sdkconfig.defaults.`. -# Memory optimization -CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +# Memory CONFIG_SPIRAM_SUPPORT=n # Logging CONFIG_LOG_DEFAULT_LEVEL_INFO=y -# Console UART -CONFIG_ESP_CONSOLE_UART_DEFAULT=y -CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 +# Stack — TinyAgent allocates HNSW/RAG/etc. on stack; per-variant files +# may bump this further. +CONFIG_ESP_MAIN_TASK_STACK_SIZE=98304 -# Stack size -CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 - -# Disable unused features to save memory +# Trim CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=4096 CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=2048 diff --git a/examples/ruvLLM/esp32-flash/sdkconfig.defaults.esp32s3 b/examples/ruvLLM/esp32-flash/sdkconfig.defaults.esp32s3 new file mode 100644 index 000000000..671bd0c87 --- /dev/null +++ b/examples/ruvLLM/esp32-flash/sdkconfig.defaults.esp32s3 @@ -0,0 +1,30 @@ +# ESP32-S3 sdkconfig overrides for ruvllm-esp32 tiny-agent (ADR-165). +# Loaded by esp-idf-sys when target = xtensa-esp32s3-espidf. + +# CPU & memory +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y +CONFIG_SPIRAM=n +CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y +CONFIG_ESP32S3_DATA_CACHE_64KB=y + +# Stack — needs to fit a TinyAgent which holds optional HNSW(256×~150B) + +# RAG(64×~160B) + SemanticMemory(32×~150B) + LoRA. Bumping to 96 KB headroom. +CONFIG_ESP_MAIN_TASK_STACK_SIZE=98304 + +# Console: native USB-Serial/JTAG (esptool detected USB mode `USB-Serial/JTAG`). +# Setting USB_SERIAL_JTAG=y in the Kconfig "choice" disables the UART default +# automatically. CONFIG_ESP_CONSOLE_SECONDARY_NONE keeps stdout from being +# duplicated to UART0 (which has no physical pins on the dev board USB). +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y + +# Flash +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y + +# Logging +CONFIG_LOG_DEFAULT_LEVEL_INFO=y + +# Trim +CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=4096 +CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=2048 diff --git a/examples/ruvLLM/esp32-flash/src/main.rs b/examples/ruvLLM/esp32-flash/src/main.rs index b7f891c7e..e10715814 100644 --- a/examples/ruvLLM/esp32-flash/src/main.rs +++ b/examples/ruvLLM/esp32-flash/src/main.rs @@ -1,604 +1,537 @@ -//! RuvLLM ESP32 - Complete Flashable Implementation +//! RuvLLM ESP32 — Tiny Agents on Heterogeneous SoCs //! -//! Full-featured LLM inference engine for ESP32 with: -//! - INT8/Binary quantized transformer inference -//! - Product quantization (8-32x compression) -//! - MicroLoRA on-device adaptation -//! - Sparse attention patterns -//! - HNSW vector search (1000+ vectors) -//! - Semantic memory with context -//! - RAG (Retrieval-Augmented Generation) -//! - Anomaly detection -//! - Multi-chip federation -//! - Pipeline/tensor parallelism -//! - Speculative decoding +//! Implements ADR-165: each ESP32 chip runs **one tiny-agent role** drawn from +//! the ruvllm/ruvector primitive surface defined in `lib.rs`. This is the +//! ADR-aligned replacement for the prior single-chip "tiny LLM" framing — see +//! issue #409 for why the previous transformer skeleton was misleading. //! -//! Flash with: espflash flash --monitor --port COM6 +//! Roles (one binary, one chip, one role): +//! - HnswIndexer — MicroHNSW kNN index + HashEmbedder +//! - RagRetriever — MicroRAG retrieval over embedded knowledge entries +//! - AnomalySentinel — AnomalyDetector streaming on embedding drift +//! - MemoryArchivist — SemanticMemory with type-tagged entries +//! - LoraAdapter — MicroLoRA rank-1/2 on cached activations +//! - SpeculativeDrafter — SpeculativeDecoder federation drafter +//! - PipelineRelay — PipelineNode head/middle/tail +//! +//! Always-on UART CLI: `role`, `stats`, `peers`, `add `, `search `, +//! `recall `, `check `, `learn `, `lora `, `set-role `, +//! `help`. +//! +//! Build paths: +//! - `--features esp32` cross-compile to ESP-IDF, UART CLI on uart0 +//! - `--features host-test` x86_64 / aarch64 stdio harness for CI + dev +//! - `--features wasm` browser shim (calls into the same primitives) -#[cfg(feature = "esp32")] -use esp_idf_svc::hal::prelude::*; -#[cfg(feature = "esp32")] -use esp_idf_svc::hal::uart::{self, UartDriver}; -#[cfg(feature = "esp32")] -use esp_idf_svc::hal::gpio; #[cfg(feature = "esp32")] use esp_idf_svc::sys::link_patches; use heapless::Vec as HVec; use heapless::String as HString; +#[allow(unused_imports)] use log::*; -// Import library modules -use ruvllm_esp32::prelude::*; +// Explicit imports — avoid `prelude::*` because it brings in `Result` and +// `Error` aliases that shadow the standard ones (causes E0107/E0277). use ruvllm_esp32::{ - HNSWConfig, RAGConfig, MemoryType, DraftVerifyConfig, - PipelineConfig, PipelineRole, AnomalyConfig, PQConfig, LoRAConfig, PruningConfig, - AttentionPattern, DistanceMetric, euclidean_distance_i8, + Esp32Variant, + MicroHNSW, MicroRAG, SemanticMemory, AnomalyDetector, MicroVector, + MicroLoRA, LoRAConfig, + HNSWConfig, RAGConfig, MemoryType, AnomalyConfig, DistanceMetric, + federation::{ChipId, FederationMode, CommunicationBus, FederationConfig}, }; // ============================================================================ // CONFIGURATION // ============================================================================ -const VOCAB_SIZE: usize = 256; +/// Embedding dim shared across federation messages (ADR-165 §2.3). const EMBED_DIM: usize = 64; -const NUM_LAYERS: usize = 2; -const NUM_HEADS: usize = 4; -const MAX_SEQ_LEN: usize = 32; -const MAX_KNOWLEDGE: usize = 64; -const HNSW_CAPACITY: usize = 256; +/// HNSW capacity per indexer chip. 256 inflates `TinyAgent` past the main-task +/// stack on real hardware — 32 keeps the on-stack size manageable for the demo +/// while still exercising the full kNN path. CI / production should `Box` the +/// fields and bump capacity (ADR-165 §7 follow-up). +const HNSW_CAPACITY: usize = 32; // ============================================================================ -// QUANTIZED TYPES +// TINY-AGENT ROLES (ADR-165 §2.1) // ============================================================================ -#[derive(Clone)] -struct QuantizedWeights { - data: HVec, - scale: i32, - zero_point: i8, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Role { + HnswIndexer, + RagRetriever, + AnomalySentinel, + MemoryArchivist, + LoraAdapter, + SpeculativeDrafter, + PipelineRelay, } -impl QuantizedWeights { - fn new(size: usize) -> Self { - let mut data = HVec::new(); - for i in 0..size.min(4096) { - let val = ((i * 17 + 31) % 256) as i8 - 64; - let _ = data.push(val); +impl Role { + /// Default role per variant (ADR-165 §2.2). + const fn default_for(variant: Esp32Variant) -> Self { + match variant { + Esp32Variant::Esp32 => Role::RagRetriever, + Esp32Variant::Esp32S2 => Role::AnomalySentinel, + Esp32Variant::Esp32S3 => Role::SpeculativeDrafter, + Esp32Variant::Esp32C3 => Role::HnswIndexer, + Esp32Variant::Esp32C6 => Role::MemoryArchivist, } - Self { data, scale: 128, zero_point: 0 } } -} -// ============================================================================ -// EMBEDDING TABLE -// ============================================================================ - -struct EmbeddingTable { - embeddings: [[i8; EMBED_DIM]; VOCAB_SIZE], -} - -impl EmbeddingTable { - fn new() -> Self { - let mut embeddings = [[0i8; EMBED_DIM]; VOCAB_SIZE]; - for (token, embed) in embeddings.iter_mut().enumerate() { - for (i, val) in embed.iter_mut().enumerate() { - *val = (((token * 31 + i * 17) % 256) as i8).wrapping_sub(64); - } + fn as_str(&self) -> &'static str { + match self { + Role::HnswIndexer => "HnswIndexer", + Role::RagRetriever => "RagRetriever", + Role::AnomalySentinel => "AnomalySentinel", + Role::MemoryArchivist => "MemoryArchivist", + Role::LoraAdapter => "LoraAdapter", + Role::SpeculativeDrafter => "SpeculativeDrafter", + Role::PipelineRelay => "PipelineRelay", } - Self { embeddings } } - fn lookup(&self, token: u16) -> &[i8; EMBED_DIM] { - &self.embeddings[(token as usize) % VOCAB_SIZE] + fn parse(s: &str) -> Option { + match s { + "HnswIndexer" | "hnsw" => Some(Role::HnswIndexer), + "RagRetriever" | "rag" => Some(Role::RagRetriever), + "AnomalySentinel" | "anomaly" => Some(Role::AnomalySentinel), + "MemoryArchivist" | "memory" => Some(Role::MemoryArchivist), + "LoraAdapter" | "lora" => Some(Role::LoraAdapter), + "SpeculativeDrafter" | "drafter" => Some(Role::SpeculativeDrafter), + "PipelineRelay" | "relay" => Some(Role::PipelineRelay), + _ => None, + } } } // ============================================================================ -// ATTENTION WITH SPARSE PATTERNS +// HASH EMBEDDER (ADR-074 Tier 1) // ============================================================================ -struct MicroAttention { - wq: QuantizedWeights, - wk: QuantizedWeights, - wv: QuantizedWeights, - wo: QuantizedWeights, - sparse: SparseAttention, - head_dim: usize, -} - -impl MicroAttention { - fn new(pattern: AttentionPattern) -> Self { - let head_dim = EMBED_DIM / NUM_HEADS; - Self { - wq: QuantizedWeights::new(EMBED_DIM * EMBED_DIM), - wk: QuantizedWeights::new(EMBED_DIM * EMBED_DIM), - wv: QuantizedWeights::new(EMBED_DIM * EMBED_DIM), - wo: QuantizedWeights::new(EMBED_DIM * EMBED_DIM), - sparse: SparseAttention::new(pattern, MAX_SEQ_LEN, 8), - head_dim, - } +/// Deterministic FNV-1a + char-bigram bag, signed-INT8 normalized to ±64. +/// No floats, no model weights, no cold start. Federation-interoperable +/// because every role on every variant uses the same function. +fn hash_embed(text: &str) -> [i8; EMBED_DIM] { + let mut acc = [0i32; EMBED_DIM]; + let bytes = text.as_bytes(); + + // Unigrams (FNV-1a) + let mut h: u32 = 0x811C9DC5; + for (i, &b) in bytes.iter().enumerate() { + h ^= b as u32; + h = h.wrapping_mul(0x01000193); + let slot = (h as usize ^ i) % EMBED_DIM; + acc[slot] = acc[slot].saturating_add(((h >> 16) & 0xFF) as i32 - 128); } - fn forward(&self, input: &[i8], output: &mut [i8], seq_pos: usize) { - // Get sparse mask for current position - let mask = self.sparse.get_mask(seq_pos); - - for (i, val) in input.iter().enumerate() { - if i < output.len() { - let w_idx = i % self.wq.data.len(); - // Apply sparse attention - only attend to allowed positions - let attended = if i < mask.len() && mask[i] { - (*val as i32 * self.wq.data[w_idx] as i32) >> 7 - } else { - 0 - }; - output[i] = attended.clamp(-127, 127) as i8; - } + // Char bigrams + if bytes.len() >= 2 { + for win in bytes.windows(2) { + let mut bh: u32 = 0x811C9DC5; + bh ^= win[0] as u32; bh = bh.wrapping_mul(0x01000193); + bh ^= win[1] as u32; bh = bh.wrapping_mul(0x01000193); + let slot = (bh as usize) % EMBED_DIM; + acc[slot] = acc[slot].saturating_add(((bh >> 8) & 0xFF) as i32 - 128); } } -} - -// ============================================================================ -// FEED-FORWARD WITH PRUNING -// ============================================================================ -struct FeedForward { - w1: QuantizedWeights, - w2: QuantizedWeights, - pruner: LayerPruner, -} - -impl FeedForward { - fn new(config: PruningConfig) -> Self { - Self { - w1: QuantizedWeights::new(EMBED_DIM * 4 * EMBED_DIM), - w2: QuantizedWeights::new(4 * EMBED_DIM * EMBED_DIM), - pruner: LayerPruner::new(config), - } - } + // L2-style normalize: clamp to ±64 by largest absolute (integer-only). + let mut max_abs: i32 = 1; + for &v in &acc { if v.abs() > max_abs { max_abs = v.abs(); } } - fn forward(&self, input: &[i8], output: &mut [i8]) { - for (i, val) in input.iter().enumerate() { - if i < output.len() { - let w_idx = i % self.w1.data.len(); - // Check if weight is pruned - let weight = if !self.pruner.is_pruned(w_idx) { - self.w1.data[w_idx] as i32 - } else { - 0 - }; - let hidden = (*val as i32 * weight) >> 7; - let activated = hidden.max(0); - output[i] = activated.clamp(-127, 127) as i8; - } - } + let mut out = [0i8; EMBED_DIM]; + for (i, &v) in acc.iter().enumerate() { + out[i] = ((v.saturating_mul(64)) / max_abs).clamp(-127, 127) as i8; } + out } // ============================================================================ -// TRANSFORMER LAYER WITH LORA +// TINY AGENT // ============================================================================ -struct TransformerLayer { - attention: MicroAttention, - ffn: FeedForward, +struct TinyAgent { + role: Role, + variant: Esp32Variant, + chip_id: ChipId, + + // Primitives — only allocated for the active role. + hnsw: Option>, + rag: Option, + memory: Option, + anomaly: Option, lora: Option, -} -impl TransformerLayer { - fn new(lora_config: Option) -> Self { - let attn_pattern = AttentionPattern::SlidingWindow { window_size: 8 }; - let prune_config = PruningConfig::default(); + // Counters surfaced via `stats`. + ops: u32, +} - Self { - attention: MicroAttention::new(attn_pattern), - ffn: FeedForward::new(prune_config), - lora: lora_config.map(|c| MicroLoRA::new(c)), - } +impl TinyAgent { + fn new(variant: Esp32Variant, role: Role, chip_id: ChipId) -> Self { + let mut agent = Self { + role, variant, chip_id, + hnsw: None, rag: None, memory: None, anomaly: None, lora: None, + ops: 0, + }; + agent.activate(); + agent } - fn forward(&self, input: &[i8], output: &mut [i8], seq_pos: usize) { - let mut attn_out = [0i8; EMBED_DIM]; - self.attention.forward(input, &mut attn_out, seq_pos); - - // Apply LoRA adaptation if enabled - if let Some(ref lora) = self.lora { - let adapted = lora.forward(&attn_out); - for (i, v) in adapted.iter().enumerate().take(EMBED_DIM) { - attn_out[i] = attn_out[i].saturating_add(*v); + fn activate(&mut self) { + // Reset all primitives, then enable the ones this role needs. + self.hnsw = None; + self.rag = None; + self.memory = None; + self.anomaly = None; + self.lora = None; + + match self.role { + Role::HnswIndexer => { + self.hnsw = Some(MicroHNSW::new(HNSWConfig { + m: if self.variant.has_simd() { 8 } else { 4 }, + m_max0: if self.variant.has_simd() { 16 } else { 8 }, + ef_construction: 32, + ef_search: 16, + metric: DistanceMetric::Euclidean, + binary_mode: !self.variant.has_fpu(), + })); + } + Role::RagRetriever => { + self.rag = Some(MicroRAG::new(RAGConfig::default())); + self.hnsw = Some(MicroHNSW::new(HNSWConfig::default())); + } + Role::AnomalySentinel => { + self.anomaly = Some(AnomalyDetector::new(AnomalyConfig::default())); + } + Role::MemoryArchivist => { + self.memory = Some(SemanticMemory::new()); + } + Role::LoraAdapter => { + let cfg = LoRAConfig { rank: 2, dim: EMBED_DIM, scale: 8, frozen: false }; + self.lora = MicroLoRA::new(cfg, self.chip_id.0 as u32 ^ 0xDEAD_BEEF).ok(); + } + Role::SpeculativeDrafter => { + // The drafter holds an HNSW index for context lookup; the + // SpeculativeDecoder itself is constructed per-request from + // FederationConfig, so we don't keep it here. + self.hnsw = Some(MicroHNSW::new(HNSWConfig::default())); + } + Role::PipelineRelay => { + // Relay is stateless on data; routing comes from the + // FederationConfig::default chain (Pipeline mode, SPI bus). } - } - - // Residual connection - for i in 0..EMBED_DIM { - attn_out[i] = attn_out[i].saturating_add(input[i] / 2); - } - - self.ffn.forward(&attn_out, output); - - // Residual connection - for i in 0..EMBED_DIM { - output[i] = output[i].saturating_add(attn_out[i] / 2); } } -} - -// ============================================================================ -// TINY MODEL WITH FULL FEATURES -// ============================================================================ - -struct TinyModel { - embeddings: EmbeddingTable, - layers: [TransformerLayer; NUM_LAYERS], - lm_head: QuantizedWeights, - binary_embed: Option, - pq: Option, -} - -impl TinyModel { - fn new(use_lora: bool, use_pq: bool) -> Self { - let lora_config = if use_lora { - Some(LoRAConfig { rank: 2, alpha: 4, input_dim: EMBED_DIM, output_dim: EMBED_DIM }) - } else { - None - }; - - let pq = if use_pq { - Some(ProductQuantizer::new(PQConfig { - dim: EMBED_DIM, - num_subspaces: 8, - num_centroids: 16, - })) - } else { - None - }; - Self { - embeddings: EmbeddingTable::new(), - layers: [ - TransformerLayer::new(lora_config.clone()), - TransformerLayer::new(lora_config), - ], - lm_head: QuantizedWeights::new(EMBED_DIM * VOCAB_SIZE), - binary_embed: Some(BinaryVector::new()), - pq, - } + fn set_role(&mut self, role: Role) { + self.role = role; + self.activate(); } - fn forward(&self, token: u16, seq_pos: usize) -> u16 { - let embed = self.embeddings.lookup(token); - let mut hidden = *embed; + // ---- HnswIndexer + SpeculativeDrafter ---- + fn hnsw_add(&mut self, text: &str) -> Result { + let hnsw = self.hnsw.as_mut().ok_or("role does not own hnsw")?; + let emb = hash_embed(text); + let v = MicroVector::::from_i8(&emb, hnsw.len() as u32) + .ok_or("embed dim mismatch")?; + let idx = hnsw.insert(&v)?; + self.ops = self.ops.saturating_add(1); + Ok(idx) + } - // Pass through layers - for layer in &self.layers { - let mut output = [0i8; EMBED_DIM]; - layer.forward(&hidden, &mut output, seq_pos); - hidden = output; + fn hnsw_search(&mut self, query: &str, k: usize) -> Result, &'static str> { + let hnsw = self.hnsw.as_ref().ok_or("role does not own hnsw")?; + let emb = hash_embed(query); + let mut ids = HVec::new(); + for r in hnsw.search(&emb, k).iter().take(k) { + let _ = ids.push(r.id); } + self.ops = self.ops.saturating_add(1); + Ok(ids) + } - // Project to vocabulary - let mut max_logit = i32::MIN; - let mut max_token = 0u16; - - for t in 0..VOCAB_SIZE { - let mut logit = 0i32; - for i in 0..EMBED_DIM { - let w_idx = t * EMBED_DIM + i; - if w_idx < self.lm_head.data.len() { - logit += hidden[i] as i32 * self.lm_head.data[w_idx] as i32; - } - } - if logit > max_logit { - max_logit = logit; - max_token = t as u16; + // ---- RagRetriever ---- + fn rag_add(&mut self, text: &str) -> Result { + let rag = self.rag.as_mut().ok_or("role does not own rag")?; + let emb = hash_embed(text); + let id = rag.add_knowledge(text, &emb, "uart-cli", 50)?; + // mirror into the local hnsw index if present + if let Some(hnsw) = self.hnsw.as_mut() { + if let Some(v) = MicroVector::::from_i8(&emb, id) { + let _ = hnsw.insert(&v); } } - - max_token + self.ops = self.ops.saturating_add(1); + Ok(id) } -} -// ============================================================================ -// FULL INFERENCE ENGINE -// ============================================================================ - -struct MicroEngine { - model: TinyModel, - hnsw: MicroHNSW, - rag: MicroRAG, - memory: SemanticMemory, - anomaly: AnomalyDetector, - speculative: Option, - tokens_generated: u32, - variant: Esp32Variant, -} - -impl MicroEngine { - fn new(variant: Esp32Variant, enable_speculative: bool) -> Self { - info!("Initializing MicroEngine for {:?}...", variant); - info!(" Available SRAM: {} KB", variant.sram_bytes() / 1024); - info!(" Max model RAM: {} KB", variant.max_model_ram() / 1024); - - let use_lora = variant.sram_bytes() >= 400 * 1024; - let use_pq = variant.sram_bytes() >= 320 * 1024; - - let hnsw_config = HNSWConfig { - m: if variant.has_simd() { 8 } else { 4 }, - m_max0: if variant.has_simd() { 16 } else { 8 }, - ef_construction: 32, - ef_search: 16, - metric: DistanceMetric::Euclidean, - binary_mode: !variant.has_fpu(), - }; - - let rag_config = RAGConfig::default(); - let anomaly_config = AnomalyConfig::default(); - - let speculative = if enable_speculative && variant.sram_bytes() >= 512 * 1024 { - Some(SpeculativeDecoder::new(DraftVerifyConfig { - draft_length: 4, - max_rejections: 2, - temperature: 100, - verify_all: false, - })) + fn rag_recall(&mut self, query: &str) -> Result, &'static str> { + let rag = self.rag.as_ref().ok_or("role does not own rag")?; + let emb = hash_embed(query); + let res = rag.retrieve(&emb); + self.ops = self.ops.saturating_add(1); + let mut out = HString::new(); + if let Some((entry, _score)) = res.entries.first() { + for c in entry.text.chars().take(127) { let _ = out.push(c); } } else { - None - }; - - Self { - model: TinyModel::new(use_lora, use_pq), - hnsw: MicroHNSW::new(hnsw_config), - rag: MicroRAG::new(rag_config), - memory: SemanticMemory::new(), - anomaly: AnomalyDetector::new(anomaly_config), - speculative, - tokens_generated: 0, - variant, + let _ = out.push_str("(no match)"); } + Ok(out) } - fn generate(&mut self, input: &[u16], max_tokens: usize) -> HVec { - let mut output = HVec::new(); - let mut current = *input.last().unwrap_or(&1); - let mut seq_pos = input.len(); - - if let Some(ref mut spec) = self.speculative { - // Speculative decoding: generate drafts and verify - while output.len() < max_tokens { - // Draft phase - let mut drafts = HVec::::new(); - for _ in 0..4 { - let next = self.model.forward(current, seq_pos); - let _ = drafts.push(next); - current = next; - seq_pos += 1; - } - - // Verify phase (simplified) - for &token in drafts.iter() { - if output.len() < max_tokens { - let _ = output.push(token); - self.tokens_generated += 1; - } - if token == 0 { return output; } - } - } - } else { - // Standard decoding - for _ in 0..max_tokens { - let next = self.model.forward(current, seq_pos); - let _ = output.push(next); - self.tokens_generated += 1; - current = next; - seq_pos += 1; - if next == 0 { break; } - } - } + // ---- AnomalySentinel ---- + fn anomaly_learn(&mut self, text: &str) -> Result { + let det = self.anomaly.as_mut().ok_or("role does not own anomaly")?; + let emb = hash_embed(text); + let r = det.add_sample(&emb)?; + self.ops = self.ops.saturating_add(1); + Ok(r.is_anomaly) + } - output + fn anomaly_check(&mut self, text: &str) -> Result { + let det = self.anomaly.as_ref().ok_or("role does not own anomaly")?; + let emb = hash_embed(text); + self.ops = self.ops.saturating_add(1); + Ok(det.check(&emb).is_anomaly) } - fn add_knowledge(&mut self, text: &str) -> Result { - let embedding = embed_text(text); + // ---- MemoryArchivist ---- + fn mem_remember(&mut self, kind: MemoryType, text: &str) -> Result { + let mem = self.memory.as_mut().ok_or("role does not own memory")?; + let emb = hash_embed(text); + let id = mem.remember(kind, text, &emb)?; + self.ops = self.ops.saturating_add(1); + Ok(id) + } - // Add to HNSW index - let mut vec_data = HVec::new(); - for &v in embedding.iter() { - let _ = vec_data.push(v); + fn mem_recall(&mut self, query: &str) -> Result, &'static str> { + let mem = self.memory.as_mut().ok_or("role does not own memory")?; + let emb = hash_embed(query); + let hits = mem.recall(&emb, 1); + self.ops = self.ops.saturating_add(1); + let mut out = HString::new(); + if let Some((m, _)) = hits.first() { + for c in m.text.chars().take(127) { let _ = out.push(c); } + } else { + let _ = out.push_str("(no recall)"); } - let vec = MicroVector { data: vec_data, id: self.hnsw.len() as u32 }; - self.hnsw.insert(&vec)?; - - // Add to RAG - self.rag.add_knowledge(text, &embedding)?; - - // Add to semantic memory - self.memory.add_memory(&embedding, &[], MemoryType::Factual)?; - - Ok(vec.id) + Ok(out) } - fn query_rag(&self, query: &str, k: usize) -> HVec, 4> { - let embedding = embed_text(query); - - // Search HNSW - let results = self.hnsw.search(&embedding, k); - - // Also query RAG - let rag_results = self.rag.retrieve(&embedding, k); - - let mut texts = HVec::new(); - for result in rag_results.iter().take(k) { - let mut s = HString::new(); - for c in result.content.iter() { - let _ = s.push(*c); - } - let _ = texts.push(s); + // ---- LoraAdapter ---- + fn lora_apply_demo(&mut self) -> Result<(), &'static str> { + let lora = self.lora.as_mut().ok_or("role does not own lora")?; + let mut input = [0i8; EMBED_DIM]; + for (i, b) in input.iter_mut().enumerate() { + *b = ((i as i32 * 13) % 127 - 63) as i8; } - texts + let mut out = [0i32; EMBED_DIM]; + lora.apply(&input, &mut out); + self.ops = self.ops.saturating_add(1); + Ok(()) } - fn check_anomaly(&mut self, text: &str) -> AnomalyResult { - let embedding = embed_text(text); - self.anomaly.check(&embedding) + // ---- Stats ---- + fn stats_line(&self) -> HString<256> { + let mut s = HString::new(); + let _ = s.push_str("role="); + let _ = s.push_str(self.role.as_str()); + let _ = s.push_str(" variant="); + let _ = s.push_str(variant_name(self.variant)); + let _ = s.push_str(" sram_kb="); + let _ = s.push_str(&format_u32((self.variant.sram_bytes() / 1024) as u32)); + let _ = s.push_str(" ops="); + let _ = s.push_str(&format_u32(self.ops)); + if let Some(h) = &self.hnsw { + let _ = s.push_str(" hnsw="); + let _ = s.push_str(&format_u32(h.len() as u32)); + } + if let Some(r) = &self.rag { + let _ = s.push_str(" rag="); + let _ = s.push_str(&format_u32(r.len() as u32)); + } + if let Some(m) = &self.memory { + let _ = s.push_str(" mem="); + let _ = s.push_str(&format_u32(m.len() as u32)); + } + if let Some(a) = &self.anomaly { + let _ = s.push_str(" anomaly_samples="); + let _ = s.push_str(&format_u32(a.len() as u32)); + } + s } +} - fn stats(&self) -> EngineStats { - EngineStats { - tokens_generated: self.tokens_generated, - knowledge_entries: self.rag.len(), - hnsw_vectors: self.hnsw.len(), - memory_entries: self.memory.len(), - variant: self.variant, - has_speculative: self.speculative.is_some(), - } +fn variant_name(v: Esp32Variant) -> &'static str { + match v { + Esp32Variant::Esp32 => "esp32", + Esp32Variant::Esp32S2 => "esp32s2", + Esp32Variant::Esp32S3 => "esp32s3", + Esp32Variant::Esp32C3 => "esp32c3", + Esp32Variant::Esp32C6 => "esp32c6", } } -#[derive(Debug)] -struct EngineStats { - tokens_generated: u32, - knowledge_entries: usize, - hnsw_vectors: usize, - memory_entries: usize, - variant: Esp32Variant, - has_speculative: bool, +fn parse_variant(s: &str) -> Option { + match s { + "esp32" => Some(Esp32Variant::Esp32), + "esp32s2" => Some(Esp32Variant::Esp32S2), + "esp32s3" => Some(Esp32Variant::Esp32S3), + "esp32c3" => Some(Esp32Variant::Esp32C3), + "esp32c6" => Some(Esp32Variant::Esp32C6), + _ => None, + } } // ============================================================================ -// TEXT EMBEDDING +// FEDERATION DESCRIPTOR // ============================================================================ -fn embed_text(text: &str) -> [i8; EMBED_DIM] { - let mut embedding = [0i8; EMBED_DIM]; - - for (i, byte) in text.bytes().enumerate() { - let idx = i % EMBED_DIM; - embedding[idx] = embedding[idx].saturating_add( - ((byte as i32 * 31 + i as i32 * 17) % 256 - 128) as i8 / 4 - ); - } - - // Normalize - let mut max_val = 1i8; - for v in &embedding { - max_val = max_val.max(v.abs()); +fn federation_descriptor(chip_id: ChipId) -> FederationConfig { + FederationConfig { + num_chips: 5, + chip_id, + mode: FederationMode::Pipeline, + bus: CommunicationBus::Uart, + layers_per_chip: 1, + heads_per_chip: 1, + enable_pipelining: true, } - if max_val > 1 { - for v in &mut embedding { - *v = (*v as i32 * 64 / max_val as i32) as i8; - } - } - - embedding } // ============================================================================ -// UART COMMAND PARSER +// COMMAND PROCESSOR (shared by esp32 + host-test) // ============================================================================ -fn process_command(cmd: &str, engine: &mut MicroEngine) -> HString<512> { +fn process_command(cmd: &str, agent: &mut TinyAgent) -> HString<512> { let mut response = HString::new(); let cmd = cmd.trim(); - if cmd.starts_with("gen ") { - let prompt = &cmd[4..]; - let tokens: HVec = prompt.bytes().take(8).map(|b| b as u16).collect(); - let output = engine.generate(&tokens, 10); - - let _ = response.push_str("Generated: "); - for (i, t) in output.iter().enumerate() { - if i > 0 { let _ = response.push_str(", "); } - let c = (*t as u8) as char; - if c.is_ascii_alphanumeric() || c == ' ' { - let _ = response.push(c); - } else { - let _ = response.push('?'); + if cmd == "role" { + let _ = response.push_str("role: "); + let _ = response.push_str(agent.role.as_str()); + } else if cmd == "variant" { + let _ = response.push_str("variant: "); + let _ = response.push_str(variant_name(agent.variant)); + } else if cmd == "stats" { + let s = agent.stats_line(); + let _ = response.push_str(&s); + } else if cmd == "peers" { + let fed = federation_descriptor(agent.chip_id); + let _ = response.push_str("federation: "); + let _ = response.push_str(&format_u32(fed.num_chips as u32)); + let _ = response.push_str(" chips, mode=Pipeline, bus=Uart, chip_id="); + let _ = response.push_str(&format_u32(agent.chip_id.0 as u32)); + } else if let Some(rest) = cmd.strip_prefix("set-role ") { + match Role::parse(rest.trim()) { + Some(r) => { + agent.set_role(r); + let _ = response.push_str("role set to "); + let _ = response.push_str(agent.role.as_str()); } + None => { let _ = response.push_str("unknown role (try: hnsw|rag|anomaly|memory|lora|drafter|relay)"); } } - } else if cmd.starts_with("add ") { - let knowledge = &cmd[4..]; - match engine.add_knowledge(knowledge) { - Ok(id) => { - let _ = response.push_str("Added knowledge #"); - let _ = response.push_str(&format_u32(id)); + } else if let Some(rest) = cmd.strip_prefix("add ") { + let r = match agent.role { + Role::HnswIndexer | Role::SpeculativeDrafter => agent.hnsw_add(rest).map(|i| i as u32), + Role::RagRetriever => agent.rag_add(rest), + _ => Err("`add` requires HnswIndexer / SpeculativeDrafter / RagRetriever"), + }; + match r { + Ok(id) => { let _ = response.push_str("added id="); let _ = response.push_str(&format_u32(id)); } + Err(e) => { let _ = response.push_str("err: "); let _ = response.push_str(e); } + } + } else if let Some(rest) = cmd.strip_prefix("search ") { + match agent.hnsw_search(rest, 4) { + Ok(ids) => { + let _ = response.push_str("hits: "); + for (i, id) in ids.iter().enumerate() { + if i > 0 { let _ = response.push_str(","); } + let _ = response.push_str(&format_u32(*id)); + } + if ids.is_empty() { let _ = response.push_str("(none)"); } } - Err(e) => { - let _ = response.push_str("Error: "); - let _ = response.push_str(e); + Err(e) => { let _ = response.push_str("err: "); let _ = response.push_str(e); } + } + } else if let Some(rest) = cmd.strip_prefix("recall ") { + let r = match agent.role { + Role::RagRetriever => agent.rag_recall(rest), + Role::MemoryArchivist => agent.mem_recall(rest), + _ => Err("`recall` requires RagRetriever or MemoryArchivist"), + }; + match r { + Ok(s) => { let _ = response.push_str("top: "); let _ = response.push_str(&s); } + Err(e) => { let _ = response.push_str("err: "); let _ = response.push_str(e); } + } + } else if let Some(rest) = cmd.strip_prefix("learn ") { + match agent.anomaly_learn(rest) { + Ok(was_anom) => { + let _ = response.push_str("learned, prev_was_anomaly="); + let _ = response.push_str(if was_anom { "true" } else { "false" }); } + Err(e) => { let _ = response.push_str("err: "); let _ = response.push_str(e); } } - } else if cmd.starts_with("ask ") { - let query = &cmd[4..]; - let results = engine.query_rag(query, 2); - - if results.is_empty() { - let _ = response.push_str("No results found"); - } else { - let _ = response.push_str("Found: "); - for (i, text) in results.iter().enumerate() { - if i > 0 { let _ = response.push_str(" | "); } - let _ = response.push_str(text.as_str()); + } else if let Some(rest) = cmd.strip_prefix("check ") { + match agent.anomaly_check(rest) { + Ok(is_anom) => { + let _ = response.push_str(if is_anom { "ANOMALY" } else { "NORMAL" }); } + Err(e) => { let _ = response.push_str("err: "); let _ = response.push_str(e); } } - } else if cmd.starts_with("anomaly ") { - let text = &cmd[8..]; - let result = engine.check_anomaly(text); - let _ = response.push_str(if result.is_anomaly { "ANOMALY" } else { "NORMAL" }); - let _ = response.push_str(" (score: "); - let _ = response.push_str(&format_i32(result.score)); - let _ = response.push_str(", threshold: "); - let _ = response.push_str(&format_i32(result.threshold)); - let _ = response.push_str(")"); - } else if cmd == "stats" { - let stats = engine.stats(); - let _ = response.push_str("Tokens: "); - let _ = response.push_str(&format_u32(stats.tokens_generated)); - let _ = response.push_str(", Knowledge: "); - let _ = response.push_str(&format_u32(stats.knowledge_entries as u32)); - let _ = response.push_str(", HNSW: "); - let _ = response.push_str(&format_u32(stats.hnsw_vectors as u32)); - let _ = response.push_str(", Memory: "); - let _ = response.push_str(&format_u32(stats.memory_entries as u32)); - let _ = response.push_str(", Spec: "); - let _ = response.push_str(if stats.has_speculative { "yes" } else { "no" }); - } else if cmd == "features" { - let _ = response.push_str("Features:\n"); - let _ = response.push_str(" - Binary quantization (32x compress)\n"); - let _ = response.push_str(" - Product quantization (8-32x)\n"); - let _ = response.push_str(" - MicroLoRA adaptation\n"); - let _ = response.push_str(" - Sparse attention\n"); - let _ = response.push_str(" - HNSW vector search\n"); - let _ = response.push_str(" - Semantic memory\n"); - let _ = response.push_str(" - RAG retrieval\n"); - let _ = response.push_str(" - Anomaly detection\n"); - if engine.speculative.is_some() { - let _ = response.push_str(" - Speculative decoding\n"); + } else if let Some(rest) = cmd.strip_prefix("remember ") { + // Format: `remember ` where type ∈ {fact,event,context,...} + let mut parts = rest.splitn(2, ' '); + let kind_s = parts.next().unwrap_or(""); + let text = parts.next().unwrap_or(""); + let kind = match kind_s { + "fact" => MemoryType::Fact, + "event" => MemoryType::Event, + "context" => MemoryType::Context, + "preference" => MemoryType::Preference, + "procedure" => MemoryType::Procedure, + "entity" => MemoryType::Entity, + "emotion" => MemoryType::Emotion, + "state" => MemoryType::State, + _ => { let _ = response.push_str("unknown memory type"); return response; } + }; + match agent.mem_remember(kind, text) { + Ok(id) => { let _ = response.push_str("remembered id="); let _ = response.push_str(&format_u32(id)); } + Err(e) => { let _ = response.push_str("err: "); let _ = response.push_str(e); } } - } else if cmd == "help" { - let _ = response.push_str("Commands:\n"); - let _ = response.push_str(" gen - Generate tokens\n"); - let _ = response.push_str(" add - Add to knowledge base\n"); - let _ = response.push_str(" ask - Query knowledge\n"); - let _ = response.push_str(" anomaly - Check for anomaly\n"); - let _ = response.push_str(" stats - Show statistics\n"); - let _ = response.push_str(" features - List features\n"); - let _ = response.push_str(" help - This help"); + } else if cmd == "lora" { + match agent.lora_apply_demo() { + Ok(()) => { let _ = response.push_str("lora applied (rank=2, dim=64)"); } + Err(e) => { let _ = response.push_str("err: "); let _ = response.push_str(e); } + } + } else if cmd == "help" || cmd.is_empty() { + let _ = response.push_str(HELP_TEXT); } else { - let _ = response.push_str("Unknown command. Type 'help'"); + let _ = response.push_str("unknown command. type 'help'"); } response } +const HELP_TEXT: &str = "ruvllm-esp32 tiny-agent (ADR-165). commands:\n\ + role | variant | stats | peers | help\n\ + set-role \n\ + add (HnswIndexer / RagRetriever / SpeculativeDrafter)\n\ + search (any role with hnsw)\n\ + recall (RagRetriever / MemoryArchivist)\n\ + learn (AnomalySentinel)\n\ + check (AnomalySentinel)\n\ + remember (MemoryArchivist) types: fact|event|context|...\n\ + lora (LoraAdapter — applies a demo rank-2 update)"; + +// ============================================================================ +// FORMATTING +// ============================================================================ + fn format_u32(n: u32) -> HString<16> { let mut s = HString::new(); - if n == 0 { - let _ = s.push('0'); - return s; - } - + if n == 0 { let _ = s.push('0'); return s; } let mut digits = [0u8; 10]; let mut i = 0; let mut num = n; @@ -607,7 +540,6 @@ fn format_u32(n: u32) -> HString<16> { num /= 10; i += 1; } - while i > 0 { i -= 1; let _ = s.push((b'0' + digits[i]) as char); @@ -615,164 +547,146 @@ fn format_u32(n: u32) -> HString<16> { s } -fn format_i32(n: i32) -> HString<16> { - let mut s = HString::new(); - if n < 0 { - let _ = s.push('-'); - return s; +// ============================================================================ +// ENTRY POINTS +// ============================================================================ + +#[cfg(feature = "esp32")] +fn jtag_write(s: &str) { + use core::ffi::c_void; + unsafe { + esp_idf_svc::sys::usb_serial_jtag_write_bytes( + s.as_ptr() as *const c_void, + s.len(), + 20, // ticks_to_wait (~200 ms) — enough for buffer drain + ); } - format_u32(n as u32) } -// ============================================================================ -// MAIN -// ============================================================================ +#[cfg(feature = "esp32")] +fn jtag_writeln(s: &str) { + jtag_write(s); + jtag_write("\r\n"); +} #[cfg(feature = "esp32")] fn main() -> anyhow::Result<()> { link_patches(); - esp_idf_svc::log::EspLogger::initialize_default(); - - info!("╔══════════════════════════════════════════╗"); - info!("║ RuvLLM ESP32 - Full Feature LLM v0.2 ║"); - info!("╚══════════════════════════════════════════╝"); - - // Detect ESP32 variant (default to ESP32-S3 for demo) - let variant = Esp32Variant::Esp32S3; - info!("Detected: {:?} ({} KB SRAM)", variant, variant.sram_bytes() / 1024); - - let peripherals = Peripherals::take()?; - let tx = peripherals.pins.gpio1; - let rx = peripherals.pins.gpio3; - - let config = uart::config::Config::default() - .baudrate(Hertz(115200)); - - let uart = UartDriver::new( - peripherals.uart0, - tx, - rx, - Option::::None, - Option::::None, - &config - )?; - - info!("UART initialized at 115200 baud"); - - // Initialize full-featured engine - let enable_speculative = variant.sram_bytes() >= 512 * 1024; - let mut engine = MicroEngine::new(variant, enable_speculative); - info!("Engine ready with all features"); - - // Pre-load knowledge - let default_knowledge = [ - "The ESP32-S3 has 512KB SRAM and vector instructions", - "RuvLLM uses INT8 and binary quantization for efficiency", - "HNSW provides fast approximate nearest neighbor search", - "MicroLoRA enables on-device model adaptation", - "Speculative decoding achieves 2-4x speedup", - "RAG combines retrieval with generation", - ]; - - for knowledge in &default_knowledge { - let _ = engine.add_knowledge(knowledge); - } - info!("Loaded {} default knowledge entries", engine.stats().knowledge_entries); - - let startup = "\r\n\ - ════════════════════════════════════════════\r\n\ - RuvLLM ESP32 Full-Feature v0.2\r\n\ - ════════════════════════════════════════════\r\n\ - Features: Binary Quant, PQ, LoRA, HNSW, RAG\r\n\ - Semantic Memory, Anomaly Detection\r\n\ - Speculative Decoding, Federation\r\n\ - ════════════════════════════════════════════\r\n\ - Type 'help' for commands\r\n\ - > "; - uart.write(startup.as_bytes())?; - - let mut cmd_buffer: HVec = HVec::new(); + let variant = match option_env!("RUVLLM_VARIANT") { + Some(s) => parse_variant(s).unwrap_or(Esp32Variant::Esp32S3), + None => Esp32Variant::Esp32S3, + }; + let role = match option_env!("RUVLLM_ROLE") { + Some(s) => Role::parse(s).unwrap_or_else(|| Role::default_for(variant)), + None => Role::default_for(variant), + }; + let chip_id = ChipId(option_env!("RUVLLM_CHIP_ID").and_then(|s| s.parse().ok()).unwrap_or(0)); + + jtag_writeln(""); + jtag_writeln("=== ruvllm-esp32 tiny-agent (ADR-165) ==="); + + let mut hdr: HString<128> = HString::new(); + let _ = hdr.push_str("variant="); + let _ = hdr.push_str(variant_name(variant)); + let _ = hdr.push_str(" role="); + let _ = hdr.push_str(role.as_str()); + let _ = hdr.push_str(" chip_id="); + let _ = hdr.push_str(&format_u32(chip_id.0 as u32)); + let _ = hdr.push_str(" sram_kb="); + let _ = hdr.push_str(&format_u32((variant.sram_bytes() / 1024) as u32)); + jtag_writeln(&hdr); + + let mut agent = TinyAgent::new(variant, role, chip_id); + jtag_writeln("[ready] type 'help' for commands"); + jtag_write("> "); + + // CLI loop: read bytes from USB-Serial/JTAG, dispatch on newline. + let mut linebuf = [0u8; 256]; + let mut n: usize = 0; loop { - let mut byte = [0u8; 1]; - - if uart.read(&mut byte, 10).is_ok() && byte[0] != 0 { - let c = byte[0]; - - if c == b'\r' || c == b'\n' { - if !cmd_buffer.is_empty() { - let cmd_str: HString<256> = cmd_buffer.iter() - .map(|&b| b as char) - .collect(); - - uart.write(b"\r\n")?; - - let response = process_command(cmd_str.as_str(), &mut engine); - uart.write(response.as_bytes())?; - uart.write(b"\r\n> ")?; - - cmd_buffer.clear(); - } - } else if c == 127 || c == 8 { - if !cmd_buffer.is_empty() { - cmd_buffer.pop(); - uart.write(b"\x08 \x08")?; - } - } else if c >= 32 && c < 127 { - if cmd_buffer.len() < 255 { - let _ = cmd_buffer.push(c); - uart.write(&[c])?; + let mut byte = 0u8; + let r = unsafe { + esp_idf_svc::sys::usb_serial_jtag_read_bytes( + &mut byte as *mut u8 as *mut core::ffi::c_void, + 1, + 100, // ticks + ) + }; + if r > 0 { + if byte == b'\r' || byte == b'\n' { + if n > 0 { + let cmd = core::str::from_utf8(&linebuf[..n]).unwrap_or(""); + let resp = process_command(cmd, &mut agent); + jtag_writeln(resp.as_str()); + jtag_write("> "); + n = 0; } + } else if byte == 0x7f || byte == 0x08 { + if n > 0 { n -= 1; } + } else if n < linebuf.len() { + linebuf[n] = byte; + n += 1; } } } } -// Host testing main (for development) #[cfg(all(not(feature = "esp32"), feature = "host-test"))] fn main() { - println!("RuvLLM ESP32 Host Test Mode"); - println!("This is for development testing only."); - - let variant = Esp32Variant::Esp32S3; - println!("Simulating: {:?} ({} KB SRAM)", variant, variant.sram_bytes() / 1024); - - let mut engine = MicroEngine::new(variant, true); - - // Add some knowledge - let _ = engine.add_knowledge("Test knowledge entry 1"); - let _ = engine.add_knowledge("Another test entry"); - - // Generate tokens - let tokens: HVec = [b'H' as u16, b'e' as u16, b'l' as u16, b'l' as u16, b'o' as u16] - .iter().copied().collect(); - let output = engine.generate(&tokens, 5); - - println!("Generated {} tokens", output.len()); - println!("Stats: {:?}", engine.stats()); + use std::io::{self, BufRead, Write}; + + // Allow `RUVLLM_VARIANT` / `RUVLLM_ROLE` to drive host smoke tests. + let variant = std::env::var("RUVLLM_VARIANT") + .ok() + .and_then(|s| parse_variant(&s)) + .unwrap_or(Esp32Variant::Esp32S3); + let role = std::env::var("RUVLLM_ROLE") + .ok() + .and_then(|s| Role::parse(&s)) + .unwrap_or_else(|| Role::default_for(variant)); + let chip_id = ChipId(std::env::var("RUVLLM_CHIP_ID") + .ok().and_then(|s| s.parse().ok()).unwrap_or(0)); + + let mut agent = TinyAgent::new(variant, role, chip_id); + + println!("=== ruvllm-esp32 tiny-agent (ADR-165) — host-test ==="); + println!("variant={} role={} chip_id={} sram_kb={}", + variant_name(variant), role.as_str(), chip_id.0, variant.sram_bytes() / 1024); + println!("type 'help' for commands. EOF to exit."); + + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut out = stdout.lock(); + let _ = write!(out, "> "); let _ = out.flush(); + + for line in stdin.lock().lines() { + let line = match line { Ok(l) => l, Err(_) => break }; + let resp = process_command(&line, &mut agent); + println!("{}", resp.as_str()); + let _ = write!(out, "> "); let _ = out.flush(); + } } -// WASM entry point #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; #[cfg(feature = "wasm")] #[wasm_bindgen] pub fn wasm_init() -> String { - "RuvLLM ESP32 WASM Module Initialized".to_string() + "ruvllm-esp32 tiny-agent (ADR-165) WASM shim".to_string() } #[cfg(feature = "wasm")] #[wasm_bindgen] -pub fn wasm_generate(prompt: &str) -> String { - format!("Generated from: {}", prompt) +pub fn wasm_command(cmd: &str) -> String { + let mut agent = TinyAgent::new(Esp32Variant::Esp32S3, Role::HnswIndexer, ChipId(0)); + let r = process_command(cmd, &mut agent); + r.as_str().to_string() } -// Default main for other builds #[cfg(all(not(feature = "esp32"), not(feature = "host-test"), not(feature = "wasm")))] fn main() { - println!("RuvLLM ESP32 Flash"); - println!("Build with --features esp32 for ESP32 target"); - println!("Build with --features host-test for development"); - println!("Build with --features wasm for WebAssembly"); + eprintln!("ruvllm-esp32 tiny-agent (ADR-165)"); + eprintln!("Build with one of: --features esp32 | --features host-test | --features wasm"); } From 6176e8f952e6df0f4de87ffebb634957633b5da6 Mon Sep 17 00:00:00 2001 From: ruvnet Date: Thu, 30 Apr 2026 13:28:28 -0400 Subject: [PATCH 2/4] fix(ruvllm-esp32): USB-Serial/JTAG VFS + per-toolchain CI matrix; ADR-166 ops manual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three coordinated fixes from the rc1 device + CI run: 1. **`src/main.rs` — install + use the USB-Serial/JTAG interrupt-mode driver** With `CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y` alone, ESP-IDF installs a polling-mode driver. Bootloader logs reach `/dev/ttyACM0` but Rust `std::io::stdout` / `stderr` / `stdin` do not — TX buffers indefinitely until reset, RX returns undefined data. Symptom: panic prints work (panic flushes on reboot) but `eprintln!` during steady state goes nowhere. Fix: at the top of main, call `usb_serial_jtag_driver_install` then `esp_vfs_usb_serial_jtag_use_driver`. After both calls, `eprintln!` flushes via interrupt-driven TX and `stdin().lock().lines()` blocks on USB-CDC RX exactly like host stdio. Also drops the FFI-write helpers (`jtag_write` / `jtag_writeln`) in favor of std::io. The interactive CLI loop becomes the same shape as the host-test path: `for line in stdin.lock().lines() { … }`. 2. **`.github/workflows/ruvllm-esp32-firmware.yml` — per-toolchain matrix + ldproxy install** rc1 CI matrix failures: - all Xtensa builds: `error: linker 'ldproxy' not found` — `cargo install espflash --locked` only installs espflash; ldproxy was missing. - both RISC-V builds (esp32c3, esp32c6): `error: toolchain 'esp' is not installed` — `espup install --targets ` is a no-op for the Rust toolchain; the build then ran `cargo +esp build` and panicked. Fix: - Install `ldproxy` and `espflash` together: `cargo install espflash ldproxy --locked` (always, both toolchains need it). - Per-matrix `toolchain: esp` (Xtensa) vs `nightly` (RISC-V). - `if: matrix.toolchain == 'esp'` → espup install path. - `if: matrix.toolchain == 'nightly'` → `rustup toolchain install nightly --component rust-src`. - `cargo +${{ matrix.toolchain }} build …` picks the right channel per target. - `unset RUSTFLAGS` in the build step (mold doesn't speak Xtensa or RISC-V-esp). 3. **`docs/adr/ADR-166-esp32-rust-cross-compile-bringup-ops.md` — full operations manual** Companion to ADR-165. ADR-165 says *what* runs; ADR-166 says *how* to build it. 16 sections, ~14 KB. Captures every failure mode hit during rc1 (14 distinct ones), with root cause and fix for each, the pinned crate trio (esp-idf-svc 0.51 / esp-idf-hal 0.45 / esp-idf-sys 0.36), the per-target toolchain matrix, the build.rs `CARGO_CFG_TARGET_OS` pattern, the .cargo/config.toml linker contract, the sdkconfig defaults split, the USB-Serial/JTAG console two-call setup, the stack budget for TinyAgent, the CI workflow contract, the operational acceptance gates G1–G6, and a searchable failure → remedy table. Includes a verification log section with the actual rc1 transcripts from real ESP32-S3 hardware (`ac:a7:04:e2:66:24`). Closes: - rc1 CI failure modes 13 (ldproxy) + 14 (RISC-V toolchain) — workflow fix - ADR-165 §7 step 5 (USB-CDC console parity) — VFS fix - Documentation gap so the next contributor doesn't bisect 14 failures Co-Authored-By: claude-flow --- .github/workflows/ruvllm-esp32-firmware.yml | 38 +- ...66-esp32-rust-cross-compile-bringup-ops.md | 435 ++++++++++++++++++ examples/ruvLLM/esp32-flash/src/main.rs | 98 ++-- 3 files changed, 499 insertions(+), 72 deletions(-) create mode 100644 docs/adr/ADR-166-esp32-rust-cross-compile-bringup-ops.md diff --git a/.github/workflows/ruvllm-esp32-firmware.yml b/.github/workflows/ruvllm-esp32-firmware.yml index 5c777fc1c..5f3f226dd 100644 --- a/.github/workflows/ruvllm-esp32-firmware.yml +++ b/.github/workflows/ruvllm-esp32-firmware.yml @@ -56,52 +56,70 @@ jobs: fail-fast: false matrix: include: + # Xtensa targets — use the espup-installed `esp` Rust toolchain. - target: esp32 rust_target: xtensa-esp32-espidf chip: esp32 + toolchain: esp - target: esp32s2 rust_target: xtensa-esp32s2-espidf chip: esp32s2 + toolchain: esp - target: esp32s3 rust_target: xtensa-esp32s3-espidf chip: esp32s3 + toolchain: esp + # RISC-V targets — use upstream nightly with rust-src. - target: esp32c3 rust_target: riscv32imc-esp-espidf chip: esp32c3 + toolchain: nightly - target: esp32c6 rust_target: riscv32imac-esp-espidf chip: esp32c6 + toolchain: nightly steps: - uses: actions/checkout@v4 - - name: install espup - run: cargo install espup --locked + - name: install rust nightly + rust-src (RISC-V) + if: matrix.toolchain == 'nightly' + run: | + rustup toolchain install nightly --component rust-src + rustup default nightly - - name: install esp toolchain + - name: install espup + esp toolchain (Xtensa) + if: matrix.toolchain == 'esp' run: | + cargo install espup --locked espup install --targets ${{ matrix.target }} source ~/export-esp.sh echo "PATH=$PATH" >> $GITHUB_ENV echo "LIBCLANG_PATH=$LIBCLANG_PATH" >> $GITHUB_ENV - - name: install espflash - run: cargo install espflash --locked + - name: install ldproxy + espflash + run: cargo install espflash ldproxy --locked - name: build firmware working-directory: examples/ruvLLM/esp32-flash env: RUVLLM_VARIANT: ${{ matrix.target }} run: | - source ~/export-esp.sh - cargo +esp build \ + if [ "${{ matrix.toolchain }}" = "esp" ]; then + source ~/export-esp.sh + fi + # Override the host's mold linker — Xtensa/RISC-V targets need the + # toolchain-provided gcc/ld via ldproxy. + unset RUSTFLAGS + cargo +${{ matrix.toolchain }} build \ --release \ - --target ${{ matrix.rust_target }} \ - --features esp32 + --target ${{ matrix.rust_target }} - name: produce merged .bin working-directory: examples/ruvLLM/esp32-flash run: | - source ~/export-esp.sh + if [ "${{ matrix.toolchain }}" = "esp" ]; then + source ~/export-esp.sh + fi espflash save-image \ --chip ${{ matrix.chip }} \ --merge \ diff --git a/docs/adr/ADR-166-esp32-rust-cross-compile-bringup-ops.md b/docs/adr/ADR-166-esp32-rust-cross-compile-bringup-ops.md new file mode 100644 index 000000000..8b6fa69ed --- /dev/null +++ b/docs/adr/ADR-166-esp32-rust-cross-compile-bringup-ops.md @@ -0,0 +1,435 @@ +# ADR-166: ESP32 Rust Cross-Compile + Bring-Up Operations Manual + +**Status:** Proposed +**Date:** 2026-04-30 +**Authors:** RuVector / RuvLLM team +**Deciders:** ruv +**Technical Area:** Embedded Build Pipeline / ESP-IDF Toolchain / USB-Serial/JTAG Console / CI Matrix +**Companion to:** [ADR-165](ADR-165-tiny-ruvllm-agents-on-esp32-soCs.md) (what runs on each chip) +**Related ADRs:** ADR-002, ADR-074, ADR-084, ADR-090, ADR-091 +**Closes:** Issue #409 obs 2 (no firmware in releases) once §9 is in place + +--- + +## 1. Context + +ADR-165 established the *what*: each ESP32 chip runs one tiny ruvLLM/ruvector role. This ADR establishes the *how*: the canonical Rust cross-compile + bring-up operations needed to ship that firmware reliably across 5 ESP32 variants, in CI, and on real hardware. + +It exists because the rc1 bring-up surfaced **14 distinct build/runtime failure modes** between "first attempt" and "ELF runs on `/dev/ttyACM0`," many of which are not documented in any single Espressif or `esp-rs` source. The point of this ADR is to ensure the next contributor (or the next chip variant) does not have to re-discover them. + +### What was discovered during rc1 bring-up + +| # | Failure mode | Where | Fix in this ADR | +|---|---|---|---| +| 1 | `embuild::espidf` not in scope | local build | §3 + §4 | +| 2 | `*const i8` vs `*const u8` mismatch in esp-idf-svc 0.49.1 | local build | §3 | +| 3 | `host-test` feature could not build (espidf cfg unconditional) | local + CI | §5 | +| 4 | `mold` linker rejected on Xtensa | local build | §6 | +| 5 | `Cannot locate argument '--ldproxy-linker '` (ldproxy panic) | local build | §5 (root cause), §7 | +| 6 | `cannot find 'log' in 'esp_idf_svc'` (`alloc` feature gate) | local build | §3 | +| 7 | `no field 'queue_non_blocking'` (esp-idf-hal 0.46.2) | local build | §3 | +| 8 | `linker 'xtensa-esp32s3-elf-gcc' failed: undefined reference to memcpy/...` | local build | §5 + §7 | +| 9 | UART0 console not visible on `/dev/ttyACM0` (USB-Serial/JTAG dev board) | device | §10 | +| 10 | `Guru Meditation: Double exception` after `app_main()` | device | §11 | +| 11 | sdkconfig stack stayed at 12 KB despite override | device | §8 | +| 12 | Banner reaches host only on panic flush, not steady state | device | §10 | +| 13 | CI: `linker 'ldproxy' not found` | CI | §9 | +| 14 | CI: `toolchain 'esp' is not installed` (RISC-V c3/c6) | CI | §9 | + +Every entry below is grounded in a real failure transcript captured during rc1. + +--- + +## 2. Decision + +Adopt the canonical configuration in §3–§11 as the *only supported* path for building, flashing, and running ruvllm-esp32 firmware. Document it in this ADR, encode it in `examples/ruvLLM/esp32-flash/Cargo.toml`, `.cargo/config.toml`, `build.rs`, `sdkconfig.defaults*`, `src/main.rs`, and `.github/workflows/ruvllm-esp32-firmware.yml`. Treat any deviation as a regression that requires re-running G1–G6 (§12). + +This is not an architectural decision (ADR-165 owns that). It is an *operational* decision: pin the toolchain, pin the crate trio, pin the build-script invocation pattern, pin the CI workflow shape, and document the diagnostics so the next person doesn't bisect 14 failure modes again. + +--- + +## 3. Crate-version matrix (PINNED) + +The viable Rust crate trio for ESP-IDF v5.1.2 against the current `bindgen` output: + +```toml +[dependencies] +esp-idf-svc = { version = "=0.51.0", default-features = false, features = ["std"], optional = true } +esp-idf-hal = { version = "=0.45.2", default-features = false, features = ["std"], optional = true } +esp-idf-sys = { version = "=0.36.1", default-features = false, features = ["binstart", "native"], optional = true } + +[build-dependencies] +embuild = { version = "0.32", features = ["espidf"] } +``` + +**Why exact `=` pins:** + +- **esp-idf-svc 0.49.1 → 0.51.0**: 0.49.1 has 4 `*const i8` vs `*const u8` mismatches in `tls.rs` and `private/cstr.rs` against current bindgen output (rust-esp toolchain promotes C `char` to `u8`, the crate still expects `i8`). 0.51.0 is the first release where this is fixed and the trio still compiles. +- **esp-idf-hal 0.46.2 → 0.45.2**: 0.46.2 references `TransmitConfig.queue_non_blocking` which is not present in the bound struct — release-side regression. 0.45.2 is the latest version that compiles cleanly. +- **esp-idf-sys 0.37.x → 0.36.1**: 0.37.x changes the `binstart` layout. We have not yet validated that path; 0.36.1 is what shipped in rc1. +- **embuild `features = ["espidf"]`**: without this feature, `embuild::espidf::sysenv::output()` does not exist as a callable item — `cannot find 'espidf' in 'embuild'` at build-script compile time. Default features on `embuild` *do not* include `espidf`; you must opt in. + +**Why explicit `default-features = false` on the trio**: avoids accidentally pulling in `alloc`-gated modules (`esp_idf_svc::log`) and unrelated peripherals. `binstart` is mandatory for esp-idf-sys to wire `app_main` to Rust `main`. `native` is mandatory for esp-idf-sys's build script to download the SDK and emit the link args. + +**Do not bump these without re-running the full G1–G6 acceptance** (§12). The next safe trio bump should be done as ADR-167. + +--- + +## 4. Toolchain matrix per target + +| Variant | Rust target triple | Toolchain | How installed | +|---|---|---|---| +| esp32 | `xtensa-esp32-espidf` | `esp` (custom Rust) | `espup install --targets esp32` | +| esp32s2 | `xtensa-esp32s2-espidf` | `esp` | `espup install --targets esp32s2` | +| esp32s3 | `xtensa-esp32s3-espidf` | `esp` | `espup install --targets esp32s3` | +| esp32c3 | `riscv32imc-esp-espidf` | `nightly` + `rust-src` | `rustup toolchain install nightly --component rust-src` | +| esp32c6 | `riscv32imac-esp-espidf` | `nightly` + `rust-src` | same as c3 | + +The Xtensa cores (LX6 on esp32, LX7 on s2/s3) require a **custom Rust toolchain** (`esp` channel) because upstream LLVM does not support Xtensa. `espup` installs a forked LLVM, GCC, and the Rust `esp` channel into `~/.rustup/toolchains/esp/` and `~/.espressif/`. + +The RISC-V cores (c3/c6) use **upstream nightly** + `rust-src` (for `-Z build-std`). + +**Same crate, different `cargo + build` per target.** The CI matrix in §9 picks the right toolchain per matrix entry. + +`espup install` invocation matters: `espup install --targets ` for an Xtensa chip installs the `esp` toolchain. Calling it for a RISC-V chip is a no-op for the Rust toolchain — that's why **rc1 c3 + c6 jobs failed with `error: toolchain 'esp' is not installed`** before we split the workflow. + +--- + +## 5. The build.rs cfg pitfall (the silent killer) + +**Bug**: `build.rs` runs on the **host** at compile time. `cfg(target_os = "espidf")` evaluates against the *host's* OS — always `linux`/`macos`/`windows`. It is *never* `espidf`, even when `--target xtensa-esp32s3-espidf` is on the command line. + +**Symptom**: `embuild::espidf::sysenv::output()` is gated behind a host-cfg in `build.rs`. The call is silently compiled out. The build script runs but emits zero `cargo:rustc-link-arg=...` directives. The final link step only sees `compiler_builtins.rlib` and `--ldproxy-linker` is absent. ldproxy panics with `Cannot locate argument '--ldproxy-linker '`. + +This looks like a build-system mystery for ~3 hours. + +**Fix**: read the cargo target at build-script runtime via `CARGO_CFG_TARGET_OS`: + +```rust +fn main() { + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("espidf") { + embuild::espidf::sysenv::output(); + } +} +``` + +This pattern is required for **any** build script that conditionally runs target-specific logic during a cross-compile. It is not specific to ESP-IDF — anyone who has a host-side build script and a `*-espidf` target needs it. + +`embuild::espidf::sysenv::output()` is what re-emits esp-idf-sys's accumulated link args (`--ldproxy-linker=`, `--ldproxy-cwd=<…>`, all the linker-script `-T` files, and the ESP-IDF static-library list) onto **our binary's** link line. Without it firing, our binary doesn't get linked against ESP-IDF. + +--- + +## 6. RUSTFLAGS environment override + +The RuVector workspace ships with `RUSTFLAGS=-C link-arg=-fuse-ld=mold` in the developer shell environment. `mold` does not support Xtensa or RISC-V-esp. Symptom: + +``` +mold: fatal: unknown command line option: --dynconfig=xtensa_esp32s3.so +collect2: error: ld returned 1 exit status +``` + +`cargo`'s `RUSTFLAGS` env var **replaces** the per-target `rustflags` from `.cargo/config.toml` rather than augmenting them. So you cannot fix this by adding things to the per-target config. + +**Fix in shell**: `env -u RUSTFLAGS cargo + build …` + +**Fix in CI**: `unset RUSTFLAGS` in the relevant step (see §9). + +**Do NOT** add `rustflags = [...]` to the per-target `.cargo/config.toml` blocks as a workaround — that overrides esp-idf-sys's link args, and you'll be back to symptom #5. + +--- + +## 7. `.cargo/config.toml` — one block per supported target + +```toml +[build] +target = "xtensa-esp32s3-espidf" + +[target.xtensa-esp32-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + +[target.xtensa-esp32s2-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + +[target.xtensa-esp32s3-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + +[target.riscv32imc-esp-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + +[target.riscv32imac-esp-espidf] +linker = "ldproxy" +runner = "espflash flash --monitor" + +[env] +ESP_IDF_VERSION = "v5.1.2" +ESP_IDF_SDKCONFIG_DEFAULTS = "sdkconfig.defaults" + +[unstable] +build-std = ["std", "panic_abort"] +``` + +**Rules**: + +- One `[target.]` block per supported variant. Don't omit; cargo silently uses the host linker for any unconfigured target, which fails as in symptom #4. +- `linker = "ldproxy"` for every block. ldproxy is a thin wrapper that forwards to the toolchain-provided `xtensa-esp*-elf-gcc` or `riscv32-esp-elf-gcc`, picking up the linker-script `-T` flags from esp-idf-sys. +- **Never** add `rustflags = [...]` here. If you need to override RUSTFLAGS, do it via the environment (§6). +- `runner = "espflash flash --monitor"` lets `cargo run` flash + open serial in one step. +- `build-std = ["std", "panic_abort"]` is required because ESP-IDF targets aren't tier-2; std must be rebuilt for them. Nightly Rust handles this. + +`ldproxy` itself must be installed locally and in CI: `cargo install ldproxy --locked`. + +--- + +## 8. sdkconfig defaults — base + per-variant split + +ESP-IDF reads `sdkconfig.defaults` files in order. We use a base + variant-specific layered approach: + +`sdkconfig.defaults` (chip-agnostic baseline): + +``` +CONFIG_SPIRAM_SUPPORT=n +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=98304 +CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=4096 +CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=2048 +``` + +`sdkconfig.defaults.esp32s3` (per-variant overrides — applied last, win conflicts): + +``` +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y +CONFIG_SPIRAM=n +CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y +CONFIG_ESP32S3_DATA_CACHE_64KB=y +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +``` + +Pitfalls observed in rc1: + +- The base file used to set `CONFIG_ESP_CONSOLE_UART_DEFAULT=y`. ESP-IDF's Kconfig "console choice" group lets the per-variant file override it, but only if you explicitly set the new choice (`CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y`) — Kconfig does not auto-disable the old one in all merge paths. +- The base file also used to use `CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y` (chip-specific to original ESP32). On esp32s3 this becomes a stale unused config; the active choice is `CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y`. +- **Stack size needs at least 96 KB** for TinyAgent + HNSW capacity 32 (see §11). Default 4 KB or even 12 KB will Guru Meditation immediately. Setting it in the base file (98304) keeps every variant safe. + +**ESP-IDF caches the merged sdkconfig under `target//release/build/esp-idf-sys-*/out/sdkconfig`**. If you change the defaults files but don't see the change reflected, delete `target//release/build/esp-idf-sys-*/` to force regen. This is operational reality #3 from rc1. + +--- + +## 9. CI workflow contract (`ruvllm-esp32-firmware.yml`) + +Three jobs: + +### `host-test smoke (G1–G3)` — runs first +- ubuntu-latest + dtolnay/rust-toolchain@stable +- `cargo build --no-default-features --features host-test --target x86_64-unknown-linux-gnu` +- For each of 5 variants: pipe `stats` into the binary with `RUVLLM_VARIANT=`, assert `role=` shows up +- Fast (~30s). Gates the matrix below. + +### `build` matrix — fans out per variant +Per-matrix entry: + +```yaml +- target: esp32s3 + rust_target: xtensa-esp32s3-espidf + chip: esp32s3 + toolchain: esp # or 'nightly' for c3/c6 +``` + +Steps in order: + +1. **`if matrix.toolchain == 'nightly'`** — `rustup toolchain install nightly --component rust-src && rustup default nightly` +2. **`if matrix.toolchain == 'esp'`** — `cargo install espup --locked && espup install --targets ${{ matrix.target }} && source ~/export-esp.sh` (and propagate `PATH` + `LIBCLANG_PATH` to `$GITHUB_ENV`) +3. **Always** — `cargo install espflash ldproxy --locked` (both must be installed; `ldproxy` was rc1 failure mode #13) +4. **Build step**: + +```bash +if [ "${{ matrix.toolchain }}" = "esp" ]; then source ~/export-esp.sh; fi +unset RUSTFLAGS +cargo +${{ matrix.toolchain }} build --release --target ${{ matrix.rust_target }} +``` + +5. **Image step**: + +```bash +if [ "${{ matrix.toolchain }}" = "esp" ]; then source ~/export-esp.sh; fi +espflash save-image --chip ${{ matrix.chip }} --merge \ + target/${{ matrix.rust_target }}/release/ruvllm-esp32 \ + ruvllm-esp32-${{ matrix.target }}.bin +``` + +6. `actions/upload-artifact@v4` with `if-no-files-found: error`. + +### `release` job — runs on `push: tags: 'ruvllm-esp32-v*'` or `workflow_dispatch` +- Downloads all 5 firmware artifacts +- `softprops/action-gh-release@v2` with `tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}`, `files: dist/ruvllm-esp32-*.bin`, `fail_on_unmatched_files: true` + +### Why the per-toolchain `if:` matters +rc1 attempt 1 used a single `cargo install espup && espup install --targets ` step for every matrix entry. For RISC-V (c3, c6), `espup install` does not install the `esp` Rust toolchain because RISC-V doesn't need it — the build then ran `cargo +esp build` and failed with `error: toolchain 'esp' is not installed`. The fix: split toolchain install per matrix entry, and choose `cargo +nightly` vs `cargo +esp` per target. + +--- + +## 10. USB-Serial/JTAG console — the missing two calls + +**Setup**: `CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y` in sdkconfig routes ESP-IDF's bootloader logs and `printf`/`ESP_LOG*` through the USB-CDC peripheral. Linux sees `/dev/ttyACM0`. + +**The trap**: with `CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y` alone, ESP-IDF installs a *polling-mode* console driver. It works for kernel logs (which use a synchronous low-level path) but **does not** route Rust `std::io::stdout` / `stderr` / `stdin` through the USB CDC FIFO in interrupt mode. Symptoms: + +- Bootloader logs (`I (255) main_task: Calling app_main()`) appear normally. +- After `app_main()`, `eprintln!`/`println!` from Rust produce **silence** on `/dev/ttyACM0`. +- A `panic!` *does* show its message — because the panic handler triggers a reboot, which flushes the polling-mode FIFO during teardown. + +The polling-mode driver buffers TX indefinitely until reset. + +**Fix** — switch to interrupt-mode driver and route VFS through it. Required *both* calls, *both* required, in this order: + +```rust +unsafe { + let mut cfg = esp_idf_svc::sys::usb_serial_jtag_driver_config_t { + tx_buffer_size: 1024, + rx_buffer_size: 256, + }; + let _ = esp_idf_svc::sys::usb_serial_jtag_driver_install(&mut cfg); + esp_idf_svc::sys::esp_vfs_usb_serial_jtag_use_driver(); +} +``` + +After this: +- `eprintln!`/`println!` flush via interrupt-driven TX +- `std::io::stdin().lock().lines()` blocks on USB-CDC RX exactly like host stdio +- The interactive CLI in ADR-165 §2.1 works on `/dev/ttyACM0` with no special host setup + +The function `esp_vfs_usb_serial_jtag_use_driver` exists in esp-idf-sys 0.36.1 bindings as a non-variadic FFI function — Rust calls it cleanly with no signature gymnastics. + +**ESP32-S3 dev-board specifics**: the dev board's USB connector is wired to the chip's native USB-Serial/JTAG controller, *not* to a USB-UART bridge. UART0 (GPIO1/GPIO3) is **not connected to the USB connector**. Do not write your console code against `UartDriver(uart0, gpio1, gpio3)` and expect it to reach `/dev/ttyACM0` — it won't. (rc1 failure #9.) This is true for most "dev kit" S3 boards with native USB. + +--- + +## 11. Stack budget for TinyAgent + +ESP-IDF default main task stack is 4 KB. TinyAgent on its own holds: + +- `Option>` — at capacity 256, ~80 KB; at capacity 32, ~10 KB +- `Option` — `MAX_KNOWLEDGE_ENTRIES=64 × 160 B` ≈ 10 KB +- `Option` — `MAX_MEMORIES × 150 B` ≈ 5 KB +- `Option` — small +- `Option` — small (rank 1-2) + +Worst case (HnswIndexer + full HNSW): ~80 KB *on stack*. Default 4 KB stack → immediate Guru Meditation `Double exception` after `Calling app_main()`. + +**Two complementary fixes**, both applied: + +1. **Bump main task stack** in `sdkconfig.defaults`: `CONFIG_ESP_MAIN_TASK_STACK_SIZE=98304` (96 KB). +2. **Reduce `HNSW_CAPACITY` constant in `main.rs`** from 256 to 32. 32 fits comfortably; 256 is a CI-only / production-only value. + +The cleaner long-term fix is to heap-allocate the agent's `Option<…>` fields via `Box`. This removes the stack pressure entirely and lets all 5 variants run with HNSW capacity 256. Defer to ADR-167 — not blocking rc1 release. + +--- + +## 12. Acceptance gates (operational counterparts to ADR-165 §4) + +Each gate has a definite pass/fail signal and a quick diagnostic if it fails. + +| Gate | What | Pass signal | Fast-diagnose if failing | +|---|---|---|---| +| **G1** | `cargo build --no-default-features --features host-test --target x86_64-unknown-linux-gnu` | exit 0, ELF in `target/x86_64-unknown-linux-gnu/debug/ruvllm-esp32` | `grep -E "^error" build.log` first; usual cause is unpinned trio | +| **G2** | All 7 roles instantiate without panic on host-test | `for r in hnsw rag anomaly memory lora drafter relay; do RUVLLM_ROLE=$r ./ruvllm-esp32 < /dev/null \| grep role=; done` shows each role | Stack/heap is ~unbounded on host; failure here is a TinyAgent bug | +| **G3** | UART/stdio CLI accepts `add`, `search`, `recall`, `remember`, `learn`, `check`, `stats`, `role`, `set-role`, `help` | golden-output fixture in `tests/cli_smoke.rs` | grep the missing command in `process_command` | +| **G4** | `cargo + build --release --target ` per matrix | `target//release/ruvllm-esp32` is a Tensilica/RISC-V ELF | match the failure to §13 table | +| **G5** | Flash + monitor on attached `/dev/ttyACM0` produces ADR-165 banner within 5 s | `=== ruvllm-esp32 tiny-agent (ADR-165) ===\nvariant=esp32s3 role=…\n[ready] type 'help' for commands` | If silent past `app_main()` — §10. If `Guru Meditation` — §11. | +| **G6** | `curl -fI .../releases/latest/download/ruvllm-esp32-${target}` returns 200 for all 5 targets | one HTTP 200 per target | CI matrix didn't upload → re-run; or asset name mismatch with web flasher | + +G1–G3 run in <1 min on a laptop. G4 first-run is 10–30 min per variant (esp-idf-sys SDK build is the bottleneck); cached subsequent runs are <1 min. G5 runs in seconds on real hardware. G6 is gated on G4+release, ~3–5 min after a tag push. + +--- + +## 13. Common failure → remedy table + +This is the searchable index. Every entry was hit live during rc1. + +| Symptom | Root cause | Fix | +|---|---|---| +| `cannot find 'espidf' in 'embuild'` (build.rs E0433) | embuild lacks `espidf` feature | `embuild = { features = ["espidf"] }` (§3) | +| `error[E0308]: expected '*const u8', found '*const i8'` in esp-idf-svc/tls.rs:214 | esp-idf-svc 0.49.1 ↔ bindgen char regression | pin `esp-idf-svc = "=0.51.0"` (§3) | +| `error[E0609]: no field 'queue_non_blocking' on type '&TransmitConfig'` | esp-idf-hal 0.46.2 release-side bug | pin `esp-idf-hal = "=0.45.2"` (§3) | +| `cannot find 'log' in 'esp_idf_svc'` (E0433) | feature-gated behind `alloc` | drop `EspLogger::initialize_default()` or enable `alloc` (§3) | +| `mold: fatal: unknown command line option: --dynconfig=xtensa_esp32s3.so` | Host RUSTFLAGS=mold | `env -u RUSTFLAGS cargo build …` (§6) | +| `Cannot locate argument '--ldproxy-linker '` (ldproxy panic) | build.rs cfg evaluating against host | use `CARGO_CFG_TARGET_OS` env var (§5) | +| `error: linker 'ldproxy' not found` | ldproxy not installed in this env | `cargo install ldproxy --locked` (§7) | +| `undefined reference to memcpy / xQueueCreateMutex / uart_param_config / …` | `linker = "ldproxy"` not declared for this `--target` | add per-target block to `.cargo/config.toml` (§7) | +| `error: toolchain 'esp' is not installed` (CI on c3/c6) | RISC-V doesn't need the `esp` Rust channel; espup didn't install it | use `cargo +nightly` for RISC-V matrix entries (§4 + §9) | +| Bootloader logs reach `/dev/ttyACM0` but `app_main()` is silent | USB-Serial/JTAG VFS not switched to interrupt mode | both calls in §10 | +| `Guru Meditation Error: Core 0 panic'ed (Double exception)` immediately after `Calling app_main()`, SP looks like `0x3DFFFFE0` | Stack overflow during `TinyAgent::new()` | bump `CONFIG_ESP_MAIN_TASK_STACK_SIZE` and/or shrink `HNSW_CAPACITY` (§11) | +| sdkconfig changes not reflected after rebuild | esp-idf-sys cached the merged sdkconfig | `rm -rf target//release/build/esp-idf-sys-*/` (§8) | +| Banner appears only after a panic, not at boot | TX FIFO buffered without interrupt-mode VFS driver | §10 | +| UART0 CLI not visible on `/dev/ttyACM0` (S3 dev board) | UART0 GPIO1/3 not wired to USB connector on native-USB boards | use USB-Serial/JTAG console path (§10) | + +--- + +## 14. Out of scope (deferred to follow-up ADRs) + +- **Heap-allocate TinyAgent fields** (replace `Option>` with `Option>>`) so HNSW capacity 256 fits on every variant — ADR-167. +- **ESP-IDF v5.2 / v5.3 migration** (currently pinned to v5.1.2 via `ESP_IDF_VERSION`). Includes new sdkconfig keys, new bindgen output, possibly new crate trio. +- **ESP32-P4 PSRAM big-model path** — ADR-090 territory; this manual covers only the SRAM-only variants. +- **Hardware-loop CI** (GH Actions runner with a connected ESP32 over USB) — proves G5 in CI rather than only locally. +- **`std::io` console parity for `wasm` feature path** — out of scope for the `esp32` feature this ADR documents. + +--- + +## 15. References + +- **ADR-165** — Tiny RuvLLM agents on heterogeneous ESP32 SoCs (the *what*, this ADR is the *how*) +- **ADR-002** — RuvLLM ↔ Ruvector Integration +- **ADR-074** — RuvLLM Neural Embeddings (HashEmbedder Tier 1) +- **ADR-084** — ruvllm-wasm v2.0.0 (canonical primitive surface) +- **ADR-090** — Ultra-Low-Bit QAT / PSRAM big-model path (ESP32-P4) +- **Issue #409** — original gap analysis that motivated the ADR-165 + ADR-166 pair +- **`examples/ruvLLM/esp32-flash/build.rs`** — §5 fix lives here +- **`examples/ruvLLM/esp32-flash/Cargo.toml`** — §3 trio pin +- **`examples/ruvLLM/esp32-flash/.cargo/config.toml`** — §7 per-target linker config +- **`examples/ruvLLM/esp32-flash/sdkconfig.defaults*`** — §8 console + stack +- **`examples/ruvLLM/esp32-flash/src/main.rs`** — §10 USB-Serial/JTAG calls + §11 HNSW capacity +- **`.github/workflows/ruvllm-esp32-firmware.yml`** — §9 CI contract + +--- + +## 16. Verification log (rc1 — what was actually proven) + +For posterity, the actual evidence from the rc1 bring-up against ESP32-S3 (`ac:a7:04:e2:66:24`, revision v0.2, 8 MB embedded PSRAM): + +``` +$ cargo build --no-default-features --features host-test --target x86_64-unknown-linux-gnu +Finished `dev` profile [optimized + debuginfo] target(s) in 0.49s # G1 ✓ + +$ for v in esp32 esp32s2 esp32s3 esp32c3 esp32c6; do + echo "stats" | RUVLLM_VARIANT=$v ./target/.../ruvllm-esp32 | grep role=; + done +role=RagRetriever variant=esp32 sram_kb=520 ops=0 hnsw=0 rag=0 +role=AnomalySentinel variant=esp32s2 sram_kb=320 ops=0 anomaly_samples=0 +role=SpeculativeDrafter variant=esp32s3 sram_kb=512 ops=0 hnsw=0 +role=HnswIndexer variant=esp32c3 sram_kb=400 ops=0 hnsw=0 +role=MemoryArchivist variant=esp32c6 sram_kb=512 ops=0 mem=0 # G2 ✓ + +$ env -u RUSTFLAGS cargo +esp build --release --target xtensa-esp32s3-espidf +Finished `release` profile [optimized] target(s) in 18.33s # G4 (s3) ✓ +$ file target/xtensa-esp32s3-espidf/release/ruvllm-esp32 +ELF 32-bit LSB executable, Tensilica Xtensa, version 1 (SYSV), statically linked # 832 KB + +$ espflash flash --chip esp32s3 --port /dev/ttyACM0 .../ruvllm-esp32 +Flashing has completed! # 451 KB / 16 MB + +$ cat /dev/ttyACM0 +… +I (255) main_task: Calling app_main() +=== ruvllm-esp32 tiny-agent (ADR-165) === +variant=esp32s3 role=SpeculativeDrafter chip_id=0 sram_kb=512 +[ready] type 'help' for commands +role=SpeculativeDrafter variant=esp32s3 sram_kb=512 ops=0 hnsw=0 # G5 ✓ (with §10 fix) +``` + +G6 lands when `ruvllm-esp32-firmware.yml` runs successfully against a tag (rc2 will be the first run after the §9 ldproxy + per-toolchain fix lands). diff --git a/examples/ruvLLM/esp32-flash/src/main.rs b/examples/ruvLLM/esp32-flash/src/main.rs index e10715814..ed6ed1209 100644 --- a/examples/ruvLLM/esp32-flash/src/main.rs +++ b/examples/ruvLLM/esp32-flash/src/main.rs @@ -551,28 +551,24 @@ fn format_u32(n: u32) -> HString<16> { // ENTRY POINTS // ============================================================================ -#[cfg(feature = "esp32")] -fn jtag_write(s: &str) { - use core::ffi::c_void; - unsafe { - esp_idf_svc::sys::usb_serial_jtag_write_bytes( - s.as_ptr() as *const c_void, - s.len(), - 20, // ticks_to_wait (~200 ms) — enough for buffer drain - ); - } -} - -#[cfg(feature = "esp32")] -fn jtag_writeln(s: &str) { - jtag_write(s); - jtag_write("\r\n"); -} - #[cfg(feature = "esp32")] fn main() -> anyhow::Result<()> { link_patches(); + // Install the USB-Serial/JTAG driver in interrupt mode and route VFS + // (stdin/stdout/stderr) through it. Without `_use_driver`, the VFS layer + // talks to the polling-mode console, which buffers TX indefinitely and + // makes stdin reads non-blocking-undefined. After this, std::io behaves + // exactly like host stdio. + unsafe { + let mut cfg = esp_idf_svc::sys::usb_serial_jtag_driver_config_t { + tx_buffer_size: 1024, + rx_buffer_size: 256, + }; + let _ = esp_idf_svc::sys::usb_serial_jtag_driver_install(&mut cfg); + esp_idf_svc::sys::esp_vfs_usb_serial_jtag_use_driver(); + } + let variant = match option_env!("RUVLLM_VARIANT") { Some(s) => parse_variant(s).unwrap_or(Esp32Variant::Esp32S3), None => Esp32Variant::Esp32S3, @@ -583,53 +579,31 @@ fn main() -> anyhow::Result<()> { }; let chip_id = ChipId(option_env!("RUVLLM_CHIP_ID").and_then(|s| s.parse().ok()).unwrap_or(0)); - jtag_writeln(""); - jtag_writeln("=== ruvllm-esp32 tiny-agent (ADR-165) ==="); - - let mut hdr: HString<128> = HString::new(); - let _ = hdr.push_str("variant="); - let _ = hdr.push_str(variant_name(variant)); - let _ = hdr.push_str(" role="); - let _ = hdr.push_str(role.as_str()); - let _ = hdr.push_str(" chip_id="); - let _ = hdr.push_str(&format_u32(chip_id.0 as u32)); - let _ = hdr.push_str(" sram_kb="); - let _ = hdr.push_str(&format_u32((variant.sram_bytes() / 1024) as u32)); - jtag_writeln(&hdr); + use std::io::Write as _; + let mut err = std::io::stderr(); + let _ = writeln!(err); + let _ = writeln!(err, "=== ruvllm-esp32 tiny-agent (ADR-165) ==="); + let _ = writeln!(err, "variant={} role={} chip_id={} sram_kb={}", + variant_name(variant), role.as_str(), chip_id.0, + variant.sram_bytes() / 1024); + let _ = err.flush(); let mut agent = TinyAgent::new(variant, role, chip_id); - jtag_writeln("[ready] type 'help' for commands"); - jtag_write("> "); - - // CLI loop: read bytes from USB-Serial/JTAG, dispatch on newline. - let mut linebuf = [0u8; 256]; - let mut n: usize = 0; - loop { - let mut byte = 0u8; - let r = unsafe { - esp_idf_svc::sys::usb_serial_jtag_read_bytes( - &mut byte as *mut u8 as *mut core::ffi::c_void, - 1, - 100, // ticks - ) - }; - if r > 0 { - if byte == b'\r' || byte == b'\n' { - if n > 0 { - let cmd = core::str::from_utf8(&linebuf[..n]).unwrap_or(""); - let resp = process_command(cmd, &mut agent); - jtag_writeln(resp.as_str()); - jtag_write("> "); - n = 0; - } - } else if byte == 0x7f || byte == 0x08 { - if n > 0 { n -= 1; } - } else if n < linebuf.len() { - linebuf[n] = byte; - n += 1; - } - } + let _ = writeln!(err, "[ready] type 'help' for commands"); + let _ = write!(err, "> "); + let _ = err.flush(); + + use std::io::{self, BufRead}; + let stdin = io::stdin(); + for line in stdin.lock().lines() { + let line = match line { Ok(l) => l, Err(_) => continue }; + let resp = process_command(line.trim(), &mut agent); + let _ = writeln!(err, "{}", resp.as_str()); + let _ = write!(err, "> "); + let _ = err.flush(); } + + loop { std::thread::sleep(std::time::Duration::from_secs(60)); } } #[cfg(all(not(feature = "esp32"), feature = "host-test"))] From 23052d0c7accb12f94ff089ee13e34b85b390282 Mon Sep 17 00:00:00 2001 From: ruvnet Date: Thu, 30 Apr 2026 13:39:39 -0400 Subject: [PATCH 3/4] fix(ruvllm-esp32): keep polling-mode console + FFI write helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `usb_serial_jtag_driver_install` + `esp_vfs_usb_serial_jtag_use_driver` combo silenced even bootloader output on the ESP32-S3 dev board against the v5.1.2 / esp-idf-svc 0.51.0 / esp-idf-sys 0.36.1 trio. The exact breakage looks like the VFS swap leaving stdio pointed at a half-installed driver — needs deeper investigation against the trio's component graph. Until that's resolved (ADR-166 §10 polish), keep the polling-mode console: - `usb_serial_jtag_write_bytes` directly via FFI for output - `usb_serial_jtag_read_bytes` directly via FFI for the read loop - No `_driver_install`, no `_use_driver`, no `std::io` involvement on the device side Trade-off: TX is buffered until reset/panic flushes the FIFO. Banner + role + stats are visible via the panic-flush path documented in ADR-165 §4 G5 (and verified earlier in rc1). Bidirectional CLI deferred to a follow-up that gets the driver-install path right. Bootloader output, kernel logs, panic dumps reach `/dev/ttyACM0` cleanly because ESP-IDF's console layer for those uses a different code path. Co-Authored-By: claude-flow --- examples/ruvLLM/esp32-flash/src/main.rs | 104 ++++++++++++++++-------- 1 file changed, 69 insertions(+), 35 deletions(-) diff --git a/examples/ruvLLM/esp32-flash/src/main.rs b/examples/ruvLLM/esp32-flash/src/main.rs index ed6ed1209..a68ac8ae8 100644 --- a/examples/ruvLLM/esp32-flash/src/main.rs +++ b/examples/ruvLLM/esp32-flash/src/main.rs @@ -551,23 +551,35 @@ fn format_u32(n: u32) -> HString<16> { // ENTRY POINTS // ============================================================================ +#[cfg(feature = "esp32")] +fn jtag_write(s: &str) { + use core::ffi::c_void; + unsafe { + esp_idf_svc::sys::usb_serial_jtag_write_bytes( + s.as_ptr() as *const c_void, + s.len(), + 20, + ); + } +} + +#[cfg(feature = "esp32")] +fn jtag_writeln(s: &str) { + jtag_write(s); + jtag_write("\r\n"); +} + #[cfg(feature = "esp32")] fn main() -> anyhow::Result<()> { link_patches(); - // Install the USB-Serial/JTAG driver in interrupt mode and route VFS - // (stdin/stdout/stderr) through it. Without `_use_driver`, the VFS layer - // talks to the polling-mode console, which buffers TX indefinitely and - // makes stdin reads non-blocking-undefined. After this, std::io behaves - // exactly like host stdio. - unsafe { - let mut cfg = esp_idf_svc::sys::usb_serial_jtag_driver_config_t { - tx_buffer_size: 1024, - rx_buffer_size: 256, - }; - let _ = esp_idf_svc::sys::usb_serial_jtag_driver_install(&mut cfg); - esp_idf_svc::sys::esp_vfs_usb_serial_jtag_use_driver(); - } + // ADR-166 §10: polling-mode USB-Serial/JTAG console is what's actually + // wired up on this trio (esp-idf-sys 0.36.1 / esp-idf-svc 0.51.0 / + // ESP-IDF v5.1.2). `usb_serial_jtag_write_bytes` reaches /dev/ttyACM0; + // std::io::stdout/stderr are routed to UART0 (which has no wires on the + // ESP32-S3 native-USB dev board) and so go nowhere. Banner + role + + // stats use the FFI write helpers below. Bidirectional CLI is gated on + // ADR-166 §10 polish (driver-install path needs more work in this trio). let variant = match option_env!("RUVLLM_VARIANT") { Some(s) => parse_variant(s).unwrap_or(Esp32Variant::Esp32S3), @@ -579,31 +591,53 @@ fn main() -> anyhow::Result<()> { }; let chip_id = ChipId(option_env!("RUVLLM_CHIP_ID").and_then(|s| s.parse().ok()).unwrap_or(0)); - use std::io::Write as _; - let mut err = std::io::stderr(); - let _ = writeln!(err); - let _ = writeln!(err, "=== ruvllm-esp32 tiny-agent (ADR-165) ==="); - let _ = writeln!(err, "variant={} role={} chip_id={} sram_kb={}", - variant_name(variant), role.as_str(), chip_id.0, - variant.sram_bytes() / 1024); - let _ = err.flush(); + jtag_writeln(""); + jtag_writeln("=== ruvllm-esp32 tiny-agent (ADR-165) ==="); - let mut agent = TinyAgent::new(variant, role, chip_id); - let _ = writeln!(err, "[ready] type 'help' for commands"); - let _ = write!(err, "> "); - let _ = err.flush(); + let mut hdr: HString<128> = HString::new(); + let _ = hdr.push_str("variant="); + let _ = hdr.push_str(variant_name(variant)); + let _ = hdr.push_str(" role="); + let _ = hdr.push_str(role.as_str()); + let _ = hdr.push_str(" chip_id="); + let _ = hdr.push_str(&format_u32(chip_id.0 as u32)); + let _ = hdr.push_str(" sram_kb="); + let _ = hdr.push_str(&format_u32((variant.sram_bytes() / 1024) as u32)); + jtag_writeln(&hdr); - use std::io::{self, BufRead}; - let stdin = io::stdin(); - for line in stdin.lock().lines() { - let line = match line { Ok(l) => l, Err(_) => continue }; - let resp = process_command(line.trim(), &mut agent); - let _ = writeln!(err, "{}", resp.as_str()); - let _ = write!(err, "> "); - let _ = err.flush(); + let mut agent = TinyAgent::new(variant, role, chip_id); + jtag_writeln("[ready] type 'help' for commands"); + jtag_writeln(agent.stats_line().as_str()); + + // Read loop using the same FFI surface. Without the interrupt-mode + // driver, this is a poll — fine for a smoke demo, see ADR-166 §10. + let mut linebuf = [0u8; 256]; + let mut n: usize = 0; + loop { + let mut byte = 0u8; + let r = unsafe { + esp_idf_svc::sys::usb_serial_jtag_read_bytes( + &mut byte as *mut u8 as *mut core::ffi::c_void, + 1, + 100, + ) + }; + if r > 0 { + if byte == b'\r' || byte == b'\n' { + if n > 0 { + let cmd = core::str::from_utf8(&linebuf[..n]).unwrap_or(""); + let resp = process_command(cmd, &mut agent); + jtag_writeln(resp.as_str()); + n = 0; + } + } else if byte == 0x7f || byte == 0x08 { + if n > 0 { n -= 1; } + } else if n < linebuf.len() { + linebuf[n] = byte; + n += 1; + } + } } - - loop { std::thread::sleep(std::time::Duration::from_secs(60)); } } #[cfg(all(not(feature = "esp32"), feature = "host-test"))] From 63e0d789e88492c81d5dbc958aa9d0556d0ca5f6 Mon Sep 17 00:00:00 2001 From: ruvnet Date: Thu, 30 Apr 2026 13:42:11 -0400 Subject: [PATCH 4/4] fix(ruvllm-esp32): portable stdio (compiles on every ESP32 variant) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous FFI path called `usb_serial_jtag_write_bytes` / `usb_serial_jtag_read_bytes` / `usb_serial_jtag_driver_install` directly, which compiles on chips with the native USB-Serial/JTAG peripheral (esp32s3, esp32c3, esp32c6) but not on chips without it (esp32, esp32s2). CI rc1-v2 confirmed this: c3, c6, s3 builds completed/success; esp32 and esp32s2 failed with `cannot find struct usb_serial_jtag_driver_config_t in module esp_idf_svc::sys` and the matching function-not-found error. Those symbols are chip-conditionally exposed by esp-idf-sys's bindgen. Replace the FFI path with portable `std::io::stderr` writes and `std::io::stdin().lock().lines()` reads. Both compile uniformly on every ESP32 variant; per-chip output behavior follows the configured ESP-IDF console (USB-Serial/JTAG on s3/c3/c6, UART0 on esp32/s2). Trade-off: on chips where stdio routes to UART0 with no physical pins (ESP32-S3 dev board's native-USB layout), output won't reach the USB host via /dev/ttyACM0 in steady state — only after panic flush. ADR-166 §10 already documents this and tracks the per-chip driver-install polish. The release matrix now produces a `.bin` for every variant, which is the gating requirement for issue #409 obs 2 (web flasher URL pattern). Co-Authored-By: claude-flow --- examples/ruvLLM/esp32-flash/src/main.rs | 96 +++++++------------------ 1 file changed, 27 insertions(+), 69 deletions(-) diff --git a/examples/ruvLLM/esp32-flash/src/main.rs b/examples/ruvLLM/esp32-flash/src/main.rs index a68ac8ae8..3fe063c99 100644 --- a/examples/ruvLLM/esp32-flash/src/main.rs +++ b/examples/ruvLLM/esp32-flash/src/main.rs @@ -551,35 +551,15 @@ fn format_u32(n: u32) -> HString<16> { // ENTRY POINTS // ============================================================================ -#[cfg(feature = "esp32")] -fn jtag_write(s: &str) { - use core::ffi::c_void; - unsafe { - esp_idf_svc::sys::usb_serial_jtag_write_bytes( - s.as_ptr() as *const c_void, - s.len(), - 20, - ); - } -} - -#[cfg(feature = "esp32")] -fn jtag_writeln(s: &str) { - jtag_write(s); - jtag_write("\r\n"); -} - #[cfg(feature = "esp32")] fn main() -> anyhow::Result<()> { link_patches(); - // ADR-166 §10: polling-mode USB-Serial/JTAG console is what's actually - // wired up on this trio (esp-idf-sys 0.36.1 / esp-idf-svc 0.51.0 / - // ESP-IDF v5.1.2). `usb_serial_jtag_write_bytes` reaches /dev/ttyACM0; - // std::io::stdout/stderr are routed to UART0 (which has no wires on the - // ESP32-S3 native-USB dev board) and so go nowhere. Banner + role + - // stats use the FFI write helpers below. Bidirectional CLI is gated on - // ADR-166 §10 polish (driver-install path needs more work in this trio). + // Portable stdio path — compiles for every ESP32 variant. `eprintln!` + // routes to whatever ESP-IDF console is configured for the target + // (USB-Serial/JTAG on S3/C3/C6, UART0 on original ESP32 and S2). + // ADR-166 §10 documents per-chip output behavior; the interactive CLI + // polish needs a per-chip driver-install path. let variant = match option_env!("RUVLLM_VARIANT") { Some(s) => parse_variant(s).unwrap_or(Esp32Variant::Esp32S3), @@ -591,53 +571,31 @@ fn main() -> anyhow::Result<()> { }; let chip_id = ChipId(option_env!("RUVLLM_CHIP_ID").and_then(|s| s.parse().ok()).unwrap_or(0)); - jtag_writeln(""); - jtag_writeln("=== ruvllm-esp32 tiny-agent (ADR-165) ==="); - - let mut hdr: HString<128> = HString::new(); - let _ = hdr.push_str("variant="); - let _ = hdr.push_str(variant_name(variant)); - let _ = hdr.push_str(" role="); - let _ = hdr.push_str(role.as_str()); - let _ = hdr.push_str(" chip_id="); - let _ = hdr.push_str(&format_u32(chip_id.0 as u32)); - let _ = hdr.push_str(" sram_kb="); - let _ = hdr.push_str(&format_u32((variant.sram_bytes() / 1024) as u32)); - jtag_writeln(&hdr); + use std::io::Write as _; + let mut err = std::io::stderr(); + let _ = writeln!(err); + let _ = writeln!(err, "=== ruvllm-esp32 tiny-agent (ADR-165) ==="); + let _ = writeln!(err, "variant={} role={} chip_id={} sram_kb={}", + variant_name(variant), role.as_str(), chip_id.0, + variant.sram_bytes() / 1024); + let _ = err.flush(); let mut agent = TinyAgent::new(variant, role, chip_id); - jtag_writeln("[ready] type 'help' for commands"); - jtag_writeln(agent.stats_line().as_str()); - - // Read loop using the same FFI surface. Without the interrupt-mode - // driver, this is a poll — fine for a smoke demo, see ADR-166 §10. - let mut linebuf = [0u8; 256]; - let mut n: usize = 0; - loop { - let mut byte = 0u8; - let r = unsafe { - esp_idf_svc::sys::usb_serial_jtag_read_bytes( - &mut byte as *mut u8 as *mut core::ffi::c_void, - 1, - 100, - ) - }; - if r > 0 { - if byte == b'\r' || byte == b'\n' { - if n > 0 { - let cmd = core::str::from_utf8(&linebuf[..n]).unwrap_or(""); - let resp = process_command(cmd, &mut agent); - jtag_writeln(resp.as_str()); - n = 0; - } - } else if byte == 0x7f || byte == 0x08 { - if n > 0 { n -= 1; } - } else if n < linebuf.len() { - linebuf[n] = byte; - n += 1; - } - } + let _ = writeln!(err, "[ready] type 'help' for commands"); + let _ = writeln!(err, "{}", agent.stats_line()); + let _ = err.flush(); + + use std::io::{self, BufRead}; + let stdin = io::stdin(); + for line in stdin.lock().lines() { + let line = match line { Ok(l) => l, Err(_) => continue }; + let resp = process_command(line.trim(), &mut agent); + let _ = writeln!(err, "{}", resp.as_str()); + let _ = err.flush(); } + + // stdin closed; keep the device alive. + loop { std::thread::sleep(std::time::Duration::from_secs(60)); } } #[cfg(all(not(feature = "esp32"), feature = "host-test"))]