Commit Graph

5 Commits

Author SHA1 Message Date
arsen c22dfcd256 fix(raw.html): guard zero RSSI + correct per-node fps counter
* nodes[].rssi_dbm of 0 used to display literally as "0.0 dBm",
  misleading the operator when rssi_history was empty on the first
  few ticks. Now coerce to "--" and skip pushing zeros to the trace.
* per-node fps was 1/dt instantaneous, blown up to 235 by multiple
  SensingUpdate emit paths firing back-to-back. Replaced with a
  1-second windowed counter — now matches the real ~38 fps per node.
2026-05-17 02:37:05 +07:00
arsen 72047a4185 feat(ops): one-command OTA deploy + mobile-first operator UI
scripts/ota-deploy.sh
  Python 3 helper (the earlier bash version tripped over macOS bash 3.2's
  missing associative arrays). One invocation with no arguments:
    1. discovers nodes in the local /24 via ARP + /ota/status:8032 probe;
    2. POSTs the firmware blob to every node in parallel;
    3. waits for reboot, polls /ota/status until running_partition flips,
       and fails-loud if any node stays on the old partition (typical
       symptom of a panic on first boot from the new slot).
  Supports `--build` (idf.py build first), `--no-verify`, explicit IP
  list, and OTA_PSK=<token> for the ADR-050 Bearer auth path.
  Measured cycle: ~25 s end-to-end for both room01 + room02.

static/mobile.html
  Mobile-first sibling of static/raw.html. The desktop page is unreadable
  on a 360-420 px screen — bars chart fights the narrow viewport, 11-12 px
  font, controls overlap the badge. The mobile page:
    - sticky global badge (30 px) + connection pill + reset (44 px tap);
    - per-node card with 22 px node badge, 18 px stat tiles, 90 px trace;
    - drops the bars chart (useless under 600 px wide);
    - viewport-fit=cover, theme-color, apple-mobile-web-app meta tags;
    - high-contrast palette tuned for outdoor light;
    - reuses the /ws/sensing contract verbatim — anything that lights up
      raw.html lights this up too.

main.rs ServeDir route
  Adds `.nest_service("/static", ServeDir::new(.../static))` so
  raw.html / mobile.html / calibrate.html / spectrum.html are served on
  the main 8080 port. Previously they needed a separate
  `python -m http.server :8091`, which the operator had to remember to
  start by hand on every deploy. Now there's exactly one URL per device.

Reachable from a phone on the LAN:
  http://<mac>:8080/static/mobile.html
  http://<mac>:8080/static/raw.html

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-17 02:21:06 +07:00
arsen c3126c39a3 feat(raw.html): per-node classification badges (ADR-101)
Surfaces the raw-amplitude classifier's per-node decision in
node_features[].classification so the UI can show which sensor is
actually seeing motion at any moment. Lets the operator visually find
the best sensor placement without physically moving things — just walk
around and watch which badge lights up.

Server side: adds amp_node_level() pure helper + amp_node_snapshot()
that reads AMP_LATEST, then plugs it into build_node_features so the
existing PerNodeFeatureInfo.classification carries the new labels.

UI: adds a global badge in the top bar and a per-node badge inline in
each h2, color-coded (grey/absent, blue/present_still, green/moving,
red/active) plus the live per-node CV %.
2026-05-17 01:01:10 +07:00
arsen 8aef82069b deploy(esp32s3): PHY gain-lock for baseline-stable CSI + raw signals UI
Ports Francesco Pace's ESPectre gain-lock (GPLv3) to RuView FW: medians
AGC and FFT scale over the first 300 packets after boot, then freezes
them via phy_force_rx_gain / phy_fft_scale_force. With both sensors
locked and proper AP→body→sensor geometry, a 30-s × 3-state capture
(empty / still / walk) now separates by ×3.4–×5.9 instead of ±0.02
within ±0.10 noise as in ADR-099.

