Skip to content

Active Person Count Is Incorrect During Live ESP32 CSI Streaming #2058

Open

Description

@ashwini1890

Description

I am testing RuView with ESP32 nodes sending live CSI data to the sensing server over UDP.

The CSI data is being received successfully by the server, and the sensing pipeline is running in live mode. However, the Active Person Count displayed by the server UI is frequently incorrect or inconsistent with the actual number of people present in the room.

For example, when the room contains a known number of people, the UI may report a different count, and the count can change dynamically even when the number of people in the room remains unchanged.

Setup

  • RuView: v2754 / current test build

  • CSI source: ESP32-S3

  • Transport: UDP

  • UDP port: 5005

  • Multiple ESP32 CSI nodes

  • Sensing server running with:

    • --source esp32

    • --udp-port 5005

    • --udp-bind 0.0.0.0

    • --http-port 8080

    • --ws-port 8765

  • Adaptive model/calibration enabled

  • ESP32 nodes are continuously sending CSI frames to the server

Observed Behavior

The server successfully receives CSI packets and switches to the live ESP32 source, but the Active Person Count does not reliably correspond to the actual occupancy.

For example:

Actual people | RuView Active Person Count -- | -- 0 | Variable 1 | Variable 2 | Sometimes 1 / other values 3 | Variable

The exact incorrect value is not always the same; the main issue is that the reported count changes even when the physical number of people remains constant.

Expected Behavior

If the ESP32 nodes are providing valid CSI data and the room occupancy remains unchanged, the Active Person Count should remain reasonably stable and correspond to the actual number of people present.

For example:

Actual occupancy: 2 people

Expected:
1 → 2
2 → 2
2 → 2
2 → 2

rather than continuously fluctuating between different values.

Additional Observation

I have also noticed that the server appears to have multiple stages involved in the occupancy/person-count calculation, including the CSI processing, adaptive/field model processing, per-node person-count state, and final person-count aggregation.

I would like to understand which component is considered authoritative for the final Active Person Count.

In particular, could you clarify whether the following path is expected to contribute to the final count?

ESP32 CSI
↓
CSI processing
↓
DynamicMinCut / correlation-based estimation
↓
corr_persons
↓
NodeState::prev_person_count
↓
aggregate_person_count()
↓
Active Person Count

Question

Could someone confirm whether this behavior is expected with the current ESP32 CSI pipeline, or whether there is a known issue affecting the Active Person Count?

I can provide:

  • Server logs

  • ESP32 CSI capture logs

  • Calibration data

  • Adaptive model information

  • Exact number of ESP32 nodes

  • Reproduction recordings

  • Screenshots/video of the changing person count

if required.

Thank you.

