From 60049dbf7c179f42a24b47236ba341fe4df7fdd4 Mon Sep 17 00:00:00 2001 From: macc Date: Wed, 8 Jul 2026 22:19:45 +0200 Subject: [PATCH 1/6] fix(firmware): normalize presence_score and motion_score before transmit 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. --- .../esp32-csi-node/main/adaptive_controller.c | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/firmware/esp32-csi-node/main/adaptive_controller.c b/firmware/esp32-csi-node/main/adaptive_controller.c index 99272ee906..9164a112d1 100644 --- a/firmware/esp32-csi-node/main/adaptive_controller.c +++ b/firmware/esp32-csi-node/main/adaptive_controller.c @@ -116,18 +116,24 @@ static void collect_observation(adapt_observation_t *out) } } - /* Edge-derived state. The ADR-039 vitals packet exposes presence_score - * and motion_energy directly; we treat motion_energy as a proxy for - * motion_score by clamping to [0,1]. anomaly_score and node_coherence - * are not yet emitted by edge_processing — placeholder until Layer 4 - * extraction lands. */ + /* Edge-derived state. The ADR-039 vitals packet's presence_score and + * motion_energy are the same raw, unbounded phase-variance quantity + * (edge_processing.c sets s_presence_score = s_motion_energy directly), + * so both must go through the same scale-then-clamp normalization + * send_feature_vector() already uses (divide by 10, clamp to [0,1]). + * A bare [0,1] clamp on either field saturates at 1.0 almost + * unconditionally, since raw values routinely exceed 1.0 — this is + * what caused presence/motion to read "detected" regardless of actual + * occupancy. anomaly_score and node_coherence are not yet emitted by + * edge_processing — placeholder until Layer 4 extraction lands. */ edge_vitals_pkt_t vitals; if (edge_get_vitals(&vitals)) { - out->presence_score = vitals.presence_score; + float p = vitals.presence_score; + if (p < 0.0f) p = 0.0f; + out->presence_score = (p > 10.0f) ? 1.0f : (p / 10.0f); float m = vitals.motion_energy; if (m < 0.0f) m = 0.0f; - if (m > 1.0f) m = 1.0f; - out->motion_score = m; + out->motion_score = (m > 10.0f) ? 1.0f : (m / 10.0f); } out->anomaly_score = 0.0f; out->node_coherence = 1.0f; From c4144749a5f365980ddace16a33e82e5e3f15685 Mon Sep 17 00:00:00 2001 From: macc Date: Wed, 8 Jul 2026 22:19:55 +0200 Subject: [PATCH 2/6] feat(ui): add standalone live-status page, CORS on sensing server 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. --- scripts/ruview-sensing-server.py | 1 + ui/live-status.html | 165 +++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 ui/live-status.html diff --git a/scripts/ruview-sensing-server.py b/scripts/ruview-sensing-server.py index 0ebc0ef1a2..193ee14f30 100644 --- a/scripts/ruview-sensing-server.py +++ b/scripts/ruview-sensing-server.py @@ -164,6 +164,7 @@ def _json(self, code: int, body: dict) -> None: self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(payload))) + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(payload) diff --git a/ui/live-status.html b/ui/live-status.html new file mode 100644 index 0000000000..6e8e19122c --- /dev/null +++ b/ui/live-status.html @@ -0,0 +1,165 @@ + + + + +RuView — Live Status + + + +

RuView — Live Sensing Status

+ connecting… + +
+ +
+
+
Presence
+
+
+
+
People Detected
+
+
+
+
Motion
+
+
+
+
Breathing Rate
+
bpm
+
+
+
Heart Rate
+
bpm
+
+
+
Confidence
+
+
+
+ +
waiting for first update…
+ + + + From c9fb6bffe3a5e7e70b733b2a7e0dcb69a92400e8 Mon Sep 17 00:00:00 2001 From: macc Date: Wed, 8 Jul 2026 22:40:36 +0200 Subject: [PATCH 3/6] fix(firmware): correct LED gamma-flicker fullscale to match motion normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- firmware/esp32-csi-node/main/Kconfig.projbuild | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index ee1dff164a..98a1c6dea0 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -487,10 +487,18 @@ menu "Onboard LED (ADR-183)" config LED_MOTION_FULLSCALE_MILLI int "Motion value (x1000) that saturates the colormap to yellow" depends on LED_GAMMA_VIZ - default 250 + default 10000 range 1 100000 help edge motion_energy that maps to the top (yellow) of the viridis - colormap, in milli-units (250 = 0.25). Lower = more sensitive + colormap, in milli-units (10000 = 10.0). Lower = more sensitive (reaches yellow with less motion). + + 10.0 matches the saturation point adaptive_controller.c's + collect_observation() uses to normalize this same raw + motion_energy/presence_score value into motion_score/presence_score + (RuView#1286) — yellow now means the same "fully saturated motion" + as motion_score == 1.0 elsewhere in the system, instead of an + unrelated 0.25 threshold that saturated to yellow almost + immediately given real-world motion_energy commonly runs 4-14. endmenu From 26917f1164d288a8ad1d1ce7046ec243dc4f638d Mon Sep 17 00:00:00 2001 From: macc Date: Wed, 8 Jul 2026 22:40:47 +0200 Subject: [PATCH 4/6] feat(ui): rewrite live-status page as a real multi-node/floor grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Poll /api/v1/edge/registry for the set of currently-registered nodes, then /api/v1/vitals//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. --- ui/live-status.html | 281 +++++++++++++++++++++++++++++++++----------- 1 file changed, 210 insertions(+), 71 deletions(-) diff --git a/ui/live-status.html b/ui/live-status.html index 6e8e19122c..443953ac83 100644 --- a/ui/live-status.html +++ b/ui/live-status.html @@ -5,6 +5,7 @@ RuView — Live Status -

RuView — Live Sensing Status

- connecting… +
+

RuView — Live Sensing Status

+ connecting… +
-
-
-
Presence
-
-
-
-
People Detected
-
-
-
-
Motion
-
-
-
-
Breathing Rate
-
bpm
-
-
-
Heart Rate
-
bpm
-
-
-
Confidence
-
-
+
+ -
waiting for first update…
-