Adds static/raw.html — per-node 56-subcarrier amplitude bars + RSSI/
broadband traces, no DSP, for live calibration.

ADR-100 documents the technique, boot calibration values for the
operator's deployment (AGC=42/44, both APPLIED), and the verified
three-state separation table.
2026-05-17 00:31:07 +07:00
arsen b292c7d869 deploy: tp-link wisp ap + rssi-Δ presence detector + live calibration ui
Operator's household environment showed CSI-variance presence detection
failing — empty room produced HIGHER variance than an occupied room because
ambient WiFi noise (neighbour APs, retransmits, BT-coex) dominated the
broadband-variance signal at multi-meter range.

Deployed a TP-Link TL-WR841N in WISP mode as a dedicated isolated AP for
the sensors:
* Sensors associate only with TP-Link_8340 (clean channel)
* TP-Link bridges to the household AP, NAT-forwards sensor UDP to the Mac
* Mac keeps its primary household-AP association — no LAN reconfig needed
* Empty-room variance dropped 50.7 → 35.8 (-30%)

Replaced presence classification with RSSI MAD-Δ override:
* Per-node rolling 120-sample (~10 s @ 12 Hz) window of frame RSSI
* Metric: mean(|Δrssi|) between consecutive frames — robust to int8
  quantisation jitter
* Thresholds tuned for the operator's geometry:
   d < 0.20  → absent
   < 0.55    → present_still
   < 1.10    → present_moving
   >= 1.10   → active
* Confidence field temporarily carries raw d for in-field threshold tuning
* CSI-based features (variance, motion_band_power, spectral_power) remain
  in features.* for vital-sign signal-quality and multi-node fusion paths

UI / tooling:
* New static/spectrum.html — live signal console: combined classification,
  all host-computed features (variance, motion_band, spectral, breathing
  band, RSSI, dominant_freq, change_points), per-node FW signals, and a
  60-second variance trace. Served via `python -m http.server 8091`.
* static/calibrate.html — simpler per-node motion/presence/RSSI bars
  with peak-hold.

Desktop UI / discovery hardening (rolled in here because they came up
during this debug session):
* commands/discovery.rs: HTTP sweep limited to 2..=60 hosts (was 1..=254),
  mDNS + UDP-broadcast paths disabled (current RuView FW doesn't advertise
  them and they were burning CPU every poll cycle). Per-request timeout
  set to 1500 ms with overall budget enforced via tokio::time::timeout +
  futures::join_all (replaces the previous sequential select loop that
  blocked on slow IPs).
* ui/hooks/useNodes.ts: poll interval 10 s → 30 s.
* ui/pages/Dashboard.tsx + NetworkDiscovery.tsx: merge new scan results
  into existing list instead of replacing — discovery races sometimes miss
  a node that was found a moment ago.

Firmware tuning:
* edge_processing.c: broadband-variance divisor /3.0 → /30.0 → /5.0
  iterated; final /5.0 chosen for multi-meter geometry (sensor 1-3 m
  from activity zone). DEBUG_MOTION_DSP scaffolding removed.
* csi_collector.c: CSI_MIN_SEND_INTERVAL_US 20 ms → 4 ms so the host can
  see every available frame (real ceiling is the WiFi CSI callback rate).

Documentation:
* docs/adr/ADR-099 — full forensic write-up: measurement tables for sit/
  walk/empty, the RSSI-Δ rationale, the WISP setup procedure, calibration
  protocol for new deployments, and open items.

Verified end-to-end on hardware (sensors at 192.168.1.17/.19 → TP-Link at
192.168.1.14 → Mac at 192.168.1.21):
* UDP/5006 packets arrive ~12 Hz combined from both nodes
* Empty-room baseline d ≈ 0.49 measured (next: capture sit + walk to
  finalize thresholds)
* Vital signs continue to populate (breathing 9–11 BPM stable)
* Two consecutive OTA round-trips remain functional after the change

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 11:26:07 +07:00