Activity

  1. ruvnet commented on Sep 30, 2026

    @ruvnet
    Owner

    Merged #2060 as e1fbbed to address confirmed state-consistency defects in this path:

    • A stale per-node count could override fresh occupancy. The regression case was an old count of three overriding a fresh count of one. Count contributors now use the room vote's strict ten-second freshness boundary.
    • Raw CSI and edge-vitals counts now use the same debounced room presence as the published classification. An absent packet from one node no longer clears a room still classified as occupied.
    • Edge-only nodes now contribute their own classification, and legacy edge messages keep presence/count local to their named node.

    Yes, the corr_persons → NodeState::prev_person_count → aggregate_person_count() path contributes to the final estimate. The server's room-wide output is sensing_update.estimated_persons, interpreted alongside classification and source/freshness status. The room presence gate decides whether a positive count is published; fresh per-node counts can raise the activity/model-derived estimate. Edge classification confidence is kept separate from the CSI count score.

    Validation: 15 focused count tests passed; the full sensing-server suite passed 929 tests with zero failures and two ignored. CI merge gates also passed, including Rust workspace tests and benchmark compilation. These are software regressions using synthetic fixtures. We still need your logs to determine whether these defects explain your particular fluctuations. Calibrated-zero precedence and buffered occupancy after a complete stream outage remain separate follow-ups.

    Please retest a build containing #2060 and share the exact commit/configuration, node count and packet types/rates, calibration status, and timestamped sensing_update plus /api/v1/nodes samples covering a period of known steady occupancy. Redact credentials and unrelated personal/network data. Keeping this issue open for field confirmation.

  2. ruvnet commented on Oct 15, 2026

    @ruvnet
    Owner

    The underlying root cause is likely #1940: the eigenvalue-based occupancy counting is compiled out of the shipped server binary (default-features = false on the signal crate), leaving a fallback scalar threshold that can only distinguish 0 vs. ≥1 person. This explains why person count is stuck and does not track real occupancy changes.

    See also #1758 for a longer-running field report from a different user with the same symptoms.


    Generated by Claude Code

  3. danalec commented on Oct 22, 2026

    @danalec
    Contributor

    Reproduced on a live ESP32-S3 node against the current server. The count is not stable, and the reason is not the one the code suggests.

    Setup

    • Node: ESP32-S3 DevKitC-1 N16R8 (16 MB flash, 8 MB PSRAM), rev v0.2, MAC 90:70:69:18:9b:d0, firmware 0.8.15 from this branch, 16 MB display-off build.
    • Server: sensing-server built from this branch, --source esp32 --udp-port 5005 --udp-bind 0.0.0.0 --udp-insecure-lan --bind-addr 127.0.0.1, one node.
    • The room was empty and the door closed for the whole window.
    • The node's serial log and the server's /api/v1/sensing/latest were sampled in the same run from the same origin: opening the serial port resets the board, so the node's uptime and the sampler's elapsed time share an origin.

    What was measured

    Active Person Count (estimated_persons), 69 samples over 150 s:

    count distribution: {1: 44, 2: 23, 4: 1, 3: 1}
    

    The count moves between 1, 2, 3 and 4 in an empty room.

    The two verdicts also disagree with each other, which is the sharper symptom. In the first ten seconds the node reported motion=0.00 presence=0.00 (state 5) and the server's own room vote was absent, yet the count was still 1:

       t count           room       var   domHz | node_state motion presence
       6.1     1         absent    45.984   1.380 |          5   0.00     0.00
       8.1     1         absent    53.357   1.318 |          5   0.00     0.00
      10.1     1         absent    61.290   1.194 |          5   0.00     0.00
      12.1     1 present_moving    40.590   1.292 |          5   0.00     0.00
      14.1     2 present_moving    42.784   1.249 |          6   1.00     5.45
      ...
      50.3     4 present_moving     7.319   0.397 |          6   1.00     7.32
      58.4     1 present_moving    40.155   1.466 |          6   1.00     4.20
      64.4     2 present_moving    18.966   0.476 |          6   1.00    18.97
    

    Why

    The count is derived from an activity score that is not a stable quantity on this scene:

    • the node's own presence score ranged 0.00 … 24.58 over 145 ticks, with motion flipping from 0.00 to 1.00 and staying there — in an empty room;
    • the server's features.variance ranged 6.32 … 281.2 over the same window — a 44x swing — and the dominant frequency hopped between 0.000 Hz and 3.935 Hz, i.e. between "no dominant component" and the motion band.

    score_to_person_count already has asymmetric hysteresis (0.70/0.85 up, 0.55/0.92 down), but the input crosses the whole band every couple of seconds, so the count crosses it on every swing. The room count is then activity_count.max(node_max) — the maximum of two counts that each carry their own hysteresis, so neither holds the other still.

    So the behaviour is a known consequence of the current scoring rather than a misconfiguration, and on an empty scene the count is not a reliable occupancy number. The observation that the count changes while the number of people stays constant is reproducible, and the "not empty" verdict it produces is a false positive.

    Where a fix would go

    The count has to hold still for a while before it changes, not merely have a dead band: the hysteresis is on the output while the input crosses the whole range. A dwell on the count (the new value has to hold for N consecutive samples) would address that, and the absent / present_moving room vote flipping in the same window suggests the same input is worth stabilising at the classification level too.

    I have the live reproduction and can supply the trace and a candidate patch with tests.

  4. danalec commented on Oct 23, 2026

    @danalec
    Contributor

    The part of this request that a pull request can own is done: the published person count no longer moves while the room is unchanged.

    Concretely, on main at fefb0f91 with one ESP32-S3 node (firmware 0.8.15) against an empty room, 200 s windows polled at 5 Hz, the same procedure before and after the change:

    before after
    changes in the published count 142 1
    values seen 1, 2, 3, 4 1
    longest stable run 16.7 s 197.5 s

    The room classification stayed present_moving for the whole of the "before" window, so the movement was entirely in the count path: the count's hysteresis holds the output steady while the activity score sits inside a band, but it cannot hold it when the score crosses the whole band on its own.

    The published count now changes only after the new value has been observed continuously for 1.5 s — the same window the room classification uses, because both describe the same debounced room state. A count of zero is not held back, so an empty room is still reported as empty as soon as the room vote says so. Six new regression tests cover it, and a RuView#2058 fix-marker keeps the dwell from being silently dropped.

    What this does not change is what the count means: the activity score it derives from is not an occupancy estimator, so in an empty scene the count can still settle on a non-zero value. The dwell removes the movement, not the bias. The eigenvalue occupancy path remains a separate matter (#1940), and which component is authoritative for the final count is unchanged by this.

  5. removed
    questionFurther information is requested
    on Oct 30, 2026
  6. added
    questionFurther information is requested
    and removed
    bugSomething isn't working
    on Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions