Spaces:
Running
Running
feat: separated node & web
Browse files- README.md +214 -0
- docs/conventions.md +6 -2
- index.html +22 -3
- public/vite.svg +0 -1
- src/cli/index.ts +1 -1
- src/counter.ts +0 -9
- src/lerobot/{find_port.ts → node/find_port.ts} +0 -0
- src/lerobot/web/find_port.ts +292 -0
- src/main.ts +202 -22
- src/style.css +0 -96
- src/typescript.svg +0 -1
- src/web_interface.css +238 -0
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 |
-
├──
|
67 |
-
└──
|
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 |
-
<!
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
<
|
19 |
-
|
20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
</div>
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|