Files
ruvnet--RuView/firmware/esp32-csi-node/components/ruv_temporal/README.md
T
ruv 3a5fe5e0de feat(firmware): mirror weight-blob parser into ruv_temporal (#513)
Closes the format contract on the firmware side. Source-only — Phase 5
toolchain blocker still prevents actually compiling, but when it
unblocks this is one less thing to write under time pressure.

- src/weights.rs — no_std mirror of v2/.../weights.rs. Same magic
  ('RVNE'), same version 1, same CRC32-IEEE polynomial (matches the C
  side in temporal_task.c). Bit-for-bit lockstep with the host: a
  blob produced by host WeightBlob::serialize() parses here as a
  WeightBlobView byte-for-byte.

  Borrowed-slice parse design: the firmware loader receives weights
  via mmap'd EMBED_FILES or NVS read into a heap buffer. The parser
  takes &[u8] with no copy — view fields point into the caller's
  buffer. Caller is responsible for keeping the buffer alive for the
  view's lifetime.

  Loader errors map to esp_err_t-style codes via
  weight_load_err_to_esp() so the C ABI can surface specific failure
  modes (ESP_ERR_INVALID_ARG for magic/version/size, ESP_ERR_INVALID_CRC
  for corruption, ESP_ERR_INVALID_SIZE for shape validation failures).

- src/lib.rs — ruv_temporal_init now optionally validates a non-NULL
  weights blob. NULL pointer is still allowed during the Phase 4/5
  bring-up window (kernel forward isn't actually consuming weights
  yet), but when caller passes a real blob we parse + sanity-check
  declared dims against runtime arguments. Catches deploy bugs at
  init() rather than at first classify() — the firmware Tmr Svc work
  in v0.6.4 taught us that classify-time crashes are the worst kind.

- README.md — Phase 6 marked done (verified by 8MB firmware build with
  feature off in commit 7994af822). Added module map table covering
  lib.rs / window.rs / weights.rs / ruv_temporal.h / shim.c.

What's deliberately NOT in this commit:
  - Cross-compile validation. Same toolchain blocker as before.
  - Kernel-side wiring of weights into the forward pass. That's
    Phase 6+ of the firmware roadmap — once the kernel is wired,
    weights become a required arg, not an optional one.
  - Tests on the firmware side. They'd need build-std working to run;
    16/16 host tests cover the format end-to-end via the lockstep
    polynomial.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-08 11:53:19 -04:00

87 lines
3.9 KiB
Markdown

# `ruv_temporal` — ESP32-S3 on-device temporal head
ESP-IDF component implementing ADR-095 (#513). The Rust staticlib at
`src/lib.rs` wraps `ruvllm_sparse_attention` (vendored at
`vendor/ruvector/crates/ruvllm_sparse_attention`) and exposes a narrow
C ABI declared in `include/ruv_temporal.h`.
## Status
| Phase | Scope | State |
|-------|-------|-------|
| 4 — Scaffold | Cargo.toml, src/{lib.rs,window.rs,weights.rs}, include/ruv_temporal.h, CMakeLists.txt, .cargo/config.toml | **Done.** |
| 5 — Cross-compile | `cargo +esp build --release --target xtensa-esp32s3-none-elf` produces `libruv_temporal.a`. | **Blocked** — see below. |
| 6 — Wire from edge_processing.c | FreeRTOS task on Core 1, queue from adaptive_controller fast loop, push() in fast tick, classify() at 1 Hz, emit `0xC5110007` packet. | **Done** in `main/temporal_task.c` (no-op shim path verified by 8MB firmware build with feature off). |
| 7 — COM8 validation | Flash 8MB build with `CONFIG_CSI_TEMPORAL_HEAD_ENABLED=y`, soak ≥5 min, check no Tmr Svc / task_wdt overflow. | Pending board reattach. |
## Module map
| File | Purpose |
|------|---------|
| `src/lib.rs` | C ABI: `ruv_temporal_init / push / classify / destroy / kernel_self_test` |
| `src/window.rs` | `FrameRing` rolling buffer used by `ruv_temporal_push` |
| `src/weights.rs` | Loader-side mirror of host `wifi_densepose_temporal::weights`. Parses the `.rvne` blob format (magic `RVNE`, version 1, FP32/FP16, CRC32-IEEE). Bit-exact with the host crate; a blob produced by the host's `WeightBlob::serialize()` parses here byte-for-byte. |
| `include/ruv_temporal.h` | Public C header consumed by `main/temporal_task.c` |
| `shim.c` | Empty C shim for `idf_component_register` |
## Phase 5 blocker — esp toolchain rust-src bug
The system esp toolchain at `C:\Users\ruv\.rustup\toolchains\esp` has
no precompiled `core` for `xtensa-esp32s3-none-elf`. It requires
`-Z build-std=core,alloc`, but the bundled rust-src snapshot
(`esp` channel, nightly 2025-09-16) hits two known bugs when build-std
compiles `core`:
1. `library/portable-simd/crates/core_simd/src/simd/ptr/mut_ptr.rs`
`Copy` trait and `size_of` not in scope, ~16,000 errors.
2. `library/core` itself — "cannot resolve a prelude import",
"attributes starting with `rustc` are reserved", `concat!` macro
not found.
These are upstream Rust nightly snapshot regressions, not anything
this component is doing wrong. The fix is to refresh the esp toolchain
to a newer nightly:
```powershell
C:/Users/ruv/.cargo/bin/espup.exe install
# (re-source export-esp.ps1 / export-esp.sh after install)
```
`espup install` pulls the latest pinned esp Rust + LLVM. It is a
~1.5 GB download and ~5-10 min install. That step lands in the next
loop iteration of #513 implementation work.
## Build (once Phase 5 unblocks)
From this directory:
```bash
cargo +esp build --release --target xtensa-esp32s3-none-elf
```
Output:
`target/xtensa-esp32s3-none-elf/release/libruv_temporal.a`.
ESP-IDF's `idf.py build` will pick this up via `CMakeLists.txt`
`add_custom_command` runs the cargo build before
`idf_component_register` consumes the static library.
## C ABI summary
```c
esp_err_t ruv_temporal_init(const uint8_t *weights, size_t wlen,
uint32_t input_dim, uint32_t window_len,
uint32_t n_classes,
ruv_temporal_ctx_t **out_ctx);
esp_err_t ruv_temporal_push(ruv_temporal_ctx_t *ctx, const float *frame);
esp_err_t ruv_temporal_classify(ruv_temporal_ctx_t *ctx,
float *logits, uint32_t n_classes);
void ruv_temporal_destroy(ruv_temporal_ctx_t *ctx);
esp_err_t ruv_temporal_kernel_self_test(void);
```
Threading: caller is responsible. Per ADR-095 §3.3, the firmware will
spawn a single dedicated FreeRTOS task that owns the context and
serialises all calls — push() and classify() are not internally
synchronised.