Spaces:
Running
Running
| /** | |
| * 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<void> { | |
| // 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<void> { | |
| 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); | |
| } | |