import { useState, useEffect, useCallback, useRef } from 'react'; import './App.css'; const API_BASE = 'http://localhost:8000/api'; function App() { // State const [task, setTask] = useState(''); const [isRecording, setIsRecording] = useState(false); const [isInitializing, setIsInitializing] = useState(false); const [isEncoding, setIsEncoding] = useState(false); const [isUploading, setIsUploading] = useState(false); const [robotsReady, setRobotsReady] = useState(false); const [elapsedTime, setElapsedTime] = useState(0); const [currentFps, setCurrentFps] = useState(0); const [loopFps, setLoopFps] = useState(0); const [episodeCount, setEpisodeCount] = useState(0); const [error, setError] = useState(null); const [statusMessage, setStatusMessage] = useState('Ready'); const [uploadStatus, setUploadStatus] = useState(null); const [rampUpRemaining, setRampUpRemaining] = useState(0); const [movingToZero, setMovingToZero] = useState(false); const [configExpanded, setConfigExpanded] = useState(false); // Configuration const [config, setConfig] = useState({ leader_type: 'openarms', // 'openarms' or 'openarms_mini' leader_left: 'can0', leader_right: 'can1', follower_left: 'can2', follower_right: 'can3', left_wrist: '/dev/video0', right_wrist: '/dev/video1', base: '/dev/video4' }); // Available options const [availableCameras, setAvailableCameras] = useState([]); const [availableUsbPorts, setAvailableUsbPorts] = useState([]); const canInterfaces = ['can0', 'can1', 'can2', 'can3']; const statusIntervalRef = useRef(null); const hasInitializedRef = useRef(false); const loadConfig = () => { try { const saved = localStorage.getItem('openarms_config'); if (saved) { const loadedConfig = JSON.parse(saved); // If OpenArms Mini is selected, ensure followers are set to can0/can1 if (loadedConfig.leader_type === 'openarms_mini') { loadedConfig.follower_left = 'can0'; loadedConfig.follower_right = 'can1'; } setConfig(prev => ({ ...prev, ...loadedConfig })); } } catch (e) { console.error('Load config error:', e); } }; const saveConfig = (newConfig) => { try { localStorage.setItem('openarms_config', JSON.stringify(newConfig || config)); } catch (e) { console.error('Save config error:', e); } }; // Fetch status periodically const fetchStatus = async () => { try { const response = await fetch(`${API_BASE}/status`); const data = await response.json(); setIsRecording(data.is_recording); setIsInitializing(data.is_initializing); setIsEncoding(data.is_encoding); setIsUploading(data.is_uploading); setRobotsReady(data.robots_ready); setElapsedTime(data.elapsed_time); setCurrentFps(data.current_fps || 0); setLoopFps(data.loop_fps || 0); setEpisodeCount(data.episode_count); setError(data.error); setStatusMessage(data.status_message || 'Ready'); setUploadStatus(data.upload_status); setRampUpRemaining(data.ramp_up_remaining || 0); setMovingToZero(data.moving_to_zero || false); if (data.config) { // Only merge server config if we don't have a saved config (first load) if (!localStorage.getItem('openarms_config')) { setConfig(prev => { const merged = { ...data.config, ...prev }; // If OpenArms Mini is selected, ensure followers are set to can0/can1 if (merged.leader_type === 'openarms_mini') { merged.follower_left = 'can0'; merged.follower_right = 'can1'; } localStorage.setItem('openarms_config', JSON.stringify(merged)); return merged; }); } } } catch (e) { console.error('Failed to fetch status:', e); } }; const setupRobots = async () => { // Show warning to verify camera positions const confirmed = window.confirm( '⚠️ IMPORTANT: Before connecting robots, please verify:\n\n' + '📹 Check that cameras are correctly positioned:\n' + ' • LEFT wrist camera is actually on the LEFT arm\n' + ' • RIGHT wrist camera is actually on the RIGHT arm\n' + ' • BASE camera is actually the BASE/overhead camera\n\n' + 'Incorrect camera positioning will result in invalid training data!\n\n' + 'Click OK to continue with robot setup, or Cancel to review configuration.' ); if (!confirmed) { return; // User cancelled, don't proceed } setError(null); try { const response = await fetch(`${API_BASE}/robots/setup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (!response.ok) { const data = await response.json(); throw new Error(data.detail || 'Failed to setup robots'); } await response.json(); saveConfig(config); } catch (e) { setError(`Robot setup failed: ${e.message}`); } }; // Disconnect robots const disconnectRobots = async () => { try { await fetch(`${API_BASE}/robots/disconnect`, { method: 'POST' }); setRobotsReady(false); } catch (e) { console.error('Failed to disconnect robots:', e); } }; // Discover cameras const discoverCameras = async () => { try { const response = await fetch(`${API_BASE}/cameras/discover`); const data = await response.json(); const cameras = data.cameras || []; setAvailableCameras(cameras); // Get list of valid camera IDs const validCameraIds = cameras.map(cam => String(cam.id)); // Auto-fix config if current values are invalid or not set const updated = { ...config }; let changed = false; // Auto-fix invalid camera config if (!config.left_wrist || !validCameraIds.includes(config.left_wrist)) { if (cameras.length >= 1) { updated.left_wrist = String(cameras[0].id); changed = true; } } if (!config.right_wrist || !validCameraIds.includes(config.right_wrist)) { if (cameras.length >= 2) { updated.right_wrist = String(cameras[1].id); changed = true; } } if (!config.base || !validCameraIds.includes(config.base)) { if (cameras.length >= 3) { updated.base = String(cameras[2].id); changed = true; } } if (changed) { setConfig(updated); saveConfig(updated); } if (cameras.length === 0) { setError('No cameras detected! Please connect cameras and refresh.'); } } catch (e) { console.error('Failed to discover cameras:', e); setError(`Camera discovery failed: ${e.message}`); } }; // Discover USB ports const discoverUsbPorts = async () => { try { const response = await fetch(`${API_BASE}/usb/discover`); const data = await response.json(); const ports = data.ports || []; setAvailableUsbPorts(ports); // Auto-fix config if OpenArms Mini is selected and ports are invalid if (config.leader_type === 'openarms_mini') { const updated = { ...config }; let changed = false; if (ports.length >= 1 && !ports.includes(config.leader_left)) { updated.leader_left = ports[0]; changed = true; } if (ports.length >= 2 && !ports.includes(config.leader_right)) { updated.leader_right = ports[1]; changed = true; } if (changed) { setConfig(updated); saveConfig(updated); } } if (ports.length === 0) { console.warn('No USB ports detected for OpenArms Mini'); } } catch (e) { console.error('Failed to discover USB ports:', e); } }; // Set task only (for pedal use) const setTaskOnly = async () => { if (!task.trim()) { setError('Please enter a task description'); return; } setError(null); try { const response = await fetch(`${API_BASE}/recording/set-task`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task, ...config }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.detail || 'Failed to set task'); } const result = await response.json(); setStatusMessage(result.message || `Task set: ${task}`); saveConfig(config); // Clear success message after 3 seconds setTimeout(() => { if (!isRecording && !isInitializing) { setStatusMessage('Ready'); } }, 3000); } catch (e) { setError(e.message); } }; // Start recording const startRecording = async () => { if (!task.trim()) { setError('Please enter a task description'); return; } setError(null); try { const response = await fetch(`${API_BASE}/recording/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task, ...config }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.detail || 'Failed to start recording'); } await response.json(); saveConfig(config); } catch (e) { setError(e.message); } }; // Stop recording const stopRecording = async () => { try { const response = await fetch(`${API_BASE}/recording/stop`, { method: 'POST' }); if (!response.ok) { const data = await response.json(); throw new Error(data.detail || 'Failed to stop recording'); } await response.json(); setError(null); } catch (e) { setError(e.message); } }; // Reset counter const resetCounter = async () => { try { await fetch(`${API_BASE}/counter/reset`, { method: 'POST' }); setEpisodeCount(0); } catch (e) { console.error('Failed to reset counter:', e); } }; // Move robot to zero position const moveToZero = async () => { setError(null); try { const response = await fetch(`${API_BASE}/robots/move-to-zero`, { method: 'POST' }); if (!response.ok) { const data = await response.json(); throw new Error(data.detail || 'Failed to move to zero position'); } await response.json(); } catch (e) { setError(`Move to zero failed: ${e.message}`); } }; // Format time as MM:SS const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; // Update config and save const updateConfig = (key, value) => { const updated = { ...config, [key]: value }; if (key === 'leader_type') { if (value === 'openarms_mini') { updated.follower_left = 'can0'; updated.follower_right = 'can1'; } } setConfig(updated); saveConfig(updated); }; // Initialize on mount only useEffect(() => { // Prevent double-initialization in development if (hasInitializedRef.current) { return; } hasInitializedRef.current = true; loadConfig(); discoverCameras(); discoverUsbPorts(); fetchStatus(); statusIntervalRef.current = setInterval(fetchStatus, 1000); return () => { if (statusIntervalRef.current) { clearInterval(statusIntervalRef.current); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Run only once on mount // Discover USB ports when leader type changes to Mini useEffect(() => { if (config.leader_type === 'openarms_mini') { discoverUsbPorts(); // Explicitly set follower CAN interfaces when using OpenArms Mini // Followers MUST use can0 = left arm, can1 = right arm // Force these values even if they were previously different const updated = { ...config, follower_left: 'can0', // Explicitly set left follower to can0 follower_right: 'can1' // Explicitly set right follower to can1 }; setConfig(updated); saveConfig(updated); } else { // When switching back to OpenArms, restore CAN interface defaults const updated = { ...config, leader_left: canInterfaces.includes(config.leader_left) ? config.leader_left : 'can0', leader_right: canInterfaces.includes(config.leader_right) ? config.leader_right : 'can1', follower_left: 'can2', // OpenArms standard: followers on can2/can3 follower_right: 'can3' }; setConfig(updated); saveConfig(updated); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [config.leader_type]); return (

OpenArms Recording

{/* Left Column: Configuration and Recording Control */}
{/* Configuration Panel */}
setConfigExpanded(!configExpanded)} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && setConfigExpanded(!configExpanded)} >

