Spaces:
Running
Running
| /** | |
| * 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<string> { | |
| 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<CalibrationProcess> { | |
| 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<CalibrationResults> => { | |
| 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, | |
| }; | |
| } | |