# lerobot API Comparison: Python vs Node.js vs Web This document provides a comprehensive three-way comparison of lerobot APIs across Python lerobot (original), Node.js lerobot.js, and Web lerobot.js platforms. ## 🔄 Core Function Comparison | Function Category | Python lerobot (Original) | Node.js lerobot.js | Web Browser lerobot.js | Key Pattern | | ------------------ | ------------------------- | ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | | **Port Discovery** | `find_port()` | `findPort()` | `findPortWeb(logger)` | Python → Node.js: Direct port, Web: Requires UI logger | | **Robot Creation** | `SO100Follower(config)` | `createSO100Follower(config)` | `createWebTeleoperationController(port, serialNumber)` | Python: Class, Node.js: Factory, Web: Pre-opened port | | **Calibration** | `calibrate(cfg)` | `calibrate(config)` | `createCalibrationController(armType, port)` | Python/Node.js: Function, Web: Controller pattern | | **Teleoperation** | `teleoperate(cfg)` | `teleoperate(config)` | `createWebTeleoperationController(port, serialNumber)` | Python/Node.js: Function, Web: Manual state management | ## 📋 Detailed API Reference ### Port Discovery | Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js | | -------------------- | ----------------------------------------- | -------------------------------------------- | ---------------------------------------------- | | **Function** | `find_port()` | `findPort()` | `findPortWeb(logger)` | | **Import** | `from lerobot.find_port import find_port` | `import { findPort } from 'lerobot.js/node'` | `import { findPortWeb } from 'lerobot.js/web'` | | **Parameters** | None | None | `logger: (message: string) => void` | | **User Interaction** | Terminal prompts via `input()` | Terminal prompts via readline | Browser modals and buttons | | **Port Access** | Direct system access via pyserial | Direct system access | Web Serial API permissions | | **Return Value** | None (prints to console) | None (prints to console) | None (calls logger function) | | **Example** | `python
find_port()
` | `js
await findPort();
` | `js
await findPortWeb(console.log);
` | ### Robot Connection & Creation | Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Creation** | `SO100Follower(config)` or `make_robot_from_config(config)` | `createSO100Follower(config)` | `createWebTeleoperationController(port, serialNumber)` | | **Connection** | `robot.connect()` | `await robot.connect()` | Port already opened before creation | | **Port Parameter** | `RobotConfig(port='/dev/ttyUSB0')` | `{ port: 'COM4' }` (string) | `SerialPort` object | | **Baud Rate** | Handled internally | Handled internally | `await port.open({ baudRate: 1000000 })` | | **Factory Pattern** | `make_robot_from_config()` factory | `createSO100Follower()` factory | `createWebTeleoperationController()` factory | | **Example** | `python
from lerobot.common.robots.so100_follower import SO100Follower
robot = SO100Follower(config)
robot.connect()
` | `js
const robot = createSO100Follower({
type: 'so100_follower',
port: 'COM4',
id: 'my_robot'
});
await robot.connect();
` | `js
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 1000000 });
const robot = await createWebTeleoperationController(
port, 'my_robot'
);
` | ### Calibration | Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Main Function** | `calibrate(cfg)` | `calibrate(config)` | `createCalibrationController(armType, port)` | | **Import** | `from lerobot.calibrate import calibrate` | `import { calibrate } from 'lerobot.js/node'` | `import { createCalibrationController } from 'lerobot.js/web'` | | **Configuration** | `CalibrateConfig` dataclass | Single config object | Controller with methods | | **Workflow** | All-in-one function calls `device.calibrate()` | All-in-one function | Step-by-step methods | | **Device Pattern** | Creates device, calls `device.calibrate()`, `device.disconnect()` | Automatic within calibrate() | Manual controller lifecycle | | **Homing** | Automatic within `device.calibrate()` | Automatic within calibrate() | `await controller.performHomingStep()` | | **Range Recording** | Automatic within `device.calibrate()` | Automatic within calibrate() | `await controller.performRangeRecordingStep()` | | **Completion** | Automatic save and disconnect | Automatic save | `await controller.finishCalibration()` | | **Data Storage** | File system (HF cache) | File system (HF cache) | localStorage + file download | | **Example** | `python
from lerobot.calibrate import calibrate
from lerobot.common.robots.so100_follower import SO100FollowerConfig
calibrate(CalibrateConfig(
robot=SO100FollowerConfig(
port='/dev/ttyUSB0', id='my_robot'
)
))
` | `js
await calibrate({
robot: {
type: 'so100_follower',
port: 'COM4',
id: 'my_robot'
}
});
` | `js
const controller = await createCalibrationController(
'so100_follower', port
);
await controller.performHomingStep();
await controller.performRangeRecordingStep(stopCondition);
const results = await controller.finishCalibration();
` | ### Teleoperation | Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Main Function** | `teleoperate(cfg)` | `teleoperate(config)` | `createWebTeleoperationController(port, serialNumber)` | | **Import** | `from lerobot.teleoperate import teleoperate` | `import { teleoperate } from 'lerobot.js/node'` | `import { createWebTeleoperationController } from 'lerobot.js/web'` | | **Control Loop** | `teleop_loop()` with `get_action()` and `send_action()` | Automatic 60 FPS loop | Manual start/stop with `controller.start()` | | **Device Management** | Creates teleop and robot devices, connects both | Device creation handled internally | Port opened externally, controller manages state | | **Input Handling** | Teleoperator `get_action()` method | Terminal raw mode | Browser event listeners | | **Key State** | Handled by teleoperator device | Internal management | `controller.updateKeyState(key, pressed)` | | **Configuration** | `TeleoperateConfig` with separate robot/teleop configs | `js
{
robot: { type, port, id },
teleop: { type: 'keyboard' },
fps: 60,
step_size: 25
}
` | Built into controller | | **Example** | `python
from lerobot.teleoperate import teleoperate
teleoperate(TeleoperateConfig(
robot=SO100FollowerConfig(port='/dev/ttyUSB0'),
teleop=SO100LeaderConfig(port='/dev/ttyUSB1')
))
` | `js
await teleoperate({
robot: {
type: 'so100_follower',
port: 'COM4',
id: 'my_robot'
},
teleop: { type: 'keyboard' }
});
` | `js
const controller = await createWebTeleoperationController(
port, 'my_robot'
);
controller.start();
// Handle keyboard events manually
document.addEventListener('keydown', (e) => {
controller.updateKeyState(e.key, true);
});
` | ### Motor Control | Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **Get Positions** | `robot.get_observation()` (includes motor positions) | `await robot.getMotorPositions()` | `controller.getMotorConfigs()` or `controller.getState()` | | **Set Positions** | `robot.send_action(action)` (dict format) | `await robot.setMotorPositions(positions)` | `await controller.setMotorPositions(positions)` | | **Position Format** | `dict[str, float]` (action format) | `Record` | `Record` ✅ Same | | **Data Flow** | `get_observation()` → `send_action()` loop | Direct position get/set methods | Controller state management | | **Action Features** | `robot.action_features` (motor names) | Motor names hardcoded in implementation | Motor configs with metadata | | **Calibration Limits** | Handled in robot implementation | `robot.getCalibrationLimits()` | `controller.getMotorConfigs()` (includes limits) | | **Home Position** | Manual via action dict | Manual calculation | `await controller.goToHomePosition()` | | **Example** | `python
obs = robot.get_observation()
action = {motor: value for motor in robot.action_features}
robot.send_action(action)
` | `js
const positions = await robot.getMotorPositions();
await robot.setMotorPositions({
shoulder_pan: 2047,
shoulder_lift: 1800
});
` | `js
const state = controller.getState();
await controller.setMotorPositions({
shoulder_pan: 2047,
shoulder_lift: 1800
});
` | ### Calibration Data Management | Aspect | Node.js | Web Browser | | -------------------- | ------------------------------------------- | ------------------------------------- | | **Storage Location** | `~/.cache/huggingface/lerobot/calibration/` | `localStorage` + file download | | **File Format** | JSON files on disk | JSON in browser storage | | **Loading** | Automatic during `robot.connect()` | Manual via `loadCalibrationConfig()` | | **Saving** | `robot.saveCalibration(results)` | `saveCalibrationResults()` + download | | **Persistence** | Permanent until deleted | Browser-specific, can be cleared | | **Sharing** | File system sharing | Manual file sharing | ### Error Handling & Debugging | Aspect | Node.js | Web Browser | | --------------------- | ------------------------ | ------------------------------------ | | **Connection Errors** | Standard Node.js errors | Web Serial API errors | | **Permission Issues** | File system permissions | User permission prompts | | **Port Conflicts** | "Port in use" errors | Silent failures or permission errors | | **Debugging** | Console.log + terminal | Browser DevTools console | | **Logging** | Built-in terminal output | Passed logger functions | ## 🎯 Usage Pattern Summary ### Python lerobot (Original) - Research & Production ```python # Configuration-driven, device-based approach from lerobot.find_port import find_port from lerobot.calibrate import calibrate, CalibrateConfig from lerobot.teleoperate import teleoperate, TeleoperateConfig from lerobot.common.robots.so100_follower import SO100FollowerConfig from lerobot.common.teleoperators.so100_leader import SO100LeaderConfig # Find port find_port() # Calibrate robot calibrate(CalibrateConfig( robot=SO100FollowerConfig(port='/dev/ttyUSB0', id='my_robot') )) # Teleoperation with leader-follower teleoperate(TeleoperateConfig( robot=SO100FollowerConfig(port='/dev/ttyUSB0'), teleop=SO100LeaderConfig(port='/dev/ttyUSB1') )) ``` ### Node.js lerobot.js - Server/Desktop Applications ```javascript // High-level, all-in-one functions (mirrors Python closely) import { findPort, calibrate, teleoperate } from "lerobot.js/node"; await findPort(); await calibrate({ robot: { type: "so100_follower", port: "COM4", id: "my_robot" }, }); await teleoperate({ robot: { type: "so100_follower", port: "COM4" }, teleop: { type: "keyboard" }, }); ``` ### Web Browser lerobot.js - Interactive Applications ```javascript // Controller-based, step-by-step approach (browser constraints) import { findPortWeb, createCalibrationController, createWebTeleoperationController, } from "lerobot.js/web"; // User interaction required const port = await navigator.serial.requestPort(); await port.open({ baudRate: 1000000 }); // Step-by-step calibration const calibrator = await createCalibrationController("so100_follower", port); await calibrator.performHomingStep(); await calibrator.performRangeRecordingStep(() => stopRecording); // Manual teleoperation control const controller = await createWebTeleoperationController(port, "my_robot"); controller.start(); ``` ## 🔑 Key Architectural Differences 1. **User Interaction Model** - **Node.js**: Terminal-based with readline prompts - **Web**: Browser UI with buttons and modals 2. **Permission Model** - **Node.js**: System-level permissions - **Web**: User-granted permissions per device 3. **State Management** - **Node.js**: Function-based, stateless - **Web**: Controller-based, stateful 4. **Data Persistence** - **Node.js**: File system with cross-session persistence - **Web**: Browser storage with limited persistence 5. **Platform Integration** - **Node.js**: Deep system integration - **Web**: Security-constrained browser environment This comparison helps developers choose the right platform and understand the API differences when porting between Node.js and Web implementations.