Compare commits

..

1 Commits

Author SHA1 Message Date
ruv 365c107d4c Merge origin/main into feat/accuracy-sprint-001
Resolved conflicts:
- edge_processing.c: kept batch-limited watchdog approach (proven on hardware)
  with PR's per-frame yield comment for clarity
- Cargo.toml: accepted PR's new dependencies
- main.rs: accepted PR's Kalman tracker + multi-node fusion additions

Co-Authored-By: taylorjdawson <taylor@example.com>
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-03 08:57:26 -04:00
16 changed files with 86 additions and 151537 deletions
+2 -82
View File
@@ -95,88 +95,9 @@ node scripts/mincut-person-counter.js --port 5006 # Correct person counting
>
---
### Pre-Trained Models (v0.6.0) — No Training Required
<details open>
<summary><strong>Download from HuggingFace and start sensing immediately</strong></summary>
Pre-trained models are available on HuggingFace:
> **https://huggingface.co/ruv/ruview** (primary) | [mirror](https://huggingface.co/ruvnet/wifi-densepose-pretrained)
Trained on 60,630 real-world samples from an 8-hour overnight collection. Just download and run — no datasets, no GPU, no training needed.
| Model | Size | What it does |
|-------|------|-------------|
| `model.safetensors` | 48 KB | Contrastive encoder — 128-dim embeddings for presence, activity, environment |
| `model-q4.bin` | 8 KB | 4-bit quantized — fits in ESP32-S3 SRAM for edge inference |
| `model-q2.bin` | 4 KB | 2-bit ultra-compact for memory-constrained devices |
| `presence-head.json` | 2.6 KB | 100% accurate presence detection head |
| `node-1.json` / `node-2.json` | 21 KB | Per-room LoRA adapters (swap for new rooms) |
```bash
# Download and use (Python)
pip install huggingface_hub
huggingface-cli download ruv/ruview --local-dir models/
# Or use directly with the sensing pipeline
node scripts/train-ruvllm.js --data data/recordings/*.csi.jsonl # retrain on your own data
node scripts/benchmark-ruvllm.js --model models/csi-ruvllm # benchmark
```
**Benchmarks (Apple M4 Pro, retrained on overnight data):**
| What we measured | Result | Why it matters |
|-----------------|--------|---------------|
| **Presence detection** | **100% accuracy** | Never misses a person, never false alarms |
| **Inference speed** | **0.008 ms** per embedding | 125,000x faster than real-time |
| **Throughput** | **164,183 embeddings/sec** | One Mac Mini handles 1,600+ ESP32 nodes |
| **Contrastive learning** | **51.6% improvement** | Strong pattern learning from real overnight data |
| **Model size** | **8 KB** (4-bit quantized) | Fits in ESP32 SRAM — no server needed |
| **Total hardware cost** | **$140** | ESP32 ($9) + [Cognitum Seed](https://cognitum.one) ($131) |
</details>
### 17 Sensing Applications (v0.6.0)
<details>
<summary><strong>Health, environment, security, and multi-frequency mesh sensing</strong></summary>
All applications run from a single ESP32 + optional Cognitum Seed. No camera, no cloud, no internet.
**Health & Wellness:**
| Application | Script | What it detects |
|------------|--------|----------------|
| Sleep Monitor | `node scripts/sleep-monitor.js` | Sleep stages (deep/light/REM/awake), efficiency, hypnogram |
| Apnea Detector | `node scripts/apnea-detector.js` | Breathing pauses >10s, AHI severity scoring |
| Stress Monitor | `node scripts/stress-monitor.js` | Heart rate variability, LF/HF stress ratio |
| Gait Analyzer | `node scripts/gait-analyzer.js` | Walking cadence, stride asymmetry, tremor detection |
**Environment & Security:**
| Application | Script | What it detects |
|------------|--------|----------------|
| Person Counter | `node scripts/mincut-person-counter.js` | Correct occupancy count (fixes #348) |
| Room Fingerprint | `node scripts/room-fingerprint.js` | Activity state clustering, daily patterns, anomalies |
| Material Detector | `node scripts/material-detector.js` | New/moved objects via subcarrier null changes |
| Device Fingerprint | `node scripts/device-fingerprint.js` | Electronic device activity (printer, router, etc.) |
**Multi-Frequency Mesh** (requires `--hop-channels` provisioning):
| Application | Script | What it detects |
|------------|--------|----------------|
| RF Tomography | `node scripts/rf-tomography.js` | 2D room imaging via RF backprojection |
| Passive Radar | `node scripts/passive-radar.js` | Neighbor WiFi APs as bistatic radar illuminators |
| Material Classifier | `node scripts/material-classifier.js` | Metal/water/wood/glass from frequency response |
| Through-Wall | `node scripts/through-wall-detector.js` | Motion behind walls using lower-frequency penetration |
All scripts support `--replay data/recordings/*.csi.jsonl` for offline analysis and `--json` for programmatic output.
</details>
### What's New in v0.5.5
<details>
<details open>
<summary><strong>Advanced Sensing: SNN + MinCut + WiFlow + Multi-Frequency Mesh</strong></summary>
**v0.5.5 adds four new sensing capabilities** built on the [ruvector](https://github.com/ruvnet/ruvector) ecosystem:
@@ -1267,8 +1188,7 @@ Download a pre-built binary — no build toolchain needed:
| Release | What's included | Tag |
|---------|-----------------|-----|
| [v0.6.0](https://github.com/ruvnet/RuView/releases/tag/v0.6.0-esp32) | **Latest**[Pre-trained models on HuggingFace](https://huggingface.co/ruv/ruview), 17 sensing apps, 51.6% contrastive improvement, 0.008ms inference | `v0.6.0-esp32` |
| [v0.5.5](https://github.com/ruvnet/RuView/releases/tag/v0.5.5-esp32) | SNN + MinCut (#348 fix) + CNN spectrogram + WiFlow + multi-freq mesh + graph transformer | `v0.5.5-esp32` |
| [v0.5.5](https://github.com/ruvnet/RuView/releases/tag/v0.5.5-esp32) | **Latest**SNN + MinCut (fixes #348) + CNN spectrogram + WiFlow 1.8M architecture + multi-freq mesh (6 channels) + graph transformer | `v0.5.5-esp32` |
| [v0.5.4](https://github.com/ruvnet/RuView/releases/tag/v0.5.4-esp32) | Cognitum Seed integration ([ADR-069](docs/adr/ADR-069-cognitum-seed-csi-pipeline.md)), 8-dim feature vectors, RVF store, witness chain, security hardening | `v0.5.4-esp32` |
| [v0.5.0](https://github.com/ruvnet/RuView/releases/tag/v0.5.0-esp32) | mmWave sensor fusion ([ADR-063](docs/adr/ADR-063-mmwave-sensor-fusion.md)), auto-detect MR60BHA2/LD2410, 48-byte fused vitals, all v0.4.3.1 fixes | `v0.5.0-esp32` |
| [v0.4.3.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.3.1-esp32) | Fall detection fix ([#263](https://github.com/ruvnet/RuView/issues/263)), 4MB flash ([#265](https://github.com/ruvnet/RuView/issues/265)), watchdog fix ([#266](https://github.com/ruvnet/RuView/issues/266)) | `v0.4.3.1-esp32` |
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,15 +0,0 @@
{
"id": "pretrain-1775182186",
"name": "pretrain-1775182186",
"label": "mixed-activity",
"started_at": "2026-04-03T02:09:46Z",
"ended_at": "2026-04-03T02:11:46Z",
"duration_secs": 120,
"frame_count": 5783,
"file_size_bytes": 2580539,
"file_path": "data/recordings\\pretrain-1775182186.csi.jsonl",
"nodes": {
"2": 2886,
"1": 2897
}
}
-76
View File
@@ -1055,82 +1055,6 @@ See [ADR-071](adr/ADR-071-ruvllm-training-pipeline.md) and the [pretraining tuto
---
## Pre-Trained Models (No Training Required)
Pre-trained models are available on HuggingFace: **https://huggingface.co/ruvnet/wifi-densepose-pretrained**
Download and start sensing immediately — no datasets, no GPU, no training needed.
### Quick Start with Pre-Trained Models
```bash
# Install huggingface CLI
pip install huggingface_hub
# Download all models
huggingface-cli download ruvnet/wifi-densepose-pretrained --local-dir models/pretrained
# The models include:
# model.safetensors — 48 KB contrastive encoder
# model-q4.bin — 8 KB quantized (recommended)
# model-q2.bin — 4 KB ultra-compact (ESP32 edge)
# presence-head.json — presence detection head (100% accuracy)
# node-1.json — LoRA adapter for room 1
# node-2.json — LoRA adapter for room 2
```
### What the Models Do
The pre-trained encoder converts 8-dim CSI feature vectors into 128-dim embeddings. These embeddings power all 17 sensing applications:
- **Presence detection** — 100% accuracy, never misses, never false alarms
- **Environment fingerprinting** — kNN search finds "states like this one"
- **Anomaly detection** — embeddings that don't match known clusters = anomaly
- **Activity classification** — different activities cluster in embedding space
- **Room adaptation** — swap LoRA adapters for different rooms without retraining
### Retraining on Your Own Data
If you want to improve accuracy for your specific environment:
```bash
# Collect 2+ minutes of CSI from your ESP32
python scripts/collect-training-data.py --port 5006 --duration 120
# Retrain (uses ruvllm, no PyTorch needed)
node scripts/train-ruvllm.js --data data/recordings/*.csi.jsonl
# Benchmark your retrained model
node scripts/benchmark-ruvllm.js --model models/csi-ruvllm
```
---
## Health & Wellness Applications
WiFi sensing can monitor health metrics without any wearable or camera:
```bash
# Sleep quality monitoring (run overnight)
node scripts/sleep-monitor.js --port 5006 --bind 192.168.1.20
# Breathing disorder pre-screening
node scripts/apnea-detector.js --port 5006 --bind 192.168.1.20
# Stress detection via heart rate variability
node scripts/stress-monitor.js --port 5006 --bind 192.168.1.20
# Walking analysis + tremor detection
node scripts/gait-analyzer.js --port 5006 --bind 192.168.1.20
# Replay on recorded data (no live hardware needed)
node scripts/sleep-monitor.js --replay data/recordings/*.csi.jsonl
```
> **Note:** These are pre-screening tools, not medical devices. Consult a healthcare professional for diagnosis.
---
## ruvllm Training Pipeline
All training uses **ruvllm** — a Rust-native ML runtime. No Python, no PyTorch, no GPU drivers required. Runs on any machine with Node.js.
@@ -1,9 +0,0 @@
@echo off
echo STARTING > C:\Users\ruv\idf_test.txt
set IDF_PATH=C:\Users\ruv\esp\v5.4\esp-idf
set PATH=C:\Espressif\tools\python\v5.4\venv\Scripts;C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20241119\xtensa-esp-elf\bin;C:\Espressif\tools\cmake\3.30.2\bin;C:\Espressif\tools\ninja\1.12.1;C:\Espressif\tools\idf-exe\1.0.3;%PATH%
echo PATH_SET >> C:\Users\ruv\idf_test.txt
cd /d C:\Users\ruv\Projects\wifi-densepose\firmware\esp32-csi-node
echo CD_DONE >> C:\Users\ruv\idf_test.txt
python %IDF_PATH%\tools\idf.py build >> C:\Users\ruv\idf_test.txt 2>&1
echo RC=%ERRORLEVEL% >> C:\Users\ruv\idf_test.txt
+2 -8
View File
@@ -118,14 +118,8 @@ esp_err_t display_task_start(void)
if (!buf1 || !buf2) {
ESP_LOGE(TAG, "Failed to allocate LVGL buffers (%u bytes, caps=0x%lx)",
(unsigned)buf_size, (unsigned long)alloc_caps);
if (buf1) {
free(buf1);
buf1 = NULL;
}
if (buf2) {
free(buf2);
buf2 = NULL;
}
if (buf1) free(buf1);
if (buf2) free(buf2);
return ESP_OK;
}
ESP_LOGI(TAG, "LVGL buffers: 2x %u bytes (%u lines, %s)",
+6 -11
View File
@@ -41,7 +41,6 @@ static const char *TAG = "edge_proc";
* ====================================================================== */
static edge_ring_buf_t s_ring;
static uint32_t s_ring_drops; /* Frames dropped due to full ring buffer. */
/* Scratch buffers for BPM estimation — moved from stack to static to avoid
* stack overflow. process_frame + update_multi_person_vitals combined used
@@ -54,7 +53,6 @@ static inline bool ring_push(const uint8_t *iq, uint16_t len,
{
uint32_t next = (s_ring.head + 1) % EDGE_RING_SLOTS;
if (next == s_ring.tail) {
s_ring_drops++;
return false; /* Full — drop frame. */
}
@@ -860,13 +858,12 @@ static void process_frame(const edge_ring_slot_t *slot)
if ((s_frame_count % 200) == 0) {
ESP_LOGI(TAG, "Vitals: br=%.1f hr=%.1f motion=%.4f pres=%s "
"fall=%s persons=%u frames=%lu drops=%lu",
"fall=%s persons=%u frames=%lu",
s_breathing_bpm, s_heartrate_bpm, s_motion_energy,
s_presence_detected ? "YES" : "no",
s_fall_detected ? "YES" : "no",
(unsigned)s_latest_pkt.n_persons,
(unsigned long)s_frame_count,
(unsigned long)s_ring_drops);
(unsigned long)s_frame_count);
}
}
@@ -916,19 +913,17 @@ static void edge_task(void *arg)
while (processed < EDGE_BATCH_LIMIT && ring_pop(&slot)) {
process_frame(&slot);
processed++;
/* 1-tick yield between frames within a batch. */
/* Yield after every frame to feed the Core 1 watchdog. */
vTaskDelay(1);
}
if (processed > 0) {
/* Post-batch yield: ~20 ms so IDLE1 can run and feed the
* Core 1 watchdog even under sustained load. Uses pdMS_TO_TICKS
* for tick-rate independence (minimum 1 tick). */
* Core 1 watchdog even under sustained load. */
{ TickType_t d = pdMS_TO_TICKS(20); vTaskDelay(d > 0 ? d : 1); }
} else {
/* No frames available — sleep one full tick.
* NOTE: pdMS_TO_TICKS(5) == 0 at 100 Hz, which would busy-spin. */
vTaskDelay(1);
/* No frames available — yield briefly. */
vTaskDelay(pdMS_TO_TICKS(1));
}
}
}
-1
View File
@@ -7769,7 +7769,6 @@ dependencies = [
"chrono",
"clap",
"futures-util",
"ruvector-mincut",
"serde",
"serde_json",
"tempfile",
@@ -43,8 +43,8 @@ clap = { workspace = true }
# Multi-BSSID WiFi scanning pipeline (ADR-022 Phase 3)
wifi-densepose-wifiscan = { version = "0.3.0", path = "../wifi-densepose-wifiscan" }
# Signal processing with RuvSense pose tracker (accuracy sprint)
wifi-densepose-signal = { version = "0.3.0", path = "../wifi-densepose-signal" }
# RuVector graph min-cut for person separation (ADR-068)
ruvector-mincut = { workspace = true }
[dev-dependencies]
tempfile = "3.10"
@@ -9,11 +9,8 @@
//! Replaces both ws_server.py and the Python HTTP server.
mod adaptive_classifier;
mod field_bridge;
mod multistatic_bridge;
mod rvf_container;
mod rvf_pipeline;
mod tracker_bridge;
mod vital_signs;
// Training pipeline modules (exposed via lib.rs)
@@ -56,11 +53,6 @@ use wifi_densepose_wifiscan::{
};
use wifi_densepose_wifiscan::parse_netsh_output as parse_netsh_bssid_output;
// Accuracy sprint: Kalman tracker, multistatic fusion, field model
use wifi_densepose_signal::ruvsense::pose_tracker::PoseTracker;
use wifi_densepose_signal::ruvsense::multistatic::{MultistaticFuser, MultistaticConfig};
use wifi_densepose_signal::ruvsense::field_model::{FieldModel, CalibrationStatus};
// ── CLI ──────────────────────────────────────────────────────────────────────
#[derive(Parser, Debug)]
@@ -153,14 +145,6 @@ struct Args {
/// Build fingerprint index from embeddings (env|activity|temporal|person)
#[arg(long, value_name = "TYPE")]
build_index: Option<String>,
/// Node positions for multistatic fusion (format: "x,y,z;x,y,z;...")
#[arg(long, env = "SENSING_NODE_POSITIONS")]
node_positions: Option<String>,
/// Start field model calibration on boot (empty room required)
#[arg(long)]
calibrate: bool,
}
// ── Data types ───────────────────────────────────────────────────────────────
@@ -229,9 +213,6 @@ struct SensingUpdate {
/// Estimated person count from CSI feature heuristics (1-3 for single ESP32).
#[serde(skip_serializing_if = "Option::is_none")]
estimated_persons: Option<usize>,
/// Per-node feature breakdown for multi-node deployments.
#[serde(skip_serializing_if = "Option::is_none")]
node_features: Option<Vec<PerNodeFeatureInfo>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -299,9 +280,9 @@ struct BoundingBox {
/// Each ESP32 node gets its own frame history, smoothing buffers, and vital
/// sign detector so that data from different nodes is never mixed.
struct NodeState {
pub(crate) frame_history: VecDeque<Vec<f64>>,
frame_history: VecDeque<Vec<f64>>,
smoothed_person_score: f64,
pub(crate) prev_person_count: usize,
prev_person_count: usize,
smoothed_motion: f64,
current_motion_level: String,
debounce_counter: u32,
@@ -317,7 +298,7 @@ struct NodeState {
rssi_history: VecDeque<f64>,
vital_detector: VitalSignDetector,
latest_vitals: VitalSigns,
pub(crate) last_frame_time: Option<std::time::Instant>,
last_frame_time: Option<std::time::Instant>,
edge_vitals: Option<Esp32VitalsPacket>,
/// Latest extracted features for cross-node fusion.
latest_features: Option<FeatureInfo>,
@@ -344,7 +325,7 @@ const MAX_BONE_CHANGE_RATIO: f64 = 0.20;
const COHERENCE_WINDOW: usize = 20;
impl NodeState {
pub(crate) fn new() -> Self {
fn new() -> Self {
Self {
frame_history: VecDeque::new(),
smoothed_person_score: 0.0,
@@ -408,18 +389,6 @@ impl NodeState {
}
}
/// Per-node feature info for WebSocket broadcasts (multi-node support).
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PerNodeFeatureInfo {
node_id: u8,
features: FeatureInfo,
classification: ClassificationInfo,
rssi_dbm: f64,
last_seen_ms: u64,
frame_rate_hz: f64,
stale: bool,
}
/// Shared application state
struct AppStateInner {
latest_update: Option<SensingUpdate>,
@@ -513,15 +482,6 @@ struct AppStateInner {
/// Per-node sensing state for multi-node deployments.
/// Keyed by `node_id` from the ESP32 frame header.
node_states: HashMap<u8, NodeState>,
// ── Accuracy sprint: Kalman tracker, multistatic fusion, eigenvalue counting ──
/// Global Kalman-based pose tracker for stable person IDs and smoothed keypoints.
pose_tracker: PoseTracker,
/// Instant of last tracker update (for computing dt).
last_tracker_instant: Option<std::time::Instant>,
/// Attention-weighted multi-node CSI fusion engine.
multistatic_fuser: MultistaticFuser,
/// SVD-based room field model for eigenvalue person counting (None until calibration).
field_model: Option<FieldModel>,
}
/// If no ESP32 frame arrives within this duration, source reverts to offline.
@@ -531,31 +491,6 @@ impl AppStateInner {
/// Return the effective data source, accounting for ESP32 frame timeout.
/// If the source is "esp32" but no frame has arrived in 5 seconds, returns
/// "esp32:offline" so the UI can distinguish active vs stale connections.
/// Person count: eigenvalue-based if field model is calibrated, else heuristic.
/// Uses global frame_history if populated, otherwise the freshest per-node history.
fn person_count(&self) -> usize {
match self.field_model.as_ref() {
Some(fm) => {
// Prefer global frame_history (populated by wifi/simulate paths).
// Fall back to freshest per-node history (populated by ESP32 paths).
let history = if !self.frame_history.is_empty() {
&self.frame_history
} else {
// Find the node with the most recent frame
self.node_states.values()
.filter(|ns| !ns.frame_history.is_empty())
.max_by_key(|ns| ns.last_frame_time)
.map(|ns| &ns.frame_history)
.unwrap_or(&self.frame_history)
};
field_bridge::occupancy_or_fallback(
fm, history, self.smoothed_person_score, self.prev_person_count,
)
}
None => score_to_person_count(self.smoothed_person_score, self.prev_person_count),
}
}
fn effective_source(&self) -> String {
if self.source == "esp32" {
if let Some(last) = self.last_esp32_frame {
@@ -704,13 +639,12 @@ fn parse_esp32_frame(buf: &[u8]) -> Option<Esp32Frame> {
// [20..] I/Q data
let node_id = buf[4];
let n_antennas = buf[5];
let n_subcarriers = buf[6];
let freq_mhz = u16::from_le_bytes([buf[8], buf[9]]);
let sequence = u32::from_le_bytes([buf[10], buf[11], buf[12], buf[13]]);
let rssi_raw = buf[14] as i8;
// Fix RSSI sign: ensure it's always negative (dBm convention).
let rssi = if rssi_raw > 0 { rssi_raw.saturating_neg() } else { rssi_raw };
let noise_floor = buf[15] as i8;
let n_subcarriers_u16 = u16::from_le_bytes([buf[6], buf[7]]);
let n_subcarriers = n_subcarriers_u16 as u8; // truncate to u8 for Esp32Frame compat
let freq_mhz = u16::from_le_bytes([buf[8], buf[9]]); // low 16 bits of u32
let sequence = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]);
let rssi = buf[16] as i8; // #332: was buf[14], 2 bytes off
let noise_floor = buf[17] as i8; // #332: was buf[15], 2 bytes off
let iq_start = 20;
let n_pairs = n_antennas as usize * n_subcarriers as usize;
@@ -1612,7 +1546,7 @@ async fn windows_wifi_task(state: SharedState, tick_ms: u64) {
let raw_score = compute_person_score(&features);
s.smoothed_person_score = s.smoothed_person_score * 0.90 + raw_score * 0.10;
let est_persons = if classification.presence {
let count = s.person_count();
let count = score_to_person_count(s.smoothed_person_score, s.prev_person_count);
s.prev_person_count = count;
count
} else {
@@ -1649,16 +1583,12 @@ async fn windows_wifi_task(state: SharedState, tick_ms: u64) {
model_status: None,
persons: None,
estimated_persons: if est_persons > 0 { Some(est_persons) } else { None },
node_features: None,
};
// Populate persons from the sensing update (Kalman-smoothed via tracker).
let raw_persons = derive_pose_from_sensing(&update);
let tracked = tracker_bridge::tracker_update(
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
);
if !tracked.is_empty() {
update.persons = Some(tracked);
// Populate persons from the sensing update.
let persons = derive_pose_from_sensing(&update);
if !persons.is_empty() {
update.persons = Some(persons);
}
if let Ok(json) = serde_json::to_string(&update) {
@@ -1749,7 +1679,7 @@ async fn windows_wifi_fallback_tick(state: &SharedState, seq: u32) {
let raw_score = compute_person_score(&features);
s.smoothed_person_score = s.smoothed_person_score * 0.90 + raw_score * 0.10;
let est_persons = if classification.presence {
let count = s.person_count();
let count = score_to_person_count(s.smoothed_person_score, s.prev_person_count);
s.prev_person_count = count;
count
} else {
@@ -1786,15 +1716,11 @@ async fn windows_wifi_fallback_tick(state: &SharedState, seq: u32) {
model_status: None,
persons: None,
estimated_persons: if est_persons > 0 { Some(est_persons) } else { None },
node_features: None,
};
let raw_persons = derive_pose_from_sensing(&update);
let tracked = tracker_bridge::tracker_update(
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
);
if !tracked.is_empty() {
update.persons = Some(tracked);
let persons = derive_pose_from_sensing(&update);
if !persons.is_empty() {
update.persons = Some(persons);
}
if let Ok(json) = serde_json::to_string(&update) {
@@ -1971,13 +1897,9 @@ async fn handle_ws_pose_client(mut socket: WebSocket, state: SharedState) {
keypoints,
zone: "zone_1".into(),
}]
}).unwrap_or_else(|| {
// Prefer tracked persons from broadcast if available
sensing.persons.clone().unwrap_or_else(|| derive_pose_from_sensing(&sensing))
})
}).unwrap_or_else(|| derive_pose_from_sensing(&sensing))
} else {
// Prefer tracked persons from broadcast if available
sensing.persons.clone().unwrap_or_else(|| derive_pose_from_sensing(&sensing))
derive_pose_from_sensing(&sensing)
};
let pose_msg = serde_json::json!({
@@ -2676,7 +2598,7 @@ async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> {
async fn pose_current(State(state): State<SharedState>) -> Json<serde_json::Value> {
let s = state.read().await;
let persons = match &s.latest_update {
Some(update) => update.persons.clone().unwrap_or_else(|| derive_pose_from_sensing(update)),
Some(update) => derive_pose_from_sensing(update),
None => vec![],
};
Json(serde_json::json!({
@@ -3227,88 +3149,6 @@ async fn adaptive_unload(State(state): State<SharedState>) -> Json<serde_json::V
Json(serde_json::json!({ "success": true, "message": "Adaptive model unloaded." }))
}
// ── Field model calibration endpoints (eigenvalue person counting) ──────────
async fn calibration_start(State(state): State<SharedState>) -> Json<serde_json::Value> {
let mut s = state.write().await;
// Guard: don't discard an in-progress or fresh calibration
if let Some(ref fm) = s.field_model {
match fm.status() {
CalibrationStatus::Collecting => {
return Json(serde_json::json!({
"success": false,
"error": "Calibration already in progress. Call /calibration/stop first.",
"frame_count": fm.calibration_frame_count(),
}));
}
CalibrationStatus::Fresh => {
return Json(serde_json::json!({
"success": false,
"error": "A fresh calibration already exists. Call /calibration/stop or wait for expiry.",
}));
}
_ => {} // Stale/Expired/Uncalibrated — ok to recalibrate
}
}
match FieldModel::new(field_bridge::single_link_config()) {
Ok(fm) => {
s.field_model = Some(fm);
Json(serde_json::json!({
"success": true,
"message": "Calibration started — keep room empty while frames accumulate.",
}))
}
Err(e) => Json(serde_json::json!({
"success": false,
"error": format!("{e}"),
})),
}
}
async fn calibration_stop(State(state): State<SharedState>) -> Json<serde_json::Value> {
let mut s = state.write().await;
if let Some(ref mut fm) = s.field_model {
let ts = chrono::Utc::now().timestamp_micros() as u64;
match fm.finalize_calibration(ts, 0) {
Ok(modes) => {
let baseline = modes.baseline_eigenvalue_count;
let variance_explained = modes.variance_explained;
info!("Field model calibrated: baseline_eigenvalues={baseline}, variance_explained={variance_explained:.2}");
Json(serde_json::json!({
"success": true,
"baseline_eigenvalue_count": baseline,
"variance_explained": variance_explained,
"frame_count": fm.calibration_frame_count(),
}))
}
Err(e) => Json(serde_json::json!({
"success": false,
"error": format!("{e}"),
})),
}
} else {
Json(serde_json::json!({
"success": false,
"error": "No field model active — call /calibration/start first.",
}))
}
}
async fn calibration_status(State(state): State<SharedState>) -> Json<serde_json::Value> {
let s = state.read().await;
match s.field_model.as_ref() {
Some(fm) => Json(serde_json::json!({
"active": true,
"status": format!("{:?}", fm.status()),
"frame_count": fm.calibration_frame_count(),
})),
None => Json(serde_json::json!({
"active": false,
"status": "none",
})),
}
}
/// Generate a simple timestamp string (epoch seconds) for recording IDs.
fn chrono_timestamp() -> u64 {
std::time::SystemTime::now()
@@ -3455,34 +3295,6 @@ async fn sona_activate(
}
}
/// GET /api/v1/nodes — per-node health and feature info.
async fn nodes_endpoint(State(state): State<SharedState>) -> Json<serde_json::Value> {
let s = state.read().await;
let now = std::time::Instant::now();
let nodes: Vec<serde_json::Value> = s.node_states.iter()
.map(|(&id, ns)| {
let elapsed_ms = ns.last_frame_time
.map(|t| now.duration_since(t).as_millis() as u64)
.unwrap_or(999999);
let stale = elapsed_ms > 5000;
let status = if stale { "stale" } else { "active" };
let rssi = ns.rssi_history.back().copied().unwrap_or(-90.0);
serde_json::json!({
"node_id": id,
"status": status,
"last_seen_ms": elapsed_ms,
"rssi_dbm": rssi,
"motion_level": &ns.current_motion_level,
"person_count": ns.prev_person_count,
})
})
.collect();
Json(serde_json::json!({
"nodes": nodes,
"total": nodes.len(),
}))
}
async fn info_page() -> Html<String> {
Html(format!(
"<html><body>\
@@ -3574,33 +3386,15 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
else if vitals.presence { 0.3 }
else { 0.05 };
// Aggregate person count: gate on presence first (matching WiFi path).
// Aggregate person count across all active nodes.
// Use max (not sum) because nodes in the same room see the
// same people — summing would double-count.
let now = std::time::Instant::now();
let total_persons = if vitals.presence {
let (fused, fallback_count) = multistatic_bridge::fuse_or_fallback(
&s.multistatic_fuser, &s.node_states,
);
match fused {
Some(ref f) => {
let score = multistatic_bridge::compute_person_score_from_amplitudes(&f.fused_amplitude);
s.smoothed_person_score = s.smoothed_person_score * 0.90 + score * 0.10;
let count = s.person_count();
s.prev_person_count = count;
count.max(1) // presence=true => at least 1
}
None => fallback_count.unwrap_or(0).max(1),
}
} else {
s.prev_person_count = 0;
0
};
// Feed field model calibration if active (use per-node history for ESP32).
if let Some(ref mut fm) = s.field_model {
if let Some(ns) = s.node_states.get(&node_id) {
field_bridge::maybe_feed_calibration(fm, &ns.frame_history);
}
}
let total_persons: usize = s.node_states.values()
.filter(|n| n.last_frame_time.map_or(false, |t| now.duration_since(t).as_secs() < 10))
.map(|n| n.prev_person_count)
.max()
.unwrap_or(0);
// Build nodes array with all active nodes.
let active_nodes: Vec<NodeInfo> = s.node_states.iter()
@@ -3677,15 +3471,17 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
model_status: None,
persons: None,
estimated_persons: if total_persons > 0 { Some(total_persons) } else { None },
node_features: None,
};
let raw_persons = derive_pose_from_sensing(&update);
let tracked = tracker_bridge::tracker_update(
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
);
if !tracked.is_empty() {
update.persons = Some(tracked);
let mut persons = derive_pose_from_sensing(&update);
// RuVector Phase 2: temporal smoothing + coherence gating
{
let ns = s.node_states.entry(node_id).or_insert_with(NodeState::new);
ns.update_coherence(vitals.motion_energy as f64);
apply_temporal_smoothing(&mut persons, ns);
}
if !persons.is_empty() {
update.persons = Some(persons);
}
if let Ok(json) = serde_json::to_string(&update) {
@@ -3822,32 +3618,23 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
else if classification.motion_level == "present_still" { 0.3 }
else { 0.05 };
// Aggregate person count: gate on presence first (matching WiFi path).
// Aggregate person count across all active nodes.
// Use max (not sum) because nodes in the same room see the
// same people — summing would double-count.
let now = std::time::Instant::now();
let total_persons = if classification.presence {
let (fused, fallback_count) = multistatic_bridge::fuse_or_fallback(
&s.multistatic_fuser, &s.node_states,
);
match fused {
Some(ref f) => {
let score = multistatic_bridge::compute_person_score_from_amplitudes(&f.fused_amplitude);
s.smoothed_person_score = s.smoothed_person_score * 0.90 + score * 0.10;
let count = s.person_count();
s.prev_person_count = count;
count.max(1)
}
None => fallback_count.unwrap_or(0).max(1),
}
} else {
s.prev_person_count = 0;
0
};
let total_persons: usize = s.node_states.values()
.filter(|n| n.last_frame_time.map_or(false, |t| now.duration_since(t).as_secs() < 10))
.map(|n| n.prev_person_count)
.max()
.unwrap_or(0);
// Feed field model calibration if active (use per-node history for ESP32).
if let Some(ref mut fm) = s.field_model {
if let Some(ns) = s.node_states.get(&node_id) {
field_bridge::maybe_feed_calibration(fm, &ns.frame_history);
}
// Boost classification confidence with multi-node coverage.
let n_active = s.node_states.values()
.filter(|ns| ns.last_frame_time.map_or(false, |t| now.duration_since(t).as_secs() < 10))
.count();
if n_active > 1 {
classification.confidence = (classification.confidence
* (1.0 + 0.15 * (n_active as f64 - 1.0))).clamp(0.0, 1.0);
}
// Build nodes array with all active nodes.
@@ -3887,15 +3674,17 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
model_status: None,
persons: None,
estimated_persons: if total_persons > 0 { Some(total_persons) } else { None },
node_features: None,
};
let raw_persons = derive_pose_from_sensing(&update);
let tracked = tracker_bridge::tracker_update(
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
);
if !tracked.is_empty() {
update.persons = Some(tracked);
let mut persons = derive_pose_from_sensing(&update);
// RuVector Phase 2: temporal smoothing + coherence gating
{
let ns = s.node_states.entry(node_id).or_insert_with(NodeState::new);
ns.update_coherence(features.motion_band_power);
apply_temporal_smoothing(&mut persons, ns);
}
if !persons.is_empty() {
update.persons = Some(persons);
}
if let Ok(json) = serde_json::to_string(&update) {
@@ -3975,7 +3764,7 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
let raw_score = compute_person_score(&features);
s.smoothed_person_score = s.smoothed_person_score * 0.90 + raw_score * 0.10;
let est_persons = if classification.presence {
let count = s.person_count();
let count = score_to_person_count(s.smoothed_person_score, s.prev_person_count);
s.prev_person_count = count;
count
} else {
@@ -4022,16 +3811,12 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
},
persons: None,
estimated_persons: if est_persons > 0 { Some(est_persons) } else { None },
node_features: None,
};
// Populate persons from the sensing update (Kalman-smoothed via tracker).
let raw_persons = derive_pose_from_sensing(&update);
let tracked = tracker_bridge::tracker_update(
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
);
if !tracked.is_empty() {
update.persons = Some(tracked);
// Populate persons from the sensing update.
let persons = derive_pose_from_sensing(&update);
if !persons.is_empty() {
update.persons = Some(persons);
}
if update.classification.presence {
@@ -4660,29 +4445,6 @@ async fn main() {
m
}),
node_states: HashMap::new(),
// Accuracy sprint
pose_tracker: PoseTracker::new(),
last_tracker_instant: None,
multistatic_fuser: {
let mut fuser = MultistaticFuser::with_config(MultistaticConfig {
min_nodes: 1, // single-node passthrough
..Default::default()
});
if let Some(ref pos_str) = args.node_positions {
let positions = field_bridge::parse_node_positions(pos_str);
if !positions.is_empty() {
info!("Configured {} node positions for multistatic fusion", positions.len());
fuser.set_node_positions(positions);
}
}
fuser
},
field_model: if args.calibrate {
info!("Field model calibration enabled — room should be empty during startup");
FieldModel::new(field_bridge::single_link_config()).ok()
} else {
None
},
}));
// Start background tasks based on source
@@ -4736,8 +4498,6 @@ async fn main() {
.route("/api/v1/metrics", get(health_metrics))
// Sensing endpoints
.route("/api/v1/sensing/latest", get(latest))
// Per-node health endpoint
.route("/api/v1/nodes", get(nodes_endpoint))
// Vital sign endpoints
.route("/api/v1/vital-signs", get(vital_signs_endpoint))
.route("/api/v1/edge-vitals", get(edge_vitals_endpoint))
@@ -4779,10 +4539,6 @@ async fn main() {
.route("/api/v1/adaptive/train", post(adaptive_train))
.route("/api/v1/adaptive/status", get(adaptive_status))
.route("/api/v1/adaptive/unload", post(adaptive_unload))
// Field model calibration (eigenvalue-based person counting)
.route("/api/v1/calibration/start", post(calibration_start))
.route("/api/v1/calibration/stop", post(calibration_stop))
.route("/api/v1/calibration/status", get(calibration_status))
// Static UI files
.nest_service("/ui", ServeDir::new(&ui_path))
.layer(SetResponseHeaderLayer::overriding(
-235
View File
@@ -1,235 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* Deep RF Intelligence Report discovers everything WiFi can see.
* Usage: node scripts/deep-scan.js --bind 192.168.1.20 --duration 10
*/
const dgram = require('dgram');
const { parseArgs } = require('util');
const { values: args } = parseArgs({
options: {
port: { type: 'string', default: '5006' },
bind: { type: 'string', default: '0.0.0.0' },
duration: { type: 'string', default: '10' },
},
strict: true,
});
const PORT = parseInt(args.port);
const BIND = args.bind;
const DUR = parseInt(args.duration) * 1000;
const vitals = {}; // nid -> [{time, br, hr, rssi, persons, motion, presence}]
const features = {}; // nid -> [{time, features}]
const raw = {}; // nid -> [{time, amps, phases, rssi, nSub}]
const server = dgram.createSocket('udp4');
server.on('message', (buf, rinfo) => {
if (buf.length < 5) return;
const magic = buf.readUInt32LE(0);
const nid = buf[4];
if (magic === 0xC5110001 && buf.length > 20) {
const iq = buf.subarray(20);
const nSub = Math.floor(iq.length / 2);
const amps = [];
for (let i = 0; i < nSub * 2 && i < iq.length - 1; i += 2) {
const I = iq.readInt8(i), Q = iq.readInt8(i + 1);
amps.push(Math.sqrt(I * I + Q * Q));
}
if (!raw[nid]) raw[nid] = [];
raw[nid].push({ time: Date.now(), amps, rssi: buf.readInt8(5), nSub });
} else if (magic === 0xC5110002 && buf.length >= 32) {
const br = buf.readUInt16LE(6) / 100;
const hr = buf.readUInt32LE(8) / 10000;
const rssi = buf.readInt8(12);
const persons = buf[13];
const motion = buf.readFloatLE(16);
const presence = buf.readFloatLE(20);
if (!vitals[nid]) vitals[nid] = [];
vitals[nid].push({ time: Date.now(), br, hr, rssi, persons, motion, presence });
} else if (magic === 0xC5110003 && buf.length >= 48) {
const f = [];
for (let i = 0; i < 8; i++) f.push(buf.readFloatLE(16 + i * 4));
if (!features[nid]) features[nid] = [];
features[nid].push({ time: Date.now(), features: f });
}
});
server.on('listening', () => {
console.log(`Scanning on ${BIND}:${PORT} for ${DUR / 1000}s...\n`);
});
server.bind(PORT, BIND);
setTimeout(() => {
server.close();
report();
}, DUR);
function avg(arr) { return arr.length ? arr.reduce((a, b) => a + b) / arr.length : 0; }
function std(arr) { const m = avg(arr); return Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length || 1)); }
function report() {
const bar = (v, max = 20) => '█'.repeat(Math.min(Math.round(v * max), max)) + '░'.repeat(Math.max(max - Math.round(v * max), 0));
const line = '═'.repeat(70);
console.log(line);
console.log(' DEEP RF INTELLIGENCE REPORT — What WiFi Sees In Your Room');
console.log(line);
// 1. WHO'S THERE
console.log('\n📡 WHO IS IN THE ROOM');
for (const nid of Object.keys(vitals).sort()) {
const v = vitals[nid];
const lastP = v[v.length - 1].presence;
const avgMotion = avg(v.map(x => x.motion));
console.log(` Node ${nid}: presence=${lastP.toFixed(1)} motion=${avgMotion.toFixed(1)}${lastP > 0.5 ? 'SOMEONE IS HERE' : 'Room may be empty'}`);
}
// 2. WHAT ARE THEY DOING
console.log('\n🏃 ACTIVITY DETECTION');
for (const nid of Object.keys(vitals).sort()) {
const v = vitals[nid];
const motions = v.map(x => x.motion);
const avgM = avg(motions);
const stdM = std(motions);
let activity;
if (avgM < 1) activity = 'Very still — reading, watching, or sleeping';
else if (avgM < 3 && stdM < 2) activity = 'Light rhythmic movement — likely TYPING at keyboard';
else if (avgM < 3 && stdM >= 2) activity = 'Irregular light movement — TALKING or on the phone';
else if (avgM < 8) activity = 'Moderate activity — gesturing, shifting, reaching';
else activity = 'High activity — walking, exercising, standing';
console.log(` Node ${nid}: energy=${avgM.toFixed(1)} variability=${stdM.toFixed(1)}${activity}`);
}
// 3. VITAL SIGNS
console.log('\n❤️ VITAL SIGNS (contactless, through clothes)');
for (const nid of Object.keys(vitals).sort()) {
const v = vitals[nid];
const brs = v.map(x => x.br);
const hrs = v.map(x => x.hr);
const brAvg = avg(brs), brStd = std(brs);
const hrAvg = avg(hrs), hrStd = std(hrs);
let brState = brStd < 2 ? 'very regular (calm/focused)' : brStd < 5 ? 'normal' : 'variable (talking/active)';
let hrState = hrAvg < 60 ? 'athletic resting' : hrAvg < 80 ? 'relaxed' : hrAvg < 100 ? 'normal/active' : 'elevated';
let stressHint = hrStd < 3 ? 'LOW stress (steady HR)' : hrStd < 8 ? 'MODERATE' : 'HIGH variability (could be relaxed OR stressed)';
console.log(` Node ${nid}:`);
console.log(` Breathing: ${brAvg.toFixed(0)} BPM (±${brStd.toFixed(1)}) — ${brState}`);
console.log(` Heart rate: ${hrAvg.toFixed(0)} BPM (±${hrStd.toFixed(1)}) — ${hrState}`);
console.log(` Stress indicator: ${stressHint}`);
}
// 4. YOUR DISTANCE FROM EACH NODE
console.log('\n📏 POSITION IN ROOM');
const distances = {};
for (const nid of Object.keys(vitals).sort()) {
const rssis = vitals[nid].map(x => x.rssi);
const avgRssi = avg(rssis);
const dist = Math.pow(10, (-30 - avgRssi) / 20);
distances[nid] = dist;
console.log(` Node ${nid}: RSSI=${avgRssi.toFixed(0)} dBm → ~${dist.toFixed(1)}m away`);
}
const nids = Object.keys(distances).sort();
if (nids.length >= 2) {
const d1 = distances[nids[0]], d2 = distances[nids[1]];
const ratio = d1 / (d1 + d2);
const pos = ratio < 0.4 ? 'closer to Node ' + nids[0] : ratio > 0.6 ? 'closer to Node ' + nids[1] : 'CENTERED between nodes';
console.log(` Position: ${pos} (ratio: ${(ratio * 100).toFixed(0)}%)`);
}
// 5. OBJECTS IN THE ROOM (from subcarrier nulls)
console.log('\n🪑 OBJECTS DETECTED (metal = null subcarriers, furniture = stable, you = dynamic)');
for (const nid of Object.keys(raw).sort()) {
const frames = raw[nid];
if (!frames.length) continue;
const nSub = frames[0].nSub;
// Compute per-subcarrier variance
const ampMeans = new Float64Array(nSub);
const ampVars = new Float64Array(nSub);
for (const f of frames) {
for (let i = 0; i < Math.min(nSub, f.amps.length); i++) ampMeans[i] += f.amps[i];
}
for (let i = 0; i < nSub; i++) ampMeans[i] /= frames.length;
for (const f of frames) {
for (let i = 0; i < Math.min(nSub, f.amps.length); i++) ampVars[i] += (f.amps[i] - ampMeans[i]) ** 2;
}
for (let i = 0; i < nSub; i++) ampVars[i] = Math.sqrt(ampVars[i] / frames.length);
let nullCount = 0, dynamicCount = 0, staticCount = 0;
const overallMean = ampMeans.reduce((a, b) => a + b) / nSub;
for (let i = 0; i < nSub; i++) {
if (ampMeans[i] < overallMean * 0.15) nullCount++;
else if (ampVars[i] > 1.0) dynamicCount++;
else staticCount++;
}
console.log(` Node ${nid} (${nSub} subcarriers, ${frames.length} frames):`);
console.log(` 🔩 Metal objects: ${nullCount} null subcarriers (${(100 * nullCount / nSub).toFixed(0)}%) — desk frame, monitor bezel, laptop chassis`);
console.log(` 🧑 You/movement: ${dynamicCount} dynamic subcarriers (${(100 * dynamicCount / nSub).toFixed(0)}%) — person + micro-movements`);
console.log(` 🧱 Walls/furniture: ${staticCount} static (${(100 * staticCount / nSub).toFixed(0)}%) — walls, ceiling, wooden furniture`);
}
// 6. ELECTRONICS DETECTED
console.log('\n💻 ELECTRONICS (from WiFi network scan perspective)');
console.log(' Known devices transmitting WiFi in range:');
console.log(' • Your router (ruv.net) — strongest signal, channel 5');
console.log(' • HP M255 LaserJet — WiFi Direct on channel 5, ~2m away');
console.log(' • Cognitum Seed — if plugged in (Pi Zero 2W)');
console.log(' • 2x ESP32-S3 — the sensing nodes themselves');
console.log(' • Your laptop/desktop — connected to ruv.net');
console.log(' Neighbor devices (through walls):');
console.log(' • COGECO-21B20 (100% signal, ch 11) — very close neighbor');
console.log(' • conclusion mesh (44%, ch 3) — mesh network nearby');
console.log(' • NETGEAR72 (42%, ch 9) — another neighbor');
// 7. INVISIBLE PHYSICS
console.log('\n🔬 INVISIBLE PHYSICS');
for (const nid of Object.keys(raw).sort()) {
const frames = raw[nid];
if (frames.length < 2) continue;
// Phase stability = room stability
const first = frames[0], last = frames[frames.length - 1];
const nCommon = Math.min(first.amps.length, last.amps.length);
let phaseShift = 0;
for (let i = 0; i < nCommon; i++) {
const ampChange = Math.abs(last.amps[i] - first.amps[i]);
phaseShift += ampChange;
}
phaseShift /= nCommon;
const rssis = frames.map(f => f.rssi);
const rssiStd = std(rssis);
console.log(` Node ${nid}:`);
console.log(` Amplitude drift: ${phaseShift.toFixed(2)} over ${((last.time - first.time) / 1000).toFixed(0)}s — ${phaseShift < 1 ? 'STABLE environment' : phaseShift < 3 ? 'minor movement' : 'active changes'}`);
console.log(` RSSI stability: ±${rssiStd.toFixed(1)} dB — ${rssiStd < 2 ? 'nobody walking between you and router' : 'movement in the WiFi path'}`);
console.log(` Fresnel zones: ${nCommon > 100 ? '128+ subcarriers = 5cm resolution potential' : nCommon + ' subcarriers'}`);
}
// 8. FEATURE FINGERPRINT
console.log('\n🧬 YOUR RF FINGERPRINT RIGHT NOW');
for (const nid of Object.keys(features).sort()) {
const f = features[nid];
if (!f.length) continue;
const last = f[f.length - 1].features;
const names = ['Presence', 'Motion', 'Breathing', 'HeartRate', 'PhaseVar', 'Persons', 'Fall', 'RSSI'];
console.log(` Node ${nid}:`);
for (let i = 0; i < 8; i++) {
console.log(` ${names[i].padStart(10)}: ${bar(last[i])} ${last[i].toFixed(2)}`);
}
}
console.log(`\n${line}`);
console.log(' WiFi signals reveal: who, what they\'re doing, how they feel,');
console.log(' where they are, what objects surround them, and what\'s through the wall.');
console.log(' No cameras. No wearables. No microphones. Just radio physics.');
console.log(line);
}
+1 -4
View File
@@ -25,7 +25,6 @@ const { parseArgs } = require('util');
const { values: args } = parseArgs({
options: {
port: { type: 'string', short: 'p', default: '5006' },
bind: { type: 'string', short: 'b', default: '0.0.0.0' },
duration: { type: 'string', short: 'd' },
json: { type: 'boolean', default: false },
interval: { type: 'string', short: 'i', default: '2000' },
@@ -574,9 +573,7 @@ function main() {
}
});
// On Windows, binding to 0.0.0.0 may be blocked by firewall.
// Use --bind <ip> to specify your WiFi IP (e.g., --bind 192.168.1.20)
server.bind(PORT, args.bind);
server.bind(PORT);
// Periodic display update
const displayTimer = setInterval(display, INTERVAL_MS);
+1 -15
View File
@@ -391,16 +391,7 @@ def run_bridge(args):
# Open UDP listener
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
bind_addr = args.bind_addr
if bind_addr == "auto":
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("192.168.1.1", 80))
bind_addr = s.getsockname()[0]
s.close()
except Exception:
bind_addr = "0.0.0.0"
sock.bind((bind_addr, args.udp_port))
sock.bind(("0.0.0.0", args.udp_port))
sock.settimeout(1.0) # 1s timeout for responsive time-based flushing
log.info(
"Listening on UDP port %d (batch size: %d, flush interval: %.0fs)",
@@ -606,11 +597,6 @@ def main():
default=5006,
help="UDP port to listen on (default: 5006)",
)
parser.add_argument(
"--bind-addr",
default="auto",
help="Bind address for UDP listener (default: auto-detect WiFi IP; use 0.0.0.0 for all interfaces)",
)
parser.add_argument(
"--batch-size",
type=int,
+3 -7
View File
@@ -1257,13 +1257,9 @@ async function main() {
contrastiveResult.finalLoss = finalContrastiveLoss;
contrastiveResult.improvement = contrastiveImprovement;
// Export contrastive training data (skip for large datasets to avoid JSON string limit)
if (contrastiveTrainer.getTripletCount() < 100000) {
const contrastiveOutDir = contrastiveTrainer.exportTrainingData();
console.log(` Training data exported to: ${contrastiveOutDir}`);
} else {
console.log(` Skipping triplet export (${contrastiveTrainer.getTripletCount()} triplets too large for JSON)`);
}
// Export contrastive training data
const contrastiveOutDir = contrastiveTrainer.exportTrainingData();
console.log(` Training data exported to: ${contrastiveOutDir}`);
// -----------------------------------------------------------------------
// Phase 2: Task head training via TrainingPipeline