mirror of
https://github.com/ruvnet/RuView
synced 2026-07-20 17:03:24 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e20bed197b | |||
| 0824de7665 | |||
| e1843c047e | |||
| 3225eee5be | |||
| d2b2cbfc69 | |||
| 770788fc85 | |||
| 4d5bdb1570 | |||
| 8505662af4 | |||
| 8eb808de03 | |||
| ca3c58a69f | |||
| d5c457aa30 | |||
| b2e3f27fa1 | |||
| e39a35edee | |||
| f49ecb163f | |||
| c79543283b | |||
| 4ab69359ef | |||
| ae792aad0d | |||
| 898d90f689 | |||
| 0c512ed06e | |||
| f39d88e711 | |||
| de5dc9a151 | |||
| c1336c6672 | |||
| 6cb0859806 | |||
| 5ebd78e796 |
@@ -1,111 +0,0 @@
|
||||
# RuView Troubleshooting Guide
|
||||
|
||||
Known issues and fixes from the rebase-to-upstream branch (upstream #301).
|
||||
|
||||
---
|
||||
|
||||
## 1. Node not appearing in /api/v1/nodes
|
||||
|
||||
**Symptom:** ESP32-S3 node associates with WiFi, LED blinks, but no CSI frames arrive at the server. Node missing from `/api/v1/spatial/nodes`.
|
||||
|
||||
**Root cause:** After USB flash, the node enters a limping state where WiFi associates but the UDP CSI sender silently fails. The SoftAP + mDNS stack initializes but the CSI callback never fires.
|
||||
|
||||
**Fix:** Power cycle the node (unplug USB, wait 2s, replug). If that doesn't work, send DTR reset via serial: `python -m serial.tools.miniterm --dtr 0 COMx 115200` then Ctrl+C.
|
||||
|
||||
**Prevention:** Firmware 0.8.0+ includes a watchdog that detects zero CSI frames for 30s and triggers a software reset automatically. Nodes 1-10 are still on old firmware and lack this recovery (OTA-vs-BLE chicken-and-egg; see issue #6).
|
||||
|
||||
---
|
||||
|
||||
## 2. Person count stuck at 1
|
||||
|
||||
**Symptom:** `estimated_persons` always returns 1 regardless of how many people are in the room.
|
||||
|
||||
**Root cause (ADR-044):** Eight converging bugs:
|
||||
1. `score_to_person_count` had a ceiling of 3
|
||||
2. `fuse_multi_node_features` used `.max()` instead of sum — N identical readings collapsed to 1
|
||||
3. Four `.max(1)` clamps forced minimum count to 1 even when absent
|
||||
4. `field_model.estimate_occupancy` capped at `.min(3)`
|
||||
5. Normalization saturated (dividing by hardcoded thresholds instead of adaptive p95)
|
||||
6. No field model auto-calibration — eigenvalue path never activated
|
||||
7. Vitals-path clamps were asymmetric
|
||||
8. Tomography produced one blob (CC=1) so dedup gave wrong count
|
||||
|
||||
**Fix applied (Waves 1-3):**
|
||||
- Wave 1 (`9cc5f604`): ceiling 3→10, `.max()` → sum/3 aggregation, softened `.max(1)` clamps
|
||||
- Wave 2 (`306f1262`): RollingP95 adaptive normalization, field_model 30s auto-calibration, vitals clamp symmetry
|
||||
- Wave 3 (`c3df375a`+`0d4bfb09`+`6ac70ddf`): CC flood-fill infrastructure, lambda 0.1→5.0, threshold 0.01→0.15, CC>1 gate
|
||||
|
||||
**Current state:** `estimated_persons` = 6-8 for 5 bodies (3 humans + 2 dogs). Overcounts because the sum/3 dedup factor is a guess. Tomography still produces one blob (CC=1), so the CC path doesn't activate. Runtime-configurable lambda would help tune without redeployment.
|
||||
|
||||
---
|
||||
|
||||
## 3. Heart rate / breathing rate jitter
|
||||
|
||||
**Symptom:** HR and BR readings jump wildly between frames. BR CV was 23.3%, HR CV was 12.9%.
|
||||
|
||||
**Root cause (ADR-045):** 11 ESP32 nodes each compute independent vitals. The server used last-write-wins — whichever node's UDP packet arrived last overwrote the global vitals. At ~20 fps per node, this meant vitals randomly interleaved from different vantage points every 50ms.
|
||||
|
||||
**Fix applied (`46fbc061`):** Best-node selection. Each node's vitals are smoothed independently via median filter + EMA. The node with the highest combined `breathing_confidence + heartbeat_confidence` is selected as authoritative. Result: BR CV 23.3% → 12.6%, HR CV 12.9% → 11.6%.
|
||||
|
||||
**Known limitation:** The `wifi-densepose-vitals` crate has a superior 4-stage pipeline (bandpass → Hilbert envelope → autocorrelation → peak detection) but is not yet wired into the sensing server. The current `VitalSignDetector` uses a simpler FFT approach with 4 BPM frequency resolution.
|
||||
|
||||
---
|
||||
|
||||
## 4. Signal quality shows 50% always
|
||||
|
||||
**Symptom:** The dashboard signal quality gauge was always stuck at ~50%.
|
||||
|
||||
**Root cause:** Signal quality was a hardcoded placeholder value, not derived from actual CSI data.
|
||||
|
||||
**Fix applied:** ADR-044 Wave 2 replaced the fake gauge with RollingP95 adaptive normalization. The UI honesty pass (`b2070ab4`) added beta tags to unvalidated metrics, replaced the fake gauge with per-node pill indicators, and surfaced the actual per-node signal data.
|
||||
|
||||
---
|
||||
|
||||
## 5. Dashboard freezes every 2-4 seconds
|
||||
|
||||
**Symptom:** The spatial view and dashboard would freeze, then reconnect, creating a visible stutter every 2-4 seconds.
|
||||
|
||||
**Root cause:** The WebSocket broadcast channel's `recv()` returned `Err(Lagged)` when a client fell behind. The server treated this as a fatal error and dropped the connection. The client immediately reconnected, creating a connect/disconnect cycle.
|
||||
|
||||
**Fix applied (`581daf4f`):**
|
||||
- Server: `Lagged` error → `continue` (skip missed frames instead of disconnecting)
|
||||
- Server: 30s ping/pong keepalive to prevent Caddy proxy idle timeouts
|
||||
- Result: 154 frames over 8 seconds sustained, zero disconnects
|
||||
|
||||
---
|
||||
|
||||
## 6. OTA update crashes at 59%
|
||||
|
||||
**Symptom:** OTA firmware update via `/api/v1/firmware/download` progresses to ~59% then the node crashes with `StoreProhibited` on Core 1.
|
||||
|
||||
**Root cause:** NimBLE BLE advertising/scanning runs on Core 1. During OTA, the HTTP client also runs on Core 1. BLE and OTA compete for stack space, and the BLE scan callback triggers a memory access violation during the OTA write.
|
||||
|
||||
**Fix:**
|
||||
1. Stop NimBLE advertising and scanning before calling `esp_https_ota_begin()`
|
||||
2. Increase httpd stack from 4KB to 8KB (`CONFIG_HTTPD_MAX_REQ_HDR_LEN` and task stack)
|
||||
3. Resume BLE after OTA completes or fails
|
||||
|
||||
**Caveat:** Nodes running old firmware (1-10) can't receive this fix via OTA because the crash happens during the OTA itself. These nodes must be USB-flashed with firmware 0.8.0+ first, then future OTA updates will work. Node 11 was USB-flashed with the watchdog firmware and can receive OTA updates.
|
||||
|
||||
---
|
||||
|
||||
## 7. Can't SSH to babycube via LAN
|
||||
|
||||
**Symptom:** `ssh thyhack@10.0.10.10` hangs at banner exchange. Ping works, TCP port 22 is open, but SSH never completes the handshake.
|
||||
|
||||
**Workaround:** Use the Tailscale IP instead:
|
||||
```
|
||||
ssh thyhack@100.90.238.87
|
||||
```
|
||||
|
||||
**Not the cause:** CrowdSec. The 10.0.0.0/8 range is whitelisted in CrowdSec (`cscli decisions list` shows no active decisions for LAN IPs). The banner hang occurs before any authentication attempt, so it's not a firewall block.
|
||||
|
||||
**Suspected cause:** Unknown. Possibly MTU/fragmentation issue on the LAN segment, or a network stack bug in the babycube's NIC driver. The Tailscale overlay network (WireGuard UDP) bypasses whatever is causing the LAN TCP issue.
|
||||
|
||||
---
|
||||
|
||||
## 8. Right USB-C port doesn't work on some ESP32-S3 boards
|
||||
|
||||
**Symptom:** Plugging into the right USB-C port (when facing the board with USB-C toward you) shows no serial device on the host.
|
||||
|
||||
**Fix:** Use the left USB-C port. On most ESP32-S3-DevKitC boards, the left port is the USB-to-UART bridge (CP2102/CH340) used for flashing and serial monitor. The right port is the native USB (USB-JTAG) which requires different drivers and isn't used by the RuView firmware.
|
||||
@@ -103,20 +103,6 @@ Example: `docker run -e CSI_SOURCE=esp32 -p 3000:3000 -p 5005:5005/udp ruvnet/wi
|
||||
|
||||
### From Source (Rust)
|
||||
|
||||
On Debian/Ubuntu-based Linux systems, install the native desktop prerequisites before the first Rust release build:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
build-essential pkg-config \
|
||||
libglib2.0-dev libgtk-3-dev \
|
||||
libsoup-3.0-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libwebkit2gtk-4.1-dev
|
||||
```
|
||||
|
||||
This prepares the native GTK/WebKit dependencies used by the desktop/Tauri crates in this workspace.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ruvnet/RuView.git
|
||||
cd RuView/rust-port/wifi-densepose-rs
|
||||
@@ -1700,28 +1686,6 @@ rustup update stable
|
||||
rustc --version
|
||||
```
|
||||
|
||||
### Build: Linux native desktop prerequisites
|
||||
|
||||
If you are compiling the Rust workspace on a Debian/Ubuntu-based Linux system, install the native desktop development packages first:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
build-essential pkg-config \
|
||||
libglib2.0-dev libgtk-3-dev \
|
||||
libsoup-3.0-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libwebkit2gtk-4.1-dev
|
||||
```
|
||||
|
||||
Then rerun:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
This is the same Linux pre-step referenced in the Rust source build section and covers the common GTK/WebKit `pkg-config` requirements used by the desktop build.
|
||||
|
||||
### Windows: RSSI mode shows no data
|
||||
|
||||
Run the terminal as Administrator (required for `netsh wlan` access). Verified working on Windows 10 and 11 with Intel AX201 and Intel BE201 adapters.
|
||||
|
||||
@@ -18,3 +18,7 @@ clap = { version = "4", features = ["derive"] }
|
||||
chrono = "0.4"
|
||||
dirs = "5"
|
||||
reqwest = { version = "0.12", features = ["json"], default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
v4l = "0.14"
|
||||
jpeg-decoder = "0.3"
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
//! Camera capture — cross-platform frame grabber.
|
||||
//!
|
||||
//! macOS: uses `screencapture` or `ffmpeg -f avfoundation` for camera frames
|
||||
//! Linux: uses `v4l2-ctl` or `ffmpeg -f v4l2` for camera frames
|
||||
//! Both: capture to JPEG, decode to RGB, return raw pixel data
|
||||
//! Linux: direct V4L2 via `v4l` crate (no subprocess, no orphans)
|
||||
//! macOS: ffmpeg -f avfoundation (subprocess)
|
||||
//! Fallback: ffmpeg subprocess on all platforms
|
||||
#![allow(dead_code)]
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use std::process::Command;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
/// Captured frame with raw RGB data.
|
||||
pub struct Frame {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub rgb: Vec<u8>, // row-major [height * width * 3]
|
||||
pub rgb: Vec<u8>,
|
||||
pub timestamp_ms: i64,
|
||||
}
|
||||
|
||||
/// Camera source configuration.
|
||||
@@ -31,41 +33,139 @@ impl Default for CameraConfig {
|
||||
|
||||
/// Capture a single frame from the camera.
|
||||
///
|
||||
/// Tries multiple backends in order: ffmpeg, v4l2, imagesnap (macOS).
|
||||
/// On Linux: uses direct V4L2 (no subprocess, no orphans).
|
||||
/// On macOS: uses ffmpeg subprocess.
|
||||
pub fn capture_frame(config: &CameraConfig) -> Result<Frame> {
|
||||
let tmp = tmp_path();
|
||||
|
||||
// Try ffmpeg first (cross-platform)
|
||||
if let Ok(frame) = capture_ffmpeg(config, &tmp) {
|
||||
return Ok(frame);
|
||||
}
|
||||
|
||||
// Linux: try v4l2
|
||||
// Linux: direct V4L2 (preferred — no subprocess)
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Ok(frame) = capture_v4l2(config, &tmp) {
|
||||
{
|
||||
match capture_v4l2_direct(config) {
|
||||
Ok(frame) => return Ok(frame),
|
||||
Err(e) => eprintln!("[camera] V4L2 direct failed: {e}, falling back to ffmpeg"),
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: ffmpeg subprocess (with timeout to prevent orphans)
|
||||
let tmp = tmp_path();
|
||||
if let Ok(frame) = capture_ffmpeg_safe(config, &tmp) {
|
||||
return Ok(frame);
|
||||
}
|
||||
|
||||
// macOS: try screencapture (camera mode)
|
||||
// macOS: screencapture
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Ok(frame) = capture_macos(config, &tmp) {
|
||||
return Ok(frame);
|
||||
}
|
||||
|
||||
bail!("No camera backend available. Install ffmpeg or run on a machine with a camera.")
|
||||
bail!("No camera backend available")
|
||||
}
|
||||
|
||||
/// Capture via ffmpeg (works on Linux + macOS).
|
||||
fn capture_ffmpeg(config: &CameraConfig, tmp: &PathBuf) -> Result<Frame> {
|
||||
let input = if cfg!(target_os = "macos") {
|
||||
format!("{}:none", config.device_index) // avfoundation: video:audio
|
||||
// ============================================================
|
||||
// Linux: Direct V4L2 capture (no subprocess, no orphans)
|
||||
// ============================================================
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn capture_v4l2_direct(config: &CameraConfig) -> Result<Frame> {
|
||||
use v4l::buffer::Type;
|
||||
use v4l::io::mmap::Stream;
|
||||
use v4l::io::traits::CaptureStream;
|
||||
use v4l::video::Capture;
|
||||
use v4l::{Device, FourCC};
|
||||
|
||||
let device_path = format!("/dev/video{}", config.device_index);
|
||||
if !std::path::Path::new(&device_path).exists() {
|
||||
bail!("no camera at {device_path}");
|
||||
}
|
||||
|
||||
let dev = Device::with_path(&device_path)?;
|
||||
|
||||
// Try MJPG first (most webcams support it), fall back to YUYV
|
||||
let mut fmt = dev.format()?;
|
||||
fmt.width = config.width;
|
||||
fmt.height = config.height;
|
||||
fmt.fourcc = FourCC::new(b"MJPG");
|
||||
let use_mjpg = dev.set_format(&fmt).is_ok();
|
||||
|
||||
if !use_mjpg {
|
||||
fmt.fourcc = FourCC::new(b"YUYV");
|
||||
dev.set_format(&fmt)?;
|
||||
}
|
||||
|
||||
let fmt = dev.format()?;
|
||||
let actual_w = fmt.width;
|
||||
let actual_h = fmt.height;
|
||||
|
||||
// Stream one frame via mmap
|
||||
let mut stream = Stream::with_buffers(&dev, Type::VideoCapture, 2)?;
|
||||
let (buf, _meta) = stream.next()?;
|
||||
|
||||
let rgb = if use_mjpg {
|
||||
decode_mjpeg_to_rgb(buf, actual_w, actual_h)?
|
||||
} else {
|
||||
format!("/dev/video{}", config.device_index) // v4l2
|
||||
yuyv_to_rgb(buf, actual_w, actual_h)
|
||||
};
|
||||
|
||||
// Stream is dropped here — device released cleanly, no orphan process
|
||||
|
||||
Ok(Frame {
|
||||
width: actual_w,
|
||||
height: actual_h,
|
||||
rgb,
|
||||
timestamp_ms: chrono::Utc::now().timestamp_millis(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn decode_mjpeg_to_rgb(data: &[u8], _w: u32, _h: u32) -> Result<Vec<u8>> {
|
||||
// Use a minimal JPEG decoder
|
||||
let mut decoder = jpeg_decoder::Decoder::new(std::io::Cursor::new(data));
|
||||
let pixels = decoder.decode()?;
|
||||
let info = decoder.info().ok_or_else(|| anyhow::anyhow!("no JPEG info"))?;
|
||||
|
||||
if info.pixel_format == jpeg_decoder::PixelFormat::RGB24 {
|
||||
Ok(pixels)
|
||||
} else if info.pixel_format == jpeg_decoder::PixelFormat::L8 {
|
||||
// Grayscale → RGB
|
||||
Ok(pixels.iter().flat_map(|&g| [g, g, g]).collect())
|
||||
} else {
|
||||
bail!("unsupported JPEG pixel format: {:?}", info.pixel_format)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn yuyv_to_rgb(data: &[u8], w: u32, h: u32) -> Vec<u8> {
|
||||
let pixel_count = (w * h) as usize;
|
||||
let mut rgb = Vec::with_capacity(pixel_count * 3);
|
||||
|
||||
for chunk in data.chunks(4) {
|
||||
if chunk.len() < 4 { break; }
|
||||
let (y0, u, y1, v) = (chunk[0] as f32, chunk[1] as f32, chunk[2] as f32, chunk[3] as f32);
|
||||
|
||||
for y in [y0, y1] {
|
||||
let r = (y + 1.402 * (v - 128.0)).clamp(0.0, 255.0) as u8;
|
||||
let g = (y - 0.344136 * (u - 128.0) - 0.714136 * (v - 128.0)).clamp(0.0, 255.0) as u8;
|
||||
let b = (y + 1.772 * (u - 128.0)).clamp(0.0, 255.0) as u8;
|
||||
rgb.extend_from_slice(&[r, g, b]);
|
||||
}
|
||||
}
|
||||
rgb.truncate(pixel_count * 3);
|
||||
rgb
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Fallback: ffmpeg subprocess (with timeout + cleanup)
|
||||
// ============================================================
|
||||
|
||||
fn capture_ffmpeg_safe(config: &CameraConfig, tmp: &PathBuf) -> Result<Frame> {
|
||||
let input = if cfg!(target_os = "macos") {
|
||||
format!("{}:none", config.device_index)
|
||||
} else {
|
||||
format!("/dev/video{}", config.device_index)
|
||||
};
|
||||
let format = if cfg!(target_os = "macos") { "avfoundation" } else { "v4l2" };
|
||||
|
||||
let status = Command::new("ffmpeg")
|
||||
// Spawn with timeout to prevent orphans
|
||||
let mut child = Command::new("ffmpeg")
|
||||
.args([
|
||||
"-y", "-f", format,
|
||||
"-video_size", &format!("{}x{}", config.width, config.height),
|
||||
@@ -76,59 +176,54 @@ fn capture_ffmpeg(config: &CameraConfig, tmp: &PathBuf) -> Result<Frame> {
|
||||
"-pix_fmt", "rgb24",
|
||||
tmp.to_str().unwrap_or("/tmp/ruview-frame.raw"),
|
||||
])
|
||||
.output()?;
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
if !status.status.success() {
|
||||
bail!("ffmpeg capture failed: {}", String::from_utf8_lossy(&status.stderr));
|
||||
// Wait with 10-second timeout
|
||||
let timeout = std::time::Duration::from_secs(10);
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
match child.try_wait()? {
|
||||
Some(status) => {
|
||||
if !status.success() {
|
||||
bail!("ffmpeg capture failed (exit {})", status.code().unwrap_or(-1));
|
||||
}
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
if start.elapsed() > timeout {
|
||||
// Kill the stuck process — this is the orphan prevention
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
bail!("ffmpeg capture timed out after 10s — killed");
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rgb = std::fs::read(tmp)?;
|
||||
let expected = (config.width * config.height * 3) as usize;
|
||||
let _ = std::fs::remove_file(tmp);
|
||||
|
||||
if rgb.len() < expected {
|
||||
bail!("frame too small: {} bytes, expected {}", rgb.len(), expected);
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(tmp);
|
||||
|
||||
Ok(Frame {
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
rgb: rgb[..expected].to_vec(),
|
||||
timestamp_ms: chrono::Utc::now().timestamp_millis(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Linux: capture via v4l2-ctl.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn capture_v4l2(config: &CameraConfig, tmp: &PathBuf) -> Result<Frame> {
|
||||
let device = format!("/dev/video{}", config.device_index);
|
||||
if !std::path::Path::new(&device).exists() {
|
||||
bail!("no camera at {device}");
|
||||
}
|
||||
|
||||
// Use v4l2-ctl to grab a frame
|
||||
let status = Command::new("v4l2-ctl")
|
||||
.args([
|
||||
"--device", &device,
|
||||
"--set-fmt-video", &format!("width={},height={},pixelformat=MJPG", config.width, config.height),
|
||||
"--stream-mmap", "--stream-count=1",
|
||||
"--stream-to", tmp.to_str().unwrap_or("/tmp/frame.mjpg"),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !status.status.success() {
|
||||
bail!("v4l2-ctl failed");
|
||||
}
|
||||
|
||||
// Decode MJPEG to RGB
|
||||
decode_jpeg_to_rgb(tmp, config.width, config.height)
|
||||
}
|
||||
|
||||
/// macOS: capture via screencapture or swift.
|
||||
/// macOS: capture via swift/screencapture.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn capture_macos(config: &CameraConfig, tmp: &PathBuf) -> Result<Frame> {
|
||||
let jpg_path = tmp.with_extension("jpg");
|
||||
|
||||
// Try swift-based capture (requires camera permission)
|
||||
let swift = format!(
|
||||
r#"import AVFoundation; import AppKit
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
@@ -147,34 +242,25 @@ o.capturePhoto(with: AVCapturePhotoSettings(), delegate: dl)
|
||||
Thread.sleep(forTimeInterval: 3)"#,
|
||||
path = jpg_path.display()
|
||||
);
|
||||
|
||||
let _ = Command::new("swift").args(["-e", &swift]).output();
|
||||
|
||||
if jpg_path.exists() {
|
||||
return decode_jpeg_to_rgb(&jpg_path, config.width, config.height);
|
||||
let data = std::fs::read(&jpg_path)?;
|
||||
let _ = std::fs::remove_file(&jpg_path);
|
||||
return Ok(Frame {
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
rgb: data,
|
||||
timestamp_ms: chrono::Utc::now().timestamp_millis(),
|
||||
});
|
||||
}
|
||||
|
||||
bail!("macOS camera capture requires GUI session with camera permission")
|
||||
}
|
||||
|
||||
fn decode_jpeg_to_rgb(path: &PathBuf, _width: u32, _height: u32) -> Result<Frame> {
|
||||
let data = std::fs::read(path)?;
|
||||
let _ = std::fs::remove_file(path);
|
||||
|
||||
// Simple JPEG decode — use the image crate if available, otherwise raw
|
||||
// For now, return the raw data and let the caller handle format
|
||||
Ok(Frame {
|
||||
width: _width,
|
||||
height: _height,
|
||||
rgb: data,
|
||||
})
|
||||
}
|
||||
|
||||
fn tmp_path() -> PathBuf {
|
||||
std::env::temp_dir().join(format!("ruview-frame-{}.raw", std::process::id()))
|
||||
}
|
||||
|
||||
/// Check if a camera is available on this system.
|
||||
/// Check if a camera is available.
|
||||
pub fn camera_available() -> bool {
|
||||
if cfg!(target_os = "macos") {
|
||||
Command::new("system_profiler")
|
||||
@@ -190,7 +276,6 @@ pub fn camera_available() -> bool {
|
||||
/// List available cameras.
|
||||
pub fn list_cameras() -> Vec<String> {
|
||||
let mut cameras = Vec::new();
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
if let Ok(output) = Command::new("system_profiler").args(["SPCameraDataType"]).output() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
@@ -25,7 +25,6 @@ axum = { workspace = true }
|
||||
tower-http = { version = "0.5", features = ["fs", "cors", "set-header"] }
|
||||
tokio = { workspace = true, features = ["full", "process"] }
|
||||
futures-util = "0.3"
|
||||
ruvector-mincut = { workspace = true }
|
||||
|
||||
# Serialization
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -8,10 +8,8 @@ pub mod vital_signs;
|
||||
pub mod rvf_container;
|
||||
pub mod rvf_pipeline;
|
||||
pub mod graph_transformer;
|
||||
#[allow(dead_code)]
|
||||
pub mod trainer;
|
||||
pub mod dataset;
|
||||
pub mod sona;
|
||||
pub mod sparse_inference;
|
||||
#[allow(dead_code)]
|
||||
pub mod embedding;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
//! - Serves the static UI files (port 8080)
|
||||
//!
|
||||
//! Replaces both ws_server.py and the Python HTTP server.
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod adaptive_classifier;
|
||||
pub mod cli;
|
||||
@@ -1659,11 +1658,9 @@ async fn windows_wifi_task(state: SharedState, tick_ms: u64) {
|
||||
|
||||
// Populate persons from the sensing update (Kalman-smoothed via tracker).
|
||||
let raw_persons = derive_pose_from_sensing(&update);
|
||||
let mut last_tracker_instant = s.last_tracker_instant.take();
|
||||
let tracked = tracker_bridge::tracker_update(
|
||||
&mut s.pose_tracker, &mut last_tracker_instant, raw_persons,
|
||||
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
|
||||
);
|
||||
s.last_tracker_instant = last_tracker_instant;
|
||||
if !tracked.is_empty() {
|
||||
update.persons = Some(tracked);
|
||||
}
|
||||
@@ -1797,11 +1794,9 @@ async fn windows_wifi_fallback_tick(state: &SharedState, seq: u32) {
|
||||
};
|
||||
|
||||
let raw_persons = derive_pose_from_sensing(&update);
|
||||
let mut last_tracker_instant = s.last_tracker_instant.take();
|
||||
let tracked = tracker_bridge::tracker_update(
|
||||
&mut s.pose_tracker, &mut last_tracker_instant, raw_persons,
|
||||
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
|
||||
);
|
||||
s.last_tracker_instant = last_tracker_instant;
|
||||
if !tracked.is_empty() {
|
||||
update.persons = Some(tracked);
|
||||
}
|
||||
@@ -3229,7 +3224,7 @@ async fn adaptive_status(State(state): State<SharedState>) -> Json<serde_json::V
|
||||
"trained_frames": model.trained_frames,
|
||||
"accuracy": model.training_accuracy,
|
||||
"version": model.version,
|
||||
"classes": model.class_names,
|
||||
"classes": adaptive_classifier::CLASSES,
|
||||
"class_stats": model.class_stats,
|
||||
})),
|
||||
None => Json(serde_json::json!({
|
||||
@@ -3615,9 +3610,9 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||
};
|
||||
|
||||
// Feed field model calibration if active (use per-node history for ESP32).
|
||||
if let Some(frame_history) = s.node_states.get(&node_id).map(|ns| ns.frame_history.clone()) {
|
||||
if let Some(ref mut fm) = s.field_model {
|
||||
field_bridge::maybe_feed_calibration(fm, &frame_history);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3700,11 +3695,9 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||
};
|
||||
|
||||
let raw_persons = derive_pose_from_sensing(&update);
|
||||
let mut last_tracker_instant = s.last_tracker_instant.take();
|
||||
let tracked = tracker_bridge::tracker_update(
|
||||
&mut s.pose_tracker, &mut last_tracker_instant, raw_persons,
|
||||
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
|
||||
);
|
||||
s.last_tracker_instant = last_tracker_instant;
|
||||
if !tracked.is_empty() {
|
||||
update.persons = Some(tracked);
|
||||
}
|
||||
@@ -3865,9 +3858,9 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||
};
|
||||
|
||||
// Feed field model calibration if active (use per-node history for ESP32).
|
||||
if let Some(frame_history) = s.node_states.get(&node_id).map(|ns| ns.frame_history.clone()) {
|
||||
if let Some(ref mut fm) = s.field_model {
|
||||
field_bridge::maybe_feed_calibration(fm, &frame_history);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3912,11 +3905,9 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||
};
|
||||
|
||||
let raw_persons = derive_pose_from_sensing(&update);
|
||||
let mut last_tracker_instant = s.last_tracker_instant.take();
|
||||
let tracked = tracker_bridge::tracker_update(
|
||||
&mut s.pose_tracker, &mut last_tracker_instant, raw_persons,
|
||||
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
|
||||
);
|
||||
s.last_tracker_instant = last_tracker_instant;
|
||||
if !tracked.is_empty() {
|
||||
update.persons = Some(tracked);
|
||||
}
|
||||
@@ -4050,11 +4041,9 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
|
||||
|
||||
// Populate persons from the sensing update (Kalman-smoothed via tracker).
|
||||
let raw_persons = derive_pose_from_sensing(&update);
|
||||
let mut last_tracker_instant = s.last_tracker_instant.take();
|
||||
let tracked = tracker_bridge::tracker_update(
|
||||
&mut s.pose_tracker, &mut last_tracker_instant, raw_persons,
|
||||
&mut s.pose_tracker, &mut s.last_tracker_instant, raw_persons,
|
||||
);
|
||||
s.last_tracker_instant = last_tracker_instant;
|
||||
if !tracked.is_empty() {
|
||||
update.persons = Some(tracked);
|
||||
}
|
||||
|
||||
Vendored
+1
-1
Submodule vendor/ruvector updated: d5d3296cd9...050c3fe6f8
Reference in New Issue
Block a user