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 [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_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 canInterfaces = ['can0', 'can1', 'can2', 'can3']; const statusIntervalRef = useRef(null); const hasInitializedRef = useRef(false); const loadConfig = () => { try { const saved = localStorage.getItem('openarms_config'); if (saved) { setConfig(prev => ({ ...prev, ...JSON.parse(saved) })); } } 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); 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 }; localStorage.setItem('openarms_config', JSON.stringify(merged)); return merged; }); } } } catch (e) { console.error('Failed to fetch status:', e); } }; const setupRobots = async () => { 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}`); } }; // 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 }; setConfig(updated); saveConfig(updated); }; // Initialize on mount only useEffect(() => { // Prevent double-initialization in development if (hasInitializedRef.current) { return; } hasInitializedRef.current = true; loadConfig(); discoverCameras(); 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 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
)}
{/* CAN Interfaces */}

CAN Interfaces

{/* 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} />
{/* 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)} @ {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;