mirror of
https://github.com/ruvnet/RuView
synced 2026-07-11 15:33:19 +00:00
ADR-081: Layer 3 mesh plane + Rust mirror trait — all 5 layers landed
Fully implements the remaining deferred pieces of the adaptive CSI mesh
firmware kernel. All 5 layers (Radio Abstraction, Adaptive Controller,
Mesh Sensing Plane, On-device Feature Extraction, Rust handoff) are
now implemented and host-tested end-to-end.
Layer 3 — Mesh Sensing Plane (firmware/esp32-csi-node/main/rv_mesh.{h,c}):
* 4 node roles: Unassigned / Anchor / Observer / FusionRelay / Coordinator
* 7 message types: TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN,
CALIBRATION_START, FEATURE_DELTA, HEALTH, ANOMALY_ALERT
* 3 auth classes: None / HMAC-SHA256-session / Ed25519-batch
* Payload types: rv_node_status_t (28 B), rv_anomaly_alert_t (28 B),
rv_time_sync_t (16 B), rv_role_assign_t (16 B),
rv_channel_plan_t (24 B), rv_calibration_start_t (20 B)
* 16-byte envelope + payload + IEEE CRC32 trailer
* Pure rv_mesh_encode()/rv_mesh_decode() plus typed convenience encoders
* rv_mesh_send_health() + rv_mesh_send_anomaly() helpers
Controller wiring (adaptive_controller.c):
* Slow loop (30 s default) now emits HEALTH
* apply_decision() emits ANOMALY_ALERT on transitions to ALERT /
DEGRADED
* Role + mesh epoch tracked in module state; epoch bumps on role
change
Layer 5 — Rust mirror (crates/wifi-densepose-hardware/src/radio_ops.rs):
* RadioOps trait mirrors rv_radio_ops_t vtable
* MockRadio backend for offline tests
* MeshHeader / NodeStatus / AnomalyAlert types mirror rv_mesh.h
* Byte-identical IEEE CRC32 (poly 0xEDB88320) verified against
firmware test vectors (0xCBF43926 for "123456789")
* decode_mesh / decode_node_status / decode_anomaly_alert / encode_health
* 8 unit tests, including mesh_constants_match_firmware which asserts
MESH_MAGIC/VERSION/HEADER_SIZE/MAX_PAYLOAD match rv_mesh.h
byte-for-byte
* Exported from lib.rs
* signal/ruvector/train/mat crates untouched — satisfies ADR-081
portability acceptance test
Tests (all passing):
test_adaptive_controller: 18/18 (C, decide() 3.2 ns/call)
test_rv_feature_state: 15/15 (C, CRC32 87 MB/s)
test_rv_mesh: 27/27 (C, roundtrip 1.0 µs)
radio_ops::tests (Rust): 8/8
--- total: 68/68 assertions green ---
Docs:
* ADR-081 status flipped to Accepted
* Implementation-status matrix updated; L3 + Rust mirror both
marked Implemented
* Benchmarks table extended with rv_mesh encode+decode roundtrip
* Verification section updated with cargo test invocation
* CHANGELOG: two new entries for L3 mesh plane + Rust mirror
Remaining follow-ups (Phase 3.5 polish, not blocking):
* Mesh RX path (UDP listener + dispatch) on the firmware
* Ed25519 signing for CHANNEL_PLAN / CALIBRATION_START
* Hardware validation on COM7
This commit is contained in:
@@ -29,7 +29,7 @@ FEATURE_STATE_SRCS := $(MAIN_DIR)/rv_feature_state.c
|
||||
# before including the .c. The decide() body itself has no ESP-IDF deps.
|
||||
# Simpler: just recompile decide() here via a small shim.
|
||||
|
||||
TESTS := test_adaptive_controller test_rv_feature_state
|
||||
TESTS := test_adaptive_controller test_rv_feature_state test_rv_mesh
|
||||
|
||||
all: $(TESTS)
|
||||
|
||||
@@ -39,10 +39,19 @@ test_adaptive_controller: test_adaptive_controller.c $(MAIN_DIR)/adaptive_contro
|
||||
test_rv_feature_state: test_rv_feature_state.c $(FEATURE_STATE_SRCS) $(MAIN_DIR)/rv_feature_state.h $(MAIN_DIR)/rv_radio_ops.h
|
||||
$(CC) $(CFLAGS) test_rv_feature_state.c $(FEATURE_STATE_SRCS) -o $@ $(LDLIBS)
|
||||
|
||||
# Mesh plane encoder/decoder: compile rv_mesh.c with RV_MESH_HOST_TEST
|
||||
# so the firmware-only send helpers (stream_sender, esp_log) are hidden.
|
||||
test_rv_mesh: test_rv_mesh.c $(MAIN_DIR)/rv_mesh.c $(MAIN_DIR)/rv_mesh.h $(FEATURE_STATE_SRCS) $(MAIN_DIR)/rv_radio_ops.h
|
||||
$(CC) $(CFLAGS) -DRV_MESH_HOST_TEST=1 \
|
||||
test_rv_mesh.c $(MAIN_DIR)/rv_mesh.c $(FEATURE_STATE_SRCS) \
|
||||
-o $@ $(LDLIBS)
|
||||
|
||||
check: all
|
||||
./test_adaptive_controller
|
||||
@echo ""
|
||||
./test_rv_feature_state
|
||||
@echo ""
|
||||
./test_rv_mesh
|
||||
|
||||
clean:
|
||||
rm -f $(TESTS) *.o
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
|
||||
typedef int esp_err_t;
|
||||
|
||||
#define ESP_OK 0
|
||||
#define ESP_FAIL -1
|
||||
#define ESP_ERR_NO_MEM 0x101
|
||||
#define ESP_ERR_INVALID_ARG 0x102
|
||||
#define ESP_OK 0
|
||||
#define ESP_FAIL -1
|
||||
#define ESP_ERR_NO_MEM 0x101
|
||||
#define ESP_ERR_INVALID_ARG 0x102
|
||||
#define ESP_ERR_INVALID_SIZE 0x104
|
||||
#define ESP_ERR_INVALID_VERSION 0x10A
|
||||
#define ESP_ERR_INVALID_CRC 0x10B
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Host unit test for ADR-081 Layer 3 mesh plane encode/decode.
|
||||
*
|
||||
* rv_mesh_encode() and rv_mesh_decode() are the pure halves of the
|
||||
* mesh plane — no ESP-IDF, no sockets — so we exercise them with the
|
||||
* RV_MESH_HOST_TEST flag that disables the send helpers.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "rv_mesh.h"
|
||||
#include "rv_feature_state.h"
|
||||
#include "rv_radio_ops.h" /* for RV_PROFILE_* enum values */
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf(" FAIL: %s (line %d)\n", msg, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
static void test_header_size(void) {
|
||||
printf("test: rv_mesh_header_t is 16 bytes\n");
|
||||
CHECK(sizeof(rv_mesh_header_t) == 16, "sizeof(header) == 16");
|
||||
}
|
||||
|
||||
static void test_encode_health_roundtrip(void) {
|
||||
printf("test: HEALTH roundtrip\n");
|
||||
rv_node_status_t st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.node_id[0] = 7;
|
||||
st.local_time_us = 1234567890ULL;
|
||||
st.role = RV_ROLE_OBSERVER;
|
||||
st.current_channel = 6;
|
||||
st.current_bw = 20;
|
||||
st.noise_floor_dbm = -93;
|
||||
st.pkt_yield = 42;
|
||||
st.sync_error_us = 12;
|
||||
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_health(RV_ROLE_OBSERVER, /*epoch*/ 100,
|
||||
&st, buf, sizeof(buf));
|
||||
CHECK(n > 0, "encode returns non-zero");
|
||||
CHECK(n == sizeof(rv_mesh_header_t) + sizeof(st) + 4,
|
||||
"encoded size = hdr+payload+crc");
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *payload = NULL;
|
||||
uint16_t payload_len = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, n, &hdr, &payload, &payload_len);
|
||||
CHECK(rc == ESP_OK, "decode OK");
|
||||
CHECK(hdr.type == RV_MSG_HEALTH, "type == HEALTH");
|
||||
CHECK(hdr.epoch == 100, "epoch survives");
|
||||
CHECK(hdr.payload_len == sizeof(st), "payload_len matches");
|
||||
CHECK(payload != NULL, "payload pointer set");
|
||||
CHECK(memcmp(payload, &st, sizeof(st)) == 0, "payload bytes match");
|
||||
}
|
||||
|
||||
static void test_encode_anomaly_roundtrip(void) {
|
||||
printf("test: ANOMALY_ALERT roundtrip\n");
|
||||
rv_anomaly_alert_t a;
|
||||
memset(&a, 0, sizeof(a));
|
||||
a.node_id[0] = 3;
|
||||
a.ts_us = 999999ULL;
|
||||
a.reason = RV_ANOMALY_FALL;
|
||||
a.severity = 200;
|
||||
a.anomaly_score = 0.85f;
|
||||
a.motion_score = 0.9f;
|
||||
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_anomaly_alert(RV_ROLE_OBSERVER, 7, &a,
|
||||
buf, sizeof(buf));
|
||||
CHECK(n > 0, "encoded");
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *payload = NULL;
|
||||
uint16_t payload_len = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, n, &hdr, &payload, &payload_len);
|
||||
CHECK(rc == ESP_OK, "decoded");
|
||||
CHECK(hdr.type == RV_MSG_ANOMALY_ALERT, "type ok");
|
||||
rv_anomaly_alert_t got;
|
||||
memcpy(&got, payload, sizeof(got));
|
||||
CHECK(got.reason == RV_ANOMALY_FALL, "reason survived");
|
||||
CHECK(got.severity == 200, "severity survived");
|
||||
}
|
||||
|
||||
static void test_encode_feature_delta_wraps_feature_state(void) {
|
||||
printf("test: FEATURE_DELTA wraps rv_feature_state_t\n");
|
||||
rv_feature_state_t fs;
|
||||
memset(&fs, 0, sizeof(fs));
|
||||
fs.motion_score = 0.5f;
|
||||
rv_feature_state_finalize(&fs, /*node*/ 9, /*seq*/ 17,
|
||||
/*ts*/ 111ULL, RV_PROFILE_FAST_MOTION);
|
||||
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_feature_delta(RV_ROLE_OBSERVER, 2, &fs,
|
||||
buf, sizeof(buf));
|
||||
CHECK(n == sizeof(rv_mesh_header_t) + sizeof(fs) + 4, "size check");
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *payload = NULL;
|
||||
uint16_t len = 0;
|
||||
CHECK(rv_mesh_decode(buf, n, &hdr, &payload, &len) == ESP_OK,
|
||||
"decode OK");
|
||||
rv_feature_state_t got;
|
||||
memcpy(&got, payload, sizeof(got));
|
||||
CHECK(got.magic == RV_FEATURE_STATE_MAGIC, "inner magic preserved");
|
||||
CHECK(got.node_id == 9, "inner node_id preserved");
|
||||
CHECK(got.seq == 17, "inner seq preserved");
|
||||
/* Inner CRC is end-to-end even though the mesh frame has its own
|
||||
* CRC too — two checks for two failure modes. */
|
||||
uint32_t inner_crc = rv_feature_state_crc32(
|
||||
(const uint8_t *)&got, sizeof(got) - sizeof(uint32_t));
|
||||
CHECK(inner_crc == got.crc32, "inner feature_state CRC still valid");
|
||||
}
|
||||
|
||||
static void test_decode_rejects_bad_magic(void) {
|
||||
printf("test: decode rejects bad magic\n");
|
||||
uint8_t buf[sizeof(rv_mesh_header_t) + 4];
|
||||
memset(buf, 0xFF, sizeof(buf));
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *p = NULL;
|
||||
uint16_t plen = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, sizeof(buf), &hdr, &p, &plen);
|
||||
CHECK(rc != ESP_OK, "bad magic rejected");
|
||||
}
|
||||
|
||||
static void test_decode_rejects_truncated(void) {
|
||||
printf("test: decode rejects truncated frame\n");
|
||||
uint8_t buf[sizeof(rv_mesh_header_t) - 1];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *p = NULL;
|
||||
uint16_t plen = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, sizeof(buf), &hdr, &p, &plen);
|
||||
CHECK(rc != ESP_OK, "truncated rejected");
|
||||
}
|
||||
|
||||
static void test_decode_rejects_bad_crc(void) {
|
||||
printf("test: decode rejects CRC mismatch\n");
|
||||
rv_node_status_t st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.role = RV_ROLE_OBSERVER;
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_health(RV_ROLE_OBSERVER, 1, &st,
|
||||
buf, sizeof(buf));
|
||||
CHECK(n > 0, "encoded");
|
||||
|
||||
/* Flip a byte in the payload — CRC must now mismatch. */
|
||||
buf[sizeof(rv_mesh_header_t) + 4] ^= 0x10;
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *p = NULL;
|
||||
uint16_t plen = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, n, &hdr, &p, &plen);
|
||||
CHECK(rc != ESP_OK, "CRC mismatch rejected");
|
||||
}
|
||||
|
||||
static void test_encode_rejects_oversize_payload(void) {
|
||||
printf("test: encode rejects oversize payload\n");
|
||||
uint8_t junk[RV_MESH_MAX_PAYLOAD + 1] = {0};
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES + 8];
|
||||
size_t n = rv_mesh_encode(RV_MSG_HEALTH, RV_ROLE_OBSERVER, RV_AUTH_NONE,
|
||||
0, junk, sizeof(junk), buf, sizeof(buf));
|
||||
CHECK(n == 0, "oversize payload → 0");
|
||||
}
|
||||
|
||||
static void test_encode_rejects_small_buf(void) {
|
||||
printf("test: encode rejects too-small buffer\n");
|
||||
rv_node_status_t st = {0};
|
||||
uint8_t buf[16]; /* header fits but not payload */
|
||||
size_t n = rv_mesh_encode_health(RV_ROLE_OBSERVER, 0, &st,
|
||||
buf, sizeof(buf));
|
||||
CHECK(n == 0, "small buf → 0");
|
||||
}
|
||||
|
||||
static void benchmark_encode(void) {
|
||||
printf("bench: encode+decode HEALTH roundtrip\n");
|
||||
rv_node_status_t st;
|
||||
memset(&st, 0x33, sizeof(st));
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
|
||||
const int N = 2000000;
|
||||
struct timespec a, b;
|
||||
clock_gettime(CLOCK_MONOTONIC, &a);
|
||||
for (int i = 0; i < N; i++) {
|
||||
st.pkt_yield = (uint16_t)i;
|
||||
size_t n = rv_mesh_encode_health(RV_ROLE_OBSERVER, (uint32_t)i,
|
||||
&st, buf, sizeof(buf));
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *p = NULL;
|
||||
uint16_t plen = 0;
|
||||
(void)rv_mesh_decode(buf, n, &hdr, &p, &plen);
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &b);
|
||||
double ns = ((b.tv_sec - a.tv_sec) * 1e9 +
|
||||
(b.tv_nsec - a.tv_nsec)) / (double)N;
|
||||
printf(" %d roundtrips, %.1f ns/call\n", N, ns);
|
||||
CHECK(ns < 20000.0, "encode+decode must be under 20us/roundtrip");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== rv_mesh encode/decode host tests ===\n\n");
|
||||
test_header_size();
|
||||
test_encode_health_roundtrip();
|
||||
test_encode_anomaly_roundtrip();
|
||||
test_encode_feature_delta_wraps_feature_state();
|
||||
test_decode_rejects_bad_magic();
|
||||
test_decode_rejects_truncated();
|
||||
test_decode_rejects_bad_crc();
|
||||
test_encode_rejects_oversize_payload();
|
||||
test_encode_rejects_small_buf();
|
||||
benchmark_encode();
|
||||
printf("\n=== result: %d pass, %d fail ===\n", g_pass, g_fail);
|
||||
return g_fail > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user