fix(sensing-server): unstick empty-room field-model calibration deadlock#1343
Open
rjperry36 wants to merge 7 commits into
Open
fix(sensing-server): unstick empty-room field-model calibration deadlock#1343rjperry36 wants to merge 7 commits into
rjperry36 wants to merge 7 commits into
Conversation
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_startcreates theFieldModelinUncalibrated.field_bridge::maybe_feed_calibrationonly fed observations while the model was alreadyCollecting.Collectingisfeed_calibrationitself, on its first fed frame.So nothing ever fed the first frame → status never left
Uncalibrated→calibration_frame_countstayed 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 whileUncalibrated | Collecting, so the first frame flips the model toCollectingand 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
field_bridge::tests::maybe_feed_calibration_advances_uncalibrated_to_collecting— assertsUncalibrated → Collectingand frame count0 → 1 → 2(fails on old code).--no-default-features):wifi-densepose-sensing-serverbin: 172 passed, 0 failedwifi-densepose-signallib: 477 passed, 0 failed🤖 Generated with claude-flow