Spaces:
Running
Running
refactor: restructure demo to examples/robot-control-web
Browse files- .github/CONTRIBUTING.md +1 -1
- examples/{demo → robot-control-web}/App.tsx +0 -0
- examples/{demo → robot-control-web}/components/CalibrationModal.tsx +0 -0
- examples/{demo → robot-control-web}/components/CalibrationPanel.tsx +0 -0
- examples/{demo → robot-control-web}/components/ErrorBoundary.tsx +0 -0
- examples/{demo → robot-control-web}/components/PortManager.tsx +0 -0
- examples/{demo → robot-control-web}/components/TeleoperationPanel.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/alert.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/badge.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/button.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/card.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/dialog.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/progress.tsx +0 -0
- examples/{demo → robot-control-web}/index.css +0 -0
- examples/{demo → robot-control-web}/lib/unified-storage.ts +0 -0
- examples/{demo → robot-control-web}/lib/utils.ts +0 -0
- examples/{demo → robot-control-web}/main.tsx +0 -0
- examples/{demo → robot-control-web}/pages/Home.tsx +0 -0
- index.html +1 -1
- packages/web/src/index.ts +2 -0
- pnpm-lock.yaml +12 -0
- src/demo/App.tsx +0 -19
- src/demo/components/CalibrationModal.tsx +0 -49
- src/demo/components/CalibrationPanel.tsx +0 -419
- src/demo/components/ErrorBoundary.tsx +0 -65
- src/demo/components/PortManager.tsx +0 -1251
- src/demo/components/TeleoperationPanel.tsx +0 -530
- src/demo/components/ui/alert.tsx +0 -58
- src/demo/components/ui/badge.tsx +0 -35
- src/demo/components/ui/button.tsx +0 -53
- src/demo/components/ui/card.tsx +0 -85
- src/demo/components/ui/dialog.tsx +0 -120
- src/demo/components/ui/progress.tsx +0 -26
- src/demo/index.css +0 -12
- src/demo/lib/unified-storage.ts +0 -325
- src/demo/lib/utils.ts +0 -6
- src/demo/main.tsx +0 -5
- src/demo/pages/Home.tsx +0 -99
- src/demo/types.ts +0 -2
- tailwind.config.js +4 -1
- 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 |
-
- **`
|
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="/
|
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 |
version: 1.1.14(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
@@ -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 |
version: 1.1.14(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
|
|
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 |
+
'@lerobot/[email protected]([email protected])':
|
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: [
|
|
|
|
|
|
|
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 |
}
|