Spaces:
Running
Running
| "use client"; | |
| import type React from "react"; | |
| import { | |
| Book, | |
| Code2, | |
| Terminal, | |
| Copy, | |
| Check, | |
| Server, | |
| Package, | |
| } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
| import { useState, createContext, useContext } from "react"; | |
| import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; | |
| import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; | |
| type CLIMode = "npm" | "npx"; | |
| const CLIModeContext = createContext<{ | |
| mode: CLIMode; | |
| setMode: (mode: CLIMode) => void; | |
| }>({ | |
| mode: "npx", | |
| setMode: () => {}, | |
| }); | |
| const CLIModeProvider = ({ children }: { children: React.ReactNode }) => { | |
| const [mode, setMode] = useState<CLIMode>("npx"); | |
| return ( | |
| <CLIModeContext.Provider value={{ mode, setMode }}> | |
| {children} | |
| </CLIModeContext.Provider> | |
| ); | |
| }; | |
| const CLIModeSwitch = ({ className = "" }: { className?: string }) => { | |
| const { mode, setMode } = useContext(CLIModeContext); | |
| return ( | |
| <div className={`flex gap-1 w-fit ${className}`}> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => setMode("npx")} | |
| className={`font-mono text-xs px-3 min-w-[60px] h-8 border transition-colors ${ | |
| mode === "npx" | |
| ? "bg-foreground text-background border-foreground" | |
| : "bg-transparent border-input hover:bg-accent hover:text-accent-foreground" | |
| }`} | |
| > | |
| npx | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => setMode("npm")} | |
| className={`font-mono text-xs px-3 min-w-[60px] h-8 border transition-colors ${ | |
| mode === "npm" | |
| ? "bg-foreground text-background border-foreground" | |
| : "bg-transparent border-input hover:bg-accent hover:text-accent-foreground" | |
| }`} | |
| > | |
| npm | |
| </Button> | |
| </div> | |
| ); | |
| }; | |
| const CLICodeBlock = ({ | |
| children, | |
| language = "bash", | |
| }: { | |
| children: React.ReactNode; | |
| language?: string; | |
| }) => { | |
| const { mode } = useContext(CLIModeContext); | |
| const [copied, setCopied] = useState(false); | |
| const copyToClipboard = async (text: string) => { | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| } catch (err) { | |
| console.error("Failed to copy:", err); | |
| } | |
| }; | |
| // Convert commands based on mode | |
| const convertCommands = (text: string) => { | |
| if (mode === "npx") { | |
| return text | |
| .replace(/^lerobot /gm, "npx lerobot@latest ") | |
| .replace(/# lerobot /gm, "# npx lerobot@latest "); | |
| } | |
| return text; | |
| }; | |
| const codeText = | |
| typeof children === "string" ? children : children?.toString() || ""; | |
| const convertedCode = convertCommands(codeText); | |
| return ( | |
| <div className="my-4"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <CLIModeSwitch /> | |
| </div> | |
| <div className="bg-slate-800 dark:bg-black/40 border border-border dark:border-white/10 rounded-md overflow-hidden relative"> | |
| <SyntaxHighlighter | |
| language={language} | |
| style={oneDark} | |
| customStyle={{ | |
| margin: 0, | |
| padding: "1rem", | |
| fontSize: "0.875rem", | |
| background: "transparent", | |
| backgroundColor: "transparent", | |
| fontFamily: | |
| "Geist Mono, ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace", | |
| }} | |
| wrapLines={true} | |
| wrapLongLines={true} | |
| codeTagProps={{ | |
| style: { | |
| background: "transparent", | |
| backgroundColor: "transparent", | |
| }, | |
| }} | |
| > | |
| {convertedCode} | |
| </SyntaxHighlighter> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => copyToClipboard(convertedCode)} | |
| className="absolute top-2 right-2 h-7 w-7 p-0 transition-all" | |
| > | |
| {copied ? ( | |
| <Check className="w-3 h-3" /> | |
| ) : ( | |
| <Copy className="w-3 h-3" /> | |
| )} | |
| </Button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const CodeBlock = ({ | |
| children, | |
| language = "typescript", | |
| }: { | |
| children: React.ReactNode; | |
| language?: string; | |
| }) => { | |
| const [copied, setCopied] = useState(false); | |
| const copyToClipboard = async (text: string) => { | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| } catch (err) { | |
| console.error("Failed to copy:", err); | |
| } | |
| }; | |
| const codeText = | |
| typeof children === "string" ? children : children?.toString() || ""; | |
| return ( | |
| <div className="bg-slate-800 dark:bg-black/40 border border-border dark:border-white/10 rounded-md overflow-hidden my-4 relative"> | |
| <SyntaxHighlighter | |
| language={language} | |
| style={oneDark} | |
| customStyle={{ | |
| margin: 0, | |
| padding: "1rem", | |
| fontSize: "0.875rem", | |
| background: "transparent", | |
| backgroundColor: "transparent", | |
| fontFamily: | |
| "Geist Mono, ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace", | |
| }} | |
| wrapLines={true} | |
| wrapLongLines={true} | |
| codeTagProps={{ | |
| style: { | |
| background: "transparent", | |
| backgroundColor: "transparent", | |
| }, | |
| }} | |
| > | |
| {codeText} | |
| </SyntaxHighlighter> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => copyToClipboard(codeText)} | |
| className="absolute top-2 right-2 h-7 w-7 p-0 transition-all" | |
| > | |
| {copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />} | |
| </Button> | |
| </div> | |
| ); | |
| }; | |
| export function DocsSection() { | |
| return ( | |
| <div className="font-mono"> | |
| <div className="mb-6"> | |
| <h2 className="text-3xl font-bold tracking-wider mb-2 uppercase flex items-center gap-3"> | |
| <Book className="w-6 h-6" /> | |
| Docs | |
| </h2> | |
| <p className="text-sm text-muted-foreground"> | |
| Complete API reference for all lerobot.js packages | |
| </p> | |
| </div> | |
| <div className="bg-muted/40 dark:bg-black/30 border border-border dark:border-white/10 p-4 md:p-6 lg:p-8 rounded-lg"> | |
| <Tabs defaultValue="web" className="w-full"> | |
| <TabsList className="grid w-full grid-cols-3 mb-8 h-auto p-2"> | |
| <TabsTrigger | |
| value="web" | |
| className="flex flex-col sm:flex-row items-center gap-2 sm:gap-3 h-20 sm:h-16 px-2 sm:px-4 data-[state=active]:bg-background" | |
| > | |
| <div className="w-6 h-6 sm:w-8 sm:h-8 bg-primary/20 rounded-lg flex items-center justify-center"> | |
| <Package className="w-4 h-4 sm:w-5 sm:h-5 text-primary" /> | |
| </div> | |
| <div className="text-center sm:text-left"> | |
| <div className="font-bold text-xs sm:text-sm uppercase tracking-wider"> | |
| WEB | |
| </div> | |
| <div className="text-xs text-muted-foreground hidden sm:block"> | |
| @lerobot/web | |
| </div> | |
| </div> | |
| </TabsTrigger> | |
| <TabsTrigger | |
| value="node" | |
| className="flex flex-col sm:flex-row items-center gap-2 sm:gap-3 h-20 sm:h-16 px-2 sm:px-4 data-[state=active]:bg-background" | |
| > | |
| <div className="w-6 h-6 sm:w-8 sm:h-8 bg-primary/20 rounded-lg flex items-center justify-center"> | |
| <Server className="w-4 h-4 sm:w-5 sm:h-5 text-primary" /> | |
| </div> | |
| <div className="text-center sm:text-left"> | |
| <div className="font-bold text-xs sm:text-sm uppercase tracking-wider"> | |
| NODE | |
| </div> | |
| <div className="text-xs text-muted-foreground hidden sm:block"> | |
| @lerobot/node | |
| </div> | |
| </div> | |
| </TabsTrigger> | |
| <TabsTrigger | |
| value="cli" | |
| className="flex flex-col sm:flex-row items-center gap-2 sm:gap-3 h-20 sm:h-16 px-2 sm:px-4 data-[state=active]:bg-background" | |
| > | |
| <div className="w-6 h-6 sm:w-8 sm:h-8 bg-primary/20 rounded-lg flex items-center justify-center"> | |
| <Terminal className="w-4 h-4 sm:w-5 sm:h-5 text-primary" /> | |
| </div> | |
| <div className="text-center sm:text-left"> | |
| <div className="font-bold text-xs sm:text-sm uppercase tracking-wider"> | |
| CLI | |
| </div> | |
| <div className="text-xs text-muted-foreground hidden sm:block"> | |
| lerobot | |
| </div> | |
| </div> | |
| </TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="web" className="space-y-14"> | |
| {/* Browser Requirements */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase"> | |
| Browser Requirements | |
| </h3> | |
| <div className="mt-4 space-y-2 text-sm"> | |
| <div> | |
| • <strong>Chromium 89+</strong> with WebSerial and WebUSB API | |
| support | |
| </div> | |
| <div> | |
| • <strong>HTTPS or localhost</strong> | |
| </div> | |
| <div> | |
| • <strong>User gesture</strong> required for initial port | |
| selection | |
| </div> | |
| </div> | |
| </div> | |
| {/* Quick Start */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2"> | |
| <Terminal className="w-5 h-5" /> | |
| Quick Start | |
| </h3> | |
| <p className="text-sm mt-2 mb-4"> | |
| Complete workflow from discovery to teleoperation. | |
| </p> | |
| <CodeBlock> | |
| {`import { findPort, releaseMotors, calibrate, teleoperate } from "@lerobot/web"; | |
| // 1. find and connect to hardware like a robot arm | |
| const findProcess = await findPort(); | |
| const robots = await findProcess.result; | |
| const robot = robots[0]; | |
| // 2. release the motors and put them into the homing position | |
| await releaseMotors(robot); | |
| // 3. calibrate the motors by moving each motor through its full range of motion | |
| const calibrationProcess = await calibrate({ | |
| robot, | |
| onProgress: (message) => console.log(message), | |
| onLiveUpdate: (data) => console.log("Live positions:", data), | |
| }); | |
| // when done, stop calibration and get the min/max ranges for each motor | |
| calibrationProcess.stop(); | |
| const calibrationData = await calibrationProcess.result; | |
| // 4. start controlling the robot arm with your keyboard | |
| const teleop = await teleoperate({ | |
| robot, | |
| calibrationData, | |
| teleop: { type: "keyboard" }, | |
| }); | |
| teleop.start(); | |
| // 5. stop control (run this when you're done) | |
| teleop.stop();`} | |
| </CodeBlock> | |
| </div> | |
| {/* API Reference */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2"> | |
| <Code2 className="w-5 h-5" /> | |
| API Reference | |
| </h3> | |
| <div className="space-y-12 mt-4"> | |
| {/* findPort */} | |
| <div> | |
| <h4 className="font-bold text-primary">findPort(config?)</h4> | |
| <p className="text-sm mt-1"> | |
| Discovers and connects to robotics hardware using WebSerial | |
| API. | |
| </p> | |
| <CodeBlock> | |
| {`// Interactive Mode (Default) - Shows port dialog | |
| const findProcess = await findPort(); | |
| const robots = await findProcess.result; // RobotConnection[] | |
| // Auto-Connect Mode - Reconnects to known robots | |
| const findProcess = await findPort({ | |
| robotConfigs: [ | |
| { robotType: "so100_follower", robotId: "left_arm", serialNumber: "USB123" } | |
| ], | |
| onMessage: (msg) => console.log(msg), | |
| });`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Options | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>robotConfigs?: RobotConfig[]</code> - | |
| Auto-connect to these known robots | |
| </li> | |
| <li> | |
| • <code>onMessage?: (message: string) => void</code>{" "} | |
| - Progress messages callback | |
| </li> | |
| </ul> | |
| </div> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Returns:{" "} | |
| <code className="bg-muted/50 px-1 rounded"> | |
| FindPortProcess | |
| </code> | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>result: Promise<RobotConnection[]></code>{" "} | |
| - Array of robot connections | |
| </li> | |
| <li> | |
| • <code>stop(): void</code> - Cancel discovery process | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* calibrate */} | |
| <div> | |
| <h4 className="font-bold text-primary">calibrate(config)</h4> | |
| <p className="text-sm mt-1"> | |
| Calibrates motor homing offsets and records range of motion. | |
| </p> | |
| <CodeBlock> | |
| {`const calibrationProcess = await calibrate({ | |
| robot, | |
| onProgress: (message) => { | |
| console.log(message); // "⚙️ Setting motor homing offsets" | |
| }, | |
| onLiveUpdate: (data) => { | |
| // Real-time motor positions during range recording | |
| Object.entries(data).forEach(([motor, info]) => { | |
| console.log(\`\${motor}: \${info.current} (range: \${info.range})\`); | |
| }); | |
| }, | |
| }); | |
| // Move robot through full range of motion... | |
| calibrationProcess.stop(); // Stop range recording | |
| const calibrationData = await calibrationProcess.result;`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Options | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>robot: RobotConnection</code> - Connected robot | |
| from findPort() | |
| </li> | |
| <li> | |
| • <code>onProgress?: (message: string) => void</code>{" "} | |
| - Progress messages callback | |
| </li> | |
| <li> | |
| •{" "} | |
| <code> | |
| onLiveUpdate?: (data: LiveCalibrationData) => void | |
| </code>{" "} | |
| - Real-time position updates | |
| </li> | |
| </ul> | |
| </div> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Returns:{" "} | |
| <code className="bg-muted/50 px-1 rounded"> | |
| CalibrationProcess | |
| </code> | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>result: Promise<CalibrationResults></code>{" "} | |
| - Python-compatible format | |
| </li> | |
| <li> | |
| • <code>stop(): void</code> - Stop calibration process | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* teleoperate */} | |
| <div> | |
| <h4 className="font-bold text-primary"> | |
| teleoperate(config) | |
| </h4> | |
| <p className="text-sm mt-1"> | |
| Enables real-time robot control with extensible input | |
| devices. | |
| </p> | |
| <CodeBlock> | |
| {`// Keyboard Teleoperation | |
| const keyboardTeleop = await teleoperate({ | |
| robot, | |
| calibrationData: savedCalibrationData, | |
| teleop: { type: "keyboard" }, | |
| onStateUpdate: (state) => { | |
| console.log(\`Active: \${state.isActive}\`); | |
| console.log(\`Motors:\`, state.motorConfigs); | |
| }, | |
| }); | |
| // Direct Teleoperation | |
| const directTeleop = await teleoperate({ | |
| robot, | |
| calibrationData: savedCalibrationData, | |
| teleop: { type: "direct" }, | |
| });`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Options | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>robot: RobotConnection</code> - Connected robot | |
| from findPort() | |
| </li> | |
| <li> | |
| • <code>teleop: TeleoperatorConfig</code> - Teleoperator | |
| configuration: | |
| <ul className="mt-1 ml-4 space-y-1"> | |
| <li> | |
| • <code>{`{ type: "keyboard" }`}</code> - Keyboard | |
| control | |
| </li> | |
| <li> | |
| • <code>{`{ type: "direct" }`}</code> - Direct | |
| programmatic control | |
| </li> | |
| </ul> | |
| </li> | |
| <li> | |
| •{" "} | |
| <code> | |
| calibrationData?: {`{ [motorName: string]: any }`} | |
| </code>{" "} | |
| - Calibration data from calibrate() | |
| </li> | |
| <li> | |
| •{" "} | |
| <code> | |
| onStateUpdate?: (state: TeleoperationState) => void | |
| </code>{" "} | |
| - State change callback | |
| </li> | |
| </ul> | |
| </div> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Returns:{" "} | |
| <code className="bg-muted/50 px-1 rounded"> | |
| TeleoperationProcess | |
| </code> | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>start(): void</code> - Begin teleoperation | |
| </li> | |
| <li> | |
| • <code>stop(): void</code> - Stop teleoperation and | |
| clear states | |
| </li> | |
| <li> | |
| • <code>getState(): TeleoperationState</code> - Current | |
| state and motor positions | |
| </li> | |
| <li> | |
| • <code>teleoperator: BaseWebTeleoperator</code> - | |
| Access teleoperator-specific methods | |
| </li> | |
| <li> | |
| • <code>disconnect(): Promise<void></code> - Stop | |
| and disconnect | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* releaseMotors */} | |
| <div> | |
| <h4 className="font-bold text-primary"> | |
| releaseMotors(robot, motorIds?) | |
| </h4> | |
| <p className="text-sm mt-1"> | |
| Releases motor torque so robot can be moved freely by hand. | |
| </p> | |
| <CodeBlock> | |
| {`// Release all motors for calibration | |
| await releaseMotors(robot); | |
| // Release specific motors only | |
| await releaseMotors(robot, [1, 2, 3]);`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Options | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>robot: RobotConnection</code> - Connected robot | |
| </li> | |
| <li> | |
| • <code>motorIds?: number[]</code> - Specific motor IDs | |
| (default: all motors for robot type) | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* record */} | |
| <div> | |
| <h4 className="font-bold text-primary">record(config)</h4> | |
| <p className="text-sm mt-1"> | |
| Records robot teleoperator data and camera streams in the LeRobot dataset format. | |
| </p> | |
| <CodeBlock> | |
| {`import { teleoperate, record } from "@lerobot/web"; | |
| // Start teleoperation first | |
| const teleoperationProcess = await teleoperate({ | |
| robot: connectedRobot, | |
| teleop: { type: "keyboard" }, | |
| calibrationData: calibrationData, | |
| }); | |
| // Create recording with teleoperator | |
| const recordProcess = await record({ | |
| teleoperator: teleoperationProcess.teleoperator, | |
| videoStreams: { | |
| main: mainCameraStream, | |
| wrist: wristCameraStream, | |
| }, | |
| robotType: "so100", | |
| options: { | |
| fps: 30, | |
| taskDescription: "Pick and place task", | |
| onStateUpdate: (state) => { | |
| console.log(\`Recording: \${state.frameCount} frames\`); | |
| }, | |
| }, | |
| }); | |
| // Start both recording and teleoperation | |
| teleoperationProcess.start(); | |
| recordProcess.start(); | |
| // Manage recording during operation | |
| recordProcess.nextEpisode(); // Start new episode if needed | |
| // Stop recording when finished | |
| const robotData = await recordProcess.stop(); | |
| await recordProcess.exportForLeRobot("zip-download");`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Options | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>teleoperator: WebTeleoperator</code> - The teleoperator to record from | |
| </li> | |
| <li> | |
| • <code>videoStreams?: {`{ [name: string]: MediaStream }`}</code> - Optional camera streams | |
| </li> | |
| <li> | |
| • <code>robotType?: string</code> - Robot metadata (e.g., "so100") | |
| </li> | |
| <li> | |
| • <code>options?: RecordOptions</code> - Optional configuration: | |
| <ul className="mt-1 ml-4 space-y-1"> | |
| <li> | |
| • <code>fps?: number</code> - Target frames per second (default: 30) | |
| </li> | |
| <li> | |
| • <code>taskDescription?: string</code> - Task description | |
| </li> | |
| <li> | |
| • <code>onStateUpdate?: (state: RecordingState) => void</code> - State changes | |
| </li> | |
| </ul> | |
| </li> | |
| </ul> | |
| </div> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Returns: RecordProcess | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>start(): void</code> - Start recording | |
| </li> | |
| <li> | |
| • <code>stop(): Promise<RobotRecordingData></code> - Stop recording | |
| </li> | |
| <li> | |
| • <code>getState(): RecordingState</code> - Current recording state | |
| </li> | |
| <li> | |
| • <code>getEpisodeCount(): number</code> - Get number of episodes | |
| </li> | |
| <li> | |
| • <code>nextEpisode(): Promise<number></code> - Start new episode | |
| </li> | |
| <li> | |
| • <code>clearEpisodes(): void</code> - Delete all episodes | |
| </li> | |
| <li> | |
| • <code>addCamera(name: string, stream: MediaStream): void</code> - Add camera dynamically | |
| </li> | |
| <li> | |
| • <code>exportForLeRobot(format?: "blobs" | "zip" | "zip-download"): Promise<any></code> - Export dataset | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="node" className="space-y-14"> | |
| {/* Node.js Requirements */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase"> | |
| Node.js Requirements | |
| </h3> | |
| <div className="mt-4 space-y-2 text-sm"> | |
| <div> | |
| • <strong>Node.js 18+</strong> (Windows, macOS, Linux) | |
| </div> | |
| <div> | |
| • <strong>Serial port access</strong> (may require | |
| permissions) | |
| </div> | |
| <div> | |
| • <strong>SO-100 robot hardware</strong> with USB connection | |
| </div> | |
| </div> | |
| </div> | |
| {/* Quick Start */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2"> | |
| <Terminal className="w-5 h-5" /> | |
| Quick Start | |
| </h3> | |
| <p className="text-sm mt-2 mb-4"> | |
| Complete workflow from discovery to teleoperation in Node.js. | |
| </p> | |
| <CodeBlock> | |
| {`import { findPort, connectPort, releaseMotors, calibrate, teleoperate } from "@lerobot/node"; | |
| // 1. find available robot ports | |
| console.log("🔍 finding available robot ports..."); | |
| const findProcess = await findPort(); | |
| const robots = await findProcess.result; | |
| if (robots.length === 0) { | |
| console.log("❌ no robots found. check connections."); | |
| process.exit(1); | |
| } | |
| // 2. connect to the first robot found | |
| console.log(\`✅ found \${robots.length} robot(s). connecting to first one...\`); | |
| const robot = await connectPort(robots[0].path, robots[0].robotType); | |
| // 3. release motors for manual positioning | |
| console.log("🔓 releasing motors for manual positioning..."); | |
| await releaseMotors(robot); | |
| // 4. calibrate motors by moving through full range | |
| console.log("⚙️ starting calibration..."); | |
| const calibrationProcess = await calibrate({ | |
| robot, | |
| onProgress: (message) => console.log(message), | |
| onLiveUpdate: (data) => console.log("live positions:", data), | |
| }); | |
| // move robot through its range, then stop calibration | |
| console.log("👋 move robot through full range, press enter when done..."); | |
| process.stdin.once("data", () => { | |
| calibrationProcess.stop(); | |
| }); | |
| const calibrationData = await calibrationProcess.result; | |
| console.log("✅ calibration complete!"); | |
| // 5. control robot with keyboard | |
| console.log("🎮 starting keyboard control..."); | |
| const teleop = await teleoperate({ | |
| robot, | |
| calibrationData, | |
| teleop: { type: "keyboard" }, | |
| }); | |
| teleop.start();`} | |
| </CodeBlock> | |
| </div> | |
| {/* How It Works */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase"> | |
| How It Works | |
| </h3> | |
| <div className="mt-4 space-y-6"> | |
| <div> | |
| <h4 className="font-bold text-primary"> | |
| Beginner Flow: findPort() → connectPort() → Use Robot | |
| </h4> | |
| <p className="text-sm mt-1 mb-3"> | |
| Most users should start with findPort() for discovery, then | |
| connectPort() for connection: | |
| </p> | |
| <CodeBlock> | |
| {`// ✅ recommended: discover then connect | |
| const findProcess = await findPort(); | |
| const robots = await findProcess.result; | |
| const robot = await connectPort(robots[0].path, robots[0].robotType);`} | |
| </CodeBlock> | |
| </div> | |
| <div> | |
| <h4 className="font-bold text-primary"> | |
| Advanced: Direct Connection with connectPort() | |
| </h4> | |
| <p className="text-sm mt-1 mb-3"> | |
| Only use connectPort() when you already know the exact port: | |
| </p> | |
| <CodeBlock> | |
| {`// ⚡ advanced: direct connection to known port | |
| const robot = await connectPort("/dev/ttyUSB0", "so100_follower");`} | |
| </CodeBlock> | |
| </div> | |
| </div> | |
| </div> | |
| {/* API Reference */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2"> | |
| <Code2 className="w-5 h-5" /> | |
| API Reference | |
| </h3> | |
| <div className="space-y-12 mt-4"> | |
| {/* findPort */} | |
| <div> | |
| <h4 className="font-bold text-primary">findPort(config?)</h4> | |
| <p className="text-sm mt-1"> | |
| Discovers available robotics hardware on serial ports. | |
| Unlike the web version, this only discovers ports - | |
| connection happens separately with connectPort(). | |
| </p> | |
| <CodeBlock> | |
| {`// Discover all available robots | |
| const findProcess = await findPort(); | |
| const robots = await findProcess.result; | |
| console.log(\`Found \${robots.length} robot(s):\`); | |
| robots.forEach((robot) => { | |
| console.log(\`- \${robot.robotType} on \${robot.path}\`); | |
| }); | |
| // Connect to specific robot | |
| const robot = await connectPort(robots[0].path, robots[0].robotType);`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Returns: FindPortProcess | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>result: Promise<DiscoveredRobot[]></code>{" "} | |
| - Array of discovered robots with path, robotType, and | |
| other metadata | |
| </li> | |
| <li> | |
| • <code>stop(): void</code> - Cancel discovery process | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* connectPort */} | |
| <div> | |
| <h4 className="font-bold text-primary"> | |
| connectPort(port, robotType, robotId?) | |
| </h4> | |
| <p className="text-sm mt-1"> | |
| Creates a connection to a robot on the specified serial | |
| port. | |
| </p> | |
| <CodeBlock> | |
| {`// Connect to SO-100 follower arm | |
| const robot = await connectPort( | |
| "/dev/ttyUSB0", // Serial port path | |
| "so100_follower", // Robot type | |
| "my_robot_arm" // Custom robot ID | |
| ); | |
| // Windows | |
| const robot = await connectPort("COM4", "so100_follower", "my_robot"); | |
| // Connection is ready to use | |
| console.log(\`Connected to \${robot.robotType} on \${robot.port.path}\`);`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Parameters | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>port: string</code> - Serial port path (e.g., | |
| /dev/ttyUSB0, COM4) | |
| </li> | |
| <li> | |
| •{" "} | |
| <code> | |
| robotType: "so100_follower" | "so100_leader" | |
| </code>{" "} | |
| - Type of robot | |
| </li> | |
| <li> | |
| • <code>robotId?: string</code> - Custom identifier for | |
| your robot | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* calibrate */} | |
| <div> | |
| <h4 className="font-bold text-primary">calibrate(config)</h4> | |
| <p className="text-sm mt-1"> | |
| Calibrates motor homing offsets and records range of motion. | |
| </p> | |
| <CodeBlock> | |
| {`const calibrationProcess = await calibrate({ | |
| robot, | |
| onProgress: (message) => { | |
| console.log(message); // "⚙️ Setting motor homing offsets" | |
| }, | |
| onLiveUpdate: (data) => { | |
| // Real-time motor positions during range recording | |
| Object.entries(data).forEach(([motor, info]) => { | |
| console.log(\`\${motor}: \${info.current} (range: \${info.range})\`); | |
| }); | |
| }, | |
| }); | |
| // Move robot through full range of motion... | |
| // When finished, stop calibration | |
| calibrationProcess.stop(); | |
| const calibrationData = await calibrationProcess.result; | |
| // Save to file (Python-compatible format) | |
| import { writeFileSync } from "fs"; | |
| writeFileSync( | |
| "./my_robot_calibration.json", | |
| JSON.stringify(calibrationData, null, 2) | |
| );`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Returns: CalibrationProcess | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>result: Promise<CalibrationResults></code>{" "} | |
| - <strong>Python-compatible</strong> calibration data | |
| </li> | |
| <li> | |
| • <code>stop(): void</code> - Stop calibration process | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* teleoperate */} | |
| <div> | |
| <h4 className="font-bold text-primary"> | |
| teleoperate(config) | |
| </h4> | |
| <p className="text-sm mt-1"> | |
| Real-time robot control with keyboard input.{" "} | |
| <strong>Smooth, responsive movement</strong> optimized for | |
| Node.js. | |
| </p> | |
| <CodeBlock> | |
| {`const teleop = await teleoperate({ | |
| robot, | |
| teleop: { type: "keyboard" }, | |
| onStateUpdate: (state) => { | |
| console.log(\`Active: \${state.isActive}\`); | |
| state.motorConfigs.forEach((motor) => { | |
| console.log(\`\${motor.name}: \${motor.currentPosition}\`); | |
| }); | |
| }, | |
| }); | |
| // Start keyboard control | |
| teleop.start(); | |
| // Control will be active until stopped | |
| setTimeout(() => teleop.stop(), 60000);`} | |
| </CodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Keyboard Controls (SO-100) | |
| </h5> | |
| <div className="mt-1 ml-4 text-sm font-mono"> | |
| <div>Arrow Keys: Shoulder pan/lift</div> | |
| <div>WASD: Elbow flex, wrist flex</div> | |
| <div>Q/E: Wrist roll</div> | |
| <div>O/C: Gripper open/close</div> | |
| <div>ESC: Emergency stop</div> | |
| <div>Ctrl+C: Exit</div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* releaseMotors */} | |
| <div> | |
| <h4 className="font-bold text-primary"> | |
| releaseMotors(robot) | |
| </h4> | |
| <p className="text-sm mt-1"> | |
| Releases motor torque so robot can be moved freely by hand. | |
| </p> | |
| <CodeBlock> | |
| {`// Release all motors for calibration | |
| await releaseMotors(robot); | |
| console.log("Motors released - you can now move the robot freely");`} | |
| </CodeBlock> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Serial Port Permissions */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase"> | |
| Serial Port Permissions | |
| </h3> | |
| <div className="mt-4 space-y-4"> | |
| <div> | |
| <h4 className="font-bold text-primary">Linux/macOS</h4> | |
| <CodeBlock language="bash"> | |
| {`# Add user to dialout group (Linux) | |
| sudo usermod -a -G dialout $USER | |
| # Set permissions (macOS) | |
| sudo chmod 666 /dev/tty.usbserial-*`} | |
| </CodeBlock> | |
| </div> | |
| <div> | |
| <h4 className="font-bold text-primary">Windows</h4> | |
| <p className="text-sm">No additional setup required.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="cli" className="space-y-14"> | |
| <CLIModeProvider> | |
| {/* Commands */} | |
| <div> | |
| <h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2"> | |
| <Code2 className="w-5 h-5" /> | |
| Available Commands | |
| </h3> | |
| <div className="space-y-8 mt-4"> | |
| {/* find-port */} | |
| <div> | |
| <h4 className="font-bold text-primary">find-port</h4> | |
| <p className="text-sm mt-1"> | |
| Interactive port discovery using cable detection. | |
| </p> | |
| <CLICodeBlock language="bash"> | |
| {`lerobot find-port`} | |
| </CLICodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Interactive Process | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li>• Lists current ports</li> | |
| <li>• Prompts to unplug USB cable</li> | |
| <li>• Detects which port disappeared</li> | |
| <li>• Prompts to reconnect cable</li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* calibrate */} | |
| <div> | |
| <h4 className="font-bold text-primary">calibrate</h4> | |
| <p className="text-sm mt-1"> | |
| Calibrate robot motors and save calibration data. | |
| </p> | |
| <CLICodeBlock language="bash"> | |
| {`lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 --robot.id=my_arm`} | |
| </CLICodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Options | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>--robot.type</code> - Robot type | |
| (so100_follower, so100_leader) | |
| </li> | |
| <li> | |
| • <code>--robot.port</code> - Serial port | |
| (/dev/ttyUSB0, COM4) | |
| </li> | |
| <li> | |
| • <code>--robot.id</code> - Robot identifier (default: | |
| "default") | |
| </li> | |
| <li> | |
| • <code>--output</code> - Custom output path for | |
| calibration file | |
| </li> | |
| </ul> | |
| </div> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Storage Location | |
| </h5> | |
| <div className="mt-1 ml-4 text-sm font-mono"> | |
| ~/.cache/huggingface/lerobot/calibration/robots/{robot_type}/{robot_id}.json | |
| </div> | |
| </div> | |
| </div> | |
| {/* teleoperate */} | |
| <div> | |
| <h4 className="font-bold text-primary">teleoperate</h4> | |
| <p className="text-sm mt-1"> | |
| Control robot through keyboard input. | |
| </p> | |
| <CLICodeBlock language="bash"> | |
| {`lerobot teleoperate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 --robot.id=my_arm`} | |
| </CLICodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Options | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>--robot.type</code> - Robot type (e.g., | |
| so100_follower) | |
| </li> | |
| <li> | |
| • <code>--robot.port</code> - Serial port (e.g., | |
| /dev/ttyUSB0, COM4) | |
| </li> | |
| <li> | |
| • <code>--robot.id</code> - Robot identifier (default: | |
| "default") | |
| </li> | |
| <li> | |
| • <code>--teleop.type</code> - Teleoperator type | |
| (default: "keyboard") | |
| </li> | |
| <li> | |
| • <code>--duration</code> - Duration in seconds, 0 = | |
| unlimited (default: "0") | |
| </li> | |
| </ul> | |
| </div> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Keyboard Controls | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>w/s</code> - Elbow flex/extend | |
| </li> | |
| <li> | |
| • <code>a/d</code> - Wrist down/up | |
| </li> | |
| <li> | |
| • <code>q/e</code> - Wrist roll left/right | |
| </li> | |
| <li> | |
| • <code>o/c</code> - Gripper open/close | |
| </li> | |
| <li> | |
| • <code>Arrow keys</code> - Shoulder lift/pan | |
| </li> | |
| <li> | |
| • <code>Ctrl+C</code> - Stop and exit | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* release-motors */} | |
| <div> | |
| <h4 className="font-bold text-primary">release-motors</h4> | |
| <p className="text-sm mt-1"> | |
| Release robot motors for manual positioning. | |
| </p> | |
| <CLICodeBlock language="bash"> | |
| {`lerobot release-motors --robot.type=so100_follower --robot.port=/dev/ttyUSB0 --robot.id=my_arm`} | |
| </CLICodeBlock> | |
| <div className="mt-3"> | |
| <h5 className="font-bold text-sm tracking-wider"> | |
| Options | |
| </h5> | |
| <ul className="mt-1 ml-4 space-y-1 text-sm"> | |
| <li> | |
| • <code>--robot.type</code> - Robot type (e.g., | |
| so100_follower) | |
| </li> | |
| <li> | |
| • <code>--robot.port</code> - Serial port (e.g., | |
| /dev/ttyUSB0, COM4) | |
| </li> | |
| <li> | |
| • <code>--robot.id</code> - Robot identifier (default: | |
| "default") | |
| </li> | |
| <li> | |
| • <code>--motors</code> - Specific motor IDs to | |
| release (comma-separated) | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </CLIModeProvider> | |
| </TabsContent> | |
| </Tabs> | |
| </div> | |
| </div> | |
| ); | |
| } | |