⚙️ Configuration

{configExpanded ? '▼' : '▶'}
{configExpanded && (
{/* Robot Setup */}

🤖 Robot Setup

{robotsReady ? (
✅ Robots Ready - Recording will start instantly
) : (
⚠️ Robots not initialized - Recording will take ~10 seconds
)}
{/* Leader Type Selection */}

🎮 Leader Type

{/* Leader Interfaces (CAN or USB based on type) */}

{config.leader_type === 'openarms_mini' ? `Leader Ports (USB/Serial) ${availableUsbPorts.length > 0 ? `(${availableUsbPorts.length} detected)` : ''}` : 'Leader Interfaces (CAN)'}

{config.leader_type === 'openarms_mini' && ( )}
{/* Follower CAN Interfaces */}

Follower Interfaces (CAN)

{config.leader_type === 'openarms_mini' && (
🔒 Auto-assigned: Follower ports are fixed as can0 (left) and can1 (right) when using OpenArms Mini
)}
{/* Camera Configuration */}

Cameras {availableCameras.length > 0 && `(${availableCameras.length} detected)`}

)}
{/* Control Panel */}

🎬 Recording Control

{/* Status Banner - Always show important statuses */} {isInitializing && (
{statusMessage}
)} {isEncoding && (
📹 {statusMessage}
)} {isUploading && (
☁️ {statusMessage}
)} {uploadStatus && !isRecording && !isEncoding && !isUploading && (
{uploadStatus}
)}
{/* Task Input and Status */}
setTask(e.target.value)} placeholder="Task description (e.g., 'pick and place')" disabled={isRecording || isInitializing || isEncoding || isUploading} onKeyPress={(e) => { if (e.key === 'Enter' && robotsReady) { setTaskOnly(); } }} />
{/* Ramp-up Countdown */} {isRecording && rampUpRemaining > 0 && (
⚡ WARMING UP - PID RAMP-UP
{rampUpRemaining.toFixed(1)}s
Recording will start automatically...
)} {/* Recording Status - Only show after ramp-up */} {isRecording && rampUpRemaining <= 0 && (
{formatTime(elapsedTime)} Loop: {loopFps.toFixed(1)} Hz {loopFps > 0 && loopFps < 29 && ⚠️} Recording: {currentFps.toFixed(1)} FPS
)}
{/* Episode Counter */}
Episodes Recorded
{episodeCount}
{/* Move to Zero Button */} {robotsReady && !isRecording && !isInitializing && (
)} {/* Error Display */} {error && (
⚠️ {error}
)}
{/* Right Column: Camera Feeds */}

📹 Camera Views

{robotsReady || isRecording || isInitializing ? (
{/* Base camera - full width */}

Base Camera

Base Camera
{/* Wrist cameras - side by side */}

Left Wrist

Left Wrist Camera

Right Wrist

Right Wrist Camera
) : (

📷 Camera feeds will appear when robots are set up

Click "Setup Robots" above to preview camera feeds

)}
); } export default App;