NERDDISCO commited on
Commit
ea49c9f
·
1 Parent(s): 130ae50

refactor: restructure demo to examples/robot-control-web

Browse files
Files changed (41) hide show
  1. .github/CONTRIBUTING.md +1 -1
  2. examples/{demo → robot-control-web}/App.tsx +0 -0
  3. examples/{demo → robot-control-web}/components/CalibrationModal.tsx +0 -0
  4. examples/{demo → robot-control-web}/components/CalibrationPanel.tsx +0 -0
  5. examples/{demo → robot-control-web}/components/ErrorBoundary.tsx +0 -0
  6. examples/{demo → robot-control-web}/components/PortManager.tsx +0 -0
  7. examples/{demo → robot-control-web}/components/TeleoperationPanel.tsx +0 -0
  8. examples/{demo → robot-control-web}/components/ui/alert.tsx +0 -0
  9. examples/{demo → robot-control-web}/components/ui/badge.tsx +0 -0
  10. examples/{demo → robot-control-web}/components/ui/button.tsx +0 -0
  11. examples/{demo → robot-control-web}/components/ui/card.tsx +0 -0
  12. examples/{demo → robot-control-web}/components/ui/dialog.tsx +0 -0
  13. examples/{demo → robot-control-web}/components/ui/progress.tsx +0 -0
  14. examples/{demo → robot-control-web}/index.css +0 -0
  15. examples/{demo → robot-control-web}/lib/unified-storage.ts +0 -0
  16. examples/{demo → robot-control-web}/lib/utils.ts +0 -0
  17. examples/{demo → robot-control-web}/main.tsx +0 -0
  18. examples/{demo → robot-control-web}/pages/Home.tsx +0 -0
  19. index.html +1 -1
  20. packages/web/src/index.ts +2 -0
  21. pnpm-lock.yaml +12 -0
  22. src/demo/App.tsx +0 -19
  23. src/demo/components/CalibrationModal.tsx +0 -49
  24. src/demo/components/CalibrationPanel.tsx +0 -419
  25. src/demo/components/ErrorBoundary.tsx +0 -65
  26. src/demo/components/PortManager.tsx +0 -1251
  27. src/demo/components/TeleoperationPanel.tsx +0 -530
  28. src/demo/components/ui/alert.tsx +0 -58
  29. src/demo/components/ui/badge.tsx +0 -35
  30. src/demo/components/ui/button.tsx +0 -53
  31. src/demo/components/ui/card.tsx +0 -85
  32. src/demo/components/ui/dialog.tsx +0 -120
  33. src/demo/components/ui/progress.tsx +0 -26
  34. src/demo/index.css +0 -12
  35. src/demo/lib/unified-storage.ts +0 -325
  36. src/demo/lib/utils.ts +0 -6
  37. src/demo/main.tsx +0 -5
  38. src/demo/pages/Home.tsx +0 -99
  39. src/demo/types.ts +0 -2
  40. tailwind.config.js +4 -1
  41. tsconfig.json +1 -1
.github/CONTRIBUTING.md CHANGED
@@ -20,7 +20,7 @@ pnpm --filter "@lerobot/web" run build
20
  ## 📦 Package Structure
21
 
22
  - **`packages/web/`** - Browser package (`@lerobot/web` on npm)
23
- - **`src/demo/`** - Demo application (deployed to HF Spaces)
24
  - **`src/cli/`** - Node.js CLI tool
25
  - **`src/lerobot/node/`** - Node.js library
26
 
 
20
  ## 📦 Package Structure
21
 
22
  - **`packages/web/`** - Browser package (`@lerobot/web` on npm)
23
+ - **`examples/robot-control-web/`** - Demo application (deployed to HF Spaces)
24
  - **`src/cli/`** - Node.js CLI tool
25
  - **`src/lerobot/node/`** - Node.js library
26
 
examples/{demo → robot-control-web}/App.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/CalibrationModal.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/CalibrationPanel.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/ErrorBoundary.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/PortManager.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/TeleoperationPanel.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/ui/alert.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/ui/badge.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/ui/button.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/ui/card.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/ui/dialog.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/components/ui/progress.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/index.css RENAMED
File without changes
examples/{demo → robot-control-web}/lib/unified-storage.ts RENAMED
File without changes
examples/{demo → robot-control-web}/lib/utils.ts RENAMED
File without changes
examples/{demo → robot-control-web}/main.tsx RENAMED
File without changes
examples/{demo → robot-control-web}/pages/Home.tsx RENAMED
File without changes
index.html CHANGED
@@ -76,6 +76,6 @@
76
  </head>
77
  <body>
78
  <div id="root"></div>
79
- <script type="module" src="/src/demo/main.tsx"></script>
80
  </body>
81
  </html>
 
76
  </head>
77
  <body>
78
  <div id="root"></div>
79
+ <script type="module" src="/examples/robot-control-web/main.tsx"></script>
80
  </body>
81
  </html>
packages/web/src/index.ts CHANGED
@@ -10,6 +10,8 @@ export { calibrate, isWebSerialSupported } from "./calibrate.js";
10
  export { teleoperate } from "./teleoperate.js";
11
  export { findPort } from "./find_port.js";
12
 
 
 
13
  // Types
