/** * Helper to recalibrate your device (robot or teleoperator). * * Example: * ``` * npx lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm * ``` */ import { createSO100Follower } from "./robots/so100_follower.js"; import { createSO100Leader } from "./teleoperators/so100_leader.js"; import { initializeDeviceCommunication, readMotorPositions, performInteractiveCalibration, setMotorLimits, verifyCalibration, } from "./common/calibration.js"; import type { CalibrateConfig } from "./types/robot-config.js"; import type { CalibrationResults } from "./types/calibration.js"; import { getSO100Config } from "./common/so100_config.js"; /** * Main calibrate function * Mirrors Python lerobot calibrate.py calibrate() function * Uses shared calibration procedures instead of device-specific implementations */ export async function calibrate(config: CalibrateConfig): Promise { // Validate configuration - exactly one device must be specified if (Boolean(config.robot) === Boolean(config.teleop)) { throw new Error("Choose either a robot or a teleop."); } const deviceConfig = config.robot || config.teleop!; let device; let calibrationResults: CalibrationResults; try { // Create device for connection management only if (config.robot) { switch (config.robot.type) { case "so100_follower": device = createSO100Follower(config.robot); break; default: throw new Error(`Unsupported robot type: ${config.robot.type}`); } } else if (config.teleop) { switch (config.teleop.type) { case "so100_leader": device = createSO100Leader(config.teleop); break; default: throw new Error( `Unsupported teleoperator type: ${config.teleop.type}` ); } } if (!device) { throw new Error("Failed to create device"); } // Connect to device (silent unless error) await device.connect(false); // calibrate=False like Python // Get SO-100 calibration configuration const so100Config = getSO100Config( deviceConfig.type as "so100_follower" | "so100_leader", (device as any).port ); // Perform shared calibration procedures (silent unless error) await initializeDeviceCommunication(so100Config); await setMotorLimits(so100Config); // Interactive calibration with live updates - THE MAIN PART calibrationResults = await performInteractiveCalibration(so100Config); // Save and cleanup (silent unless error) await verifyCalibration(so100Config); await (device as any).saveCalibration(calibrationResults); await device.disconnect(); } catch (error) { // Ensure we disconnect even if there's an error if (device) { try { await device.disconnect(); } catch (disconnectError) { console.warn("Warning: Failed to disconnect properly"); } } throw error; } } /** * Parse command line arguments in Python argparse style * Handles --robot.type=so100_follower --robot.port=COM4 format */ export function parseArgs(args: string[]): CalibrateConfig { const config: CalibrateConfig = {}; for (const arg of args) { if (arg.startsWith("--robot.")) { if (!config.robot) { config.robot = { type: "so100_follower", port: "" }; } const [key, value] = arg.substring(8).split("="); switch (key) { case "type": if (value !== "so100_follower") { throw new Error(`Unsupported robot type: ${value}`); } config.robot.type = value as "so100_follower"; break; case "port": config.robot.port = value; break; case "id": config.robot.id = value; break; case "disable_torque_on_disconnect": config.robot.disable_torque_on_disconnect = value === "true"; break; case "max_relative_target": config.robot.max_relative_target = value ? parseInt(value) : null; break; case "use_degrees": config.robot.use_degrees = value === "true"; break; default: throw new Error(`Unknown robot parameter: ${key}`); } } else if (arg.startsWith("--teleop.")) { if (!config.teleop) { config.teleop = { type: "so100_leader", port: "" }; } const [key, value] = arg.substring(9).split("="); switch (key) { case "type": if (value !== "so100_leader") { throw new Error(`Unsupported teleoperator type: ${value}`); } config.teleop.type = value as "so100_leader"; break; case "port": config.teleop.port = value; break; case "id": config.teleop.id = value; break; default: throw new Error(`Unknown teleoperator parameter: ${key}`); } } else if (arg === "--help" || arg === "-h") { showUsage(); process.exit(0); } else if (!arg.startsWith("--")) { // Skip non-option arguments continue; } else { throw new Error(`Unknown argument: ${arg}`); } } // Validate required fields if (config.robot && !config.robot.port) { throw new Error("Robot port is required (--robot.port=PORT)"); } if (config.teleop && !config.teleop.port) { throw new Error("Teleoperator port is required (--teleop.port=PORT)"); } return config; } /** * Show usage information matching Python argparse output */ function showUsage(): void { console.log("Usage: lerobot calibrate [options]"); console.log(""); console.log("Recalibrate your device (robot or teleoperator)"); console.log(""); console.log("Options:"); console.log(" --robot.type=TYPE Robot type (so100_follower)"); console.log( " --robot.port=PORT Robot serial port (e.g., COM4, /dev/ttyUSB0)" ); console.log(" --robot.id=ID Robot identifier"); console.log(" --teleop.type=TYPE Teleoperator type (so100_leader)"); console.log(" --teleop.port=PORT Teleoperator serial port"); console.log(" --teleop.id=ID Teleoperator identifier"); console.log(" -h, --help Show this help message"); console.log(""); console.log("Examples:"); console.log( " lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm" ); console.log( " lerobot calibrate --teleop.type=so100_leader --teleop.port=COM3 --teleop.id=my_leader_arm" ); console.log(""); console.log("Use 'lerobot find-port' to discover available ports."); } /** * CLI entry point when called directly * Mirrors Python's if __name__ == "__main__": pattern */ export async function main(args: string[]): Promise { try { if (args.length === 0 || args.includes("--help") || args.includes("-h")) { showUsage(); return; } const config = parseArgs(args); await calibrate(config); } catch (error) { if (error instanceof Error) { console.error("Error:", error.message); } else { console.error("Error:", error); } console.error(""); console.error("Please verify:"); console.error("1. The device is connected to the specified port"); console.error("2. No other application is using the port"); console.error("3. You have permission to access the port"); console.error(""); console.error("Use 'lerobot find-port' to discover available ports."); process.exit(1); } } if (import.meta.url === `file://${process.argv[1]}`) { const args = process.argv.slice(2); main(args); }