Spaces:
Running
Running
docs: updated to reflect the current state
Browse files- packages/web/README.md +43 -205
packages/web/README.md
CHANGED
@@ -1,10 +1,11 @@
|
|
1 |
# @lerobot/web
|
2 |
|
3 |
-
Browser-native robotics control using WebSerial
|
4 |
|
5 |
## Features
|
6 |
|
7 |
- **Direct Hardware Control**: STS3215 motor communication via WebSerial API
|
|
|
8 |
- **Real-time Teleoperation**: Keyboard control with live motor feedback
|
9 |
- **Motor Calibration**: Automated homing offset and range recording
|
10 |
- **Cross-browser Support**: Chrome/Edge 89+ with HTTPS or localhost
|
@@ -13,29 +14,54 @@ Browser-native robotics control using WebSerial API. No Python dependencies requ
|
|
13 |
## Installation
|
14 |
|
15 |
```bash
|
|
|
|
|
|
|
|
|
16 |
npm install @lerobot/web
|
|
|
|
|
|
|
17 |
```
|
18 |
|
19 |
## Quick Start
|
20 |
|
21 |
```typescript
|
22 |
-
import { findPort, calibrate, teleoperate } from "@lerobot/web";
|
23 |
|
24 |
// 1. Find and connect to hardware
|
25 |
const findProcess = await findPort();
|
26 |
const robots = await findProcess.result;
|
27 |
const robot = robots[0];
|
28 |
|
29 |
-
// 2.
|
|
|
|
|
|
|
|
|
30 |
const calibrationProcess = await calibrate(robot, {
|
31 |
onProgress: (message) => console.log(message),
|
32 |
onLiveUpdate: (data) => console.log("Live positions:", data),
|
33 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
const calibrationData = await calibrationProcess.result;
|
35 |
|
36 |
-
//
|
37 |
const teleop = await teleoperate(robot, { calibrationData });
|
38 |
teleop.start();
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
```
|
40 |
|
41 |
## Core API
|
@@ -109,55 +135,6 @@ console.log(
|
|
109 |
);
|
110 |
```
|
111 |
|
112 |
-
#### Complete Workflow Example
|
113 |
-
|
114 |
-
```typescript
|
115 |
-
// First run: Interactive discovery
|
116 |
-
async function discoverNewRobots() {
|
117 |
-
const findProcess = await findPort();
|
118 |
-
const robots = await findProcess.result;
|
119 |
-
|
120 |
-
for (const robot of robots) {
|
121 |
-
// Configure each robot
|
122 |
-
robot.robotType = "so100_follower"; // User choice
|
123 |
-
robot.robotId = `robot_${Date.now()}`; // User input
|
124 |
-
|
125 |
-
// Save for auto-connect
|
126 |
-
localStorage.setItem(
|
127 |
-
`robot-${robot.serialNumber}`,
|
128 |
-
JSON.stringify({
|
129 |
-
robotType: robot.robotType,
|
130 |
-
robotId: robot.robotId,
|
131 |
-
serialNumber: robot.serialNumber,
|
132 |
-
})
|
133 |
-
);
|
134 |
-
}
|
135 |
-
|
136 |
-
return robots;
|
137 |
-
}
|
138 |
-
|
139 |
-
// Subsequent runs: Auto-connect
|
140 |
-
async function reconnectSavedRobots() {
|
141 |
-
// Load saved robot configs
|
142 |
-
const robotConfigs = [];
|
143 |
-
for (let i = 0; i < localStorage.length; i++) {
|
144 |
-
const key = localStorage.key(i);
|
145 |
-
if (key?.startsWith("robot-")) {
|
146 |
-
const saved = JSON.parse(localStorage.getItem(key)!);
|
147 |
-
robotConfigs.push(saved);
|
148 |
-
}
|
149 |
-
}
|
150 |
-
|
151 |
-
if (robotConfigs.length === 0) {
|
152 |
-
return discoverNewRobots(); // No saved robots, go interactive
|
153 |
-
}
|
154 |
-
|
155 |
-
// Auto-connect to saved robots
|
156 |
-
const findProcess = await findPort({ robotConfigs });
|
157 |
-
return await findProcess.result;
|
158 |
-
}
|
159 |
-
```
|
160 |
-
|
161 |
#### RobotConfig Structure
|
162 |
|
163 |
```typescript
|
@@ -198,7 +175,11 @@ const calibrationProcess = await calibrate(robot, {
|
|
198 |
});
|
199 |
|
200 |
// Move robot through full range of motion...
|
201 |
-
//
|
|
|
|
|
|
|
|
|
202 |
|
203 |
const calibrationData = await calibrationProcess.result;
|
204 |
// Save calibration data to localStorage or file
|
@@ -256,8 +237,11 @@ await teleop.setMotorPositions({
|
|
256 |
elbow_flex: 1500,
|
257 |
});
|
258 |
|
259 |
-
// Stop when
|
260 |
-
|
|
|
|
|
|
|
261 |
```
|
262 |
|
263 |
#### Parameters
|
@@ -289,19 +273,6 @@ Escape: Emergency stop
|
|
289 |
|
290 |
---
|
291 |
|
292 |
-
### `isWebSerialSupported(): boolean`
|
293 |
-
|
294 |
-
Checks if WebSerial API is available in the current browser.
|
295 |
-
|
296 |
-
```typescript
|
297 |
-
if (!isWebSerialSupported()) {
|
298 |
-
console.log("Please use Chrome/Edge 89+ with HTTPS or localhost");
|
299 |
-
return;
|
300 |
-
}
|
301 |
-
```
|
302 |
-
|
303 |
-
---
|
304 |
-
|
305 |
### `releaseMotors(robot, motorIds?): Promise<void>`
|
306 |
|
307 |
Releases motor torque so robot can be moved freely by hand.
|
@@ -311,7 +282,7 @@ Releases motor torque so robot can be moved freely by hand.
|
|
311 |
await releaseMotors(robot);
|
312 |
|
313 |
// Release specific motors only
|
314 |
-
await releaseMotors(robot, [1, 2, 3]);
|
315 |
```
|
316 |
|
317 |
#### Parameters
|
@@ -319,145 +290,12 @@ await releaseMotors(robot, [1, 2, 3]); // First 3 motors
|
|
319 |
- `robot: RobotConnection` - Connected robot
|
320 |
- `motorIds?: number[]` - Specific motor IDs (default: all motors for robot type)
|
321 |
|
322 |
-
## Types
|
323 |
-
|
324 |
-
### `RobotConnection`
|
325 |
-
|
326 |
-
```typescript
|
327 |
-
interface RobotConnection {
|
328 |
-
port: SerialPort; // WebSerial port object
|
329 |
-
name: string; // Display name
|
330 |
-
isConnected: boolean; // Connection status
|
331 |
-
robotType?: "so100_follower" | "so100_leader";
|
332 |
-
robotId?: string; // User-defined ID
|
333 |
-
serialNumber: string; // Device serial number
|
334 |
-
error?: string; // Error message if failed
|
335 |
-
}
|
336 |
-
```
|
337 |
-
|
338 |
-
### `WebCalibrationResults`
|
339 |
-
|
340 |
-
```typescript
|
341 |
-
interface WebCalibrationResults {
|
342 |
-
[motorName: string]: {
|
343 |
-
id: number; // Motor ID (1-6)
|
344 |
-
drive_mode: number; // Drive mode (0 for SO-100)
|
345 |
-
homing_offset: number; // Calculated offset
|
346 |
-
range_min: number; // Minimum position
|
347 |
-
range_max: number; // Maximum position
|
348 |
-
};
|
349 |
-
}
|
350 |
-
```
|
351 |
-
|
352 |
-
### `TeleoperationState`
|
353 |
-
|
354 |
-
```typescript
|
355 |
-
interface TeleoperationState {
|
356 |
-
isActive: boolean; // Whether teleoperation is running
|
357 |
-
motorConfigs: MotorConfig[]; // Current motor positions and limits
|
358 |
-
lastUpdate: number; // Timestamp of last update
|
359 |
-
keyStates: { [key: string]: { pressed: boolean; timestamp: number } };
|
360 |
-
}
|
361 |
-
```
|
362 |
-
|
363 |
-
### `MotorConfig`
|
364 |
-
|
365 |
-
```typescript
|
366 |
-
interface MotorConfig {
|
367 |
-
id: number; // Motor ID
|
368 |
-
name: string; // Motor name (e.g., "shoulder_pan")
|
369 |
-
currentPosition: number; // Current position
|
370 |
-
minPosition: number; // Position limit minimum
|
371 |
-
maxPosition: number; // Position limit maximum
|
372 |
-
}
|
373 |
-
```
|
374 |
-
|
375 |
-
### `RobotConfig`
|
376 |
-
|
377 |
-
```typescript
|
378 |
-
interface RobotConfig {
|
379 |
-
robotType: "so100_follower" | "so100_leader";
|
380 |
-
robotId: string; // Your custom identifier (e.g., "left_arm")
|
381 |
-
serialNumber: string; // Device serial number (from previous findPort)
|
382 |
-
}
|
383 |
-
```
|
384 |
-
|
385 |
-
## Advanced Usage
|
386 |
-
|
387 |
-
### Custom Motor Communication
|
388 |
-
|
389 |
-
```typescript
|
390 |
-
import {
|
391 |
-
WebSerialPortWrapper,
|
392 |
-
readMotorPosition,
|
393 |
-
writeMotorPosition,
|
394 |
-
} from "@lerobot/web";
|
395 |
-
|
396 |
-
const port = new WebSerialPortWrapper(robotConnection.port);
|
397 |
-
await port.initialize();
|
398 |
-
|
399 |
-
// Read motor position directly
|
400 |
-
const position = await readMotorPosition(port, 1);
|
401 |
-
|
402 |
-
// Write motor position directly
|
403 |
-
await writeMotorPosition(port, 1, 2048);
|
404 |
-
```
|
405 |
-
|
406 |
-
### Robot Configuration
|
407 |
-
|
408 |
-
```typescript
|
409 |
-
import { createSO100Config, SO100_KEYBOARD_CONTROLS } from "@lerobot/web";
|
410 |
-
|
411 |
-
const config = createSO100Config("so100_follower");
|
412 |
-
console.log("Motor names:", config.motorNames);
|
413 |
-
console.log("Motor IDs:", config.motorIds);
|
414 |
-
console.log("Keyboard controls:", SO100_KEYBOARD_CONTROLS);
|
415 |
-
```
|
416 |
-
|
417 |
-
### Low-Level Motor Control
|
418 |
-
|
419 |
-
```typescript
|
420 |
-
import { releaseMotorsLowLevel } from "@lerobot/web";
|
421 |
-
|
422 |
-
const port = new WebSerialPortWrapper(robotConnection.port);
|
423 |
-
await port.initialize();
|
424 |
-
|
425 |
-
// Release specific motors at low level
|
426 |
-
await releaseMotorsLowLevel(port, [1, 2, 3]);
|
427 |
-
```
|
428 |
-
|
429 |
-
## Error Handling
|
430 |
-
|
431 |
-
```typescript
|
432 |
-
try {
|
433 |
-
const findProcess = await findPort();
|
434 |
-
const robots = await findProcess.result;
|
435 |
-
|
436 |
-
if (robots.length === 0) {
|
437 |
-
throw new Error("No robots found");
|
438 |
-
}
|
439 |
-
|
440 |
-
const robot = robots[0];
|
441 |
-
if (!robot.isConnected) {
|
442 |
-
throw new Error(`Failed to connect: ${robot.error}`);
|
443 |
-
}
|
444 |
-
|
445 |
-
// Proceed with calibration/teleoperation
|
446 |
-
} catch (error) {
|
447 |
-
console.error("Robot connection failed:", error.message);
|
448 |
-
}
|
449 |
-
```
|
450 |
-
|
451 |
## Browser Requirements
|
452 |
|
453 |
-
- **
|
454 |
-
- **HTTPS or localhost**
|
455 |
- **User gesture** required for initial port selection
|
456 |
|
457 |
## Hardware Support
|
458 |
|
459 |
Currently supports SO-100 follower and leader arms with STS3215 motors. More devices coming soon.
|
460 |
-
|
461 |
-
## License
|
462 |
-
|
463 |
-
Apache-2.0
|
|
|
1 |
# @lerobot/web
|
2 |
|
3 |
+
Browser-native robotics control using WebSerial and WebUSB APIs.
|
4 |
|
5 |
## Features
|
6 |
|
7 |
- **Direct Hardware Control**: STS3215 motor communication via WebSerial API
|
8 |
+
- **Device Persistence**: WebUSB API for automatic robot reconnection
|
9 |
- **Real-time Teleoperation**: Keyboard control with live motor feedback
|
10 |
- **Motor Calibration**: Automated homing offset and range recording
|
11 |
- **Cross-browser Support**: Chrome/Edge 89+ with HTTPS or localhost
|
|
|
14 |
## Installation
|
15 |
|
16 |
```bash
|
17 |
+
# pnpm (recommended)
|
18 |
+
pnpm add @lerobot/web
|
19 |
+
|
20 |
+
# npm
|
21 |
npm install @lerobot/web
|
22 |
+
|
23 |
+
# yarn
|
24 |
+
yarn add @lerobot/web
|
25 |
```
|
26 |
|
27 |
## Quick Start
|
28 |
|
29 |
```typescript
|
30 |
+
import { findPort, releaseMotors, calibrate, teleoperate } from "@lerobot/web";
|
31 |
|
32 |
// 1. Find and connect to hardware
|
33 |
const findProcess = await findPort();
|
34 |
const robots = await findProcess.result;
|
35 |
const robot = robots[0];
|
36 |
|
37 |
+
// 2. Release motors for manual positioning
|
38 |
+
await releaseMotors(robot);
|
39 |
+
console.log("🔓 Motors released - you can move the robot by hand");
|
40 |
+
|
41 |
+
// 3. Calibrate motors
|
42 |
const calibrationProcess = await calibrate(robot, {
|
43 |
onProgress: (message) => console.log(message),
|
44 |
onLiveUpdate: (data) => console.log("Live positions:", data),
|
45 |
});
|
46 |
+
|
47 |
+
// Move robot through its full range of motion...
|
48 |
+
// When done, stop calibration to proceed
|
49 |
+
setTimeout(() => {
|
50 |
+
console.log("⏱️ Stopping calibration...");
|
51 |
+
calibrationProcess.stop();
|
52 |
+
}, 10000); // Stop after 10 seconds, or call stop() when user is ready
|
53 |
+
|
54 |
const calibrationData = await calibrationProcess.result;
|
55 |
|
56 |
+
// 4. Start teleoperation
|
57 |
const teleop = await teleoperate(robot, { calibrationData });
|
58 |
teleop.start();
|
59 |
+
|
60 |
+
// Stop teleoperation when done
|
61 |
+
setTimeout(() => {
|
62 |
+
console.log("⏹️ Stopping teleoperation...");
|
63 |
+
teleop.stop();
|
64 |
+
}, 30000); // Stop after 30 seconds, or call stop() when needed
|
65 |
```
|
66 |
|
67 |
## Core API
|
|
|
135 |
);
|
136 |
```
|
137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
#### RobotConfig Structure
|
139 |
|
140 |
```typescript
|
|
|
175 |
});
|
176 |
|
177 |
// Move robot through full range of motion...
|
178 |
+
// When finished recording ranges, stop the calibration
|
179 |
+
console.log("Move robot through its range, then stopping in 10 seconds...");
|
180 |
+
setTimeout(() => {
|
181 |
+
calibrationProcess.stop(); // Stop range recording
|
182 |
+
}, 10000);
|
183 |
|
184 |
const calibrationData = await calibrationProcess.result;
|
185 |
// Save calibration data to localStorage or file
|
|
|
237 |
elbow_flex: 1500,
|
238 |
});
|
239 |
|
240 |
+
// Stop teleoperation when finished
|
241 |
+
setTimeout(() => {
|
242 |
+
console.log("Stopping teleoperation...");
|
243 |
+
teleop.stop();
|
244 |
+
}, 30000); // Or call teleop.stop() when user is done
|
245 |
```
|
246 |
|
247 |
#### Parameters
|
|
|
273 |
|
274 |
---
|
275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
276 |
### `releaseMotors(robot, motorIds?): Promise<void>`
|
277 |
|
278 |
Releases motor torque so robot can be moved freely by hand.
|
|
|
282 |
await releaseMotors(robot);
|
283 |
|
284 |
// Release specific motors only
|
285 |
+
await releaseMotors(robot, [1, 2, 3]);
|
286 |
```
|
287 |
|
288 |
#### Parameters
|
|
|
290 |
- `robot: RobotConnection` - Connected robot
|
291 |
- `motorIds?: number[]` - Specific motor IDs (default: all motors for robot type)
|
292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
293 |
## Browser Requirements
|
294 |
|
295 |
+
- **chromium 89+** with WebSerial and WebUSB API support
|
296 |
+
- **HTTPS or localhost**
|
297 |
- **User gesture** required for initial port selection
|
298 |
|
299 |
## Hardware Support
|
300 |
|
301 |
Currently supports SO-100 follower and leader arms with STS3215 motors. More devices coming soon.
|
|
|
|
|
|
|
|