14
  export type {
15
  RobotConnection,
 
10
  export { teleoperate } from "./teleoperate.js";
11
  export { findPort } from "./find_port.js";
12
 
13
+ console.log("asdfasdfasdfasdf");
14
+
15
  // Types
16
  export type {
17
  RobotConnection,
pnpm-lock.yaml CHANGED
@@ -8,6 +8,9 @@ importers:
8
 
9
  .:
10
  dependencies:
 
 
 
11
  '@radix-ui/react-dialog':
12
  specifier: ^1.1.14
13
@@ -399,6 +402,11 @@ packages:
399
  '@jridgewell/[email protected]':
400
  resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
401
 
 
 
 
 
 
402
  '@manypkg/[email protected]':
403
  resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
404
 
@@ -2077,6 +2085,10 @@ snapshots:
2077
  '@jridgewell/resolve-uri': 3.1.2
2078
  '@jridgewell/sourcemap-codec': 1.5.0
2079
 
 
 
 
 
2080
  '@manypkg/[email protected]':
2081
  dependencies:
2082
  '@babel/runtime': 7.27.6
 
8
 
9
  .:
10
  dependencies:
11
+ '@lerobot/web':
12
+ specifier: ^0.1.1
13
+ version: 0.1.1([email protected])
14
  '@radix-ui/react-dialog':
15
  specifier: ^1.1.14
16
 
402
  '@jridgewell/[email protected]':
403
  resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
404
 
405
+ '@lerobot/[email protected]':
406
+ resolution: {integrity: sha512-8xLGBTIQQetJzqauM9OtiSUIaSLphCH2qAqiGVzszmJk7pAcucqouezcIGRHiDOVRFDfpAYerRyfoeFLdoKqDQ==}
407
+ peerDependencies:
408
+ typescript: '>=4.5.0'
409
+
410
  '@manypkg/[email protected]':
411
  resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
412
 
 
2085
  '@jridgewell/resolve-uri': 3.1.2
2086
  '@jridgewell/sourcemap-codec': 1.5.0
2087
 
2088
2089
+ dependencies:
2090
+ typescript: 5.8.3
2091
+
2092
  '@manypkg/[email protected]':
2093
  dependencies:
2094
  '@babel/runtime': 7.27.6
src/demo/App.tsx DELETED
@@ -1,19 +0,0 @@
1
- import { useState } from "react";
2
- import { Home } from "./pages/Home";
3
- import { ErrorBoundary } from "./components/ErrorBoundary";
4
- import type { RobotConnection } from "@lerobot/web";
5
-
6
- export function App() {
7
- const [connectedRobots, setConnectedRobots] = useState<RobotConnection[]>([]);
8
-
9
- return (
10
- <ErrorBoundary>
11
- <div className="min-h-screen bg-background">
12
- <Home
13
- connectedRobots={connectedRobots}
14
- onConnectedRobotsChange={setConnectedRobots}
15
- />
16
- </div>
17
- </ErrorBoundary>
18
- );
19
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/CalibrationModal.tsx DELETED
@@ -1,49 +0,0 @@
1
- import {
2
- Dialog,
3
- DialogContent,
4
- DialogDescription,
5
- DialogFooter,
6
- DialogHeader,
7
- DialogTitle,
8
- } from "./ui/dialog";
9
- import { Button } from "./ui/button";
10
-
11
- interface CalibrationModalProps {
12
- open: boolean;
13
- onOpenChange: (open: boolean) => void;
14
- deviceType: string;
15
- onContinue: () => void;
16
- }
17
-
18
- export function CalibrationModal({
19
- open,
20
- onOpenChange,
21
- deviceType,
22
- onContinue,
23
- }: CalibrationModalProps) {
24
- return (
25
- <Dialog open={open} onOpenChange={onOpenChange}>
26
- <DialogContent className="sm:max-w-md">
27
- <DialogHeader>
28
- <DialogTitle>📍 Set Homing Position</DialogTitle>
29
- <DialogDescription className="text-base py-4">
30
- Move the SO-100 {deviceType} to the <strong>MIDDLE</strong> of its
31
- range of motion and click OK when ready.
32
- <br />
33
- <br />
34
- The calibration will then automatically:
35
- <br />• Record homing offsets
36
- <br />• Record joint ranges (manual - you control when to stop)
37
- <br />• Save configuration file
38
- </DialogDescription>
39
- </DialogHeader>
40
-
41
- <DialogFooter>
42
- <Button onClick={onContinue} className="w-full">
43
- OK - Start Calibration
44
- </Button>
45
- </DialogFooter>
46
- </DialogContent>
47
- </Dialog>
48
- );
49
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/CalibrationPanel.tsx DELETED
@@ -1,419 +0,0 @@
1
- import { useState, useCallback, useMemo } from "react";
2
- import { Button } from "./ui/button";
3
- import {
4
- Card,
5
- CardContent,
6
- CardDescription,
7
- CardHeader,
8
- CardTitle,
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";
17
- import { releaseMotors } from "@lerobot/web";
18
- import { WebSerialPortWrapper } from "@lerobot/web";
19
- import { createSO100Config } from "@lerobot/web";
20
- import { CalibrationModal } from "./CalibrationModal";
21
- import type { RobotConnection } from "@lerobot/web";
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",
44
- "shoulder_lift",
45
- "elbow_flex",
46
- "wrist_flex",
47
- "wrist_roll",
48
- "gripper",
49
- ],
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) {
99
- return;
100
- }
101
-
102
- try {
103
- setStatus("🤖 Starting calibration process...");
104
- setIsCalibrating(true);
105
- initializeMotorData();
106
-
107
- // Use the simple calibrate API - just pass the robot connection
108
- const process = await calibrate(robot, {
109
- onLiveUpdate: (data) => {
110
- setMotorData(data);
111
- setStatus(
112
- "📏 Recording joint ranges - move all joints through their full range"
113
- );
114
- },
115
- onProgress: (message) => {
116
- setStatus(message);
117
- },
118
- });
119
-
120
- setCalibrationProcess(process);
121
-
122
- // Add Enter key listener for stopping (matching Node.js UX)
123
- const handleKeyPress = (event: KeyboardEvent) => {
124
- if (event.key === "Enter") {
125
- process.stop();
126
- }
127
- };
128
- document.addEventListener("keydown", handleKeyPress);
129
-
130
- try {
131
- // Wait for calibration to complete
132
- const result = await process.result;
133
- setCalibrationResult(result);
134
-
135
- // App-level concern: Save results to storage
136
- const serialNumber =
137
- robot.serialNumber || robot.usbMetadata?.serialNumber || "unknown";
138
- await saveCalibrationResults(
139
- result,
140
- robot.robotType,
141
- robot.robotId || `${robot.robotType}_1`,
142
- serialNumber
143
- );
144
-
145
- setStatus(
146
- "✅ Calibration completed successfully! Configuration saved."
147
- );
148
- } finally {
149
- document.removeEventListener("keydown", handleKeyPress);
150
- setCalibrationProcess(null);
151
- setIsCalibrating(false);
152
- }
153
- } catch (error) {
154
- console.error("❌ Calibration failed:", error);
155
- setStatus(
156
- `❌ Calibration failed: ${
157
- error instanceof Error ? error.message : error
158
- }`
159
- );
160
- setIsCalibrating(false);
161
- setCalibrationProcess(null);
162
- }
163
- }, [robot, initializeMotorData]);
164
-
165
- // Stop calibration recording
166
- const handleStopRecording = useCallback(() => {
167
- if (calibrationProcess) {
168
- calibrationProcess.stop();
169
- }
170
- }, [calibrationProcess]);
171
-
172
- // App-level concern: Save calibration results
173
- const saveCalibrationResults = async (
174
- results: WebCalibrationResults,
175
- robotType: string,
176
- robotId: string,
177
- serialNumber: string
178
- ) => {
179
- try {
180
- // Save to unified storage (app-level functionality)
181
- const { saveCalibrationData } = await import("../lib/unified-storage.js");
182
-
183
- const fullCalibrationData = {
184
- ...results,
185
- device_type: robotType,
186
- device_id: robotId,
187
- calibrated_at: new Date().toISOString(),
188
- platform: "web",
189
- api: "Web Serial API",
190
- };
191
-
192
- const metadata = {
193
- timestamp: new Date().toISOString(),
194
- readCount: Object.keys(motorData).length > 0 ? 100 : 0, // Estimate
195
- };
196
-
197
- saveCalibrationData(serialNumber, fullCalibrationData, metadata);
198
- } catch (error) {
199
- console.warn("Failed to save calibration results:", error);
200
- }
201
- };
202
-
203
- // App-level concern: JSON export functionality
204
- const downloadConfigJSON = useCallback(() => {
205
- if (!calibrationResult) return;
206
-
207
- const jsonString = JSON.stringify(calibrationResult, null, 2);
208
- const blob = new Blob([jsonString], { type: "application/json" });
209
- const url = URL.createObjectURL(blob);
210
-
211
- const link = document.createElement("a");
212
- link.href = url;
213
- link.download = `${robot.robotId || robot.robotType}_calibration.json`;
214
- document.body.appendChild(link);
215
- link.click();
216
- document.body.removeChild(link);
217
- URL.revokeObjectURL(url);
218
- }, [calibrationResult, robot.robotId, robot.robotType]);
219
-
220
- return (
221
- <div className="space-y-4">
222
- {/* Calibration Status Card */}
223
- <Card>
224
- <CardHeader>
225
- <div className="flex items-center justify-between">
226
- <div>
227
- <CardTitle className="text-lg">
228
- 🛠️ Calibrating: {robot.robotId}
229
- </CardTitle>
230
- <CardDescription>
231
- {robot.robotType?.replace("_", " ")} • {robot.name}
232
- </CardDescription>
233
- </div>
234
- <Badge
235
- variant={
236
- isCalibrating
237
- ? "default"
238
- : calibrationResult
239
- ? "default"
240
- : "outline"
241
- }
242
- >
243
- {isCalibrating
244
- ? "Recording"
245
- : calibrationResult
246
- ? "Complete"
247
- : "Ready"}
248
- </Badge>
249
- </div>
250
- </CardHeader>
251
- <CardContent>
252
- <div className="space-y-4">
253
- <div className="p-3 bg-blue-50 rounded-lg">
254
- <p className="text-sm font-medium text-blue-900">Status:</p>
255
- <p className="text-sm text-blue-800">{status}</p>
256
- {isCalibrating && (
257
- <p className="text-xs text-blue-600 mt-1">
258
- Move joints through full range | Press "Finish Recording" or
259
- Enter key when done
260
- </p>
261
- )}
262
- </div>
263
-
264
- <div className="flex gap-2">
265
- {!isCalibrating && !calibrationResult && (
266
- <Button
267
- onClick={async () => {
268
- // Release motor torque FIRST - so user can move joints immediately
269
- await releaseMotorTorque();
270
- // THEN open modal - user can now follow instructions right away
271
- setModalOpen(true);
272
- }}
273
- disabled={isPreparing}
274
- >
275
- {isPreparing ? "Preparing..." : "Start Calibration"}
276
- </Button>
277
- )}
278
-
279
- {isCalibrating && calibrationProcess && (
280
- <Button onClick={handleStopRecording} variant="default">
281
- Finish Recording
282
- </Button>
283
- )}
284
-
285
- {calibrationResult && (
286
- <>
287
- <Button onClick={downloadConfigJSON} variant="outline">
288
- Download Config JSON
289
- </Button>
290
- <Button onClick={onFinish}>Done</Button>
291
- </>
292
- )}
293
- </div>
294
- </div>
295
- </CardContent>
296
- </Card>
297
-
298
- {/* Configuration JSON Display */}
299
- {calibrationResult && (
300
- <Card>
301
- <CardHeader>
302
- <CardTitle className="text-lg">
303
- 🎯 Calibration Configuration
304
- </CardTitle>
305
- <CardDescription>
306
- Copy this JSON or download it for your robot setup
307
- </CardDescription>
308
- </CardHeader>
309
- <CardContent>
310
- <div className="space-y-3">
311
- <pre className="bg-gray-100 p-4 rounded-lg text-sm overflow-x-auto border">
312
- <code>{JSON.stringify(calibrationResult, null, 2)}</code>
313
- </pre>
314
- <div className="flex gap-2">
315
- <Button onClick={downloadConfigJSON} variant="outline">
316
- 📄 Download JSON File
317
- </Button>
318
- <Button
319
- onClick={() => {
320
- navigator.clipboard.writeText(
321
- JSON.stringify(calibrationResult, null, 2)
322
- );
323
- }}
324
- variant="outline"
325
- >
326
- 📋 Copy to Clipboard
327
- </Button>
328
- </div>
329
- </div>
330
- </CardContent>
331
- </Card>
332
- )}
333
-
334
- {/* Live Position Recording Table */}
335
- <Card>
336
- <CardHeader>
337
- <CardTitle className="text-lg">Live Position Recording</CardTitle>
338
- <CardDescription>
339
- Real-time motor position feedback during calibration
340
- </CardDescription>
341
- </CardHeader>
342
- <CardContent>
343
- <div className="overflow-hidden rounded-lg border">
344
- <table className="w-full font-mono text-sm">
345
- <thead className="bg-gray-50">
346
- <tr>
347
- <th className="px-4 py-2 text-left font-medium text-gray-900">
348
- Motor Name
349
- </th>
350
- <th className="px-4 py-2 text-right font-medium text-gray-900">
351
- Current
352
- </th>
353
- <th className="px-4 py-2 text-right font-medium text-gray-900">
354
- Min
355
- </th>
356
- <th className="px-4 py-2 text-right font-medium text-gray-900">
357
- Max
358
- </th>
359
- <th className="px-4 py-2 text-right font-medium text-gray-900">
360
- Range
361
- </th>
362
- </tr>
363
- </thead>
364
- <tbody className="divide-y divide-gray-200">
365
- {motorNames.map((motorName) => {
366
- const motor = motorData[motorName] || {
367
- current: 2047,
368
- min: 2047,
369
- max: 2047,
370
- range: 0,
371
- };
372
-
373
- return (
374
- <tr key={motorName} className="hover:bg-gray-50">
375
- <td className="px-4 py-2 font-medium flex items-center gap-2">
376
- {motorName}
377
- {motor.range > 100 && (
378
- <span className="text-green-600 text-xs">✓</span>
379
- )}
380
- </td>
381
- <td className="px-4 py-2 text-right">{motor.current}</td>
382
- <td className="px-4 py-2 text-right">{motor.min}</td>
383
- <td className="px-4 py-2 text-right">{motor.max}</td>
384
- <td className="px-4 py-2 text-right font-medium">
385
- <span
386
- className={
387
- motor.range > 100
388
- ? "text-green-600"
389
- : "text-gray-500"
390
- }
391
- >
392
- {motor.range}
393
- </span>
394
- </td>
395
- </tr>
396
- );
397
- })}
398
- </tbody>
399
- </table>
400
- </div>
401
-
402
- {isCalibrating && (
403
- <div className="mt-3 text-center text-sm text-gray-600">
404
- Move joints through their full range of motion...
405
- </div>
406
- )}
407
- </CardContent>
408
- </Card>
409
-
410
- {/* Calibration Modal */}
411
- <CalibrationModal
412
- open={modalOpen}
413
- onOpenChange={setModalOpen}
414
- deviceType={robot.robotType || "robot"}
415
- onContinue={handleContinueCalibration}
416
- />
417
- </div>
418
- );
419
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/ErrorBoundary.tsx DELETED
@@ -1,65 +0,0 @@
1
- import { Component, type ErrorInfo, type ReactNode } from "react";
2
- import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
3
- import { Button } from "./ui/button";
4
-
5
- interface Props {
6
- children: ReactNode;
7
- }
8
-
9
- interface State {
10
- hasError: boolean;
11
- error?: Error;
12
- }
13
-
14
- export class ErrorBoundary extends Component<Props, State> {
15
- constructor(props: Props) {
16
- super(props);
17
- this.state = { hasError: false };
18
- }
19
-
20
- static getDerivedStateFromError(error: Error): State {
21
- return { hasError: true, error };
22
- }
23
-
24
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
25
- console.error("ErrorBoundary caught an error:", error, errorInfo);
26
- }
27
-
28
- render() {
29
- if (this.state.hasError) {
30
- return (
31
- <div className="min-h-screen flex items-center justify-center p-8">
32
- <div className="max-w-md w-full">
33
- <Alert variant="destructive">
34
- <AlertTitle>Something went wrong</AlertTitle>
35
- <AlertDescription>
36
- The application encountered an error. Please try refreshing the
37
- page or contact support if the problem persists.
38
- </AlertDescription>
39
- </Alert>
40
- <div className="mt-4 flex gap-2">
41
- <Button onClick={() => window.location.reload()}>
42
- Refresh Page
43
- </Button>
44
- <Button
45
- variant="outline"
46
- onClick={() =>
47
- this.setState({ hasError: false, error: undefined })
48
- }
49
- >
50
- Try Again
51
- </Button>
52
- </div>
53
- {process.env.NODE_ENV === "development" && this.state.error && (
54
- <div className="mt-4 p-4 bg-gray-100 rounded-md text-xs">
55
- <pre>{this.state.error.stack}</pre>
56
- </div>
57
- )}
58
- </div>
59
- </div>
60
- );
61
- }
62
-
63
- return this.props.children;
64
- }
65
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/PortManager.tsx DELETED
@@ -1,1251 +0,0 @@
1
- import { useState, useEffect } from "react";
2
- import { Button } from "./ui/button";
3
- import {
4
- Card,
5
- CardContent,
6
- CardDescription,
7
- CardHeader,
8
- CardTitle,
9
- } from "./ui/card";
10
- import { Alert, AlertDescription } from "./ui/alert";
11
- import { Badge } from "./ui/badge";
12
- import {
13
- Dialog,
14
- DialogContent,
15
- DialogDescription,
16
- DialogFooter,
17
- DialogHeader,
18
- DialogTitle,
19
- } from "./ui/dialog";
20
- import { isWebSerialSupported } from "@lerobot/web";
21
- import type { RobotConnection } from "@lerobot/web";
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({
49
- connectedRobots,
50
- onConnectedRobotsChange,
51
- onCalibrate,
52
- onTeleoperate,
53
- }: PortManagerProps) {
54
- const [isConnecting, setIsConnecting] = useState(false);
55
- const [isFindingPorts, setIsFindingPorts] = useState(false);
56
- const [findPortsLog, setFindPortsLog] = useState<string[]>([]);
57
- const [error, setError] = useState<string | null>(null);
58
- const [confirmDeleteDialog, setConfirmDeleteDialog] = useState<{
59
- open: boolean;
60
- robotIndex: number;
61
- robotName: string;
62
- serialNumber: string;
63
- }>({
64
- open: false,
65
- robotIndex: -1,
66
- robotName: "",
67
- serialNumber: "",
68
- });
69
- // Load saved port data from localStorage on mount
70
- useEffect(() => {
71
- loadSavedPorts();
72
- }, []);
73
-
74
- // Note: Robot data is now automatically saved to unified storage when robot config is updated
75
-
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
83
- let serialNumber = null;
84
- let usbMetadata = null;
85
-
86
- try {
87
- // Get all USB devices and try to match with this serial port
88
- const usbDevices = await navigator.usb.getDevices();
89
- const portInfo = port.getInfo();
90
-
91
- // Try to find matching USB device by vendor/product ID
92
- const matchingDevice = usbDevices.find(
93
- (device) =>
94
- device.vendorId === portInfo.usbVendorId &&
95
- device.productId === portInfo.usbProductId
96
- );
97
-
98
- if (matchingDevice) {
99
- serialNumber =
100
- matchingDevice.serialNumber ||
101
- `${matchingDevice.vendorId}-${
102
- matchingDevice.productId
103
- }-${Date.now()}`;
104
- usbMetadata = {
105
- vendorId: `0x${matchingDevice.vendorId
106
- .toString(16)
107
- .padStart(4, "0")}`,
108
- productId: `0x${matchingDevice.productId
109
- .toString(16)
110
- .padStart(4, "0")}`,
111
- serialNumber: matchingDevice.serialNumber || "Generated ID",
112
- manufacturerName: matchingDevice.manufacturerName || "Unknown",
113
- productName: matchingDevice.productName || "Unknown",
114
- usbVersionMajor: matchingDevice.usbVersionMajor,
115
- usbVersionMinor: matchingDevice.usbVersionMinor,
116
- deviceClass: matchingDevice.deviceClass,
117
- deviceSubclass: matchingDevice.deviceSubclass,
118
- deviceProtocol: matchingDevice.deviceProtocol,
119
- };
120
- console.log("✅ Restored USB metadata for port:", serialNumber);
121
- }
122
- } catch (usbError) {
123
- console.log("⚠️ Could not restore USB metadata:", usbError);
124
- // Generate fallback if no USB metadata available
125
- serialNumber = `fallback-${Date.now()}-${Math.random()
126
- .toString(36)
127
- .substr(2, 9)}`;
128
- }
129
-
130
- // Load robot configuration from unified storage
131
- let robotType: "so100_follower" | "so100_leader" | undefined;
132
- let robotId: string | undefined;
133
- let shouldAutoConnect = false;
134
-
135
- if (serialNumber) {
136
- try {
137
- const { getUnifiedRobotData } = await import(
138
- "../lib/unified-storage"
139
- );
140
- const unifiedData = getUnifiedRobotData(serialNumber);
141
- if (unifiedData?.device_info) {
142
- robotType = unifiedData.device_info.robotType;
143
- robotId = unifiedData.device_info.robotId;
144
- shouldAutoConnect = true;
145
- console.log(
146
- `📋 Loaded robot config from unified storage: ${robotType} (${robotId})`
147
- );
148
- }
149
- } catch (error) {
150
- console.warn("Failed to load unified robot data:", error);
151
- }
152
- }
153
-
154
- // Auto-connect to configured robots
155
- let isConnected = false;
156
- try {
157
- // Check if already open
158
- if (port.readable !== null && port.writable !== null) {
159
- isConnected = true;
160
- console.log("Port already open, reusing connection");
161
- } else if (shouldAutoConnect && robotType && robotId) {
162
- // Auto-open robots that have saved configuration
163
- console.log(
164
- `Auto-connecting to saved robot: ${robotType} (${robotId})`
165
- );
166
- await port.open({ baudRate: 1000000 });
167
- isConnected = true;
168
- } else {
169
- console.log(
170
- "Port found but no saved robot configuration, skipping auto-connect"
171
- );
172
- isConnected = false;
173
- }
174
- } catch (error) {
175
- console.log("Could not auto-connect to robot:", error);
176
- isConnected = false;
177
- }
178
-
179
- restoredPorts.push({
180
- port,
181
- name: getPortDisplayName(port),
182
- isConnected,
183
- robotType,
184
- robotId,
185
- serialNumber: serialNumber!,
186
- usbMetadata: usbMetadata || undefined,
187
- });
188
- }
189
-
190
- onConnectedRobotsChange(restoredPorts);
191
- } catch (error) {
192
- console.error("Failed to load saved ports:", error);
193
- }
194
- };
195
-
196
- const getPortDisplayName = (port: SerialPort): string => {
197
- try {
198
- const info = port.getInfo();
199
- if (info.usbVendorId && info.usbProductId) {
200
- return `USB Port (${info.usbVendorId}:${info.usbProductId})`;
201
- }
202
- if (info.usbVendorId) {
203
- return `Serial Port (VID:${info.usbVendorId
204
- .toString(16)
205
- .toUpperCase()})`;
206
- }
207
- } catch (error) {
208
- // getInfo() might not be available
209
- }
210
- return `Serial Port ${Date.now()}`;
211
- };
212
-
213
- const handleConnect = async () => {
214
- if (!isWebSerialSupported()) {
215
- setError("Web Serial API is not supported in this browser");
216
- return;
217
- }
218
-
219
- try {
220
- setIsConnecting(true);
221
- setError(null);
222
-
223
- // Step 1: Request Web Serial port
224
- console.log("Step 1: Requesting Web Serial port...");
225
- const port = await navigator.serial.requestPort();
226
- await port.open({ baudRate: 1000000 });
227
-
228
- // Step 2: Request WebUSB device for metadata
229
- console.log(
230
- "Step 2: Requesting WebUSB device for unique identification..."
231
- );
232
- let serialNumber = null;
233
- let usbMetadata = null;
234
-
235
- try {
236
- // Request USB device access for metadata
237
- const usbDevice = await navigator.usb.requestDevice({
238
- filters: [
239
- { vendorId: 0x0403 }, // FTDI
240
- { vendorId: 0x067b }, // Prolific
241
- { vendorId: 0x10c4 }, // Silicon Labs
242
- { vendorId: 0x1a86 }, // QinHeng Electronics (CH340)
243
- { vendorId: 0x239a }, // Adafruit
244
- { vendorId: 0x2341 }, // Arduino
245
- { vendorId: 0x2e8a }, // Raspberry Pi Foundation
246
- { vendorId: 0x1b4f }, // SparkFun
247
- ],
248
- });
249
-
250
- if (usbDevice) {
251
- serialNumber =
252
- usbDevice.serialNumber ||
253
- `${usbDevice.vendorId}-${usbDevice.productId}-${Date.now()}`;
254
- usbMetadata = {
255
- vendorId: `0x${usbDevice.vendorId.toString(16).padStart(4, "0")}`,
256
- productId: `0x${usbDevice.productId.toString(16).padStart(4, "0")}`,
257
- serialNumber: usbDevice.serialNumber || "Generated ID",
258
- manufacturerName: usbDevice.manufacturerName || "Unknown",
259
- productName: usbDevice.productName || "Unknown",
260
- usbVersionMajor: usbDevice.usbVersionMajor,
261
- usbVersionMinor: usbDevice.usbVersionMinor,
262
- deviceClass: usbDevice.deviceClass,
263
- deviceSubclass: usbDevice.deviceSubclass,
264
- deviceProtocol: usbDevice.deviceProtocol,
265
- };
266
- console.log("✅ USB device metadata acquired:", usbMetadata);
267
- }
268
- } catch (usbError) {
269
- console.log(
270
- "⚠️ WebUSB request failed, generating fallback ID:",
271
- usbError
272
- );
273
- // Generate a fallback unique ID if WebUSB fails
274
- serialNumber = `fallback-${Date.now()}-${Math.random()
275
- .toString(36)
276
- .substr(2, 9)}`;
277
- usbMetadata = {
278
- vendorId: "Unknown",
279
- productId: "Unknown",
280
- serialNumber: serialNumber,
281
- manufacturerName: "USB Metadata Not Available",
282
- productName: "Check browser WebUSB support",
283
- };
284
- }
285
-
286
- const portName = getPortDisplayName(port);
287
-
288
- // Step 3: Check if this robot (by serial number) is already connected
289
- const existingIndex = connectedRobots.findIndex(
290
- (robot) => robot.serialNumber === serialNumber
291
- );
292
-
293
- if (existingIndex === -1) {
294
- // New robot - add to list
295
- const newRobot: RobotConnection = {
296
- port,
297
- name: portName,
298
- isConnected: true,
299
- serialNumber: serialNumber!,
300
- usbMetadata: usbMetadata || undefined,
301
- };
302
-
303
- // Try to load saved robot info by serial number using unified storage
304
- if (serialNumber) {
305
- try {
306
- const { getRobotConfig } = await import("../lib/unified-storage");
307
- const savedConfig = getRobotConfig(serialNumber);
308
- if (savedConfig) {
309
- newRobot.robotType = savedConfig.robotType as
310
- | "so100_follower"
311
- | "so100_leader";
312
- newRobot.robotId = savedConfig.robotId;
313
- console.log("📋 Loaded saved robot configuration:", savedConfig);
314
- }
315
- } catch (error) {
316
- console.warn("Failed to load saved robot data:", error);
317
- }
318
- }
319
-
320
- onConnectedRobotsChange([...connectedRobots, newRobot]);
321
- console.log("🤖 New robot connected with ID:", serialNumber);
322
- } else {
323
- // Existing robot - update port and connection status
324
- const updatedRobots = connectedRobots.map((robot, index) =>
325
- index === existingIndex
326
- ? { ...robot, port, isConnected: true, name: portName }
327
- : robot
328
- );
329
- onConnectedRobotsChange(updatedRobots);
330
- console.log("🔄 Existing robot reconnected:", serialNumber);
331
- }
332
- } catch (error) {
333
- if (
334
- error instanceof Error &&
335
- (error.message.includes("cancelled") ||
336
- error.message.includes("No port selected by the user") ||
337
- error.name === "NotAllowedError")
338
- ) {
339
- // User cancelled - no error message needed, just log to console
340
- console.log("Connection cancelled by user");
341
- return;
342
- }
343
- setError(
344
- error instanceof Error ? error.message : "Failed to connect to robot"
345
- );
346
- } finally {
347
- setIsConnecting(false);
348
- }
349
- };
350
-
351
- const handleDisconnect = async (index: number) => {
352
- const portInfo = connectedRobots[index];
353
- const robotName = portInfo.robotId || portInfo.name;
354
- const serialNumber = portInfo.serialNumber || "unknown";
355
-
356
- // Show confirmation dialog
357
- setConfirmDeleteDialog({
358
- open: true,
359
- robotIndex: index,
360
- robotName,
361
- serialNumber,
362
- });
363
- };
364
-
365
- const confirmDelete = async () => {
366
- const { robotIndex } = confirmDeleteDialog;
367
- const portInfo = connectedRobots[robotIndex];
368
-
369
- setConfirmDeleteDialog({
370
- open: false,
371
- robotIndex: -1,
372
- robotName: "",
373
- serialNumber: "",
374
- });
375
-
376
- try {
377
- // Close the serial port connection
378
- if (portInfo.isConnected) {
379
- await portInfo.port.close();
380
- }
381
-
382
- // Delete from unified storage if serial number is available
383
- if (portInfo.serialNumber) {
384
- try {
385
- const { getUnifiedKey } = await import("../lib/unified-storage");
386
- const unifiedKey = getUnifiedKey(portInfo.serialNumber);
387
-
388
- // Remove unified storage data
389
- localStorage.removeItem(unifiedKey);
390
- console.log(`🗑️ Deleted unified robot data: ${unifiedKey}`);
391
- } catch (error) {
392
- console.warn("Failed to delete unified storage data:", error);
393
- }
394
- }
395
-
396
- // Remove from UI
397
- const updatedRobots = connectedRobots.filter((_, i) => i !== robotIndex);
398
- onConnectedRobotsChange(updatedRobots);
399
-
400
- console.log(
401
- `✅ Robot "${confirmDeleteDialog.robotName}" permanently removed from system`
402
- );
403
- } catch (error) {
404
- setError(
405
- error instanceof Error ? error.message : "Failed to remove robot"
406
- );
407
- }
408
- };
409
-
410
- const cancelDelete = () => {
411
- setConfirmDeleteDialog({
412
- open: false,
413
- robotIndex: -1,
414
- robotName: "",
415
- serialNumber: "",
416
- });
417
- };
418
-
419
- const handleUpdatePortInfo = (
420
- index: number,
421
- robotType: "so100_follower" | "so100_leader",
422
- robotId: string
423
- ) => {
424
- const updatedRobots = connectedRobots.map((robot, i) => {
425
- if (i === index) {
426
- const updatedRobot = { ...robot, robotType, robotId };
427
-
428
- // Save robot configuration using unified storage
429
- if (updatedRobot.serialNumber) {
430
- import("../lib/unified-storage")
431
- .then(({ saveRobotConfig }) => {
432
- saveRobotConfig(
433
- updatedRobot.serialNumber!,
434
- robotType,
435
- robotId,
436
- updatedRobot.usbMetadata
437
- );
438
- console.log(
439
- "💾 Saved robot configuration for:",
440
- updatedRobot.serialNumber
441
- );
442
- })
443
- .catch((error) => {
444
- console.warn("Failed to save robot configuration:", error);
445
- });
446
- }
447
-
448
- return updatedRobot;
449
- }
450
- return robot;
451
- });
452
- onConnectedRobotsChange(updatedRobots);
453
- };
454
-
455
- const handleFindPorts = async () => {
456
- if (!isWebSerialSupported()) {
457
- setError("Web Serial API is not supported in this browser");
458
- return;
459
- }
460
-
461
- try {
462
- setIsFindingPorts(true);
463
- setFindPortsLog([]);
464
- setError(null);
465
-
466
- // Use the new findPort API from standard library
467
- const { findPort } = await import("@lerobot/web");
468
-
469
- const findPortProcess = await findPort({
470
- onMessage: (message) => {
471
- setFindPortsLog((prev) => [...prev, message]);
472
- },
473
- });
474
-
475
- const robotConnections = (await findPortProcess.result) as any; // RobotConnection[] from findPort
476
- const robotConnection = robotConnections[0]; // Get first robot from array
477
-
478
- const portName = getPortDisplayName(robotConnection.port);
479
- setFindPortsLog((prev) => [...prev, `✅ Port ready: ${portName}`]);
480
-
481
- // Add to connected ports if not already there
482
- const existingIndex = connectedRobots.findIndex(
483
- (p) => p.name === portName
484
- );
485
- if (existingIndex === -1) {
486
- const newPort: RobotConnection = {
487
- port: robotConnection.port,
488
- name: portName,
489
- isConnected: true,
490
- robotType: robotConnection.robotType,
491
- robotId: robotConnection.robotId,
492
- serialNumber: robotConnection.serialNumber,
493
- };
494
- onConnectedRobotsChange([...connectedRobots, newPort]);
495
- }
496
- } catch (error) {
497
- if (
498
- error instanceof Error &&
499
- (error.message.includes("cancelled") ||
500
- error.name === "NotAllowedError")
501
- ) {
502
- // User cancelled - no message needed, just log to console
503
- console.log("Port identification cancelled by user");
504
- return;
505
- }
506
- setError(error instanceof Error ? error.message : "Failed to find ports");
507
- } finally {
508
- setIsFindingPorts(false);
509
- }
510
- };
511
-
512
- const ensurePortIsOpen = async (robotIndex: number) => {
513
- const robot = connectedRobots[robotIndex];
514
- if (!robot) return false;
515
-
516
- try {
517
- // If port is already open, we're good
518
- if (robot.port.readable !== null && robot.port.writable !== null) {
519
- return true;
520
- }
521
-
522
- // Try to open the port
523
- await robot.port.open({ baudRate: 1000000 });
524
-
525
- // Update the robot's connection status
526
- const updatedRobots = connectedRobots.map((r, i) =>
527
- i === robotIndex ? { ...r, isConnected: true } : r
528
- );
529
- onConnectedRobotsChange(updatedRobots);
530
-
531
- return true;
532
- } catch (error) {
533
- console.error("Failed to open port for calibration:", error);
534
- setError(error instanceof Error ? error.message : "Failed to open port");
535
- return false;
536
- }
537
- };
538
-
539
- const handleCalibrate = async (port: RobotConnection) => {
540
- if (!port.robotType || !port.robotId) {
541
- setError("Please set robot type and ID before calibrating");
542
- return;
543
- }
544
-
545
- // Find the robot index
546
- const robotIndex = connectedRobots.findIndex((r) => r.port === port.port);
547
- if (robotIndex === -1) {
548
- setError("Robot not found in connected robots list");
549
- return;
550
- }
551
-
552
- // Ensure port is open before calibrating
553
- const isOpen = await ensurePortIsOpen(robotIndex);
554
- if (!isOpen) {
555
- return; // Error already set in ensurePortIsOpen
556
- }
557
-
558
- if (onCalibrate) {
559
- onCalibrate(port.port, port.robotType, port.robotId);
560
- }
561
- };
562
-
563
- return (
564
- <Card>
565
- <CardHeader>
566
- <CardTitle>🔌 Robot Connection Manager</CardTitle>
567
- <CardDescription>
568
- Connect, identify, and manage your robot arms
569
- </CardDescription>
570
- </CardHeader>
571
- <CardContent>
572
- <div className="space-y-6">
573
- {/* Error Display */}
574
- {error && (
575
- <Alert variant="destructive">
576
- <AlertDescription>{error}</AlertDescription>
577
- </Alert>
578
- )}
579
-
580
- {/* Connection Controls */}
581
- <div className="flex gap-2">
582
- <Button
583
- onClick={handleConnect}
584
- disabled={isConnecting || !isWebSerialSupported()}
585
- className="flex-1"
586
- >
587
- {isConnecting ? "Connecting..." : "Connect Robot"}
588
- </Button>
589
- <Button
590
- variant="outline"
591
- onClick={handleFindPorts}
592
- disabled={isFindingPorts || !isWebSerialSupported()}
593
- className="flex-1"
594
- >
595
- {isFindingPorts ? "Finding..." : "Find Port"}
596
- </Button>
597
- </div>
598
-
599
- {/* Find Ports Log */}
600
- {findPortsLog.length > 0 && (
601
- <div className="bg-gray-50 p-3 rounded-md text-sm space-y-1">
602
- {findPortsLog.map((log, index) => (
603
- <div key={index} className="text-gray-700">
604
- {log}
605
- </div>
606
- ))}
607
- </div>
608
- )}
609
-
610
- {/* Connected Ports */}
611
- <div>
612
- <h4 className="font-semibold mb-3">
613
- Connected Robots ({connectedRobots.length})
614
- </h4>
615
-
616
- {connectedRobots.length === 0 ? (
617
- <div className="text-center py-8 text-gray-500">
618
- <div className="text-2xl mb-2">🤖</div>
619
- <p>No robots connected</p>
620
- <p className="text-xs">
621
- Use "Connect Robot" or "Find Port" to add robots
622
- </p>
623
- </div>
624
- ) : (
625
- <div className="space-y-4">
626
- {connectedRobots.map((portInfo, index) => (
627
- <PortCard
628
- key={index}
629
- portInfo={portInfo}
630
- onDisconnect={() => handleDisconnect(index)}
631
- onUpdateInfo={(robotType, robotId) =>
632
- handleUpdatePortInfo(index, robotType, robotId)
633
- }
634
- onCalibrate={() => handleCalibrate(portInfo)}
635
- onTeleoperate={() => onTeleoperate?.(portInfo)}
636
- />
637
- ))}
638
- </div>
639
- )}
640
- </div>
641
- </div>
642
- </CardContent>
643
-
644
- {/* Confirmation Dialog */}
645
- <Dialog open={confirmDeleteDialog.open} onOpenChange={cancelDelete}>
646
- <DialogContent>
647
- <DialogHeader>
648
- <DialogTitle>🗑️ Permanently Delete Robot Data?</DialogTitle>
649
- <DialogDescription>
650
- This action cannot be undone. All robot data will be permanently
651
- deleted.
652
- </DialogDescription>
653
- </DialogHeader>
654
-
655
- <div className="space-y-3">
656
- <div className="p-4 bg-red-50 rounded-lg border border-red-200">
657
- <div className="font-medium text-red-900 mb-2">
658
- Robot Information:
659
- </div>
660
- <div className="text-sm text-red-800 space-y-1">
661
- <div>
662
- • Name:{" "}
663
- <span className="font-mono">
664
- {confirmDeleteDialog.robotName}
665
- </span>
666
- </div>
667
- <div>
668
- • Serial:{" "}
669
- <span className="font-mono">
670
- {confirmDeleteDialog.serialNumber}
671
- </span>
672
- </div>
673
- </div>
674
- </div>
675
-
676
- <div className="p-4 bg-red-50 rounded-lg border border-red-200">
677
- <div className="font-medium text-red-900 mb-2">
678
- This will permanently delete:
679
- </div>
680
- <div className="text-sm text-red-800 space-y-1">
681
- <div>• Robot configuration</div>
682
- <div>• Calibration data</div>
683
- <div>• All saved settings</div>
684
- </div>
685
- </div>
686
- </div>
687
-
688
- <DialogFooter>
689
- <Button variant="outline" onClick={cancelDelete}>
690
- Cancel
691
- </Button>
692
- <Button variant="destructive" onClick={confirmDelete}>
693
- Delete Forever
694
- </Button>
695
- </DialogFooter>
696
- </DialogContent>
697
- </Dialog>
698
- </Card>
699
- );
700
- }
701
-
702
- interface PortCardProps {
703
- portInfo: RobotConnection;
704
- onDisconnect: () => void;
705
- onUpdateInfo: (
706
- robotType: "so100_follower" | "so100_leader",
707
- robotId: string
708
- ) => void;
709
- onCalibrate: () => void;
710
- onTeleoperate: () => void;
711
- }
712
-
713
- function PortCard({
714
- portInfo,
715
- onDisconnect,
716
- onUpdateInfo,
717
- onCalibrate,
718
- onTeleoperate,
719
- }: PortCardProps) {
720
- const [robotType, setRobotType] = useState<"so100_follower" | "so100_leader">(
721
- portInfo.robotType || "so100_follower"
722
- );
723
- const [robotId, setRobotId] = useState(portInfo.robotId || "");
724
- const [isEditing, setIsEditing] = useState(false);
725
- const [isScanning, setIsScanning] = useState(false);
726
- const [motorIDs, setMotorIDs] = useState<number[]>([]);
727
- const [portMetadata, setPortMetadata] = useState<any>(null);
728
- const [showDeviceInfo, setShowDeviceInfo] = useState(false);
729
-
730
- // Check for calibration using unified storage
731
- const getCalibrationStatus = () => {
732
- // Use the same serial number logic as calibration: prefer main serialNumber, fallback to USB metadata, then "unknown"
733
- const serialNumber =
734
- portInfo.serialNumber || portInfo.usbMetadata?.serialNumber || "unknown";
735
-
736
- try {
737
- // Use unified storage system with automatic migration
738
- import("../lib/unified-storage")
739
- .then(({ getCalibrationStatus }) => {
740
- const status = getCalibrationStatus(serialNumber);
741
- return status;
742
- })
743
- .catch((error) => {
744
- console.warn("Failed to load unified calibration data:", error);
745
- return null;
746
- });
747
-
748
- // For immediate synchronous return, try to get existing unified data first
749
- const unifiedKey = `lerobotjs-${serialNumber}`;
750
- const existing = localStorage.getItem(unifiedKey);
751
- if (existing) {
752
- const data = JSON.parse(existing);
753
- if (data.calibration?.metadata) {
754
- return {
755
- timestamp: data.calibration.metadata.timestamp,
756
- readCount: data.calibration.metadata.readCount,
757
- };
758
- }
759
- }
760
- } catch (error) {
761
- console.warn("Failed to read calibration from unified storage:", error);
762
- }
763
- return null;
764
- };
765
-
766
- const calibrationStatus = getCalibrationStatus();
767
-
768
- const handleSave = () => {
769
- if (robotId.trim()) {
770
- onUpdateInfo(robotType, robotId.trim());
771
- setIsEditing(false);
772
- }
773
- };
774
-
775
- // Use current values (either from props or local state)
776
- const currentRobotType = portInfo.robotType || robotType;
777
- const currentRobotId = portInfo.robotId || robotId;
778
-
779
- const handleCancel = () => {
780
- setRobotType(portInfo.robotType || "so100_follower");
781
- setRobotId(portInfo.robotId || "");
782
- setIsEditing(false);
783
- };
784
-
785
- // Scan for motor IDs and gather USB device metadata
786
- const scanDeviceInfo = async () => {
787
- if (!portInfo.port || !portInfo.isConnected) {
788
- console.warn("Port not connected");
789
- return;
790
- }
791
-
792
- setIsScanning(true);
793
- setMotorIDs([]);
794
- setPortMetadata(null);
795
- const foundIDs: number[] = [];
796
-
797
- try {
798
- // Try to get USB device info using WebUSB for better metadata
799
- let usbDeviceInfo = null;
800
-
801
- try {
802
- // First, check if we already have USB device permissions
803
- let usbDevices = await navigator.usb.getDevices();
804
- console.log("Already permitted USB devices:", usbDevices);
805
-
806
- // If no devices found, request permission for USB-to-serial devices
807
- if (usbDevices.length === 0) {
808
- console.log(
809
- "No USB permissions yet, requesting access to USB-to-serial devices..."
810
- );
811
-
812
- // Request access to common USB-to-serial chips
813
- try {
814
- const device = await navigator.usb.requestDevice({
815
- filters: [
816
- { vendorId: 0x0403 }, // FTDI
817
- { vendorId: 0x067b }, // Prolific
818
- { vendorId: 0x10c4 }, // Silicon Labs
819
- { vendorId: 0x1a86 }, // QinHeng Electronics (CH340)
820
- { vendorId: 0x239a }, // Adafruit
821
- { vendorId: 0x2341 }, // Arduino
822
- { vendorId: 0x2e8a }, // Raspberry Pi Foundation
823
- { vendorId: 0x1b4f }, // SparkFun
824
- ],
825
- });
826
-
827
- if (device) {
828
- usbDevices = [device];
829
- console.log("USB device access granted:", device);
830
- }
831
- } catch (requestError) {
832
- console.log(
833
- "User cancelled USB device selection or no devices found"
834
- );
835
- // Try requesting any device as fallback
836
- try {
837
- const anyDevice = await navigator.usb.requestDevice({
838
- filters: [], // Allow any USB device
839
- });
840
- if (anyDevice) {
841
- usbDevices = [anyDevice];
842
- console.log("Fallback USB device selected:", anyDevice);
843
- }
844
- } catch (fallbackError) {
845
- console.log("No USB device selected");
846
- }
847
- }
848
- }
849
-
850
- // Try to match with Web Serial port (this is tricky, so we'll take the first available)
851
- if (usbDevices.length > 0) {
852
- // Look for common USB-to-serial chip vendor IDs
853
- const serialChipVendors = [
854
- 0x0403, // FTDI
855
- 0x067b, // Prolific
856
- 0x10c4, // Silicon Labs
857
- 0x1a86, // QinHeng Electronics (CH340)
858
- 0x239a, // Adafruit
859
- 0x2341, // Arduino
860
- 0x2e8a, // Raspberry Pi Foundation
861
- 0x1b4f, // SparkFun
862
- ];
863
-
864
- const serialDevice =
865
- usbDevices.find((device) =>
866
- serialChipVendors.includes(device.vendorId)
867
- ) || usbDevices[0]; // Fallback to first device
868
-
869
- if (serialDevice) {
870
- usbDeviceInfo = {
871
- vendorId: `0x${serialDevice.vendorId
872
- .toString(16)
873
- .padStart(4, "0")}`,
874
- productId: `0x${serialDevice.productId
875
- .toString(16)
876
- .padStart(4, "0")}`,
877
- serialNumber: serialDevice.serialNumber || "Not available",
878
- manufacturerName: serialDevice.manufacturerName || "Unknown",
879
- productName: serialDevice.productName || "Unknown",
880
- usbVersionMajor: serialDevice.usbVersionMajor,
881
- usbVersionMinor: serialDevice.usbVersionMinor,
882
- deviceClass: serialDevice.deviceClass,
883
- deviceSubclass: serialDevice.deviceSubclass,
884
- deviceProtocol: serialDevice.deviceProtocol,
885
- };
886
- console.log("USB device info:", usbDeviceInfo);
887
- }
888
- }
889
- } catch (usbError) {
890
- console.log("WebUSB not available or no permissions:", usbError);
891
- // Fallback to Web Serial API info
892
- const portInfo_metadata = portInfo.port.getInfo();
893
- console.log("Serial port metadata fallback:", portInfo_metadata);
894
- if (Object.keys(portInfo_metadata).length > 0) {
895
- usbDeviceInfo = {
896
- vendorId: portInfo_metadata.usbVendorId
897
- ? `0x${portInfo_metadata.usbVendorId
898
- .toString(16)
899
- .padStart(4, "0")}`
900
- : "Not available",
901
- productId: portInfo_metadata.usbProductId
902
- ? `0x${portInfo_metadata.usbProductId
903
- .toString(16)
904
- .padStart(4, "0")}`
905
- : "Not available",
906
- serialNumber: "Not available via Web Serial",
907
- manufacturerName: "Not available via Web Serial",
908
- productName: "Not available via Web Serial",
909
- };
910
- }
911
- }
912
-
913
- setPortMetadata(usbDeviceInfo);
914
-
915
- // Get reader/writer for the port
916
- const reader = portInfo.port.readable?.getReader();
917
- const writer = portInfo.port.writable?.getWriter();
918
-
919
- if (!reader || !writer) {
920
- console.warn("Cannot access port reader/writer");
921
- setShowDeviceInfo(true);
922
- return;
923
- }
924
-
925
- // Test motor IDs 1-10 (common range for servos)
926
- for (let motorId = 1; motorId <= 10; motorId++) {
927
- try {
928
- // Create STS3215 ping packet
929
- const packet = new Uint8Array([
930
- 0xff,
931
- 0xff,
932
- motorId,
933
- 0x02,
934
- 0x01,
935
- 0x00,
936
- ]);
937
- const checksum = ~(motorId + 0x02 + 0x01) & 0xff;
938
- packet[5] = checksum;
939
-
940
- // Send ping
941
- await writer.write(packet);
942
-
943
- // Wait a bit for response
944
- await new Promise((resolve) => setTimeout(resolve, 20));
945
-
946
- // Try to read response with timeout
947
- const timeoutPromise = new Promise((_, reject) =>
948
- setTimeout(() => reject(new Error("Timeout")), 50)
949
- );
950
-
951
- try {
952
- const result = (await Promise.race([
953
- reader.read(),
954
- timeoutPromise,
955
- ])) as ReadableStreamReadResult<Uint8Array>;
956
-
957
- if (
958
- result &&
959
- !result.done &&
960
- result.value &&
961
- result.value.length >= 6
962
- ) {
963
- const response = result.value;
964
- const responseId = response[2];
965
-
966
- // If we got a response with matching ID, motor exists
967
- if (responseId === motorId) {
968
- foundIDs.push(motorId);
969
- }
970
- }
971
- } catch (readError) {
972
- // No response from this motor ID - that's normal
973
- }
974
- } catch (error) {
975
- console.warn(`Error testing motor ID ${motorId}:`, error);
976
- }
977
-
978
- // Small delay between tests
979
- await new Promise((resolve) => setTimeout(resolve, 10));
980
- }
981
-
982
- reader.releaseLock();
983
- writer.releaseLock();
984
-
985
- setMotorIDs(foundIDs);
986
- setShowDeviceInfo(true);
987
- } catch (error) {
988
- console.error("Device info scan failed:", error);
989
- } finally {
990
- setIsScanning(false);
991
- }
992
- };
993
-
994
- return (
995
- <div className="border rounded-lg p-4 space-y-3">
996
- {/* Header with port name and status */}
997
- <div className="flex items-center justify-between">
998
- <div className="flex items-center space-x-2">
999
- <div className="flex flex-col">
1000
- <span className="font-medium">{portInfo.name}</span>
1001
- {portInfo.serialNumber && (
1002
- <span className="text-xs text-gray-500 font-mono">
1003
- ID:{" "}
1004
- {portInfo.serialNumber.length > 20
1005
- ? portInfo.serialNumber.substring(0, 20) + "..."
1006
- : portInfo.serialNumber}
1007
- </span>
1008
- )}
1009
- </div>
1010
- <Badge variant={portInfo.isConnected ? "default" : "outline"}>
1011
- {portInfo.isConnected ? "Connected" : "Available"}
1012
- </Badge>
1013
- </div>
1014
- <Button variant="destructive" size="sm" onClick={onDisconnect}>
1015
- Remove
1016
- </Button>
1017
- </div>
1018
-
1019
- {/* Robot Info Display (when not editing) */}
1020
- {!isEditing && currentRobotType && currentRobotId && (
1021
- <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
1022
- <div className="flex items-center space-x-3">
1023
- <div>
1024
- <div className="font-medium text-sm">{currentRobotId}</div>
1025
- <div className="text-xs text-gray-600">
1026
- {currentRobotType.replace("_", " ")}
1027
- </div>
1028
- </div>
1029
- {calibrationStatus && (
1030
- <Badge variant="default" className="bg-green-100 text-green-800">
1031
- ✅ Calibrated
1032
- </Badge>
1033
- )}
1034
- </div>
1035
- <Button
1036
- variant="outline"
1037
- size="sm"
1038
- onClick={() => setIsEditing(true)}
1039
- >
1040
- Edit
1041
- </Button>
1042
- </div>
1043
- )}
1044
-
1045
- {/* Setup prompt for unconfigured robots */}
1046
- {!isEditing && (!currentRobotType || !currentRobotId) && (
1047
- <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
1048
- <div className="text-sm text-blue-800">
1049
- Robot needs configuration before use
1050
- </div>
1051
- <Button
1052
- variant="outline"
1053
- size="sm"
1054
- onClick={() => setIsEditing(true)}
1055
- >
1056
- Configure
1057
- </Button>
1058
- </div>
1059
- )}
1060
-
1061
- {/* Robot Configuration Form (when editing) */}
1062
- {isEditing && (
1063
- <div className="space-y-3 p-3 bg-gray-50 rounded-lg">
1064
- <div className="grid grid-cols-2 gap-3">
1065
- <div>
1066
- <label className="text-sm font-medium block mb-1">
1067
- Robot Type
1068
- </label>
1069
- <select
1070
- value={robotType}
1071
- onChange={(e) =>
1072
- setRobotType(
1073
- e.target.value as "so100_follower" | "so100_leader"
1074
- )
1075
- }
1076
- className="w-full px-2 py-1 border rounded text-sm"
1077
- >
1078
- <option value="so100_follower">SO-100 Follower</option>
1079
- <option value="so100_leader">SO-100 Leader</option>
1080
- </select>
1081
- </div>
1082
- <div>
1083
- <label className="text-sm font-medium block mb-1">Robot ID</label>
1084
- <input
1085
- type="text"
1086
- value={robotId}
1087
- onChange={(e) => setRobotId(e.target.value)}
1088
- placeholder="e.g., my_robot"
1089
- className="w-full px-2 py-1 border rounded text-sm"
1090
- />
1091
- </div>
1092
- </div>
1093
-
1094
- <div className="flex gap-2">
1095
- <Button size="sm" onClick={handleSave} disabled={!robotId.trim()}>
1096
- Save
1097
- </Button>
1098
- <Button size="sm" variant="outline" onClick={handleCancel}>
1099
- Cancel
1100
- </Button>
1101
- </div>
1102
- </div>
1103
- )}
1104
-
1105
- {/* Calibration Status and Action */}
1106
- {currentRobotType && currentRobotId && (
1107
- <div className="space-y-3">
1108
- <div className="flex items-center justify-between">
1109
- <div className="text-sm text-gray-600">
1110
- {calibrationStatus ? (
1111
- <span>
1112
- Last calibrated:{" "}
1113
- {new Date(calibrationStatus.timestamp).toLocaleDateString()}
1114
- <span className="text-xs ml-1">
1115
- ({calibrationStatus.readCount} readings)
1116
- </span>
1117
- </span>
1118
- ) : (
1119
- <span>Not calibrated yet</span>
1120
- )}
1121
- </div>
1122
- <div className="flex gap-2">
1123
- <Button
1124
- size="sm"
1125
- variant={calibrationStatus ? "outline" : "default"}
1126
- onClick={onCalibrate}
1127
- disabled={!currentRobotType || !currentRobotId}
1128
- >
1129
- {calibrationStatus ? "Re-calibrate" : "Calibrate"}
1130
- </Button>
1131
- <Button
1132
- size="sm"
1133
- variant="outline"
1134
- onClick={onTeleoperate}
1135
- disabled={
1136
- !currentRobotType || !currentRobotId || !portInfo.isConnected
1137
- }
1138
- >
1139
- 🎮 Teleoperate
1140
- </Button>
1141
- </div>
1142
- </div>
1143
-
1144
- {/* Device Info Scanner */}
1145
- <div className="flex items-center justify-between">
1146
- <div className="text-sm text-gray-600">
1147
- Scan device info and motor IDs
1148
- </div>
1149
- <Button
1150
- size="sm"
1151
- variant="outline"
1152
- onClick={scanDeviceInfo}
1153
- disabled={!portInfo.isConnected || isScanning}
1154
- >
1155
- {isScanning ? "Scanning..." : "Show Device Info"}
1156
- </Button>
1157
- </div>
1158
-
1159
- {/* Device Info Results */}
1160
- {showDeviceInfo && (
1161
- <div className="p-3 bg-gray-50 rounded-lg space-y-3">
1162
- {/* USB Device Information */}
1163
- {portMetadata && (
1164
- <div>
1165
- <div className="text-sm font-medium mb-2">
1166
- 📱 USB Device Info:
1167
- </div>
1168
- <div className="space-y-1 text-xs">
1169
- <div className="flex justify-between">
1170
- <span className="text-gray-600">Vendor ID:</span>
1171
- <span className="font-mono">{portMetadata.vendorId}</span>
1172
- </div>
1173
- <div className="flex justify-between">
1174
- <span className="text-gray-600">Product ID:</span>
1175
- <span className="font-mono">
1176
- {portMetadata.productId}
1177
- </span>
1178
- </div>
1179
- <div className="flex justify-between">
1180
- <span className="text-gray-600">Serial Number:</span>
1181
- <span className="font-mono text-green-600 font-semibold">
1182
- {portMetadata.serialNumber}
1183
- </span>
1184
- </div>
1185
- <div className="flex justify-between">
1186
- <span className="text-gray-600">Manufacturer:</span>
1187
- <span>{portMetadata.manufacturerName}</span>
1188
- </div>
1189
- <div className="flex justify-between">
1190
- <span className="text-gray-600">Product:</span>
1191
- <span>{portMetadata.productName}</span>
1192
- </div>
1193
- {portMetadata.usbVersionMajor && (
1194
- <div className="flex justify-between">
1195
- <span className="text-gray-600">USB Version:</span>
1196
- <span>
1197
- {portMetadata.usbVersionMajor}.
1198
- {portMetadata.usbVersionMinor}
1199
- </span>
1200
- </div>
1201
- )}
1202
- {portMetadata.deviceClass !== undefined && (
1203
- <div className="flex justify-between">
1204
- <span className="text-gray-600">Device Class:</span>
1205
- <span>
1206
- 0x
1207
- {portMetadata.deviceClass
1208
- .toString(16)
1209
- .padStart(2, "0")}
1210
- </span>
1211
- </div>
1212
- )}
1213
- </div>
1214
- </div>
1215
- )}
1216
-
1217
- {/* Motor IDs */}
1218
- <div>
1219
- <div className="text-sm font-medium mb-2">
1220
- 🤖 Found Motor IDs:
1221
- </div>
1222
- {motorIDs.length > 0 ? (
1223
- <div className="flex flex-wrap gap-2">
1224
- {motorIDs.map((id) => (
1225
- <Badge key={id} variant="outline" className="text-xs">
1226
- Motor {id}
1227
- </Badge>
1228
- ))}
1229
- </div>
1230
- ) : (
1231
- <div className="text-sm text-gray-500">
1232
- No motor IDs found. Check connection and power.
1233
- </div>
1234
- )}
1235
- </div>
1236
-
1237
- <Button
1238
- size="sm"
1239
- variant="outline"
1240
- onClick={() => setShowDeviceInfo(false)}
1241
- className="mt-2 text-xs"
1242
- >
1243
- Hide
1244
- </Button>
1245
- </div>
1246
- )}
1247
- </div>
1248
- )}
1249
- </div>
1250
- );
1251
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/TeleoperationPanel.tsx DELETED
@@ -1,530 +0,0 @@
1
- import { useState, useEffect, useRef, useCallback } from "react";
2
- import { Button } from "./ui/button";
3
- import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
4
- import { Badge } from "./ui/badge";
5
- import { Alert, AlertDescription } from "./ui/alert";
6
- import {
7
- teleoperate,
8
- type TeleoperationProcess,
9
- type TeleoperationState,
10
- } from "@lerobot/web";
11
- import { getUnifiedRobotData } from "../lib/unified-storage";
12
- import type { RobotConnection } from "@lerobot/web";
13
- import { SO100_KEYBOARD_CONTROLS } from "@lerobot/web";
14
-
15
- interface TeleoperationPanelProps {
16
- robot: RobotConnection;
17
- onClose: () => void;
18
- }
19
-
20
- export function TeleoperationPanel({
21
- robot,
22
- onClose,
23
- }: TeleoperationPanelProps) {
24
- const [teleoperationState, setTeleoperationState] =
25
- useState<TeleoperationState>({
26
- isActive: false,
27
- motorConfigs: [],
28
- lastUpdate: 0,
29
- keyStates: {},
30
- });
31
- const [error, setError] = useState<string | null>(null);
32
- const [, setIsInitialized] = useState(false);
33
-
34
- const teleoperationProcessRef = useRef<TeleoperationProcess | null>(null);
35
-
36
- // Initialize teleoperation process
37
- useEffect(() => {
38
- const initializeTeleoperation = async () => {
39
- if (!robot || !robot.robotType) {
40
- setError("No robot configuration available");
41
- return;
42
- }
43
-
44
- try {
45
- // Load calibration data from demo storage (app concern)
46
- let calibrationData;
47
- if (robot.serialNumber) {
48
- const data = getUnifiedRobotData(robot.serialNumber);
49
- calibrationData = data?.calibration;
50
- if (calibrationData) {
51
- console.log("✅ Loaded calibration data for", robot.serialNumber);
52
- }
53
- }
54
-
55
- // Create teleoperation process using clean library API
56
- const process = await teleoperate(robot, {
57
- calibrationData,
58
- onStateUpdate: (state: TeleoperationState) => {
59
- setTeleoperationState(state);
60
- },
61
- });
62
-
63
- teleoperationProcessRef.current = process;
64
- setTeleoperationState(process.getState());
65
- setIsInitialized(true);
66
- setError(null);
67
- } catch (error) {
68
- const errorMessage =
69
- error instanceof Error
70
- ? error.message
71
- : "Failed to initialize teleoperation";
72
- setError(errorMessage);
73
- console.error("❌ Failed to initialize teleoperation:", error);
74
- }
75
- };
76
-
77
- initializeTeleoperation();
78
-
79
- return () => {
80
- // Cleanup on unmount
81
- if (teleoperationProcessRef.current) {
82
- teleoperationProcessRef.current.disconnect();
83
- teleoperationProcessRef.current = null;
84
- }
85
- };
86
- }, [robot]);
87
-
88
- // Keyboard event handlers
89
- const handleKeyDown = useCallback(
90
- (event: KeyboardEvent) => {
91
- if (!teleoperationState.isActive || !teleoperationProcessRef.current)
92
- return;
93
-
94
- const key = event.key;
95
- event.preventDefault();
96
- teleoperationProcessRef.current.updateKeyState(key, true);
97
- },
98
- [teleoperationState.isActive]
99
- );
100
-
101
- const handleKeyUp = useCallback(
102
- (event: KeyboardEvent) => {
103
- if (!teleoperationState.isActive || !teleoperationProcessRef.current)
104
- return;
105
-
106
- const key = event.key;
107
- event.preventDefault();
108
- teleoperationProcessRef.current.updateKeyState(key, false);
109
- },
110
- [teleoperationState.isActive]
111
- );
112
-
113
- // Register keyboard events
114
- useEffect(() => {
115
- if (teleoperationState.isActive) {
116
- window.addEventListener("keydown", handleKeyDown);
117
- window.addEventListener("keyup", handleKeyUp);
118
-
119
- return () => {
120
- window.removeEventListener("keydown", handleKeyDown);
121
- window.removeEventListener("keyup", handleKeyUp);
122
- };
123
- }
124
- }, [teleoperationState.isActive, handleKeyDown, handleKeyUp]);
125
-
126
- const handleStart = () => {
127
- if (!teleoperationProcessRef.current) {
128
- setError("Teleoperation not initialized");
129
- return;
130
- }
131
-
132
- try {
133
- teleoperationProcessRef.current.start();
134
- console.log("🎮 Teleoperation started");
135
- } catch (error) {
136
- const errorMessage =
137
- error instanceof Error
138
- ? error.message
139
- : "Failed to start teleoperation";
140
- setError(errorMessage);
141
- }
142
- };
143
-
144
- const handleStop = () => {
145
- if (!teleoperationProcessRef.current) return;
146
-
147
- teleoperationProcessRef.current.stop();
148
- console.log("🛑 Teleoperation stopped");
149
- };
150
-
151
- const handleClose = () => {
152
- if (teleoperationProcessRef.current) {
153
- teleoperationProcessRef.current.stop();
154
- }
155
- onClose();
156
- };
157
-
158
- const simulateKeyPress = (key: string) => {
159
- if (!teleoperationProcessRef.current) return;
160
- teleoperationProcessRef.current.updateKeyState(key, true);
161
- };
162
-
163
- const simulateKeyRelease = (key: string) => {
164
- if (!teleoperationProcessRef.current) return;
165
- teleoperationProcessRef.current.updateKeyState(key, false);
166
- };
167
-
168
- const moveMotorToPosition = async (motorIndex: number, position: number) => {
169
- if (!teleoperationProcessRef.current) return;
170
-
171
- try {
172
- const motorName = teleoperationState.motorConfigs[motorIndex]?.name;
173
- if (motorName) {
174
- await teleoperationProcessRef.current.moveMotor(motorName, position);
175
- }
176
- } catch (error) {
177
- console.warn(
178
- `Failed to move motor ${motorIndex + 1} to position ${position}:`,
179
- error
180
- );
181
- }
182
- };
183
-
184
- const isConnected = robot?.isConnected || false;
185
- const isActive = teleoperationState.isActive;
186
- const motorConfigs = teleoperationState.motorConfigs;
187
- const keyStates = teleoperationState.keyStates;
188
-
189
- // Virtual keyboard component
190
- const VirtualKeyboard = () => {
191
- const isKeyPressed = (key: string) => {
192
- return keyStates[key]?.pressed || false;
193
- };
194
-
195
- const KeyButton = ({
196
- keyCode,
197
- children,
198
- className = "",
199
- size = "default" as "default" | "sm" | "lg" | "icon",
200
- }: {
201
- keyCode: string;
202
- children: React.ReactNode;
203
- className?: string;
204
- size?: "default" | "sm" | "lg" | "icon";
205
- }) => {
206
- const control =
207
- SO100_KEYBOARD_CONTROLS[
208
- keyCode as keyof typeof SO100_KEYBOARD_CONTROLS
209
- ];
210
- const pressed = isKeyPressed(keyCode);
211
-
212
- return (
213
- <Button
214
- variant={pressed ? "default" : "outline"}
215
- size={size}
216
- className={`
217
- ${className}
218
- ${
219
- pressed
220
- ? "bg-blue-600 text-white shadow-inner"
221
- : "hover:bg-gray-100"
222
- }
223
- transition-all duration-75 font-mono text-xs
224
- ${!isActive ? "opacity-50 cursor-not-allowed" : ""}
225
- `}
226
- disabled={!isActive}
227
- onMouseDown={(e) => {
228
- e.preventDefault();
229
- if (isActive) simulateKeyPress(keyCode);
230
- }}
231
- onMouseUp={(e) => {
232
- e.preventDefault();
233
- if (isActive) simulateKeyRelease(keyCode);
234
- }}
235
- onMouseLeave={(e) => {
236
- e.preventDefault();
237
- if (isActive) simulateKeyRelease(keyCode);
238
- }}
239
- title={control?.description || keyCode}
240
- >
241
- {children}
242
- </Button>
243
- );
244
- };
245
-
246
- return (
247
- <div className="space-y-4">
248
- {/* Arrow Keys */}
249
- <div className="text-center">
250
- <h4 className="text-xs font-semibold mb-2 text-gray-600">Shoulder</h4>
251
- <div className="flex flex-col items-center gap-1">
252
- <KeyButton keyCode="ArrowUp" size="sm">
253
-
254
- </KeyButton>
255
- <div className="flex gap-1">
256
- <KeyButton keyCode="ArrowLeft" size="sm">
257
-
258
- </KeyButton>
259
- <KeyButton keyCode="ArrowDown" size="sm">
260
-
261
- </KeyButton>
262
- <KeyButton keyCode="ArrowRight" size="sm">
263
-
264
- </KeyButton>
265
- </div>
266
- </div>
267
- </div>
268
-
269
- {/* WASD Keys */}
270
- <div className="text-center">
271
- <h4 className="text-xs font-semibold mb-2 text-gray-600">
272
- Elbow/Wrist
273
- </h4>
274
- <div className="flex flex-col items-center gap-1">
275
- <KeyButton keyCode="w" size="sm">
276
- W
277
- </KeyButton>
278
- <div className="flex gap-1">
279
- <KeyButton keyCode="a" size="sm">
280
- A
281
- </KeyButton>
282
- <KeyButton keyCode="s" size="sm">
283
- S
284
- </KeyButton>
285
- <KeyButton keyCode="d" size="sm">
286
- D
287
- </KeyButton>
288
- </div>
289
- </div>
290
- </div>
291
-
292
- {/* Q/E and Gripper */}
293
- <div className="flex justify-center gap-2">
294
- <div className="text-center">
295
- <h4 className="text-xs font-semibold mb-2 text-gray-600">Roll</h4>
296
- <div className="flex gap-1">
297
- <KeyButton keyCode="q" size="sm">
298
- Q
299
- </KeyButton>
300
- <KeyButton keyCode="e" size="sm">
301
- E
302
- </KeyButton>
303
- </div>
304
- </div>
305
- <div className="text-center">
306
- <h4 className="text-xs font-semibold mb-2 text-gray-600">
307
- Gripper
308
- </h4>
309
- <div className="flex gap-1">
310
- <KeyButton keyCode="o" size="sm">
311
- O
312
- </KeyButton>
313
- <KeyButton keyCode="c" size="sm">
314
- C
315
- </KeyButton>
316
- </div>
317
- </div>
318
- </div>
319
-
320
- {/* Emergency Stop */}
321
- <div className="text-center border-t pt-2">
322
- <KeyButton
323
- keyCode="Escape"
324
- className="bg-red-100 border-red-300 hover:bg-red-200 text-red-800 text-xs"
325
- >
326
- ESC
327
- </KeyButton>
328
- </div>
329
- </div>
330
- );
331
- };
332
-
333
- return (
334
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
335
- <div className="container mx-auto px-6 py-8">
336
- {/* Header */}
337
- <div className="flex justify-between items-center mb-6">
338
- <div>
339
- <h1 className="text-3xl font-bold text-gray-900">
340
- 🎮 Robot Teleoperation
341
- </h1>
342
- <p className="text-gray-600">
343
- {robot.robotId || robot.name} - {robot.serialNumber}
344
- </p>
345
- </div>
346
- <Button variant="outline" onClick={handleClose}>
347
- ← Back to Dashboard
348
- </Button>
349
- </div>
350
-
351
- {/* Error Alert */}
352
- {error && (
353
- <Alert variant="destructive" className="mb-6">
354
- <AlertDescription>{error}</AlertDescription>
355
- </Alert>
356
- )}
357
-
358
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
359
- {/* Status Panel */}
360
- <Card>
361
- <CardHeader>
362
- <CardTitle className="flex items-center gap-2">
363
- Status
364
- <Badge variant={isConnected ? "default" : "destructive"}>
365
- {isConnected ? "Connected" : "Disconnected"}
366
- </Badge>
367
- </CardTitle>
368
- </CardHeader>
369
- <CardContent className="space-y-4">
370
- <div className="flex items-center justify-between">
371
- <span className="text-sm text-gray-600">Teleoperation</span>
372
- <Badge variant={isActive ? "default" : "secondary"}>
373
- {isActive ? "Active" : "Stopped"}
374
- </Badge>
375
- </div>
376
-
377
- <div className="flex items-center justify-between">
378
- <span className="text-sm text-gray-600">Active Keys</span>
379
- <Badge variant="outline">
380
- {
381
- Object.values(keyStates).filter((state) => state.pressed)
382
- .length
383
- }
384
- </Badge>
385
- </div>
386
-
387
- <div className="space-y-2">
388
- {isActive ? (
389
- <Button
390
- onClick={handleStop}
391
- variant="destructive"
392
- className="w-full"
393
- >
394
- ⏹️ Stop Teleoperation
395
- </Button>
396
- ) : (
397
- <Button
398
- onClick={handleStart}
399
- disabled={!isConnected}
400
- className="w-full"
401
- >
402
- ▶️ Start Teleoperation
403
- </Button>
404
- )}
405
- </div>
406
- </CardContent>
407
- </Card>
408
-
409
- {/* Virtual Keyboard */}
410
- <Card>
411
- <CardHeader>
412
- <CardTitle>Virtual Keyboard</CardTitle>
413
- </CardHeader>
414
- <CardContent>
415
- <VirtualKeyboard />
416
- </CardContent>
417
- </Card>
418
-
419
- {/* Motor Status */}
420
- <Card>
421
- <CardHeader>
422
- <CardTitle>Motor Positions</CardTitle>
423
- </CardHeader>
424
- <CardContent className="space-y-3">
425
- {motorConfigs.map((motor, index) => {
426
- return (
427
- <div key={motor.name} className="space-y-1">
428
- <div className="flex justify-between items-center">
429
- <span className="text-sm font-medium">
430
- {motor.name.replace("_", " ")}
431
- </span>
432
- <span className="text-xs text-gray-500">
433
- {motor.currentPosition}
434
- </span>
435
- </div>
436
- <input
437
- type="range"
438
- min={motor.minPosition}
439
- max={motor.maxPosition}
440
- value={motor.currentPosition}
441
- disabled={!isActive}
442
- className={`w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200 slider-thumb ${
443
- !isActive ? "opacity-50 cursor-not-allowed" : ""
444
- }`}
445
- style={{
446
- background: isActive
447
- ? `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${
448
- ((motor.currentPosition - motor.minPosition) /
449
- (motor.maxPosition - motor.minPosition)) *
450
- 100
451
- }%, #e5e7eb ${
452
- ((motor.currentPosition - motor.minPosition) /
453
- (motor.maxPosition - motor.minPosition)) *
454
- 100
455
- }%, #e5e7eb 100%)`
456
- : "#e5e7eb",
457
- }}
458
- onChange={async (e) => {
459
- if (!isActive) return;
460
- const newPosition = parseInt(e.target.value);
461
- try {
462
- await moveMotorToPosition(index, newPosition);
463
- } catch (error) {
464
- console.warn(
465
- "Failed to move motor via slider:",
466
- error
467
- );
468
- }
469
- }}
470
- />
471
- <div className="flex justify-between text-xs text-gray-400">
472
- <span>{motor.minPosition}</span>
473
- <span>{motor.maxPosition}</span>
474
- </div>
475
- </div>
476
- );
477
- })}
478
- </CardContent>
479
- </Card>
480
- </div>
481
-
482
- {/* Help Card */}
483
- <Card className="mt-6">
484
- <CardHeader>
485
- <CardTitle>Control Instructions</CardTitle>
486
- </CardHeader>
487
- <CardContent>
488
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
489
- <div>
490
- <h4 className="font-semibold mb-2">Arrow Keys</h4>
491
- <ul className="space-y-1 text-gray-600">
492
- <li>↑ ↓ Shoulder lift</li>
493
- <li>← → Shoulder pan</li>
494
- </ul>
495
- </div>
496
- <div>
497
- <h4 className="font-semibold mb-2">WASD Keys</h4>
498
- <ul className="space-y-1 text-gray-600">
499
- <li>W S Elbow flex</li>
500
- <li>A D Wrist flex</li>
501
- </ul>
502
- </div>
503
- <div>
504
- <h4 className="font-semibold mb-2">Other Keys</h4>
505
- <ul className="space-y-1 text-gray-600">
506
- <li>Q E Wrist roll</li>
507
- <li>O Open gripper</li>
508
- <li>C Close gripper</li>
509
- </ul>
510
- </div>
511
- <div>
512
- <h4 className="font-semibold mb-2 text-red-700">Emergency</h4>
513
- <ul className="space-y-1 text-red-600">
514
- <li>ESC Emergency stop</li>
515
- </ul>
516
- </div>
517
- </div>
518
- <div className="mt-4 p-3 bg-blue-50 rounded-lg">
519
- <p className="text-sm text-blue-800">
520
- 💡 <strong>Pro tip:</strong> Use your physical keyboard for
521
- faster control, or click the virtual keys below. Hold keys down
522
- for continuous movement.
523
- </p>
524
- </div>
525
- </CardContent>
526
- </Card>
527
- </div>
528
- </div>
529
- );
530
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/ui/alert.tsx DELETED
@@ -1,58 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
- import { cn } from "../../lib/utils";
4
-
5
- const alertVariants = cva(
6
- "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
7
- {
8
- variants: {
9
- variant: {
10
- default: "bg-background text-foreground",
11
- destructive:
12
- "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
13
- },
14
- },
15
- defaultVariants: {
16
- variant: "default",
17
- },
18
- }
19
- );
20
-
21
- const Alert = React.forwardRef<
22
- HTMLDivElement,
23
- React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
24
- >(({ className, variant, ...props }, ref) => (
25
- <div
26
- ref={ref}
27
- role="alert"
28
- className={cn(alertVariants({ variant }), className)}
29
- {...props}
30
- />
31
- ));
32
- Alert.displayName = "Alert";
33
-
34
- const AlertTitle = React.forwardRef<
35
- HTMLParagraphElement,
36
- React.HTMLAttributes<HTMLHeadingElement>
37
- >(({ className, ...props }, ref) => (
38
- <h5
39
- ref={ref}
40
- className={cn("mb-1 font-medium leading-none tracking-tight", className)}
41
- {...props}
42
- />
43
- ));
44
- AlertTitle.displayName = "AlertTitle";
45
-
46
- const AlertDescription = React.forwardRef<
47
- HTMLParagraphElement,
48
- React.HTMLAttributes<HTMLParagraphElement>
49
- >(({ className, ...props }, ref) => (
50
- <div
51
- ref={ref}
52
- className={cn("text-sm [&_p]:leading-relaxed", className)}
53
- {...props}
54
- />
55
- ));
56
- AlertDescription.displayName = "AlertDescription";
57
-
58
- export { Alert, AlertTitle, AlertDescription };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/ui/badge.tsx DELETED
@@ -1,35 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
- import { cn } from "../../lib/utils";
4
-
5
- const badgeVariants = cva(
6
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
7
- {
8
- variants: {
9
- variant: {
10
- default:
11
- "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
- secondary:
13
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
- destructive:
15
- "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16
- outline: "text-foreground",
17
- },
18
- },
19
- defaultVariants: {
20
- variant: "default",
21
- },
22
- }
23
- );
24
-
25
- export interface BadgeProps
26
- extends React.HTMLAttributes<HTMLDivElement>,
27
- VariantProps<typeof badgeVariants> {}
28
-
29
- function Badge({ className, variant, ...props }: BadgeProps) {
30
- return (
31
- <div className={cn(badgeVariants({ variant }), className)} {...props} />
32
- );
33
- }
34
-
35
- export { Badge, badgeVariants };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/ui/button.tsx DELETED
@@ -1,53 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
- import { cn } from "../../lib/utils";
4
-
5
- const buttonVariants = cva(
6
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
7
- {
8
- variants: {
9
- variant: {
10
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
11
- destructive:
12
- "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13
- outline:
14
- "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15
- secondary:
16
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
17
- ghost: "hover:bg-accent hover:text-accent-foreground",
18
- link: "text-primary underline-offset-4 hover:underline",
19
- },
20
- size: {
21
- default: "h-10 px-4 py-2",
22
- sm: "h-9 rounded-md px-3",
23
- lg: "h-11 rounded-md px-8",
24
- icon: "h-10 w-10",
25
- },
26
- },
27
- defaultVariants: {
28
- variant: "default",
29
- size: "default",
30
- },
31
- }
32
- );
33
-
34
- export interface ButtonProps
35
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
36
- VariantProps<typeof buttonVariants> {
37
- asChild?: boolean;
38
- }
39
-
40
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
41
- ({ className, variant, size, asChild = false, ...props }, ref) => {
42
- return (
43
- <button
44
- className={cn(buttonVariants({ variant, size, className }))}
45
- ref={ref}
46
- {...props}
47
- />
48
- );
49
- }
50
- );
51
- Button.displayName = "Button";
52
-
53
- export { Button, buttonVariants };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/ui/card.tsx DELETED
@@ -1,85 +0,0 @@
1
- import * as React from "react";
2
- import { cn } from "../../lib/utils";
3
-
4
- const Card = React.forwardRef<
5
- HTMLDivElement,
6
- React.HTMLAttributes<HTMLDivElement>
7
- >(({ className, ...props }, ref) => (
8
- <div
9
- ref={ref}
10
- className={cn(
11
- "rounded-lg border bg-card text-card-foreground shadow-sm",
12
- className
13
- )}
14
- {...props}
15
- />
16
- ));
17
- Card.displayName = "Card";
18
-
19
- const CardHeader = React.forwardRef<
20
- HTMLDivElement,
21
- React.HTMLAttributes<HTMLDivElement>
22
- >(({ className, ...props }, ref) => (
23
- <div
24
- ref={ref}
25
- className={cn("flex flex-col space-y-1.5 p-6", className)}
26
- {...props}
27
- />
28
- ));
29
- CardHeader.displayName = "CardHeader";
30
-
31
- const CardTitle = React.forwardRef<
32
- HTMLParagraphElement,
33
- React.HTMLAttributes<HTMLHeadingElement>
34
- >(({ className, ...props }, ref) => (
35
- <h3
36
- ref={ref}
37
- className={cn(
38
- "text-2xl font-semibold leading-none tracking-tight",
39
- className
40
- )}
41
- {...props}
42
- />
43
- ));
44
- CardTitle.displayName = "CardTitle";
45
-
46
- const CardDescription = React.forwardRef<
47
- HTMLParagraphElement,
48
- React.HTMLAttributes<HTMLParagraphElement>
49
- >(({ className, ...props }, ref) => (
50
- <p
51
- ref={ref}
52
- className={cn("text-sm text-muted-foreground", className)}
53
- {...props}
54
- />
55
- ));
56
- CardDescription.displayName = "CardDescription";
57
-
58
- const CardContent = React.forwardRef<
59
- HTMLDivElement,
60
- React.HTMLAttributes<HTMLDivElement>
61
- >(({ className, ...props }, ref) => (
62
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
63
- ));
64
- CardContent.displayName = "CardContent";
65
-
66
- const CardFooter = React.forwardRef<
67
- HTMLDivElement,
68
- React.HTMLAttributes<HTMLDivElement>
69
- >(({ className, ...props }, ref) => (
70
- <div
71
- ref={ref}
72
- className={cn("flex items-center p-6 pt-0", className)}
73
- {...props}
74
- />
75
- ));
76
- CardFooter.displayName = "CardFooter";
77
-
78
- export {
79
- Card,
80
- CardHeader,
81
- CardFooter,
82
- CardTitle,
83
- CardDescription,
84
- CardContent,
85
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/ui/dialog.tsx DELETED
@@ -1,120 +0,0 @@
1
- import * as React from "react";
2
- import * as DialogPrimitive from "@radix-ui/react-dialog";
3
- import { X } from "lucide-react";
4
-
5
- import { cn } from "../../lib/utils";
6
-
7
- const Dialog = DialogPrimitive.Root;
8
-
9
- const DialogTrigger = DialogPrimitive.Trigger;
10
-
11
- const DialogPortal = DialogPrimitive.Portal;
12
-
13
- const DialogClose = DialogPrimitive.Close;
14
-
15
- const DialogOverlay = React.forwardRef<
16
- React.ElementRef<typeof DialogPrimitive.Overlay>,
17
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18
- >(({ className, ...props }, ref) => (
19
- <DialogPrimitive.Overlay
20
- ref={ref}
21
- className={cn(
22
- "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
23
- className
24
- )}
25
- {...props}
26
- />
27
- ));
28
- DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
29
-
30
- const DialogContent = React.forwardRef<
31
- React.ElementRef<typeof DialogPrimitive.Content>,
32
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
33
- >(({ className, children, ...props }, ref) => (
34
- <DialogPortal>
35
- <DialogOverlay />
36
- <DialogPrimitive.Content
37
- ref={ref}
38
- className={cn(
39
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
40
- className
41
- )}
42
- {...props}
43
- >
44
- {children}
45
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
46
- <X className="h-4 w-4" />
47
- <span className="sr-only">Close</span>
48
- </DialogPrimitive.Close>
49
- </DialogPrimitive.Content>
50
- </DialogPortal>
51
- ));
52
- DialogContent.displayName = DialogPrimitive.Content.displayName;
53
-
54
- const DialogHeader = ({
55
- className,
56
- ...props
57
- }: React.HTMLAttributes<HTMLDivElement>) => (
58
- <div
59
- className={cn(
60
- "flex flex-col space-y-1.5 text-center sm:text-left",
61
- className
62
- )}
63
- {...props}
64
- />
65
- );
66
- DialogHeader.displayName = "DialogHeader";
67
-
68
- const DialogFooter = ({
69
- className,
70
- ...props
71
- }: React.HTMLAttributes<HTMLDivElement>) => (
72
- <div
73
- className={cn(
74
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
75
- className
76
- )}
77
- {...props}
78
- />
79
- );
80
- DialogFooter.displayName = "DialogFooter";
81
-
82
- const DialogTitle = React.forwardRef<
83
- React.ElementRef<typeof DialogPrimitive.Title>,
84
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
85
- >(({ className, ...props }, ref) => (
86
- <DialogPrimitive.Title
87
- ref={ref}
88
- className={cn(
89
- "text-lg font-semibold leading-none tracking-tight",
90
- className
91
- )}
92
- {...props}
93
- />
94
- ));
95
- DialogTitle.displayName = DialogPrimitive.Title.displayName;
96
-
97
- const DialogDescription = React.forwardRef<
98
- React.ElementRef<typeof DialogPrimitive.Description>,
99
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
100
- >(({ className, ...props }, ref) => (
101
- <DialogPrimitive.Description
102
- ref={ref}
103
- className={cn("text-sm text-muted-foreground", className)}
104
- {...props}
105
- />
106
- ));
107
- DialogDescription.displayName = DialogPrimitive.Description.displayName;
108
-
109
- export {
110
- Dialog,
111
- DialogPortal,
112
- DialogOverlay,
113
- DialogClose,
114
- DialogContent,
115
- DialogDescription,
116
- DialogFooter,
117
- DialogHeader,
118
- DialogTitle,
119
- DialogTrigger,
120
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/components/ui/progress.tsx DELETED
@@ -1,26 +0,0 @@
1
- import * as React from "react";
2
- import * as ProgressPrimitive from "@radix-ui/react-progress";
3
-
4
- import { cn } from "../../lib/utils";
5
-
6
- const Progress = React.forwardRef<
7
- React.ElementRef<typeof ProgressPrimitive.Root>,
8
- React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
9
- >(({ className, value, ...props }, ref) => (
10
- <ProgressPrimitive.Root
11
- ref={ref}
12
- className={cn(
13
- "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
14
- className
15
- )}
16
- {...props}
17
- >
18
- <ProgressPrimitive.Indicator
19
- className="h-full w-full flex-1 bg-primary transition-all"
20
- style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
21
- />
22
- </ProgressPrimitive.Root>
23
- ));
24
- Progress.displayName = ProgressPrimitive.Root.displayName;
25
-
26
- export { Progress };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/index.css DELETED
@@ -1,12 +0,0 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- @layer base {
6
- * {
7
- @apply border-border;
8
- }
9
- body {
10
- @apply bg-background text-foreground;
11
- }
12
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/lib/unified-storage.ts DELETED
@@ -1,325 +0,0 @@
1
- // Unified storage system for robot data
2
- // Consolidates robot config, calibration data, and metadata under one key per device
3
-
4
- export interface UnifiedRobotData {
5
- device_info: {
6
- serialNumber: string;
7
- robotType: "so100_follower" | "so100_leader";
8
- robotId: string;
9
- usbMetadata?: any;
10
- lastUpdated: string;
11
- };
12
- calibration?: {
13
- // Motor calibration data (from lerobot_calibration_* keys)
14
- shoulder_pan?: {
15
- id: number;
16
- drive_mode: number;
17
- homing_offset: number;
18
- range_min: number;
19
- range_max: number;
20
- };
21
- shoulder_lift?: {
22
- id: number;
23
- drive_mode: number;
24
- homing_offset: number;
25
- range_min: number;
26
- range_max: number;
27
- };
28
- elbow_flex?: {
29
- id: number;
30
- drive_mode: number;
31
- homing_offset: number;
32
- range_min: number;
33
- range_max: number;
34
- };
35
- wrist_flex?: {
36
- id: number;
37
- drive_mode: number;
38
- homing_offset: number;
39
- range_min: number;
40
- range_max: number;
41
- };
42
- wrist_roll?: {
43
- id: number;
44
- drive_mode: number;
45
- homing_offset: number;
46
- range_min: number;
47
- range_max: number;
48
- };
49
- gripper?: {
50
- id: number;
51
- drive_mode: number;
52
- homing_offset: number;
53
- range_min: number;
54
- range_max: number;
55
- };
56
-
57
- // Calibration metadata (from lerobot-calibration-* keys)
58
- metadata: {
59
- timestamp: string;
60
- readCount: number;
61
- platform: string;
62
- api: string;
63
- device_type: string;
64
- device_id: string;
65
- calibrated_at: string;
66
- };
67
- };
68
- }
69
-
70
- /**
71
- * Get unified storage key for a robot by serial number
72
- */
73
- export function getUnifiedKey(serialNumber: string): string {
74
- return `lerobotjs-${serialNumber}`;
75
- }
76
-
77
- /**
78
- * Migrate data from old storage keys to unified format
79
- * Safely combines data from three sources:
80
- * 1. lerobot-robot-{serialNumber} - robot config
81
- * 2. lerobot-calibration-{serialNumber} - calibration metadata
82
- * 3. lerobot_calibration_{robotType}_{robotId} - actual calibration data
83
- */
84
- export function migrateToUnifiedStorage(
85
- serialNumber: string
86
- ): UnifiedRobotData | null {
87
- try {
88
- const unifiedKey = getUnifiedKey(serialNumber);
89
-
90
- // Check if already migrated
91
- const existing = localStorage.getItem(unifiedKey);
92
- if (existing) {
93
- console.log(`✅ Data already unified for ${serialNumber}`);
94
- return JSON.parse(existing);
95
- }
96
-
97
- console.log(`🔄 Migrating data for serial number: ${serialNumber}`);
98
-
99
- // 1. Get robot configuration
100
- const robotConfigKey = `lerobot-robot-${serialNumber}`;
101
- const robotConfigRaw = localStorage.getItem(robotConfigKey);
102
-
103
- if (!robotConfigRaw) {
104
- return null;
105
- }
106
-
107
- const robotConfig = JSON.parse(robotConfigRaw);
108
- console.log(`📋 Found robot config:`, robotConfig);
109
-
110
- // 2. Get calibration metadata
111
- const calibrationMetaKey = `lerobot-calibration-${serialNumber}`;
112
- const calibrationMetaRaw = localStorage.getItem(calibrationMetaKey);
113
- const calibrationMeta = calibrationMetaRaw
114
- ? JSON.parse(calibrationMetaRaw)
115
- : null;
116
- console.log(`📊 Found calibration metadata:`, calibrationMeta);
117
-
118
- // 3. Get actual calibration data (using robotType and robotId from config)
119
- const calibrationDataKey = `lerobot_calibration_${robotConfig.robotType}_${robotConfig.robotId}`;
120
- const calibrationDataRaw = localStorage.getItem(calibrationDataKey);
121
- const calibrationData = calibrationDataRaw
122
- ? JSON.parse(calibrationDataRaw)
123
- : null;
124
- console.log(`🔧 Found calibration data:`, calibrationData);
125
-
126
- // 4. Build unified structure
127
- const unifiedData: UnifiedRobotData = {
128
- device_info: {
129
- serialNumber: robotConfig.serialNumber || serialNumber,
130
- robotType: robotConfig.robotType,
131
- robotId: robotConfig.robotId,
132
- lastUpdated: robotConfig.lastUpdated || new Date().toISOString(),
133
- },
134
- };
135
-
136
- // Add calibration if available
137
- if (calibrationData && calibrationMeta) {
138
- const motors: any = {};
139
-
140
- // Copy motor data (excluding metadata fields)
141
- Object.keys(calibrationData).forEach((key) => {
142
- if (
143
- ![
144
- "device_type",
145
- "device_id",
146
- "calibrated_at",
147
- "platform",
148
- "api",
149
- ].includes(key)
150
- ) {
151
- motors[key] = calibrationData[key];
152
- }
153
- });
154
-
155
- unifiedData.calibration = {
156
- ...motors,
157
- metadata: {
158
- timestamp: calibrationMeta.timestamp || calibrationData.calibrated_at,
159
- readCount: calibrationMeta.readCount || 0,
160
- platform: calibrationData.platform || "web",
161
- api: calibrationData.api || "Web Serial API",
162
- device_type: calibrationData.device_type || robotConfig.robotType,
163
- device_id: calibrationData.device_id || robotConfig.robotId,
164
- calibrated_at:
165
- calibrationData.calibrated_at || calibrationMeta.timestamp,
166
- },
167
- };
168
- }
169
-
170
- // 5. Save unified data
171
- localStorage.setItem(unifiedKey, JSON.stringify(unifiedData));
172
- console.log(`✅ Successfully unified data for ${serialNumber}`);
173
- console.log(`📦 Unified data:`, unifiedData);
174
-
175
- // 6. Clean up old keys (optional - keep for now for safety)
176
- // localStorage.removeItem(robotConfigKey);
177
- // localStorage.removeItem(calibrationMetaKey);
178
- // localStorage.removeItem(calibrationDataKey);
179
-
180
- return unifiedData;
181
- } catch (error) {
182
- console.error(`❌ Failed to migrate data for ${serialNumber}:`, error);
183
- return null;
184
- }
185
- }
186
-
187
- /**
188
- * Get unified robot data
189
- */
190
- export function getUnifiedRobotData(
191
- serialNumber: string
192
- ): UnifiedRobotData | null {
193
- const unifiedKey = getUnifiedKey(serialNumber);
194
-
195
- // Try to get existing unified data
196
- const existing = localStorage.getItem(unifiedKey);
197
- if (existing) {
198
- try {
199
- return JSON.parse(existing);
200
- } catch (error) {
201
- console.warn(`Failed to parse unified data for ${serialNumber}:`, error);
202
- }
203
- }
204
-
205
- return null;
206
- }
207
-
208
- /**
209
- * Save robot configuration to unified storage
210
- */
211
- export function saveRobotConfig(
212
- serialNumber: string,
213
- robotType: "so100_follower" | "so100_leader",
214
- robotId: string,
215
- usbMetadata?: any
216
- ): void {
217
- const unifiedKey = getUnifiedKey(serialNumber);
218
- const existing =
219
- getUnifiedRobotData(serialNumber) || ({} as UnifiedRobotData);
220
-
221
- existing.device_info = {
222
- serialNumber,
223
- robotType,
224
- robotId,
225
- usbMetadata,
226
- lastUpdated: new Date().toISOString(),
227
- };
228
-
229
- localStorage.setItem(unifiedKey, JSON.stringify(existing));
230
- console.log(`💾 Saved robot config for ${serialNumber}`);
231
- }
232
-
233
- /**
234
- * Save calibration data to unified storage
235
- */
236
- export function saveCalibrationData(
237
- serialNumber: string,
238
- calibrationData: any,
239
- metadata: { timestamp: string; readCount: number }
240
- ): void {
241
- const unifiedKey = getUnifiedKey(serialNumber);
242
- const existing =
243
- getUnifiedRobotData(serialNumber) || ({} as UnifiedRobotData);
244
-
245
- // Ensure device_info exists
246
- if (!existing.device_info) {
247
- console.warn(
248
- `No device info found for ${serialNumber}, cannot save calibration`
249
- );
250
- return;
251
- }
252
-
253
- // Extract motor data (exclude metadata fields)
254
- const motors: any = {};
255
- Object.keys(calibrationData).forEach((key) => {
256
- if (
257
- ![
258
- "device_type",
259
- "device_id",
260
- "calibrated_at",
261
- "platform",
262
- "api",
263
- ].includes(key)
264
- ) {
265
- motors[key] = calibrationData[key];
266
- }
267
- });
268
-
269
- existing.calibration = {
270
- ...motors,
271
- metadata: {
272
- timestamp: metadata.timestamp,
273
- readCount: metadata.readCount,
274
- platform: calibrationData.platform || "web",
275
- api: calibrationData.api || "Web Serial API",
276
- device_type:
277
- calibrationData.device_type || existing.device_info.robotType,
278
- device_id: calibrationData.device_id || existing.device_info.robotId,
279
- calibrated_at: calibrationData.calibrated_at || metadata.timestamp,
280
- },
281
- };
282
-
283
- localStorage.setItem(unifiedKey, JSON.stringify(existing));
284
- console.log(`🔧 Saved calibration data for ${serialNumber}`);
285
- }
286
-
287
- /**
288
- * Check if robot is calibrated
289
- */
290
- export function isRobotCalibrated(serialNumber: string): boolean {
291
- const data = getUnifiedRobotData(serialNumber);
292
- return !!data?.calibration?.metadata?.timestamp;
293
- }
294
-
295
- /**
296
- * Get calibration status for dashboard
297
- */
298
- export function getCalibrationStatus(
299
- serialNumber: string
300
- ): { timestamp: string; readCount: number } | null {
301
- const data = getUnifiedRobotData(serialNumber);
302
- if (data?.calibration?.metadata) {
303
- return {
304
- timestamp: data.calibration.metadata.timestamp,
305
- readCount: data.calibration.metadata.readCount,
306
- };
307
- }
308
- return null;
309
- }
310
-
311
- /**
312
- * Get robot configuration
313
- */
314
- export function getRobotConfig(
315
- serialNumber: string
316
- ): { robotType: string; robotId: string } | null {
317
- const data = getUnifiedRobotData(serialNumber);
318
- if (data?.device_info) {
319
- return {
320
- robotType: data.device_info.robotType,
321
- robotId: data.device_info.robotId,
322
- };
323
- }
324
- return null;
325
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/lib/utils.ts DELETED
@@ -1,6 +0,0 @@
1
- import { type ClassValue, clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
 
 
 
 
 
 
 
src/demo/main.tsx DELETED
@@ -1,5 +0,0 @@
1
- import ReactDOM from "react-dom/client";
2
- import { App } from "./App";
3
- import "./index.css";
4
-
5
- ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
 
 
 
 
 
 
src/demo/pages/Home.tsx DELETED
@@ -1,99 +0,0 @@
1
- import { useState } from "react";
2
- import { Button } from "../components/ui/button";
3
- import { Alert, AlertDescription } from "../components/ui/alert";
4
- import { PortManager } from "../components/PortManager";
5
- import { CalibrationPanel } from "../components/CalibrationPanel";
6
- import { TeleoperationPanel } from "../components/TeleoperationPanel";
7
- import { isWebSerialSupported } from "@lerobot/web";
8
- import type { RobotConnection } from "@lerobot/web";
9
-
10
- interface HomeProps {
11
- connectedRobots: RobotConnection[];
12
- onConnectedRobotsChange: (robots: RobotConnection[]) => void;
13
- }
14
-
15
- export function Home({ connectedRobots, onConnectedRobotsChange }: HomeProps) {
16
- const [calibratingRobot, setCalibratingRobot] =
17
- useState<RobotConnection | null>(null);
18
- const [teleoperatingRobot, setTeleoperatingRobot] =
19
- useState<RobotConnection | null>(null);
20
- const isSupported = isWebSerialSupported();
21
-
22
- const handleCalibrate = (port: SerialPort) => {
23
- // Find the robot from connectedRobots
24
- const robot = connectedRobots.find((r) => r.port === port);
25
- if (robot) {
26
- setCalibratingRobot(robot);
27
- }
28
- };
29
-
30
- const handleTeleoperate = (robot: RobotConnection) => {
31
- setTeleoperatingRobot(robot);
32
- };
33
-
34
- const handleFinishCalibration = () => {
35
- setCalibratingRobot(null);
36
- };
37
-
38
- const handleFinishTeleoperation = () => {
39
- setTeleoperatingRobot(null);
40
- };
41
-
42
- return (
43
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
44
- <div className="container mx-auto px-6 py-12">
45
- {/* Header */}
46
- <div className="text-center mb-12">
47
- <h1 className="text-4xl font-bold text-gray-900 mb-4">
48
- 🤖 LeRobot.js
49
- </h1>
50
- <p className="text-xl text-gray-600 mb-8">
51
- Robotics for the web and node
52
- </p>
53
-
54
- {!isSupported && (
55
- <Alert variant="destructive" className="max-w-2xl mx-auto mb-8">
56
- <AlertDescription>
57
- Web Serial API is not supported in this browser. Please use
58
- Chrome, Edge, or another Chromium-based browser to use this
59
- demo.
60
- </AlertDescription>
61
- </Alert>
62
- )}
63
- </div>
64
-
65
- {/* Main Content */}
66
- {calibratingRobot ? (
67
- <div className="max-w-6xl mx-auto">
68
- <div className="mb-4">
69
- <Button
70
- variant="outline"
71
- onClick={() => setCalibratingRobot(null)}
72
- >
73
- ← Back to Dashboard
74
- </Button>
75
- </div>
76
- <CalibrationPanel
77
- robot={calibratingRobot}
78
- onFinish={handleFinishCalibration}
79
- />
80
- </div>
81
- ) : teleoperatingRobot ? (
82
- <TeleoperationPanel
83
- robot={teleoperatingRobot}
84
- onClose={handleFinishTeleoperation}
85
- />
86
- ) : (
87
- <div className="max-w-6xl mx-auto">
88
- <PortManager
89
- onCalibrate={handleCalibrate}
90
- onTeleoperate={handleTeleoperate}
91
- connectedRobots={connectedRobots}
92
- onConnectedRobotsChange={onConnectedRobotsChange}
93
- />
94
- </div>
95
- )}
96
- </div>
97
- </div>
98
- );
99
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/demo/types.ts DELETED
@@ -1,2 +0,0 @@
1
- // Demo uses the standard library RobotConnection interface directly
2
- // No need for separate types - just import RobotConnection where needed
 
 
 
tailwind.config.js CHANGED
@@ -1,6 +1,9 @@
1
  /** @type {import('tailwindcss').Config} */
2
  export default {
3
- content: ["./index.html", "./src/demo/**/*.{js,ts,jsx,tsx}"],
 
 
 
4
  theme: {
5
  extend: {
6
  borderRadius: {
 
1
  /** @type {import('tailwindcss').Config} */
2
  export default {
3
+ content: [
4
+ "./index.html",
5
+ "./examples/robot-control-web/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
  theme: {
8
  extend: {
9
  borderRadius: {
tsconfig.json CHANGED
@@ -24,5 +24,5 @@
24
  "noFallthroughCasesInSwitch": true,
25
  "noUncheckedSideEffectImports": true
26
  },
27
- "include": ["src"]
28
  }
 
24
  "noFallthroughCasesInSwitch": true,
25
  "noUncheckedSideEffectImports": true
26
  },
27
+ "include": ["src", "examples/robot-control-web"]
28
  }