Spaces:
Running
Running
# User Story 006: Node.js Package Architecture | |
## Story | |
**As a** robotics developer building server-side applications, CLI tools, and desktop robotics software | |
**I want** to use lerobot.js functionality directly from Node.js with the same API as the web version | |
**So that** I can build Node.js applications, command-line tools, and desktop software without browser constraints while maintaining familiar APIs | |
## Background | |
We have successfully implemented `packages/web` that provides `findPort`, `calibrate`, `releaseMotors`, and `teleoperate` functionality using Web APIs (Web Serial, Web USB). This package is published as `@lerobot/web` and provides a clean, typed API for browser-based robotics applications. | |
We also have existing Node.js code in `src/lerobot/node` that was working but abandoned when we focused on getting the web version right. Now that the web version is stable and proven, we want to create a proper `packages/node` package that: | |
1. **Mirrors the Web API**: Provides the same function signatures and behavior as `@lerobot/web` | |
2. **Uses Node.js APIs**: Leverages `serialport` instead of Web Serial for hardware communication | |
3. **Python lerobot Faithfulness**: Maintains exact compatibility with Python lerobot CLI commands and behavior | |
4. **Server-Side Ready**: Enables robotics applications in Node.js servers, CLI tools, and desktop applications | |
5. **Reuses Proven Logic**: Builds on existing `src/lerobot/node` code that was already working | |
This will enable developers to use the same lerobot.js API in both browser and Node.js environments, choosing the appropriate platform based on their application needs. | |
## Acceptance Criteria | |
### Core Functionality | |
- [ ] **Same API Surface**: Mirror `@lerobot/web` API with identical function signatures where possible | |
- [ ] **Four Core Functions**: Implement `findPort`, `calibrate`, `releaseMotors`, and `teleoperate` | |
- [ ] **SerialPort Integration**: Use `serialport` package instead of Web Serial API | |
- [ ] **TypeScript Support**: Full TypeScript coverage with strict type checking | |
- [ ] **NPM Package**: Published as `@lerobot/node` with proper package.json | |
### Platform Requirements | |
- [ ] **Node.js 18+**: Support current LTS and newer versions | |
- [ ] **Cross-Platform**: Work on Windows, macOS, and Linux | |
- [ ] **ES Modules**: Use ES module format for consistency with web package | |
- [ ] **CLI Integration**: Enable `npx lerobot` commands using this package | |
- [ ] **No Browser Dependencies**: No Web API dependencies or browser-specific code | |
### API Alignment | |
- [ ] **Same Types**: Reuse or mirror types from `@lerobot/web` where appropriate | |
- [ ] **Same Exports**: Mirror the export structure of `@lerobot/web/index.ts` | |
- [ ] **Same Behavior**: Identical behavior for shared functionality (calibration algorithms, motor control) | |
- [ ] **Platform-Specific Adaptations**: Handle Node.js-specific differences (file system, process management) | |
### Code Quality | |
- [ ] **Reuse Existing Code**: Build on proven `src/lerobot/node` implementations | |
- [ ] **No Code Duplication**: Share logic with web package where possible (copy for now, per requirements) | |
- [ ] **Clean Architecture**: Follow the same patterns as `packages/web` | |
- [ ] **Comprehensive Testing**: Unit tests for all core functionality | |
## Expected User Flow | |
### Installation and Usage | |
```bash | |
# Install the Node.js package | |
npm install @lerobot/node | |
# Use in Node.js applications | |
import { findPort, calibrate, teleoperate } from "@lerobot/node"; | |
``` | |
### Find Port (Node.js) | |
```typescript | |
// Node.js - programmatic usage | |
import { findPort } from "@lerobot/node"; | |
const portProcess = await findPort(); | |
const availablePorts = await portProcess.getAvailablePorts(); | |
console.log("Available ports:", availablePorts); | |
// Interactive mode (CLI-like) - matches Python lerobot exactly | |
const portProcess = await findPort({ | |
interactive: true, // shows "disconnect cable" prompts like Python | |
}); | |
const detectedPort = await portProcess.detectPort(); | |
``` | |
### Calibration (Node.js) | |
```typescript | |
// Node.js - same API as web | |
import { calibrate } from "@lerobot/node"; | |
const calibrationProcess = await calibrate({ | |
robot: { | |
type: "so100_follower", | |
port: "/dev/ttyUSB0", // or "COM4" on Windows | |
robotId: "my_follower_arm", | |
}, | |
onLiveUpdate: (data) => { | |
console.log("Live calibration data:", data); | |
}, | |
}); | |
const results = await calibrationProcess.result; | |
console.log("Calibration completed:", results); | |
``` | |
### Teleoperation (Node.js) | |
```typescript | |
// Node.js - same API as web | |
import { teleoperate } from "@lerobot/node"; | |
const teleoperationProcess = await teleoperate({ | |
robot: { | |
type: "so100_follower", | |
port: "/dev/ttyUSB0", | |
robotId: "my_follower_arm", | |
}, | |
teleop: { | |
type: "keyboard", | |
stepSize: 25, | |
}, | |
calibrationData: loadedCalibrationData, | |
onStateUpdate: (state) => { | |
console.log("Robot state:", state); | |
}, | |
}); | |
teleoperationProcess.start(); | |
``` | |
### Release Motors (Node.js) | |
```typescript | |
// Node.js - same API as web | |
import { releaseMotors } from "@lerobot/node"; | |
await releaseMotors({ | |
robot: { | |
type: "so100_follower", | |
port: "/dev/ttyUSB0", | |
robotId: "my_follower_arm", | |
}, | |
}); | |
``` | |
### CLI Integration | |
```bash | |
# Python lerobot compatibility - same commands work | |
npx lerobot find-port | |
npx lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 | |
npx lerobot teleoperate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 | |
# Global installation also works | |
npm install -g @lerobot/node | |
lerobot find-port | |
lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 | |
``` | |
## Implementation Details | |
### File Structure | |
``` | |
packages/node/ | |
βββ package.json # NPM package configuration | |
βββ tsconfig.build.json # TypeScript build configuration | |
βββ README.md # Package documentation | |
βββ CHANGELOG.md # Version history | |
βββ src/ | |
βββ index.ts # Main exports (mirror web package) | |
βββ find_port.ts # Port discovery using serialport | |
βββ calibrate.ts # Calibration using Node.js APIs | |
βββ teleoperate.ts # Teleoperation using Node.js APIs | |
βββ release_motors.ts # Motor release using Node.js APIs | |
βββ types/ | |
β βββ robot-connection.ts # Robot connection types | |
β βββ port-discovery.ts # Port discovery types | |
β βββ calibration.ts # Calibration types | |
β βββ teleoperation.ts # Teleoperation types | |
β βββ robot-config.ts # Robot configuration types | |
βββ utils/ | |
β βββ serial-port-wrapper.ts # SerialPort wrapper | |
β βββ motor-communication.ts # Motor communication utilities | |
β βββ motor-calibration.ts # Calibration utilities | |
β βββ sts3215-protocol.ts # Protocol constants | |
βββ robots/ | |
β βββ so100_config.ts # SO-100 configuration | |
βββ teleoperators/ | |
βββ index.ts # Teleoperator exports | |
βββ base-teleoperator.ts # Base teleoperator class | |
βββ keyboard-teleoperator.ts # Keyboard teleoperator | |
``` | |
### Key Dependencies | |
#### Core Dependencies | |
- **serialport**: Node.js serial communication (replaces Web Serial API) | |
- **chalk**: Terminal colors and formatting | |
- **commander**: CLI argument parsing | |
#### Development Dependencies | |
- **typescript**: TypeScript compiler | |
- **@types/node**: Node.js type definitions | |
- **vitest**: Testing framework | |
### Migration Strategy | |
#### Phase 1: Package Setup | |
- [ ] Create `packages/node` directory structure | |
- [ ] Set up package.json with proper exports | |
- [ ] Configure TypeScript build process | |
- [ ] Set up testing infrastructure | |
#### Phase 2: Core Function Migration | |
- [ ] Migrate `src/lerobot/node/find_port.ts` to `packages/node/src/find_port.ts` | |
- [ ] Migrate `src/lerobot/node/calibrate.ts` to `packages/node/src/calibrate.ts` | |
- [ ] Migrate `src/lerobot/node/teleoperate.ts` to `packages/node/src/teleoperate.ts` | |
- [ ] Create `release_motors.ts` using existing motor communication code | |
#### Phase 3: API Alignment | |
- [ ] Ensure all functions match `@lerobot/web` signatures | |
- [ ] Copy and adapt types from `packages/web/src/types/` | |
- [ ] Update utilities to use serialport instead of Web Serial | |
- [ ] Test API compatibility with existing web examples | |
#### Phase 4: Testing and Documentation | |
- [ ] Create comprehensive tests for all functions | |
- [ ] Update documentation and examples | |
- [ ] Validate Python lerobot CLI compatibility | |
- [ ] Test cross-platform compatibility | |
### Core Functions to Implement | |
#### Package Exports (Mirror Web Package) | |
```typescript | |
// packages/node/src/index.ts | |
export { calibrate } from "./calibrate.js"; | |
export { teleoperate } from "./teleoperate.js"; | |
export { findPort } from "./find_port.js"; | |
export { releaseMotors } from "./release_motors.js"; | |
// Types (mirror web package) | |
export type { | |
RobotConnection, | |
RobotConfig, | |
SerialPort, | |
SerialPortInfo, | |
SerialOptions, | |
} from "./types/robot-connection.js"; | |
export type { | |
FindPortConfig, | |
FindPortProcess, | |
} from "./types/port-discovery.js"; | |
export type { | |
CalibrateConfig, | |
CalibrationResults, | |
LiveCalibrationData, | |
CalibrationProcess, | |
} from "./types/calibration.js"; | |
export type { | |
MotorConfig, | |
TeleoperationState, | |
TeleoperationProcess, | |
TeleoperateConfig, | |
TeleoperatorConfig, | |
} from "./types/teleoperation.js"; | |
// Node.js utilities | |
export { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; | |
export { createSO100Config } from "./robots/so100_config.js"; | |
``` | |
#### SerialPort Wrapper (Node.js) | |
```typescript | |
// packages/node/src/utils/serial-port-wrapper.ts | |
import { SerialPort } from "serialport"; | |
export class NodeSerialPortWrapper { | |
private port: SerialPort; | |
private isConnected: boolean = false; | |
constructor(path: string, options: any = {}) { | |
this.port = new SerialPort({ | |
path, | |
baudRate: options.baudRate || 1000000, | |
dataBits: options.dataBits || 8, | |
parity: options.parity || "none", | |
stopBits: options.stopBits || 1, | |
autoOpen: false, | |
}); | |
} | |
async initialize(): Promise<void> { | |
return new Promise((resolve, reject) => { | |
this.port.open((err) => { | |
if (err) { | |
reject(err); | |
} else { | |
this.isConnected = true; | |
resolve(); | |
} | |
}); | |
}); | |
} | |
async writeAndRead(data: Uint8Array): Promise<Uint8Array> { | |
return new Promise((resolve, reject) => { | |
this.port.write(Buffer.from(data), (err) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
// Wait for response | |
setTimeout(() => { | |
this.port.read((readErr, readData) => { | |
if (readErr) { | |
reject(readErr); | |
} else { | |
resolve(new Uint8Array(readData || [])); | |
} | |
}); | |
}, 10); // 10ms delay for response | |
}); | |
}); | |
} | |
async close(): Promise<void> { | |
return new Promise((resolve) => { | |
this.port.close(() => { | |
this.isConnected = false; | |
resolve(); | |
}); | |
}); | |
} | |
} | |
``` | |
#### Find Port Implementation | |
```typescript | |
// packages/node/src/find_port.ts - Build on existing code | |
import { SerialPort } from "serialport"; | |
export interface FindPortConfig { | |
interactive?: boolean; | |
} | |
export interface FindPortProcess { | |
getAvailablePorts(): Promise<string[]>; | |
detectPort(): Promise<string>; // Interactive cable detection like Python | |
} | |
export async function findPort( | |
config: FindPortConfig = {} | |
): Promise<FindPortProcess> { | |
const { interactive = false } = config; | |
return { | |
async getAvailablePorts(): Promise<string[]> { | |
// Use existing implementation from src/lerobot/node/find_port.ts | |
const ports = await SerialPort.list(); | |
return ports.map((port) => port.path); | |
}, | |
async detectPort(): Promise<string> { | |
if (interactive) { | |
// Existing Python-compatible implementation from src/lerobot/node/find_port.ts | |
// Shows "disconnect cable" prompts and detects port automatically | |
console.log("Finding all available ports for the MotorsBus."); | |
const portsBefore = await this.getAvailablePorts(); | |
console.log( | |
"Remove the USB cable from your MotorsBus and press Enter when done." | |
); | |
// ... wait for user input ... | |
const portsAfter = await this.getAvailablePorts(); | |
const portsDiff = portsBefore.filter( | |
(port) => !portsAfter.includes(port) | |
); | |
if (portsDiff.length === 1) { | |
return portsDiff[0]; | |
} else { | |
throw new Error("Could not detect port"); | |
} | |
} else { | |
// Programmatic mode - return first available port | |
const ports = await this.getAvailablePorts(); | |
return ports[0]; | |
} | |
}, | |
}; | |
} | |
``` | |
### Technical Considerations | |
#### API Compatibility with Web Package | |
The Node.js package should maintain the same API surface as the web package where possible: | |
```typescript | |
// Same function signatures | |
await calibrate(config); // Both packages | |
await teleoperate(config); // Both packages | |
await findPort(config); // Both packages | |
await releaseMotors(config); // Both packages | |
``` | |
#### Platform-Specific Adaptations | |
**File System Access:** | |
- Node.js: Direct file system access for calibration data | |
- Web: localStorage/IndexedDB for calibration data | |
**Process Management:** | |
- Node.js: Process signals, stdin/stdout handling | |
- Web: Browser events, DOM keyboard handling | |
**Error Handling:** | |
- Node.js: Process exit codes, console.error | |
- Web: User-friendly error dialogs | |
#### Python lerobot CLI Compatibility | |
The Node.js package must maintain exact Python lerobot CLI compatibility: | |
```bash | |
# These commands must work identically | |
npx lerobot find-port | |
npx lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 | |
npx lerobot teleoperate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 | |
``` | |
#### Calibration Data Storage Location | |
The CLI should store calibration data in the same location as Python lerobot: | |
```bash | |
# Default location (matches Python lerobot) | |
~/.cache/huggingface/lerobot/calibration/robots/ | |
``` | |
This ensures calibration files are compatible between Python lerobot and Node.js lerobot: | |
```typescript | |
// Use HF_HOME environment variable like Python lerobot | |
const HF_HOME = | |
process.env.HF_HOME || path.join(os.homedir(), ".cache", "huggingface"); | |
const CALIBRATION_DIR = path.join(HF_HOME, "lerobot", "calibration", "robots"); | |
``` | |
### Package Configuration | |
#### package.json | |
```json | |
{ | |
"name": "@lerobot/node", | |
"version": "0.1.0", | |
"description": "Node.js-based robotics control using SerialPort", | |
"type": "module", | |
"main": "./dist/index.js", | |
"types": "./dist/index.d.ts", | |
"bin": { | |
"lerobot": "./dist/cli.js" | |
}, | |
"exports": { | |
".": { | |
"import": "./dist/index.js", | |
"types": "./dist/index.d.ts" | |
}, | |
"./calibrate": { | |
"import": "./dist/calibrate.js", | |
"types": "./dist/calibrate.d.ts" | |
}, | |
"./teleoperate": { | |
"import": "./dist/teleoperate.js", | |
"types": "./dist/teleoperate.d.ts" | |
}, | |
"./find-port": { | |
"import": "./dist/find_port.js", | |
"types": "./dist/find_port.d.ts" | |
} | |
}, | |
"files": ["dist/**/*", "README.md"], | |
"keywords": [ | |
"robotics", | |
"serialport", | |
"hardware-control", | |
"nodejs", | |
"typescript" | |
], | |
"scripts": { | |
"build": "tsc --project tsconfig.build.json", | |
"prepublishOnly": "npm run build" | |
}, | |
"dependencies": { | |
"serialport": "^12.0.0", | |
"chalk": "^5.3.0", | |
"commander": "^11.0.0" | |
}, | |
"peerDependencies": { | |
"typescript": ">=4.5.0" | |
}, | |
"engines": { | |
"node": ">=18.0.0" | |
} | |
} | |
``` | |
## Definition of Done | |
- [ ] **Package Structure**: Complete `packages/node` directory with proper NPM package setup | |
- [ ] **API Mirror**: All four core functions (`findPort`, `calibrate`, `releaseMotors`, `teleoperate`) implemented with same API as web package | |
- [ ] **SerialPort Integration**: All hardware communication uses `serialport` package instead of Web Serial | |
- [ ] **Type Safety**: Full TypeScript coverage with strict type checking | |
- [ ] **Code Migration**: Existing `src/lerobot/node` code successfully migrated and enhanced | |
- [ ] **Cross-Platform**: Works on Windows, macOS, and Linux with Node.js 18+ | |
- [ ] **CLI Integration**: `npx lerobot` commands work using the Node.js package | |
- [ ] **Python Compatibility**: CLI commands match Python lerobot behavior exactly | |
- [ ] **NPM Ready**: Package published as `@lerobot/node` with proper versioning | |
- [ ] **Documentation**: Complete README with usage examples and API documentation | |
- [ ] **Testing**: Comprehensive test suite covering all core functionality | |
- [ ] **No Regressions**: All existing Node.js functionality preserved and enhanced | |