diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/src/commands/discovery.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/src/commands/discovery.rs index e5d5cef1..8d66b632 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/src/commands/discovery.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/src/commands/discovery.rs @@ -276,11 +276,24 @@ fn parse_beacon_response(data: &[u8], addr: SocketAddr) -> Option Result, String> { - let ports = available_ports().map_err(|e| format!("Failed to enumerate ports: {}", e))?; + tracing::info!("list_serial_ports called"); + + let ports = match available_ports() { + Ok(p) => { + tracing::info!("Found {} ports from tokio_serial", p.len()); + p + } + Err(e) => { + tracing::error!("Failed to enumerate ports: {}", e); + // Fallback: try to list /dev/cu.usb* manually on macOS + return list_serial_ports_fallback(); + } + }; let mut result = Vec::new(); for port in ports { + tracing::debug!("Processing port: {}", port.port_name); let info = match port.port_type { tokio_serial::SerialPortType::UsbPort(usb_info) => { SerialPortInfo { @@ -294,12 +307,13 @@ pub async fn list_serial_ports() -> Result, String> { } _ => { SerialPortInfo { - name: port.port_name, + name: port.port_name.clone(), vid: None, pid: None, manufacturer: None, serial_number: None, - is_esp32_compatible: false, + // Mark /dev/cu.usb* ports as potentially compatible + is_esp32_compatible: port.port_name.contains("usb"), } } }; @@ -307,9 +321,72 @@ pub async fn list_serial_ports() -> Result, String> { result.push(info); } + // If no ports found via tokio_serial, try fallback + if result.is_empty() { + tracing::warn!("No ports from tokio_serial, trying fallback"); + return list_serial_ports_fallback(); + } + // Sort ESP32-compatible ports first result.sort_by(|a, b| b.is_esp32_compatible.cmp(&a.is_esp32_compatible)); + tracing::info!("Returning {} serial ports", result.len()); + Ok(result) +} + +/// Fallback serial port listing for macOS when tokio_serial fails +fn list_serial_ports_fallback() -> Result, String> { + tracing::info!("Using fallback serial port listing"); + + let mut result = Vec::new(); + + // List /dev/cu.usb* devices on macOS + #[cfg(target_os = "macos")] + { + use std::fs; + if let Ok(entries) = fs::read_dir("/dev") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("cu.usb") { + let path = format!("/dev/{}", name); + tracing::info!("Fallback found port: {}", path); + result.push(SerialPortInfo { + name: path, + vid: None, + pid: None, + manufacturer: Some("USB Serial".to_string()), + serial_number: None, + is_esp32_compatible: true, // Assume USB serial is ESP32 + }); + } + } + } + } + + // Linux fallback + #[cfg(target_os = "linux")] + { + use std::fs; + if let Ok(entries) = fs::read_dir("/dev") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("ttyUSB") || name.starts_with("ttyACM") { + let path = format!("/dev/{}", name); + tracing::info!("Fallback found port: {}", path); + result.push(SerialPortInfo { + name: path, + vid: None, + pid: None, + manufacturer: Some("USB Serial".to_string()), + serial_number: None, + is_esp32_compatible: true, + }); + } + } + } + } + + tracing::info!("Fallback found {} ports", result.len()); Ok(result) } diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/src/commands/server.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/src/commands/server.rs index 895ac2f4..2993b9b0 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/src/commands/server.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/src/commands/server.rs @@ -148,14 +148,15 @@ pub async fn start_server( /// First attempts graceful termination (SIGTERM), then SIGKILL after timeout. #[tauri::command] pub async fn stop_server(state: State<'_, AppState>) -> Result<(), String> { - // Extract child process ID and take ownership of child for killing - // This releases the lock before any await points - let child_id = { - let srv = state.server.lock().map_err(|e| e.to_string())?; + // Extract child process and take ownership for killing + let (child_id, mut child_process) = { + let mut srv = state.server.lock().map_err(|e| e.to_string())?; if !srv.running { return Err("Server is not running".into()); } - srv.pid + let pid = srv.pid; + let child = srv.child.take(); // Take ownership of child + (pid, child) }; let child_id = match child_id { @@ -163,46 +164,60 @@ pub async fn stop_server(state: State<'_, AppState>) -> Result<(), String> { None => return Err("No server process found".into()), }; - // First try graceful termination + tracing::info!("Stopping sensing server with PID {}", child_id); + + // First try graceful termination via SIGTERM #[cfg(unix)] { unsafe { - libc::kill(child_id as i32, libc::SIGTERM); + // Kill the process group (negative PID) to kill all children too + let _ = libc::kill(-(child_id as i32), libc::SIGTERM); + // Also kill the main process directly + let _ = libc::kill(child_id as i32, libc::SIGTERM); } } - // Wait briefly for graceful shutdown (async operation - no lock held) - let wait_result: Result, _> = tokio::time::timeout( - std::time::Duration::from_secs(5), - tokio::task::spawn_blocking({ - move || { - std::thread::sleep(std::time::Duration::from_millis(100)); - // Check if process is still alive - let mut sys = System::new(); - let pid = Pid::from_u32(child_id); - sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); - sys.process(pid).is_some() - } - }) - ).await; + // Wait briefly for graceful shutdown + tokio::time::sleep(std::time::Duration::from_millis(500)).await; - // Force kill if still running - re-acquire lock - let still_running = match wait_result { - Ok(Ok(running)) => running, - _ => true, + // Check if still running + let still_running = { + let mut sys = System::new(); + let pid = Pid::from_u32(child_id); + sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); + sys.process(pid).is_some() }; - { - let mut srv = state.server.lock().map_err(|e| e.to_string())?; + // Force kill if still running + if still_running { + tracing::warn!("Server still running after SIGTERM, sending SIGKILL"); - if still_running { - if let Some(ref mut child) = srv.child { - let _ = child.kill(); - let _ = child.wait(); + #[cfg(unix)] + { + unsafe { + // SIGKILL the process group and main process + let _ = libc::kill(-(child_id as i32), libc::SIGKILL); + let _ = libc::kill(child_id as i32, libc::SIGKILL); } } - // Clear state + // Also use the child handle if available + if let Some(ref mut child) = child_process { + let _ = child.kill(); + } + } + + // Wait for process to actually terminate + if let Some(ref mut child) = child_process { + let _ = child.wait(); + } + + // Final verification and cleanup + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + // Clear state + { + let mut srv = state.server.lock().map_err(|e| e.to_string())?; srv.running = false; srv.pid = None; srv.http_port = None; @@ -211,6 +226,19 @@ pub async fn stop_server(state: State<'_, AppState>) -> Result<(), String> { srv.child = None; } + // Verify process is dead + let still_alive = { + let mut sys = System::new(); + let pid = Pid::from_u32(child_id); + sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); + sys.process(pid).is_some() + }; + + if still_alive { + tracing::error!("Failed to kill server process {}", child_id); + return Err(format!("Failed to stop server process {}", child_id)); + } + tracing::info!("Stopped sensing server"); Ok(()) @@ -366,6 +394,7 @@ mod tests { log_level: None, bind_address: None, server_path: None, + source: Some("simulate".to_string()), }; assert_eq!(config.http_port, Some(8080)); diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/tauri.conf.json b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/tauri.conf.json index 3bc285c7..6c9b2280 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/tauri.conf.json +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json", "productName": "RuView Desktop", - "version": "0.4.2", + "version": "0.4.3", "identifier": "net.ruv.ruview", "build": { "frontendDist": "ui/dist", diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/package.json b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/package.json index 2ecd2253..1fcda95a 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/package.json +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/package.json @@ -1,7 +1,7 @@ { "name": "ruview-desktop-ui", "private": true, - "version": "0.4.2", + "version": "0.4.3", "type": "module", "scripts": { "dev": "vite", diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/App.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/App.tsx index 16dce57b..51c5fe93 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/App.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/App.tsx @@ -92,7 +92,7 @@ const App: React.FC = () => { const renderPage = () => { switch (activePage) { case "dashboard": return ; - case "discovery": return ; + case "discovery": return ; case "nodes": return ; case "flash": return ; case "ota": return ; diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/NetworkDiscovery.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/NetworkDiscovery.tsx index b2de3cd1..0095e1ac 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/NetworkDiscovery.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/NetworkDiscovery.tsx @@ -3,6 +3,12 @@ import { invoke } from "@tauri-apps/api/core"; import { StatusBadge } from "../components/StatusBadge"; import type { HealthStatus, Chip, MeshRole, DiscoveryMethod } from "../types"; +type Page = "dashboard" | "discovery" | "nodes" | "flash" | "ota" | "wasm" | "sensing" | "mesh" | "settings"; + +interface NetworkDiscoveryProps { + onNavigate?: (page: Page) => void; +} + interface DiscoveredNode { ip: string; mac: string | null; @@ -34,7 +40,7 @@ interface SerialPortInfo { type DiscoveryTab = "network" | "serial" | "manual"; -const NetworkDiscovery: React.FC = () => { +const NetworkDiscovery: React.FC = ({ onNavigate }) => { const [activeTab, setActiveTab] = useState("network"); const [nodes, setNodes] = useState([]); const [serialPorts, setSerialPorts] = useState([]); @@ -112,16 +118,22 @@ const NetworkDiscovery: React.FC = () => { } }, [manualIp, manualMac]); + // Scan both network and serial ports on mount useEffect(() => { scanNetwork(); + scanSerialPorts(); }, []); + // Also refresh serial ports when switching to that tab useEffect(() => { if (activeTab === "serial") { scanSerialPorts(); } }, [activeTab, scanSerialPorts]); + // Count ESP32-compatible serial ports + const esp32SerialCount = serialPorts.filter((p) => p.is_esp32_compatible).length; + const filteredNodes = nodes.filter((node) => { if (filterOnline && node.health !== "online") return false; if (searchQuery) { @@ -302,21 +314,61 @@ const NetworkDiscovery: React.FC = () => {
{"◉"}
- {isScanning ? "Scanning for nodes..." : "No nodes discovered"} + {isScanning ? "Scanning for nodes..." : "No network nodes found"}
{isScanning ? "Please wait while we search for ESP32 devices on your network." - : "Click 'Scan Network' to discover ESP32 devices using mDNS and UDP broadcast."} + : "Network discovery uses mDNS/UDP to find ESP32 devices running firmware on WiFi."}
+ + {/* USB device hint */} + {!isScanning && esp32SerialCount > 0 && ( +
+
+ 🔌 + + {esp32SerialCount} USB device{esp32SerialCount > 1 ? "s" : ""} detected! + +
+
+ Your ESP32 is connected via USB. To flash firmware or configure it: +
+ +
+ )}
) : (
{ Manufacturer VID:PID Compatible + Actions @@ -417,6 +470,25 @@ const NetworkDiscovery: React.FC = () => { -- )} + + {port.is_esp32_compatible && onNavigate && ( + + )} + ))} diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Sensing.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Sensing.tsx index f3f6002c..6d16b46f 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Sensing.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Sensing.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef, useCallback } from "react"; import { useServer } from "../hooks/useServer"; -import type { SensingUpdate } from "../types"; +import type { SensingUpdate, DataSource } from "../types"; // --------------------------------------------------------------------------- // Log entry model @@ -176,11 +176,12 @@ function LogViewer({ paused: boolean; onTogglePause: () => void; }) { - const bottomRef = useRef(null); + const containerRef = useRef(null); useEffect(() => { - if (!paused && bottomRef.current) { - bottomRef.current.scrollIntoView({ behavior: "smooth" }); + // Scroll to bottom within the container only (not the page) + if (!paused && containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [entries, paused]); @@ -254,6 +255,7 @@ function LogViewer({ {/* Log entries */}
)) )} -
); @@ -301,6 +302,9 @@ export const Sensing: React.FC = () => { const [starting, setStarting] = useState(false); const [stopping, setStopping] = useState(false); + // Data source selection + const [dataSource, setDataSource] = useState("simulate"); + // Log viewer state const [logEntries, setLogEntries] = useState([]); const [paused, setPaused] = useState(false); @@ -430,7 +434,7 @@ export const Sensing: React.FC = () => { const handleStart = async () => { setStarting(true); try { - await start(); + await start({ source: dataSource }); } finally { setStarting(false); } @@ -524,24 +528,61 @@ export const Sensing: React.FC = () => { )}
- {/* Right: action button */} - + {/* Right: data source + action button */} +
+ {/* Data source selector */} +
+ + +
+ + {/* Action button */} + +
{/* Error display */} diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/version.ts b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/version.ts index 923ee74f..f90cfcd8 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/version.ts +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/version.ts @@ -1,2 +1,2 @@ // Application version - single source of truth -export const APP_VERSION = "0.4.2"; +export const APP_VERSION = "0.4.3";