发布

  • v1746 0c2b1c16cc

    fix: ESP32 vitals over-count + presence flicker (#998/#996) + Observatory per-person position/motion (#1050) (#1060)

    frostbyte_neo 发布于 2026-06-14 04:31:30 +00:00 | 283 次提交 在此版本后已推送到 main

    • fix(firmware): gate phantom persons + add presence hysteresis (#998, #996)

    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(firmware): host C99 tests for vitals count + presence logic (#998, #996)

    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

    • docs: record ESP32 vitals count/presence fixes (#998, #996)

    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

    下载附件