import React, { useState, useRef, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useToast } from "@/hooks/use-toast"; import { ArrowRight } from "lucide-react"; import LandingHeader from "@/components/landing/LandingHeader"; import RobotModelSelector from "@/components/landing/RobotModelSelector"; import ActionList from "@/components/landing/ActionList"; import PermissionModal from "@/components/landing/PermissionModal"; import TeleoperationModal from "@/components/landing/TeleoperationModal"; import RecordingModal from "@/components/landing/RecordingModal"; import { Action } from "@/components/landing/types"; import UsageInstructionsModal from "@/components/landing/UsageInstructionsModal"; import DirectFollowerModal from "@/components/landing/DirectFollowerModal"; import { useApi } from "@/contexts/ApiContext"; import { CameraConfig } from "@/components/recording/CameraConfiguration"; const Landing = () => { const [robotModel, setRobotModel] = useState("SO101"); const [showPermissionModal, setShowPermissionModal] = useState(false); const [showTeleoperationModal, setShowTeleoperationModal] = useState(false); const [showUsageModal, setShowUsageModal] = useState(false); const [leaderPort, setLeaderPort] = useState("/dev/tty.usbmodem5A460816421"); const [followerPort, setFollowerPort] = useState( "/dev/tty.usbmodem5A460816621" ); const [leaderConfig, setLeaderConfig] = useState(""); const [followerConfig, setFollowerConfig] = useState(""); const [leaderConfigs, setLeaderConfigs] = useState([]); const [followerConfigs, setFollowerConfigs] = useState([]); const [isLoadingConfigs, setIsLoadingConfigs] = useState(false); const { baseUrl, fetchWithHeaders } = useApi(); // Recording state const [showRecordingModal, setShowRecordingModal] = useState(false); const [recordLeaderPort, setRecordLeaderPort] = useState( "/dev/tty.usbmodem5A460816421" ); const [recordFollowerPort, setRecordFollowerPort] = useState( "/dev/tty.usbmodem5A460816621" ); const [recordLeaderConfig, setRecordLeaderConfig] = useState(""); const [recordFollowerConfig, setRecordFollowerConfig] = useState(""); const [datasetRepoId, setDatasetRepoId] = useState(""); const [singleTask, setSingleTask] = useState(""); const [numEpisodes, setNumEpisodes] = useState(5); const [cameras, setCameras] = useState([]); // Camera stream release ref const releaseStreamsRef = useRef<(() => void) | null>(null); // Direct follower control state const [showDirectFollowerModal, setShowDirectFollowerModal] = useState(false); const [directFollowerPort, setDirectFollowerPort] = useState( "/dev/tty.usbmodem5A460816621" ); const [directFollowerConfig, setDirectFollowerConfig] = useState(""); const navigate = useNavigate(); const { toast } = useToast(); // Clear camera state and release streams when returning to landing page useEffect(() => { // If we have cameras and returning from a recording session, clear them if (cameras.length > 0) { console.log( "๐Ÿงน Landing page: Cleaning up camera state from previous session" ); if (releaseStreamsRef.current) { releaseStreamsRef.current(); } setCameras([]); // Clear camera configuration } }, []); // Only run on mount // Cleanup when leaving landing page useEffect(() => { return () => { if (releaseStreamsRef.current) { console.log("๐Ÿงน Landing page: Cleaning up camera streams on unmount"); releaseStreamsRef.current(); } }; }, []); const loadConfigs = async () => { setIsLoadingConfigs(true); try { const response = await fetchWithHeaders(`${baseUrl}/get-configs`); const data = await response.json(); setLeaderConfigs(data.leader_configs || []); setFollowerConfigs(data.follower_configs || []); } catch (error) { toast({ title: "Error Loading Configs", description: "Could not load calibration configs from the backend.", variant: "destructive", }); } finally { setIsLoadingConfigs(false); } }; const handleBeginSession = () => { if (robotModel) { setShowPermissionModal(true); } }; const handleTeleoperationClick = () => { if (robotModel) { setShowTeleoperationModal(true); loadConfigs(); } }; const handleCalibrationClick = () => { if (robotModel) { navigate("/calibration"); } }; const handleRecordingClick = () => { if (robotModel) { setShowRecordingModal(true); loadConfigs(); } }; const handleRecordingModalClose = (open: boolean) => { setShowRecordingModal(open); // Release camera streams when modal is closed if (!open && releaseStreamsRef.current) { console.log("๐Ÿงน Modal closed: Releasing camera streams"); releaseStreamsRef.current(); } }; const handleTrainingClick = () => { if (robotModel) { navigate("/training"); } }; const handleReplayDatasetClick = () => { if (robotModel) { navigate("/replay-dataset"); } }; const handleDirectFollowerClick = () => { if (robotModel) { setShowDirectFollowerModal(true); loadConfigs(); } }; const handleStartTeleoperation = async () => { if (!leaderConfig || !followerConfig) { toast({ title: "Missing Configuration", description: "Please select calibration configs for both leader and follower.", variant: "destructive", }); return; } try { const response = await fetchWithHeaders(`${baseUrl}/move-arm`, { method: "POST", body: JSON.stringify({ leader_port: leaderPort, follower_port: followerPort, leader_config: leaderConfig, follower_config: followerConfig, }), }); const data = await response.json(); if (response.ok) { toast({ title: "Teleoperation Started", description: data.message || "Successfully started teleoperation session.", }); setShowTeleoperationModal(false); navigate("/teleoperation"); } else { toast({ title: "Error Starting Teleoperation", description: data.message || "Failed to start teleoperation session.", variant: "destructive", }); } } catch (error) { toast({ title: "Connection Error", description: "Could not connect to the backend server.", variant: "destructive", }); } }; const handleStartRecording = async () => { if ( !recordLeaderConfig || !recordFollowerConfig || !datasetRepoId || !singleTask ) { toast({ title: "Missing Configuration", description: "Please fill in all required fields: calibration configs, dataset ID, and task name.", variant: "destructive", }); return; } // ๐Ÿ”“ CRITICAL: Release all camera streams before backend accesses them if (cameras.length > 0 && releaseStreamsRef.current) { console.log("๐Ÿ”“ Releasing camera streams before starting recording..."); toast({ title: "Preparing Camera Resources", description: `Releasing ${cameras.length} camera stream(s) for recording...`, }); releaseStreamsRef.current(); // Wait a moment for camera resources to be fully released await new Promise((resolve) => setTimeout(resolve, 500)); console.log("โœ… Camera streams released, proceeding with recording..."); toast({ title: "Camera Resources Ready", description: "Camera streams released successfully. Starting recording...", }); } // Convert cameras to the LeRobot format const cameraDict = cameras.reduce((acc, cam) => { acc[cam.name] = { type: cam.type, camera_index: cam.camera_index, width: cam.width, height: cam.height, fps: cam.fps, }; return acc; }, {} as Record); const recordingConfig = { leader_port: recordLeaderPort, follower_port: recordFollowerPort, leader_config: recordLeaderConfig, follower_config: recordFollowerConfig, dataset_repo_id: datasetRepoId, single_task: singleTask, num_episodes: numEpisodes, episode_time_s: 60, reset_time_s: 15, fps: 30, video: true, push_to_hub: false, resume: false, cameras: cameraDict, }; setShowRecordingModal(false); navigate("/recording", { state: { recordingConfig } }); }; const handleStartDirectFollower = async () => { if (!directFollowerConfig) { toast({ title: "Missing Configuration", description: "Please select a calibration config for the follower.", variant: "destructive", }); return; } try { const response = await fetch("http://localhost:8000/direct-follower", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ follower_port: directFollowerPort, follower_config: directFollowerConfig, }), }); const data = await response.json(); if (response.ok) { toast({ title: "Direct Follower Control Started", description: data.message || "Successfully started direct follower control.", }); setShowDirectFollowerModal(false); navigate("/direct-follower"); } else { toast({ title: "Error Starting Direct Follower Control", description: data.message || "Failed to start direct follower control.", variant: "destructive", }); } } catch (error) { toast({ title: "Connection Error", description: "Could not connect to the backend server.", variant: "destructive", }); } }; const handlePermissions = async (allow: boolean) => { setShowPermissionModal(false); if (allow) { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true, }); stream.getTracks().forEach((track) => track.stop()); toast({ title: "Permissions Granted", description: "Camera and microphone access enabled. Entering control session...", }); navigate("/control"); } catch (error) { toast({ title: "Permission Denied", description: "Camera and microphone access is required for robot control.", variant: "destructive", }); } } else { toast({ title: "Permission Denied", description: "You can proceed, but with limited functionality.", variant: "destructive", }); navigate("/control"); } }; const actions: Action[] = [ { title: "Calibration", description: "Calibrate robot arm positions.", handler: handleCalibrationClick, color: "bg-indigo-500 hover:bg-indigo-600", isWorkInProgress: false, }, { title: "Teleoperation", description: "Control the robot arm in real-time.", handler: handleTeleoperationClick, color: "bg-yellow-500 hover:bg-yellow-600", }, { title: "Record Dataset", description: "Record episodes for training data.", handler: handleRecordingClick, color: "bg-red-500 hover:bg-red-600", }, { title: "Replay Dataset", description: "Replay and analyze recorded datasets.", handler: handleReplayDatasetClick, color: "bg-purple-500 hover:bg-purple-600", isWorkInProgress: true, }, { title: "Training", description: "Train a model on your datasets.", handler: handleTrainingClick, color: "bg-green-500 hover:bg-green-600", isWorkInProgress: true, }, { title: "Direct Follower Control", description: "Control robot arm with mouse movements.", handler: handleDirectFollowerClick, color: "bg-blue-500 hover:bg-blue-600", isWorkInProgress: true, }, ]; return (
setShowUsageModal(true)} />
); }; export default Landing;