feat: happiness scoring pipeline with ESP32 swarm + Cognitum Seed coordinator

ADR-065: Hotel guest happiness scoring from WiFi CSI physiological proxies.
ADR-066: ESP32 swarm with Cognitum Seed as coordinator for multi-zone analytics.

Firmware:
- swarm_bridge.c/h: FreeRTOS task on Core 0, HTTP client with Bearer auth,
  registers with Seed, sends heartbeats (30s) and happiness vectors (5s)
- nvs_config: seed_url, seed_token, zone_name, swarm intervals
- provision.py: --seed-url, --seed-token, --zone CLI args
- esp32-hello-world: capability discovery firmware for 4MB ESP32-S3 variant

WASM edge modules:
- exo_happiness_score.rs: 8-dim happiness vector from gait speed, stride
  regularity, movement fluidity, breathing calm, posture, dwell time
  (events 690-694, 11 tests, ESP32-optimized buffers + event decimation)
- ghost_hunter.rs standalone binary: 5.7 KB WASM, feature-gated default pipeline

RuView Live:
- --mode happiness dashboard with bar visualization
- --seed flag for Cognitum Seed bridge (urllib, background POST)
- HappinessScorer + SeedBridge classes (stdlib only, no deps)

Examples:
- seed_query.py: CLI tool (status, search, witness, monitor, report)
- provision_swarm.sh: batch provisioning for multi-node deployment
- happiness_vector_schema.json: 8-dim vector format documentation

Verified live: ESP32 on COM5 (4MB flash) registered with Seed at 10.1.10.236,
vectors flowing, witness chain growing (epoch 455, chain 1108).

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-03-20 18:32:10 -04:00
parent 8a84748a83
commit a2d1739c07
21 changed files with 3067 additions and 35 deletions
+20
View File
@@ -302,6 +302,26 @@ void nvs_config_load(nvs_config_t *cfg)
cfg->filter_mac[3], cfg->filter_mac[4], cfg->filter_mac[5]);
}
/* ADR-066: Swarm bridge */
len = sizeof(cfg->seed_url);
if (nvs_get_str(handle, "seed_url", cfg->seed_url, &len) != ESP_OK) {
cfg->seed_url[0] = '\0'; /* Disabled by default */
}
len = sizeof(cfg->seed_token);
if (nvs_get_str(handle, "seed_token", cfg->seed_token, &len) != ESP_OK) {
cfg->seed_token[0] = '\0';
}
len = sizeof(cfg->zone_name);
if (nvs_get_str(handle, "zone_name", cfg->zone_name, &len) != ESP_OK) {
strncpy(cfg->zone_name, "default", sizeof(cfg->zone_name) - 1);
}
if (nvs_get_u16(handle, "swarm_hb", &cfg->swarm_heartbeat_sec) != ESP_OK) {
cfg->swarm_heartbeat_sec = 30;
}
if (nvs_get_u16(handle, "swarm_ingest", &cfg->swarm_ingest_sec) != ESP_OK) {
cfg->swarm_ingest_sec = 5;
}
/* Validate tdm_slot_index < tdm_node_count */
if (cfg->tdm_slot_index >= cfg->tdm_node_count) {
ESP_LOGW(TAG, "tdm_slot_index=%u >= tdm_node_count=%u, clamping to 0",