NERDDISCO commited on
Commit
5eb1bc0
·
1 Parent(s): efe2c71

refactor: calibrate for web

Browse files
docs/api-comparison.md ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.
docs/planning/005_same_api.md ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # User Story 005: Building Block API - Clean, Composable, Unified
2
+
3
+ ## 🎯 Goal
4
+
5
+ Create a clean building block API across Python lerobot, Node.js lerobot.js, and Web lerobot.js where users manage state and workflow while we abstract away robotics complexity. Users learn core concepts once and apply them everywhere.
6
+
7
+ ## 🚨 Problem Statement
8
+
9
+ Currently, the APIs have fundamentally different approaches:
10
+
11
+ ### Current Inconsistencies
12
+
13
+ 1. **State Management**:
14
+ - Python/Node.js: Functions handle connection lifecycle internally
15
+ - Web: User must manually manage controllers and connections
16
+ 2. **Complexity Exposure**:
17
+ - Python/Node.js: `calibrate(config)` - single call does everything
18
+ - Web: Multi-step `controller.performHomingStep()`, `controller.moveToPosition()`, etc.
19
+ 3. **Resource Management**:
20
+ - Python/Node.js: Each function connects/disconnects automatically
21
+ - Web: User manages port lifecycle manually
22
+ 4. **Device Identification**:
23
+ - Node.js: Port discovery finds system ports by name (COM4, /dev/ttyUSB0)
24
+ - Web: No device identification - WebSerial can't distinguish between identical devices
25
+
26
+ ### User Pain Points
27
+
28
+ - **Different Mental Models**: Same operations require different approaches per platform
29
+ - **Web Complexity Exposure**: Users must understand servo protocols and multi-step procedures
30
+ - **Inconsistent Resource Management**: Some platforms auto-manage, others don't
31
+ - **Device Identification Issues**: Web can't identify which physical robot is connected
32
+ - **Hidden State Problems**: Users can't control when connections happen or track device status
33
+
34
+ ## 🎯 Success Criteria
35
+
36
+ ### Primary Goal: Building Block Consistency
37
+
38
+ - [ ] **Same Operation Patterns**: All platforms use identical patterns for core operations
39
+ - [ ] **Clean Responsibility Split**: Users manage state/UI, library handles robotics
40
+ - [ ] **Self-Contained Operations**: Each function manages its own connection lifecycle
41
+ - [ ] **Unified Control Model**: Long-running operations use same session-based control
42
+
43
+ ### Secondary Goal: Platform-Appropriate Implementation
44
+
45
+ - [ ] **Smart Device Pairing**: Web uses WebUSB + WebSerial for device identification + communication
46
+ - [ ] **Hidden Robotics Complexity**: Baud rates, protocols, servo commands abstracted away
47
+ - [ ] **Graceful Resource Management**: Automatic connection cleanup with user control
48
+ - [ ] **Consistent Error Handling**: Platform-specific errors mapped to common patterns
49
+
50
+ ## 📋 Ground Truth Decisions
51
+
52
+ ### 1. Building Block Philosophy
53
+
54
+ **DECIDED: User manages state, we provide clean tools**
55
+
56
+ ✅ **User Responsibilities:**
57
+
58
+ - Device pairing and storage (when to pair, how to persist)
59
+ - UI workflow (when to show dialogs, how to display progress)
60
+ - State management (which devices they have, tracking connections)
61
+ - Error handling UI (how to display errors to end users)
62
+
63
+ ✅ **Library Responsibilities:**
64
+
65
+ - WebUSB + WebSerial pairing dialogs (native browser interactions)
66
+ - Baud rate configuration (1000000 for SO-100)
67
+ - STS3215 servo protocol implementation
68
+ - Calibration and teleoperation procedures
69
+ - Resource conflict prevention with clear error messages
70
+
71
+ ### 2. Device Pairing vs Connection Pattern
72
+
73
+ **DECIDED: Separate pairing (once) from connection (per operation)**
74
+
75
+ ```javascript
76
+ // PAIRING: One-time device identification (stores device reference)
77
+ const deviceInfo = await lerobot.pairDevice();
78
+ // User stores this: localStorage, state management, etc.
79
+
80
+ // OPERATIONS: Each function connects internally using device reference
81
+ const session = await lerobot.calibrate({ robot: { device: deviceInfo } });
82
+ await session.stop(); // Graceful disconnect
83
+
84
+ const session2 = await lerobot.teleoperate({ robot: { device: deviceInfo } });
85
+ await session2.stop(); // Independent connection lifecycle
86
+ ```
87
+
88
+ **Web Implementation Details:**
89
+
90
+ - `pairDevice()` shows WebUSB dialog → gets device info → shows WebSerial dialog → extracts serial ID
91
+ - Each operation connects to paired device internally (no singleton state)
92
+ - Users never see servo protocols, baud rates, or connection management
93
+
94
+ ### 3. Connection Lifecycle Pattern
95
+
96
+ **DECIDED: Each operation is self-contained (like Python/Node.js)**
97
+
98
+ ```javascript
99
+ // PYTHON/NODE.JS CURRENT PATTERN (keep this):
100
+ await calibrate(config); // Internally: connect → calibrate → disconnect
101
+ await teleoperate(config); // Internally: connect → teleoperate → disconnect
102
+
103
+ // WEB NEW PATTERN (match Python/Node.js):
104
+ const cal = await calibrate({ robot: { device: pairedDevice } }); // connect → start calibration
105
+ await cal.stop(); // disconnect
106
+
107
+ const tel = await teleoperate({ robot: { device: pairedDevice } }); // connect → start teleoperation
108
+ await tel.stop(); // disconnect
109
+ ```
110
+
111
+ **Key Principle:** No shared connections, no singletons, each operation manages its own lifecycle.
112
+
113
+ ### 4. Long-Running Operation Control
114
+
115
+ **DECIDED: Both calibrate and teleoperate are long-running until user stops**
116
+
117
+ ```javascript
118
+ // UNIFIED SESSION PATTERN for both operations:
119
+ interface RobotSession {
120
+ stop(): Promise<void>; // Graceful shutdown + disconnect
121
+ getState(): SessionState; // Current operation state
122
+ }
123
+
124
+ // Both operations return controllable sessions:
125
+ const calibration = await lerobot.calibrate({
126
+ robot: { device: myDevice },
127
+ onStateChange: (state) => updateUI(state),
128
+ });
129
+
130
+ const teleoperation = await lerobot.teleoperate({
131
+ robot: { device: myDevice },
132
+ onStateChange: (state) => updateUI(state),
133
+ });
134
+
135
+ // Same control pattern for both:
136
+ await calibration.stop();
137
+ await teleoperation.stop();
138
+ ```
139
+
140
+ **Calibrate Reality Check:**
141
+
142
+ - Starts calibration procedure
143
+ - Waits for user to move robot through ranges
144
+ - Records positions until user decides to stop
145
+ - Can run indefinitely like teleoperate
146
+
147
+ ### 5. Web Device Identification Solution
148
+
149
+ **DECIDED: WebUSB + WebSerial both required**
150
+
151
+ ```javascript
152
+ // Why both APIs are needed:
153
+ // 1. WebUSB: Get device serial number → "This is robot arm #ABC123"
154
+ // 2. WebSerial: Actual communication → Send servo commands
155
+
156
+ async function pairDevice(): Promise<DeviceInfo> {
157
+ // Step 1: WebUSB for device identification
158
+ const usbDevice = await navigator.usb.requestDevice({...});
159
+ const serialId = extractSerialNumber(usbDevice);
160
+
161
+ // Step 2: WebSerial for communication
162
+ const serialPort = await navigator.serial.requestPort();
163
+ await serialPort.open({ baudRate: 1000000 }); // We handle baud rate
164
+
165
+ return {
166
+ serialId,
167
+ usbDevice,
168
+ serialPort,
169
+ type: detectRobotType(usbDevice) // We detect SO-100 vs other robots
170
+ };
171
+ }
172
+ ```
173
+
174
+ ## 🛠️ Implementation Architecture
175
+
176
+ ### Core Building Blocks
177
+
178
+ ```javascript
179
+ // DEVICE PAIRING (Web-specific, one-time)
180
+ export async function pairDevice(): Promise<DeviceInfo>
181
+
182
+ // UNIFIED OPERATIONS (Same across all platforms)
183
+ export async function calibrate(config: CalibrationConfig): Promise<CalibrationSession>
184
+ export async function teleoperate(config: TeleoperationConfig): Promise<TeleoperationSession>
185
+
186
+ // UNIFIED SESSION CONTROL
187
+ interface RobotSession {
188
+ stop(): Promise<void>
189
+ getState(): SessionState
190
+ }
191
+ ```
192
+
193
+ ### Platform-Specific Implementation
194
+
195
+ **Node.js (No changes needed - already correct pattern):**
196
+
197
+ ```javascript
198
+ // Each function connects/disconnects internally - keep as-is
199
+ export async function calibrate(config) {
200
+ const device = createDevice(config.robot);
201
+ await device.connect();
202
+ // ... calibration work
203
+ await device.disconnect();
204
+ }
205
+ ```
206
+
207
+ **Web (New unified wrapper):**
208
+
209
+ ```javascript
210
+ // Hide controller complexity behind unified interface
211
+ export async function calibrate(config) {
212
+ let connection = null;
213
+ try {
214
+ connection = await connectToDevice(config.robot.device); // We handle WebSerial setup
215
+ const session = new CalibrationSession(connection, config);
216
+ return session; // User controls session lifecycle
217
+ } catch (error) {
218
+ if (connection) await connection.disconnect();
219
+ throw error;
220
+ }
221
+ }
222
+ ```
223
+
224
+ ## 📋 Detailed API Specification
225
+
226
+ ### 1. Device Pairing API (Web Only)
227
+
228
+ ```javascript
229
+ // Web-specific device pairing
230
+ interface DeviceInfo {
231
+ serialId: string; // From WebUSB device descriptor
232
+ robotType: 'so100_follower' | 'so100_leader';
233
+ usbVendorId: number;
234
+ usbProductId: number;
235
+ // Internal connection details (user doesn't need to know)
236
+ }
237
+
238
+ export async function pairDevice(): Promise<DeviceInfo>
239
+ ```
240
+
241
+ **User Workflow:**
242
+
243
+ 1. Call `pairDevice()` → Native browser dialogs appear
244
+ 2. Store `DeviceInfo` in their state management
245
+ 3. Use stored device for all subsequent operations
246
+
247
+ ### 2. Unified Calibration API
248
+
249
+ ```javascript
250
+ interface CalibrationConfig {
251
+ robot: {
252
+ device: DeviceInfo; // Web: paired device, Node.js: { type, port }
253
+ };
254
+ onStateChange?: (state: 'connecting' | 'connected' | 'active' | 'stopping' | 'stopped') => void;
255
+ onProgress?: (progress: CalibrationProgress) => void;
256
+ }
257
+
258
+ interface CalibrationSession extends RobotSession {
259
+ stop(): Promise<CalibrationResults>; // Returns calibration data
260
+ }
261
+
262
+ // SAME across all platforms:
263
+ export async function calibrate(config: CalibrationConfig): Promise<CalibrationSession>
264
+ ```
265
+
266
+ ### 3. Unified Teleoperation API
267
+
268
+ ```javascript
269
+ interface TeleoperationConfig {
270
+ robot: {
271
+ device: DeviceInfo; // Web: paired device, Node.js: { type, port }
272
+ };
273
+ controls?: {
274
+ [key: string]: ControlMapping;
275
+ };
276
+ onStateChange?: (state: SessionState) => void;
277
+ }
278
+
279
+ interface TeleoperationSession extends RobotSession {
280
+ updateKeyState(key: string, pressed: boolean): void; // For keyboard control
281
+ getCurrentPositions(): Promise<Record<string, number>>;
282
+ }
283
+
284
+ // SAME across all platforms:
285
+ export async function teleoperate(config: TeleoperationConfig): Promise<TeleoperationSession>
286
+ ```
287
+
288
+ ### 4. Error Handling
289
+
290
+ ```javascript
291
+ // Standard error types across platforms
292
+ class DeviceNotPairedError extends Error {}
293
+ class DeviceConnectionError extends Error {}
294
+ class DeviceAlreadyConnectedError extends Error {}
295
+ class CalibrationError extends Error {}
296
+ class TeleoperationError extends Error {}
297
+
298
+ // Platform-specific errors mapped to standard types
299
+ // Web: WebSerial/WebUSB errors → DeviceConnectionError
300
+ // Node.js: SerialPort errors → DeviceConnectionError
301
+ ```
302
+
303
+ ## 🔄 User Experience Flows
304
+
305
+ ### First-Time Web User
306
+
307
+ ```javascript
308
+ // 1. Pair device once (shows native browser dialogs)
309
+ const myRobot = await lerobot.pairDevice();
310
+ localStorage.setItem("myRobot", JSON.stringify(myRobot));
311
+
312
+ // 2. Use paired device for operations
313
+ const calibration = await lerobot.calibrate({
314
+ robot: { device: myRobot },
315
+ onStateChange: (state) => updateStatus(state),
316
+ });
317
+
318
+ // 3. User controls session
319
+ await calibration.stop();
320
+ ```
321
+
322
+ ### Returning Web User
323
+
324
+ ```javascript
325
+ // 1. Load stored device (no browser dialogs)
326
+ const myRobot = JSON.parse(localStorage.getItem("myRobot"));
327
+
328
+ // 2. Operations work immediately (connect internally)
329
+ const teleoperation = await lerobot.teleoperate({
330
+ robot: { device: myRobot },
331
+ onStateChange: (state) => updateStatus(state),
332
+ });
333
+ ```
334
+
335
+ ### Node.js User (Unchanged)
336
+
337
+ ```javascript
338
+ // Same as current - no changes needed
339
+ const calibration = await lerobot.calibrate({
340
+ robot: { type: "so100_follower", port: "COM4" },
341
+ });
342
+ await calibration.stop();
343
+ ```
344
+
345
+ ## 🧪 Implementation Phases
346
+
347
+ ### Phase 1: Core Building Blocks
348
+
349
+ - [ ] Define unified interfaces (`RobotSession`, `DeviceInfo`)
350
+ - [ ] Implement Web `pairDevice()` with WebUSB + WebSerial
351
+ - [ ] Create session-based wrappers for Web calibration/teleoperation
352
+ - [ ] Standardize error types and mapping
353
+
354
+ ### Phase 2: API Unification
355
+
356
+ - [ ] Ensure Node.js returns session objects (minimal changes)
357
+ - [ ] Web operations return sessions that hide controller complexity
358
+ - [ ] Unified state change callbacks and progress reporting
359
+ - [ ] Cross-platform testing with real hardware
360
+
361
+ ### Phase 3: Developer Experience
362
+
363
+ - [ ] Update examples to use building block pattern
364
+ - [ ] Create migration guide from current APIs
365
+ - [ ] Unified documentation with platform-specific notes
366
+ - [ ] Performance optimization and bundle size analysis
367
+
368
+ ## 📊 Success Metrics
369
+
370
+ ### Developer Experience
371
+
372
+ - [ ] **Single Learning Curve**: Core concepts (pairing, sessions, operations) work everywhere
373
+ - [ ] **User Control**: Developers control state, UI, and workflow completely
374
+ - [ ] **Platform Abstraction**: Robotics complexity hidden, platform differences minimal
375
+
376
+ ### Technical Quality
377
+
378
+ - [ ] **Resource Management**: No connection leaks, graceful cleanup
379
+ - [ ] **Error Handling**: Clear, actionable errors with consistent types
380
+ - [ ] **Performance**: No regressions, efficient resource usage
381
+
382
+ ### API Consistency
383
+
384
+ - [ ] **Same Patterns**: Operations, sessions, and control work identically
385
+ - [ ] **Platform Appropriate**: Each platform uses native capabilities optimally
386
+ - [ ] **Migration Path**: Existing code can adopt new patterns incrementally
387
+
388
+ ## 🔍 Edge Cases & Considerations
389
+
390
+ ### Multiple Device Management
391
+
392
+ ```javascript
393
+ // User manages multiple devices
394
+ const robot1 = await lerobot.pairDevice(); // First robot
395
+ const robot2 = await lerobot.pairDevice(); // Second robot
396
+
397
+ // Independent sessions
398
+ const cal1 = await lerobot.calibrate({ robot: { device: robot1 } });
399
+ const tel2 = await lerobot.teleoperate({ robot: { device: robot2 } });
400
+ ```
401
+
402
+ ### Resource Conflicts
403
+
404
+ ```javascript
405
+ // Library prevents conflicts with clear errors
406
+ const session1 = await lerobot.calibrate({ robot: { device: myRobot } });
407
+
408
+ try {
409
+ const session2 = await lerobot.teleoperate({ robot: { device: myRobot } });
410
+ } catch (error) {
411
+ if (error instanceof DeviceAlreadyConnectedError) {
412
+ // User gets clear message: "Device already in use by calibration. Stop calibration first."
413
+ await session1.stop();
414
+ const session2 = await lerobot.teleoperate({ robot: { device: myRobot } });
415
+ }
416
+ }
417
+ ```
418
+
419
+ ### Browser Tab Management
420
+
421
+ ```javascript
422
+ // Sessions automatically cleanup on page unload
423
+ window.addEventListener("beforeunload", async () => {
424
+ if (currentSession) {
425
+ await currentSession.stop(); // Graceful disconnect
426
+ }
427
+ });
428
+ ```
429
+
430
+ ## 📝 Definition of Done
431
+
432
+ - [ ] **Web Pairing**: `pairDevice()` uses WebUSB + WebSerial to identify and store device info
433
+ - [ ] **Session Control**: Both `calibrate()` and `teleoperate()` return controllable sessions
434
+ - [ ] **Self-Contained Operations**: Each operation connects/disconnects internally, no shared state
435
+ - [ ] **Unified Error Handling**: Platform-specific errors mapped to consistent error types
436
+ - [ ] **Resource Management**: Clear error messages for conflicts, automatic cleanup
437
+ - [ ] **Documentation**: Single API reference with platform-specific implementation notes
438
+ - [ ] **Migration Path**: Existing users can adopt new patterns without breaking changes
439
+ - [ ] **Hardware Validation**: Tested with real SO-100 hardware on all platforms
440
+
441
+ ---
442
+
443
+ **Priority**: 🔴 Critical - Blocks adoption due to API inconsistency
444
+ **Effort**: 🔥🔥🔥 High - Significant Web refactoring, but Node.js mostly unchanged
445
+ **Impact**: 🚀🚀🚀 Transformative - Enables true cross-platform development
446
+
447
+ **Impact**: 🚀🚀🚀 Very High - Transforms user experience and enables widespread adoption
src/demo/App.tsx CHANGED
@@ -1,10 +1,10 @@
1
  import React, { useState } from "react";
