fix(firmware): normalize presence_score and motion_score before transmit#1286
Open
macraj wants to merge 6 commits into
Open
fix(firmware): normalize presence_score and motion_score before transmit#1286macraj wants to merge 6 commits into
macraj wants to merge 6 commits into
Conversation
Both fields carried the same raw, unbounded phase-variance value (edge_processing.c sets s_presence_score = s_motion_energy directly) but were handled inconsistently downstream: presence_score passed through uncapped, motion_score was bare-clamped to [0,1] without first scaling, saturating at 1.0 almost unconditionally since raw values routinely exceed 1.0. Net effect: presence/motion read as "detected" regardless of actual occupancy. Apply the same divide-by-10-then-clamp normalization send_feature_vector() already uses for its equivalent dimensions.
None of the existing ui/*.html pages match the running bridge server (they expect a FastAPI backend on :8000 or a WebSocket that doesn't exist here). Add a minimal polling page against the working /api/v1/sensing/latest endpoint. Requires CORS since the page loads from file:// / a different origin than the server.
…rmalization CONFIG_LED_MOTION_FULLSCALE_MILLI defaulted to 250 (0.25), but the raw motion_energy it colors (via the viridis LUT in main.c's 40Hz gamma callback) commonly runs 4-14 in practice — same unnormalized-scale bug as the presence/motion fix in adaptive_controller.c. The LED was saturating to yellow almost unconditionally instead of tracking real motion. Set to 10000 (10.0) to match the saturation point collect_observation() now uses, so yellow means the same thing here as motion_score == 1.0 does elsewhere.
Poll /api/v1/edge/registry for the set of currently-registered nodes, then /api/v1/vitals/<node_id>/latest for each, rendering one card per node instead of assuming a single fixed node. Each card has an editable, localStorage-persisted floor label. Shows exactly what the backend actually reports (one node today, since only one c6-presence-watcher.py instance is running) rather than mocking additional nodes — more cards appear automatically once a second device + watcher process exists.
…alid ones write_state()/write_feature() only ran inside the presence_valid branch, so /tmp/ruview-last-feature.json only got a fresh ts during "confident" readings (firmware presence_score >= 0.5). Before the presence/motion normalization fix, presence_score was almost always saturated near max, so this was effectively "write every packet" and the bug stayed hidden. Now that presence_score correctly drops below 0.5 in a genuinely calm room, long stretches of presence_valid=False meant the feature file never got rewritten, its ts aged past ruview-sensing-server.py's 10s staleness gate, and consumers (edge/registry, sensing/latest) reported the node as gone — even though the device/watcher were completely healthy and just reporting "empty room" correctly. Write on every privacy-eligible packet regardless of presence_valid; the presence_valid gate still controls which readings drive the motion/occupancy toggles.
ruview-sensing-server.py: background thread samples the feature file every 30s and appends (ts, presence) to a per-node JSONL file, kept as a rolling 24h window in memory too. New endpoint GET /api/v1/sensing/history/<node_id>?hours=24&bucket_min=10 returns 144 ten-minute buckets (occupied fraction each) plus exact state-transition timestamps. No backfill — history only exists from whenever the sampler was first started. ui/live-status.html: each node card gets an SVG timeline strip below the stat grid — gray for no data, dark gray for confirmed-empty, green (intensity = occupied fraction) for occupied, with thin lines marking exact become-present/become-absent transitions. Polls the history endpoint every 30s, independent of the 1s vitals poll.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
presence_scoreandmotion_scoreincollect_observation()(firmware/esp32-csi-node/main/adaptive_controller.c) are both derived from the same raw, unbounded CSI phase-variance quantity (edge_processing.csetss_presence_score = s_motion_energydirectly), but were normalized inconsistently:presence_scorepassed through with no clamp at all, whilemotion_scorewas clamped straight to[0,1]without first scaling — since raw values routinely exceed 1.0, this saturatedmotion_scoreat1.0almost unconditionally.0.5fquality-valid gate, and consumers likescripts/c6-presence-watcher.py's0.40/0.20and0.30/0.15hysteresis thresholds) all assume a genuine0..1scale.edge_processing.c'ssend_feature_vector()(a separate ADR-069 feature-vector wire path):p > 10.0f ? 1.0f : p / 10.0f. This PR applies the same scale-then-clamp to both fields incollect_observation().Test plan
idf.py build), no errors.motionandpresencenow report identical, fluctuating values (e.g.motion=0.18 presence=0.18,motion=1.00 presence=1.00) instead ofmotionbeing pinned at1.00.c6-presence-watcher.py) thatavg_motion/avg_presencenow match in every window and vary meaningfully (0.5–1.0 while near the device).avg_presencedropped to0.00andmotion/occupancyboth went toFalse, holding steady for 2.5+ minutes — confirming the original false-positive ("presence detected" in an empty room) is resolved.tests/host/test_adaptive_controller.c) don't exercisecollect_observation()'s normalization path directly (they setpresence_scoreon the observation struct directly), so this change doesn't touch tested behavior — no regressions expected there, but flagging that this code path had no direct unit coverage before or after this change.