feat: happiness scoring pipeline + ESP32 swarm with Cognitum Seed (#285)

* 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>

* ci: raise firmware binary size gate to 1100 KB for HTTP client stack

The swarm bridge (ADR-066) adds esp_http_client for Seed communication,
which pulls in the HTTP/TLS stack (~150 KB). Binary grew from ~978 KB to
~1077 KB. Raise the gate from 950 KB to 1100 KB. Still fits comfortably
in both 4MB (1856 KB OTA slot, 43% free) and 8MB flash variants.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
rUv
2026-03-20 18:46:34 -04:00
committed by GitHub
parent 8a84748a83
commit 2b8a7cc458
22 changed files with 3071 additions and 39 deletions
+26
View File
@@ -71,6 +71,17 @@ def build_nvs_csv(args):
mac_bytes = bytes(int(b, 16) for b in args.filter_mac.split(":"))
# NVS blob: write as hex-encoded string for CSV compatibility
writer.writerow(["filter_mac", "data", "hex2bin", mac_bytes.hex()])
# ADR-066: Swarm bridge configuration
if args.seed_url is not None:
writer.writerow(["seed_url", "data", "string", args.seed_url])
if args.seed_token is not None:
writer.writerow(["seed_token", "data", "string", args.seed_token])
if args.zone is not None:
writer.writerow(["zone_name", "data", "string", args.zone])
if args.swarm_hb is not None:
writer.writerow(["swarm_hb", "data", "u16", str(args.swarm_hb)])
if args.swarm_ingest is not None:
writer.writerow(["swarm_ingest", "data", "u16", str(args.swarm_ingest)])
return buf.getvalue()
@@ -170,6 +181,12 @@ def main():
parser.add_argument("--channel", type=int, help="CSI channel (1-14 for 2.4GHz, 36-177 for 5GHz). "
"Overrides auto-detection from connected AP.")
parser.add_argument("--filter-mac", type=str, help="MAC address to filter CSI frames (AA:BB:CC:DD:EE:FF)")
# ADR-066: Swarm bridge
parser.add_argument("--seed-url", type=str, help="Cognitum Seed base URL (e.g. http://10.1.10.236)")
parser.add_argument("--seed-token", type=str, help="Seed Bearer token (from pairing)")
parser.add_argument("--zone", type=str, help="Zone name for this node (e.g. lobby, hallway)")
parser.add_argument("--swarm-hb", type=int, help="Swarm heartbeat interval in seconds (default 30)")
parser.add_argument("--swarm-ingest", type=int, help="Swarm vector ingest interval in seconds (default 5)")
parser.add_argument("--dry-run", action="store_true", help="Generate NVS binary but don't flash")
args = parser.parse_args()
@@ -182,6 +199,7 @@ def main():
args.fall_thresh is not None, args.vital_win is not None,
args.vital_int is not None, args.subk_count is not None,
args.channel is not None, args.filter_mac is not None,
args.seed_url is not None, args.zone is not None,
])
if not has_value:
parser.error("At least one config value must be specified")
@@ -238,6 +256,14 @@ def main():
print(f" CSI Channel: {args.channel}")
if args.filter_mac is not None:
print(f" Filter MAC: {args.filter_mac}")
if args.seed_url is not None:
print(f" Seed URL: {args.seed_url}")
if args.zone is not None:
print(f" Zone: {args.zone}")
if args.swarm_hb is not None:
print(f" Swarm HB: {args.swarm_hb}s")
if args.swarm_ingest is not None:
print(f" Swarm Ingest: {args.swarm_ingest}s")
csv_content = build_nvs_csv(args)