NERDDISCO commited on
Commit
c8b4583
·
1 Parent(s): 540cfa6

feat: separated node & web

Browse files
README.md ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🤖 lerobot.js
2
+
3
+ **State-of-the-art AI for real-world robotics in JavaScript/TypeScript**
4
+
5
+ A faithful TypeScript/JavaScript port of [Hugging Face's lerobot](https://github.com/huggingface/lerobot), bringing cutting-edge robotics AI to the JavaScript ecosystem with **zero Python dependencies**.
6
+
7
+ ## ✨ Features
8
+
9
+ - 🔌 **USB Port Detection**: Find robot arm serial ports in Node.js CLI and browser
10
+ - 🌐 **Universal**: Works in Node.js, browsers, and Edge devices
11
+ - 🎯 **Python Faithful**: Identical UX and messaging to original lerobot
12
+ - 📱 **WebSerial**: Browser-native serial port access (Chrome/Edge 89+)
13
+ - 🚀 **Zero Dependencies**: No Python runtime required
14
+ - 📦 **Lightweight**: ~2.3KB package size
15
+
16
+ ## 🚀 Quick Start
17
+
18
+ ### CLI Usage (Node.js)
19
+
20
+ ```bash
21
+ # Use directly without installation
22
+ npx lerobot find-port
23
+
24
+ # Or install globally
25
+ npm install -g lerobot
26
+ lerobot find-port
27
+ ```
28
+
29
+ ### Browser Usage
30
+
31
+ 1. **Development**:
32
+
33
+ ```bash
34
+ git clone https://github.com/timpietrusky/lerobot.js
35
+ cd lerobot.js
36
+ pnpm install
37
+ pnpm run dev
38
+ ```
39
+
40
+ 2. **Visit**: `http://localhost:5173`
41
+
42
+ 3. **Use the interface**:
43
+ - Click "Show Available Ports" to see connected devices
44
+ - Click "Find MotorsBus Port" for guided port detection
45
+
46
+ ## 📖 Documentation
47
+
48
+ ### Find USB Ports
49
+
50
+ Identify which USB ports your robot arms are connected to - essential for SO-100 setup.
51
+
52
+ #### CLI Example
53
+
54
+ ```bash
55
+ $ npx lerobot find-port
56
+
57
+ Finding all available ports for the MotorsBus.
58
+ Ports before disconnecting: ['COM3', 'COM4']
59
+ Remove the USB cable from your MotorsBus and press Enter when done.
60
+
61
+ The port of this MotorsBus is 'COM3'
62
+ Reconnect the USB cable.
63
+ ```
64
+
65
+ #### Browser Example
66
+
67
+ 1. Click "Show Available Ports" → Browser asks for permission
68
+ 2. Grant access to your serial devices
69
+ 3. See list of connected ports
70
+ 4. Use "Find MotorsBus Port" for detection workflow
71
+
72
+ ### Platform Support
73
+
74
+ | Platform | Method | Requirements |
75
+ | ----------- | ----------------------- | ----------------------------------- |
76
+ | **Node.js** | `npx lerobot find-port` | Node.js 18+, Windows/macOS/Linux |
77
+ | **Browser** | Web interface | Chrome/Edge 89+, HTTPS or localhost |
78
+ | **Mobile** | Browser | Chrome Android 105+ |
79
+
80
+ ### Browser Compatibility
81
+
82
+ The browser version uses the [WebSerial API](https://web.dev/serial/):
83
+
84
+ - ✅ **Chrome/Edge 89+** (Desktop)
85
+ - ✅ **Chrome Android 105+** (Mobile)
86
+ - ✅ **HTTPS** or localhost required
87
+ - ❌ Firefox/Safari (WebSerial not supported)
88
+
89
+ ## 🛠️ Development
90
+
91
+ ### Setup
92
+
93
+ ```bash
94
+ git clone https://github.com/timpietrusky/lerobot.js
95
+ cd lerobot.js
96
+ pnpm install
97
+ ```
98
+
99
+ ### Scripts
100
+
101
+ ```bash
102
+ # Development server (browser)
103
+ pnpm run dev
104
+
105
+ # Build CLI for Node.js
106
+ pnpm run build:cli
107
+
108
+ # Build web interface
109
+ pnpm run build:web
110
+
111
+ # Test CLI locally
112
+ pnpm run cli:find-port
113
+
114
+ # Build everything
115
+ pnpm run build
116
+ ```
117
+
118
+ ### Project Structure
119
+
120
+ ```
121
+ src/
122
+ ├── lerobot/
123
+ │ ├── node/
124
+ │ │ └── find_port.ts # Node.js implementation
125
+ │ └── web/
126
+ │ └── find_port.ts # Browser implementation (WebSerial)
127
+ ├── cli/
128
+ │ └── index.ts # CLI interface
129
+ ├── main.ts # Web interface
130
+ └── web_interface.css # UI styles
131
+ ```
132
+
133
+ ## 🎯 Design Principles
134
+
135
+ ### 1. Python lerobot Faithfulness
136
+
137
+ - **Identical commands**: `npx lerobot find-port` ↔ `python -m lerobot.find_port`
138
+ - **Same terminology**: "MotorsBus", not "robot arms"
139
+ - **Matching output**: Error messages and workflows identical
140
+ - **Familiar UX**: Python lerobot users feel immediately at home
141
+
142
+ ### 2. Platform Abstraction
143
+
144
+ - **Universal core**: Shared robotics logic
145
+ - **Adaptive UX**: CLI prompts vs. browser modals
146
+ - **Progressive enhancement**: Works everywhere, enhanced on capable platforms
147
+
148
+ ### 3. Zero Dependencies
149
+
150
+ - **Node.js**: Only uses built-in modules + lightweight `serialport`
151
+ - **Browser**: Native WebSerial API, no external libraries
152
+ - **Deployment**: Single package, no Python runtime needed
153
+
154
+ ## 🔧 Technical Details
155
+
156
+ ### WebSerial API
157
+
158
+ The browser implementation leverages modern web APIs:
159
+
160
+ ```typescript
161
+ // Request permission to access serial ports
162
+ await navigator.serial.requestPort();
163
+
164
+ // List granted ports
165
+ const ports = await navigator.serial.getPorts();
166
+
167
+ // Detect disconnected devices
168
+ const removedPorts = portsBefore.filter(/* ... */);
169
+ ```
170
+
171
+ ### CLI Implementation
172
+
173
+ Node.js version uses the `serialport` library for cross-platform compatibility:
174
+
175
+ ```typescript
176
+ import { SerialPort } from "serialport";
177
+
178
+ // Cross-platform port detection
179
+ const ports = await SerialPort.list();
180
+ ```
181
+
182
+ ## 🗺️ Roadmap
183
+
184
+ - [x] **Phase 1**: USB port detection (CLI + Browser)
185
+ - [ ] **Phase 2**: Motor communication and setup
186
+ - [ ] **Phase 3**: Robot calibration tools
187
+ - [ ] **Phase 4**: Dataset management and visualization
188
+ - [ ] **Phase 5**: Policy inference (ONNX.js)
189
+ - [ ] **Phase 6**: Training infrastructure
190
+
191
+ ## 🤝 Contributing
192
+
193
+ We welcome contributions! This project follows the principle of **Python lerobot faithfulness** - all features should maintain identical UX to the original.
194
+
195
+ ### Guidelines
196
+
197
+ 1. Check Python lerobot implementation first
198
+ 2. Maintain identical command structure and messaging
199
+ 3. Follow snake_case file naming convention
200
+ 4. Test on both Node.js and browser platforms
201
+
202
+ ## 📄 License
203
+
204
+ Apache 2.0 - Same as original lerobot
205
+
206
+ ## 🙏 Acknowledgments
207
+
208
+ - [Hugging Face lerobot team](https://github.com/huggingface/lerobot) for the original Python implementation
209
+ - [WebSerial API](https://web.dev/serial/) for browser-native hardware access
210
+ - [serialport](https://github.com/serialport/node-serialport) for Node.js cross-platform support
211
+
212
+ ---
213
+
214
+ **Built with ❤️ for the robotics community**
docs/conventions.md CHANGED
@@ -8,6 +8,10 @@
8
 
9
  > Lower the barrier to entry for robotics by making cutting-edge robotic AI accessible through JavaScript, the world's most widely used programming language.
10
 
 
 
 
 
11
  ## Project Goals
12
 
13
  ### Primary Objectives
@@ -63,8 +67,8 @@ lerobot/
63
  │ ├── devices/ # Hardware device interfaces
64
  │ └── utils/ # Shared utilities
65
  ├── core/ # Core robotics primitives
66
- ├── web/ # Browser-specific implementations
67
- └── node/ # Node.js-specific implementations
68
  ```
69
 
70
  ### 3. Platform Abstraction
 
8
 
9
  > Lower the barrier to entry for robotics by making cutting-edge robotic AI accessible through JavaScript, the world's most widely used programming language.
10
 
11
+ ## Core Rules
12
+
13
+ - you never start the dev server, because it is already running
14
+
15
  ## Project Goals
16
 
17
  ### Primary Objectives
 
67
  │ ├── devices/ # Hardware device interfaces
68
  │ └── utils/ # Shared utilities
69
  ├── core/ # Core robotics primitives
70
+ ├── node/ # Node.js-specific implementations
71
+ └── web/ # Browser-specific implementations
72
  ```
73
 
74
  ### 3. Platform Abstraction
index.html CHANGED
@@ -1,13 +1,32 @@
1
- <!doctype html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Vite + TS</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
10
  <div id="app"></div>
11
  <script type="module" src="/src/main.ts"></script>
 
 
 
 
 
 
12
  </body>
13
  </html>
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>🤖 lerobot.js - Robotics in the Browser</title>
7
+ <meta
8
+ name="description"
9
+ content="State-of-the-art AI for real-world robotics in JavaScript/TypeScript"
10
+ />
11
+ <style>
12
+ /* Prevent flash of unstyled content */
13
+ body {
14
+ opacity: 0;
15
+ }
16
+ body.loaded {
17
+ opacity: 1;
18
+ transition: opacity 0.3s ease;
19
+ }
20
+ </style>
21
  </head>
22
  <body>
23
  <div id="app"></div>
24
  <script type="module" src="/src/main.ts"></script>
25
+ <script>
26
+ // Add loaded class when page is ready
27
+ window.addEventListener("load", () => {
28
+ document.body.classList.add("loaded");
29
+ });
30
+ </script>
31
  </body>
32
  </html>
public/vite.svg DELETED
src/cli/index.ts CHANGED
@@ -7,7 +7,7 @@
7
  * Maintains compatibility with Python lerobot command structure
8
  */
9
 
10
- import { findPort } from "../lerobot/find_port.js";
11
 
12
  /**
13
  * Show usage information
 
7
  * Maintains compatibility with Python lerobot command structure
8
  */
9
 
10
+ import { findPort } from "../lerobot/node/find_port.js";
11
 
12
  /**
13
  * Show usage information
src/counter.ts DELETED
@@ -1,9 +0,0 @@
1
- export function setupCounter(element: HTMLButtonElement) {
2
- let counter = 0
3
- const setCounter = (count: number) => {
4
- counter = count
5
- element.innerHTML = `count is ${counter}`
6
- }
7
- element.addEventListener('click', () => setCounter(counter + 1))
8
- setCounter(0)
9
- }
 
 
 
 
 
 
 
 
 
 
src/lerobot/{find_port.ts → node/find_port.ts} RENAMED
File without changes
src/lerobot/web/find_port.ts ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ */
11
+ interface SerialPort {
12
+ readonly readable: ReadableStream;
13
+ readonly writable: WritableStream;
14
+ getInfo(): SerialPortInfo;
15
+ open(options: SerialOptions): Promise<void>;
16
+ close(): Promise<void>;
17
+ }
18
+
19
+ interface SerialPortInfo {
20
+ usbVendorId?: number;
21
+ usbProductId?: number;
22
+ }
23
+
24
+ interface SerialOptions {
25
+ baudRate: number;
26
+ dataBits?: number;
27
+ stopBits?: number;
28
+ parity?: "none" | "even" | "odd";
29
+ }
30
+
31
+ interface Serial extends EventTarget {
32
+ getPorts(): Promise<SerialPort[]>;
33
+ requestPort(options?: SerialPortRequestOptions): Promise<SerialPort>;
34
+ }
35
+
36
+ interface SerialPortRequestOptions {
37
+ filters?: SerialPortFilter[];
38
+ }
39
+
40
+ interface SerialPortFilter {
41
+ usbVendorId?: number;
42
+ usbProductId?: number;
43
+ }
44
+
45
+ declare global {
46
+ interface Navigator {
47
+ serial: Serial;
48
+ }
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
+ }
src/main.ts CHANGED
@@ -1,24 +1,204 @@
1
- import './style.css'
2
- import typescriptLogo from './typescript.svg'
3
- import viteLogo from '/vite.svg'
4
- import { setupCounter } from './counter.ts'
5
-
6
- document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
7
- <div>
8
- <a href="https://vite.dev" target="_blank">
9
- <img src="${viteLogo}" class="logo" alt="Vite logo" />
10
- </a>
11
- <a href="https://www.typescriptlang.org/" target="_blank">
12
- <img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
13
- </a>
14
- <h1>Vite + TypeScript</h1>
15
- <div class="card">
16
- <button id="counter" type="button"></button>
17
- </div>
18
- <p class="read-the-docs">
19
- Click on the Vite and TypeScript logos to learn more
20
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </div>
22
- `
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * lerobot.js Web Interface
3
+ *
4
+ * Browser-based interface for lerobot functionality
5
+ * Provides the same find-port functionality as the CLI but in the browser
6
+ */
7
+
8
+ import "./web_interface.css";
9
+ import { findPortWeb } from "./lerobot/web/find_port.js";
10
+
11
+ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
12
+ <div class="lerobot-app">
13
+ <header class="lerobot-header">
14
+ <h1>🤖 lerobot.js</h1>
15
+ <p>use your robot in the web & node with an api similar to LeRobot in python</p>
16
+ </header>
17
+
18
+ <main class="lerobot-main">
19
+ <section class="tool-section">
20
+ <h2>🔍 Find USB Ports</h2>
21
+ <p>Identify which USB ports your robot arms are connected to</p>
22
+ <div class="button-group">
23
+ <button id="show-ports-btn" class="secondary-btn" style="display: none;">Show Available Ports</button>
24
+ <button id="manage-devices-btn" class="secondary-btn">Manage Devices</button>
25
+ <button id="find-port-btn" class="primary-btn">Find MotorsBus Port</button>
26
+ </div>
27
+ <div id="info-box" class="info-box" style="display: none;">
28
+ <span class="info-icon">💡</span>
29
+ <span class="info-text">Use "Manage Devices" to pair additional devices or "Find MotorsBus Port" to start detection.</span>
30
+ </div>
31
+ <div id="port-results" class="results-area"></div>
32
+ </section>
33
+
34
+ <section class="info-section" id="compatibility-section" style="display: none;">
35
+ <h3>Browser Compatibility Issue</h3>
36
+ <p>Your browser doesn't support the <a href="https://web.dev/serial/" target="_blank">WebSerial API</a>. Please use:</p>
37
+ <ul>
38
+ <li>Chrome/Edge 89+ or Chrome Android 105+</li>
39
+ <li>HTTPS connection (or localhost for development)</li>
40
+ </ul>
41
+ <p>Alternatively, use the <strong>CLI version</strong>: <code>npx lerobot find-port</code></p>
42
+ </section>
43
+ </main>
44
  </div>
45
+ `;
46
+
47
+ // Set up button functionality
48
+ const showPortsBtn =
49
+ document.querySelector<HTMLButtonElement>("#show-ports-btn")!;
50
+ const manageDevicesBtn = document.querySelector<HTMLButtonElement>(
51
+ "#manage-devices-btn"
52
+ )!;
53
+ const findPortBtn =
54
+ document.querySelector<HTMLButtonElement>("#find-port-btn")!;
55
+ const resultsArea = document.querySelector<HTMLDivElement>("#port-results")!;
56
+ const infoBox = document.querySelector<HTMLDivElement>("#info-box")!;
57
+
58
+ // Function to display paired devices
59
+ async function displayPairedDevices() {
60
+ try {
61
+ // Check WebSerial support
62
+ if (!("serial" in navigator)) {
63
+ resultsArea.innerHTML =
64
+ '<p class="error">WebSerial API not supported. Please use Chrome/Edge 89+ with HTTPS or localhost.</p>';
65
+ infoBox.style.display = "none";
66
+ showPortsBtn.style.display = "inline-block";
67
+ return;
68
+ }
69
+
70
+ // Check what ports we already have access to
71
+ const ports = await navigator.serial.getPorts();
72
+
73
+ if (ports.length > 0) {
74
+ // We have paired devices, show them
75
+ resultsArea.innerHTML = `<p class="success">Found ${ports.length} paired device(s):</p>`;
76
+ ports.forEach((port, index) => {
77
+ const info = port.getInfo();
78
+ if (info.usbVendorId && info.usbProductId) {
79
+ resultsArea.innerHTML += `<p class="log">Port ${index + 1}: USB:${
80
+ info.usbVendorId
81
+ }:${info.usbProductId}</p>`;
82
+ } else {
83
+ resultsArea.innerHTML += `<p class="log">Port ${
84
+ index + 1
85
+ }: Serial device</p>`;
86
+ }
87
+ });
88
+
89
+ // Show the info box with guidance
90
+ infoBox.style.display = "flex";
91
+
92
+ // Hide show ports button since we have devices
93
+ showPortsBtn.style.display = "none";
94
+ } else {
95
+ // No devices paired, show helpful message
96
+ resultsArea.innerHTML =
97
+ '<p class="log">No paired devices found. Click "Show Available Ports" to get started.</p>';
98
+
99
+ // Hide the info box since we don't have devices
100
+ infoBox.style.display = "none";
101
+
102
+ // Show the show ports button since we need it
103
+ showPortsBtn.style.display = "inline-block";
104
+ }
105
+ } catch (error) {
106
+ resultsArea.innerHTML += `<p class="error">Error checking devices: ${
107
+ error instanceof Error ? error.message : error
108
+ }</p>`;
109
+ infoBox.style.display = "none";
110
+ showPortsBtn.style.display = "inline-block";
111
+ }
112
+ }
113
+
114
+ // Check browser compatibility and show warning if needed
115
+ function checkBrowserCompatibility() {
116
+ const compatibilitySection = document.querySelector(
117
+ "#compatibility-section"
118
+ ) as HTMLElement;
119
+
120
+ if (!("serial" in navigator)) {
121
+ // Browser doesn't support WebSerial API, show compatibility warning
122
+ compatibilitySection.style.display = "block";
123
+ } else {
124
+ // Browser supports WebSerial API, hide compatibility section
125
+ compatibilitySection.style.display = "none";
126
+ }
127
+ }
128
+
129
+ // Check for paired devices and browser compatibility on page load
130
+ checkBrowserCompatibility();
131
+ displayPairedDevices();
132
+
133
+ // Show available ports button (only for when no devices are paired)
134
+ showPortsBtn.addEventListener("click", async () => {
135
+ try {
136
+ showPortsBtn.disabled = true;
137
+ showPortsBtn.textContent = "Pairing devices...";
138
+ resultsArea.innerHTML =
139
+ '<p class="status">Requesting permission to access serial ports...</p>';
140
+
141
+ try {
142
+ await navigator.serial.requestPort();
143
+ // Refresh the display
144
+ await displayPairedDevices();
145
+ } catch (permissionError) {
146
+ console.log("Permission dialog cancelled:", permissionError);
147
+ }
148
+ } catch (error) {
149
+ resultsArea.innerHTML += `<p class="error">Error: ${
150
+ error instanceof Error ? error.message : error
151
+ }</p>`;
152
+ } finally {
153
+ showPortsBtn.disabled = false;
154
+ showPortsBtn.textContent = "Show Available Ports";
155
+ }
156
+ });
157
+
158
+ // Manage devices button (always available)
159
+ manageDevicesBtn.addEventListener("click", async () => {
160
+ try {
161
+ manageDevicesBtn.disabled = true;
162
+ manageDevicesBtn.textContent = "Managing...";
163
+
164
+ // Always show the permission dialog to pair new devices
165
+ try {
166
+ await navigator.serial.requestPort();
167
+ // Refresh the display to show updated device list
168
+ await displayPairedDevices();
169
+ resultsArea.innerHTML +=
170
+ '<p class="success">Device pairing completed. Updated device list above.</p>';
171
+ } catch (permissionError) {
172
+ console.log("Permission dialog cancelled:", permissionError);
173
+ }
174
+ } catch (error) {
175
+ resultsArea.innerHTML += `<p class="error">Error: ${
176
+ error instanceof Error ? error.message : error
177
+ }</p>`;
178
+ } finally {
179
+ manageDevicesBtn.disabled = false;
180
+ manageDevicesBtn.textContent = "Manage Devices";
181
+ resultsArea.scrollTop = resultsArea.scrollHeight;
182
+ }
183
+ });
184
+
185
+ // Find port button
186
+ findPortBtn.addEventListener("click", async () => {
187
+ try {
188
+ findPortBtn.disabled = true;
189
+ findPortBtn.textContent = "Finding ports...";
190
+ resultsArea.innerHTML = '<p class="status">Starting port detection...</p>';
191
 
192
+ await findPortWeb((message: string) => {
193
+ resultsArea.innerHTML += `<p class="log">${message}</p>`;
194
+ resultsArea.scrollTop = resultsArea.scrollHeight;
195
+ });
196
+ } catch (error) {
197
+ resultsArea.innerHTML += `<p class="error">Error: ${
198
+ error instanceof Error ? error.message : error
199
+ }</p>`;
200
+ } finally {
201
+ findPortBtn.disabled = false;
202
+ findPortBtn.textContent = "Find MotorsBus Port";
203
+ }
204
+ });
src/style.css DELETED
@@ -1,96 +0,0 @@
1
- :root {
2
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
- line-height: 1.5;
4
- font-weight: 400;
5
-
6
- color-scheme: light dark;
7
- color: rgba(255, 255, 255, 0.87);
8
- background-color: #242424;
9
-
10
- font-synthesis: none;
11
- text-rendering: optimizeLegibility;
12
- -webkit-font-smoothing: antialiased;
13
- -moz-osx-font-smoothing: grayscale;
14
- }
15
-
16
- a {
17
- font-weight: 500;
18
- color: #646cff;
19
- text-decoration: inherit;
20
- }
21
- a:hover {
22
- color: #535bf2;
23
- }
24
-
25
- body {
26
- margin: 0;
27
- display: flex;
28
- place-items: center;
29
- min-width: 320px;
30
- min-height: 100vh;
31
- }
32
-
33
- h1 {
34
- font-size: 3.2em;
35
- line-height: 1.1;
36
- }
37
-
38
- #app {
39
- max-width: 1280px;
40
- margin: 0 auto;
41
- padding: 2rem;
42
- text-align: center;
43
- }
44
-
45
- .logo {
46
- height: 6em;
47
- padding: 1.5em;
48
- will-change: filter;
49
- transition: filter 300ms;
50
- }
51
- .logo:hover {
52
- filter: drop-shadow(0 0 2em #646cffaa);
53
- }
54
- .logo.vanilla:hover {
55
- filter: drop-shadow(0 0 2em #3178c6aa);
56
- }
57
-
58
- .card {
59
- padding: 2em;
60
- }
61
-
62
- .read-the-docs {
63
- color: #888;
64
- }
65
-
66
- button {
67
- border-radius: 8px;
68
- border: 1px solid transparent;
69
- padding: 0.6em 1.2em;
70
- font-size: 1em;
71
- font-weight: 500;
72
- font-family: inherit;
73
- background-color: #1a1a1a;
74
- cursor: pointer;
75
- transition: border-color 0.25s;
76
- }
77
- button:hover {
78
- border-color: #646cff;
79
- }
80
- button:focus,
81
- button:focus-visible {
82
- outline: 4px auto -webkit-focus-ring-color;
83
- }
84
-
85
- @media (prefers-color-scheme: light) {
86
- :root {
87
- color: #213547;
88
- background-color: #ffffff;
89
- }
90
- a:hover {
91
- color: #747bff;
92
- }
93
- button {
94
- background-color: #f9f9f9;
95
- }
96
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/typescript.svg DELETED
src/web_interface.css ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* lerobot.js Web Interface Styles */
2
+
3
+ * {
4
+ margin: 0;
5
+ padding: 0;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ min-height: 100vh;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ color: #333;
17
+ }
18
+
19
+ .lerobot-app {
20
+ background: white;
21
+ border-radius: 12px;
22
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
23
+ max-width: 800px;
24
+ width: 90%;
25
+ padding: 2rem;
26
+ margin: 2rem;
27
+ }
28
+
29
+ .lerobot-header {
30
+ text-align: center;
31
+ margin-bottom: 2rem;
32
+ padding-bottom: 1rem;
33
+ border-bottom: 1px solid #eee;
34
+ }
35
+
36
+ .lerobot-header h1 {
37
+ font-size: 2.5rem;
38
+ color: #2c3e50;
39
+ margin-bottom: 0.5rem;
40
+ }
41
+
42
+ .lerobot-header p {
43
+ color: #7f8c8d;
44
+ font-size: 1.1rem;
45
+ }
46
+
47
+ .lerobot-main {
48
+ display: grid;
49
+ gap: 2rem;
50
+ }
51
+
52
+ .tool-section {
53
+ background: #f8f9fa;
54
+ padding: 1.5rem;
55
+ border-radius: 8px;
56
+ border-left: 4px solid #3498db;
57
+ }
58
+
59
+ .tool-section h2 {
60
+ color: #2c3e50;
61
+ margin-bottom: 0.5rem;
62
+ font-size: 1.4rem;
63
+ }
64
+
65
+ .tool-section p {
66
+ color: #7f8c8d;
67
+ margin-bottom: 1rem;
68
+ }
69
+
70
+ .button-group {
71
+ display: flex;
72
+ gap: 0.5rem;
73
+ margin-bottom: 1rem;
74
+ flex-wrap: wrap;
75
+ }
76
+
77
+ .info-box {
78
+ background: #e8f4fd;
79
+ border: 1px solid #3498db;
80
+ border-radius: 6px;
81
+ padding: 12px 16px;
82
+ margin-bottom: 1rem;
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 8px;
86
+ color: #2c3e50;
87
+ }
88
+
89
+ .info-icon {
90
+ font-size: 1.1rem;
91
+ flex-shrink: 0;
92
+ }
93
+
94
+ .info-text {
95
+ font-size: 0.95rem;
96
+ line-height: 1.4;
97
+ }
98
+
99
+ .primary-btn,
100
+ .secondary-btn {
101
+ border: none;
102
+ padding: 12px 24px;
103
+ border-radius: 6px;
104
+ font-size: 1rem;
105
+ font-weight: 600;
106
+ cursor: pointer;
107
+ transition: all 0.3s ease;
108
+ }
109
+
110
+ .primary-btn {
111
+ background: linear-gradient(135deg, #3498db, #2980b9);
112
+ color: white;
113
+ }
114
+
115
+ .secondary-btn {
116
+ background: linear-gradient(135deg, #95a5a6, #7f8c8d);
117
+ color: white;
118
+ }
119
+
120
+ .primary-btn:hover:not(:disabled) {
121
+ transform: translateY(-2px);
122
+ box-shadow: 0 6px 12px rgba(52, 152, 219, 0.3);
123
+ }
124
+
125
+ .secondary-btn:hover:not(:disabled) {
126
+ transform: translateY(-2px);
127
+ box-shadow: 0 6px 12px rgba(149, 165, 166, 0.3);
128
+ }
129
+
130
+ .primary-btn:disabled,
131
+ .secondary-btn:disabled {
132
+ opacity: 0.6;
133
+ cursor: not-allowed;
134
+ transform: none;
135
+ }
136
+
137
+ .results-area {
138
+ background: #2c3e50;
139
+ color: #ecf0f1;
140
+ padding: 1rem;
141
+ border-radius: 6px;
142
+ font-family: "Fira Code", "Courier New", monospace;
143
+ font-size: 0.9rem;
144
+ line-height: 1.4;
145
+ max-height: 300px;
146
+ overflow-y: auto;
147
+ min-height: 100px;
148
+ }
149
+
150
+ .results-area:empty::before {
151
+ content: "Results will appear here...";
152
+ color: #7f8c8d;
153
+ font-style: italic;
154
+ }
155
+
156
+ .results-area .log {
157
+ margin: 0.25rem 0;
158
+ color: #bdc3c7;
159
+ }
160
+
161
+ .results-area .status {
162
+ margin: 0.25rem 0;
163
+ color: #3498db;
164
+ font-weight: 600;
165
+ }
166
+
167
+ .results-area .error {
168
+ margin: 0.25rem 0;
169
+ color: #e74c3c;
170
+ font-weight: 600;
171
+ }
172
+
173
+ .results-area .success {
174
+ margin: 0.25rem 0;
175
+ color: #27ae60;
176
+ font-weight: 600;
177
+ }
178
+
179
+ .info-section {
180
+ background: #fff3cd;
181
+ padding: 1.5rem;
182
+ border-radius: 8px;
183
+ border-left: 4px solid #ffc107;
184
+ }
185
+
186
+ .info-section h3 {
187
+ color: #856404;
188
+ margin-bottom: 0.5rem;
189
+ font-size: 1.2rem;
190
+ }
191
+
192
+ .info-section p {
193
+ color: #856404;
194
+ margin-bottom: 0.5rem;
195
+ }
196
+
197
+ .info-section ul {
198
+ color: #856404;
199
+ margin-left: 1.5rem;
200
+ }
201
+
202
+ .info-section li {
203
+ margin-bottom: 0.25rem;
204
+ }
205
+
206
+ .info-section a {
207
+ color: #0056b3;
208
+ text-decoration: none;
209
+ }
210
+
211
+ .info-section a:hover {
212
+ text-decoration: underline;
213
+ }
214
+
215
+ .info-section code {
216
+ background: #f5f5f5;
217
+ padding: 2px 6px;
218
+ border-radius: 3px;
219
+ font-family: "Fira Code", "Courier New", monospace;
220
+ font-size: 0.9em;
221
+ color: #333;
222
+ }
223
+
224
+ /* Responsive design */
225
+ @media (max-width: 600px) {
226
+ .lerobot-app {
227
+ margin: 1rem;
228
+ padding: 1.5rem;
229
+ }
230
+
231
+ .lerobot-header h1 {
232
+ font-size: 2rem;
233
+ }
234
+
235
+ .lerobot-header p {
236
+ font-size: 1rem;
237
+ }
238
+ }