Spaces:
Running
Running
| /** | |
| * Node.js teleoperation functionality using serialport API | |
| * Provides both Python lerobot compatible CLI behavior and programmatic usage | |
| * Uses proven teleoperator classes with web-compatible API | |
| */ | |
| import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; | |
| import { createSO100Config } from "./robots/so100_config.js"; | |
| import { readAllMotorPositions } from "./utils/motor-communication.js"; | |
| import { | |
| KeyboardTeleoperator, | |
| DirectTeleoperator, | |
| } from "./teleoperators/index.js"; | |
| import { readFile } from "fs/promises"; | |
| import { join } from "path"; | |
| import { homedir } from "os"; | |
| import { existsSync } from "fs"; | |
| import type { | |
| TeleoperateConfig, | |
| TeleoperationProcess, | |
| MotorConfig, | |
| TeleoperationState, | |
| } from "./types/teleoperation.js"; | |
| import type { RobotConnection } from "./types/robot-connection.js"; | |
| import type { CalibrationResults } from "./types/calibration.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`); | |
| } | |
| /** | |
| * Load calibration data from file system | |
| */ | |
| async function loadCalibrationData( | |
| robotType: string, | |
| robotId: string | |
| ): Promise<CalibrationResults | null> { | |
| const calibrationPath = getCalibrationFilePath(robotType, robotId); | |
| if (!existsSync(calibrationPath)) { | |
| return null; | |
| } | |
| try { | |
| const calibrationJson = await readFile(calibrationPath, "utf-8"); | |
| return JSON.parse(calibrationJson) as CalibrationResults; | |
| } catch (error) { | |
| console.warn( | |
| `Failed to load calibration data from ${calibrationPath}:`, | |
| error | |
| ); | |
| return null; | |
| } | |
| } | |
| /** | |
| * Build motor configurations from robot config and calibration data | |
| */ | |
| function buildMotorConfigs( | |
| robotConfig: any, | |
| calibrationData?: CalibrationResults | null | |
| ): MotorConfig[] { | |
| const motorConfigs: MotorConfig[] = []; | |
| for (let i = 0; i < robotConfig.motorNames.length; i++) { | |
| const motorName = robotConfig.motorNames[i]; | |
| const motorId = robotConfig.motorIds[i]; | |
| let minPosition = 0; | |
| let maxPosition = robotConfig.protocol.resolution - 1; // Default full range | |
| // Use calibration data if available | |
| if (calibrationData && calibrationData[motorName]) { | |
| minPosition = calibrationData[motorName].range_min; | |
| maxPosition = calibrationData[motorName].range_max; | |
| } | |
| motorConfigs.push({ | |
| id: motorId, | |
| name: motorName, | |
| currentPosition: Math.floor((minPosition + maxPosition) / 2), // Start at center | |
| minPosition, | |
| maxPosition, | |
| }); | |
| } | |
| return motorConfigs; | |
| } | |
| /** | |
| * Main teleoperate function with web-compatible API | |
| */ | |
| export async function teleoperate( | |
| config: TeleoperateConfig | |
| ): Promise<TeleoperationProcess> { | |
| const { robot, teleop, calibrationData, onStateUpdate } = config; | |
| // Validate robot configuration | |
| if (!robot.robotType) { | |
| throw new Error( | |
| "Robot type is required for teleoperation. Please configure the robot first." | |
| ); | |
| } | |
| if (!robot.isConnected || !robot.port) { | |
| throw new Error( | |
| "Robot is not connected. Please use findPort() to connect first." | |
| ); | |
| } | |
| // Use the EXISTING port connection (don't create new one!) | |
| const 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}`); | |
| } | |
| // Load or use provided calibration data | |
| let effectiveCalibrationData = calibrationData; | |
| if (!effectiveCalibrationData && robot.robotId) { | |
| effectiveCalibrationData = await loadCalibrationData( | |
| robot.robotType, | |
| robot.robotId | |
| ); | |
| } | |
| if (!effectiveCalibrationData) { | |
| console.warn( | |
| "No calibration data found. Using default motor ranges. Consider running calibration first." | |
| ); | |
| } | |
| // Build motor configurations | |
| const motorConfigs = buildMotorConfigs(robotConfig, effectiveCalibrationData); | |
| // Read current motor positions | |
| try { | |
| const currentPositions = await readAllMotorPositions( | |
| port, | |
| robotConfig.motorIds | |
| ); | |
| for (let i = 0; i < motorConfigs.length; i++) { | |
| motorConfigs[i].currentPosition = currentPositions[i]; | |
| } | |
| } catch (error) { | |
| console.warn("Failed to read initial motor positions:", error); | |
| } | |
| // Create appropriate teleoperator based on configuration | |
| let teleoperator; | |
| switch (teleop.type) { | |
| case "keyboard": | |
| teleoperator = new KeyboardTeleoperator( | |
| teleop, | |
| port, | |
| motorConfigs, | |
| robotConfig.keyboardControls, | |
| onStateUpdate | |
| ); | |
| break; | |
| case "direct": | |
| teleoperator = new DirectTeleoperator(teleop, port, motorConfigs); | |
| break; | |
| default: | |
| throw new Error(`Unsupported teleoperator type: ${(teleop as any).type}`); | |
| } | |
| // Initialize teleoperator | |
| await teleoperator.initialize(); | |
| // Build process object | |
| const process: TeleoperationProcess = { | |
| start(): void { | |
| teleoperator.start(); | |
| }, | |
| stop(): void { | |
| teleoperator.stop(); | |
| }, | |
| updateKeyState(key: string, pressed: boolean): void { | |
| if ("updateKeyState" in teleoperator) { | |
| (teleoperator as any).updateKeyState(key, pressed); | |
| } | |
| }, | |
| getState(): TeleoperationState { | |
| const teleoperatorSpecificState = teleoperator.getState(); | |
| return { | |
| isActive: teleoperator.isActiveTeleoperator, | |
| motorConfigs: [...teleoperator.motorConfigs], | |
| lastUpdate: Date.now(), | |
| ...teleoperatorSpecificState, | |
| }; | |
| }, | |
| teleoperator: teleoperator, | |
| async disconnect(): Promise<void> { | |
| await teleoperator.disconnect(); | |
| }, | |
| }; | |
| return process; | |
| } | |