mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
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:
@@ -1,16 +1,27 @@
|
||||
# ESP-IDF component manifest for the ruv_temporal Rust staticlib (ADR-095).
|
||||
#
|
||||
# Build flow:
|
||||
# 1. Run `cargo +esp build --release --target xtensa-esp32s3-none-elf` in
|
||||
# this directory. Output: target/xtensa-esp32s3-none-elf/release/libruv_temporal.a
|
||||
# 2. Register the resulting static library and the public header dir
|
||||
# with idf_component_register so it shows up on the firmware's
|
||||
# include path and link line.
|
||||
# - When CONFIG_CSI_TEMPORAL_HEAD_ENABLED is OFF (default): register an
|
||||
# empty stub. main/temporal_task.c compiles the no-op shim path, no
|
||||
# cargo, no Rust toolchain dependency. Default firmware build is
|
||||
# unaffected.
|
||||
# - When CONFIG_CSI_TEMPORAL_HEAD_ENABLED is ON: invoke
|
||||
# `cargo +esp build --release --target xtensa-esp32s3-none-elf`,
|
||||
# register the resulting libruv_temporal.a, and expose include/.
|
||||
#
|
||||
# Phase 4: scaffold only — registered but no kernel work runs yet.
|
||||
# Phase 5: cross-compile validated, binary delta measured.
|
||||
# Phase 6: enabled via CONFIG_CSI_TEMPORAL_HEAD_ENABLED Kconfig flag and
|
||||
# fed from edge_processing.c.
|
||||
# add_custom_command is intentionally placed AFTER idf_component_register
|
||||
# because ESP-IDF runs every component's CMakeLists.txt twice — once in
|
||||
# script mode for dependency discovery (where add_custom_command is
|
||||
# forbidden), and once for the actual build.
|
||||
|
||||
if(NOT CONFIG_CSI_TEMPORAL_HEAD_ENABLED)
|
||||
# Feature disabled — register an empty component so the directory's
|
||||
# mere existence doesn't break the build, but do NOT invoke cargo
|
||||
# or pull include/ onto consumers' include paths (the C ABI header
|
||||
# would advertise capabilities we cannot honour).
|
||||
idf_component_register()
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(RUV_TEMPORAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(RUV_TEMPORAL_TARGET "xtensa-esp32s3-none-elf")
|
||||
@@ -18,9 +29,13 @@ set(RUV_TEMPORAL_PROFILE "release")
|
||||
set(RUV_TEMPORAL_LIB
|
||||
"${RUV_TEMPORAL_DIR}/target/${RUV_TEMPORAL_TARGET}/${RUV_TEMPORAL_PROFILE}/libruv_temporal.a")
|
||||
|
||||
# Run the cargo build as a custom command. ESP-IDF's CMake runs at
|
||||
# configure time; we want the staticlib to exist before idf_component_register
|
||||
# runs so it can be added as INTERFACE_LINK_LIBRARIES.
|
||||
idf_component_register(
|
||||
SRCS "shim.c"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES "esp_common"
|
||||
)
|
||||
|
||||
# Custom command + target run only at build time, not in script mode.
|
||||
add_custom_command(
|
||||
OUTPUT "${RUV_TEMPORAL_LIB}"
|
||||
WORKING_DIRECTORY "${RUV_TEMPORAL_DIR}"
|
||||
@@ -30,12 +45,5 @@ add_custom_command(
|
||||
)
|
||||
add_custom_target(ruv_temporal_rust_build ALL DEPENDS "${RUV_TEMPORAL_LIB}")
|
||||
|
||||
idf_component_register(
|
||||
SRCS "shim.c" # tiny C shim so idf_component_register has a SRCS
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES "esp_common"
|
||||
)
|
||||
|
||||
# Wire the staticlib in.
|
||||
add_dependencies(${COMPONENT_LIB} ruv_temporal_rust_build)
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE "${RUV_TEMPORAL_LIB}")
|
||||
|
||||
@@ -9,10 +9,19 @@ set(SRCS
|
||||
"rv_feature_state.c"
|
||||
"rv_mesh.c"
|
||||
"adaptive_controller.c"
|
||||
# ADR-095 / #513 — on-device temporal head (no-op shims when CONFIG_CSI_TEMPORAL_HEAD_ENABLED off)
|
||||
"temporal_task.c"
|
||||
)
|
||||
|
||||
set(REQUIRES "")
|
||||
|
||||
# ADR-095: link the Rust ruv_temporal staticlib only when the feature is on,
|
||||
# so the default firmware build doesn't depend on the (currently blocked)
|
||||
# esp Rust toolchain.
|
||||
if(CONFIG_CSI_TEMPORAL_HEAD_ENABLED)
|
||||
list(APPEND REQUIRES ruv_temporal)
|
||||
endif()
|
||||
|
||||
# ADR-061: Mock CSI generator for QEMU testing + ADR-081 mock radio binding
|
||||
if(CONFIG_CSI_MOCK_ENABLED)
|
||||
list(APPEND SRCS "mock_csi.c" "rv_radio_ops_mock.c")
|
||||
|
||||
@@ -323,3 +323,56 @@ menu "Mock CSI (QEMU Testing)"
|
||||
depends on CSI_MOCK_ENABLED
|
||||
default n
|
||||
endmenu
|
||||
|
||||
menu "On-device temporal head (ADR-095, #513)"
|
||||
|
||||
config CSI_TEMPORAL_HEAD_ENABLED
|
||||
bool "Enable on-device temporal-head classification"
|
||||
default n
|
||||
help
|
||||
Compiles the ruv_temporal FreeRTOS task that runs a learned
|
||||
transformer-style temporal head over the rv_feature_state
|
||||
stream. Backed by the Rust ruvllm_sparse_attention staticlib
|
||||
in components/ruv_temporal/. Default off — the Rust component
|
||||
requires the esp Rust toolchain (see component README) and
|
||||
adds ~376 KB to the firmware image. Off-board (8 MB) only
|
||||
until the binary delta is measured on real hardware.
|
||||
|
||||
config TEMPORAL_INPUT_DIM
|
||||
int "Input feature dimension"
|
||||
depends on CSI_TEMPORAL_HEAD_ENABLED
|
||||
default 16
|
||||
range 1 256
|
||||
help
|
||||
Per-frame feature dimension fed into the temporal head.
|
||||
16 matches a small projection of rv_feature_state_t; bump
|
||||
after the host-side training crate fixes the model schema.
|
||||
|
||||
config TEMPORAL_WINDOW_LEN
|
||||
int "Rolling window length (frames)"
|
||||
depends on CSI_TEMPORAL_HEAD_ENABLED
|
||||
default 256
|
||||
range 32 1024
|
||||
help
|
||||
Number of feature frames the temporal head reasons over.
|
||||
256 frames at the controller's 5 Hz fast-loop rate is ~50 s.
|
||||
|
||||
config TEMPORAL_N_CLASSES
|
||||
int "Number of output classes"
|
||||
depends on CSI_TEMPORAL_HEAD_ENABLED
|
||||
default 4
|
||||
range 2 16
|
||||
help
|
||||
Number of classification logits the model produces. Must be
|
||||
≤ TEMPORAL_MAX_LOGITS in temporal_task.c (16).
|
||||
|
||||
config TEMPORAL_CLASSIFY_PERIOD_MS
|
||||
int "Classification cadence (ms)"
|
||||
depends on CSI_TEMPORAL_HEAD_ENABLED
|
||||
default 1000
|
||||
range 100 60000
|
||||
help
|
||||
How often the temporal task runs ruv_temporal_classify and
|
||||
emits a 0xC5110007 packet. Default 1 s.
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "csi_collector.h"
|
||||
#include "stream_sender.h"
|
||||
#include "temporal_task.h" /* ADR-095 / #513 */
|
||||
#include "nvs_config.h"
|
||||
#include "edge_processing.h"
|
||||
#include "ota_update.h"
|
||||
@@ -310,6 +311,22 @@ void app_main(void)
|
||||
esp_err_to_name(adapt_ret));
|
||||
}
|
||||
|
||||
/* ADR-095 / #513: spin up the on-device temporal head. Returns
|
||||
* ESP_ERR_NOT_SUPPORTED when CONFIG_CSI_TEMPORAL_HEAD_ENABLED is
|
||||
* off — that is the default and not an error. The fast loop
|
||||
* pushes feature frames; the task runs classify at a slower
|
||||
* cadence and emits 0xC5110007 packets. */
|
||||
#ifdef CONFIG_CSI_TEMPORAL_HEAD_ENABLED
|
||||
esp_err_t tmp_ret = temporal_task_start(
|
||||
(uint32_t)CONFIG_TEMPORAL_INPUT_DIM,
|
||||
(uint32_t)CONFIG_TEMPORAL_WINDOW_LEN,
|
||||
(uint32_t)CONFIG_TEMPORAL_N_CLASSES);
|
||||
if (tmp_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "temporal task init failed: %s",
|
||||
esp_err_to_name(tmp_ret));
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Initialize power management. */
|
||||
power_mgmt_init(g_nvs_config.power_duty);
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* @file temporal_task.c
|
||||
* @brief ADR-095 / #513 — On-device temporal head FreeRTOS task.
|
||||
*
|
||||
* Owns the only `ruv_temporal_ctx_t` in the firmware. Receives feature
|
||||
* frames from the adaptive_controller fast loop via a FreeRTOS queue,
|
||||
* pushes them into the rolling window, and at ~1 Hz runs a
|
||||
* classification forward through the Rust `ruvllm_sparse_attention`
|
||||
* staticlib (when built — see CONFIG_CSI_TEMPORAL_HEAD_ENABLED).
|
||||
*
|
||||
* The whole file compiles down to no-op shims when the feature is off,
|
||||
* so adaptive_controller.c can call `temporal_task_push_frame()`
|
||||
* unconditionally — the function returns ESP_ERR_NOT_SUPPORTED and
|
||||
* costs one nullable check.
|
||||
*/
|
||||
|
||||
#include "temporal_task.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
static const char *TAG = "temporal";
|
||||
|
||||
#ifdef CONFIG_CSI_TEMPORAL_HEAD_ENABLED
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "csi_collector.h" /* node_id */
|
||||
#include "stream_sender.h"
|
||||
#include "ruv_temporal.h" /* C ABI from components/ruv_temporal */
|
||||
|
||||
/* Queue depth — picked so that the adaptive controller's fast loop
|
||||
* (default 5 Hz) can't overrun the temporal task even if classify()
|
||||
* stalls for ~6 s. Drops beyond that are logged. */
|
||||
#define TEMPORAL_QUEUE_DEPTH 32
|
||||
|
||||
/* Stack sized per ADR-095 §3.3. The kernel forward + intermediate
|
||||
* tensors are bounded by `forward_flash` tiling, but rv_feature_state
|
||||
* marshalling, logging, and stream_sender_send all share this stack. */
|
||||
#define TEMPORAL_TASK_STACK 16384
|
||||
|
||||
/* Pinned to Core 1, like edge_dsp. WiFi runs on Core 0 — keep them
|
||||
* apart so the temporal forward doesn't compete with CSI capture. */
|
||||
#define TEMPORAL_TASK_CORE 1
|
||||
|
||||
/* Classification cadence in milliseconds. 1 Hz is the ADR-095 §3 default. */
|
||||
#ifndef CONFIG_TEMPORAL_CLASSIFY_PERIOD_MS
|
||||
#define CONFIG_TEMPORAL_CLASSIFY_PERIOD_MS 1000
|
||||
#endif
|
||||
|
||||
/* Maximum logits buffer — sized to the largest n_classes any of the
|
||||
* ADR-095 §4 use cases needs (anomaly = 2, fall = 3, gesture = 8). */
|
||||
#define TEMPORAL_MAX_LOGITS 16
|
||||
|
||||
/* ---- Module state ----------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
float frame[TEMPORAL_MAX_LOGITS * 8]; /* generous; trimmed via input_dim */
|
||||
uint32_t frame_len;
|
||||
} temporal_msg_t;
|
||||
|
||||
static QueueHandle_t s_queue;
|
||||
static TaskHandle_t s_task;
|
||||
static ruv_temporal_ctx_t *s_ctx;
|
||||
static uint32_t s_input_dim;
|
||||
static uint32_t s_window_len;
|
||||
static uint32_t s_n_classes;
|
||||
static uint32_t s_seq;
|
||||
static uint32_t s_drop_count;
|
||||
static uint64_t s_last_drop_log_us;
|
||||
|
||||
/* Lightweight CRC32 (IEEE 802.3 polynomial 0xEDB88320), table-free.
|
||||
* Used only for the 36-byte classification packet — speed isn't
|
||||
* critical. Existing firmware has its own CRC32 in csi_collector.c
|
||||
* but we don't link against it from here to keep coupling narrow. */
|
||||
static uint32_t crc32_ieee(const uint8_t *data, size_t len)
|
||||
{
|
||||
uint32_t crc = 0xFFFFFFFFu;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (int b = 0; b < 8; b++) {
|
||||
uint32_t mask = -(int32_t)(crc & 1u);
|
||||
crc = (crc >> 1) ^ (0xEDB88320u & mask);
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
static void emit_classification(const float *logits, uint32_t n)
|
||||
{
|
||||
/* Find argmax + margin in one pass. */
|
||||
uint32_t argmax = 0;
|
||||
float top1 = logits[0];
|
||||
float top2 = -1e30f;
|
||||
for (uint32_t i = 1; i < n; i++) {
|
||||
float v = logits[i];
|
||||
if (v > top1) {
|
||||
top2 = top1;
|
||||
top1 = v;
|
||||
argmax = i;
|
||||
} else if (v > top2) {
|
||||
top2 = v;
|
||||
}
|
||||
}
|
||||
|
||||
rv_temporal_pkt_t pkt;
|
||||
memset(&pkt, 0, sizeof(pkt));
|
||||
pkt.magic = RV_TEMPORAL_PKT_MAGIC;
|
||||
pkt.version = 1;
|
||||
pkt.n_classes = (uint16_t)n;
|
||||
pkt.node_id = csi_collector_get_node_id();
|
||||
pkt.ts_us = (uint64_t)esp_timer_get_time();
|
||||
pkt.seq = ++s_seq;
|
||||
pkt.argmax = (uint8_t)argmax;
|
||||
pkt.top_logit = top1;
|
||||
pkt.top1_minus_top2 = top1 - top2;
|
||||
pkt.crc32 = crc32_ieee((const uint8_t *)&pkt, sizeof(pkt) - sizeof(pkt.crc32));
|
||||
|
||||
int sent = stream_sender_send((const uint8_t *)&pkt, sizeof(pkt));
|
||||
if (sent < 0) {
|
||||
ESP_LOGW(TAG, "classification emit failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void temporal_task_loop(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
ESP_LOGI(TAG, "temporal task online (window=%u dim=%u classes=%u core=%d)",
|
||||
(unsigned)s_window_len, (unsigned)s_input_dim,
|
||||
(unsigned)s_n_classes, TEMPORAL_TASK_CORE);
|
||||
|
||||
/* Self-test the kernel link before touching real frames. */
|
||||
if (ruv_temporal_kernel_self_test() != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ruv_temporal_kernel_self_test FAILED — temporal head disabled");
|
||||
s_ctx = NULL;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t next_classify_us = esp_timer_get_time()
|
||||
+ (uint64_t)CONFIG_TEMPORAL_CLASSIFY_PERIOD_MS * 1000ull;
|
||||
float logits[TEMPORAL_MAX_LOGITS];
|
||||
|
||||
for (;;) {
|
||||
temporal_msg_t msg;
|
||||
/* Block up to 100 ms for a frame, then check if it's time to
|
||||
* classify. This double-poll keeps the cadence honest even
|
||||
* during long quiet periods. */
|
||||
if (xQueueReceive(s_queue, &msg, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
if (s_ctx != NULL) {
|
||||
(void)ruv_temporal_push(s_ctx, msg.frame);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t now_us = esp_timer_get_time();
|
||||
if (now_us >= next_classify_us && s_ctx != NULL) {
|
||||
esp_err_t cret = ruv_temporal_classify(s_ctx, logits, s_n_classes);
|
||||
if (cret == ESP_OK) {
|
||||
emit_classification(logits, s_n_classes);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "classify returned 0x%x", (unsigned)cret);
|
||||
}
|
||||
next_classify_us = now_us
|
||||
+ (uint64_t)CONFIG_TEMPORAL_CLASSIFY_PERIOD_MS * 1000ull;
|
||||
}
|
||||
|
||||
/* Coalesce drop-count logs to once per second so a backlog
|
||||
* doesn't flood the serial console. */
|
||||
if (s_drop_count > 0 && now_us - s_last_drop_log_us > 1000000ull) {
|
||||
ESP_LOGW(TAG, "queue full — dropped %u feature frames",
|
||||
(unsigned)s_drop_count);
|
||||
s_drop_count = 0;
|
||||
s_last_drop_log_us = now_us;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t temporal_task_start(uint32_t input_dim,
|
||||
uint32_t window_len,
|
||||
uint32_t n_classes)
|
||||
{
|
||||
if (s_task != NULL) {
|
||||
return ESP_OK; /* idempotent */
|
||||
}
|
||||
if (input_dim == 0 || window_len == 0 || n_classes == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (n_classes > TEMPORAL_MAX_LOGITS) {
|
||||
ESP_LOGE(TAG, "n_classes=%u exceeds TEMPORAL_MAX_LOGITS=%d",
|
||||
(unsigned)n_classes, TEMPORAL_MAX_LOGITS);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* Allocate the kernel context. Phase 4 stub returns ESP_OK without
|
||||
* weights; Phase 5b will accept a real weights blob. */
|
||||
esp_err_t ret = ruv_temporal_init(NULL, 0, input_dim, window_len, n_classes,
|
||||
&s_ctx);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ruv_temporal_init failed: 0x%x", (unsigned)ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
s_input_dim = input_dim;
|
||||
s_window_len = window_len;
|
||||
s_n_classes = n_classes;
|
||||
s_seq = 0;
|
||||
s_drop_count = 0;
|
||||
s_last_drop_log_us = 0;
|
||||
|
||||
s_queue = xQueueCreate(TEMPORAL_QUEUE_DEPTH, sizeof(temporal_msg_t));
|
||||
if (s_queue == NULL) {
|
||||
ESP_LOGE(TAG, "queue create failed");
|
||||
ruv_temporal_destroy(s_ctx);
|
||||
s_ctx = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
BaseType_t ok = xTaskCreatePinnedToCore(
|
||||
temporal_task_loop, "ruv_temporal", TEMPORAL_TASK_STACK,
|
||||
NULL, 4 /* priority, below edge_dsp */,
|
||||
&s_task, TEMPORAL_TASK_CORE);
|
||||
if (ok != pdPASS) {
|
||||
ESP_LOGE(TAG, "task create failed");
|
||||
vQueueDelete(s_queue);
|
||||
s_queue = NULL;
|
||||
ruv_temporal_destroy(s_ctx);
|
||||
s_ctx = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t temporal_task_push_frame(const float *frame, uint32_t frame_len)
|
||||
{
|
||||
if (frame == NULL || frame_len == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (s_queue == NULL) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
temporal_msg_t msg;
|
||||
uint32_t cap = (uint32_t)(sizeof(msg.frame) / sizeof(msg.frame[0]));
|
||||
uint32_t n = (frame_len < cap) ? frame_len : cap;
|
||||
if (n < s_input_dim) {
|
||||
/* Pad short frames with zeros so the rolling window stays
|
||||
* dimension-stable from the kernel's perspective. */
|
||||
memcpy(msg.frame, frame, n * sizeof(float));
|
||||
memset(&msg.frame[n], 0, (s_input_dim - n) * sizeof(float));
|
||||
msg.frame_len = s_input_dim;
|
||||
} else {
|
||||
memcpy(msg.frame, frame, s_input_dim * sizeof(float));
|
||||
msg.frame_len = s_input_dim;
|
||||
}
|
||||
|
||||
/* Non-blocking — temporal head is best-effort. */
|
||||
if (xQueueSend(s_queue, &msg, 0) != pdPASS) {
|
||||
s_drop_count++;
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void temporal_task_stop(void)
|
||||
{
|
||||
if (s_task != NULL) {
|
||||
vTaskDelete(s_task);
|
||||
s_task = NULL;
|
||||
}
|
||||
if (s_queue != NULL) {
|
||||
vQueueDelete(s_queue);
|
||||
s_queue = NULL;
|
||||
}
|
||||
if (s_ctx != NULL) {
|
||||
ruv_temporal_destroy(s_ctx);
|
||||
s_ctx = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
#else /* !CONFIG_CSI_TEMPORAL_HEAD_ENABLED */
|
||||
|
||||
esp_err_t temporal_task_start(uint32_t input_dim,
|
||||
uint32_t window_len,
|
||||
uint32_t n_classes)
|
||||
{
|
||||
(void)input_dim;
|
||||
(void)window_len;
|
||||
(void)n_classes;
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_err_t temporal_task_push_frame(const float *frame, uint32_t frame_len)
|
||||
{
|
||||
(void)frame;
|
||||
(void)frame_len;
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
void temporal_task_stop(void) {}
|
||||
|
||||
#endif /* CONFIG_CSI_TEMPORAL_HEAD_ENABLED */
|
||||
@@ -0,0 +1,98 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* temporal_task.h — On-device temporal head FreeRTOS task (ADR-095, #513).
|
||||
*
|
||||
* Owns the lifecycle of the `ruv_temporal_ctx_t` from
|
||||
* components/ruv_temporal/include/ruv_temporal.h. Exposes:
|
||||
*
|
||||
* 1. `temporal_task_start()` — spawn the task with its own 16 KB stack
|
||||
* pinned to Core 1, allocate a feed queue. Caller (main.c) ignores
|
||||
* ESP_ERR_NOT_SUPPORTED when CONFIG_CSI_TEMPORAL_HEAD_ENABLED is off.
|
||||
* 2. `temporal_task_push_frame()` — non-blocking enqueue from the
|
||||
* adaptive_controller fast loop. Drops on full queue (logs once
|
||||
* per second) — the temporal head is best-effort, the physics-only
|
||||
* path keeps producing vitals regardless.
|
||||
* 3. `temporal_task_stop()` — cleanly tear down (currently used only
|
||||
* for tests; production firmware never calls this).
|
||||
*
|
||||
* Thread safety: per ADR-095 §3.3 the temporal task itself is the
|
||||
* single owner of the underlying `ruv_temporal_ctx_t`. Callers
|
||||
* communicate exclusively via the FreeRTOS queue.
|
||||
*
|
||||
* Output: every ~1 s the task runs `ruv_temporal_classify` and emits a
|
||||
* `0xC5110007 RV_TEMPORAL_CLASSIFICATION` packet via stream_sender.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Magic for the classification packet (ADR-095 §3.5). 0xC5110001..0006
|
||||
* are taken; 0007 is the next free slot. */
|
||||
#define RV_TEMPORAL_PKT_MAGIC 0xC5110007u
|
||||
|
||||
/* On-the-wire packet for one classification result. Little-endian.
|
||||
* Size: 40 bytes. CRC covers everything before it.
|
||||
*
|
||||
* Field layout (bytes):
|
||||
* [00..04) magic 4
|
||||
* [04..06) version 2
|
||||
* [06..08) n_classes 2
|
||||
* [08..09) node_id 1
|
||||
* [09..0C) reserved 3
|
||||
* [0C..14) ts_us 8
|
||||
* [14..18) seq 4
|
||||
* [18..19) argmax 1
|
||||
* [19..1C) reserved2 3
|
||||
* [1C..20) top_logit 4
|
||||
* [20..24) top1_minus_top2 4
|
||||
* [24..28) crc32 4
|
||||
* total: 40
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; /* 0xC5110007 */
|
||||
uint16_t version; /* 1 */
|
||||
uint16_t n_classes; /* matches init() value */
|
||||
uint8_t node_id; /* csi_collector_get_node_id() */
|
||||
uint8_t reserved[3];
|
||||
uint64_t ts_us; /* esp_timer_get_time() at classify */
|
||||
uint32_t seq; /* monotonic, increments per emit */
|
||||
uint8_t argmax; /* highest-logit class */
|
||||
uint8_t reserved2[3];
|
||||
float top_logit; /* logits[argmax] */
|
||||
float top1_minus_top2; /* margin — useful for downstream gating */
|
||||
uint32_t crc32;
|
||||
} rv_temporal_pkt_t;
|
||||
|
||||
/* Build-time guard so the wire format never silently changes. */
|
||||
_Static_assert(sizeof(rv_temporal_pkt_t) == 40,
|
||||
"rv_temporal_pkt_t must be 40 bytes (ADR-095 §3.5)");
|
||||
|
||||
/* Start the temporal task. Returns ESP_ERR_NOT_SUPPORTED when the
|
||||
* feature is compiled out — caller should treat that as a non-error
|
||||
* and continue. Returns ESP_OK on success.
|
||||
*
|
||||
* input_dim : feature dimension per frame (e.g. 60 for rv_feature_state_t)
|
||||
* window_len : rolling window in frames (e.g. 256)
|
||||
* n_classes : number of output logits the model produces (e.g. 4)
|
||||
*/
|
||||
esp_err_t temporal_task_start(uint32_t input_dim,
|
||||
uint32_t window_len,
|
||||
uint32_t n_classes);
|
||||
|
||||
/* Non-blocking push from the adaptive_controller fast loop. Returns
|
||||
* ESP_OK on enqueue, ESP_ERR_NOT_FOUND if the task isn't running,
|
||||
* ESP_ERR_TIMEOUT if the queue was full. Never blocks the caller. */
|
||||
esp_err_t temporal_task_push_frame(const float *frame, uint32_t frame_len);
|
||||
|
||||
/* Optional teardown — currently unit-test only. */
|
||||
void temporal_task_stop(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user