Skip to content

fix(sensing-server): unstick empty-room field-model calibration deadlock#1343

Open
rjperry36 wants to merge 7 commits into
ruvnet:mainfrom
rjperry36:fix/calibration-feed-deadlock
Open

fix(sensing-server): unstick empty-room field-model calibration deadlock#1343
rjperry36 wants to merge 7 commits into
ruvnet:mainfrom
rjperry36:fix/calibration-feed-deadlock

Conversation

@rjperry36

Copy link
Copy Markdown
Contributor

Problem

POST /api/v1/calibration/* is a dead endpoint on a live streaming node — the field-model calibration can never start. Observed on a real ESP32-S3 deployment as {"status":"Uncalibrated","frame_count":0} that never advances, no matter how long the room is left empty.

Root cause — a two-gate deadlock

  • calibration_start creates the FieldModel in Uncalibrated.
  • The per-frame server feed field_bridge::maybe_feed_calibration only fed observations while the model was already Collecting.
  • But the only thing that sets Collecting is feed_calibration itself, on its first fed frame.

So nothing ever fed the first frame → status never left Uncalibratedcalibration_frame_count stayed pinned at 0 → the SVD room eigenstructure (eigenvalue-based person counting / localization) could never calibrate.

Presence/motion/vitals are unaffected — they use the separate automatic rolling baseline, not the field model.

Fix

  • field_bridge::maybe_feed_calibration — feed while Uncalibrated | Collecting, so the first frame flips the model to Collecting and the count advances.
  • calibration_stop — return a structured {success:false, frame_count, frames_needed} instead of an opaque 500 when finalized before enough empty-room frames accumulate.
  • FieldModel::min_calibration_frames() — new accessor used by the guard above.

Tests

  • New regression test field_bridge::tests::maybe_feed_calibration_advances_uncalibrated_to_collecting — asserts Uncalibrated → Collecting and frame count 0 → 1 → 2 (fails on old code).
  • Verified locally (--no-default-features):
    • wifi-densepose-sensing-server bin: 172 passed, 0 failed
    • wifi-densepose-signal lib: 477 passed, 0 failed

🤖 Generated with claude-flow

rjperry36 and others added 7 commits June 29, 2026 11:21
* chore(deploy): harden QNAP deploy script for idempotent re-runs

Make scripts/deploy-qnap.sh safe to re-run on the QNAP NAS:

- Persist an auto-generated RUVIEW_API_TOKEN to $TOKEN_FILE and reuse it
  on every run, so the image's security gate (exit 64) is satisfied and
  clients keep working across redeploys. Respects a passed RUVIEW_API_TOKEN.
- Auto-derive SENSING_ALLOWED_HOSTS from the NAS LAN IP + port so browsers
  reach the UI instead of HTTP 421 (DNS-rebinding defense). Overridable.
- Add RUVIEW_ALLOW_UNAUTHENTICATED=1 opt-out for trusted-LAN demos.
- Robust LAN IP detection (ip route get first; BusyBox hostname lacks -I),
  guarded so a failed probe can't abort under set -euo pipefail.
- Add a /health probe after start and return a clean exit 0 on success.
- Pass through optional WDP_RUFIELD_SIGNING_SEED for real deployments.

Verified on the target NAS: bash -n clean, three idempotent live runs,
health 200, reachable over the LAN.

Co-Authored-By: claude-flow <ruv@ruv.net>

* chore(deploy): add reusable deployment templates under deploy/

Establish the software-vs-deployment split: this fork stays the core
(now tracking upstream ruvnet/RuView), while deploy/ holds reusable,
committed templates that consume the published image. Real per-project
instances (with tokens/hosts) stay out of git.

- deploy/README.md: the template->instance model + fork-vs-deploy rule.
- deploy/qnap/docker-compose.yml: declarative deploy, image pinned to
  v0.8.3-esp32 (digest-verified == the build running on the NAS).
- deploy/qnap/.env.example: config keys, no secrets (token left blank).
- deploy/qnap/README.md: script vs compose paths, ESP32 go-live note.

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: claude-flow <ruv@ruv.net>
POST /api/v1/calibration/start created the FieldModel in Uncalibrated, but
field_bridge::maybe_feed_calibration only fed frames while already Collecting
— and the only thing that sets Collecting is feed_calibration on its first
fed frame. The two gates deadlocked: no first frame was ever fed, so
calibration_frame_count stayed 0 and status never left Uncalibrated. Observed
live on a streaming ESP32 node as {"status":"Uncalibrated","frame_count":0}
that never advanced.

- field_bridge::maybe_feed_calibration: feed while Uncalibrated | Collecting
  so the first frame flips the model to Collecting and the count advances.
- calibration_stop: return structured {success:false, frame_count,
  frames_needed} instead of an opaque 500 when finalized with too few frames.
- FieldModel::min_calibration_frames() accessor for the guard above.
- Regression test: maybe_feed_calibration_advances_uncalibrated_to_collecting.

Presence/motion/vitals were unaffected (separate auto rolling baseline).

Co-Authored-By: claude-flow <ruv@ruv.net>
…amd64)

Option A redeploy path: the upstream docker workflow needs ruvnet secrets a
fork can't use, so this fork-local workflow builds the sensing-server image on
GitHub runners and pushes to ghcr.io/<owner>/wifi-densepose with the built-in
GITHUB_TOKEN. amd64-only (QNAP TS-853A is x86_64). The NAS then pulls it via
IMAGE=ghcr.io/rjperry36/wifi-densepose:calfix ./scripts/deploy-qnap.sh.

Co-Authored-By: claude-flow <ruv@ruv.net>
…ruvnet#864 guard)

The image built + pushed fine; only the smoke-test docker run failed because
the entrypoint refuses to boot on 0.0.0.0 without a token or explicit
RUVIEW_ALLOW_UNAUTHENTICATED=1. Match the QNAP LAN posture so the test boots.

Co-Authored-By: claude-flow <ruv@ruv.net>
…(real HT40 fix)

Follow-up to the calibration deadlock fix. With the status gate unstuck,
maybe_feed_calibration reached feed_calibration, but a real ESP32 HT40 node
streams 128-wide amplitude frames while the single-link FieldModel is the
canonical 56-tone grid. LinkStats::update returned DimensionMismatch,
feed_calibration bubbled it, and maybe_feed_calibration swallowed it at debug
level — so frame_count stayed pinned at 0 on live hardware (presence/vitals,
which read the global history, were unaffected).

Resample each frame onto the model's canonical 56-tone grid via
HardwareNormalizer::resample_to_canonical before feeding — the same length-only
canonicalization the multistatic fusion path uses (ruvnet#1170).

Pinned by maybe_feed_calibration_resamples_wide_frames_and_accumulates
(128-wide -> Collecting + count 1). sensing-server bin: 173 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>
…oint sh)

The asset-check 'docker run --rm IMAGE sh -c ls' tripped the docker-entrypoint
ruvnet#864 guard (exit 64) before running ls, since it carries no token/allow-unauth.
Run it with --entrypoint sh so it lists UI assets without booting the server.
The boot run keeps RUVIEW_ALLOW_UNAUTHENTICATED=1.

Co-Authored-By: claude-flow <ruv@ruv.net>
…false-positive presence)

An empty room read present_moving forever in high-multipath (strong-RSSI)
environments. motion_score mixes a true frame-to-frame temporal term (~0 when
empty) with absolute static terms (variance/10, motion_band_power/25) that
saturate to 1.0 when the room's ambient exceeds those divisors — raw ~0.47 with
nobody present. smooth_and_classify[_node] learns that ambient as baseline but
only subtracted 70% (raw - baseline*0.7 = ~0.14), above the present_moving
(>0.12) and presence (>0.03) thresholds.

Subtract the full learned baseline via new BASELINE_SUBTRACT_FACTOR = 1.0 so
presence reflects motion ABOVE the room's static ambient. Empty -> absent; a
person still registers above baseline.

Pinned by csi sustained_ambient_settles_to_absent_after_warmup. bin 174/0.
Person-present sensitivity to be validated on live hardware.

Co-Authored-By: claude-flow <ruv@ruv.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant