import { useState, useEffect } from "react"; import { Button } from "./ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card"; import { Alert, AlertDescription } from "./ui/alert"; import { Badge } from "./ui/badge"; import { findPort, isWebSerialSupported } from "@lerobot/web"; import type { RobotConnection } from "@lerobot/web"; interface PortManagerProps { connectedRobots: RobotConnection[]; onConnectedRobotsChange: (robots: RobotConnection[]) => void; onCalibrate?: (port: any) => void; // Let library handle port type onTeleoperate?: (robot: RobotConnection) => void; } export function PortManager({ connectedRobots, onConnectedRobotsChange, onCalibrate, onTeleoperate, }: PortManagerProps) { const [isFindingPorts, setIsFindingPorts] = useState(false); const [findPortsLog, setFindPortsLog] = useState([]); const [error, setError] = useState(null); // Load saved robots on mount by calling findPort with saved data useEffect(() => { loadSavedRobots(); }, []); const loadSavedRobots = async () => { try { console.log("🔄 Loading saved robots from localStorage..."); // Load saved robot configs for auto-connect mode const robotConfigs: any[] = []; const { getUnifiedRobotData } = await import("../lib/unified-storage"); // Check localStorage for saved robot data for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith("lerobotjs-")) { const serialNumber = key.replace("lerobotjs-", ""); const robotData = getUnifiedRobotData(serialNumber); if (robotData) { console.log( `✅ Found saved robot: ${robotData.device_info.robotId}` ); // Create robot config for auto-connect mode robotConfigs.push({ robotType: robotData.device_info.robotType, robotId: robotData.device_info.robotId, serialNumber: serialNumber, }); } } } if (robotConfigs.length > 0) { console.log( `🔄 Auto-connecting to ${robotConfigs.length} saved robots...` ); // Use auto-connect mode - NO DIALOG will be shown! const findPortProcess = await findPort({ robotConfigs, onMessage: (message) => { console.log(`Auto-connect: ${message}`); }, }); const reconnectedRobots = await findPortProcess.result; console.log( `✅ Auto-connected to ${ reconnectedRobots.filter((r) => r.isConnected).length }/${robotConfigs.length} saved robots` ); onConnectedRobotsChange(reconnectedRobots); } else { console.log("No saved robots found in localStorage"); } } catch (error) { console.error("Failed to load saved robots:", error); } }; const handleFindPorts = async () => { if (!isWebSerialSupported()) { setError("Web Serial API is not supported in this browser"); return; } try { setIsFindingPorts(true); setFindPortsLog([]); setError(null); // Use clean library API - library handles everything! const findPortProcess = await findPort({ onMessage: (message) => { setFindPortsLog((prev) => [...prev, message]); }, }); const robotConnections = await findPortProcess.result; // Add new robots to the list (avoid duplicates) const newRobots = robotConnections.filter( (newRobot) => !connectedRobots.some( (existing) => existing.serialNumber === newRobot.serialNumber ) ); onConnectedRobotsChange([...connectedRobots, ...newRobots]); setFindPortsLog((prev) => [ ...prev, `✅ Found ${newRobots.length} new robots`, ]); } catch (error) { if ( error instanceof Error && (error.message.includes("cancelled") || error.name === "NotAllowedError") ) { console.log("Port discovery cancelled by user"); return; } setError(error instanceof Error ? error.message : "Failed to find ports"); } finally { setIsFindingPorts(false); } }; const handleDisconnect = (index: number) => { const updatedRobots = connectedRobots.filter((_, i) => i !== index); onConnectedRobotsChange(updatedRobots); }; const handleCalibrate = (robot: RobotConnection) => { if (!robot.robotType || !robot.robotId) { setError("Please configure robot type and ID first"); return; } if (onCalibrate) { onCalibrate(robot.port); } }; const handleTeleoperate = (robot: RobotConnection) => { if (!robot.robotType || !robot.robotId) { setError("Please configure robot type and ID first"); return; } if (!robot.isConnected || !robot.port) { setError( "Robot is not connected. Please use 'Find & Connect Robots' first." ); return; } // Robot is connected, proceed with teleoperation if (onTeleoperate) { onTeleoperate(robot); } }; return ( 🔌 Robot Connection Manager Find and connect to your robot devices
{/* Error Display */} {error && ( {error} )} {/* Find Ports Button */} {/* Find Ports Log */} {findPortsLog.length > 0 && (
{findPortsLog.map((log, index) => (
{log}
))}
)} {/* Connected Robots */}

Connected Robots ({connectedRobots.length})

{connectedRobots.length === 0 ? (
🤖

No robots found

Click "Find & Connect Robots" to discover devices

) : (
{connectedRobots.map((robot, index) => ( handleDisconnect(index)} onCalibrate={() => handleCalibrate(robot)} onTeleoperate={() => handleTeleoperate(robot)} /> ))}
)}
); } interface RobotCardProps { robot: RobotConnection; onDisconnect: () => void; onCalibrate: () => void; onTeleoperate: () => void; } function RobotCard({ robot, onDisconnect, onCalibrate, onTeleoperate, }: RobotCardProps) { const [calibrationStatus, setCalibrationStatus] = useState<{ timestamp: string; readCount: number; } | null>(null); const [isEditing, setIsEditing] = useState(false); const [editRobotType, setEditRobotType] = useState< "so100_follower" | "so100_leader" >(robot.robotType || "so100_follower"); const [editRobotId, setEditRobotId] = useState(robot.robotId || ""); const isConfigured = robot.robotType && robot.robotId; // Check calibration status using unified storage useEffect(() => { const checkCalibrationStatus = async () => { if (!robot.serialNumber) return; try { const { getCalibrationStatus } = await import("../lib/unified-storage"); const status = getCalibrationStatus(robot.serialNumber); setCalibrationStatus(status); } catch (error) { console.warn("Failed to check calibration status:", error); } }; checkCalibrationStatus(); }, [robot.serialNumber]); const handleSaveConfig = async () => { if (!editRobotId.trim() || !robot.serialNumber) return; try { const { saveRobotConfig } = await import("../lib/unified-storage"); saveRobotConfig( robot.serialNumber, editRobotType, editRobotId.trim(), robot.usbMetadata ); // Update the robot object (this should trigger a re-render) robot.robotType = editRobotType; robot.robotId = editRobotId.trim(); setIsEditing(false); console.log("✅ Robot configuration saved"); } catch (error) { console.error("Failed to save robot configuration:", error); } }; const handleCancelEdit = () => { setEditRobotType(robot.robotType || "so100_follower"); setEditRobotId(robot.robotId || ""); setIsEditing(false); }; return (
{/* Header */}
{robot.robotId || robot.name || "Unnamed Robot"} {robot.robotType?.replace("_", " ") || "Not configured"} {robot.serialNumber && ( {robot.serialNumber.length > 20 ? robot.serialNumber.substring(0, 20) + "..." : robot.serialNumber} )}
{robot.isConnected ? "Connected" : "Available"} {calibrationStatus && ( ✅ Calibrated )}
{/* Robot Configuration Display (when not editing) */} {!isEditing && isConfigured && (
{robot.robotId}
{robot.robotType?.replace("_", " ")}
)} {/* Configuration Prompt for unconfigured robots */} {!isEditing && !isConfigured && (
Robot needs configuration before use
)} {/* Robot Configuration Form (when editing) */} {isEditing && (
setEditRobotId(e.target.value)} placeholder="e.g., my_robot" className="w-full px-2 py-1 border rounded text-sm" />
)} {/* Calibration Status */} {isConfigured && !isEditing && (
{calibrationStatus ? ( Last calibrated:{" "} {new Date(calibrationStatus.timestamp).toLocaleDateString()} ({calibrationStatus.readCount} readings) ) : ( Not calibrated yet )}
)} {/* Actions */} {isConfigured && !isEditing && (
)}
); }