Skip to content

fix(sensing-server): parse ADR-081 feature_state (0xC511_0006), the node's default payload#1296

Open
lakinduakash wants to merge 3 commits into
ruvnet:mainfrom
lakinduakash:fix/sensing-server-feature-state-0x06
Open

fix(sensing-server): parse ADR-081 feature_state (0xC511_0006), the node's default payload#1296
lakinduakash wants to merge 3 commits into
ruvnet:mainfrom
lakinduakash:fix/sensing-server-feature-state-0x06

Conversation

@lakinduakash

Copy link
Copy Markdown

Problem

udp_receiver_task in wifi-densepose-sensing-server decodes only:

  • 0xC511_0001 raw CSI
  • 0xC511_0002 edge vitals
  • 0xC511_0004 fused vitals
  • 0xC511_0007 WASM output

But firmware rv_feature_state.h documents feature_state (0xC511_0006) as:

"The default upstream payload from a node ... replaces raw ADR-018 CSI as the primary stream."

So a node emitting its default 60-byte compact state (presence / motion / respiration / heartbeat / anomaly / coherence) produces zero UI updates/health stuck at tick:0, the UI banner reads OFFLINE — even though frames are arriving on UDP :5005.

MEASURED on an ESP32-S3-DevKitC-1: ~5 pps of 0xC511_0006 received and silently dropped; after this change the same stream advances tick and drives the sensing UI.

Fix

  • Add parse_esp32_feature_state(): validates the magic + 60-byte length and maps the packet into the existing Esp32VitalsPacket.
    • Presence fails closed: requires the node's RV_QFLAG_PRESENCE_VALID bit and presence_score >= 0.5.
    • ts_us → timestamp_ms; respiration_bpm / heartbeat_bpm passthrough; motion_score drives the motion flag.
  • Wire it as an .or_else() fallback after parse_esp32_vitals, so it flows through the unchanged SensingUpdate / WebSocket path (no downstream churn).

Tests

New feature_state_parser_tests (4):

  • field extraction + presence mapping when valid-flag set
  • fail-closed when PRESENCE_VALID is clear (high score, still not present)
  • wrong-magic rejection
  • short-buffer rejection

wifi-densepose-sensing-server bin tests 171 → 175, 0 failed.

Field note (not in this diff)

On a display-less DevKitC the shipped v0.7.0 binary mis-detects a phantom panel (disp_hal: TCA9554 not found — assuming display power is always-on) → has_display=true → CSI promiscuous filter stays MGMT-only → 0 pps → DEGRADED. A display-disabled build restores ~30 pps yield. main already carries the RuView#1000 touch-probe bail, so this looks like a stale-release / build-variant issue rather than a code regression — flagging it so the headless release binaries can be rebuilt/re-released. No firmware code changed here.

🤖 Generated with claude-flow

lakinduakash and others added 3 commits July 9, 2026 19:30
…ode's default payload

udp_receiver_task decoded only CSI (0x01), vitals (0x02), fused vitals
(0x04), and WASM output (0x07). Per firmware rv_feature_state.h, the
feature_state packet (0xC511_0006) is "the default upstream payload from
a node ... replaces raw ADR-018 CSI as the primary stream" — so a node
emitting its default 60-byte compact state produced zero UI updates
(/health stuck at tick:0, banner OFFLINE) despite frames arriving on
UDP :5005. Observed on an ESP32-S3-DevKitC-1: ~5 pps 0xC511_0006 all
dropped.

Add parse_esp32_feature_state(): validates magic + 60-byte length and
maps into the existing Esp32VitalsPacket. Presence fails closed on the
node's RV_QFLAG_PRESENCE_VALID bit AND presence_score >= 0.5. Wired as
an .or_else() fallback after the vitals parser so it flows through the
unchanged SensingUpdate/WebSocket path.

Pinned by feature_state_parser_tests (4): field extraction + presence
mapping, fail-closed when valid-flag clear, wrong-magic reject,
short-buffer reject. Bin tests 171 -> 175, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>
Firmware adaptive_controller.c copies a raw, unclamped detector magnitude
into presence_score/motion_score even though rv_feature_state.h documents
them as 0..1 (respiration_conf reuses the same value). MEASURED 13.38 on
an ESP32-S3, which surfaced as "1338%" confidence in the UI.

Normalise at the parse boundary: non-finite -> 0.0, clamp to [0,1]. The
presence/motion booleans derive from the clamped score, so a raw 13.38
still reads as present. Defensive: the server should not trust the node
to honor the wire contract.

Pinned by feature_state_parser_tests::clamps_raw_and_non_finite_scores_to_unit_range.

Co-Authored-By: claude-flow <ruv@ruv.net>
…over raw-CSI classifier

With CSI capture healthy (~34 pps raw 0xC511_0001), the server's raw-CSI
motion classifier runs per frame and, being uncalibrated for the room,
reads ambient CSI variance (the node's own 50 Hz self-ping, other WiFi,
multipath) as present_moving. An empty room the ESP32 correctly reports
as empty then rendered as "1 person", flickering as raw-CSI updates
interleaved with the correct feature_state updates.

The ESP32 already computes presence on-device against a 60 s empty-room
baseline (ADR-081 feature_state, 0xC511_0006), which reads empty=0. Add
prefer_board_presence(): when a fresh feature_state presence exists
(< 5 s, tracked via AppStateInner::last_feature_state_at), override the
raw-CSI classifier's presence/motion_level on the aggregate and per-node
classification. Confidence is left as the server computed it.

Pinned by prefer_board_presence_tests (3). Bin tests 175 -> 178, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant