Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use wifi_densepose_sensing_server::{graph_transformer, trainer, dataset, embeddi

use std::collections::VecDeque;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::path::{Path as FsPath, PathBuf};
use std::sync::Arc;
use std::time::Duration;

Expand Down Expand Up @@ -70,7 +70,7 @@ struct Args {
udp_port: u16,

/// Path to UI static files
#[arg(long, default_value = "../../ui")]
#[arg(long, default_value = DEFAULT_UI_PATH)]
ui_path: PathBuf,

/// Tick interval in milliseconds (default 100 ms = 10 fps for smooth pose animation)
Expand Down Expand Up @@ -146,6 +146,54 @@ struct Args {
build_index: Option<String>,
}

const DEFAULT_UI_PATH: &str = "../../ui";

fn is_valid_ui_dir(path: &FsPath) -> bool {
path.join("index.html").is_file()
}

fn push_ancestor_ui_candidates(candidates: &mut Vec<PathBuf>, base: &FsPath) {
for ancestor in base.ancestors() {
candidates.push(ancestor.join("ui"));
}
}

fn resolve_ui_path_with_context(
configured_path: &FsPath,
current_dir: Option<&FsPath>,
executable_path: Option<&FsPath>,
) -> PathBuf {
let mut candidates = vec![configured_path.to_path_buf()];

if let Some(cwd) = current_dir {
candidates.push(cwd.join(configured_path));
push_ancestor_ui_candidates(&mut candidates, cwd);
}

if let Some(exe_dir) = executable_path.and_then(FsPath::parent) {
candidates.push(exe_dir.join(configured_path));
push_ancestor_ui_candidates(&mut candidates, exe_dir);
}

push_ancestor_ui_candidates(&mut candidates, FsPath::new(env!("CARGO_MANIFEST_DIR")));

candidates
.into_iter()
.find(|candidate| is_valid_ui_dir(candidate))
.and_then(|candidate| std::fs::canonicalize(&candidate).ok().or(Some(candidate)))
.unwrap_or_else(|| {
current_dir
.map(|cwd| cwd.join(configured_path))
.unwrap_or_else(|| configured_path.to_path_buf())
})
}

fn resolve_ui_path(configured_path: &FsPath) -> PathBuf {
let current_dir = std::env::current_dir().ok();
let executable_path = std::env::current_exe().ok();
resolve_ui_path_with_context(configured_path, current_dir.as_deref(), executable_path.as_deref())
}

// ── Data types ───────────────────────────────────────────────────────────────

/// ADR-018 ESP32 CSI binary frame header (20 bytes)
Expand Down Expand Up @@ -3028,6 +3076,22 @@ async fn main() {
.init();

let args = Args::parse();
let ui_path = resolve_ui_path(&args.ui_path);

if ui_path != args.ui_path {
info!(
"Resolved UI path from {} to {}",
args.ui_path.display(),
ui_path.display()
);
}

if !is_valid_ui_dir(&ui_path) {
warn!(
"UI assets not found at {}. Static /ui requests will return 404; pass --ui-path /path/to/RuView/ui",
ui_path.display()
);
}

// Handle --benchmark mode: run vital sign benchmark and exit
if args.benchmark {
Expand Down Expand Up @@ -3455,7 +3519,7 @@ async fn main() {
info!(" HTTP: http://localhost:{}", args.http_port);
info!(" WebSocket: ws://localhost:{}/ws/sensing", args.ws_port);
info!(" UDP: 0.0.0.0:{} (ESP32 CSI)", args.udp_port);
info!(" UI path: {}", args.ui_path.display());
info!(" UI path: {}", ui_path.display());
info!(" Source: {}", args.source);

// Auto-detect data source
Expand Down Expand Up @@ -3645,7 +3709,6 @@ async fn main() {
});

// HTTP server (serves UI + full DensePose-compatible REST API)
let ui_path = args.ui_path.clone();
let http_app = Router::new()
.route("/", get(info_page))
// Health endpoints (DensePose-compatible)
Expand Down Expand Up @@ -3763,3 +3826,60 @@ async fn main() {

info!("Server shut down cleanly");
}

#[cfg(test)]
mod tests {
use super::{resolve_ui_path_with_context, DEFAULT_UI_PATH};
use std::path::{Path as FsPath, PathBuf};
use tempfile::tempdir;

fn create_ui_dir(root: &FsPath) -> PathBuf {
let ui_dir = root.join("ui");
std::fs::create_dir_all(&ui_dir).unwrap();
std::fs::write(ui_dir.join("index.html"), "<!doctype html>").unwrap();
ui_dir
}

#[test]
fn resolve_ui_path_keeps_existing_configured_directory() {
let temp = tempdir().unwrap();
let ui_dir = create_ui_dir(temp.path());

let resolved = resolve_ui_path_with_context(&ui_dir, Some(temp.path()), None);

assert_eq!(resolved, std::fs::canonicalize(ui_dir).unwrap());
}

#[test]
fn resolve_ui_path_finds_repo_ui_from_current_dir_ancestors() {
let temp = tempdir().unwrap();
let ui_dir = create_ui_dir(temp.path());
let cwd = temp.path().join("rust-port/wifi-densepose-rs/target/release");
std::fs::create_dir_all(&cwd).unwrap();

let resolved = resolve_ui_path_with_context(FsPath::new(DEFAULT_UI_PATH), Some(&cwd), None);

assert_eq!(resolved, std::fs::canonicalize(ui_dir).unwrap());
}

#[test]
fn resolve_ui_path_finds_repo_ui_from_executable_ancestors() {
let temp = tempdir().unwrap();
let ui_dir = create_ui_dir(temp.path());
let exe_dir = temp.path().join("rust-port/wifi-densepose-rs/target/release");
std::fs::create_dir_all(&exe_dir).unwrap();
let executable_path = exe_dir.join("sensing-server");
std::fs::write(&executable_path, b"").unwrap();

let unrelated_cwd = temp.path().join("tmp/run");
std::fs::create_dir_all(&unrelated_cwd).unwrap();

let resolved = resolve_ui_path_with_context(
FsPath::new(DEFAULT_UI_PATH),
Some(&unrelated_cwd),
Some(&executable_path),
);

assert_eq!(resolved, std::fs::canonicalize(ui_dir).unwrap());
}
}