Spaces:
Running
Running
feat: remove unused code, apply DRY
Browse files- docs/api-comparison.md +0 -194
- src/demo/App.tsx +1 -1
- src/demo/components/CalibrationPanel.tsx +3 -11
- src/demo/components/PortManager.tsx +1 -1
- src/demo/components/TeleoperationPanel.tsx +168 -31
- src/demo/hooks/useTeleoperation.ts +0 -518
- src/demo/pages/Home.tsx +1 -1
- src/lerobot/web/calibrate.ts +45 -57
- src/lerobot/web/find_port.ts +6 -61
- src/lerobot/web/robots/so100_config.ts +49 -3
- src/lerobot/web/teleoperate.ts +182 -326
- src/lerobot/web/types/robot-config.ts +40 -0
- src/lerobot/web/types/robot-connection.ts +64 -0
docs/api-comparison.md
DELETED
@@ -1,194 +0,0 @@
|
|
1 |
-
# lerobot API Comparison: Python vs Node.js vs Web
|
2 |
-
|
3 |
-
This document provides a comprehensive three-way comparison of lerobot APIs across Python lerobot (original), Node.js lerobot.js, and Web lerobot.js platforms.
|
4 |
-
|
5 |
-
## 🔄 Core Function Comparison
|
6 |
-
|
7 |
-
| Function Category | Python lerobot (Original) | Node.js lerobot.js | Web Browser lerobot.js | Key Pattern |
|
8 |
-
| ------------------ | ------------------------- | ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------ |
|
9 |
-
| **Port Discovery** | `find_port()` | `findPort()` | `findPortWeb(logger)` | Python → Node.js: Direct port, Web: Requires UI logger |
|
10 |
-
| **Robot Creation** | `SO100Follower(config)` | `createSO100Follower(config)` | `createWebTeleoperationController(port, serialNumber)` | Python: Class, Node.js: Factory, Web: Pre-opened port |
|
11 |
-
| **Calibration** | `calibrate(cfg)` | `calibrate(config)` | `createCalibrationController(armType, port)` | Python/Node.js: Function, Web: Controller pattern |
|
12 |
-
| **Teleoperation** | `teleoperate(cfg)` | `teleoperate(config)` | `createWebTeleoperationController(port, serialNumber)` | Python/Node.js: Function, Web: Manual state management |
|
13 |
-
|
14 |
-
## 📋 Detailed API Reference
|
15 |
-
|
16 |
-
### Port Discovery
|
17 |
-
|
18 |
-
| Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js |
|
19 |
-
| -------------------- | ----------------------------------------- | -------------------------------------------- | ---------------------------------------------- |
|
20 |
-
| **Function** | `find_port()` | `findPort()` | `findPortWeb(logger)` |
|
21 |
-
| **Import** | `from lerobot.find_port import find_port` | `import { findPort } from 'lerobot.js/node'` | `import { findPortWeb } from 'lerobot.js/web'` |
|
22 |
-
| **Parameters** | None | None | `logger: (message: string) => void` |
|
23 |
-
| **User Interaction** | Terminal prompts via `input()` | Terminal prompts via readline | Browser modals and buttons |
|
24 |
-
| **Port Access** | Direct system access via pyserial | Direct system access | Web Serial API permissions |
|
25 |
-
| **Return Value** | None (prints to console) | None (prints to console) | None (calls logger function) |
|
26 |
-
| **Example** | `python<br>find_port()<br>` | `js<br>await findPort();<br>` | `js<br>await findPortWeb(console.log);<br>` |
|
27 |
-
|
28 |
-
### Robot Connection & Creation
|
29 |
-
|
30 |
-
| Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js |
|
31 |
-
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
32 |
-
| **Creation** | `SO100Follower(config)` or `make_robot_from_config(config)` | `createSO100Follower(config)` | `createWebTeleoperationController(port, serialNumber)` |
|
33 |
-
| **Connection** | `robot.connect()` | `await robot.connect()` | Port already opened before creation |
|
34 |
-
| **Port Parameter** | `RobotConfig(port='/dev/ttyUSB0')` | `{ port: 'COM4' }` (string) | `SerialPort` object |
|
35 |
-
| **Baud Rate** | Handled internally | Handled internally | `await port.open({ baudRate: 1000000 })` |
|
36 |
-
| **Factory Pattern** | `make_robot_from_config()` factory | `createSO100Follower()` factory | `createWebTeleoperationController()` factory |
|
37 |
-
| **Example** | `python<br>from lerobot.common.robots.so100_follower import SO100Follower<br>robot = SO100Follower(config)<br>robot.connect()<br>` | `js<br>const robot = createSO100Follower({<br> type: 'so100_follower',<br> port: 'COM4',<br> id: 'my_robot'<br>});<br>await robot.connect();<br>` | `js<br>const port = await navigator.serial.requestPort();<br>await port.open({ baudRate: 1000000 });<br>const robot = await createWebTeleoperationController(<br> port, 'my_robot'<br>);<br>` |
|
38 |
-
|
39 |
-
### Calibration
|
40 |
-
|
41 |
-
| Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js |
|
42 |
-
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
43 |
-
| **Main Function** | `calibrate(cfg)` | `calibrate(config)` | `createCalibrationController(armType, port)` |
|
44 |
-
| **Import** | `from lerobot.calibrate import calibrate` | `import { calibrate } from 'lerobot.js/node'` | `import { createCalibrationController } from 'lerobot.js/web'` |
|
45 |
-
| **Configuration** | `CalibrateConfig` dataclass | Single config object | Controller with methods |
|
46 |
-
| **Workflow** | All-in-one function calls `device.calibrate()` | All-in-one function | Step-by-step methods |
|
47 |
-
| **Device Pattern** | Creates device, calls `device.calibrate()`, `device.disconnect()` | Automatic within calibrate() | Manual controller lifecycle |
|
48 |
-
| **Homing** | Automatic within `device.calibrate()` | Automatic within calibrate() | `await controller.performHomingStep()` |
|
49 |
-
| **Range Recording** | Automatic within `device.calibrate()` | Automatic within calibrate() | `await controller.performRangeRecordingStep()` |
|
50 |
-
| **Completion** | Automatic save and disconnect | Automatic save | `await controller.finishCalibration()` |
|
51 |
-
| **Data Storage** | File system (HF cache) | File system (HF cache) | localStorage + file download |
|
52 |
-
| **Example** | `python<br>from lerobot.calibrate import calibrate<br>from lerobot.common.robots.so100_follower import SO100FollowerConfig<br>calibrate(CalibrateConfig(<br> robot=SO100FollowerConfig(<br> port='/dev/ttyUSB0', id='my_robot'<br> )<br>))<br>` | `js<br>await calibrate({<br> robot: {<br> type: 'so100_follower',<br> port: 'COM4',<br> id: 'my_robot'<br> }<br>});<br>` | `js<br>const controller = await createCalibrationController(<br> 'so100_follower', port<br>);<br>await controller.performHomingStep();<br>await controller.performRangeRecordingStep(stopCondition);<br>const results = await controller.finishCalibration();<br>` |
|
53 |
-
|
54 |
-
### Teleoperation
|
55 |
-
|
56 |
-
| Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js |
|
57 |
-
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
58 |
-
| **Main Function** | `teleoperate(cfg)` | `teleoperate(config)` | `createWebTeleoperationController(port, serialNumber)` |
|
59 |
-
| **Import** | `from lerobot.teleoperate import teleoperate` | `import { teleoperate } from 'lerobot.js/node'` | `import { createWebTeleoperationController } from 'lerobot.js/web'` |
|
60 |
-
| **Control Loop** | `teleop_loop()` with `get_action()` and `send_action()` | Automatic 60 FPS loop | Manual start/stop with `controller.start()` |
|
61 |
-
| **Device Management** | Creates teleop and robot devices, connects both | Device creation handled internally | Port opened externally, controller manages state |
|
62 |
-
| **Input Handling** | Teleoperator `get_action()` method | Terminal raw mode | Browser event listeners |
|
63 |
-
| **Key State** | Handled by teleoperator device | Internal management | `controller.updateKeyState(key, pressed)` |
|
64 |
-
| **Configuration** | `TeleoperateConfig` with separate robot/teleop configs | `js<br>{<br> robot: { type, port, id },<br> teleop: { type: 'keyboard' },<br> fps: 60,<br> step_size: 25<br>}<br>` | Built into controller |
|
65 |
-
| **Example** | `python<br>from lerobot.teleoperate import teleoperate<br>teleoperate(TeleoperateConfig(<br> robot=SO100FollowerConfig(port='/dev/ttyUSB0'),<br> teleop=SO100LeaderConfig(port='/dev/ttyUSB1')<br>))<br>` | `js<br>await teleoperate({<br> robot: {<br> type: 'so100_follower',<br> port: 'COM4',<br> id: 'my_robot'<br> },<br> teleop: { type: 'keyboard' }<br>});<br>` | `js<br>const controller = await createWebTeleoperationController(<br> port, 'my_robot'<br>);<br>controller.start();<br>// Handle keyboard events manually<br>document.addEventListener('keydown', (e) => {<br> controller.updateKeyState(e.key, true);<br>});<br>` |
|
66 |
-
|
67 |
-
### Motor Control
|
68 |
-
|
69 |
-
| Aspect | Python lerobot | Node.js lerobot.js | Web Browser lerobot.js |
|
70 |
-
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
71 |
-
| **Get Positions** | `robot.get_observation()` (includes motor positions) | `await robot.getMotorPositions()` | `controller.getMotorConfigs()` or `controller.getState()` |
|
72 |
-
| **Set Positions** | `robot.send_action(action)` (dict format) | `await robot.setMotorPositions(positions)` | `await controller.setMotorPositions(positions)` |
|
73 |
-
| **Position Format** | `dict[str, float]` (action format) | `Record<string, number>` | `Record<string, number>` ✅ Same |
|
74 |
-
| **Data Flow** | `get_observation()` → `send_action()` loop | Direct position get/set methods | Controller state management |
|
75 |
-
| **Action Features** | `robot.action_features` (motor names) | Motor names hardcoded in implementation | Motor configs with metadata |
|
76 |
-
| **Calibration Limits** | Handled in robot implementation | `robot.getCalibrationLimits()` | `controller.getMotorConfigs()` (includes limits) |
|
77 |
-
| **Home Position** | Manual via action dict | Manual calculation | `await controller.goToHomePosition()` |
|
78 |
-
| **Example** | `python<br>obs = robot.get_observation()<br>action = {motor: value for motor in robot.action_features}<br>robot.send_action(action)<br>` | `js<br>const positions = await robot.getMotorPositions();<br>await robot.setMotorPositions({<br> shoulder_pan: 2047,<br> shoulder_lift: 1800<br>});<br>` | `js<br>const state = controller.getState();<br>await controller.setMotorPositions({<br> shoulder_pan: 2047,<br> shoulder_lift: 1800<br>});<br>` |
|
79 |
-
|
80 |
-
### Calibration Data Management
|
81 |
-
|
82 |
-
| Aspect | Node.js | Web Browser |
|
83 |
-
| -------------------- | ------------------------------------------- | ------------------------------------- |
|
84 |
-
| **Storage Location** | `~/.cache/huggingface/lerobot/calibration/` | `localStorage` + file download |
|
85 |
-
| **File Format** | JSON files on disk | JSON in browser storage |
|
86 |
-
| **Loading** | Automatic during `robot.connect()` | Manual via `loadCalibrationConfig()` |
|
87 |
-
| **Saving** | `robot.saveCalibration(results)` | `saveCalibrationResults()` + download |
|
88 |
-
| **Persistence** | Permanent until deleted | Browser-specific, can be cleared |
|
89 |
-
| **Sharing** | File system sharing | Manual file sharing |
|
90 |
-
|
91 |
-
### Error Handling & Debugging
|
92 |
-
|
93 |
-
| Aspect | Node.js | Web Browser |
|
94 |
-
| --------------------- | ------------------------ | ------------------------------------ |
|
95 |
-
| **Connection Errors** | Standard Node.js errors | Web Serial API errors |
|
96 |
-
| **Permission Issues** | File system permissions | User permission prompts |
|
97 |
-
| **Port Conflicts** | "Port in use" errors | Silent failures or permission errors |
|
98 |
-
| **Debugging** | Console.log + terminal | Browser DevTools console |
|
99 |
-
| **Logging** | Built-in terminal output | Passed logger functions |
|
100 |
-
|
101 |
-
## 🎯 Usage Pattern Summary
|
102 |
-
|
103 |
-
### Python lerobot (Original) - Research & Production
|
104 |
-
|
105 |
-
```python
|
106 |
-
# Configuration-driven, device-based approach
|
107 |
-
from lerobot.find_port import find_port
|
108 |
-
from lerobot.calibrate import calibrate, CalibrateConfig
|
109 |
-
from lerobot.teleoperate import teleoperate, TeleoperateConfig
|
110 |
-
from lerobot.common.robots.so100_follower import SO100FollowerConfig
|
111 |
-
from lerobot.common.teleoperators.so100_leader import SO100LeaderConfig
|
112 |
-
|
113 |
-
# Find port
|
114 |
-
find_port()
|
115 |
-
|
116 |
-
# Calibrate robot
|
117 |
-
calibrate(CalibrateConfig(
|
118 |
-
robot=SO100FollowerConfig(port='/dev/ttyUSB0', id='my_robot')
|
119 |
-
))
|
120 |
-
|
121 |
-
# Teleoperation with leader-follower
|
122 |
-
teleoperate(TeleoperateConfig(
|
123 |
-
robot=SO100FollowerConfig(port='/dev/ttyUSB0'),
|
124 |
-
teleop=SO100LeaderConfig(port='/dev/ttyUSB1')
|
125 |
-
))
|
126 |
-
```
|
127 |
-
|
128 |
-
### Node.js lerobot.js - Server/Desktop Applications
|
129 |
-
|
130 |
-
```javascript
|
131 |
-
// High-level, all-in-one functions (mirrors Python closely)
|
132 |
-
import { findPort, calibrate, teleoperate } from "lerobot.js/node";
|
133 |
-
|
134 |
-
await findPort();
|
135 |
-
await calibrate({
|
136 |
-
robot: { type: "so100_follower", port: "COM4", id: "my_robot" },
|
137 |
-
});
|
138 |
-
await teleoperate({
|
139 |
-
robot: { type: "so100_follower", port: "COM4" },
|
140 |
-
teleop: { type: "keyboard" },
|
141 |
-
});
|
142 |
-
```
|
143 |
-
|
144 |
-
### Web Browser lerobot.js - Interactive Applications
|
145 |
-
|
146 |
-
```javascript
|
147 |
-
// Controller-based, step-by-step approach (browser constraints)
|
148 |
-
import {
|
149 |
-
findPortWeb,
|
150 |
-
createCalibrationController,
|
151 |
-
createWebTeleoperationController,
|
152 |
-
} from "lerobot.js/web";
|
153 |
-
|
154 |
-
// User interaction required
|
155 |
-
const port = await navigator.serial.requestPort();
|
156 |
-
await port.open({ baudRate: 1000000 });
|
157 |
-
|
158 |
-
// Step-by-step calibration
|
159 |
-
const calibrator = await createCalibrationController("so100_follower", port);
|
160 |
-
await calibrator.performHomingStep();
|
161 |
-
await calibrator.performRangeRecordingStep(() => stopRecording);
|
162 |
-
|
163 |
-
// Manual teleoperation control
|
164 |
-
const controller = await createWebTeleoperationController(port, "my_robot");
|
165 |
-
controller.start();
|
166 |
-
```
|
167 |
-
|
168 |
-
## 🔑 Key Architectural Differences
|
169 |
-
|
170 |
-
1. **User Interaction Model**
|
171 |
-
|
172 |
-
- **Node.js**: Terminal-based with readline prompts
|
173 |
-
- **Web**: Browser UI with buttons and modals
|
174 |
-
|
175 |
-
2. **Permission Model**
|
176 |
-
|
177 |
-
- **Node.js**: System-level permissions
|
178 |
-
- **Web**: User-granted permissions per device
|
179 |
-
|
180 |
-
3. **State Management**
|
181 |
-
|
182 |
-
- **Node.js**: Function-based, stateless
|
183 |
-
- **Web**: Controller-based, stateful
|
184 |
-
|
185 |
-
4. **Data Persistence**
|
186 |
-
|
187 |
-
- **Node.js**: File system with cross-session persistence
|
188 |
-
- **Web**: Browser storage with limited persistence
|
189 |
-
|
190 |
-
5. **Platform Integration**
|
191 |
-
- **Node.js**: Deep system integration
|
192 |
-
- **Web**: Security-constrained browser environment
|
193 |
-
|
194 |
-
This comparison helps developers choose the right platform and understand the API differences when porting between Node.js and Web implementations.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/App.tsx
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
import React, { useState } from "react";
|
2 |
import { Home } from "./pages/Home";
|
3 |
import { ErrorBoundary } from "./components/ErrorBoundary";
|
4 |
-
import type { RobotConnection } from "../lerobot/web/
|
5 |
|
6 |
export function App() {
|
7 |
const [connectedRobots, setConnectedRobots] = useState<RobotConnection[]>([]);
|
|
|
1 |
import React, { useState } from "react";
|
2 |
import { Home } from "./pages/Home";
|
3 |
import { ErrorBoundary } from "./components/ErrorBoundary";
|
4 |
+
import type { RobotConnection } from "../lerobot/web/types/robot-connection.js";
|
5 |
|
6 |
export function App() {
|
7 |
const [connectedRobots, setConnectedRobots] = useState<RobotConnection[]>([]);
|
src/demo/components/CalibrationPanel.tsx
CHANGED
@@ -18,7 +18,7 @@ import { releaseMotors } from "../../lerobot/web/utils/motor-communication.js";
|
|
18 |
import { WebSerialPortWrapper } from "../../lerobot/web/utils/serial-port-wrapper.js";
|
19 |
import { createSO100Config } from "../../lerobot/web/robots/so100_config.js";
|
20 |
import { CalibrationModal } from "./CalibrationModal";
|
21 |
-
import type { RobotConnection } from "../../lerobot/web/
|
22 |
|
23 |
interface CalibrationPanelProps {
|
24 |
robot: RobotConnection;
|
@@ -104,16 +104,8 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
|
|
104 |
setIsCalibrating(true);
|
105 |
initializeMotorData();
|
106 |
|
107 |
-
// Use the
|
108 |
-
const
|
109 |
-
port: robot.port,
|
110 |
-
robotType: robot.robotType!,
|
111 |
-
robotId: robot.robotId || `${robot.robotType}_1`,
|
112 |
-
serialNumber: robot.serialNumber || `unknown_${Date.now()}`,
|
113 |
-
connected: robot.isConnected,
|
114 |
-
} as any; // Type assertion to work around SerialPort type differences
|
115 |
-
|
116 |
-
const process = await calibrate(robotConnection, {
|
117 |
onLiveUpdate: (data) => {
|
118 |
setMotorData(data);
|
119 |
setStatus(
|
|
|
18 |
import { WebSerialPortWrapper } from "../../lerobot/web/utils/serial-port-wrapper.js";
|
19 |
import { createSO100Config } from "../../lerobot/web/robots/so100_config.js";
|
20 |
import { CalibrationModal } from "./CalibrationModal";
|
21 |
+
import type { RobotConnection } from "../../lerobot/web/types/robot-connection.js";
|
22 |
|
23 |
interface CalibrationPanelProps {
|
24 |
robot: RobotConnection;
|
|
|
104 |
setIsCalibrating(true);
|
105 |
initializeMotorData();
|
106 |
|
107 |
+
// Use the simple calibrate API - just pass the robot connection
|
108 |
+
const process = await calibrate(robot, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
onLiveUpdate: (data) => {
|
110 |
setMotorData(data);
|
111 |
setStatus(
|
src/demo/components/PortManager.tsx
CHANGED
@@ -18,7 +18,7 @@ import {
|
|
18 |
DialogTitle,
|
19 |
} from "./ui/dialog";
|
20 |
import { isWebSerialSupported } from "../../lerobot/web/calibrate";
|
21 |
-
import type { RobotConnection } from "../../lerobot/web/
|
22 |
|
23 |
/**
|
24 |
* Type definitions for WebSerial API (missing from TypeScript)
|
|
|
18 |
DialogTitle,
|
19 |
} from "./ui/dialog";
|
20 |
import { isWebSerialSupported } from "../../lerobot/web/calibrate";
|
21 |
+
import type { RobotConnection } from "../../lerobot/web/types/robot-connection.js";
|
22 |
|
23 |
/**
|
24 |
* Type definitions for WebSerial API (missing from TypeScript)
|
src/demo/components/TeleoperationPanel.tsx
CHANGED
@@ -1,12 +1,17 @@
|
|
1 |
-
import React, { useState } from "react";
|
2 |
import { Button } from "./ui/button";
|
3 |
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
4 |
import { Badge } from "./ui/badge";
|
5 |
import { Alert, AlertDescription } from "./ui/alert";
|
6 |
import { Progress } from "./ui/progress";
|
7 |
-
import {
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
interface TeleoperationPanelProps {
|
12 |
robot: RobotConnection;
|
@@ -17,41 +22,171 @@ export function TeleoperationPanel({
|
|
17 |
robot,
|
18 |
onClose,
|
19 |
}: TeleoperationPanelProps) {
|
20 |
-
const [
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
};
|
43 |
|
44 |
const handleStop = () => {
|
45 |
-
|
46 |
-
|
|
|
|
|
47 |
};
|
48 |
|
49 |
const handleClose = () => {
|
50 |
-
|
51 |
-
|
|
|
52 |
onClose();
|
53 |
};
|
54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
// Virtual keyboard component
|
56 |
const VirtualKeyboard = () => {
|
57 |
const isKeyPressed = (key: string) => {
|
@@ -70,7 +205,9 @@ export function TeleoperationPanel({
|
|
70 |
size?: "default" | "sm" | "lg" | "icon";
|
71 |
}) => {
|
72 |
const control =
|
73 |
-
|
|
|
|
|
74 |
const pressed = isKeyPressed(keyCode);
|
75 |
|
76 |
return (
|
|
|
1 |
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
2 |
import { Button } from "./ui/button";
|
3 |
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
4 |
import { Badge } from "./ui/badge";
|
5 |
import { Alert, AlertDescription } from "./ui/alert";
|
6 |
import { Progress } from "./ui/progress";
|
7 |
+
import {
|
8 |
+
teleoperate,
|
9 |
+
type TeleoperationProcess,
|
10 |
+
type TeleoperationState,
|
11 |
+
} from "../../lerobot/web/teleoperate.js";
|
12 |
+
import { getUnifiedRobotData } from "../lib/unified-storage";
|
13 |
+
import type { RobotConnection } from "../../lerobot/web/types/robot-connection.js";
|
14 |
+
import { SO100_KEYBOARD_CONTROLS } from "../../lerobot/web/robots/so100_config.js";
|
15 |
|
16 |
interface TeleoperationPanelProps {
|
17 |
robot: RobotConnection;
|
|
|
22 |
robot,
|
23 |
onClose,
|
24 |
}: TeleoperationPanelProps) {
|
25 |
+
const [teleoperationState, setTeleoperationState] =
|
26 |
+
useState<TeleoperationState>({
|
27 |
+
isActive: false,
|
28 |
+
motorConfigs: [],
|
29 |
+
lastUpdate: 0,
|
30 |
+
keyStates: {},
|
31 |
+
});
|
32 |
+
const [error, setError] = useState<string | null>(null);
|
33 |
+
const [isInitialized, setIsInitialized] = useState(false);
|
34 |
+
|
35 |
+
const teleoperationProcessRef = useRef<TeleoperationProcess | null>(null);
|
36 |
+
|
37 |
+
// Initialize teleoperation process
|
38 |
+
useEffect(() => {
|
39 |
+
const initializeTeleoperation = async () => {
|
40 |
+
if (!robot || !robot.robotType) {
|
41 |
+
setError("No robot configuration available");
|
42 |
+
return;
|
43 |
+
}
|
44 |
+
|
45 |
+
try {
|
46 |
+
// Load calibration data from demo storage (app concern)
|
47 |
+
let calibrationData;
|
48 |
+
if (robot.serialNumber) {
|
49 |
+
const data = getUnifiedRobotData(robot.serialNumber);
|
50 |
+
calibrationData = data?.calibration;
|
51 |
+
if (calibrationData) {
|
52 |
+
console.log("✅ Loaded calibration data for", robot.serialNumber);
|
53 |
+
}
|
54 |
+
}
|
55 |
+
|
56 |
+
// Create teleoperation process using clean library API
|
57 |
+
const process = await teleoperate(robot, {
|
58 |
+
calibrationData,
|
59 |
+
onStateUpdate: (state: TeleoperationState) => {
|
60 |
+
setTeleoperationState(state);
|
61 |
+
},
|
62 |
+
});
|
63 |
+
|
64 |
+
teleoperationProcessRef.current = process;
|
65 |
+
setTeleoperationState(process.getState());
|
66 |
+
setIsInitialized(true);
|
67 |
+
setError(null);
|
68 |
+
} catch (error) {
|
69 |
+
const errorMessage =
|
70 |
+
error instanceof Error
|
71 |
+
? error.message
|
72 |
+
: "Failed to initialize teleoperation";
|
73 |
+
setError(errorMessage);
|
74 |
+
console.error("❌ Failed to initialize teleoperation:", error);
|
75 |
+
}
|
76 |
+
};
|
77 |
+
|
78 |
+
initializeTeleoperation();
|
79 |
+
|
80 |
+
return () => {
|
81 |
+
// Cleanup on unmount
|
82 |
+
if (teleoperationProcessRef.current) {
|
83 |
+
teleoperationProcessRef.current.disconnect();
|
84 |
+
teleoperationProcessRef.current = null;
|
85 |
+
}
|
86 |
+
};
|
87 |
+
}, [robot]);
|
88 |
+
|
89 |
+
// Keyboard event handlers
|
90 |
+
const handleKeyDown = useCallback(
|
91 |
+
(event: KeyboardEvent) => {
|
92 |
+
if (!teleoperationState.isActive || !teleoperationProcessRef.current)
|
93 |
+
return;
|
94 |
+
|
95 |
+
const key = event.key;
|
96 |
+
event.preventDefault();
|
97 |
+
teleoperationProcessRef.current.updateKeyState(key, true);
|
98 |
+
},
|
99 |
+
[teleoperationState.isActive]
|
100 |
+
);
|
101 |
+
|
102 |
+
const handleKeyUp = useCallback(
|
103 |
+
(event: KeyboardEvent) => {
|
104 |
+
if (!teleoperationState.isActive || !teleoperationProcessRef.current)
|
105 |
+
return;
|
106 |
+
|
107 |
+
const key = event.key;
|
108 |
+
event.preventDefault();
|
109 |
+
teleoperationProcessRef.current.updateKeyState(key, false);
|
110 |
+
},
|
111 |
+
[teleoperationState.isActive]
|
112 |
+
);
|
113 |
+
|
114 |
+
// Register keyboard events
|
115 |
+
useEffect(() => {
|
116 |
+
if (teleoperationState.isActive) {
|
117 |
+
window.addEventListener("keydown", handleKeyDown);
|
118 |
+
window.addEventListener("keyup", handleKeyUp);
|
119 |
+
|
120 |
+
return () => {
|
121 |
+
window.removeEventListener("keydown", handleKeyDown);
|
122 |
+
window.removeEventListener("keyup", handleKeyUp);
|
123 |
+
};
|
124 |
+
}
|
125 |
+
}, [teleoperationState.isActive, handleKeyDown, handleKeyUp]);
|
126 |
+
|
127 |
+
const handleStart = () => {
|
128 |
+
if (!teleoperationProcessRef.current) {
|
129 |
+
setError("Teleoperation not initialized");
|
130 |
+
return;
|
131 |
+
}
|
132 |
+
|
133 |
+
try {
|
134 |
+
teleoperationProcessRef.current.start();
|
135 |
+
console.log("🎮 Teleoperation started");
|
136 |
+
} catch (error) {
|
137 |
+
const errorMessage =
|
138 |
+
error instanceof Error
|
139 |
+
? error.message
|
140 |
+
: "Failed to start teleoperation";
|
141 |
+
setError(errorMessage);
|
142 |
+
}
|
143 |
};
|
144 |
|
145 |
const handleStop = () => {
|
146 |
+
if (!teleoperationProcessRef.current) return;
|
147 |
+
|
148 |
+
teleoperationProcessRef.current.stop();
|
149 |
+
console.log("🛑 Teleoperation stopped");
|
150 |
};
|
151 |
|
152 |
const handleClose = () => {
|
153 |
+
if (teleoperationProcessRef.current) {
|
154 |
+
teleoperationProcessRef.current.stop();
|
155 |
+
}
|
156 |
onClose();
|
157 |
};
|
158 |
|
159 |
+
const simulateKeyPress = (key: string) => {
|
160 |
+
if (!teleoperationProcessRef.current) return;
|
161 |
+
teleoperationProcessRef.current.updateKeyState(key, true);
|
162 |
+
};
|
163 |
+
|
164 |
+
const simulateKeyRelease = (key: string) => {
|
165 |
+
if (!teleoperationProcessRef.current) return;
|
166 |
+
teleoperationProcessRef.current.updateKeyState(key, false);
|
167 |
+
};
|
168 |
+
|
169 |
+
const moveMotorToPosition = async (motorIndex: number, position: number) => {
|
170 |
+
if (!teleoperationProcessRef.current) return;
|
171 |
+
|
172 |
+
try {
|
173 |
+
const motorName = teleoperationState.motorConfigs[motorIndex]?.name;
|
174 |
+
if (motorName) {
|
175 |
+
await teleoperationProcessRef.current.moveMotor(motorName, position);
|
176 |
+
}
|
177 |
+
} catch (error) {
|
178 |
+
console.warn(
|
179 |
+
`Failed to move motor ${motorIndex + 1} to position ${position}:`,
|
180 |
+
error
|
181 |
+
);
|
182 |
+
}
|
183 |
+
};
|
184 |
+
|
185 |
+
const isConnected = robot?.isConnected || false;
|
186 |
+
const isActive = teleoperationState.isActive;
|
187 |
+
const motorConfigs = teleoperationState.motorConfigs;
|
188 |
+
const keyStates = teleoperationState.keyStates;
|
189 |
+
|
190 |
// Virtual keyboard component
|
191 |
const VirtualKeyboard = () => {
|
192 |
const isKeyPressed = (key: string) => {
|
|
|
205 |
size?: "default" | "sm" | "lg" | "icon";
|
206 |
}) => {
|
207 |
const control =
|
208 |
+
SO100_KEYBOARD_CONTROLS[
|
209 |
+
keyCode as keyof typeof SO100_KEYBOARD_CONTROLS
|
210 |
+
];
|
211 |
const pressed = isKeyPressed(keyCode);
|
212 |
|
213 |
return (
|
src/demo/hooks/useTeleoperation.ts
DELETED
@@ -1,518 +0,0 @@
|
|
1 |
-
import { useState, useEffect, useCallback, useRef } from "react";
|
2 |
-
import { useRobotConnection } from "./useRobotConnection";
|
3 |
-
import { getUnifiedRobotData } from "../lib/unified-storage";
|
4 |
-
import type { RobotConnection } from "../../lerobot/web/find_port.js";
|
5 |
-
|
6 |
-
export interface MotorConfig {
|
7 |
-
name: string;
|
8 |
-
minPosition: number;
|
9 |
-
maxPosition: number;
|
10 |
-
currentPosition: number;
|
11 |
-
homePosition: number;
|
12 |
-
}
|
13 |
-
|
14 |
-
export interface KeyState {
|
15 |
-
pressed: boolean;
|
16 |
-
lastPressed: number;
|
17 |
-
}
|
18 |
-
|
19 |
-
export interface UseTeleoperationOptions {
|
20 |
-
robot: RobotConnection;
|
21 |
-
enabled: boolean;
|
22 |
-
onError?: (error: string) => void;
|
23 |
-
}
|
24 |
-
|
25 |
-
export interface UseTeleoperationResult {
|
26 |
-
// Connection state from singleton
|
27 |
-
isConnected: boolean;
|
28 |
-
isActive: boolean;
|
29 |
-
|
30 |
-
// Motor state
|
31 |
-
motorConfigs: MotorConfig[];
|
32 |
-
|
33 |
-
// Keyboard state
|
34 |
-
keyStates: Record<string, KeyState>;
|
35 |
-
|
36 |
-
// Error state
|
37 |
-
error: string | null;
|
38 |
-
|
39 |
-
// Control methods
|
40 |
-
start: () => void;
|
41 |
-
stop: () => void;
|
42 |
-
goToHome: () => Promise<void>;
|
43 |
-
simulateKeyPress: (key: string) => void;
|
44 |
-
simulateKeyRelease: (key: string) => void;
|
45 |
-
moveMotorToPosition: (motorIndex: number, position: number) => Promise<void>;
|
46 |
-
}
|
47 |
-
|
48 |
-
const MOTOR_CONFIGS: MotorConfig[] = [
|
49 |
-
{
|
50 |
-
name: "shoulder_pan",
|
51 |
-
minPosition: 0,
|
52 |
-
maxPosition: 4095,
|
53 |
-
currentPosition: 2048,
|
54 |
-
homePosition: 2048,
|
55 |
-
},
|
56 |
-
{
|
57 |
-
name: "shoulder_lift",
|
58 |
-
minPosition: 1024,
|
59 |
-
maxPosition: 3072,
|
60 |
-
currentPosition: 2048,
|
61 |
-
homePosition: 2048,
|
62 |
-
},
|
63 |
-
{
|
64 |
-
name: "elbow_flex",
|
65 |
-
minPosition: 1024,
|
66 |
-
maxPosition: 3072,
|
67 |
-
currentPosition: 2048,
|
68 |
-
homePosition: 2048,
|
69 |
-
},
|
70 |
-
{
|
71 |
-
name: "wrist_flex",
|
72 |
-
minPosition: 1024,
|
73 |
-
maxPosition: 3072,
|
74 |
-
currentPosition: 2048,
|
75 |
-
homePosition: 2048,
|
76 |
-
},
|
77 |
-
{
|
78 |
-
name: "wrist_roll",
|
79 |
-
minPosition: 0,
|
80 |
-
maxPosition: 4095,
|
81 |
-
currentPosition: 2048,
|
82 |
-
homePosition: 2048,
|
83 |
-
},
|
84 |
-
{
|
85 |
-
name: "gripper",
|
86 |
-
minPosition: 1800,
|
87 |
-
maxPosition: 2400,
|
88 |
-
currentPosition: 2100,
|
89 |
-
homePosition: 2100,
|
90 |
-
},
|
91 |
-
];
|
92 |
-
|
93 |
-
// PROVEN VALUES from Node.js implementation (conventions.md)
|
94 |
-
const SMOOTH_CONTROL_CONFIG = {
|
95 |
-
STEP_SIZE: 25, // Proven optimal from conventions.md
|
96 |
-
CHANGE_THRESHOLD: 0.5, // Prevents micro-movements and unnecessary commands
|
97 |
-
MOTOR_DELAY: 1, // Minimal delay between motor commands (from conventions.md)
|
98 |
-
UPDATE_INTERVAL: 30, // 30ms = ~33Hz for responsive control (was 50ms = 20Hz)
|
99 |
-
} as const;
|
100 |
-
|
101 |
-
const KEYBOARD_CONTROLS = {
|
102 |
-
ArrowUp: { motorIndex: 1, direction: 1, description: "Shoulder lift up" },
|
103 |
-
ArrowDown: {
|
104 |
-
motorIndex: 1,
|
105 |
-
direction: -1,
|
106 |
-
description: "Shoulder lift down",
|
107 |
-
},
|
108 |
-
ArrowLeft: { motorIndex: 0, direction: -1, description: "Shoulder pan left" },
|
109 |
-
ArrowRight: {
|
110 |
-
motorIndex: 0,
|
111 |
-
direction: 1,
|
112 |
-
description: "Shoulder pan right",
|
113 |
-
},
|
114 |
-
w: { motorIndex: 2, direction: 1, description: "Elbow flex up" },
|
115 |
-
s: { motorIndex: 2, direction: -1, description: "Elbow flex down" },
|
116 |
-
a: { motorIndex: 3, direction: -1, description: "Wrist flex left" },
|
117 |
-
d: { motorIndex: 3, direction: 1, description: "Wrist flex right" },
|
118 |
-
q: { motorIndex: 4, direction: -1, description: "Wrist roll left" },
|
119 |
-
e: { motorIndex: 4, direction: 1, description: "Wrist roll right" },
|
120 |
-
o: { motorIndex: 5, direction: 1, description: "Gripper open" },
|
121 |
-
c: { motorIndex: 5, direction: -1, description: "Gripper close" },
|
122 |
-
Escape: { motorIndex: -1, direction: 0, description: "Emergency stop" },
|
123 |
-
};
|
124 |
-
|
125 |
-
export function useTeleoperation({
|
126 |
-
robot,
|
127 |
-
enabled,
|
128 |
-
onError,
|
129 |
-
}: UseTeleoperationOptions): UseTeleoperationResult {
|
130 |
-
const connection = useRobotConnection();
|
131 |
-
const [isActive, setIsActive] = useState(false);
|
132 |
-
const [motorConfigs, setMotorConfigs] =
|
133 |
-
useState<MotorConfig[]>(MOTOR_CONFIGS);
|
134 |
-
const [keyStates, setKeyStates] = useState<Record<string, KeyState>>({});
|
135 |
-
const [error, setError] = useState<string | null>(null);
|
136 |
-
|
137 |
-
const activeKeysRef = useRef<Set<string>>(new Set());
|
138 |
-
const motorPositionsRef = useRef<number[]>(
|
139 |
-
MOTOR_CONFIGS.map((m) => m.homePosition)
|
140 |
-
);
|
141 |
-
const movementIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
142 |
-
|
143 |
-
// Load calibration data
|
144 |
-
useEffect(() => {
|
145 |
-
const loadCalibration = async () => {
|
146 |
-
try {
|
147 |
-
if (!robot.serialNumber) {
|
148 |
-
console.warn("No serial number available for calibration loading");
|
149 |
-
return;
|
150 |
-
}
|
151 |
-
|
152 |
-
const data = getUnifiedRobotData(robot.serialNumber);
|
153 |
-
if (data?.calibration) {
|
154 |
-
// Map motor names to calibration data
|
155 |
-
const motorNames = [
|
156 |
-
"shoulder_pan",
|
157 |
-
"shoulder_lift",
|
158 |
-
"elbow_flex",
|
159 |
-
"wrist_flex",
|
160 |
-
"wrist_roll",
|
161 |
-
"gripper",
|
162 |
-
];
|
163 |
-
const calibratedConfigs = MOTOR_CONFIGS.map((config, index) => {
|
164 |
-
const motorName = motorNames[index] as keyof NonNullable<
|
165 |
-
typeof data.calibration
|
166 |
-
>;
|
167 |
-
const calibratedMotor = data.calibration![motorName];
|
168 |
-
if (
|
169 |
-
calibratedMotor &&
|
170 |
-
typeof calibratedMotor === "object" &&
|
171 |
-
"homing_offset" in calibratedMotor &&
|
172 |
-
"range_min" in calibratedMotor &&
|
173 |
-
"range_max" in calibratedMotor
|
174 |
-
) {
|
175 |
-
// Use 2048 as default home position, adjusted by homing offset
|
176 |
-
const homePosition = 2048 + (calibratedMotor.homing_offset || 0);
|
177 |
-
return {
|
178 |
-
...config,
|
179 |
-
homePosition,
|
180 |
-
currentPosition: homePosition,
|
181 |
-
// IMPORTANT: Use actual calibrated limits instead of hardcoded ones
|
182 |
-
minPosition: calibratedMotor.range_min || config.minPosition,
|
183 |
-
maxPosition: calibratedMotor.range_max || config.maxPosition,
|
184 |
-
};
|
185 |
-
}
|
186 |
-
return config;
|
187 |
-
});
|
188 |
-
setMotorConfigs(calibratedConfigs);
|
189 |
-
// DON'T set motorPositionsRef here - it will be set when teleoperation starts
|
190 |
-
// motorPositionsRef.current = calibratedConfigs.map((m) => m.homePosition);
|
191 |
-
console.log("✅ Loaded calibration data for", robot.serialNumber);
|
192 |
-
}
|
193 |
-
} catch (error) {
|
194 |
-
console.warn("Failed to load calibration:", error);
|
195 |
-
}
|
196 |
-
};
|
197 |
-
|
198 |
-
loadCalibration();
|
199 |
-
}, [robot.serialNumber]);
|
200 |
-
|
201 |
-
// Keyboard event handlers
|
202 |
-
const handleKeyDown = useCallback(
|
203 |
-
(event: KeyboardEvent) => {
|
204 |
-
if (!isActive) return;
|
205 |
-
|
206 |
-
const key = event.key;
|
207 |
-
if (key in KEYBOARD_CONTROLS) {
|
208 |
-
event.preventDefault();
|
209 |
-
|
210 |
-
if (key === "Escape") {
|
211 |
-
setIsActive(false);
|
212 |
-
activeKeysRef.current.clear();
|
213 |
-
return;
|
214 |
-
}
|
215 |
-
|
216 |
-
if (!activeKeysRef.current.has(key)) {
|
217 |
-
activeKeysRef.current.add(key);
|
218 |
-
setKeyStates((prev) => ({
|
219 |
-
...prev,
|
220 |
-
[key]: { pressed: true, lastPressed: Date.now() },
|
221 |
-
}));
|
222 |
-
}
|
223 |
-
}
|
224 |
-
},
|
225 |
-
[isActive]
|
226 |
-
);
|
227 |
-
|
228 |
-
const handleKeyUp = useCallback(
|
229 |
-
(event: KeyboardEvent) => {
|
230 |
-
if (!isActive) return;
|
231 |
-
|
232 |
-
const key = event.key;
|
233 |
-
if (key in KEYBOARD_CONTROLS) {
|
234 |
-
event.preventDefault();
|
235 |
-
activeKeysRef.current.delete(key);
|
236 |
-
setKeyStates((prev) => ({
|
237 |
-
...prev,
|
238 |
-
[key]: { pressed: false, lastPressed: Date.now() },
|
239 |
-
}));
|
240 |
-
}
|
241 |
-
},
|
242 |
-
[isActive]
|
243 |
-
);
|
244 |
-
|
245 |
-
// Register keyboard events
|
246 |
-
useEffect(() => {
|
247 |
-
if (enabled && isActive) {
|
248 |
-
window.addEventListener("keydown", handleKeyDown);
|
249 |
-
window.addEventListener("keyup", handleKeyUp);
|
250 |
-
|
251 |
-
return () => {
|
252 |
-
window.removeEventListener("keydown", handleKeyDown);
|
253 |
-
window.removeEventListener("keyup", handleKeyUp);
|
254 |
-
};
|
255 |
-
}
|
256 |
-
}, [enabled, isActive, handleKeyDown, handleKeyUp]);
|
257 |
-
|
258 |
-
// CONTINUOUS MOVEMENT: For held keys with PROVEN smooth patterns from Node.js
|
259 |
-
useEffect(() => {
|
260 |
-
if (!isActive || !connection.isConnected) {
|
261 |
-
if (movementIntervalRef.current) {
|
262 |
-
clearInterval(movementIntervalRef.current);
|
263 |
-
movementIntervalRef.current = null;
|
264 |
-
}
|
265 |
-
return;
|
266 |
-
}
|
267 |
-
|
268 |
-
const processMovement = async () => {
|
269 |
-
if (activeKeysRef.current.size === 0) return;
|
270 |
-
|
271 |
-
const activeKeys = Array.from(activeKeysRef.current);
|
272 |
-
const changedMotors: Array<{ index: number; position: number }> = [];
|
273 |
-
|
274 |
-
// PROVEN PATTERN: Process all active keys and collect changes
|
275 |
-
for (const key of activeKeys) {
|
276 |
-
const control =
|
277 |
-
KEYBOARD_CONTROLS[key as keyof typeof KEYBOARD_CONTROLS];
|
278 |
-
if (control && control.motorIndex >= 0) {
|
279 |
-
const motorIndex = control.motorIndex;
|
280 |
-
const direction = control.direction;
|
281 |
-
const motor = motorConfigs[motorIndex];
|
282 |
-
|
283 |
-
if (motor) {
|
284 |
-
const currentPos = motorPositionsRef.current[motorIndex];
|
285 |
-
let newPos =
|
286 |
-
currentPos + direction * SMOOTH_CONTROL_CONFIG.STEP_SIZE;
|
287 |
-
|
288 |
-
// Clamp to motor limits
|
289 |
-
newPos = Math.max(
|
290 |
-
motor.minPosition,
|
291 |
-
Math.min(motor.maxPosition, newPos)
|
292 |
-
);
|
293 |
-
|
294 |
-
// PROVEN PATTERN: Only update if change is meaningful (0.5 unit threshold)
|
295 |
-
if (
|
296 |
-
Math.abs(newPos - currentPos) >
|
297 |
-
SMOOTH_CONTROL_CONFIG.CHANGE_THRESHOLD
|
298 |
-
) {
|
299 |
-
motorPositionsRef.current[motorIndex] = newPos;
|
300 |
-
changedMotors.push({ index: motorIndex, position: newPos });
|
301 |
-
}
|
302 |
-
}
|
303 |
-
}
|
304 |
-
}
|
305 |
-
|
306 |
-
// PROVEN PATTERN: Only send commands for motors that actually changed
|
307 |
-
if (changedMotors.length > 0) {
|
308 |
-
try {
|
309 |
-
for (const { index, position } of changedMotors) {
|
310 |
-
await connection.writeMotorPosition(index + 1, position);
|
311 |
-
|
312 |
-
// PROVEN PATTERN: Minimal delay between motor commands (1ms)
|
313 |
-
if (changedMotors.length > 1) {
|
314 |
-
await new Promise((resolve) =>
|
315 |
-
setTimeout(resolve, SMOOTH_CONTROL_CONFIG.MOTOR_DELAY)
|
316 |
-
);
|
317 |
-
}
|
318 |
-
}
|
319 |
-
|
320 |
-
// Update UI to reflect changes
|
321 |
-
setMotorConfigs((prev) =>
|
322 |
-
prev.map((config, index) => ({
|
323 |
-
...config,
|
324 |
-
currentPosition: motorPositionsRef.current[index],
|
325 |
-
}))
|
326 |
-
);
|
327 |
-
} catch (error) {
|
328 |
-
console.warn("Failed to update robot positions:", error);
|
329 |
-
}
|
330 |
-
}
|
331 |
-
};
|
332 |
-
|
333 |
-
// PROVEN TIMING: 30ms interval (~33Hz) for responsive continuous movement
|
334 |
-
movementIntervalRef.current = setInterval(
|
335 |
-
processMovement,
|
336 |
-
SMOOTH_CONTROL_CONFIG.UPDATE_INTERVAL
|
337 |
-
);
|
338 |
-
|
339 |
-
return () => {
|
340 |
-
if (movementIntervalRef.current) {
|
341 |
-
clearInterval(movementIntervalRef.current);
|
342 |
-
movementIntervalRef.current = null;
|
343 |
-
}
|
344 |
-
};
|
345 |
-
}, [
|
346 |
-
isActive,
|
347 |
-
connection.isConnected,
|
348 |
-
connection.writeMotorPosition,
|
349 |
-
motorConfigs,
|
350 |
-
]);
|
351 |
-
|
352 |
-
// Control methods
|
353 |
-
const start = useCallback(async () => {
|
354 |
-
if (!connection.isConnected) {
|
355 |
-
setError("Robot not connected");
|
356 |
-
onError?.("Robot not connected");
|
357 |
-
return;
|
358 |
-
}
|
359 |
-
|
360 |
-
try {
|
361 |
-
console.log(
|
362 |
-
"🎮 Starting teleoperation - reading current motor positions..."
|
363 |
-
);
|
364 |
-
|
365 |
-
// Read current positions of all motors using PROVEN utility
|
366 |
-
const motorIds = [1, 2, 3, 4, 5, 6];
|
367 |
-
const currentPositions = await connection.readAllMotorPositions(motorIds);
|
368 |
-
|
369 |
-
// Log all positions (trust the utility's fallback handling)
|
370 |
-
for (let i = 0; i < currentPositions.length; i++) {
|
371 |
-
const position = currentPositions[i];
|
372 |
-
console.log(`📍 Motor ${i + 1} current position: ${position}`);
|
373 |
-
}
|
374 |
-
|
375 |
-
// CRITICAL: Update positions BEFORE activating movement
|
376 |
-
motorPositionsRef.current = currentPositions;
|
377 |
-
|
378 |
-
// Update UI to show actual current positions
|
379 |
-
setMotorConfigs((prev) =>
|
380 |
-
prev.map((config, index) => ({
|
381 |
-
...config,
|
382 |
-
currentPosition: currentPositions[index],
|
383 |
-
}))
|
384 |
-
);
|
385 |
-
|
386 |
-
// IMPORTANT: Only activate AFTER positions are synchronized
|
387 |
-
setIsActive(true);
|
388 |
-
setError(null);
|
389 |
-
console.log(
|
390 |
-
"✅ Teleoperation started with synchronized positions:",
|
391 |
-
currentPositions
|
392 |
-
);
|
393 |
-
} catch (error) {
|
394 |
-
const errorMessage =
|
395 |
-
error instanceof Error
|
396 |
-
? error.message
|
397 |
-
: "Failed to start teleoperation";
|
398 |
-
setError(errorMessage);
|
399 |
-
onError?.(errorMessage);
|
400 |
-
console.error("❌ Failed to start teleoperation:", error);
|
401 |
-
}
|
402 |
-
}, [
|
403 |
-
connection.isConnected,
|
404 |
-
connection.readAllMotorPositions,
|
405 |
-
motorConfigs,
|
406 |
-
onError,
|
407 |
-
]);
|
408 |
-
|
409 |
-
const stop = useCallback(() => {
|
410 |
-
setIsActive(false);
|
411 |
-
activeKeysRef.current.clear();
|
412 |
-
setKeyStates({});
|
413 |
-
console.log("🛑 Teleoperation stopped");
|
414 |
-
}, []);
|
415 |
-
|
416 |
-
const goToHome = useCallback(async () => {
|
417 |
-
if (!connection.isConnected) {
|
418 |
-
setError("Robot not connected");
|
419 |
-
return;
|
420 |
-
}
|
421 |
-
|
422 |
-
try {
|
423 |
-
for (let i = 0; i < motorConfigs.length; i++) {
|
424 |
-
const motor = motorConfigs[i];
|
425 |
-
await connection.writeMotorPosition(i + 1, motor.homePosition);
|
426 |
-
motorPositionsRef.current[i] = motor.homePosition;
|
427 |
-
}
|
428 |
-
|
429 |
-
setMotorConfigs((prev) =>
|
430 |
-
prev.map((config) => ({
|
431 |
-
...config,
|
432 |
-
currentPosition: config.homePosition,
|
433 |
-
}))
|
434 |
-
);
|
435 |
-
|
436 |
-
console.log("🏠 Moved to home position");
|
437 |
-
} catch (error) {
|
438 |
-
const errorMessage =
|
439 |
-
error instanceof Error ? error.message : "Failed to go to home";
|
440 |
-
setError(errorMessage);
|
441 |
-
onError?.(errorMessage);
|
442 |
-
}
|
443 |
-
}, [
|
444 |
-
connection.isConnected,
|
445 |
-
connection.writeMotorPosition,
|
446 |
-
motorConfigs,
|
447 |
-
onError,
|
448 |
-
]);
|
449 |
-
|
450 |
-
const simulateKeyPress = useCallback(
|
451 |
-
(key: string) => {
|
452 |
-
if (!isActive) return;
|
453 |
-
|
454 |
-
activeKeysRef.current.add(key);
|
455 |
-
setKeyStates((prev) => ({
|
456 |
-
...prev,
|
457 |
-
[key]: { pressed: true, lastPressed: Date.now() },
|
458 |
-
}));
|
459 |
-
},
|
460 |
-
[isActive]
|
461 |
-
);
|
462 |
-
|
463 |
-
const simulateKeyRelease = useCallback(
|
464 |
-
(key: string) => {
|
465 |
-
if (!isActive) return;
|
466 |
-
|
467 |
-
activeKeysRef.current.delete(key);
|
468 |
-
setKeyStates((prev) => ({
|
469 |
-
...prev,
|
470 |
-
[key]: { pressed: false, lastPressed: Date.now() },
|
471 |
-
}));
|
472 |
-
},
|
473 |
-
[isActive]
|
474 |
-
);
|
475 |
-
|
476 |
-
const moveMotorToPosition = useCallback(
|
477 |
-
async (motorIndex: number, position: number) => {
|
478 |
-
if (!connection.isConnected) {
|
479 |
-
return;
|
480 |
-
}
|
481 |
-
|
482 |
-
try {
|
483 |
-
await connection.writeMotorPosition(motorIndex + 1, position);
|
484 |
-
|
485 |
-
// Update internal state
|
486 |
-
motorPositionsRef.current[motorIndex] = position;
|
487 |
-
|
488 |
-
setMotorConfigs((prev) =>
|
489 |
-
prev.map((config, index) => ({
|
490 |
-
...config,
|
491 |
-
currentPosition:
|
492 |
-
index === motorIndex ? position : config.currentPosition,
|
493 |
-
}))
|
494 |
-
);
|
495 |
-
} catch (error) {
|
496 |
-
console.warn(
|
497 |
-
`Failed to move motor ${motorIndex + 1} to position ${position}:`,
|
498 |
-
error
|
499 |
-
);
|
500 |
-
}
|
501 |
-
},
|
502 |
-
[connection.isConnected, connection.writeMotorPosition]
|
503 |
-
);
|
504 |
-
|
505 |
-
return {
|
506 |
-
isConnected: connection.isConnected,
|
507 |
-
isActive,
|
508 |
-
motorConfigs,
|
509 |
-
keyStates,
|
510 |
-
error,
|
511 |
-
start,
|
512 |
-
stop,
|
513 |
-
goToHome,
|
514 |
-
simulateKeyPress,
|
515 |
-
simulateKeyRelease,
|
516 |
-
moveMotorToPosition,
|
517 |
-
};
|
518 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/pages/Home.tsx
CHANGED
@@ -12,7 +12,7 @@ import { PortManager } from "../components/PortManager";
|
|
12 |
import { CalibrationPanel } from "../components/CalibrationPanel";
|
13 |
import { TeleoperationPanel } from "../components/TeleoperationPanel";
|
14 |
import { isWebSerialSupported } from "../../lerobot/web/calibrate";
|
15 |
-
import type { RobotConnection } from "../../lerobot/web/
|
16 |
|
17 |
interface HomeProps {
|
18 |
onGetStarted: () => void;
|
|
|
12 |
import { CalibrationPanel } from "../components/CalibrationPanel";
|
13 |
import { TeleoperationPanel } from "../components/TeleoperationPanel";
|
14 |
import { isWebSerialSupported } from "../../lerobot/web/calibrate";
|
15 |
+
import type { RobotConnection } from "../../lerobot/web/types/robot-connection.js";
|
16 |
|
17 |
interface HomeProps {
|
18 |
onGetStarted: () => void;
|
src/lerobot/web/calibrate.ts
CHANGED
@@ -1,14 +1,10 @@
|
|
1 |
/**
|
2 |
* Web calibration functionality using Web Serial API
|
3 |
-
*
|
4 |
*
|
5 |
-
*
|
6 |
-
* 1. Creating robot-specific config files in ./robots/
|
7 |
-
* 2. Extending the calibrate() function to accept different robot types
|
8 |
-
* 3. Adding robot-specific protocol configurations
|
9 |
*/
|
10 |
|
11 |
-
import { createSO100Config } from "./robots/so100_config.js";
|
12 |
import { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js";
|
13 |
import {
|
14 |
readAllMotorPositions,
|
@@ -19,33 +15,11 @@ import {
|
|
19 |
setHomingOffsets,
|
20 |
writeHardwarePositionLimits,
|
21 |
} from "./utils/motor-calibration.js";
|
22 |
-
import
|
23 |
-
|
24 |
-
/**
|
25 |
-
* Device calibration configuration interface
|
26 |
-
* Currently designed for SO-100, but can be extended for other robot types
|
27 |
-
*/
|
28 |
-
interface WebCalibrationConfig {
|
29 |
-
deviceType: "so100_follower" | "so100_leader";
|
30 |
-
port: WebSerialPortWrapper;
|
31 |
-
motorNames: string[];
|
32 |
-
motorIds: number[];
|
33 |
-
driveModes: number[];
|
34 |
|
35 |
-
|
36 |
-
|
37 |
-
resolution: number;
|
38 |
-
homingOffsetAddress: number;
|
39 |
-
homingOffsetLength: number;
|
40 |
-
presentPositionAddress: number;
|
41 |
-
presentPositionLength: number;
|
42 |
-
minPositionLimitAddress: number;
|
43 |
-
minPositionLimitLength: number;
|
44 |
-
maxPositionLimitAddress: number;
|
45 |
-
maxPositionLimitLength: number;
|
46 |
-
signMagnitudeBit: number;
|
47 |
-
};
|
48 |
-
}
|
49 |
|
50 |
/**
|
51 |
* Calibration results structure matching Python lerobot format exactly
|
@@ -149,25 +123,33 @@ async function recordRangesOfMotion(
|
|
149 |
}
|
150 |
|
151 |
/**
|
152 |
-
*
|
|
|
153 |
*/
|
154 |
-
function
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
|
|
164 |
}
|
165 |
|
166 |
/**
|
167 |
-
* Main calibrate function -
|
168 |
-
* Currently supports SO-100 robots (follower and leader)
|
169 |
-
*
|
170 |
-
* Takes a unified RobotConnection object from findPort()
|
171 |
*/
|
172 |
export async function calibrate(
|
173 |
robotConnection: RobotConnection,
|
@@ -187,8 +169,13 @@ export async function calibrate(
|
|
187 |
const port = new WebSerialPortWrapper(robotConnection.port);
|
188 |
await port.initialize();
|
189 |
|
190 |
-
// Get
|
191 |
-
|
|
|
|
|
|
|
|
|
|
|
192 |
|
193 |
let shouldStop = false;
|
194 |
const stopFunction = () => shouldStop;
|
@@ -198,30 +185,31 @@ export async function calibrate(
|
|
198 |
// Step 1: Set homing offsets (automatic)
|
199 |
options?.onProgress?.("⚙️ Setting motor homing offsets");
|
200 |
const homingOffsets = await setHomingOffsets(
|
201 |
-
|
202 |
config.motorIds,
|
203 |
config.motorNames
|
204 |
);
|
205 |
|
206 |
// Step 2: Record ranges of motion with live updates
|
207 |
const { rangeMins, rangeMaxes } = await recordRangesOfMotion(
|
208 |
-
|
209 |
config.motorIds,
|
210 |
config.motorNames,
|
211 |
stopFunction,
|
212 |
options?.onLiveUpdate
|
213 |
);
|
214 |
|
215 |
-
// Step 3:
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
|
|
221 |
|
222 |
// Step 4: Write hardware position limits to motors
|
223 |
await writeHardwarePositionLimits(
|
224 |
-
|
225 |
config.motorIds,
|
226 |
config.motorNames,
|
227 |
rangeMins,
|
|
|
1 |
/**
|
2 |
* Web calibration functionality using Web Serial API
|
3 |
+
* Simple library API - pass in robotConnection, get calibration results
|
4 |
*
|
5 |
+
* Handles different robot types internally - users don't need to know about configs
|
|
|
|
|
|
|
6 |
*/
|
7 |
|
|
|
8 |
import { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js";
|
9 |
import {
|
10 |
readAllMotorPositions,
|
|
|
15 |
setHomingOffsets,
|
16 |
writeHardwarePositionLimits,
|
17 |
} from "./utils/motor-calibration.js";
|
18 |
+
import { createSO100Config } from "./robots/so100_config.js";
|
19 |
+
import type { RobotConnection } from "./types/robot-connection.js";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
+
// Import shared robot hardware configuration interface
|
22 |
+
import type { RobotHardwareConfig } from "./types/robot-config.js";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
/**
|
25 |
* Calibration results structure matching Python lerobot format exactly
|
|
|
123 |
}
|
124 |
|
125 |
/**
|
126 |
+
* Apply robot-specific range adjustments
|
127 |
+
* Different robot types may have special cases (like continuous rotation motors)
|
128 |
*/
|
129 |
+
function applyRobotSpecificRangeAdjustments(
|
130 |
+
robotType: string,
|
131 |
+
protocol: { resolution: number },
|
132 |
+
rangeMins: { [motor: string]: number },
|
133 |
+
rangeMaxes: { [motor: string]: number }
|
134 |
+
): void {
|
135 |
+
// SO-100 specific: wrist_roll is a continuous rotation motor
|
136 |
+
if (robotType.startsWith("so100") && rangeMins["wrist_roll"] !== undefined) {
|
137 |
+
// The wrist_roll is a continuous rotation motor that should use the full
|
138 |
+
// 0-4095 range regardless of what the user recorded during calibration.
|
139 |
+
// This matches the hardware specification and Python lerobot behavior.
|
140 |
+
rangeMins["wrist_roll"] = 0;
|
141 |
+
rangeMaxes["wrist_roll"] = protocol.resolution - 1;
|
142 |
+
}
|
143 |
|
144 |
+
// Future robot types can add their own specific adjustments here
|
145 |
+
// if (robotType.startsWith('newrobot') && rangeMins["special_joint"] !== undefined) {
|
146 |
+
// rangeMins["special_joint"] = 0;
|
147 |
+
// rangeMaxes["special_joint"] = 2048;
|
148 |
+
// }
|
149 |
}
|
150 |
|
151 |
/**
|
152 |
+
* Main calibrate function - simple API, handles robot types internally
|
|
|
|
|
|
|
153 |
*/
|
154 |
export async function calibrate(
|
155 |
robotConnection: RobotConnection,
|
|
|
169 |
const port = new WebSerialPortWrapper(robotConnection.port);
|
170 |
await port.initialize();
|
171 |
|
172 |
+
// Get robot-specific configuration (extensible - add new robot types here)
|
173 |
+
let config: RobotHardwareConfig;
|
174 |
+
if (robotConnection.robotType.startsWith("so100")) {
|
175 |
+
config = createSO100Config(robotConnection.robotType);
|
176 |
+
} else {
|
177 |
+
throw new Error(`Unsupported robot type: ${robotConnection.robotType}`);
|
178 |
+
}
|
179 |
|
180 |
let shouldStop = false;
|
181 |
const stopFunction = () => shouldStop;
|
|
|
185 |
// Step 1: Set homing offsets (automatic)
|
186 |
options?.onProgress?.("⚙️ Setting motor homing offsets");
|
187 |
const homingOffsets = await setHomingOffsets(
|
188 |
+
port,
|
189 |
config.motorIds,
|
190 |
config.motorNames
|
191 |
);
|
192 |
|
193 |
// Step 2: Record ranges of motion with live updates
|
194 |
const { rangeMins, rangeMaxes } = await recordRangesOfMotion(
|
195 |
+
port,
|
196 |
config.motorIds,
|
197 |
config.motorNames,
|
198 |
stopFunction,
|
199 |
options?.onLiveUpdate
|
200 |
);
|
201 |
|
202 |
+
// Step 3: Apply robot-specific range adjustments
|
203 |
+
applyRobotSpecificRangeAdjustments(
|
204 |
+
robotConnection.robotType!,
|
205 |
+
config.protocol,
|
206 |
+
rangeMins,
|
207 |
+
rangeMaxes
|
208 |
+
);
|
209 |
|
210 |
// Step 4: Write hardware position limits to motors
|
211 |
await writeHardwarePositionLimits(
|
212 |
+
port,
|
213 |
config.motorIds,
|
214 |
config.motorNames,
|
215 |
rangeMins,
|
src/lerobot/web/find_port.ts
CHANGED
@@ -29,30 +29,15 @@
|
|
29 |
*/
|
30 |
|
31 |
import { getRobotConnectionManager } from "./robot-connection.js";
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
/**
|
34 |
-
*
|
35 |
*/
|
36 |
-
interface SerialPort {
|
37 |
-
readonly readable: ReadableStream;
|
38 |
-
readonly writable: WritableStream;
|
39 |
-
getInfo(): SerialPortInfo;
|
40 |
-
open(options: SerialOptions): Promise<void>;
|
41 |
-
close(): Promise<void>;
|
42 |
-
}
|
43 |
-
|
44 |
-
interface SerialPortInfo {
|
45 |
-
usbVendorId?: number;
|
46 |
-
usbProductId?: number;
|
47 |
-
}
|
48 |
-
|
49 |
-
interface SerialOptions {
|
50 |
-
baudRate: number;
|
51 |
-
dataBits?: number;
|
52 |
-
stopBits?: number;
|
53 |
-
parity?: "none" | "even" | "odd";
|
54 |
-
}
|
55 |
-
|
56 |
interface Serial extends EventTarget {
|
57 |
getPorts(): Promise<SerialPort[]>;
|
58 |
requestPort(options?: SerialPortRequestOptions): Promise<SerialPort>;
|
@@ -73,43 +58,6 @@ declare global {
|
|
73 |
}
|
74 |
}
|
75 |
|
76 |
-
/**
|
77 |
-
* Unified robot connection interface used across all functions
|
78 |
-
* This same object works for findPort, calibrate, teleoperate, etc.
|
79 |
-
* Includes all fields needed by demo and other applications
|
80 |
-
*/
|
81 |
-
export interface RobotConnection {
|
82 |
-
port: SerialPort;
|
83 |
-
name: string; // Display name for UI
|
84 |
-
isConnected: boolean; // Connection status
|
85 |
-
robotType?: "so100_follower" | "so100_leader"; // Optional until user configures
|
86 |
-
robotId?: string; // Optional until user configures
|
87 |
-
serialNumber: string; // Always required for identification
|
88 |
-
error?: string; // Error message if connection failed
|
89 |
-
usbMetadata?: {
|
90 |
-
// USB device information
|
91 |
-
vendorId: string;
|
92 |
-
productId: string;
|
93 |
-
serialNumber: string;
|
94 |
-
manufacturerName: string;
|
95 |
-
productName: string;
|
96 |
-
usbVersionMajor?: number;
|
97 |
-
usbVersionMinor?: number;
|
98 |
-
deviceClass?: number;
|
99 |
-
deviceSubclass?: number;
|
100 |
-
deviceProtocol?: number;
|
101 |
-
};
|
102 |
-
}
|
103 |
-
|
104 |
-
/**
|
105 |
-
* Minimal robot config for finding/connecting to specific robots
|
106 |
-
*/
|
107 |
-
export interface RobotConfig {
|
108 |
-
robotType: "so100_follower" | "so100_leader";
|
109 |
-
robotId: string;
|
110 |
-
serialNumber: string;
|
111 |
-
}
|
112 |
-
|
113 |
/**
|
114 |
* Options for findPort function
|
115 |
*/
|
@@ -366,6 +314,3 @@ export async function findPort(
|
|
366 |
},
|
367 |
};
|
368 |
}
|
369 |
-
|
370 |
-
// Export the main function (renamed from findPortWeb)
|
371 |
-
export { findPort as findPortWeb }; // Backward compatibility alias
|
|
|
29 |
*/
|
30 |
|
31 |
import { getRobotConnectionManager } from "./robot-connection.js";
|
32 |
+
import type {
|
33 |
+
RobotConnection,
|
34 |
+
RobotConfig,
|
35 |
+
SerialPort,
|
36 |
+
} from "./types/robot-connection.js";
|
37 |
|
38 |
/**
|
39 |
+
* Extended WebSerial API type definitions
|
40 |
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
interface Serial extends EventTarget {
|
42 |
getPorts(): Promise<SerialPort[]>;
|
43 |
requestPort(options?: SerialPortRequestOptions): Promise<SerialPort>;
|
|
|
58 |
}
|
59 |
}
|
60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
/**
|
62 |
* Options for findPort function
|
63 |
*/
|
|
|
314 |
},
|
315 |
};
|
316 |
}
|
|
|
|
|
|
src/lerobot/web/robots/so100_config.ts
CHANGED
@@ -1,8 +1,10 @@
|
|
1 |
/**
|
2 |
-
* SO-100 specific configuration
|
3 |
* Matches Node.js SO-100 config structure and Python lerobot exactly
|
4 |
*/
|
5 |
|
|
|
|
|
6 |
/**
|
7 |
* STS3215 Protocol Configuration for SO-100 devices
|
8 |
*/
|
@@ -38,16 +40,60 @@ export const SO100_CONFIG = {
|
|
38 |
};
|
39 |
|
40 |
/**
|
41 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
*/
|
43 |
export function createSO100Config(
|
44 |
deviceType: "so100_follower" | "so100_leader"
|
45 |
-
) {
|
46 |
return {
|
47 |
deviceType,
|
48 |
motorNames: SO100_CONFIG.motorNames,
|
49 |
motorIds: SO100_CONFIG.motorIds,
|
50 |
driveModes: SO100_CONFIG.driveModes,
|
|
|
51 |
protocol: WEB_STS3215_PROTOCOL,
|
52 |
};
|
53 |
}
|
|
|
1 |
/**
|
2 |
+
* SO-100 specific hardware configuration
|
3 |
* Matches Node.js SO-100 config structure and Python lerobot exactly
|
4 |
*/
|
5 |
|
6 |
+
import type { RobotHardwareConfig } from "../types/robot-config.js";
|
7 |
+
|
8 |
/**
|
9 |
* STS3215 Protocol Configuration for SO-100 devices
|
10 |
*/
|
|
|
40 |
};
|
41 |
|
42 |
/**
|
43 |
+
* SO-100 Keyboard Controls for Teleoperation
|
44 |
+
* Robot-specific mapping optimized for SO-100 joint layout
|
45 |
+
*/
|
46 |
+
export const SO100_KEYBOARD_CONTROLS = {
|
47 |
+
// Shoulder controls
|
48 |
+
ArrowUp: { motor: "shoulder_lift", direction: 1, description: "Shoulder up" },
|
49 |
+
ArrowDown: {
|
50 |
+
motor: "shoulder_lift",
|
51 |
+
direction: -1,
|
52 |
+
description: "Shoulder down",
|
53 |
+
},
|
54 |
+
ArrowLeft: {
|
55 |
+
motor: "shoulder_pan",
|
56 |
+
direction: -1,
|
57 |
+
description: "Shoulder left",
|
58 |
+
},
|
59 |
+
ArrowRight: {
|
60 |
+
motor: "shoulder_pan",
|
61 |
+
direction: 1,
|
62 |
+
description: "Shoulder right",
|
63 |
+
},
|
64 |
+
|
65 |
+
// WASD controls
|
66 |
+
w: { motor: "elbow_flex", direction: 1, description: "Elbow flex" },
|
67 |
+
s: { motor: "elbow_flex", direction: -1, description: "Elbow extend" },
|
68 |
+
a: { motor: "wrist_flex", direction: -1, description: "Wrist down" },
|
69 |
+
d: { motor: "wrist_flex", direction: 1, description: "Wrist up" },
|
70 |
+
|
71 |
+
// Wrist roll and gripper
|
72 |
+
q: { motor: "wrist_roll", direction: -1, description: "Wrist roll left" },
|
73 |
+
e: { motor: "wrist_roll", direction: 1, description: "Wrist roll right" },
|
74 |
+
o: { motor: "gripper", direction: 1, description: "Gripper open" },
|
75 |
+
c: { motor: "gripper", direction: -1, description: "Gripper close" },
|
76 |
+
|
77 |
+
// Emergency stop
|
78 |
+
Escape: {
|
79 |
+
motor: "emergency_stop",
|
80 |
+
direction: 0,
|
81 |
+
description: "Emergency stop",
|
82 |
+
},
|
83 |
+
} as const;
|
84 |
+
|
85 |
+
/**
|
86 |
+
* Create SO-100 hardware configuration
|
87 |
*/
|
88 |
export function createSO100Config(
|
89 |
deviceType: "so100_follower" | "so100_leader"
|
90 |
+
): RobotHardwareConfig {
|
91 |
return {
|
92 |
deviceType,
|
93 |
motorNames: SO100_CONFIG.motorNames,
|
94 |
motorIds: SO100_CONFIG.motorIds,
|
95 |
driveModes: SO100_CONFIG.driveModes,
|
96 |
+
keyboardControls: SO100_KEYBOARD_CONTROLS,
|
97 |
protocol: WEB_STS3215_PROTOCOL,
|
98 |
};
|
99 |
}
|
src/lerobot/web/teleoperate.ts
CHANGED
@@ -3,7 +3,18 @@
|
|
3 |
* Mirrors the Node.js implementation but adapted for browser environment
|
4 |
*/
|
5 |
|
6 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
/**
|
9 |
* Motor position and limits for teleoperation
|
@@ -14,7 +25,6 @@ export interface MotorConfig {
|
|
14 |
currentPosition: number;
|
15 |
minPosition: number;
|
16 |
maxPosition: number;
|
17 |
-
homePosition: number;
|
18 |
}
|
19 |
|
20 |
/**
|
@@ -28,328 +38,103 @@ export interface TeleoperationState {
|
|
28 |
}
|
29 |
|
30 |
/**
|
31 |
-
*
|
32 |
*/
|
33 |
-
export
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
description: "Shoulder left",
|
45 |
-
},
|
46 |
-
ArrowRight: {
|
47 |
-
motor: "shoulder_pan",
|
48 |
-
direction: 1,
|
49 |
-
description: "Shoulder right",
|
50 |
-
},
|
51 |
-
|
52 |
-
// WASD controls
|
53 |
-
w: { motor: "elbow_flex", direction: 1, description: "Elbow flex" },
|
54 |
-
s: { motor: "elbow_flex", direction: -1, description: "Elbow extend" },
|
55 |
-
a: { motor: "wrist_flex", direction: -1, description: "Wrist down" },
|
56 |
-
d: { motor: "wrist_flex", direction: 1, description: "Wrist up" },
|
57 |
-
|
58 |
-
// Wrist roll and gripper
|
59 |
-
q: { motor: "wrist_roll", direction: -1, description: "Wrist roll left" },
|
60 |
-
e: { motor: "wrist_roll", direction: 1, description: "Wrist roll right" },
|
61 |
-
o: { motor: "gripper", direction: 1, description: "Gripper open" },
|
62 |
-
c: { motor: "gripper", direction: -1, description: "Gripper close" },
|
63 |
-
|
64 |
-
// Emergency stop
|
65 |
-
Escape: {
|
66 |
-
motor: "emergency_stop",
|
67 |
-
direction: 0,
|
68 |
-
description: "Emergency stop",
|
69 |
-
},
|
70 |
-
} as const;
|
71 |
|
72 |
/**
|
73 |
-
*
|
74 |
-
*
|
75 |
*/
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
this.port.readable !== null &&
|
87 |
-
this.port.writable !== null
|
88 |
-
);
|
89 |
-
}
|
90 |
-
|
91 |
-
async initialize(): Promise<void> {
|
92 |
-
if (!this.port.readable || !this.port.writable) {
|
93 |
-
throw new Error("Port is not open for teleoperation");
|
94 |
-
}
|
95 |
-
// Port is already open and ready - no need to grab persistent readers/writers
|
96 |
-
}
|
97 |
-
|
98 |
-
async writeMotorPosition(
|
99 |
-
motorId: number,
|
100 |
-
position: number
|
101 |
-
): Promise<boolean> {
|
102 |
-
if (!this.port.writable) {
|
103 |
-
throw new Error("Port not open for writing");
|
104 |
-
}
|
105 |
-
|
106 |
-
try {
|
107 |
-
// STS3215 Write Goal_Position packet (matches Node.js exactly)
|
108 |
-
const packet = new Uint8Array([
|
109 |
-
0xff,
|
110 |
-
0xff, // Header
|
111 |
-
motorId, // Servo ID
|
112 |
-
0x05, // Length
|
113 |
-
0x03, // Instruction: WRITE_DATA
|
114 |
-
42, // Goal_Position register address
|
115 |
-
position & 0xff, // Position low byte
|
116 |
-
(position >> 8) & 0xff, // Position high byte
|
117 |
-
0x00, // Checksum placeholder
|
118 |
-
]);
|
119 |
-
|
120 |
-
// Calculate checksum
|
121 |
-
const checksum =
|
122 |
-
~(
|
123 |
-
motorId +
|
124 |
-
0x05 +
|
125 |
-
0x03 +
|
126 |
-
42 +
|
127 |
-
(position & 0xff) +
|
128 |
-
((position >> 8) & 0xff)
|
129 |
-
) & 0xff;
|
130 |
-
packet[8] = checksum;
|
131 |
-
|
132 |
-
// Use per-operation writer like calibration does
|
133 |
-
const writer = this.port.writable.getWriter();
|
134 |
-
try {
|
135 |
-
await writer.write(packet);
|
136 |
-
return true;
|
137 |
-
} finally {
|
138 |
-
writer.releaseLock();
|
139 |
-
}
|
140 |
-
} catch (error) {
|
141 |
-
console.warn(`Failed to write motor ${motorId} position:`, error);
|
142 |
-
return false;
|
143 |
-
}
|
144 |
-
}
|
145 |
-
|
146 |
-
async readMotorPosition(motorId: number): Promise<number | null> {
|
147 |
-
if (!this.port.writable || !this.port.readable) {
|
148 |
-
throw new Error("Port not open for reading/writing");
|
149 |
-
}
|
150 |
-
|
151 |
-
const writer = this.port.writable.getWriter();
|
152 |
-
const reader = this.port.readable.getReader();
|
153 |
-
|
154 |
-
try {
|
155 |
-
// STS3215 Read Present_Position packet
|
156 |
-
const packet = new Uint8Array([
|
157 |
-
0xff,
|
158 |
-
0xff, // Header
|
159 |
-
motorId, // Servo ID
|
160 |
-
0x04, // Length
|
161 |
-
0x02, // Instruction: READ_DATA
|
162 |
-
56, // Present_Position register address
|
163 |
-
0x02, // Data length (2 bytes)
|
164 |
-
0x00, // Checksum placeholder
|
165 |
-
]);
|
166 |
-
|
167 |
-
const checksum = ~(motorId + 0x04 + 0x02 + 56 + 0x02) & 0xff;
|
168 |
-
packet[7] = checksum;
|
169 |
-
|
170 |
-
// Clear buffer first
|
171 |
-
try {
|
172 |
-
const { value, done } = await reader.read();
|
173 |
-
if (done) return null;
|
174 |
-
} catch (e) {
|
175 |
-
// Buffer was empty, continue
|
176 |
-
}
|
177 |
-
|
178 |
-
await writer.write(packet);
|
179 |
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
180 |
-
|
181 |
-
const { value: response, done } = await reader.read();
|
182 |
-
if (done || !response || response.length < 7) {
|
183 |
-
return null;
|
184 |
-
}
|
185 |
-
|
186 |
-
const id = response[2];
|
187 |
-
const error = response[4];
|
188 |
-
|
189 |
-
if (id === motorId && error === 0) {
|
190 |
-
return response[5] | (response[6] << 8);
|
191 |
-
}
|
192 |
-
|
193 |
-
return null;
|
194 |
-
} catch (error) {
|
195 |
-
console.warn(`Failed to read motor ${motorId} position:`, error);
|
196 |
-
return null;
|
197 |
-
} finally {
|
198 |
-
reader.releaseLock();
|
199 |
-
writer.releaseLock();
|
200 |
-
}
|
201 |
-
}
|
202 |
-
|
203 |
-
async disconnect(): Promise<void> {
|
204 |
-
// Don't close the port itself - just cleanup wrapper
|
205 |
-
// The port is managed by PortManager
|
206 |
-
}
|
207 |
}
|
208 |
|
209 |
/**
|
210 |
-
*
|
211 |
-
*
|
212 |
*/
|
213 |
-
export function
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
name: "elbow_flex",
|
235 |
-
currentPosition: 2048,
|
236 |
-
minPosition: 1024,
|
237 |
-
maxPosition: 3072,
|
238 |
-
homePosition: 2048,
|
239 |
-
},
|
240 |
-
{
|
241 |
-
id: 4,
|
242 |
-
name: "wrist_flex",
|
243 |
-
currentPosition: 2048,
|
244 |
-
minPosition: 1024,
|
245 |
-
maxPosition: 3072,
|
246 |
-
homePosition: 2048,
|
247 |
-
},
|
248 |
-
{
|
249 |
-
id: 5,
|
250 |
-
name: "wrist_roll",
|
251 |
-
currentPosition: 2048,
|
252 |
-
minPosition: 1024,
|
253 |
-
maxPosition: 3072,
|
254 |
-
homePosition: 2048,
|
255 |
-
},
|
256 |
-
{
|
257 |
-
id: 6,
|
258 |
-
name: "gripper",
|
259 |
-
currentPosition: 2048,
|
260 |
-
minPosition: 1024,
|
261 |
-
maxPosition: 3072,
|
262 |
-
homePosition: 2048,
|
263 |
-
},
|
264 |
-
];
|
265 |
-
|
266 |
-
try {
|
267 |
-
// Load from unified storage
|
268 |
-
const unifiedKey = `lerobotjs-${serialNumber}`;
|
269 |
-
const unifiedDataRaw = localStorage.getItem(unifiedKey);
|
270 |
-
|
271 |
-
if (!unifiedDataRaw) {
|
272 |
-
console.log(
|
273 |
-
`No calibration data found for ${serialNumber}, using defaults`
|
274 |
-
);
|
275 |
-
return defaultConfigs;
|
276 |
-
}
|
277 |
-
|
278 |
-
const unifiedData: UnifiedRobotData = JSON.parse(unifiedDataRaw);
|
279 |
-
|
280 |
-
if (!unifiedData.calibration) {
|
281 |
-
console.log(
|
282 |
-
`No calibration in unified data for ${serialNumber}, using defaults`
|
283 |
-
);
|
284 |
-
return defaultConfigs;
|
285 |
}
|
286 |
|
287 |
-
|
288 |
-
|
289 |
-
(defaultConfig) => {
|
290 |
-
const calibData = (unifiedData.calibration as any)?.[
|
291 |
-
defaultConfig.name
|
292 |
-
];
|
293 |
-
|
294 |
-
if (
|
295 |
-
calibData &&
|
296 |
-
typeof calibData === "object" &&
|
297 |
-
"id" in calibData &&
|
298 |
-
"range_min" in calibData &&
|
299 |
-
"range_max" in calibData
|
300 |
-
) {
|
301 |
-
// Use calibrated values but keep current position as default
|
302 |
-
return {
|
303 |
-
...defaultConfig,
|
304 |
-
id: calibData.id,
|
305 |
-
minPosition: calibData.range_min,
|
306 |
-
maxPosition: calibData.range_max,
|
307 |
-
homePosition: Math.floor(
|
308 |
-
(calibData.range_min + calibData.range_max) / 2
|
309 |
-
),
|
310 |
-
};
|
311 |
-
}
|
312 |
-
|
313 |
-
return defaultConfig;
|
314 |
-
}
|
315 |
-
);
|
316 |
-
|
317 |
-
console.log(`✅ Loaded calibration data for ${serialNumber}`);
|
318 |
-
return calibratedConfigs;
|
319 |
-
} catch (error) {
|
320 |
-
console.warn(`Failed to load calibration for ${serialNumber}:`, error);
|
321 |
-
return defaultConfigs;
|
322 |
-
}
|
323 |
}
|
324 |
|
325 |
/**
|
326 |
* Web teleoperation controller
|
|
|
327 |
*/
|
328 |
export class WebTeleoperationController {
|
329 |
-
private port:
|
330 |
private motorConfigs: MotorConfig[] = [];
|
|
|
331 |
private isActive: boolean = false;
|
332 |
private updateInterval: NodeJS.Timeout | null = null;
|
333 |
private keyStates: {
|
334 |
[key: string]: { pressed: boolean; timestamp: number };
|
335 |
} = {};
|
|
|
336 |
|
337 |
// Movement parameters (matches Node.js)
|
338 |
private readonly STEP_SIZE = 8;
|
339 |
private readonly UPDATE_RATE = 60; // 60 FPS
|
340 |
-
private readonly KEY_TIMEOUT =
|
341 |
-
|
342 |
-
constructor(
|
343 |
-
|
344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
345 |
}
|
346 |
|
347 |
async initialize(): Promise<void> {
|
348 |
-
|
349 |
-
|
350 |
-
// Read current positions
|
351 |
for (const config of this.motorConfigs) {
|
352 |
-
const position = await this.port
|
353 |
if (position !== null) {
|
354 |
config.currentPosition = position;
|
355 |
}
|
@@ -401,11 +186,16 @@ export class WebTeleoperationController {
|
|
401 |
this.keyStates = {};
|
402 |
|
403 |
console.log("⏹️ Web teleoperation stopped");
|
|
|
|
|
|
|
|
|
|
|
404 |
}
|
405 |
|
406 |
async disconnect(): Promise<void> {
|
407 |
this.stop();
|
408 |
-
|
409 |
}
|
410 |
|
411 |
private updateMotorPositions(): void {
|
@@ -435,7 +225,7 @@ export class WebTeleoperationController {
|
|
435 |
const targetPositions: { [motorName: string]: number } = {};
|
436 |
|
437 |
for (const key of activeKeys) {
|
438 |
-
const control =
|
439 |
if (!control || control.motor === "emergency_stop") continue;
|
440 |
|
441 |
const motorConfig = this.motorConfigs.find(
|
@@ -455,16 +245,23 @@ export class WebTeleoperationController {
|
|
455 |
);
|
456 |
}
|
457 |
|
458 |
-
// Send motor commands
|
459 |
Object.entries(targetPositions).forEach(([motorName, targetPosition]) => {
|
460 |
const motorConfig = this.motorConfigs.find((m) => m.name === motorName);
|
461 |
if (motorConfig && targetPosition !== motorConfig.currentPosition) {
|
462 |
-
|
463 |
-
.
|
464 |
-
.
|
465 |
-
|
466 |
-
|
467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
});
|
469 |
}
|
470 |
});
|
@@ -480,15 +277,18 @@ export class WebTeleoperationController {
|
|
480 |
Math.min(motorConfig.maxPosition, targetPosition)
|
481 |
);
|
482 |
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
|
|
488 |
motorConfig.currentPosition = clampedPosition;
|
|
|
|
|
|
|
|
|
489 |
}
|
490 |
-
|
491 |
-
return success;
|
492 |
}
|
493 |
|
494 |
async setMotorPositions(positions: {
|
@@ -502,25 +302,81 @@ export class WebTeleoperationController {
|
|
502 |
|
503 |
return results.every((result) => result);
|
504 |
}
|
505 |
-
|
506 |
-
async goToHomePosition(): Promise<boolean> {
|
507 |
-
const homePositions = this.motorConfigs.reduce((acc, config) => {
|
508 |
-
acc[config.name] = config.homePosition;
|
509 |
-
return acc;
|
510 |
-
}, {} as { [motorName: string]: number });
|
511 |
-
|
512 |
-
return this.setMotorPositions(homePositions);
|
513 |
-
}
|
514 |
}
|
515 |
|
516 |
/**
|
517 |
-
*
|
|
|
518 |
*/
|
519 |
-
export async function
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
524 |
await controller.initialize();
|
525 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
526 |
}
|
|
|
3 |
* Mirrors the Node.js implementation but adapted for browser environment
|
4 |
*/
|
5 |
|
6 |
+
import { createSO100Config } from "./robots/so100_config.js";
|
7 |
+
import type {
|
8 |
+
RobotHardwareConfig,
|
9 |
+
KeyboardControl,
|
10 |
+
} from "./types/robot-config.js";
|
11 |
+
import type { RobotConnection } from "./types/robot-connection.js";
|
12 |
+
import { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js";
|
13 |
+
import {
|
14 |
+
readMotorPosition,
|
15 |
+
writeMotorPosition,
|
16 |
+
type MotorCommunicationPort,
|
17 |
+
} from "./utils/motor-communication.js";
|
18 |
|
19 |
/**
|
20 |
* Motor position and limits for teleoperation
|
|
|
25 |
currentPosition: number;
|
26 |
minPosition: number;
|
27 |
maxPosition: number;
|
|
|
28 |
}
|
29 |
|
30 |
/**
|
|
|
38 |
}
|
39 |
|
40 |
/**
|
41 |
+
* Teleoperation process control object (matches calibrate pattern)
|
42 |
*/
|
43 |
+
export interface TeleoperationProcess {
|
44 |
+
start(): void;
|
45 |
+
stop(): void;
|
46 |
+
updateKeyState(key: string, pressed: boolean): void;
|
47 |
+
getState(): TeleoperationState;
|
48 |
+
moveMotor(motorName: string, position: number): Promise<boolean>;
|
49 |
+
setMotorPositions(positions: {
|
50 |
+
[motorName: string]: number;
|
51 |
+
}): Promise<boolean>;
|
52 |
+
disconnect(): Promise<void>;
|
53 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
/**
|
56 |
+
* Create motor configurations from robot hardware config
|
57 |
+
* Pure function - converts robot specs to motor configs with defaults
|
58 |
*/
|
59 |
+
function createMotorConfigsFromRobotConfig(
|
60 |
+
robotConfig: RobotHardwareConfig
|
61 |
+
): MotorConfig[] {
|
62 |
+
return robotConfig.motorNames.map((name: string, i: number) => ({
|
63 |
+
id: robotConfig.motorIds[i],
|
64 |
+
name,
|
65 |
+
currentPosition: 2048,
|
66 |
+
minPosition: 1024,
|
67 |
+
maxPosition: 3072,
|
68 |
+
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
}
|
70 |
|
71 |
/**
|
72 |
+
* Apply calibration data to motor configurations
|
73 |
+
* Pure function - takes calibration data as parameter
|
74 |
*/
|
75 |
+
export function applyCalibrationToMotorConfigs(
|
76 |
+
defaultConfigs: MotorConfig[],
|
77 |
+
calibrationData: { [motorName: string]: any }
|
78 |
+
): MotorConfig[] {
|
79 |
+
return defaultConfigs.map((defaultConfig) => {
|
80 |
+
const calibData = calibrationData[defaultConfig.name];
|
81 |
+
|
82 |
+
if (
|
83 |
+
calibData &&
|
84 |
+
typeof calibData === "object" &&
|
85 |
+
"id" in calibData &&
|
86 |
+
"range_min" in calibData &&
|
87 |
+
"range_max" in calibData
|
88 |
+
) {
|
89 |
+
// Use calibrated values but keep current position as default
|
90 |
+
return {
|
91 |
+
...defaultConfig,
|
92 |
+
id: calibData.id,
|
93 |
+
minPosition: calibData.range_min,
|
94 |
+
maxPosition: calibData.range_max,
|
95 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
}
|
97 |
|
98 |
+
return defaultConfig;
|
99 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
}
|
101 |
|
102 |
/**
|
103 |
* Web teleoperation controller
|
104 |
+
* Now uses shared utilities instead of custom port handling
|
105 |
*/
|
106 |
export class WebTeleoperationController {
|
107 |
+
private port: MotorCommunicationPort;
|
108 |
private motorConfigs: MotorConfig[] = [];
|
109 |
+
private keyboardControls: { [key: string]: KeyboardControl } = {};
|
110 |
private isActive: boolean = false;
|
111 |
private updateInterval: NodeJS.Timeout | null = null;
|
112 |
private keyStates: {
|
113 |
[key: string]: { pressed: boolean; timestamp: number };
|
114 |
} = {};
|
115 |
+
private onStateUpdate?: (state: TeleoperationState) => void;
|
116 |
|
117 |
// Movement parameters (matches Node.js)
|
118 |
private readonly STEP_SIZE = 8;
|
119 |
private readonly UPDATE_RATE = 60; // 60 FPS
|
120 |
+
private readonly KEY_TIMEOUT = 600; // ms - longer than browser keyboard repeat delay (~500ms)
|
121 |
+
|
122 |
+
constructor(
|
123 |
+
port: MotorCommunicationPort,
|
124 |
+
motorConfigs: MotorConfig[],
|
125 |
+
keyboardControls: { [key: string]: KeyboardControl },
|
126 |
+
onStateUpdate?: (state: TeleoperationState) => void
|
127 |
+
) {
|
128 |
+
this.port = port;
|
129 |
+
this.motorConfigs = motorConfigs;
|
130 |
+
this.keyboardControls = keyboardControls;
|
131 |
+
this.onStateUpdate = onStateUpdate;
|
132 |
}
|
133 |
|
134 |
async initialize(): Promise<void> {
|
135 |
+
// Read current positions using proven utilities
|
|
|
|
|
136 |
for (const config of this.motorConfigs) {
|
137 |
+
const position = await readMotorPosition(this.port, config.id);
|
138 |
if (position !== null) {
|
139 |
config.currentPosition = position;
|
140 |
}
|
|
|
186 |
this.keyStates = {};
|
187 |
|
188 |
console.log("⏹️ Web teleoperation stopped");
|
189 |
+
|
190 |
+
// Notify UI of state change
|
191 |
+
if (this.onStateUpdate) {
|
192 |
+
this.onStateUpdate(this.getState());
|
193 |
+
}
|
194 |
}
|
195 |
|
196 |
async disconnect(): Promise<void> {
|
197 |
this.stop();
|
198 |
+
// No need to manually disconnect - port wrapper handles this
|
199 |
}
|
200 |
|
201 |
private updateMotorPositions(): void {
|
|
|
225 |
const targetPositions: { [motorName: string]: number } = {};
|
226 |
|
227 |
for (const key of activeKeys) {
|
228 |
+
const control = this.keyboardControls[key];
|
229 |
if (!control || control.motor === "emergency_stop") continue;
|
230 |
|
231 |
const motorConfig = this.motorConfigs.find(
|
|
|
245 |
);
|
246 |
}
|
247 |
|
248 |
+
// Send motor commands using proven utilities
|
249 |
Object.entries(targetPositions).forEach(([motorName, targetPosition]) => {
|
250 |
const motorConfig = this.motorConfigs.find((m) => m.name === motorName);
|
251 |
if (motorConfig && targetPosition !== motorConfig.currentPosition) {
|
252 |
+
writeMotorPosition(
|
253 |
+
this.port,
|
254 |
+
motorConfig.id,
|
255 |
+
Math.round(targetPosition)
|
256 |
+
)
|
257 |
+
.then(() => {
|
258 |
+
motorConfig.currentPosition = targetPosition;
|
259 |
+
})
|
260 |
+
.catch((error) => {
|
261 |
+
console.warn(
|
262 |
+
`Failed to write motor ${motorConfig.id} position:`,
|
263 |
+
error
|
264 |
+
);
|
265 |
});
|
266 |
}
|
267 |
});
|
|
|
277 |
Math.min(motorConfig.maxPosition, targetPosition)
|
278 |
);
|
279 |
|
280 |
+
try {
|
281 |
+
await writeMotorPosition(
|
282 |
+
this.port,
|
283 |
+
motorConfig.id,
|
284 |
+
Math.round(clampedPosition)
|
285 |
+
);
|
286 |
motorConfig.currentPosition = clampedPosition;
|
287 |
+
return true;
|
288 |
+
} catch (error) {
|
289 |
+
console.warn(`Failed to move motor ${motorName}:`, error);
|
290 |
+
return false;
|
291 |
}
|
|
|
|
|
292 |
}
|
293 |
|
294 |
async setMotorPositions(positions: {
|
|
|
302 |
|
303 |
return results.every((result) => result);
|
304 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
305 |
}
|
306 |
|
307 |
/**
|
308 |
+
* Main teleoperate function - simple API matching calibrate pattern
|
309 |
+
* Handles robot types internally, creates appropriate motor configurations
|
310 |
*/
|
311 |
+
export async function teleoperate(
|
312 |
+
robotConnection: RobotConnection,
|
313 |
+
options?: {
|
314 |
+
calibrationData?: { [motorName: string]: any };
|
315 |
+
onStateUpdate?: (state: TeleoperationState) => void;
|
316 |
+
}
|
317 |
+
): Promise<TeleoperationProcess> {
|
318 |
+
// Validate required fields
|
319 |
+
if (!robotConnection.robotType) {
|
320 |
+
throw new Error(
|
321 |
+
"Robot type is required for teleoperation. Please configure the robot first."
|
322 |
+
);
|
323 |
+
}
|
324 |
+
|
325 |
+
// Create web serial port wrapper (same pattern as calibrate.ts)
|
326 |
+
const port = new WebSerialPortWrapper(robotConnection.port);
|
327 |
+
await port.initialize();
|
328 |
+
|
329 |
+
// Get robot-specific configuration (same pattern as calibrate.ts)
|
330 |
+
let config: RobotHardwareConfig;
|
331 |
+
if (robotConnection.robotType.startsWith("so100")) {
|
332 |
+
config = createSO100Config(robotConnection.robotType);
|
333 |
+
} else {
|
334 |
+
throw new Error(`Unsupported robot type: ${robotConnection.robotType}`);
|
335 |
+
}
|
336 |
+
|
337 |
+
// Create motor configs from robot hardware specs (single call, no duplication)
|
338 |
+
const defaultMotorConfigs = createMotorConfigsFromRobotConfig(config);
|
339 |
+
|
340 |
+
// Apply calibration data if provided
|
341 |
+
const motorConfigs = options?.calibrationData
|
342 |
+
? applyCalibrationToMotorConfigs(
|
343 |
+
defaultMotorConfigs,
|
344 |
+
options.calibrationData
|
345 |
+
)
|
346 |
+
: defaultMotorConfigs;
|
347 |
+
|
348 |
+
// Create and initialize controller using shared utilities
|
349 |
+
const controller = new WebTeleoperationController(
|
350 |
+
port,
|
351 |
+
motorConfigs,
|
352 |
+
config.keyboardControls,
|
353 |
+
options?.onStateUpdate
|
354 |
+
);
|
355 |
await controller.initialize();
|
356 |
+
|
357 |
+
// Wrap controller in process object (matches calibrate pattern)
|
358 |
+
return {
|
359 |
+
start: () => {
|
360 |
+
controller.start();
|
361 |
+
// Optional state update callback
|
362 |
+
if (options?.onStateUpdate) {
|
363 |
+
const updateLoop = () => {
|
364 |
+
if (controller.getState().isActive) {
|
365 |
+
options.onStateUpdate!(controller.getState());
|
366 |
+
setTimeout(updateLoop, 100); // 10fps state updates
|
367 |
+
}
|
368 |
+
};
|
369 |
+
updateLoop();
|
370 |
+
}
|
371 |
+
},
|
372 |
+
stop: () => controller.stop(),
|
373 |
+
updateKeyState: (key: string, pressed: boolean) =>
|
374 |
+
controller.updateKeyState(key, pressed),
|
375 |
+
getState: () => controller.getState(),
|
376 |
+
moveMotor: (motorName: string, position: number) =>
|
377 |
+
controller.moveMotor(motorName, position),
|
378 |
+
setMotorPositions: (positions: { [motorName: string]: number }) =>
|
379 |
+
controller.setMotorPositions(positions),
|
380 |
+
disconnect: () => controller.disconnect(),
|
381 |
+
};
|
382 |
}
|
src/lerobot/web/types/robot-config.ts
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Shared robot hardware configuration types
|
3 |
+
* Used across calibration, teleoperation, and other robot operations
|
4 |
+
*/
|
5 |
+
|
6 |
+
/**
|
7 |
+
* Keyboard control mapping for teleoperation
|
8 |
+
*/
|
9 |
+
export interface KeyboardControl {
|
10 |
+
motor: string;
|
11 |
+
direction: number;
|
12 |
+
description: string;
|
13 |
+
}
|
14 |
+
|
15 |
+
/**
|
16 |
+
* Robot hardware configuration interface
|
17 |
+
* Defines the contract that all robot configurations must implement
|
18 |
+
*/
|
19 |
+
export interface RobotHardwareConfig {
|
20 |
+
deviceType: string;
|
21 |
+
motorNames: string[];
|
22 |
+
motorIds: number[];
|
23 |
+
driveModes: number[];
|
24 |
+
|
25 |
+
// Keyboard controls for teleoperation (robot-specific)
|
26 |
+
keyboardControls: { [key: string]: KeyboardControl };
|
27 |
+
|
28 |
+
protocol: {
|
29 |
+
resolution: number;
|
30 |
+
homingOffsetAddress: number;
|
31 |
+
homingOffsetLength: number;
|
32 |
+
presentPositionAddress: number;
|
33 |
+
presentPositionLength: number;
|
34 |
+
minPositionLimitAddress: number;
|
35 |
+
minPositionLimitLength: number;
|
36 |
+
maxPositionLimitAddress: number;
|
37 |
+
maxPositionLimitLength: number;
|
38 |
+
signMagnitudeBit: number;
|
39 |
+
};
|
40 |
+
}
|
src/lerobot/web/types/robot-connection.ts
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Core robot connection types used across the lerobot.js web library
|
3 |
+
* These types are shared between findPort, calibrate, teleoperate, and other modules
|
4 |
+
*/
|
5 |
+
|
6 |
+
/**
|
7 |
+
* Type definitions for WebSerial API (not yet in all TypeScript libs)
|
8 |
+
*/
|
9 |
+
export interface SerialPort {
|
10 |
+
readonly readable: ReadableStream;
|
11 |
+
readonly writable: WritableStream;
|
12 |
+
getInfo(): SerialPortInfo;
|
13 |
+
open(options: SerialOptions): Promise<void>;
|
14 |
+
close(): Promise<void>;
|
15 |
+
}
|
16 |
+
|
17 |
+
export interface SerialPortInfo {
|
18 |
+
usbVendorId?: number;
|
19 |
+
usbProductId?: number;
|
20 |
+
}
|
21 |
+
|
22 |
+
export interface SerialOptions {
|
23 |
+
baudRate: number;
|
24 |
+
dataBits?: number;
|
25 |
+
stopBits?: number;
|
26 |
+
parity?: "none" | "even" | "odd";
|
27 |
+
}
|
28 |
+
|
29 |
+
/**
|
30 |
+
* Unified robot connection interface used across all functions
|
31 |
+
* This same object works for findPort, calibrate, teleoperate, etc.
|
32 |
+
* Includes all fields needed by demo and other applications
|
33 |
+
*/
|
34 |
+
export interface RobotConnection {
|
35 |
+
port: SerialPort;
|
36 |
+
name: string; // Display name for UI
|
37 |
+
isConnected: boolean; // Connection status
|
38 |
+
robotType?: "so100_follower" | "so100_leader"; // Optional until user configures
|
39 |
+
robotId?: string; // Optional until user configures
|
40 |
+
serialNumber: string; // Always required for identification
|
41 |
+
error?: string; // Error message if connection failed
|
42 |
+
usbMetadata?: {
|
43 |
+
// USB device information
|
44 |
+
vendorId: string;
|
45 |
+
productId: string;
|
46 |
+
serialNumber: string;
|
47 |
+
manufacturerName: string;
|
48 |
+
productName: string;
|
49 |
+
usbVersionMajor?: number;
|
50 |
+
usbVersionMinor?: number;
|
51 |
+
deviceClass?: number;
|
52 |
+
deviceSubclass?: number;
|
53 |
+
deviceProtocol?: number;
|
54 |
+
};
|
55 |
+
}
|
56 |
+
|
57 |
+
/**
|
58 |
+
* Minimal robot config for finding/connecting to specific robots
|
59 |
+
*/
|
60 |
+
export interface RobotConfig {
|
61 |
+
robotType: "so100_follower" | "so100_leader";
|
62 |
+
robotId: string;
|
63 |
+
serialNumber: string;
|
64 |
+
}
|