2
  import { Home } from "./pages/Home";
3
  import { ErrorBoundary } from "./components/ErrorBoundary";
4
- import type { ConnectedRobot } from "./types";
5
 
6
  export function App() {
7
- const [connectedRobots, setConnectedRobots] = useState<ConnectedRobot[]>([]);
8
 
9
  return (
10
  <ErrorBoundary>
 
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/find_port.js";
5
 
6
  export function App() {
7
+ const [connectedRobots, setConnectedRobots] = useState<RobotConnection[]>([]);
8
 
9
  return (
10
  <ErrorBoundary>
src/demo/components/CalibrationPanel.tsx CHANGED
@@ -1,10 +1,4 @@
1
- import React, {
2
- useState,
3
- useEffect,
4
- useCallback,
5
- useRef,
6
- useMemo,
7
- } from "react";
8
  import { Button } from "./ui/button";
9
  import {
10
  Card,
@@ -15,45 +9,35 @@ import {
15
  } from "./ui/card";
16
  import { Badge } from "./ui/badge";
17
  import {
18
- createCalibrationController,
19
- WebCalibrationController,
20
- saveCalibrationResults,
21
  type WebCalibrationResults,
 
 
22
  } from "../../lerobot/web/calibrate";
 
 
 
23
  import { CalibrationModal } from "./CalibrationModal";
24
- import type { ConnectedRobot } from "../types";
25
 
26
  interface CalibrationPanelProps {
27
- robot: ConnectedRobot;
28
  onFinish: () => void;
29
  }
30
 
31
- interface MotorCalibrationData {
32
- name: string;
33
- current: number;
34
- min: number;
35
- max: number;
36
- range: number;
37
- }
38
-
39
- /**
40
- * Custom hook for calibration that manages the serial port properly
41
- * Uses vanilla calibration functions internally, provides React-friendly interface
42
- */
43
- function useCalibration(robot: ConnectedRobot) {
44
- const [controller, setController] = useState<WebCalibrationController | null>(
45
- null
46
- );
47
  const [isCalibrating, setIsCalibrating] = useState(false);
48
- const [isRecordingRanges, setIsRecordingRanges] = useState(false);
49
  const [calibrationResult, setCalibrationResult] =
50
  useState<WebCalibrationResults | null>(null);
51
  const [status, setStatus] = useState<string>("Ready to calibrate");
 
 
 
 
 
52
 
53
- // Motor data state
54
- const [motorData, setMotorData] = useState<MotorCalibrationData[]>([]);
55
-
56
- // Static motor names - use useMemo to prevent recreation on every render
57
  const motorNames = useMemo(
58
  () => [
59
  "shoulder_pan",
@@ -66,268 +50,49 @@ function useCalibration(robot: ConnectedRobot) {
66
  []
67
  );
68
 
69
- // Initialize controller when robot changes
70
- const initializeController = useCallback(async () => {
71
- if (!robot.port || !robot.robotType) {
72
- throw new Error("Invalid robot configuration");
73
- }
74
-
75
- const newController = await createCalibrationController(
76
- robot.robotType,
77
- robot.port
78
- );
79
- setController(newController);
80
- return newController;
81
- }, [robot.port, robot.robotType]);
82
-
83
- // Read motor positions using the controller (no concurrent access)
84
- const readMotorPositions = useCallback(async (): Promise<number[]> => {
85
- if (!controller) {
86
- throw new Error("Controller not initialized");
87
- }
88
- return await controller.readMotorPositions();
89
- }, [controller]);
90
-
91
- // Update motor data from positions
92
- const updateMotorData = useCallback(
93
- (
94
- positions: number[],
95
- rangeMins?: { [motor: string]: number },
96
- rangeMaxes?: { [motor: string]: number }
97
- ) => {
98
- const newData = motorNames.map((name, index) => {
99
- const current = positions[index];
100
- const min = rangeMins ? rangeMins[name] : current;
101
- const max = rangeMaxes ? rangeMaxes[name] : current;
102
-
103
- return {
104
- name,
105
- current,
106
- min,
107
- max,
108
- range: max - min,
109
- };
110
- });
111
-
112
- setMotorData(newData);
113
- },
114
- [motorNames]
115
- );
116
-
117
  // Initialize motor data
118
  const initializeMotorData = useCallback(() => {
119
- const initialData = motorNames.map((name) => ({
120
- name,
121
- current: 2047,
122
- min: 2047,
123
- max: 2047,
124
- range: 0,
125
- }));
 
 
126
  setMotorData(initialData);
127
  }, [motorNames]);
128
 
129
- // Start calibration process
130
- const startCalibration = useCallback(async () => {
 
 
 
 
131
  try {
132
- setStatus("🤖 Starting calibration process...");
133
- setIsCalibrating(true);
134
 
135
- const ctrl = await initializeController();
 
 
 
136
 
137
- // Step 1: Homing
138
- setStatus("📍 Setting homing position...");
139
- await ctrl.performHomingStep();
140
 
141
- return ctrl;
142
  } catch (error) {
143
- setIsCalibrating(false);
144
- throw error;
 
 
145
  }
146
- }, [initializeController]);
147
-
148
- // Start range recording
149
- const startRangeRecording = useCallback(
150
- async (
151
- controllerToUse: WebCalibrationController,
152
- stopFunction: () => boolean,
153
- onUpdate?: (
154
- mins: { [motor: string]: number },
155
- maxes: { [motor: string]: number },
156
- currentPositions: { [motor: string]: number }
157
- ) => void
158
- ) => {
159
- if (!controllerToUse) {
160
- throw new Error("Controller not provided");
161
- }
162
-
163
- setStatus(
164
- "📏 Recording joint ranges - move all joints through their full range"
165
- );
166
- setIsRecordingRanges(true);
167
-
168
- try {
169
- await controllerToUse.performRangeRecordingStep(
170
- stopFunction,
171
- (rangeMins, rangeMaxes, currentPositions) => {
172
- setStatus("📏 Recording joint ranges - capturing data...");
173
-
174
- // Update motor data with CURRENT LIVE POSITIONS (not averages!)
175
- const currentPositionsArray = motorNames.map(
176
- (name) => currentPositions[name]
177
- );
178
- updateMotorData(currentPositionsArray, rangeMins, rangeMaxes);
179
-
180
- if (onUpdate) {
181
- onUpdate(rangeMins, rangeMaxes, currentPositions);
182
- }
183
- }
184
- );
185
- } finally {
186
- setIsRecordingRanges(false);
187
- }
188
- },
189
- [motorNames, updateMotorData]
190
- );
191
-
192
- // Finish calibration
193
- const finishCalibration = useCallback(
194
- async (
195
- controllerToUse?: WebCalibrationController,
196
- recordingCount?: number
197
- ) => {
198
- const ctrl = controllerToUse || controller;
199
- if (!ctrl) {
200
- throw new Error("Controller not initialized");
201
- }
202
-
203
- setStatus("💾 Finishing calibration...");
204
- const result = await ctrl.finishCalibration();
205
- setCalibrationResult(result);
206
-
207
- // Save results using serial number for dashboard detection
208
- // Use the same serial number logic as dashboard: prefer main serialNumber, fallback to USB metadata, then "unknown"
209
- const serialNumber =
210
- robot.serialNumber || robot.usbMetadata?.serialNumber || "unknown";
211
-
212
- console.log("🔍 Debug - Saving calibration with:", {
213
- robotType: robot.robotType,
214
- robotId: robot.robotId,
215
- mainSerialNumber: robot.serialNumber,
216
- usbSerialNumber: robot.usbMetadata?.serialNumber,
217
- finalSerialNumber: serialNumber,
218
- storageKey: `lerobotjs-${serialNumber}`,
219
- });
220
-
221
- await saveCalibrationResults(
222
- result,
223
- robot.robotType!,
224
- robot.robotId || `${robot.robotType}_1`,
225
- serialNumber,
226
- recordingCount || 0
227
- );
228
-
229
- // Update final motor data
230
- const finalData = motorNames.map((motorName) => {
231
- const motorResult = result[motorName];
232
- return {
233
- name: motorName,
234
- current: (motorResult.range_min + motorResult.range_max) / 2,
235
- min: motorResult.range_min,
236
- max: motorResult.range_max,
237
- range: motorResult.range_max - motorResult.range_min,
238
- };
239
- });
240
 
241
- setMotorData(finalData);
242
- setStatus("✅ Calibration completed successfully! Configuration saved.");
243
- setIsCalibrating(false);
244
-
245
- return result;
246
- },
247
- [controller, robot.robotType, robot.robotId, motorNames]
248
- );
249
-
250
- // Reset states
251
- const reset = useCallback(() => {
252
- setController(null);
253
- setIsCalibrating(false);
254
- setIsRecordingRanges(false);
255
- setCalibrationResult(null);
256
- setStatus("Ready to calibrate");
257
- initializeMotorData();
258
- }, [initializeMotorData]);
259
-
260
- // Initialize on mount
261
- useEffect(() => {
262
- initializeMotorData();
263
- }, [initializeMotorData]);
264
-
265
- return {
266
- // State
267
- controller,
268
- isCalibrating,
269
- isRecordingRanges,
270
- calibrationResult,
271
- status,
272
- motorData,
273
-
274
- // Actions
275
- startCalibration,
276
- startRangeRecording,
277
- finishCalibration,
278
- readMotorPositions,
279
- reset,
280
-
281
- // Utilities
282
- updateMotorData,
283
- };
284
- }
285
-
286
- export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
287
- const {
288
- controller,
289
- isCalibrating,
290
- isRecordingRanges,
291
- calibrationResult,
292
- status,
293
- motorData,
294
- startCalibration,
295
- startRangeRecording,
296
- finishCalibration,
297
- readMotorPositions,
298
- reset,
299
- updateMotorData,
300
- } = useCalibration(robot);
301
-
302
- // Modal state
303
- const [modalOpen, setModalOpen] = useState(false);
304
-
305
- // Recording state
306
- const [stopRecordingFunction, setStopRecordingFunction] = useState<
307
- (() => void) | null
308
- >(null);
309
-
310
- // Motor names matching Python lerobot exactly (NOT Node CLI)
311
- const motorNames = [
312
- "shoulder_pan",
313
- "shoulder_lift",
314
- "elbow_flex",
315
- "wrist_flex",
316
- "wrist_roll",
317
- "gripper",
318
- ];
319
-
320
- // Motor IDs for SO-100 (STS3215 servos)
321
- const motorIds = [1, 2, 3, 4, 5, 6];
322
-
323
- // Keep track of last known good positions to avoid glitches
324
- const lastKnownPositions = useRef<number[]>([
325
- 2047, 2047, 2047, 2047, 2047, 2047,
326
- ]);
327
-
328
- // NO concurrent motor reading - let the calibration hook handle all serial operations
329
-
330
- const handleContinueCalibration = async () => {
331
  setModalOpen(false);
332
 
333
  if (!robot.port || !robot.robotType) {
@@ -335,76 +100,119 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
335
  }
336
 
337
  try {
338
- const ctrl = await startCalibration();
339
-
340
- // Set up manual control - user decides when to stop
341
- let shouldStopRecording = false;
342
- let recordingCount = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
- // Create stop function and store it in state for the button
345
- const stopRecording = () => {
346
- shouldStopRecording = true;
347
- };
348
- setStopRecordingFunction(() => stopRecording);
349
 
350
- // Add Enter key listener
351
  const handleKeyPress = (event: KeyboardEvent) => {
352
  if (event.key === "Enter") {
353
- shouldStopRecording = true;
354
  }
355
  };
356
-
357
  document.addEventListener("keydown", handleKeyPress);
358
 
359
  try {
360
- await startRangeRecording(
361
- ctrl,
362
- () => {
363
- return shouldStopRecording;
364
- },
365
- (rangeMins, rangeMaxes, currentPositions) => {
366
- recordingCount++;
367
- }
 
 
 
 
 
 
 
 
368
  );
369
  } finally {
370
  document.removeEventListener("keydown", handleKeyPress);
371
- setStopRecordingFunction(null);
 
372
  }
373
-
374
- // Step 3: Finish calibration with recording count
375
- await finishCalibration(ctrl, recordingCount);
376
  } catch (error) {
377
  console.error("❌ Calibration failed:", error);
 
 
 
 
 
 
 
378
  }
379
- };
380
 
381
- // Generate calibration config JSON matching Python lerobot format (OBJECT format, not arrays)
382
- const generateConfigJSON = () => {
383
- // Use the calibration result if available
384
- if (calibrationResult) {
385
- return calibrationResult;
386
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
- // Fallback: generate from motor data (shouldn't happen with new flow)
389
- const calibrationData: any = {};
390
- motorNames.forEach((motorName, index) => {
391
- const motor = motorData[index];
392
- calibrationData[motorName] = {
393
- homing_offset: motor.current - 2047, // Center offset for STS3215 (4095/2)
394
- drive_mode: 0, // Python lerobot SO-100 uses drive_mode 0
395
- start_pos: motor.min,
396
- end_pos: motor.max,
397
- calib_mode: "middle", // Python lerobot SO-100 standard
398
  };
399
- });
400
 
401
- return calibrationData;
 
 
 
402
  };
403
 
404
- // Download calibration config as JSON file
405
- const downloadConfigJSON = () => {
406
- const configData = generateConfigJSON();
407
- const jsonString = JSON.stringify(configData, null, 2);
 
408
  const blob = new Blob([jsonString], { type: "application/json" });
409
  const url = URL.createObjectURL(blob);
410
 
@@ -415,7 +223,7 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
415
  link.click();
416
  document.body.removeChild(link);
417
  URL.revokeObjectURL(url);
418
- };
419
 
420
  return (
421
  <div className="space-y-4">
@@ -455,27 +263,29 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
455
  <p className="text-sm text-blue-800">{status}</p>
456
  {isCalibrating && (
457
  <p className="text-xs text-blue-600 mt-1">
458
- Move joints through full range | Press "Finish Recording" when
459
- done
460
  </p>
461
  )}
462
  </div>
463
 
464
  <div className="flex gap-2">
465
  {!isCalibrating && !calibrationResult && (
466
- <Button onClick={() => setModalOpen(true)}>
467
- Start Calibration
468
- </Button>
469
- )}
470
-
471
- {isCalibrating && !isRecordingRanges && (
472
- <Button onClick={finishCalibration} variant="outline">
473
- Finish Calibration
 
 
474
  </Button>
475
  )}
476
 
477
- {isRecordingRanges && stopRecordingFunction && (
478
- <Button onClick={stopRecordingFunction} variant="default">
479
  Finish Recording
480
  </Button>
481
  )}
@@ -507,7 +317,7 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
507
  <CardContent>
508
  <div className="space-y-3">
509
  <pre className="bg-gray-100 p-4 rounded-lg text-sm overflow-x-auto border">
510
- <code>{JSON.stringify(generateConfigJSON(), null, 2)}</code>
511
  </pre>
512
  <div className="flex gap-2">
513
  <Button onClick={downloadConfigJSON} variant="outline">
@@ -516,7 +326,7 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
516
  <Button
517
  onClick={() => {
518
  navigator.clipboard.writeText(
519
- JSON.stringify(generateConfigJSON(), null, 2)
520
  );
521
  }}
522
  variant="outline"
@@ -529,12 +339,12 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
529
  </Card>
530
  )}
531
 
532
- {/* Live Position Recording Table (matching Node CLI exactly) */}
533
  <Card>
534
  <CardHeader>
535
  <CardTitle className="text-lg">Live Position Recording</CardTitle>
536
  <CardDescription>
537
- Real-time motor position feedback - exactly like Node CLI
538
  </CardDescription>
539
  </CardHeader>
540
  <CardContent>
@@ -560,28 +370,39 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
560
  </tr>
561
  </thead>
562
  <tbody className="divide-y divide-gray-200">
563
- {motorData.map((motor, index) => (
564
- <tr key={index} className="hover:bg-gray-50">
565
- <td className="px-4 py-2 font-medium flex items-center gap-2">
566
- {motor.name}
567
- {motor.range > 100 && (
568
- <span className="text-green-600 text-xs">✓</span>
569
- )}
570
- </td>
571
- <td className="px-4 py-2 text-right">{motor.current}</td>
572
- <td className="px-4 py-2 text-right">{motor.min}</td>
573
- <td className="px-4 py-2 text-right">{motor.max}</td>
574
- <td className="px-4 py-2 text-right font-medium">
575
- <span
576
- className={
577
- motor.range > 100 ? "text-green-600" : "text-gray-500"
578
- }
579
- >
580
- {motor.range}
581
- </span>
582
- </td>
583
- </tr>
584
- ))}
 
 
 
 
 
 
 
 
 
 
 
585
  </tbody>
586
  </table>
587
  </div>
 
1
+ import React, { useState, useCallback, useMemo } from "react";
 
 
 
 
 
 
2
  import { Button } from "./ui/button";
3
  import {
4
  Card,
 
9
  } from "./ui/card";
10
  import { Badge } from "./ui/badge";
11
  import {
12
+ calibrate,
 
 
13
  type WebCalibrationResults,
14
+ type LiveCalibrationData,
15
+ type CalibrationProcess,
16
  } from "../../lerobot/web/calibrate";
17
+ 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/find_port.js";
22
 
23
  interface CalibrationPanelProps {
24
+ robot: RobotConnection;
25
  onFinish: () => void;
26
  }
27
 
28
+ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
29
+ // Simple state management
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  const [isCalibrating, setIsCalibrating] = useState(false);
 
31
  const [calibrationResult, setCalibrationResult] =
32
  useState<WebCalibrationResults | null>(null);
33
  const [status, setStatus] = useState<string>("Ready to calibrate");
34
+ const [modalOpen, setModalOpen] = useState(false);
35
+ const [calibrationProcess, setCalibrationProcess] =
36
+ useState<CalibrationProcess | null>(null);
37
+ const [motorData, setMotorData] = useState<LiveCalibrationData>({});
38
+ const [isPreparing, setIsPreparing] = useState(false);
39
 
40
+ // Motor names for display
 
 
 
41
  const motorNames = useMemo(
42
  () => [
43
  "shoulder_pan",
 
50
  []
51
  );
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  // Initialize motor data
54
  const initializeMotorData = useCallback(() => {
55
+ const initialData: LiveCalibrationData = {};
56
+ motorNames.forEach((name) => {
57
+ initialData[name] = {
58
+ current: 2047,
59
+ min: 2047,
60
+ max: 2047,
61
+ range: 0,
62
+ };
63
+ });
64
  setMotorData(initialData);
65
  }, [motorNames]);
66
 
67
+ // Release motor torque for better UX - allows immediate joint movement
68
+ const releaseMotorTorque = useCallback(async () => {
69
+ if (!robot.port || !robot.robotType) {
70
+ return;
71
+ }
72
+
73
  try {
74
+ setIsPreparing(true);
75
+ setStatus("🔓 Releasing motor torque - joints can now be moved freely");
76
 
77
+ // Create port wrapper and config to get motor IDs
78
+ const port = new WebSerialPortWrapper(robot.port);
79
+ await port.initialize();
80
+ const config = createSO100Config(robot.robotType);
81
 
82
+ // Release motors so they can be moved freely by hand
83
+ await releaseMotors(port, config.motorIds);
 
84
 
85
+ setStatus("✅ Joints are now free to move - set your homing position");
86
  } catch (error) {
87
+ console.warn("Failed to release motor torque:", error);
88
+ setStatus("⚠️ Could not release motor torque - try moving joints gently");
89
+ } finally {
90
+ setIsPreparing(false);
91
  }
92
+ }, [robot]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ // Start calibration using new API
95
+ const handleContinueCalibration = useCallback(async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  setModalOpen(false);
97
 
98
  if (!robot.port || !robot.robotType) {
 
100
  }
101
 
102
  try {
103
+ setStatus("🤖 Starting calibration process...");
104
+ setIsCalibrating(true);
105
+ initializeMotorData();
106
+
107
+ // Use the new unified calibrate API - pass the whole robot connection
108
+ const robotConnection = {
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(
120
+ "📏 Recording joint ranges - move all joints through their full range"
121
+ );
122
+ },
123
+ onProgress: (message) => {
124
+ setStatus(message);
125
+ },
126
+ });
127
 
128
+ setCalibrationProcess(process);
 
 
 
 
129
 
130
+ // Add Enter key listener for stopping (matching Node.js UX)
131
  const handleKeyPress = (event: KeyboardEvent) => {
132
  if (event.key === "Enter") {
133
+ process.stop();
134
  }
135
  };
 
136
  document.addEventListener("keydown", handleKeyPress);
137
 
138
  try {
139
+ // Wait for calibration to complete
140
+ const result = await process.result;
141
+ setCalibrationResult(result);
142
+
143
+ // App-level concern: Save results to storage
144
+ const serialNumber =
145
+ robot.serialNumber || robot.usbMetadata?.serialNumber || "unknown";
146
+ await saveCalibrationResults(
147
+ result,
148
+ robot.robotType,
149
+ robot.robotId || `${robot.robotType}_1`,
150
+ serialNumber
151
+ );
152
+
153
+ setStatus(
154
+ "✅ Calibration completed successfully! Configuration saved."
155
  );
156
  } finally {
157
  document.removeEventListener("keydown", handleKeyPress);
158
+ setCalibrationProcess(null);
159
+ setIsCalibrating(false);
160
  }
 
 
 
161
  } catch (error) {
162
  console.error("❌ Calibration failed:", error);
163
+ setStatus(
164
+ `❌ Calibration failed: ${
165
+ error instanceof Error ? error.message : error
166
+ }`
167
+ );
168
+ setIsCalibrating(false);
169
+ setCalibrationProcess(null);
170
  }
171
+ }, [robot, initializeMotorData]);
172
 
173
+ // Stop calibration recording
174
+ const handleStopRecording = useCallback(() => {
175
+ if (calibrationProcess) {
176
+ calibrationProcess.stop();
 
177
  }
178
+ }, [calibrationProcess]);
179
+
180
+ // App-level concern: Save calibration results
181
+ const saveCalibrationResults = async (
182
+ results: WebCalibrationResults,
183
+ robotType: string,
184
+ robotId: string,
185
+ serialNumber: string
186
+ ) => {
187
+ try {
188
+ // Save to unified storage (app-level functionality)
189
+ const { saveCalibrationData } = await import("../lib/unified-storage.js");
190
+
191
+ const fullCalibrationData = {
192
+ ...results,
193
+ device_type: robotType,
194
+ device_id: robotId,
195
+ calibrated_at: new Date().toISOString(),
196
+ platform: "web",
197
+ api: "Web Serial API",
198
+ };
199
 
200
+ const metadata = {
201
+ timestamp: new Date().toISOString(),
202
+ readCount: Object.keys(motorData).length > 0 ? 100 : 0, // Estimate
 
 
 
 
 
 
 
203
  };
 
204
 
205
+ saveCalibrationData(serialNumber, fullCalibrationData, metadata);
206
+ } catch (error) {
207
+ console.warn("Failed to save calibration results:", error);
208
+ }
209
  };
210
 
211
+ // App-level concern: JSON export functionality
212
+ const downloadConfigJSON = useCallback(() => {
213
+ if (!calibrationResult) return;
214
+
215
+ const jsonString = JSON.stringify(calibrationResult, null, 2);
216
  const blob = new Blob([jsonString], { type: "application/json" });
217
  const url = URL.createObjectURL(blob);
218
 
 
223
  link.click();
224
  document.body.removeChild(link);
225
  URL.revokeObjectURL(url);
226
+ }, [calibrationResult, robot.robotId, robot.robotType]);
227
 
228
  return (
229
  <div className="space-y-4">
 
263
  <p className="text-sm text-blue-800">{status}</p>
264
  {isCalibrating && (
265
  <p className="text-xs text-blue-600 mt-1">
266
+ Move joints through full range | Press "Finish Recording" or
267
+ Enter key when done
268
  </p>
269
  )}
270
  </div>
271
 
272
  <div className="flex gap-2">
273
  {!isCalibrating && !calibrationResult && (
274
+ <Button
275
+ onClick={async () => {
276
+ // Release motor torque FIRST - so user can move joints immediately
277
+ await releaseMotorTorque();
278
+ // THEN open modal - user can now follow instructions right away
279
+ setModalOpen(true);
280
+ }}
281
+ disabled={isPreparing}
282
+ >
283
+ {isPreparing ? "Preparing..." : "Start Calibration"}
284
  </Button>
285
  )}
286
 
287
+ {isCalibrating && calibrationProcess && (
288
+ <Button onClick={handleStopRecording} variant="default">
289
  Finish Recording
290
  </Button>
291
  )}
 
317
  <CardContent>
318
  <div className="space-y-3">
319
  <pre className="bg-gray-100 p-4 rounded-lg text-sm overflow-x-auto border">
320
+ <code>{JSON.stringify(calibrationResult, null, 2)}</code>
321
  </pre>
322
  <div className="flex gap-2">
323
  <Button onClick={downloadConfigJSON} variant="outline">
 
326
  <Button
327
  onClick={() => {
328
  navigator.clipboard.writeText(
329
+ JSON.stringify(calibrationResult, null, 2)
330
  );
331
  }}
332
  variant="outline"
 
339
  </Card>
340
  )}
341
 
342
+ {/* Live Position Recording Table */}
343
  <Card>
344
  <CardHeader>
345
  <CardTitle className="text-lg">Live Position Recording</CardTitle>
346
  <CardDescription>
347
+ Real-time motor position feedback during calibration
348
  </CardDescription>
349
  </CardHeader>
350
  <CardContent>
 
370
  </tr>
371
  </thead>
372
  <tbody className="divide-y divide-gray-200">
373
+ {motorNames.map((motorName) => {
374
+ const motor = motorData[motorName] || {
375
+ current: 2047,
376
+ min: 2047,
377
+ max: 2047,
378
+ range: 0,
379
+ };
380
+
381
+ return (
382
+ <tr key={motorName} className="hover:bg-gray-50">
383
+ <td className="px-4 py-2 font-medium flex items-center gap-2">
384
+ {motorName}
385
+ {motor.range > 100 && (
386
+ <span className="text-green-600 text-xs">✓</span>
387
+ )}
388
+ </td>
389
+ <td className="px-4 py-2 text-right">{motor.current}</td>
390
+ <td className="px-4 py-2 text-right">{motor.min}</td>
391
+ <td className="px-4 py-2 text-right">{motor.max}</td>
392
+ <td className="px-4 py-2 text-right font-medium">
393
+ <span
394
+ className={
395
+ motor.range > 100
396
+ ? "text-green-600"
397
+ : "text-gray-500"
398
+ }
399
+ >
400
+ {motor.range}
401
+ </span>
402
+ </td>
403
+ </tr>
404
+ );
405
+ })}
406
  </tbody>
407
  </table>
408
  </div>
src/demo/components/PortManager.tsx CHANGED
@@ -18,17 +18,31 @@ import {
18
  DialogTitle,
19
  } from "./ui/dialog";
20
  import { isWebSerialSupported } from "../../lerobot/web/calibrate";
21
- import type { ConnectedRobot } from "../types";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  interface PortManagerProps {
24
- connectedRobots: ConnectedRobot[];
25
- onConnectedRobotsChange: (robots: ConnectedRobot[]) => void;
26
  onCalibrate?: (
27
  port: SerialPort,
28
  robotType: "so100_follower" | "so100_leader",
29
  robotId: string
30
  ) => void;
31
- onTeleoperate?: (robot: ConnectedRobot) => void;
32
  }
33
 
34
  export function PortManager({
@@ -62,7 +76,7 @@ export function PortManager({
62
  const loadSavedPorts = async () => {
63
  try {
64
  const existingPorts = await navigator.serial.getPorts();
65
- const restoredPorts: ConnectedRobot[] = [];
66
 
67
  for (const port of existingPorts) {
68
  // Get USB device metadata to determine serial number
@@ -297,7 +311,7 @@ export function PortManager({
297
 
298
  if (existingIndex === -1) {
299
  // New robot - add to list
300
- const newRobot: ConnectedRobot = {
301
  port,
302
  name: portName,
303
  isConnected: true,
@@ -489,47 +503,35 @@ export function PortManager({
489
  setFindPortsLog([]);
490
  setError(null);
491
 
492
- // Get initial ports
493
- const initialPorts = await navigator.serial.getPorts();
494
- setFindPortsLog((prev) => [
495
- ...prev,
496
- `Found ${initialPorts.length} existing paired port(s)`,
497
- ]);
498
-
499
- // Ask user to disconnect
500
- setFindPortsLog((prev) => [
501
- ...prev,
502
- "Please disconnect the USB cable from your robot and click OK",
503
- ]);
504
-
505
- // Simple implementation - just show the instruction
506
- // In a real implementation, we'd monitor port changes
507
- const confirmed = confirm(
508
- "Disconnect the USB cable from your robot and click OK when done"
509
- );
510
 
511
- if (confirmed) {
512
- setFindPortsLog((prev) => [...prev, "Reconnect the USB cable now"]);
 
 
 
513
 
514
- // Request port selection
515
- const port = await navigator.serial.requestPort();
516
- await port.open({ baudRate: 1000000 });
517
 
518
- const portName = getPortDisplayName(port);
519
- setFindPortsLog((prev) => [...prev, `Identified port: ${portName}`]);
520
 
521
- // Add to connected ports if not already there
522
- const existingIndex = connectedRobots.findIndex(
523
- (p) => p.name === portName
524
- );
525
- if (existingIndex === -1) {
526
- const newPort: ConnectedRobot = {
527
- port,
528
- name: portName,
529
- isConnected: true,
530
- };
531
- onConnectedRobotsChange([...connectedRobots, newPort]);
532
- }
 
 
533
  }
534
  } catch (error) {
535
  if (
@@ -574,7 +576,7 @@ export function PortManager({
574
  }
575
  };
576
 
577
- const handleCalibrate = async (port: ConnectedRobot) => {
578
  if (!port.robotType || !port.robotId) {
579
  setError("Please set robot type and ID before calibrating");
580
  return;
@@ -738,7 +740,7 @@ export function PortManager({
738
  }
739
 
740
  interface PortCardProps {
741
- portInfo: ConnectedRobot;
742
  onDisconnect: () => void;
743
  onUpdateInfo: (
744
  robotType: "so100_follower" | "so100_leader",
 
18
  DialogTitle,
19
  } from "./ui/dialog";
20
  import { isWebSerialSupported } from "../../lerobot/web/calibrate";
21
+ import type { RobotConnection } from "../../lerobot/web/find_port.js";
22
+
23
+ /**
24
+ * Type definitions for WebSerial API (missing from TypeScript)
25
+ */
26
+ interface SerialPortInfo {
27
+ usbVendorId?: number;
28
+ usbProductId?: number;
29
+ }
30
+
31
+ declare global {
32
+ interface SerialPort {
33
+ getInfo(): SerialPortInfo;
34
+ }
35
+ }
36
 
37
  interface PortManagerProps {
38
+ connectedRobots: RobotConnection[];
39
+ onConnectedRobotsChange: (robots: RobotConnection[]) => void;
40
  onCalibrate?: (
41
  port: SerialPort,
42
  robotType: "so100_follower" | "so100_leader",
43
  robotId: string
44
  ) => void;
45
+ onTeleoperate?: (robot: RobotConnection) => void;
46
  }
47
 
48
  export function PortManager({
 
76
  const loadSavedPorts = async () => {
77
  try {
78
  const existingPorts = await navigator.serial.getPorts();
79
+ const restoredPorts: RobotConnection[] = [];
80
 
81
  for (const port of existingPorts) {
82
  // Get USB device metadata to determine serial number
 
311
 
312
  if (existingIndex === -1) {
313
  // New robot - add to list
314
+ const newRobot: RobotConnection = {
315
  port,
316
  name: portName,
317
  isConnected: true,
 
503
  setFindPortsLog([]);
504
  setError(null);
505
 
506
+ // Use the new findPort API from standard library
507
+ const { findPort } = await import("../../lerobot/web/find_port.js");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
+ const findPortProcess = await findPort({
510
+ onMessage: (message) => {
511
+ setFindPortsLog((prev) => [...prev, message]);
512
+ },
513
+ });
514
 
515
+ const robotConnections = (await findPortProcess.result) as any; // RobotConnection[] from findPort
516
+ const robotConnection = robotConnections[0]; // Get first robot from array
 
517
 
518
+ const portName = getPortDisplayName(robotConnection.port);
519
+ setFindPortsLog((prev) => [...prev, `✅ Port ready: ${portName}`]);
520
 
521
+ // Add to connected ports if not already there
522
+ const existingIndex = connectedRobots.findIndex(
523
+ (p) => p.name === portName
524
+ );
525
+ if (existingIndex === -1) {
526
+ const newPort: RobotConnection = {
527
+ port: robotConnection.port,
528
+ name: portName,
529
+ isConnected: true,
530
+ robotType: robotConnection.robotType,
531
+ robotId: robotConnection.robotId,
532
+ serialNumber: robotConnection.serialNumber,
533
+ };
534
+ onConnectedRobotsChange([...connectedRobots, newPort]);
535
  }
536
  } catch (error) {
537
  if (
 
576
  }
577
  };
578
 
579
+ const handleCalibrate = async (port: RobotConnection) => {
580
  if (!port.robotType || !port.robotId) {
581
  setError("Please set robot type and ID before calibrating");
582
  return;
 
740
  }
741
 
742
  interface PortCardProps {
743
+ portInfo: RobotConnection;
744
  onDisconnect: () => void;
745
  onUpdateInfo: (
746
  robotType: "so100_follower" | "so100_leader",
src/demo/components/TeleoperationPanel.tsx CHANGED
@@ -5,11 +5,11 @@ import { Badge } from "./ui/badge";
5
  import { Alert, AlertDescription } from "./ui/alert";
6
  import { Progress } from "./ui/progress";
7
  import { useTeleoperation } from "../hooks/useTeleoperation";
8
- import type { ConnectedRobot } from "../types";
9
  import { KEYBOARD_CONTROLS } from "../../lerobot/web/teleoperate";
10
 
11
  interface TeleoperationPanelProps {
12
- robot: ConnectedRobot;
13
  onClose: () => void;
14
  }
15
 
@@ -27,9 +27,9 @@ export function TeleoperationPanel({
27
  error,
28
  start,
29
  stop,
30
- goToHome,
31
  simulateKeyPress,
32
  simulateKeyRelease,
 
33
  } = useTeleoperation({
34
  robot,
35
  enabled,
@@ -153,7 +153,7 @@ export function TeleoperationPanel({
153
  </div>
154
  </div>
155
 
156
- {/* Q/E and Space */}
157
  <div className="flex justify-center gap-2">
158
  <div className="text-center">
159
  <h4 className="text-xs font-semibold mb-2 text-gray-600">Roll</h4>
@@ -170,9 +170,14 @@ export function TeleoperationPanel({
170
  <h4 className="text-xs font-semibold mb-2 text-gray-600">
171
  Gripper
172
  </h4>
173
- <KeyButton keyCode=" " size="sm" className="min-w-16">
174
-
175
- </KeyButton>
 
 
 
 
 
176
  </div>
177
  </div>
178
 
@@ -261,15 +266,6 @@ export function TeleoperationPanel({
261
  ▶️ Start Teleoperation
262
  </Button>
263
  )}
264
-
265
- <Button
266
- onClick={goToHome}
267
- variant="outline"
268
- disabled={!isConnected}
269
- className="w-full"
270
- >
271
- 🏠 Go to Home
272
- </Button>
273
  </div>
274
  </CardContent>
275
  </Card>
@@ -290,11 +286,7 @@ export function TeleoperationPanel({
290
  <CardTitle>Motor Positions</CardTitle>
291
  </CardHeader>
292
  <CardContent className="space-y-3">
293
- {motorConfigs.map((motor) => {
294
- const range = motor.maxPosition - motor.minPosition;
295
- const position = motor.currentPosition - motor.minPosition;
296
- const percentage = range > 0 ? (position / range) * 100 : 0;
297
-
298
  return (
299
  <div key={motor.name} className="space-y-1">
300
  <div className="flex justify-between items-center">
@@ -305,7 +297,41 @@ export function TeleoperationPanel({
305
  {motor.currentPosition}
306
  </span>
307
  </div>
308
- <Progress value={percentage} className="h-2" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  <div className="flex justify-between text-xs text-gray-400">
310
  <span>{motor.minPosition}</span>
311
  <span>{motor.maxPosition}</span>
@@ -342,7 +368,8 @@ export function TeleoperationPanel({
342
  <h4 className="font-semibold mb-2">Other Keys</h4>
343
  <ul className="space-y-1 text-gray-600">
344
  <li>Q E Wrist roll</li>
345
- <li>Space Gripper</li>
 
346
  </ul>
347
  </div>
348
  <div>
 
5
  import { Alert, AlertDescription } from "./ui/alert";
6
  import { Progress } from "./ui/progress";
7
  import { useTeleoperation } from "../hooks/useTeleoperation";
8
+ import type { RobotConnection } from "../../lerobot/web/find_port.js";
9
  import { KEYBOARD_CONTROLS } from "../../lerobot/web/teleoperate";
10
 
11
  interface TeleoperationPanelProps {
12
+ robot: RobotConnection;
13
  onClose: () => void;
14
  }
15
 
 
27
  error,
28
  start,
29
  stop,
 
30
  simulateKeyPress,
31
  simulateKeyRelease,
32
+ moveMotorToPosition,
33
  } = useTeleoperation({
34
  robot,
35
  enabled,
 
153
  </div>
154
  </div>
155
 
156
+ {/* Q/E and Gripper */}
157
  <div className="flex justify-center gap-2">
158
  <div className="text-center">
159
  <h4 className="text-xs font-semibold mb-2 text-gray-600">Roll</h4>
 
170
  <h4 className="text-xs font-semibold mb-2 text-gray-600">
171
  Gripper
172
  </h4>
173
+ <div className="flex gap-1">
174
+ <KeyButton keyCode="o" size="sm">
175
+ O
176
+ </KeyButton>
177
+ <KeyButton keyCode="c" size="sm">
178
+ C
179
+ </KeyButton>
180
+ </div>
181
  </div>
182
  </div>
183
 
 
266
  ▶️ Start Teleoperation
267
  </Button>
268
  )}
 
 
 
 
 
 
 
 
 
269
  </div>
270
  </CardContent>
271
  </Card>
 
286
  <CardTitle>Motor Positions</CardTitle>
287
  </CardHeader>
288
  <CardContent className="space-y-3">
289
+ {motorConfigs.map((motor, index) => {
 
 
 
 
290
  return (
291
  <div key={motor.name} className="space-y-1">
292
  <div className="flex justify-between items-center">
 
297
  {motor.currentPosition}
298
  </span>
299
  </div>
300
+ <input
301
+ type="range"
302
+ min={motor.minPosition}
303
+ max={motor.maxPosition}
304
+ value={motor.currentPosition}
305
+ disabled={!isActive}
306
+ className={`w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200 slider-thumb ${
307
+ !isActive ? "opacity-50 cursor-not-allowed" : ""
308
+ }`}
309
+ style={{
310
+ background: isActive
311
+ ? `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${
312
+ ((motor.currentPosition - motor.minPosition) /
313
+ (motor.maxPosition - motor.minPosition)) *
314
+ 100
315
+ }%, #e5e7eb ${
316
+ ((motor.currentPosition - motor.minPosition) /
317
+ (motor.maxPosition - motor.minPosition)) *
318
+ 100
319
+ }%, #e5e7eb 100%)`
320
+ : "#e5e7eb",
321
+ }}
322
+ onChange={async (e) => {
323
+ if (!isActive) return;
324
+ const newPosition = parseInt(e.target.value);
325
+ try {
326
+ await moveMotorToPosition(index, newPosition);
327
+ } catch (error) {
328
+ console.warn(
329
+ "Failed to move motor via slider:",
330
+ error
331
+ );
332
+ }
333
+ }}
334
+ />
335
  <div className="flex justify-between text-xs text-gray-400">
336
  <span>{motor.minPosition}</span>
337
  <span>{motor.maxPosition}</span>
 
368
  <h4 className="font-semibold mb-2">Other Keys</h4>
369
  <ul className="space-y-1 text-gray-600">
370
  <li>Q E Wrist roll</li>
371
+ <li>O Open gripper</li>
372
+ <li>C Close gripper</li>
373
  </ul>
374
  </div>
375
  <div>
src/demo/hooks/useTeleoperation.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
  import { useRobotConnection } from "./useRobotConnection";
3
  import { getUnifiedRobotData } from "../lib/unified-storage";
4
- import type { ConnectedRobot } from "../types";
5
 
6
  export interface MotorConfig {
7
  name: string;
@@ -17,7 +17,7 @@ export interface KeyState {
17
  }
18
 
19
  export interface UseTeleoperationOptions {
20
- robot: ConnectedRobot;
21
  enabled: boolean;
22
  onError?: (error: string) => void;
23
  }
@@ -42,6 +42,7 @@ export interface UseTeleoperationResult {
42
  goToHome: () => Promise<void>;
43
  simulateKeyPress: (key: string) => void;
44
  simulateKeyRelease: (key: string) => void;
 
45
  }
46
 
47
  const MOTOR_CONFIGS: MotorConfig[] = [
@@ -116,7 +117,8 @@ const KEYBOARD_CONTROLS = {
116
  d: { motorIndex: 3, direction: 1, description: "Wrist flex right" },
117
  q: { motorIndex: 4, direction: -1, description: "Wrist roll left" },
118
  e: { motorIndex: 4, direction: 1, description: "Wrist roll right" },
119
- " ": { motorIndex: 5, direction: 1, description: "Gripper open/close" },
 
120
  Escape: { motorIndex: -1, direction: 0, description: "Emergency stop" },
121
  };
122
 
@@ -471,6 +473,35 @@ export function useTeleoperation({
471
  [isActive]
472
  );
473
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  return {
475
  isConnected: connection.isConnected,
476
  isActive,
@@ -482,5 +513,6 @@ export function useTeleoperation({
482
  goToHome,
483
  simulateKeyPress,
484
  simulateKeyRelease,
 
485
  };
486
  }
 
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;
 
17
  }
18
 
19
  export interface UseTeleoperationOptions {
20
+ robot: RobotConnection;
21
  enabled: boolean;
22
  onError?: (error: string) => void;
23
  }
 
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[] = [
 
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
 
 
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,
 
513
  goToHome,
514
  simulateKeyPress,
515
  simulateKeyRelease,
516
+ moveMotorToPosition,
517
  };
518
  }
src/demo/pages/Home.tsx CHANGED
@@ -12,12 +12,12 @@ 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 { ConnectedRobot } from "../types";
16
 
17
  interface HomeProps {
18
  onGetStarted: () => void;
19
- connectedRobots: ConnectedRobot[];
20
- onConnectedRobotsChange: (robots: ConnectedRobot[]) => void;
21
  }
22
 
23
  export function Home({
@@ -26,9 +26,9 @@ export function Home({
26
  onConnectedRobotsChange,
27
  }: HomeProps) {
28
  const [calibratingRobot, setCalibratingRobot] =
29
- useState<ConnectedRobot | null>(null);
30
  const [teleoperatingRobot, setTeleoperatingRobot] =
31
- useState<ConnectedRobot | null>(null);
32
  const isSupported = isWebSerialSupported();
33
 
34
  const handleCalibrate = (
@@ -43,7 +43,7 @@ export function Home({
43
  }
44
  };
45
 
46
- const handleTeleoperate = (robot: ConnectedRobot) => {
47
  setTeleoperatingRobot(robot);
48
  };
49
 
 
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/find_port.js";
16
 
17
  interface HomeProps {
18
  onGetStarted: () => void;
19
+ connectedRobots: RobotConnection[];
20
+ onConnectedRobotsChange: (robots: RobotConnection[]) => void;
21
  }
22
 
23
  export function Home({
 
26
  onConnectedRobotsChange,
27
  }: HomeProps) {
28
  const [calibratingRobot, setCalibratingRobot] =
29
+ useState<RobotConnection | null>(null);
30
  const [teleoperatingRobot, setTeleoperatingRobot] =
31
+ useState<RobotConnection | null>(null);
32
  const isSupported = isWebSerialSupported();
33
 
34
  const handleCalibrate = (
 
43
  }
44
  };
45
 
46
+ const handleTeleoperate = (robot: RobotConnection) => {
47
  setTeleoperatingRobot(robot);
48
  };
49
 
src/demo/types.ts CHANGED
@@ -1,20 +1,2 @@
1
- export interface ConnectedRobot {
2
- port: SerialPort;
3
- name: string;
4
- isConnected: boolean;
5
- robotType?: "so100_follower" | "so100_leader";
6
- robotId?: string;
7
- serialNumber?: string; // Unique identifier from USB device
8
- usbMetadata?: {
9
- vendorId: string;
10
- productId: string;
11
- serialNumber: string;
12
- manufacturerName: string;
13
- productName: string;
14
- usbVersionMajor?: number;
15
- usbVersionMinor?: number;
16
- deviceClass?: number;
17
- deviceSubclass?: number;
18
- deviceProtocol?: number;
19
- };
20
- }
 
1
+ // Demo uses the standard library RobotConnection interface directly
2
+ // No need for separate types - just import RobotConnection where needed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/web/calibrate.ts CHANGED
@@ -1,13 +1,29 @@
1
  /**
2
  * Web calibration functionality using Web Serial API
3
- * For browser environments - matches Node.js implementation exactly
 
 
 
 
 
4
  */
5
 
6
- import type { CalibrateConfig } from "../node/robots/config.js";
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  /**
9
- * Device-agnostic calibration configuration for web
10
- * Mirrors the Node.js SO100CalibrationConfig exactly
11
  */
12
  interface WebCalibrationConfig {
13
  deviceType: "so100_follower" | "so100_leader";
@@ -15,9 +31,8 @@ interface WebCalibrationConfig {
15
  motorNames: string[];
16
  motorIds: number[];
17
  driveModes: number[];
18
- calibModes: string[];
19
 
20
- // Protocol-specific configuration (matches Node.js exactly)
21
  protocol: {
22
  resolution: number;
23
  homingOffsetAddress: number;
@@ -30,13 +45,6 @@ interface WebCalibrationConfig {
30
  maxPositionLimitLength: number;
31
  signMagnitudeBit: number;
32
  };
33
-
34
- limits: {
35
- position_min: number[];
36
- position_max: number[];
37
- velocity_max: number[];
38
- torque_max: number[];
39
- };
40
  }
41
 
42
  /**
@@ -53,470 +61,34 @@ export interface WebCalibrationResults {
53
  }
54
 
55
  /**
56
- * STS3215 Protocol Configuration for web (matches Node.js exactly)
57
- */
58
- const WEB_STS3215_PROTOCOL = {
59
- resolution: 4096, // 12-bit resolution (0-4095)
60
- homingOffsetAddress: 31, // Address for Homing_Offset register
61
- homingOffsetLength: 2, // 2 bytes for Homing_Offset
62
- presentPositionAddress: 56, // Address for Present_Position register
63
- presentPositionLength: 2, // 2 bytes for Present_Position
64
- minPositionLimitAddress: 9, // Address for Min_Position_Limit register
65
- minPositionLimitLength: 2, // 2 bytes for Min_Position_Limit
66
- maxPositionLimitAddress: 11, // Address for Max_Position_Limit register
67
- maxPositionLimitLength: 2, // 2 bytes for Max_Position_Limit
68
- signMagnitudeBit: 11, // Bit 11 is sign bit for Homing_Offset encoding
69
- } as const;
70
-
71
- /**
72
- * Sign-magnitude encoding functions (matches Node.js exactly)
73
- */
74
- function encodeSignMagnitude(value: number, signBitIndex: number): number {
75
- const maxMagnitude = (1 << signBitIndex) - 1;
76
- const magnitude = Math.abs(value);
77
-
78
- if (magnitude > maxMagnitude) {
79
- throw new Error(
80
- `Magnitude ${magnitude} exceeds ${maxMagnitude} (max for signBitIndex=${signBitIndex})`
81
- );
82
- }
83
-
84
- const directionBit = value < 0 ? 1 : 0;
85
- return (directionBit << signBitIndex) | magnitude;
86
- }
87
-
88
- /**
89
- * PROPER Web Serial Port wrapper following Chrome documentation exactly
90
- * Direct write/read with immediate lock release - NO persistent connections
91
- */
92
- class WebSerialPortWrapper {
93
- private port: SerialPort;
94
-
95
- constructor(port: SerialPort) {
96
- this.port = port;
97
- }
98
-
99
- get isOpen(): boolean {
100
- return (
101
- this.port !== null &&
102
- this.port.readable !== null &&
103
- this.port.writable !== null
104
- );
105
- }
106
-
107
- async initialize(): Promise<void> {
108
- if (!this.port.readable || !this.port.writable) {
109
- throw new Error("Port is not open for reading/writing");
110
- }
111
- }
112
-
113
- /**
114
- * Write data - EXACTLY like Chrome documentation
115
- * Get writer, write, release lock immediately
116
- */
117
- async write(data: Uint8Array): Promise<void> {
118
- if (!this.port.writable) {
119
- throw new Error("Port not open for writing");
120
- }
121
-
122
- // Write packet to motor
123
-
124
- const writer = this.port.writable.getWriter();
125
- try {
126
- await writer.write(data);
127
- } finally {
128
- writer.releaseLock();
129
- }
130
- }
131
-
132
- /**
133
- * Read data - EXACTLY like Chrome documentation
134
- * Get reader, read once, release lock immediately
135
- */
136
- async read(timeout: number = 1000): Promise<Uint8Array> {
137
- if (!this.port.readable) {
138
- throw new Error("Port not open for reading");
139
- }
140
-
141
- const reader = this.port.readable.getReader();
142
-
143
- try {
144
- // Set up timeout
145
- const timeoutPromise = new Promise<never>((_, reject) => {
146
- setTimeout(() => reject(new Error("Read timeout")), timeout);
147
- });
148
-
149
- // Race between read and timeout
150
- const result = await Promise.race([reader.read(), timeoutPromise]);
151
-
152
- const { value, done } = result;
153
-
154
- if (done || !value) {
155
- throw new Error("Read failed - port closed or no data");
156
- }
157
-
158
- const response = new Uint8Array(value);
159
- return response;
160
- } finally {
161
- reader.releaseLock();
162
- }
163
- }
164
-
165
- async close(): Promise<void> {
166
- // Don't close the port itself - just wrapper cleanup
167
- }
168
- }
169
-
170
- /**
171
- * Read motor positions using device-agnostic configuration (exactly like Node.js)
172
- */
173
- async function readMotorPositions(
174
- config: WebCalibrationConfig
175
- ): Promise<number[]> {
176
- const motorPositions: number[] = [];
177
-
178
- // Reading motor positions
179
-
180
- for (let i = 0; i < config.motorIds.length; i++) {
181
- const motorId = config.motorIds[i];
182
- const motorName = config.motorNames[i];
183
-
184
- try {
185
- // Create Read Position packet using configurable address
186
- const packet = new Uint8Array([
187
- 0xff,
188
- 0xff,
189
- motorId,
190
- 0x04,
191
- 0x02,
192
- config.protocol.presentPositionAddress, // Configurable address
193
- 0x02,
194
- 0x00,
195
- ]);
196
- const checksum =
197
- ~(
198
- motorId +
199
- 0x04 +
200
- 0x02 +
201
- config.protocol.presentPositionAddress +
202
- 0x02
203
- ) & 0xff;
204
- packet[7] = checksum;
205
-
206
- // Professional Feetech communication pattern (based on matthieuvigne/STS_servos)
207
- let attempts = 0;
208
- let success = false;
209
-
210
- while (attempts < 3 && !success) {
211
- attempts++;
212
-
213
- // Clear any remaining data in buffer first (critical for Web Serial)
214
- try {
215
- await config.port.read(0); // Non-blocking read to clear buffer
216
- } catch (e) {
217
- // Expected - buffer was empty
218
- }
219
-
220
- // Write command with proper timing
221
- await config.port.write(packet);
222
-
223
- // Arduino library uses careful timing - Web Serial needs more
224
- await new Promise((resolve) => setTimeout(resolve, 10));
225
-
226
- const response = await config.port.read(150);
227
-
228
- if (response.length >= 7) {
229
- const id = response[2];
230
- const error = response[4];
231
-
232
- if (id === motorId && error === 0) {
233
- const position = response[5] | (response[6] << 8);
234
- motorPositions.push(position);
235
- success = true;
236
- } else if (id === motorId && error !== 0) {
237
- // Motor error, retry
238
- } else {
239
- // Wrong response ID, retry
240
- }
241
- } else {
242
- // Short response, retry
243
- }
244
-
245
- // Professional timing between attempts (like Arduino libraries)
246
- if (!success && attempts < 3) {
247
- await new Promise((resolve) => setTimeout(resolve, 20));
248
- }
249
- }
250
-
251
- // If all attempts failed, use fallback
252
- if (!success) {
253
- const fallback = Math.floor((config.protocol.resolution - 1) / 2);
254
- motorPositions.push(fallback);
255
- }
256
- } catch (error) {
257
- const fallback = Math.floor((config.protocol.resolution - 1) / 2);
258
- motorPositions.push(fallback);
259
- }
260
-
261
- // Professional inter-motor delay (based on Arduino STS_servos library)
262
- await new Promise((resolve) => setTimeout(resolve, 10));
263
- }
264
-
265
- return motorPositions;
266
- }
267
-
268
- /**
269
- * Reset homing offsets to 0 for all motors (matches Node.js exactly)
270
- */
271
- async function resetHomingOffsets(config: WebCalibrationConfig): Promise<void> {
272
- for (let i = 0; i < config.motorIds.length; i++) {
273
- const motorId = config.motorIds[i];
274
- const motorName = config.motorNames[i];
275
-
276
- try {
277
- const homingOffsetValue = 0;
278
-
279
- // Create Write Homing_Offset packet using configurable address
280
- const packet = new Uint8Array([
281
- 0xff,
282
- 0xff, // Header
283
- motorId, // Servo ID
284
- 0x05, // Length
285
- 0x03, // Instruction: WRITE_DATA
286
- config.protocol.homingOffsetAddress, // Configurable address
287
- homingOffsetValue & 0xff, // Data_L (low byte)
288
- (homingOffsetValue >> 8) & 0xff, // Data_H (high byte)
289
- 0x00, // Checksum (will calculate)
290
- ]);
291
-
292
- // Calculate checksum using configurable address
293
- const checksum =
294
- ~(
295
- motorId +
296
- 0x05 +
297
- 0x03 +
298
- config.protocol.homingOffsetAddress +
299
- (homingOffsetValue & 0xff) +
300
- ((homingOffsetValue >> 8) & 0xff)
301
- ) & 0xff;
302
- packet[8] = checksum;
303
-
304
- // Simple write then read like Node.js
305
- await config.port.write(packet);
306
-
307
- // Wait for response (silent unless error)
308
- try {
309
- await config.port.read(200);
310
- } catch (error) {
311
- // Silent - response not required for successful operation
312
- }
313
- } catch (error) {
314
- throw new Error(
315
- `Failed to reset homing offset for ${motorName}: ${
316
- error instanceof Error ? error.message : error
317
- }`
318
- );
319
- }
320
- }
321
- }
322
-
323
- /**
324
- * Write homing offsets to motor registers immediately (matches Node.js exactly)
325
- */
326
- async function writeHomingOffsetsToMotors(
327
- config: WebCalibrationConfig,
328
- homingOffsets: { [motor: string]: number }
329
- ): Promise<void> {
330
- for (let i = 0; i < config.motorIds.length; i++) {
331
- const motorId = config.motorIds[i];
332
- const motorName = config.motorNames[i];
333
- const homingOffset = homingOffsets[motorName];
334
-
335
- try {
336
- // Encode using sign-magnitude format
337
- const encodedOffset = encodeSignMagnitude(
338
- homingOffset,
339
- config.protocol.signMagnitudeBit
340
- );
341
-
342
- // Create Write Homing_Offset packet
343
- const packet = new Uint8Array([
344
- 0xff,
345
- 0xff, // Header
346
- motorId, // Servo ID
347
- 0x05, // Length
348
- 0x03, // Instruction: WRITE_DATA
349
- config.protocol.homingOffsetAddress, // Homing_Offset address
350
- encodedOffset & 0xff, // Data_L (low byte)
351
- (encodedOffset >> 8) & 0xff, // Data_H (high byte)
352
- 0x00, // Checksum (will calculate)
353
- ]);
354
-
355
- // Calculate checksum
356
- const checksum =
357
- ~(
358
- motorId +
359
- 0x05 +
360
- 0x03 +
361
- config.protocol.homingOffsetAddress +
362
- (encodedOffset & 0xff) +
363
- ((encodedOffset >> 8) & 0xff)
364
- ) & 0xff;
365
- packet[8] = checksum;
366
-
367
- // Simple write then read like Node.js
368
- await config.port.write(packet);
369
-
370
- // Wait for response (silent unless error)
371
- try {
372
- await config.port.read(200);
373
- } catch (error) {
374
- // Silent - response not required for successful operation
375
- }
376
- } catch (error) {
377
- throw new Error(
378
- `Failed to write homing offset for ${motorName}: ${
379
- error instanceof Error ? error.message : error
380
- }`
381
- );
382
- }
383
- }
384
- }
385
-
386
- /**
387
- * Record homing offsets with immediate writing (matches Node.js exactly)
388
  */
389
- async function setHomingOffsets(
390
- config: WebCalibrationConfig
391
- ): Promise<{ [motor: string]: number }> {
392
- console.log("🏠 Setting homing offsets...");
393
-
394
- // CRITICAL: Reset existing homing offsets to 0 first (matching Python)
395
- await resetHomingOffsets(config);
396
-
397
- // Wait a moment for reset to take effect
398
- await new Promise((resolve) => setTimeout(resolve, 100));
399
-
400
- // Now read positions (which will be true physical positions)
401
- const currentPositions = await readMotorPositions(config);
402
- const homingOffsets: { [motor: string]: number } = {};
403
-
404
- const halfTurn = Math.floor((config.protocol.resolution - 1) / 2);
405
-
406
- for (let i = 0; i < config.motorNames.length; i++) {
407
- const motorName = config.motorNames[i];
408
- const position = currentPositions[i];
409
-
410
- // Generic formula: pos - int((max_res - 1) / 2) using configurable resolution
411
- const homingOffset = position - halfTurn;
412
- homingOffsets[motorName] = homingOffset;
413
- }
414
-
415
- // CRITICAL: Write homing offsets to motors immediately (matching Python exactly)
416
- await writeHomingOffsetsToMotors(config, homingOffsets);
417
-
418
- return homingOffsets;
419
- }
420
-
421
- /**
422
- * Generic function to write a 2-byte value to a motor register (matches Node.js exactly)
423
- */
424
- async function writeMotorRegister(
425
- config: WebCalibrationConfig,
426
- motorId: number,
427
- registerAddress: number,
428
- value: number,
429
- description: string
430
- ): Promise<void> {
431
- // Create Write Register packet
432
- const packet = new Uint8Array([
433
- 0xff,
434
- 0xff, // Header
435
- motorId, // Servo ID
436
- 0x05, // Length
437
- 0x03, // Instruction: WRITE_DATA
438
- registerAddress, // Register address
439
- value & 0xff, // Data_L (low byte)
440
- (value >> 8) & 0xff, // Data_H (high byte)
441
- 0x00, // Checksum (will calculate)
442
- ]);
443
-
444
- // Calculate checksum
445
- const checksum =
446
- ~(
447
- motorId +
448
- 0x05 +
449
- 0x03 +
450
- registerAddress +
451
- (value & 0xff) +
452
- ((value >> 8) & 0xff)
453
- ) & 0xff;
454
- packet[8] = checksum;
455
-
456
- // Simple write then read like Node.js
457
- await config.port.write(packet);
458
-
459
- // Wait for response (silent unless error)
460
- try {
461
- await config.port.read(200);
462
- } catch (error) {
463
- // Silent - response not required for successful operation
464
- }
465
  }
466
 
467
  /**
468
- * Write hardware position limits to motors (matches Node.js exactly)
469
  */
470
- async function writeHardwarePositionLimits(
471
- config: WebCalibrationConfig,
472
- rangeMins: { [motor: string]: number },
473
- rangeMaxes: { [motor: string]: number }
474
- ): Promise<void> {
475
- for (let i = 0; i < config.motorIds.length; i++) {
476
- const motorId = config.motorIds[i];
477
- const motorName = config.motorNames[i];
478
- const minLimit = rangeMins[motorName];
479
- const maxLimit = rangeMaxes[motorName];
480
-
481
- try {
482
- // Write Min_Position_Limit register
483
- await writeMotorRegister(
484
- config,
485
- motorId,
486
- config.protocol.minPositionLimitAddress,
487
- minLimit,
488
- `Min_Position_Limit for ${motorName}`
489
- );
490
-
491
- // Write Max_Position_Limit register
492
- await writeMotorRegister(
493
- config,
494
- motorId,
495
- config.protocol.maxPositionLimitAddress,
496
- maxLimit,
497
- `Max_Position_Limit for ${motorName}`
498
- );
499
- } catch (error) {
500
- throw new Error(
501
- `Failed to write position limits for ${motorName}: ${
502
- error instanceof Error ? error.message : error
503
- }`
504
- );
505
- }
506
- }
507
  }
508
 
509
  /**
510
- * Record ranges of motion with manual control (user decides when to stop)
511
  */
512
  async function recordRangesOfMotion(
513
- config: WebCalibrationConfig,
 
 
514
  shouldStop: () => boolean,
515
- onUpdate?: (
516
- rangeMins: { [motor: string]: number },
517
- rangeMaxes: { [motor: string]: number },
518
- currentPositions: { [motor: string]: number }
519
- ) => void
520
  ): Promise<{
521
  rangeMins: { [motor: string]: number };
522
  rangeMaxes: { [motor: string]: number };
@@ -525,29 +97,23 @@ async function recordRangesOfMotion(
525
  const rangeMaxes: { [motor: string]: number } = {};
526
 
527
  // Read actual current positions (matching Python exactly)
528
- // After homing offsets are applied, these should be ~2047 (centered)
529
- const startPositions = await readMotorPositions(config);
530
 
531
- for (let i = 0; i < config.motorNames.length; i++) {
532
- const motorName = config.motorNames[i];
533
  const startPosition = startPositions[i];
534
- rangeMins[motorName] = startPosition; // Use actual position, not hardcoded 2047
535
- rangeMaxes[motorName] = startPosition; // Use actual position, not hardcoded 2047
536
  }
537
 
538
- // Manual recording using simple while loop like Node.js
539
- let recordingCount = 0;
540
-
541
  while (!shouldStop()) {
542
  try {
543
- const positions = await readMotorPositions(config);
544
- recordingCount++;
545
 
546
- for (let i = 0; i < config.motorNames.length; i++) {
547
- const motorName = config.motorNames[i];
548
  const position = positions[i];
549
- const oldMin = rangeMins[motorName];
550
- const oldMax = rangeMaxes[motorName];
551
 
552
  if (position < rangeMins[motorName]) {
553
  rangeMins[motorName] = position;
@@ -555,275 +121,141 @@ async function recordRangesOfMotion(
555
  if (position > rangeMaxes[motorName]) {
556
  rangeMaxes[motorName] = position;
557
  }
558
-
559
- // Track range expansions silently
560
  }
561
 
562
- // Continue recording silently
563
-
564
- // Call update callback if provided (for live UI updates)
565
- if (onUpdate) {
566
- // Convert positions array to motor name map for UI
567
- const currentPositions: { [motor: string]: number } = {};
568
- for (let i = 0; i < config.motorNames.length; i++) {
569
- currentPositions[config.motorNames[i]] = positions[i];
 
 
 
570
  }
571
- onUpdate(rangeMins, rangeMaxes, currentPositions);
572
  }
573
  } catch (error) {
574
- console.warn("Error during range recording:", error);
575
  }
576
 
577
- // 20fps reading rate for stable Web Serial communication while maintaining responsive UI
578
  await new Promise((resolve) => setTimeout(resolve, 50));
579
  }
580
 
581
- // Range recording finished
582
-
583
  return { rangeMins, rangeMaxes };
584
  }
585
 
586
  /**
587
- * Interactive web calibration with manual control - user decides when to stop recording
588
  */
589
- async function performWebCalibration(
590
- config: WebCalibrationConfig,
591
- shouldStopRecording: () => boolean,
592
- onRangeUpdate?: (
593
- rangeMins: { [motor: string]: number },
594
- rangeMaxes: { [motor: string]: number },
595
- currentPositions: { [motor: string]: number }
596
- ) => void
597
- ): Promise<WebCalibrationResults> {
598
- // Step 1: Set homing position
599
- const homingOffsets = await setHomingOffsets(config);
600
-
601
- // Step 2: Record ranges of motion with manual control
602
- const { rangeMins, rangeMaxes } = await recordRangesOfMotion(
603
- config,
604
- shouldStopRecording,
605
- onRangeUpdate
606
- );
607
-
608
- // Step 3: Set special range for wrist_roll (full turn motor)
609
- rangeMins["wrist_roll"] = 0;
610
- rangeMaxes["wrist_roll"] = 4095;
611
-
612
- // Step 4: Write hardware position limits to motors (matching Python behavior)
613
- await writeHardwarePositionLimits(config, rangeMins, rangeMaxes);
614
-
615
- // Compile results in Python-compatible format (NOT array format!)
616
- const results: WebCalibrationResults = {};
617
-
618
- for (let i = 0; i < config.motorNames.length; i++) {
619
- const motorName = config.motorNames[i];
620
- const motorId = config.motorIds[i];
621
-
622
- results[motorName] = {
623
- id: motorId,
624
- drive_mode: config.driveModes[i],
625
- homing_offset: homingOffsets[motorName],
626
- range_min: rangeMins[motorName],
627
- range_max: rangeMaxes[motorName],
628
- };
629
- }
630
 
631
- return results;
 
 
 
632
  }
633
 
634
  /**
635
- * Step-by-step calibration for React components
 
 
 
636
  */
637
- export class WebCalibrationController {
638
- private config: WebCalibrationConfig;
639
- private homingOffsets: { [motor: string]: number } | null = null;
640
- private rangeMins: { [motor: string]: number } | null = null;
641
- private rangeMaxes: { [motor: string]: number } | null = null;
642
-
643
- constructor(config: WebCalibrationConfig) {
644
- this.config = config;
645
  }
646
-
647
- async readMotorPositions(): Promise<number[]> {
648
- return await readMotorPositions(this.config);
 
 
 
649
  }
650
 
651
- async performHomingStep(): Promise<void> {
652
- this.homingOffsets = await setHomingOffsets(this.config);
653
- }
654
 
655
- async performRangeRecordingStep(
656
- shouldStop: () => boolean,
657
- onUpdate?: (
658
- rangeMins: { [motor: string]: number },
659
- rangeMaxes: { [motor: string]: number },
660
- currentPositions: { [motor: string]: number }
661
- ) => void
662
- ): Promise<void> {
663
- const { rangeMins, rangeMaxes } = await recordRangesOfMotion(
664
- this.config,
665
- shouldStop,
666
- onUpdate
 
 
667
  );
668
- this.rangeMins = rangeMins;
669
- this.rangeMaxes = rangeMaxes;
670
 
671
- // Set special range for wrist_roll (full turn motor)
672
- this.rangeMins["wrist_roll"] = 0;
673
- this.rangeMaxes["wrist_roll"] = 4095;
674
- }
 
 
 
 
675
 
676
- async finishCalibration(): Promise<WebCalibrationResults> {
677
- if (!this.homingOffsets || !this.rangeMins || !this.rangeMaxes) {
678
- throw new Error("Must complete all calibration steps first");
679
- }
 
 
680
 
681
- // Write hardware position limits to motors (matching Python behavior)
682
  await writeHardwarePositionLimits(
683
- this.config,
684
- this.rangeMins,
685
- this.rangeMaxes
 
 
686
  );
687
 
688
- // Compile results in Python-compatible format (NOT array format!)
689
  const results: WebCalibrationResults = {};
690
 
691
- for (let i = 0; i < this.config.motorNames.length; i++) {
692
- const motorName = this.config.motorNames[i];
693
- const motorId = this.config.motorIds[i];
694
 
695
  results[motorName] = {
696
  id: motorId,
697
- drive_mode: this.config.driveModes[i],
698
- homing_offset: this.homingOffsets[motorName],
699
- range_min: this.rangeMins[motorName],
700
- range_max: this.rangeMaxes[motorName],
701
  };
702
  }
703
 
704
- console.log("🎉 Calibration completed successfully!");
705
  return results;
706
- }
707
- }
708
 
709
- /**
710
- * Create SO-100 web configuration (matches Node.js exactly)
711
- */
712
- function createSO100WebConfig(
713
- deviceType: "so100_follower" | "so100_leader",
714
- port: WebSerialPortWrapper
715
- ): WebCalibrationConfig {
716
  return {
717
- deviceType,
718
- port,
719
- motorNames: [
720
- "shoulder_pan",
721
- "shoulder_lift",
722
- "elbow_flex",
723
- "wrist_flex",
724
- "wrist_roll",
725
- "gripper",
726
- ],
727
- motorIds: [1, 2, 3, 4, 5, 6],
728
- protocol: WEB_STS3215_PROTOCOL,
729
- driveModes: [0, 0, 0, 0, 0, 0], // Python lerobot uses drive_mode=0 for all motors
730
- calibModes: ["DEGREE", "DEGREE", "DEGREE", "DEGREE", "DEGREE", "LINEAR"],
731
- limits: {
732
- position_min: [-180, -90, -90, -90, -90, -90],
733
- position_max: [180, 90, 90, 90, 90, 90],
734
- velocity_max: [100, 100, 100, 100, 100, 100],
735
- torque_max: [50, 50, 50, 50, 25, 25],
736
  },
 
737
  };
738
  }
739
 
740
- /**
741
- * Create a calibration controller for step-by-step calibration in React components
742
- */
743
- export async function createCalibrationController(
744
- armType: "so100_follower" | "so100_leader",
745
- connectedPort: SerialPort
746
- ): Promise<WebCalibrationController> {
747
- // Create web serial port wrapper
748
- const port = new WebSerialPortWrapper(connectedPort);
749
- await port.initialize();
750
-
751
- // Get device-agnostic calibration configuration
752
- const config = createSO100WebConfig(armType, port);
753
-
754
- return new WebCalibrationController(config);
755
- }
756
-
757
- /**
758
- * Save calibration results to unified storage system
759
- */
760
- export async function saveCalibrationResults(
761
- calibrationResults: WebCalibrationResults,
762
- armType: "so100_follower" | "so100_leader",
763
- armId: string,
764
- serialNumber: string,
765
- recordingCount: number = 0
766
- ): Promise<void> {
767
- // Prepare full calibration data
768
- const fullCalibrationData = {
769
- ...calibrationResults,
770
- device_type: armType,
771
- device_id: armId,
772
- calibrated_at: new Date().toISOString(),
773
- platform: "web",
774
- api: "Web Serial API",
775
- };
776
-
777
- const metadata = {
778
- timestamp: new Date().toISOString(),
779
- readCount: recordingCount,
780
- };
781
-
782
- // Try to save using unified storage system
783
- try {
784
- const { saveCalibrationData } = await import(
785
- "../../demo/lib/unified-storage.js"
786
- );
787
- saveCalibrationData(serialNumber, fullCalibrationData, metadata);
788
- console.log(
789
- `✅ Calibration saved to unified storage: lerobotjs-${serialNumber}`
790
- );
791
- } catch (error) {
792
- console.warn(
793
- "Failed to save to unified storage, falling back to old format:",
794
- error
795
- );
796
-
797
- // Fallback to old storage format for compatibility
798
- const fullDataKey = `lerobot_calibration_${armType}_${armId}`;
799
- localStorage.setItem(fullDataKey, JSON.stringify(fullCalibrationData));
800
-
801
- const dashboardKey = `lerobot-calibration-${serialNumber}`;
802
- localStorage.setItem(dashboardKey, JSON.stringify(metadata));
803
-
804
- console.log(`📊 Dashboard data saved to: ${dashboardKey}`);
805
- console.log(`🔧 Full calibration data saved to: ${fullDataKey}`);
806
- }
807
- }
808
-
809
- /**
810
- * Download calibration data as JSON file
811
- */
812
- function downloadCalibrationFile(calibrationData: any, deviceId: string): void {
813
- const dataStr = JSON.stringify(calibrationData, null, 2);
814
- const dataBlob = new Blob([dataStr], { type: "application/json" });
815
-
816
- const url = URL.createObjectURL(dataBlob);
817
- const link = document.createElement("a");
818
- link.href = url;
819
- link.download = `${deviceId}_calibration.json`;
820
-
821
- document.body.appendChild(link);
822
- link.click();
823
- document.body.removeChild(link);
824
- URL.revokeObjectURL(url);
825
- }
826
-
827
  /**
828
  * Check if Web Serial API is supported
829
  */
 
1
  /**
2
  * Web calibration functionality using Web Serial API
3
+ * Minimal library - does calibration, user handles storage/UI/etc.
4
+ *
5
+ * Currently supports SO-100 robots. Other robot types can be added by:
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,
15
+ releaseMotors,
16
+ type MotorCommunicationPort,
17
+ } from "./utils/motor-communication.js";
18
+ import {
19
+ setHomingOffsets,
20
+ writeHardwarePositionLimits,
21
+ } from "./utils/motor-calibration.js";
22
+ import type { RobotConnection } from "./find_port.js";
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";
 
31
  motorNames: string[];
32
  motorIds: number[];
33
  driveModes: number[];
 
34
 
35
+ // Protocol-specific configuration
36
  protocol: {
37
  resolution: number;
38
  homingOffsetAddress: number;
 
45
  maxPositionLimitLength: number;
46
  signMagnitudeBit: number;
47
  };
 
 
 
 
 
 
 
48
  }
49
 
50
  /**
 
61
  }
62
 
63
  /**
64
+ * Live calibration data with current positions and ranges
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  */
66
+ export interface LiveCalibrationData {
67
+ [motorName: string]: {
68
+ current: number;
69
+ min: number;
70
+ max: number;
71
+ range: number;
72
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
 
75
  /**
76
+ * Calibration process control object
77
  */
78
+ export interface CalibrationProcess {
79
+ stop(): void;
80
+ result: Promise<WebCalibrationResults>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
82
 
83
  /**
84
+ * Record ranges of motion with live updates
85
  */
86
  async function recordRangesOfMotion(
87
+ port: MotorCommunicationPort,
88
+ motorIds: number[],
89
+ motorNames: string[],
90
  shouldStop: () => boolean,
91
+ onLiveUpdate?: (data: LiveCalibrationData) => void
 
 
 
 
92
  ): Promise<{
93
  rangeMins: { [motor: string]: number };
94
  rangeMaxes: { [motor: string]: number };
 
97
  const rangeMaxes: { [motor: string]: number } = {};
98
 
99
  // Read actual current positions (matching Python exactly)
100
+ const startPositions = await readAllMotorPositions(port, motorIds);
 
101
 
102
+ for (let i = 0; i < motorNames.length; i++) {
103
+ const motorName = motorNames[i];
104
  const startPosition = startPositions[i];
105
+ rangeMins[motorName] = startPosition;
106
+ rangeMaxes[motorName] = startPosition;
107
  }
108
 
109
+ // Recording loop
 
 
110
  while (!shouldStop()) {
111
  try {
112
+ const positions = await readAllMotorPositions(port, motorIds);
 
113
 
114
+ for (let i = 0; i < motorNames.length; i++) {
115
+ const motorName = motorNames[i];
116
  const position = positions[i];
 
 
117
 
118
  if (position < rangeMins[motorName]) {
119
  rangeMins[motorName] = position;
 
121
  if (position > rangeMaxes[motorName]) {
122
  rangeMaxes[motorName] = position;
123
  }
 
 
124
  }
125
 
126
+ // Call live update callback if provided
127
+ if (onLiveUpdate) {
128
+ const liveData: LiveCalibrationData = {};
129
+ for (let i = 0; i < motorNames.length; i++) {
130
+ const motorName = motorNames[i];
131
+ liveData[motorName] = {
132
+ current: positions[i],
133
+ min: rangeMins[motorName],
134
+ max: rangeMaxes[motorName],
135
+ range: rangeMaxes[motorName] - rangeMins[motorName],
136
+ };
137
  }
138
+ onLiveUpdate(liveData);
139
  }
140
  } catch (error) {
141
+ // Continue recording despite errors
142
  }
143
 
144
+ // 20fps reading rate for stable Web Serial communication
145
  await new Promise((resolve) => setTimeout(resolve, 50));
146
  }
147
 
 
 
148
  return { rangeMins, rangeMaxes };
149
  }
150
 
151
  /**
152
+ * Create SO-100 web configuration
153
  */
154
+ function createSO100WebConfig(
155
+ deviceType: "so100_follower" | "so100_leader",
156
+ port: WebSerialPortWrapper
157
+ ): WebCalibrationConfig {
158
+ const so100Config = createSO100Config(deviceType);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ return {
161
+ ...so100Config,
162
+ port,
163
+ };
164
  }
165
 
166
  /**
167
+ * Main calibrate function - minimal library API
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,
174
+ options?: {
175
+ onLiveUpdate?: (data: LiveCalibrationData) => void;
176
+ onProgress?: (message: string) => void;
 
 
 
177
  }
178
+ ): Promise<CalibrationProcess> {
179
+ // Validate required fields
180
+ if (!robotConnection.robotType) {
181
+ throw new Error(
182
+ "Robot type is required for calibration. Please configure the robot first."
183
+ );
184
  }
185
 
186
+ // Create web serial port wrapper
187
+ const port = new WebSerialPortWrapper(robotConnection.port);
188
+ await port.initialize();
189
 
190
+ // Get SO-100 specific calibration configuration
191
+ const config = createSO100WebConfig(robotConnection.robotType, port);
192
+
193
+ let shouldStop = false;
194
+ const stopFunction = () => shouldStop;
195
+
196
+ // Start calibration process
197
+ const resultPromise = (async (): Promise<WebCalibrationResults> => {
198
+ // Step 1: Set homing offsets (automatic)
199
+ options?.onProgress?.("⚙️ Setting motor homing offsets");
200
+ const homingOffsets = await setHomingOffsets(
201
+ config.port,
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
+ config.port,
209
+ config.motorIds,
210
+ config.motorNames,
211
+ stopFunction,
212
+ options?.onLiveUpdate
213
+ );
214
 
215
+ // Step 3: Set special range for wrist_roll (full turn motor)
216
+ // The wrist_roll is a continuous rotation motor that should use the full
217
+ // 0-4095 range regardless of what the user recorded during calibration.
218
+ // This matches the hardware specification and Python lerobot behavior.
219
+ rangeMins["wrist_roll"] = 0;
220
+ rangeMaxes["wrist_roll"] = 4095;
221
 
222
+ // Step 4: Write hardware position limits to motors
223
  await writeHardwarePositionLimits(
224
+ config.port,
225
+ config.motorIds,
226
+ config.motorNames,
227
+ rangeMins,
228
+ rangeMaxes
229
  );
230
 
231
+ // Step 5: Compile results in Python-compatible format
232
  const results: WebCalibrationResults = {};
233
 
234
+ for (let i = 0; i < config.motorNames.length; i++) {
235
+ const motorName = config.motorNames[i];
236
+ const motorId = config.motorIds[i];
237
 
238
  results[motorName] = {
239
  id: motorId,
240
+ drive_mode: config.driveModes[i],
241
+ homing_offset: homingOffsets[motorName],
242
+ range_min: rangeMins[motorName],
243
+ range_max: rangeMaxes[motorName],
244
  };
245
  }
246
 
 
247
  return results;
248
+ })();
 
249
 
250
+ // Return control object
 
 
 
 
 
 
251
  return {
252
+ stop: () => {
253
+ shouldStop = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  },
255
+ result: resultPromise,
256
  };
257
  }
258
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  /**
260
  * Check if Web Serial API is supported
261
  */
src/lerobot/web/find_port.ts CHANGED
@@ -1,10 +1,35 @@
1
  /**
2
  * Browser implementation of find_port using WebSerial API
 
3
  *
4
- * Provides the same functionality as the Node.js version but adapted for browser environment
5
- * Uses WebSerial API for serial port detection and user interaction through DOM
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  */
7
 
 
 
8
  /**
9
  * Type definitions for WebSerial API (not yet in all TypeScript libs)
10
  */
@@ -49,244 +74,298 @@ declare global {
49
  }
50
 
51
  /**
52
- * Check if WebSerial API is available
 
 
53
  */
54
- function isWebSerialSupported(): boolean {
55
- return "serial" in navigator;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
 
58
  /**
59
- * Get all available serial ports (requires user permission)
60
- * Browser equivalent of Node.js findAvailablePorts()
61
  */
62
- async function findAvailablePortsWeb(): Promise<SerialPort[]> {
63
- if (!isWebSerialSupported()) {
64
- throw new Error(
65
- "WebSerial API not supported. Please use Chrome/Edge 89+ or Chrome Android 105+"
66
- );
67
- }
68
-
69
- try {
70
- return await navigator.serial.getPorts();
71
- } catch (error) {
72
- throw new Error(
73
- `Failed to get serial ports: ${
74
- error instanceof Error ? error.message : error
75
- }`
76
- );
77
- }
78
  }
79
 
80
  /**
81
- * Format port info for display
82
- * Mimics the Node.js port listing format
83
  */
84
- function formatPortInfo(ports: SerialPort[]): string[] {
85
- return ports.map((port, index) => {
86
- const info = port.getInfo();
87
- if (info.usbVendorId && info.usbProductId) {
88
- return `Port ${index + 1} (USB:${info.usbVendorId}:${info.usbProductId})`;
89
- }
90
- return `Port ${index + 1}`;
91
- });
 
 
92
  }
93
 
94
  /**
95
- * Sleep for specified milliseconds
96
- * Same as Node.js version
97
  */
98
- function sleep(ms: number): Promise<void> {
99
- return new Promise((resolve) => setTimeout(resolve, ms));
 
 
 
 
 
 
100
  }
101
 
102
  /**
103
- * Wait for user interaction (button click or similar)
104
- * Browser equivalent of Node.js readline input()
105
  */
106
- function waitForUserAction(message: string): Promise<void> {
107
- return new Promise((resolve) => {
108
- const modal = document.createElement("div");
109
- modal.style.cssText = `
110
- position: fixed;
111
- top: 0;
112
- left: 0;
113
- width: 100%;
114
- height: 100%;
115
- background: rgba(0,0,0,0.5);
116
- display: flex;
117
- align-items: center;
118
- justify-content: center;
119
- z-index: 1000;
120
- `;
121
-
122
- const dialog = document.createElement("div");
123
- dialog.style.cssText = `
124
- background: white;
125
- padding: 2rem;
126
- border-radius: 8px;
127
- text-align: center;
128
- max-width: 500px;
129
- margin: 1rem;
130
- `;
131
-
132
- dialog.innerHTML = `
133
- <h3>Port Detection</h3>
134
- <p style="margin: 1rem 0;">${message}</p>
135
- <button id="continue-btn" style="
136
- background: #3498db;
137
- color: white;
138
- border: none;
139
- padding: 12px 24px;
140
- border-radius: 6px;
141
- cursor: pointer;
142
- font-size: 1rem;
143
- ">Continue</button>
144
- `;
145
-
146
- modal.appendChild(dialog);
147
- document.body.appendChild(modal);
148
-
149
- const continueBtn = dialog.querySelector(
150
- "#continue-btn"
151
- ) as HTMLButtonElement;
152
- continueBtn.addEventListener("click", () => {
153
- document.body.removeChild(modal);
154
- resolve();
155
- });
156
- });
157
  }
158
 
159
  /**
160
- * Request permission to access serial ports
161
  */
162
- async function requestSerialPermission(
163
- logger: (message: string) => void
164
- ): Promise<void> {
165
- logger("Requesting permission to access serial ports...");
166
- logger(
167
- 'Please select a serial device when prompted, or click "Cancel" if no devices are connected yet.'
168
- );
169
 
 
 
 
 
170
  try {
171
- // This will show the browser's serial port selection dialog
172
- await navigator.serial.requestPort();
173
- logger("✅ Permission granted to access serial ports.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  } catch (error) {
175
- // User cancelled the dialog - this is OK, they might not have devices connected yet
176
- console.log("Permission dialog cancelled:", error);
177
  }
178
  }
179
 
180
  /**
181
- * Main find port function for browser
182
- * Maintains identical UX and messaging to Node.js version
183
  */
184
- export async function findPortWeb(
185
- logger: (message: string) => void
186
- ): Promise<void> {
187
- logger("Finding all available ports for the MotorsBus.");
188
 
189
- // Check WebSerial support
190
- if (!isWebSerialSupported()) {
191
- throw new Error(
192
- "WebSerial API not supported. Please use Chrome/Edge 89+ with HTTPS or localhost."
193
- );
194
- }
195
 
196
- // Get initial ports (check what we already have access to)
197
- let portsBefore: SerialPort[];
198
  try {
199
- portsBefore = await findAvailablePortsWeb();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  } catch (error) {
 
 
 
 
 
 
201
  throw new Error(
202
- `Failed to get serial ports: ${
203
- error instanceof Error ? error.message : error
204
- }`
205
  );
206
  }
 
207
 
208
- // If no ports are available, request permission
209
- if (portsBefore.length === 0) {
210
- logger(
211
- "⚠️ No serial ports available. Requesting permission to access devices..."
212
- );
213
- await requestSerialPermission(logger);
214
-
215
- // Try again after permission request
216
- portsBefore = await findAvailablePortsWeb();
217
-
218
- if (portsBefore.length === 0) {
219
- throw new Error(
220
- 'No ports detected. Please connect your devices, use "Show Available Ports" first, or check browser compatibility.'
221
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
 
225
- // Show current ports
226
- const portsBeforeFormatted = formatPortInfo(portsBefore);
227
- logger(
228
- `Ports before disconnecting: [${portsBeforeFormatted
229
- .map((p) => `'${p}'`)
230
- .join(", ")}]`
231
  );
232
 
233
- // Ask user to disconnect device
234
- logger(
235
- "Remove the USB cable from your MotorsBus and press Continue when done."
236
- );
237
- await waitForUserAction(
238
- "Remove the USB cable from your MotorsBus and press Continue when done."
239
- );
240
 
241
- // Allow some time for port to be released (equivalent to Python's time.sleep(0.5))
242
- await sleep(500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
- // Get ports after disconnection
245
- const portsAfter = await findAvailablePortsWeb();
246
- const portsAfterFormatted = formatPortInfo(portsAfter);
247
- logger(
248
- `Ports after disconnecting: [${portsAfterFormatted
249
- .map((p) => `'${p}'`)
250
- .join(", ")}]`
251
  );
252
 
253
- // Find the difference by comparing port objects directly
254
- // This handles cases where multiple devices have the same vendor/product ID
255
- const removedPorts = portsBefore.filter((portBefore) => {
256
- return !portsAfter.includes(portBefore);
257
- });
258
-
259
- // If object comparison fails (e.g., browser creates new objects), fall back to count-based detection
260
- if (removedPorts.length === 0 && portsBefore.length > portsAfter.length) {
261
- const countDifference = portsBefore.length - portsAfter.length;
262
- if (countDifference === 1) {
263
- logger(`The port of this MotorsBus is one of the disconnected devices.`);
264
- logger(
265
- "Note: Exact port identification not possible with identical devices."
266
- );
267
- logger("Reconnect the USB cable.");
268
- return;
269
- } else {
270
- logger(`${countDifference} ports were removed, but expected exactly 1.`);
271
- logger("Please disconnect only one device and try again.");
272
- return;
273
  }
274
- }
275
 
276
- if (removedPorts.length === 1) {
277
- const port = formatPortInfo(removedPorts)[0];
278
- logger(`The port of this MotorsBus is '${port}'`);
279
- logger("Reconnect the USB cable.");
280
- } else if (removedPorts.length === 0) {
281
- logger("No difference found, did you remove the USB cable?");
282
- logger("Please try again: disconnect one device and click Continue.");
283
- return;
284
- } else {
285
- const portNames = formatPortInfo(removedPorts);
286
- throw new Error(
287
- `Could not detect the port. More than one port was found (${JSON.stringify(
288
- portNames
289
- )}).`
290
- );
291
- }
292
  }
 
 
 
 
1
  /**
2
  * Browser implementation of find_port using WebSerial API
3
+ * Clean API with native dialogs and auto-connect modes
4
  *
5
+ * Usage Examples:
6
+ *
7
+ * // Interactive mode - always returns array
8
+ * const findProcess = await findPort();
9
+ * const robotConnections = await findProcess.result;
10
+ * const robot = robotConnections[0]; // First (and only) robot
11
+ * await calibrate(robot, options);
12
+ *
13
+ * // Auto-connect mode - returns array of all attempted connections
14
+ * const findProcess = await findPort({
15
+ * robotConfigs: [
16
+ * { robotType: "so100_follower", robotId: "arm1", serialNumber: "ABC123" },
17
+ * { robotType: "so100_leader", robotId: "arm2", serialNumber: "DEF456" }
18
+ * ]
19
+ * });
20
+ * const robotConnections = await findProcess.result;
21
+ * for (const robot of robotConnections.filter(r => r.isConnected)) {
22
+ * await calibrate(robot, options);
23
+ * }
24
+ *
25
+ * // Store/load from localStorage
26
+ * localStorage.setItem('myRobots', JSON.stringify(robotConnections));
27
+ * const storedRobots = JSON.parse(localStorage.getItem('myRobots'));
28
+ * await calibrate(storedRobots[0], options);
29
  */
30
 
31
+ import { getRobotConnectionManager } from "./robot-connection.js";
32
+
33
  /**
34
  * Type definitions for WebSerial API (not yet in all TypeScript libs)
35
  */
 
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
  */
116
+ export interface FindPortOptions {
117
+ // Auto-connect mode: provide robot configs to connect to
118
+ robotConfigs?: RobotConfig[];
119
+
120
+ // Callbacks
121
+ onMessage?: (message: string) => void;
122
+ onRequestUserAction?: (
123
+ message: string,
124
+ type: "confirm" | "select"
125
+ ) => Promise<boolean>;
126
  }
127
 
128
  /**
129
+ * Process object returned by findPort
 
130
  */
131
+ export interface FindPortProcess {
132
+ // Result promise - Always returns RobotConnection[] (consistent API)
133
+ // Interactive mode: single robot in array
134
+ // Auto-connect mode: all successfully connected robots in array
135
+ result: Promise<RobotConnection[]>;
136
+
137
+ // Control
138
+ stop: () => void;
139
  }
140
 
141
  /**
142
+ * Check if WebSerial API is available
 
143
  */
144
+ function isWebSerialSupported(): boolean {
145
+ return "serial" in navigator;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  }
147
 
148
  /**
149
+ * Get display name for a port
150
  */
151
+ function getPortDisplayName(port: SerialPort): string {
152
+ const info = port.getInfo();
153
+ if (info.usbVendorId && info.usbProductId) {
154
+ return `USB Device (${info.usbVendorId}:${info.usbProductId})`;
155
+ }
156
+ return "Serial Device";
157
+ }
158
 
159
+ /**
160
+ * Try to get serial number from a port by opening and reading device info
161
+ */
162
+ async function getPortSerialNumber(port: SerialPort): Promise<string | null> {
163
  try {
164
+ const wasOpen = port.readable !== null;
165
+
166
+ // Open port if not already open
167
+ if (!wasOpen) {
168
+ await port.open({ baudRate: 1000000 });
169
+ }
170
+
171
+ // For now, we'll return null since reading serial number from STS3215 motors
172
+ // requires specific protocol implementation. This is a placeholder for future enhancement.
173
+ // In practice, serial numbers are typically stored in device metadata or configuration.
174
+
175
+ // Close port if we opened it
176
+ if (!wasOpen && port.readable) {
177
+ await port.close();
178
+ }
179
+
180
+ return null;
181
  } catch (error) {
182
+ console.warn("Could not read serial number from port:", error);
183
+ return null;
184
  }
185
  }
186
 
187
  /**
188
+ * Interactive mode: Show native dialog for port selection
 
189
  */
190
+ async function findPortInteractive(
191
+ options: FindPortOptions
192
+ ): Promise<RobotConnection[]> {
193
+ const { onMessage } = options;
194
 
195
+ onMessage?.("Opening port selection dialog...");
 
 
 
 
 
196
 
 
 
197
  try {
198
+ // Use native browser dialog - much better UX than port diffing!
199
+ const port = await navigator.serial.requestPort();
200
+
201
+ // Open the port
202
+ await port.open({ baudRate: 1000000 });
203
+
204
+ const portName = getPortDisplayName(port);
205
+ onMessage?.(`✅ Connected to ${portName}`);
206
+
207
+ // Return unified RobotConnection object in array (consistent API)
208
+ // In interactive mode, user will need to specify robot details separately
209
+ return [
210
+ {
211
+ port,
212
+ name: portName,
213
+ isConnected: true,
214
+ robotType: "so100_follower", // Default, user can change
215
+ robotId: "interactive_robot",
216
+ serialNumber: `interactive_${Date.now()}`,
217
+ },
218
+ ];
219
  } catch (error) {
220
+ if (
221
+ error instanceof Error &&
222
+ (error.message.includes("cancelled") || error.name === "NotAllowedError")
223
+ ) {
224
+ throw new Error("Port selection cancelled by user");
225
+ }
226
  throw new Error(
227
+ `Failed to select port: ${error instanceof Error ? error.message : error}`
 
 
228
  );
229
  }
230
+ }
231
 
232
+ /**
233
+ * Auto-connect mode: Connect to robots by serial number
234
+ * Returns all successfully connected robots
235
+ */
236
+ async function findPortAutoConnect(
237
+ robotConfigs: RobotConfig[],
238
+ options: FindPortOptions
239
+ ): Promise<RobotConnection[]> {
240
+ const { onMessage } = options;
241
+ const results: RobotConnection[] = [];
242
+
243
+ onMessage?.(`🔍 Auto-connecting to ${robotConfigs.length} robot(s)...`);
244
+
245
+ // Get all available ports
246
+ const availablePorts = await navigator.serial.getPorts();
247
+ onMessage?.(`Found ${availablePorts.length} available port(s)`);
248
+
249
+ for (const config of robotConfigs) {
250
+ onMessage?.(`Connecting to ${config.robotId} (${config.serialNumber})...`);
251
+
252
+ let connected = false;
253
+ let matchedPort: SerialPort | null = null;
254
+ let error: string | undefined;
255
+
256
+ try {
257
+ // For now, we'll try each available port and see if we can connect
258
+ // In a future enhancement, we could match by actual serial number reading
259
+ for (const port of availablePorts) {
260
+ try {
261
+ // Try to open and use this port
262
+ const wasOpen = port.readable !== null;
263
+ if (!wasOpen) {
264
+ await port.open({ baudRate: 1000000 });
265
+ }
266
+
267
+ // Test connection by trying to communicate
268
+ const manager = getRobotConnectionManager();
269
+ await manager.connect(
270
+ port,
271
+ config.robotType,
272
+ config.robotId,
273
+ config.serialNumber
274
+ );
275
+
276
+ matchedPort = port;
277
+ connected = true;
278
+ onMessage?.(`✅ Connected to ${config.robotId}`);
279
+ break;
280
+ } catch (portError) {
281
+ // This port didn't work, try next one
282
+ console.log(
283
+ `Port ${getPortDisplayName(port)} didn't match ${config.robotId}:`,
284
+ portError
285
+ );
286
+ continue;
287
+ }
288
+ }
289
+
290
+ if (!connected) {
291
+ error = `No matching port found for ${config.robotId} (${config.serialNumber})`;
292
+ onMessage?.(`❌ ${error}`);
293
+ }
294
+ } catch (err) {
295
+ error = err instanceof Error ? err.message : "Unknown error";
296
+ onMessage?.(`❌ Failed to connect to ${config.robotId}: ${error}`);
297
  }
298
+
299
+ // Add result (successful or failed)
300
+ results.push({
301
+ port: matchedPort!,
302
+ name: matchedPort ? getPortDisplayName(matchedPort) : "Unknown Port",
303
+ isConnected: connected,
304
+ robotType: config.robotType,
305
+ robotId: config.robotId,
306
+ serialNumber: config.serialNumber,
307
+ error,
308
+ });
309
  }
310
 
311
+ const successCount = results.filter((r) => r.isConnected).length;
312
+ onMessage?.(
313
+ `🎯 Connected to ${successCount}/${robotConfigs.length} robot(s)`
 
 
 
314
  );
315
 
316
+ return results;
317
+ }
 
 
 
 
 
318
 
319
+ /**
320
+ * Main findPort function - clean API with Two modes:
321
+ *
322
+ * Mode 1: Interactive - Returns single RobotConnection
323
+ * Mode 2: Auto-connect - Returns RobotConnection[]
324
+ */
325
+ export async function findPort(
326
+ options: FindPortOptions = {}
327
+ ): Promise<FindPortProcess> {
328
+ // Check WebSerial support
329
+ if (!isWebSerialSupported()) {
330
+ throw new Error(
331
+ "WebSerial API not supported. Please use Chrome/Edge 89+ with HTTPS or localhost."
332
+ );
333
+ }
334
+
335
+ const { robotConfigs, onMessage } = options;
336
+ let stopped = false;
337
+
338
+ // Determine mode
339
+ const isAutoConnectMode = robotConfigs && robotConfigs.length > 0;
340
 
341
+ onMessage?.(
342
+ `🤖 ${
343
+ isAutoConnectMode ? "Auto-connect" : "Interactive"
344
+ } port discovery started`
 
 
 
345
  );
346
 
347
+ // Create result promise
348
+ const resultPromise = (async () => {
349
+ if (stopped) {
350
+ throw new Error("Port discovery was stopped");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  }
 
352
 
353
+ if (isAutoConnectMode) {
354
+ return await findPortAutoConnect(robotConfigs!, options);
355
+ } else {
356
+ return await findPortInteractive(options);
357
+ }
358
+ })();
359
+
360
+ // Return process object
361
+ return {
362
+ result: resultPromise,
363
+ stop: () => {
364
+ stopped = true;
365
+ onMessage?.("🛑 Port discovery stopped");
366
+ },
367
+ };
 
368
  }
369
+
370
+ // Export the main function (renamed from findPortWeb)
371
+ export { findPort as findPortWeb }; // Backward compatibility alias
src/lerobot/web/robot-connection.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
  readAllMotorPositions as readAllMotorPositionsUtil,
11
  writeMotorRegister,
12
  type MotorCommunicationPort,
13
- } from "./motor-utils.js";
14
 
15
  export interface RobotConnectionState {
16
  isConnected: boolean;
 
10
  readAllMotorPositions as readAllMotorPositionsUtil,
11
  writeMotorRegister,
12
  type MotorCommunicationPort,
13
+ } from "./utils/motor-communication.js";
14
 
15
  export interface RobotConnectionState {
16
  isConnected: boolean;
src/lerobot/web/robots/so100_config.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * SO-100 specific configuration for web calibration
3
+ * Matches Node.js SO-100 config structure and Python lerobot exactly
4
+ */
5
+
6
+ /**
7
+ * STS3215 Protocol Configuration for SO-100 devices
8
+ */
9
+ export const WEB_STS3215_PROTOCOL = {
10
+ resolution: 4096, // 12-bit resolution (0-4095)
11
+ homingOffsetAddress: 31, // Address for Homing_Offset register
12
+ homingOffsetLength: 2, // 2 bytes for Homing_Offset
13
+ presentPositionAddress: 56, // Address for Present_Position register
14
+ presentPositionLength: 2, // 2 bytes for Present_Position
15
+ minPositionLimitAddress: 9, // Address for Min_Position_Limit register
16
+ minPositionLimitLength: 2, // 2 bytes for Min_Position_Limit
17
+ maxPositionLimitAddress: 11, // Address for Max_Position_Limit register
18
+ maxPositionLimitLength: 2, // 2 bytes for Max_Position_Limit
19
+ signMagnitudeBit: 11, // Bit 11 is sign bit for Homing_Offset encoding
20
+ } as const;
21
+
22
+ /**
23
+ * SO-100 Device Configuration
24
+ * Motor names, IDs, and drive modes for both follower and leader
25
+ */
26
+ export const SO100_CONFIG = {
27
+ motorNames: [
28
+ "shoulder_pan",
29
+ "shoulder_lift",
30
+ "elbow_flex",
31
+ "wrist_flex",
32
+ "wrist_roll",
33
+ "gripper",
34
+ ],
35
+ motorIds: [1, 2, 3, 4, 5, 6],
36
+ // Python lerobot uses drive_mode=0 for all SO-100 motors
37
+ driveModes: [0, 0, 0, 0, 0, 0],
38
+ };
39
+
40
+ /**
41
+ * Create SO-100 calibration configuration
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
+ }
src/lerobot/web/teleoperate.ts CHANGED
@@ -58,7 +58,8 @@ export const KEYBOARD_CONTROLS = {
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
- " ": { motor: "gripper", direction: 1, description: "Gripper toggle" },
 
62
 
63
  // Emergency stop
64
  Escape: {
 
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: {
src/lerobot/web/utils/motor-calibration.ts ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Motor Calibration Utilities
3
+ * Specialized functions for motor calibration procedures
4
+ */
5
+
6
+ import { STS3215_PROTOCOL } from "./sts3215-protocol.js";
7
+ import { encodeSignMagnitude } from "./sign-magnitude.js";
8
+ import {
9
+ readAllMotorPositions,
10
+ writeMotorRegister,
11
+ type MotorCommunicationPort,
12
+ } from "./motor-communication.js";
13
+
14
+ /**
15
+ * Reset homing offsets to 0 for all motors (matches Node.js exactly)
16
+ */
17
+ export async function resetHomingOffsets(
18
+ port: MotorCommunicationPort,
19
+ motorIds: number[]
20
+ ): Promise<void> {
21
+ for (let i = 0; i < motorIds.length; i++) {
22
+ const motorId = motorIds[i];
23
+
24
+ try {
25
+ const packet = new Uint8Array([
26
+ 0xff,
27
+ 0xff,
28
+ motorId,
29
+ 0x05,
30
+ 0x03,
31
+ STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS,
32
+ 0x00, // Low byte of 0
33
+ 0x00, // High byte of 0
34
+ 0x00, // Checksum
35
+ ]);
36
+
37
+ const checksum =
38
+ ~(
39
+ motorId +
40
+ 0x05 +
41
+ 0x03 +
42
+ STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS +
43
+ 0x00 +
44
+ 0x00
45
+ ) & 0xff;
46
+ packet[8] = checksum;
47
+
48
+ await port.write(packet);
49
+
50
+ try {
51
+ await port.read(200);
52
+ } catch (error) {
53
+ // Silent - response not required
54
+ }
55
+ } catch (error) {
56
+ throw new Error(`Failed to reset homing offset for motor ${motorId}`);
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Write homing offsets to motor registers immediately
63
+ */
64
+ export async function writeHomingOffsetsToMotors(
65
+ port: MotorCommunicationPort,
66
+ motorIds: number[],
67
+ motorNames: string[],
68
+ homingOffsets: { [motor: string]: number }
69
+ ): Promise<void> {
70
+ for (let i = 0; i < motorIds.length; i++) {
71
+ const motorId = motorIds[i];
72
+ const motorName = motorNames[i];
73
+ const homingOffset = homingOffsets[motorName];
74
+
75
+ try {
76
+ const encodedOffset = encodeSignMagnitude(
77
+ homingOffset,
78
+ STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT
79
+ );
80
+
81
+ const packet = new Uint8Array([
82
+ 0xff,
83
+ 0xff,
84
+ motorId,
85
+ 0x05,
86
+ 0x03,
87
+ STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS,
88
+ encodedOffset & 0xff,
89
+ (encodedOffset >> 8) & 0xff,
90
+ 0x00,
91
+ ]);
92
+
93
+ const checksum =
94
+ ~(
95
+ motorId +
96
+ 0x05 +
97
+ 0x03 +
98
+ STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS +
99
+ (encodedOffset & 0xff) +
100
+ ((encodedOffset >> 8) & 0xff)
101
+ ) & 0xff;
102
+ packet[8] = checksum;
103
+
104
+ await port.write(packet);
105
+
106
+ try {
107
+ await port.read(200);
108
+ } catch (error) {
109
+ // Silent - response not required
110
+ }
111
+ } catch (error) {
112
+ throw new Error(`Failed to write homing offset for ${motorName}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Set homing offsets with immediate writing (matches Node.js exactly)
119
+ */
120
+ export async function setHomingOffsets(
121
+ port: MotorCommunicationPort,
122
+ motorIds: number[],
123
+ motorNames: string[]
124
+ ): Promise<{ [motor: string]: number }> {
125
+ // Reset existing homing offsets to 0 first
126
+ await resetHomingOffsets(port, motorIds);
127
+ await new Promise((resolve) => setTimeout(resolve, 100));
128
+
129
+ // Read positions (which will be true physical positions)
130
+ const currentPositions = await readAllMotorPositions(port, motorIds);
131
+ const homingOffsets: { [motor: string]: number } = {};
132
+
133
+ const halfTurn = Math.floor((STS3215_PROTOCOL.RESOLUTION - 1) / 2);
134
+
135
+ for (let i = 0; i < motorNames.length; i++) {
136
+ const motorName = motorNames[i];
137
+ const position = currentPositions[i];
138
+ homingOffsets[motorName] = position - halfTurn;
139
+ }
140
+
141
+ // Write homing offsets to motors immediately
142
+ await writeHomingOffsetsToMotors(port, motorIds, motorNames, homingOffsets);
143
+
144
+ return homingOffsets;
145
+ }
146
+
147
+ /**
148
+ * Write hardware position limits to motors
149
+ */
150
+ export async function writeHardwarePositionLimits(
151
+ port: MotorCommunicationPort,
152
+ motorIds: number[],
153
+ motorNames: string[],
154
+ rangeMins: { [motor: string]: number },
155
+ rangeMaxes: { [motor: string]: number }
156
+ ): Promise<void> {
157
+ for (let i = 0; i < motorIds.length; i++) {
158
+ const motorId = motorIds[i];
159
+ const motorName = motorNames[i];
160
+ const minLimit = rangeMins[motorName];
161
+ const maxLimit = rangeMaxes[motorName];
162
+
163
+ try {
164
+ // Write Min_Position_Limit register
165
+ await writeMotorRegister(
166
+ port,
167
+ motorId,
168
+ STS3215_PROTOCOL.MIN_POSITION_LIMIT_ADDRESS,
169
+ minLimit
170
+ );
171
+
172
+ // Write Max_Position_Limit register
173
+ await writeMotorRegister(
174
+ port,
175
+ motorId,
176
+ STS3215_PROTOCOL.MAX_POSITION_LIMIT_ADDRESS,
177
+ maxLimit
178
+ );
179
+ } catch (error) {
180
+ throw new Error(`Failed to write position limits for ${motorName}`);
181
+ }
182
+ }
183
+ }
src/lerobot/web/{motor-utils.ts → utils/motor-communication.ts} RENAMED
@@ -1,40 +1,22 @@
1
  /**
2
- * Shared Motor Communication Utilities
3
- * Proven patterns from calibrate.ts for consistent motor communication
4
- * Used by both calibration and teleoperation
5
  */
6
 
 
 
 
 
 
 
7
  export interface MotorCommunicationPort {
8
  write(data: Uint8Array): Promise<void>;
9
  read(timeout?: number): Promise<Uint8Array>;
10
  }
11
 
12
- /**
13
- * STS3215 Protocol Constants
14
- * Single source of truth for all motor communication
15
- */
16
- export const STS3215_PROTOCOL = {
17
- // Register addresses
18
- PRESENT_POSITION_ADDRESS: 56,
19
- GOAL_POSITION_ADDRESS: 42,
20
- HOMING_OFFSET_ADDRESS: 31,
21
- MIN_POSITION_LIMIT_ADDRESS: 9,
22
- MAX_POSITION_LIMIT_ADDRESS: 11,
23
-
24
- // Protocol constants
25
- RESOLUTION: 4096, // 12-bit resolution (0-4095)
26
- SIGN_MAGNITUDE_BIT: 11, // Bit 11 is sign bit for Homing_Offset encoding
27
-
28
- // Communication timing (proven from calibration)
29
- WRITE_TO_READ_DELAY: 10,
30
- RETRY_DELAY: 20,
31
- INTER_MOTOR_DELAY: 10,
32
- MAX_RETRIES: 3,
33
- } as const;
34
-
35
  /**
36
  * Read single motor position with PROVEN retry logic
37
- * Reuses exact patterns from calibrate.ts
38
  */
39
  export async function readMotorPosition(
40
  port: MotorCommunicationPort,
@@ -94,13 +76,7 @@ export async function readMotorPosition(
94
  if (id === motorId && error === 0) {
95
  const position = response[5] | (response[6] << 8);
96
  return position;
97
- } else if (id === motorId && error !== 0) {
98
- // Motor error, retry
99
- } else {
100
- // Wrong response ID, retry
101
  }
102
- } else {
103
- // Short response, retry
104
  }
105
  } catch (readError) {
106
  // Read timeout, retry
@@ -117,7 +93,6 @@ export async function readMotorPosition(
117
  // If all attempts failed, return null
118
  return null;
119
  } catch (error) {
120
- console.warn(`Failed to read motor ${motorId} position:`, error);
121
  return null;
122
  }
123
  }
@@ -155,7 +130,7 @@ export async function readAllMotorPositions(
155
  }
156
 
157
  /**
158
- * Write motor position with error handling
159
  */
160
  export async function writeMotorPosition(
161
  port: MotorCommunicationPort,
@@ -237,30 +212,71 @@ export async function writeMotorRegister(
237
  }
238
 
239
  /**
240
- * Sign-magnitude encoding functions (from calibrate.ts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  */
242
- export function encodeSignMagnitude(
243
- value: number,
244
- signBitIndex: number = STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT
245
- ): number {
246
- const maxMagnitude = (1 << signBitIndex) - 1;
247
- const magnitude = Math.abs(value);
 
 
 
 
 
 
 
 
 
248
 
249
- if (magnitude > maxMagnitude) {
250
- throw new Error(
251
- `Magnitude ${magnitude} exceeds ${maxMagnitude} (max for signBitIndex=${signBitIndex})`
 
 
 
 
 
 
 
 
 
252
  );
253
  }
254
-
255
- const directionBit = value < 0 ? 1 : 0;
256
- return (directionBit << signBitIndex) | magnitude;
257
  }
258
 
259
- export function decodeSignMagnitude(
260
- encodedValue: number,
261
- signBitIndex: number = STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT
262
- ): number {
263
- const signBit = (encodedValue >> signBitIndex) & 1;
264
- const magnitude = encodedValue & ((1 << signBitIndex) - 1);
265
- return signBit ? -magnitude : magnitude;
 
 
 
 
 
 
 
266
  }
 
1
  /**
2
+ * Motor Communication Utilities
3
+ * Proven patterns for STS3215 motor reading and writing operations
 
4
  */
5
 
6
+ import { STS3215_PROTOCOL } from "./sts3215-protocol.js";
7
+
8
+ /**
9
+ * Interface for motor communication port
10
+ * Compatible with both WebSerialPortWrapper and RobotConnectionManager
11
+ */
12
  export interface MotorCommunicationPort {
13
  write(data: Uint8Array): Promise<void>;
14
  read(timeout?: number): Promise<Uint8Array>;
15
  }
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  /**
18
  * Read single motor position with PROVEN retry logic
19
+ * Extracted from calibrate.ts with all proven timing and retry patterns
20
  */
21
  export async function readMotorPosition(
22
  port: MotorCommunicationPort,
 
76
  if (id === motorId && error === 0) {
77
  const position = response[5] | (response[6] << 8);
78
  return position;
 
 
 
 
79
  }
 
 
80
  }
81
  } catch (readError) {
82
  // Read timeout, retry
 
93
  // If all attempts failed, return null
94
  return null;
95
  } catch (error) {
 
96
  return null;
97
  }
98
  }
 
130
  }
131
 
132
  /**
133
+ * Write motor goal position
134
  */
135
  export async function writeMotorPosition(
136
  port: MotorCommunicationPort,
 
212
  }
213
 
214
  /**
215
+ * Lock a motor (motor will hold its position and resist movement)
216
+ */
217
+ export async function lockMotor(
218
+ port: MotorCommunicationPort,
219
+ motorId: number
220
+ ): Promise<void> {
221
+ await writeMotorRegister(
222
+ port,
223
+ motorId,
224
+ STS3215_PROTOCOL.TORQUE_ENABLE_ADDRESS,
225
+ 1
226
+ );
227
+ // Small delay for command processing
228
+ await new Promise((resolve) =>
229
+ setTimeout(resolve, STS3215_PROTOCOL.WRITE_TO_READ_DELAY)
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Release a motor (motor can be moved freely by hand)
235
  */
236
+ export async function releaseMotor(
237
+ port: MotorCommunicationPort,
238
+ motorId: number
239
+ ): Promise<void> {
240
+ await writeMotorRegister(
241
+ port,
242
+ motorId,
243
+ STS3215_PROTOCOL.TORQUE_ENABLE_ADDRESS,
244
+ 0
245
+ );
246
+ // Small delay for command processing
247
+ await new Promise((resolve) =>
248
+ setTimeout(resolve, STS3215_PROTOCOL.WRITE_TO_READ_DELAY)
249
+ );
250
+ }
251
 
252
+ /**
253
+ * Release motors (motors can be moved freely - perfect for calibration)
254
+ */
255
+ export async function releaseMotors(
256
+ port: MotorCommunicationPort,
257
+ motorIds: number[]
258
+ ): Promise<void> {
259
+ for (const motorId of motorIds) {
260
+ await releaseMotor(port, motorId);
261
+ // Small delay between motors
262
+ await new Promise((resolve) =>
263
+ setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY)
264
  );
265
  }
 
 
 
266
  }
267
 
268
+ /**
269
+ * Lock motors (motors will hold their positions - perfect after calibration)
270
+ */
271
+ export async function lockMotors(
272
+ port: MotorCommunicationPort,
273
+ motorIds: number[]
274
+ ): Promise<void> {
275
+ for (const motorId of motorIds) {
276
+ await lockMotor(port, motorId);
277
+ // Small delay between motors
278
+ await new Promise((resolve) =>
279
+ setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY)
280
+ );
281
+ }
282
  }
src/lerobot/web/utils/serial-port-wrapper.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Web Serial Port Wrapper
3
+ * Standardized Web Serial API interface with immediate lock release
4
+ */
5
+
6
+ /**
7
+ * Web Serial Port wrapper - direct write/read with immediate lock release
8
+ * Follows Chrome documentation exactly for proper Web Serial API usage
9
+ */
10
+ export class WebSerialPortWrapper {
11
+ private port: SerialPort;
12
+
13
+ constructor(port: SerialPort) {
14
+ this.port = port;
15
+ }
16
+
17
+ get isOpen(): boolean {
18
+ return (
19
+ this.port !== null &&
20
+ this.port.readable !== null &&
21
+ this.port.writable !== null
22
+ );
23
+ }
24
+
25
+ async initialize(): Promise<void> {
26
+ if (!this.port.readable || !this.port.writable) {
27
+ throw new Error("Port is not open for reading/writing");
28
+ }
29
+ }
30
+
31
+ async write(data: Uint8Array): Promise<void> {
32
+ if (!this.port.writable) {
33
+ throw new Error("Port not open for writing");
34
+ }
35
+
36
+ const writer = this.port.writable.getWriter();
37
+ try {
38
+ await writer.write(data);
39
+ } finally {
40
+ writer.releaseLock();
41
+ }
42
+ }
43
+
44
+ async read(timeout: number = 1000): Promise<Uint8Array> {
45
+ if (!this.port.readable) {
46
+ throw new Error("Port not open for reading");
47
+ }
48
+
49
+ const reader = this.port.readable.getReader();
50
+
51
+ try {
52
+ const timeoutPromise = new Promise<never>((_, reject) => {
53
+ setTimeout(() => reject(new Error("Read timeout")), timeout);
54
+ });
55
+
56
+ const result = await Promise.race([reader.read(), timeoutPromise]);
57
+ const { value, done } = result;
58
+
59
+ if (done || !value) {
60
+ throw new Error("Read failed - port closed or no data");
61
+ }
62
+
63
+ return new Uint8Array(value);
64
+ } finally {
65
+ reader.releaseLock();
66
+ }
67
+ }
68
+
69
+ async close(): Promise<void> {
70
+ // Don't close the port itself - just wrapper cleanup
71
+ }
72
+ }
src/lerobot/web/utils/sign-magnitude.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Sign-Magnitude Encoding Utilities
3
+ * For STS3215 motor homing offset encoding/decoding
4
+ */
5
+
6
+ import { STS3215_PROTOCOL } from "./sts3215-protocol.js";
7
+
8
+ /**
9
+ * Encode a signed integer using sign-magnitude format
10
+ * Bit at sign_bit_index represents sign (0=positive, 1=negative)
11
+ * Lower bits represent magnitude
12
+ */
13
+ export function encodeSignMagnitude(
14
+ value: number,
15
+ signBitIndex: number = STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT
16
+ ): number {
17
+ const maxMagnitude = (1 << signBitIndex) - 1;
18
+ const magnitude = Math.abs(value);
19
+
20
+ if (magnitude > maxMagnitude) {
21
+ throw new Error(
22
+ `Magnitude ${magnitude} exceeds ${maxMagnitude} (max for signBitIndex=${signBitIndex})`
23
+ );
24
+ }
25
+
26
+ const directionBit = value < 0 ? 1 : 0;
27
+ return (directionBit << signBitIndex) | magnitude;
28
+ }
29
+
30
+ /**
31
+ * Decode a sign-magnitude encoded value back to signed integer
32
+ * Extracts sign bit and magnitude, then applies sign
33
+ */
34
+ export function decodeSignMagnitude(
35
+ encodedValue: number,
36
+ signBitIndex: number = STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT
37
+ ): number {
38
+ const signBit = (encodedValue >> signBitIndex) & 1;
39
+ const magnitude = encodedValue & ((1 << signBitIndex) - 1);
40
+ return signBit ? -magnitude : magnitude;
41
+ }
src/lerobot/web/utils/sts3215-protocol.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * STS3215 Protocol Constants and Configuration
3
+ * Single source of truth for all STS3215 motor communication
4
+ */
5
+
6
+ /**
7
+ * STS3215 Protocol Configuration
8
+ * Register addresses, timing, and communication constants
9
+ */
10
+ export const STS3215_PROTOCOL = {
11
+ // Register addresses
12
+ PRESENT_POSITION_ADDRESS: 56,
13
+ GOAL_POSITION_ADDRESS: 42,
14
+ HOMING_OFFSET_ADDRESS: 31,
15
+ MIN_POSITION_LIMIT_ADDRESS: 9,
16
+ MAX_POSITION_LIMIT_ADDRESS: 11,
17
+ TORQUE_ENABLE_ADDRESS: 40, // Torque Enable register (0=disable, 1=enable)
18
+
19
+ // Protocol constants
20
+ RESOLUTION: 4096, // 12-bit resolution (0-4095)
21
+ SIGN_MAGNITUDE_BIT: 11, // Bit 11 is sign bit for Homing_Offset encoding
22
+
23
+ // Data lengths
24
+ HOMING_OFFSET_LENGTH: 2,
25
+ PRESENT_POSITION_LENGTH: 2,
26
+ MIN_POSITION_LIMIT_LENGTH: 2,
27
+ MAX_POSITION_LIMIT_LENGTH: 2,
28
+
29
+ // Communication timing (proven from calibration)
30
+ WRITE_TO_READ_DELAY: 10,
31
+ RETRY_DELAY: 20,
32
+ INTER_MOTOR_DELAY: 10,
33
+ MAX_RETRIES: 3,
34
+ } as const;
tsconfig.json CHANGED
@@ -6,6 +6,9 @@
6
  "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
  "skipLibCheck": true,
8
 
 
 
 
9
  /* Bundler mode */
10
  "moduleResolution": "bundler",
11
  "allowImportingTsExtensions": true,
 
6
  "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
  "skipLibCheck": true,
8
 
9
+ /* React */
10
+ "jsx": "react-jsx",
11
+
12
  /* Bundler mode */
13
  "moduleResolution": "bundler",
14
  "allowImportingTsExtensions": true,