feat(firmware): wire temporal_task.c + Kconfig + ruv_temporal component (Phase 6, #513)

Phase 6 of #513: C-side wiring for the on-device temporal head. Builds
cleanly with feature OFF (default); 8MB binary delta is +96 bytes vs
v0.6.4-esp32 — that's the no-op shim path. Feature ON depends on the
Rust component (Phase 5, currently blocked by upstream esp-rs nightly).

Files:

- main/temporal_task.{c,h} — owns the FreeRTOS task lifecycle. Per
  ADR-095 §3.3 the task has its own 16 KB stack pinned to Core 1 and
  is fed via a 32-deep FreeRTOS queue. With feature OFF the .c file
  collapses to three ESP_ERR_NOT_SUPPORTED stubs so callers don't
  need #ifdefs at every call site.
- main/temporal_task.h — defines rv_temporal_pkt_t (40 bytes,
  magic 0xC5110007 — next free in the existing 0xC5110001..0006
  family) and the task lifecycle API. Build-time _Static_assert
  pins the wire format.
- main/Kconfig.projbuild — new menu "On-device temporal head
  (ADR-095, #513)" with CONFIG_CSI_TEMPORAL_HEAD_ENABLED (default n)
  plus four runtime-tuneable knobs: TEMPORAL_INPUT_DIM (16),
  TEMPORAL_WINDOW_LEN (256), TEMPORAL_N_CLASSES (4), and
  TEMPORAL_CLASSIFY_PERIOD_MS (1000).
- main/CMakeLists.txt — adds temporal_task.c to SRCS unconditionally
  (the .c file feature-gates internally), and adds ruv_temporal to
  REQUIRES only when the feature is enabled so default builds don't
  pull in the Rust component.
- main/adaptive_controller.c — fast_loop_cb now extracts the 9
  feature floats from the pkt it just built and pushes them into
  temporal_task_push_frame after the existing stream_sender_send.
  Non-blocking; queue-full drops are coalesced and logged 1/sec.
- main/main.c — temporal_task_start() called right after
  adaptive_controller_init(). Wrapped in #ifdef so feature-off
  builds don't reference the (no-op-anyway) function.
- components/ruv_temporal/CMakeLists.txt — restructured. Top-level
  Kconfig guard registers an empty component when the feature is
  off (avoids running cargo without a working toolchain).
  add_custom_command moved AFTER idf_component_register so it
  doesn't fire in script mode (required by ESP-IDF v5.4).

Validation:
- Firmware builds clean with default config (feature OFF) on
  ESP-IDF v5.4 / esp32s3 target. Binary 1062 KiB / 2 MiB partition,
  48 % free.
- Static assertion catches wire-format drift (rv_temporal_pkt_t size).
- Host-side `cargo test -p wifi-densepose-temporal` still 5/5 from
  the earlier commit (no regression, this commit only touches
  firmware/).

Phase 7 (flash to COM8 + soak) deferred this iteration — board is
currently not enumerating on COM8; will pick up next iteration when
the ESP32 is reattached.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-05-08 11:28:11 -04:00
parent 22d47a71e3
commit 7994af8221
7 changed files with 521 additions and 19 deletions
@@ -19,6 +19,7 @@
#include "edge_processing.h"
#include "stream_sender.h"
#include "csi_collector.h"
#include "temporal_task.h" /* ADR-095 / #513: on-device temporal head */
#include <string.h>
#include "freertos/FreeRTOS.h"
@@ -314,6 +315,18 @@ static void emit_feature_state(void)
if (sent < 0) {
ESP_LOGW(TAG, "feature_state emit failed");
}
/* ADR-095 / #513: feed the same 9 feature floats into the on-device
* temporal head if it is enabled. Non-blocking — drops are logged
* by temporal_task itself, never by us. With CONFIG_CSI_TEMPORAL_HEAD_ENABLED
* off, this resolves to a single ESP_ERR_NOT_SUPPORTED return. */
const float feat[9] = {
pkt.motion_score, pkt.presence_score,
pkt.respiration_bpm, pkt.respiration_conf,
pkt.heartbeat_bpm, pkt.heartbeat_conf,
pkt.anomaly_score, pkt.env_shift_score, pkt.node_coherence,
};
(void)temporal_task_push_frame(feat, 9);
}
static void slow_loop_cb(TimerHandle_t t)