Automated release from CI pipeline
Changes:
fix: ESP32 vitals over-count + presence flicker (#998/#996) + Observatory per-person position/motion (#1050) (#1060)
Two ESP32 edge-vitals logic bugs in edge_processing.c. Both are
robustness/logic fixes — NOT validated-accuracy claims. True count/PCK
vs labelled ground truth remains hardware/data-gated (COM9 ESP32-S3).
#998 — n_persons over-counted (reported 4 for one person):
update_multi_person_vitals() split top-K subcarriers into top_k_count/2
groups and marked EVERY group active, so one body's multipath always
read the full EDGE_MAX_PERSONS. Added two pure, host-testable helpers:
- count_distinct_persons(): per-group energy gate
(EDGE_PERSON_MIN_ENERGY_RATIO) + spatial dedup
(EDGE_PERSON_MIN_SC_SEP) so weak/adjacent multipath groups don't
count as separate bodies. Strongest group always counts (>=1). - person_count_debounce(): a gated count must hold
EDGE_PERSON_PERSIST_FRAMES consecutive frames before it's emitted,
so a single noisy frame can't promote a phantom.
The active flags now mark only the strongest stable_count groups.
#996 — presence flag flickered at ~50cm despite high presence_score:
the bare score > threshold compare chattered on a noisy score
(field-observed 2.6-26.7 frame-to-frame). Replaced with a Schmitt
trigger + clear-debounce (presence_flag_update): assert above
threshold, hold in the dead band down to threshold *
EDGE_PRESENCE_HYST_RATIO, clear only after EDGE_PRESENCE_CLEAR_FRAMES
consecutive sub-low frames. presence_score itself is unchanged and
still emitted for consumer-side thresholding.
All thresholds are named, documented constants in edge_processing.h.
Firmware builds clean for esp32s3 (idf.py build RC=0).
Co-Authored-By: claude-flow ruv@ruv.net
test/test_vitals_count_presence.c pins the two fixes with deterministic
host-buildable tests (no ESP-IDF needed). 13 cases / 22 assertions, all
passing under gcc 13 -Wall -Wextra:
#998 count gate: single strong signature + multipath -> count==1;
two well-separated -> 2; two strong-but-adjacent -> 1 (dedup);
no signal -> 0; three well-separated -> 3.
#998 debounce: transient spike rejected; sustained change accepted;
flapping count stays stable.
#996 presence: dithering trace -> stable flag (no flicker); brief dips
held by clear-debounce; genuine departure clears within hold window;
dead-band holds state.
The named tuning constants are #include'd from the real
edge_processing.h so the test and firmware can never disagree on
thresholds. make run_vitals / make host_tests added; binaries
gitignored.
Hardware-gated caveat documented in the test header: these pin the
decision LOGIC; the exact energy/separation/hysteresis values that best
match a real room vs labelled occupancy remain on-device tuning.
Co-Authored-By: claude-flow ruv@ruv.net
CHANGELOG [Unreleased] Fixed: root cause + fix + named constants + test
- explicit hardware/data-gated caveat for both bugs.
ADR-021 Implementation Notes: dated 2026-06 entry noting the edge-path
person-count + presence-flicker fixes are boolean/count emission-logic
fixes, not a validated-accuracy claim; thresholds pending on-device
calibration.
Co-Authored-By: claude-flow ruv@ruv.net
- fix(sensing-server): emit real field-derived person position/motion to /ws/sensing (#1050)
The Observatory 3D figure never animated because the sensing_update WS
frame carried no per-person position/motion_score/pose — only image-space
keypoints. The FigurePool/PoseSystem (and demo-data.js's own contract)
animate each figure from persons[i].position (room-world), .motion_score
(0..100), and .pose; none were on the live stream.
Honest scope (Case 2): the pipeline has no calibrated per-person room
localizer or per-person skeletal pose. New field_localize module extracts
the strongest peak(s) from the real signal_field grid (subcarrier
variances x motion-band power) and maps the peak cell to Observatory world
coords with the exact _buildSignalField transform. motion_score is the
measured motion_band_power passed through; pose is set only from a real
aggregate posture estimate, else None (never a fabricated skeleton).
Empty/below-threshold field -> persons: [] (no phantom); present person
with no resolvable peak keeps position [0,0,0], not invented coords.
attach_field_positions runs after the tracker step at all five broadcast
sites. New position/motion_score/pose fields added to both PersonDetection
structs. No UI change needed — the Observatory already reads these fields.
Tests: field_localize peak/coordinate/empty/separation units +
observatory_persons_field_position_tests (known-peak -> emitted position,
empty-room -> no phantom, pose real-or-None, below-threshold honesty).
sensing-server bin 441->451, 0 failed.
Co-Authored-By: claude-flow ruv@ruv.net
- docs(changelog): record #1050 Observatory persons position/motion fix
Co-Authored-By: claude-flow ruv@ruv.net
Docker Image:
ghcr.io/ruvnet/RuView:0c2b1c16cca3f65e707b81d6b960065312b9b904