/** * Node.js calibration functionality using serialport API * Provides both Python lerobot compatible CLI behavior and programmatic usage * Uses proven calibration algorithms with web-compatible API */ import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; import { createSO100Config } from "./robots/so100_config.js"; import { readAllMotorPositions, releaseMotors as releaseMotorsLowLevel, type MotorCommunicationPort, } from "./utils/motor-communication.js"; import { setHomingOffsets, writeHardwarePositionLimits, } from "./utils/motor-calibration.js"; import { createInterface } from "readline"; import { writeFile } from "fs/promises"; import { join } from "path"; import { homedir } from "os"; // Debug logging removed - calibration working perfectly import type { CalibrateConfig, CalibrationResults, LiveCalibrationData, CalibrationProcess, } from "./types/calibration.js"; import type { RobotConnection } from "./types/robot-connection.js"; /** * Get calibration file path (matches Python lerobot location) */ function getCalibrationFilePath(robotType: string, robotId: string): string { const HF_HOME = process.env.HF_HOME || join(homedir(), ".cache", "huggingface"); const calibrationDir = join( HF_HOME, "lerobot", "calibration", "robots", robotType ); return join(calibrationDir, `${robotId}.json`); } /** * Create readline interface for user input */ function createReadlineInterface() { return createInterface({ input: process.stdin, output: process.stdout, }); } /** * Wait for user input with a prompt */ function waitForInput(rl: any, prompt: string): Promise { return new Promise((resolve) => { rl.question(prompt, (answer: string) => { resolve(answer); }); }); } /** * Record ranges of motion with live updates */ async function recordRangesOfMotion( port: MotorCommunicationPort, motorIds: number[], motorNames: string[], shouldStop: () => boolean, onLiveUpdate?: (data: LiveCalibrationData) => void, onProgress?: (message: string) => void ): Promise<{ rangeMins: { [motor: string]: number }; rangeMaxes: { [motor: string]: number }; }> { const rangeMins: { [motor: string]: number } = {}; const rangeMaxes: { [motor: string]: number } = {}; // Read actual current positions (now centered due to applied homing offsets) const startPositions = await readAllMotorPositions(port, motorIds); for (let i = 0; i < motorNames.length; i++) { const motorName = motorNames[i]; rangeMins[motorName] = startPositions[i]; rangeMaxes[motorName] = startPositions[i]; } if (onProgress) { onProgress( "Move each motor through its full range of motion. The ranges will be recorded automatically." ); onProgress( "Press Enter when you have finished moving all motors through their ranges." ); } else { console.log( "Move each motor through its full range of motion. The ranges will be recorded automatically." ); console.log( "Press Enter when you have finished moving all motors through their ranges." ); } // Set up readline for user input const rl = createReadlineInterface(); let isRecording = true; // Start recording in background const recordingInterval = setInterval(async () => { if (!isRecording) return; try { const currentPositions = await readAllMotorPositions(port, motorIds); const liveData: LiveCalibrationData = {}; for (let i = 0; i < motorNames.length; i++) { const motorName = motorNames[i]; const position = currentPositions[i]; // Update ranges rangeMins[motorName] = Math.min(rangeMins[motorName], position); rangeMaxes[motorName] = Math.max(rangeMaxes[motorName], position); // Build live data liveData[motorName] = { current: position, min: rangeMins[motorName], max: rangeMaxes[motorName], range: rangeMaxes[motorName] - rangeMins[motorName], }; } if (onLiveUpdate) { onLiveUpdate(liveData); } } catch (error) { // Silent - continue recording } }, 100); // Update every 100ms // Wait for user to finish try { await waitForInput(rl, ""); // IMMEDIATELY stop recording and live updates isRecording = false; clearInterval(recordingInterval); } finally { // Ensure cleanup even if there's an error isRecording = false; clearInterval(recordingInterval); rl.close(); } return { rangeMins, rangeMaxes }; } /** * Main calibrate function with web-compatible API */ export async function calibrate( config: CalibrateConfig ): Promise { const { robot, onLiveUpdate, onProgress, outputPath } = config; // Validate robot configuration if (!robot.robotType) { throw new Error( "Robot type is required for calibration. Please configure the robot first." ); } if (!robot.isConnected || !robot.port) { throw new Error( "Robot is not connected. Please use findPort() to connect first." ); } let shouldStop = false; let port: NodeSerialPortWrapper | null = null; const calibrationPromise = (async (): Promise => { try { // Use the EXISTING port connection (don't create new one!) port = robot.port; // Get robot-specific configuration let robotConfig; if (robot.robotType.startsWith("so100")) { robotConfig = createSO100Config(robot.robotType); } else { throw new Error(`Unsupported robot type: ${robot.robotType}`); } const { motorIds, motorNames, driveModes } = robotConfig; // Debug logging removed - calibration working perfectly // Starting calibration silently // Step 1: Set homing offsets (motors should already be released and positioned) // Note: Motors should be released BEFORE calling calibrate(), not inside it // Setting homing offsets silently const homingOffsets = await setHomingOffsets(port, motorIds, motorNames); // Early debug test removed - calibration working perfectly if (shouldStop) throw new Error("Calibration stopped by user"); // Step 2: Record ranges of motion silently const { rangeMins, rangeMaxes } = await recordRangesOfMotion( port, motorIds, motorNames, () => shouldStop, onLiveUpdate, onProgress ); if (shouldStop) throw new Error("Calibration stopped by user"); // Step 3: Write hardware position limits silently await writeHardwarePositionLimits( port, motorIds, motorNames, rangeMins, rangeMaxes ); // Step 4: Skip motor locking (Python lerobot doesn't lock motors after calibration) // Build calibration results (Python lerobot compatible format) const calibrationResults: CalibrationResults = {}; for (let i = 0; i < motorNames.length; i++) { const motorName = motorNames[i]; const homingOffsetValue = homingOffsets[motorName]; calibrationResults[motorName] = { id: motorIds[i], drive_mode: driveModes[i], homing_offset: homingOffsetValue, range_min: rangeMins[motorName], range_max: rangeMaxes[motorName], }; } // Save calibration file const calibrationPath = outputPath || getCalibrationFilePath(robot.robotType, robot.robotId || "default"); // Ensure directory exists const { mkdir } = await import("fs/promises"); const { dirname } = await import("path"); await mkdir(dirname(calibrationPath), { recursive: true }); await writeFile( calibrationPath, JSON.stringify(calibrationResults, null, 2) ); if (onProgress) { onProgress(`Calibration complete! Saved to: ${calibrationPath}`); } else { console.log(`Calibration complete! Saved to: ${calibrationPath}`); } return calibrationResults; } finally { // Note: Don't close the port - it belongs to the robot connection } })(); return { stop(): void { shouldStop = true; }, result: calibrationPromise, }; }