import React, { useState, useEffect, useCallback, useRef } from "react"; import { Button } from "./ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card"; import { Badge } from "./ui/badge"; import { calibrateWithPort } from "../../lerobot/web/calibrate"; import type { ConnectedRobot } from "../types"; interface CalibrationPanelProps { robot: ConnectedRobot; onFinish: () => void; } interface MotorCalibrationData { name: string; current: number; min: number; max: number; range: number; } export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) { const [isCalibrating, setIsCalibrating] = useState(false); const [motorData, setMotorData] = useState([]); const [calibrationStatus, setCalibrationStatus] = useState("Ready to calibrate"); const [calibrationComplete, setCalibrationComplete] = useState(false); const [readCount, setReadCount] = useState(0); const animationFrameRef = useRef(); const lastReadTime = useRef(0); const isReading = useRef(false); // Motor names matching Node CLI exactly const motorNames = [ "waist", "shoulder", "elbow", "forearm_roll", "wrist_angle", "wrist_rotate", ]; // Initialize motor data with center positions const initializeMotorData = useCallback(() => { const initialData = motorNames.map((name) => ({ name, current: 2047, // Center position for STS3215 (4095/2) min: 2047, max: 2047, range: 0, })); setMotorData(initialData); setReadCount(0); }, []); // Keep track of last known good positions to avoid glitches const lastKnownPositions = useRef([ 2047, 2047, 2047, 2047, 2047, 2047, ]); // Read actual motor positions with robust error handling const readMotorPositions = useCallback(async (): Promise => { if (!robot.port || !robot.port.readable || !robot.port.writable) { throw new Error("Robot port not available for communication"); } const positions: number[] = []; const motorIds = [1, 2, 3, 4, 5, 6]; // Get persistent reader/writer for this session const reader = robot.port.readable.getReader(); const writer = robot.port.writable.getWriter(); try { for (let index = 0; index < motorIds.length; index++) { const motorId = motorIds[index]; let success = false; let retries = 2; // Allow 2 retries per motor while (!success && retries > 0) { try { // Create STS3215 Read Position packet const packet = new Uint8Array([ 0xff, 0xff, motorId, 0x04, 0x02, 0x38, 0x02, 0x00, ]); const checksum = ~(motorId + 0x04 + 0x02 + 0x38 + 0x02) & 0xff; packet[7] = checksum; // Write packet await writer.write(packet); // Wait for response await new Promise((resolve) => setTimeout(resolve, 10)); // Read with timeout const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 100) ); const result = (await Promise.race([ reader.read(), timeoutPromise, ])) as ReadableStreamReadResult; if ( result && !result.done && result.value && result.value.length >= 7 ) { const response = result.value; const responseId = response[2]; const error = response[4]; // Check if this is the response we're looking for if (responseId === motorId && error === 0) { const position = response[5] | (response[6] << 8); positions.push(position); lastKnownPositions.current[index] = position; // Update last known good position success = true; } else { // Wrong motor ID or error - might be out of sync, try again retries--; await new Promise((resolve) => setTimeout(resolve, 5)); } } else { retries--; await new Promise((resolve) => setTimeout(resolve, 5)); } } catch (error) { retries--; await new Promise((resolve) => setTimeout(resolve, 5)); } } if (!success) { // Use last known good position instead of fallback center position positions.push(lastKnownPositions.current[index]); } // Small delay between motors await new Promise((resolve) => setTimeout(resolve, 2)); } } finally { reader.releaseLock(); writer.releaseLock(); } return positions; }, [robot.port]); // Update motor data with new readings - NO SIMULATION, REAL VALUES ONLY const updateMotorData = useCallback(async () => { if (!isCalibrating || isReading.current) return; const now = performance.now(); // Read at ~15Hz to reduce serial communication load (66ms intervals) if (now - lastReadTime.current < 66) return; lastReadTime.current = now; isReading.current = true; try { const positions = await readMotorPositions(); // Always update since we're now keeping last known good positions // Only show warning if all motors are still at center position (no successful reads yet) const allAtCenter = positions.every((pos) => pos === 2047); if (allAtCenter && readCount === 0) { console.log("No motor data received yet - still trying to connect"); setCalibrationStatus("Connecting to motors - please wait..."); } setMotorData((prev) => prev.map((motor, index) => { const current = positions[index]; const min = Math.min(motor.min, current); const max = Math.max(motor.max, current); const range = max - min; return { ...motor, current, min, max, range, }; }) ); setReadCount((prev) => prev + 1); console.log(`Real motor positions:`, positions); } catch (error) { console.warn("Failed to read motor positions:", error); setCalibrationStatus( `Error reading motors: ${ error instanceof Error ? error.message : error }` ); } finally { isReading.current = false; } }, [isCalibrating, readMotorPositions]); // Animation loop using RAF (requestAnimationFrame) const animationLoop = useCallback(() => { updateMotorData(); if (isCalibrating) { animationFrameRef.current = requestAnimationFrame(animationLoop); } }, [isCalibrating, updateMotorData]); useEffect(() => { initializeMotorData(); }, [initializeMotorData]); useEffect(() => { if (isCalibrating) { animationFrameRef.current = requestAnimationFrame(animationLoop); } else { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } } return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [isCalibrating, animationLoop]); const startCalibration = async () => { if (!robot.port || !robot.robotType) { setCalibrationStatus("Error: Invalid robot configuration"); return; } setCalibrationStatus( "Initializing calibration - reading current positions..." ); try { // Get current positions to use as starting point for min/max const currentPositions = await readMotorPositions(); // Reset calibration data with current positions as both min and max const freshData = motorNames.map((name, index) => ({ name, current: currentPositions[index], min: currentPositions[index], // Start with current position max: currentPositions[index], // Start with current position range: 0, // No range yet })); setMotorData(freshData); setReadCount(0); setIsCalibrating(true); setCalibrationComplete(false); setCalibrationStatus( "Recording ranges of motion - move all joints through their full range..." ); } catch (error) { setCalibrationStatus( `Error starting calibration: ${ error instanceof Error ? error.message : error }` ); } }; // Generate calibration config JSON matching Node CLI format const generateConfigJSON = () => { const calibrationData = { homing_offset: motorData.map((motor) => motor.current - 2047), // Center offset drive_mode: [3, 3, 3, 3, 3, 3], // SO-100 standard drive mode start_pos: motorData.map((motor) => motor.min), end_pos: motorData.map((motor) => motor.max), calib_mode: ["middle", "middle", "middle", "middle", "middle", "middle"], // SO-100 standard motor_names: motorNames, }; return calibrationData; }; // Download calibration config as JSON file const downloadConfigJSON = () => { const configData = generateConfigJSON(); const jsonString = JSON.stringify(configData, null, 2); const blob = new Blob([jsonString], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${robot.robotId || robot.robotType}_calibration.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; const finishCalibration = () => { setIsCalibrating(false); setCalibrationComplete(true); setCalibrationStatus( `✅ Calibration completed! Recorded ${readCount} position readings.` ); // Save calibration config to localStorage using serial number const configData = generateConfigJSON(); const serialNumber = (robot as any).serialNumber; if (!serialNumber) { console.warn("⚠️ No serial number available for calibration storage"); setCalibrationStatus( `⚠️ Calibration completed but cannot save - no robot serial number` ); return; } const calibrationKey = `lerobot-calibration-${serialNumber}`; try { localStorage.setItem( calibrationKey, JSON.stringify({ config: configData, timestamp: new Date().toISOString(), serialNumber: serialNumber, robotId: robot.robotId, robotType: robot.robotType, readCount: readCount, }) ); console.log(`💾 Calibration saved for robot serial: ${serialNumber}`); } catch (error) { console.warn("Failed to save calibration to localStorage:", error); setCalibrationStatus( `⚠️ Calibration completed but save failed: ${error}` ); } }; return (
{/* Calibration Status Card */}
🛠️ Calibrating: {robot.robotId} {robot.robotType?.replace("_", " ")} • {robot.name}
{isCalibrating ? "Recording" : calibrationComplete ? "Complete" : "Ready"}

Status:

{calibrationStatus}

{isCalibrating && (

Readings: {readCount} | Press "Finish Calibration" when done

)}
{!isCalibrating && !calibrationComplete && ( )} {isCalibrating && ( )} {calibrationComplete && ( <> )}
{/* Configuration JSON Display */} {calibrationComplete && ( 🎯 Calibration Configuration Copy this JSON or download it for your robot setup
                {JSON.stringify(generateConfigJSON(), null, 2)}
              
)} {/* Live Position Recording Table (matching Node CLI exactly) */} Live Position Recording Real-time motor position feedback - exactly like Node CLI
{motorData.map((motor, index) => ( ))}
Motor Name Current Min Max Range
{motor.name} {motor.range > 100 && ( )} {motor.current} {motor.min} {motor.max} 100 ? "text-green-600" : "text-gray-500" } > {motor.range}
{isCalibrating && (
Move joints through their full range of motion...
)}
); }