Spaces:
Running
Running
chore: remove unused files
Browse files- examples/robot-control-web/App.tsx +0 -19
- examples/robot-control-web/components/CalibrationModal.tsx +0 -49
- examples/robot-control-web/components/CalibrationPanel.tsx +0 -408
- examples/robot-control-web/components/ErrorBoundary.tsx +0 -65
- examples/robot-control-web/components/PortManager.tsx +0 -485
- examples/robot-control-web/components/TeleoperationPanel.tsx +0 -587
- examples/robot-control-web/components/ui/alert.tsx +0 -58
- examples/robot-control-web/components/ui/badge.tsx +0 -35
- examples/robot-control-web/components/ui/button.tsx +0 -53
- examples/robot-control-web/components/ui/card.tsx +0 -85
- examples/robot-control-web/components/ui/dialog.tsx +0 -120
- examples/robot-control-web/components/ui/progress.tsx +0 -26
- examples/robot-control-web/index.css +0 -12
- examples/robot-control-web/lib/unified-storage.ts +0 -325
- examples/robot-control-web/lib/utils.ts +0 -6
- examples/robot-control-web/main.tsx +0 -5
- examples/robot-control-web/pages/Home.tsx +0 -99
- index.html +0 -81
- postcss.config.mjs +0 -6
- src/web_interface.css +0 -238
- tailwind.config.js +0 -59
- tsconfig.json +0 -28
- vite.config.ts +0 -83
examples/robot-control-web/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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/components/CalibrationPanel.tsx
DELETED
@@ -1,408 +0,0 @@
|
|
1 |
-
import { useState, useCallback, useMemo } from "react";
|
2 |
-
import { Button } from "./ui/button.js";
|
3 |
-
import {
|
4 |
-
Card,
|
5 |
-
CardContent,
|
6 |
-
CardDescription,
|
7 |
-
CardHeader,
|
8 |
-
CardTitle,
|
9 |
-
} from "./ui/card.js";
|
10 |
-
import { Badge } from "./ui/badge.js";
|
11 |
-
import {
|
12 |
-
calibrate,
|
13 |
-
releaseMotors,
|
14 |
-
type WebCalibrationResults,
|
15 |
-
type LiveCalibrationData,
|
16 |
-
type CalibrationProcess,
|
17 |
-
} from "@lerobot/web";
|
18 |
-
import { CalibrationModal } from "./CalibrationModal.js";
|
19 |
-
import type { RobotConnection } from "@lerobot/web";
|
20 |
-
|
21 |
-
interface CalibrationPanelProps {
|
22 |
-
robot: RobotConnection;
|
23 |
-
onFinish: () => void;
|
24 |
-
}
|
25 |
-
|
26 |
-
export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
|
27 |
-
// Simple state management
|
28 |
-
const [isCalibrating, setIsCalibrating] = useState(false);
|
29 |
-
const [calibrationResult, setCalibrationResult] =
|
30 |
-
useState<WebCalibrationResults | null>(null);
|
31 |
-
const [status, setStatus] = useState<string>("Ready to calibrate");
|
32 |
-
const [modalOpen, setModalOpen] = useState(false);
|
33 |
-
const [calibrationProcess, setCalibrationProcess] =
|
34 |
-
useState<CalibrationProcess | null>(null);
|
35 |
-
const [motorData, setMotorData] = useState<LiveCalibrationData>({});
|
36 |
-
const [isPreparing, setIsPreparing] = useState(false);
|
37 |
-
|
38 |
-
// Motor names for display
|
39 |
-
const motorNames = useMemo(
|
40 |
-
() => [
|
41 |
-
"shoulder_pan",
|
42 |
-
"shoulder_lift",
|
43 |
-
"elbow_flex",
|
44 |
-
"wrist_flex",
|
45 |
-
"wrist_roll",
|
46 |
-
"gripper",
|
47 |
-
],
|
48 |
-
[]
|
49 |
-
);
|
50 |
-
|
51 |
-
// Initialize motor data
|
52 |
-
const initializeMotorData = useCallback(() => {
|
53 |
-
const initialData: LiveCalibrationData = {};
|
54 |
-
motorNames.forEach((name) => {
|
55 |
-
initialData[name] = {
|
56 |
-
current: 2047,
|
57 |
-
min: 2047,
|
58 |
-
max: 2047,
|
59 |
-
range: 0,
|
60 |
-
};
|
61 |
-
});
|
62 |
-
setMotorData(initialData);
|
63 |
-
}, [motorNames]);
|
64 |
-
|
65 |
-
// Release motor torque
|
66 |
-
const releaseMotorTorque = useCallback(async () => {
|
67 |
-
try {
|
68 |
-
setIsPreparing(true);
|
69 |
-
setStatus("🔓 Releasing motor torque - joints can now be moved freely");
|
70 |
-
|
71 |
-
await releaseMotors(robot);
|
72 |
-
|
73 |
-
setStatus("✅ Joints are now free to move - set your homing position");
|
74 |
-
} catch (error) {
|
75 |
-
console.warn("Failed to release motor torque:", error);
|
76 |
-
setStatus("⚠️ Could not release motor torque - try moving joints gently");
|
77 |
-
} finally {
|
78 |
-
setIsPreparing(false);
|
79 |
-
}
|
80 |
-
}, [robot]);
|
81 |
-
|
82 |
-
// Start calibration using new API
|
83 |
-
const handleContinueCalibration = useCallback(async () => {
|
84 |
-
setModalOpen(false);
|
85 |
-
|
86 |
-
if (!robot.port || !robot.robotType) {
|
87 |
-
return;
|
88 |
-
}
|
89 |
-
|
90 |
-
try {
|
91 |
-
setStatus("🤖 Starting calibration process...");
|
92 |
-
setIsCalibrating(true);
|
93 |
-
initializeMotorData();
|
94 |
-
|
95 |
-
// Use the unified config API for calibration
|
96 |
-
const process = await calibrate({
|
97 |
-
robot,
|
98 |
-
onLiveUpdate: (data) => {
|
99 |
-
setMotorData(data);
|
100 |
-
setStatus(
|
101 |
-
"📏 Recording joint ranges - move all joints through their full range"
|
102 |
-
);
|
103 |
-
},
|
104 |
-
onProgress: (message) => {
|
105 |
-
setStatus(message);
|
106 |
-
},
|
107 |
-
});
|
108 |
-
|
109 |
-
setCalibrationProcess(process);
|
110 |
-
|
111 |
-
// Add Enter key listener for stopping (matching Node.js UX)
|
112 |
-
const handleKeyPress = (event: KeyboardEvent) => {
|
113 |
-
if (event.key === "Enter") {
|
114 |
-
process.stop();
|
115 |
-
}
|
116 |
-
};
|
117 |
-
document.addEventListener("keydown", handleKeyPress);
|
118 |
-
|
119 |
-
try {
|
120 |
-
// Wait for calibration to complete
|
121 |
-
const result = await process.result;
|
122 |
-
setCalibrationResult(result);
|
123 |
-
|
124 |
-
// App-level concern: Save results to storage
|
125 |
-
const serialNumber =
|
126 |
-
robot.serialNumber || robot.usbMetadata?.serialNumber || "unknown";
|
127 |
-
await saveCalibrationResults(
|
128 |
-
result,
|
129 |
-
robot.robotType,
|
130 |
-
robot.robotId || `${robot.robotType}_1`,
|
131 |
-
serialNumber
|
132 |
-
);
|
133 |
-
|
134 |
-
setStatus(
|
135 |
-
"✅ Calibration completed successfully! Configuration saved."
|
136 |
-
);
|
137 |
-
} finally {
|
138 |
-
document.removeEventListener("keydown", handleKeyPress);
|
139 |
-
setCalibrationProcess(null);
|
140 |
-
setIsCalibrating(false);
|
141 |
-
}
|
142 |
-
} catch (error) {
|
143 |
-
console.error("❌ Calibration failed:", error);
|
144 |
-
setStatus(
|
145 |
-
`❌ Calibration failed: ${
|
146 |
-
error instanceof Error ? error.message : error
|
147 |
-
}`
|
148 |
-
);
|
149 |
-
setIsCalibrating(false);
|
150 |
-
setCalibrationProcess(null);
|
151 |
-
}
|
152 |
-
}, [robot, initializeMotorData]);
|
153 |
-
|
154 |
-
// Stop calibration recording
|
155 |
-
const handleStopRecording = useCallback(() => {
|
156 |
-
if (calibrationProcess) {
|
157 |
-
calibrationProcess.stop();
|
158 |
-
}
|
159 |
-
}, [calibrationProcess]);
|
160 |
-
|
161 |
-
// App-level concern: Save calibration results
|
162 |
-
const saveCalibrationResults = async (
|
163 |
-
results: WebCalibrationResults,
|
164 |
-
robotType: string,
|
165 |
-
robotId: string,
|
166 |
-
serialNumber: string
|
167 |
-
) => {
|
168 |
-
try {
|
169 |
-
// Save to unified storage (app-level functionality)
|
170 |
-
const { saveCalibrationData } = await import("../lib/unified-storage.js");
|
171 |
-
|
172 |
-
const fullCalibrationData = {
|
173 |
-
...results,
|
174 |
-
device_type: robotType,
|
175 |
-
device_id: robotId,
|
176 |
-
calibrated_at: new Date().toISOString(),
|
177 |
-
platform: "web",
|
178 |
-
api: "Web Serial API",
|
179 |
-
};
|
180 |
-
|
181 |
-
const metadata = {
|
182 |
-
timestamp: new Date().toISOString(),
|
183 |
-
readCount: Object.keys(motorData).length > 0 ? 100 : 0, // Estimate
|
184 |
-
};
|
185 |
-
|
186 |
-
saveCalibrationData(serialNumber, fullCalibrationData, metadata);
|
187 |
-
} catch (error) {
|
188 |
-
console.warn("Failed to save calibration results:", error);
|
189 |
-
}
|
190 |
-
};
|
191 |
-
|
192 |
-
// App-level concern: JSON export functionality
|
193 |
-
const downloadConfigJSON = useCallback(() => {
|
194 |
-
if (!calibrationResult) return;
|
195 |
-
|
196 |
-
const jsonString = JSON.stringify(calibrationResult, null, 2);
|
197 |
-
const blob = new Blob([jsonString], { type: "application/json" });
|
198 |
-
const url = URL.createObjectURL(blob);
|
199 |
-
|
200 |
-
const link = document.createElement("a");
|
201 |
-
link.href = url;
|
202 |
-
link.download = `${robot.robotId || robot.robotType}_calibration.json`;
|
203 |
-
document.body.appendChild(link);
|
204 |
-
link.click();
|
205 |
-
document.body.removeChild(link);
|
206 |
-
URL.revokeObjectURL(url);
|
207 |
-
}, [calibrationResult, robot.robotId, robot.robotType]);
|
208 |
-
|
209 |
-
return (
|
210 |
-
<div className="space-y-4">
|
211 |
-
{/* Calibration Status Card */}
|
212 |
-
<Card>
|
213 |
-
<CardHeader>
|
214 |
-
<div className="flex items-center justify-between">
|
215 |
-
<div>
|
216 |
-
<CardTitle className="text-lg">
|
217 |
-
🛠️ Calibrating: {robot.robotId}
|
218 |
-
</CardTitle>
|
219 |
-
<CardDescription>
|
220 |
-
{robot.robotType?.replace("_", " ")} • {robot.name}
|
221 |
-
</CardDescription>
|
222 |
-
</div>
|
223 |
-
<Badge
|
224 |
-
variant={
|
225 |
-
isCalibrating
|
226 |
-
? "default"
|
227 |
-
: calibrationResult
|
228 |
-
? "default"
|
229 |
-
: "outline"
|
230 |
-
}
|
231 |
-
>
|
232 |
-
{isCalibrating
|
233 |
-
? "Recording"
|
234 |
-
: calibrationResult
|
235 |
-
? "Complete"
|
236 |
-
: "Ready"}
|
237 |
-
</Badge>
|
238 |
-
</div>
|
239 |
-
</CardHeader>
|
240 |
-
<CardContent>
|
241 |
-
<div className="space-y-4">
|
242 |
-
<div className="p-3 bg-blue-50 rounded-lg">
|
243 |
-
<p className="text-sm font-medium text-blue-900">Status:</p>
|
244 |
-
<p className="text-sm text-blue-800">{status}</p>
|
245 |
-
{isCalibrating && (
|
246 |
-
<p className="text-xs text-blue-600 mt-1">
|
247 |
-
Move joints through full range | Press "Finish Recording" or
|
248 |
-
Enter key when done
|
249 |
-
</p>
|
250 |
-
)}
|
251 |
-
</div>
|
252 |
-
|
253 |
-
<div className="flex gap-2">
|
254 |
-
{!isCalibrating && !calibrationResult && (
|
255 |
-
<Button
|
256 |
-
onClick={async () => {
|
257 |
-
// ✅ Release motor torque FIRST - so user can move joints immediately
|
258 |
-
await releaseMotorTorque();
|
259 |
-
// THEN open modal - user can now follow instructions right away
|
260 |
-
setModalOpen(true);
|
261 |
-
}}
|
262 |
-
disabled={isPreparing}
|
263 |
-
>
|
264 |
-
{isPreparing ? "Preparing..." : "Start Calibration"}
|
265 |
-
</Button>
|
266 |
-
)}
|
267 |
-
|
268 |
-
{isCalibrating && calibrationProcess && (
|
269 |
-
<Button onClick={handleStopRecording} variant="default">
|
270 |
-
Finish Recording
|
271 |
-
</Button>
|
272 |
-
)}
|
273 |
-
|
274 |
-
{calibrationResult && (
|
275 |
-
<>
|
276 |
-
<Button onClick={downloadConfigJSON} variant="outline">
|
277 |
-
Download Config JSON
|
278 |
-
</Button>
|
279 |
-
<Button onClick={onFinish}>Done</Button>
|
280 |
-
</>
|
281 |
-
)}
|
282 |
-
</div>
|
283 |
-
</div>
|
284 |
-
</CardContent>
|
285 |
-
</Card>
|
286 |
-
|
287 |
-
{/* Configuration JSON Display */}
|
288 |
-
{calibrationResult && (
|
289 |
-
<Card>
|
290 |
-
<CardHeader>
|
291 |
-
<CardTitle className="text-lg">
|
292 |
-
🎯 Calibration Configuration
|
293 |
-
</CardTitle>
|
294 |
-
<CardDescription>
|
295 |
-
Copy this JSON or download it for your robot setup
|
296 |
-
</CardDescription>
|
297 |
-
</CardHeader>
|
298 |
-
<CardContent>
|
299 |
-
<div className="space-y-3">
|
300 |
-
<pre className="bg-gray-100 p-4 rounded-lg text-sm overflow-x-auto border">
|
301 |
-
<code>{JSON.stringify(calibrationResult, null, 2)}</code>
|
302 |
-
</pre>
|
303 |
-
<div className="flex gap-2">
|
304 |
-
<Button onClick={downloadConfigJSON} variant="outline">
|
305 |
-
📄 Download JSON File
|
306 |
-
</Button>
|
307 |
-
<Button
|
308 |
-
onClick={() => {
|
309 |
-
navigator.clipboard.writeText(
|
310 |
-
JSON.stringify(calibrationResult, null, 2)
|
311 |
-
);
|
312 |
-
}}
|
313 |
-
variant="outline"
|
314 |
-
>
|
315 |
-
📋 Copy to Clipboard
|
316 |
-
</Button>
|
317 |
-
</div>
|
318 |
-
</div>
|
319 |
-
</CardContent>
|
320 |
-
</Card>
|
321 |
-
)}
|
322 |
-
|
323 |
-
{/* Live Position Recording Table */}
|
324 |
-
<Card>
|
325 |
-
<CardHeader>
|
326 |
-
<CardTitle className="text-lg">Live Position Recording</CardTitle>
|
327 |
-
<CardDescription>
|
328 |
-
Real-time motor position feedback during calibration
|
329 |
-
</CardDescription>
|
330 |
-
</CardHeader>
|
331 |
-
<CardContent>
|
332 |
-
<div className="overflow-hidden rounded-lg border">
|
333 |
-
<table className="w-full font-mono text-sm">
|
334 |
-
<thead className="bg-gray-50">
|
335 |
-
<tr>
|
336 |
-
<th className="px-4 py-2 text-left font-medium text-gray-900">
|
337 |
-
Motor Name
|
338 |
-
</th>
|
339 |
-
<th className="px-4 py-2 text-right font-medium text-gray-900">
|
340 |
-
Current
|
341 |
-
</th>
|
342 |
-
<th className="px-4 py-2 text-right font-medium text-gray-900">
|
343 |
-
Min
|
344 |
-
</th>
|
345 |
-
<th className="px-4 py-2 text-right font-medium text-gray-900">
|
346 |
-
Max
|
347 |
-
</th>
|
348 |
-
<th className="px-4 py-2 text-right font-medium text-gray-900">
|
349 |
-
Range
|
350 |
-
</th>
|
351 |
-
</tr>
|
352 |
-
</thead>
|
353 |
-
<tbody className="divide-y divide-gray-200">
|
354 |
-
{motorNames.map((motorName) => {
|
355 |
-
const motor = motorData[motorName] || {
|
356 |
-
current: 2047,
|
357 |
-
min: 2047,
|
358 |
-
max: 2047,
|
359 |
-
range: 0,
|
360 |
-
};
|
361 |
-
|
362 |
-
return (
|
363 |
-
<tr key={motorName} className="hover:bg-gray-50">
|
364 |
-
<td className="px-4 py-2 font-medium flex items-center gap-2">
|
365 |
-
{motorName}
|
366 |
-
{motor.range > 100 && (
|
367 |
-
<span className="text-green-600 text-xs">✓</span>
|
368 |
-
)}
|
369 |
-
</td>
|
370 |
-
<td className="px-4 py-2 text-right">{motor.current}</td>
|
371 |
-
<td className="px-4 py-2 text-right">{motor.min}</td>
|
372 |
-
<td className="px-4 py-2 text-right">{motor.max}</td>
|
373 |
-
<td className="px-4 py-2 text-right font-medium">
|
374 |
-
<span
|
375 |
-
className={
|
376 |
-
motor.range > 100
|
377 |
-
? "text-green-600"
|
378 |
-
: "text-gray-500"
|
379 |
-
}
|
380 |
-
>
|
381 |
-
{motor.range}
|
382 |
-
</span>
|
383 |
-
</td>
|
384 |
-
</tr>
|
385 |
-
);
|
386 |
-
})}
|
387 |
-
</tbody>
|
388 |
-
</table>
|
389 |
-
</div>
|
390 |
-
|
391 |
-
{isCalibrating && (
|
392 |
-
<div className="mt-3 text-center text-sm text-gray-600">
|
393 |
-
Move joints through their full range of motion...
|
394 |
-
</div>
|
395 |
-
)}
|
396 |
-
</CardContent>
|
397 |
-
</Card>
|
398 |
-
|
399 |
-
{/* Calibration Modal */}
|
400 |
-
<CalibrationModal
|
401 |
-
open={modalOpen}
|
402 |
-
onOpenChange={setModalOpen}
|
403 |
-
deviceType={robot.robotType || "robot"}
|
404 |
-
onContinue={handleContinueCalibration}
|
405 |
-
/>
|
406 |
-
</div>
|
407 |
-
);
|
408 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/components/PortManager.tsx
DELETED
@@ -1,485 +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 { findPort, isWebSerialSupported } from "@lerobot/web";
|
13 |
-
import type { RobotConnection } from "@lerobot/web";
|
14 |
-
|
15 |
-
interface PortManagerProps {
|
16 |
-
connectedRobots: RobotConnection[];
|
17 |
-
onConnectedRobotsChange: (robots: RobotConnection[]) => void;
|
18 |
-
onCalibrate?: (port: any) => void; // Let library handle port type
|
19 |
-
onTeleoperate?: (robot: RobotConnection) => void;
|
20 |
-
}
|
21 |
-
|
22 |
-
export function PortManager({
|
23 |
-
connectedRobots,
|
24 |
-
onConnectedRobotsChange,
|
25 |
-
onCalibrate,
|
26 |
-
onTeleoperate,
|
27 |
-
}: PortManagerProps) {
|
28 |
-
const [isFindingPorts, setIsFindingPorts] = useState(false);
|
29 |
-
const [findPortsLog, setFindPortsLog] = useState<string[]>([]);
|
30 |
-
const [error, setError] = useState<string | null>(null);
|
31 |
-
|
32 |
-
// Load saved robots on mount by calling findPort with saved data
|
33 |
-
useEffect(() => {
|
34 |
-
loadSavedRobots();
|
35 |
-
}, []);
|
36 |
-
|
37 |
-
const loadSavedRobots = async () => {
|
38 |
-
try {
|
39 |
-
console.log("🔄 Loading saved robots from localStorage...");
|
40 |
-
|
41 |
-
// Load saved robot configs for auto-connect mode
|
42 |
-
const robotConfigs: any[] = [];
|
43 |
-
const { getUnifiedRobotData } = await import("../lib/unified-storage");
|
44 |
-
|
45 |
-
// Check localStorage for saved robot data
|
46 |
-
for (let i = 0; i < localStorage.length; i++) {
|
47 |
-
const key = localStorage.key(i);
|
48 |
-
if (key && key.startsWith("lerobotjs-")) {
|
49 |
-
const serialNumber = key.replace("lerobotjs-", "");
|
50 |
-
const robotData = getUnifiedRobotData(serialNumber);
|
51 |
-
|
52 |
-
if (robotData) {
|
53 |
-
console.log(
|
54 |
-
`✅ Found saved robot: ${robotData.device_info.robotId}`
|
55 |
-
);
|
56 |
-
|
57 |
-
// Create robot config for auto-connect mode
|
58 |
-
robotConfigs.push({
|
59 |
-
robotType: robotData.device_info.robotType,
|
60 |
-
robotId: robotData.device_info.robotId,
|
61 |
-
serialNumber: serialNumber,
|
62 |
-
});
|
63 |
-
}
|
64 |
-
}
|
65 |
-
}
|
66 |
-
|
67 |
-
if (robotConfigs.length > 0) {
|
68 |
-
console.log(
|
69 |
-
`🔄 Auto-connecting to ${robotConfigs.length} saved robots...`
|
70 |
-
);
|
71 |
-
|
72 |
-
// Use auto-connect mode - NO DIALOG will be shown!
|
73 |
-
const findPortProcess = await findPort({
|
74 |
-
robotConfigs,
|
75 |
-
onMessage: (message) => {
|
76 |
-
console.log(`Auto-connect: ${message}`);
|
77 |
-
},
|
78 |
-
});
|
79 |
-
|
80 |
-
const reconnectedRobots = await findPortProcess.result;
|
81 |
-
console.log(
|
82 |
-
`✅ Auto-connected to ${
|
83 |
-
reconnectedRobots.filter((r) => r.isConnected).length
|
84 |
-
}/${robotConfigs.length} saved robots`
|
85 |
-
);
|
86 |
-
|
87 |
-
onConnectedRobotsChange(reconnectedRobots);
|
88 |
-
} else {
|
89 |
-
console.log("No saved robots found in localStorage");
|
90 |
-
}
|
91 |
-
} catch (error) {
|
92 |
-
console.error("Failed to load saved robots:", error);
|
93 |
-
}
|
94 |
-
};
|
95 |
-
|
96 |
-
const handleFindPorts = async () => {
|
97 |
-
if (!isWebSerialSupported()) {
|
98 |
-
setError("Web Serial API is not supported in this browser");
|
99 |
-
return;
|
100 |
-
}
|
101 |
-
|
102 |
-
try {
|
103 |
-
setIsFindingPorts(true);
|
104 |
-
setFindPortsLog([]);
|
105 |
-
setError(null);
|
106 |
-
|
107 |
-
// Use clean library API - library handles everything!
|
108 |
-
const findPortProcess = await findPort({
|
109 |
-
onMessage: (message) => {
|
110 |
-
setFindPortsLog((prev) => [...prev, message]);
|
111 |
-
},
|
112 |
-
});
|
113 |
-
|
114 |
-
const robotConnections = await findPortProcess.result;
|
115 |
-
|
116 |
-
// Add new robots to the list (avoid duplicates)
|
117 |
-
const newRobots = robotConnections.filter(
|
118 |
-
(newRobot) =>
|
119 |
-
!connectedRobots.some(
|
120 |
-
(existing) => existing.serialNumber === newRobot.serialNumber
|
121 |
-
)
|
122 |
-
);
|
123 |
-
|
124 |
-
onConnectedRobotsChange([...connectedRobots, ...newRobots]);
|
125 |
-
setFindPortsLog((prev) => [
|
126 |
-
...prev,
|
127 |
-
`✅ Found ${newRobots.length} new robots`,
|
128 |
-
]);
|
129 |
-
} catch (error) {
|
130 |
-
if (
|
131 |
-
error instanceof Error &&
|
132 |
-
(error.message.includes("cancelled") ||
|
133 |
-
error.name === "NotAllowedError")
|
134 |
-
) {
|
135 |
-
console.log("Port discovery cancelled by user");
|
136 |
-
return;
|
137 |
-
}
|
138 |
-
setError(error instanceof Error ? error.message : "Failed to find ports");
|
139 |
-
} finally {
|
140 |
-
setIsFindingPorts(false);
|
141 |
-
}
|
142 |
-
};
|
143 |
-
|
144 |
-
const handleDisconnect = (index: number) => {
|
145 |
-
const updatedRobots = connectedRobots.filter((_, i) => i !== index);
|
146 |
-
onConnectedRobotsChange(updatedRobots);
|
147 |
-
};
|
148 |
-
|
149 |
-
const handleCalibrate = (robot: RobotConnection) => {
|
150 |
-
if (!robot.robotType || !robot.robotId) {
|
151 |
-
setError("Please configure robot type and ID first");
|
152 |
-
return;
|
153 |
-
}
|
154 |
-
if (onCalibrate) {
|
155 |
-
onCalibrate(robot.port);
|
156 |
-
}
|
157 |
-
};
|
158 |
-
|
159 |
-
const handleTeleoperate = (robot: RobotConnection) => {
|
160 |
-
if (!robot.robotType || !robot.robotId) {
|
161 |
-
setError("Please configure robot type and ID first");
|
162 |
-
return;
|
163 |
-
}
|
164 |
-
|
165 |
-
if (!robot.isConnected || !robot.port) {
|
166 |
-
setError(
|
167 |
-
"Robot is not connected. Please use 'Find & Connect Robots' first."
|
168 |
-
);
|
169 |
-
return;
|
170 |
-
}
|
171 |
-
|
172 |
-
// Robot is connected, proceed with teleoperation
|
173 |
-
if (onTeleoperate) {
|
174 |
-
onTeleoperate(robot);
|
175 |
-
}
|
176 |
-
};
|
177 |
-
|
178 |
-
return (
|
179 |
-
<Card>
|
180 |
-
<CardHeader>
|
181 |
-
<CardTitle>🔌 Robot Connection Manager</CardTitle>
|
182 |
-
<CardDescription>
|
183 |
-
Find and connect to your robot devices
|
184 |
-
</CardDescription>
|
185 |
-
</CardHeader>
|
186 |
-
<CardContent>
|
187 |
-
<div className="space-y-6">
|
188 |
-
{/* Error Display */}
|
189 |
-
{error && (
|
190 |
-
<Alert variant="destructive">
|
191 |
-
<AlertDescription>{error}</AlertDescription>
|
192 |
-
</Alert>
|
193 |
-
)}
|
194 |
-
|
195 |
-
{/* Find Ports Button */}
|
196 |
-
<Button
|
197 |
-
onClick={handleFindPorts}
|
198 |
-
disabled={isFindingPorts || !isWebSerialSupported()}
|
199 |
-
className="w-full"
|
200 |
-
>
|
201 |
-
{isFindingPorts ? "Finding Robots..." : "🔍 Find & Connect Robots"}
|
202 |
-
</Button>
|
203 |
-
|
204 |
-
{/* Find Ports Log */}
|
205 |
-
{findPortsLog.length > 0 && (
|
206 |
-
<div className="bg-gray-50 p-3 rounded-md text-sm space-y-1 max-h-32 overflow-y-auto">
|
207 |
-
{findPortsLog.map((log, index) => (
|
208 |
-
<div key={index} className="text-gray-700">
|
209 |
-
{log}
|
210 |
-
</div>
|
211 |
-
))}
|
212 |
-
</div>
|
213 |
-
)}
|
214 |
-
|
215 |
-
{/* Connected Robots */}
|
216 |
-
<div>
|
217 |
-
<h4 className="font-semibold mb-3">
|
218 |
-
Connected Robots ({connectedRobots.length})
|
219 |
-
</h4>
|
220 |
-
|
221 |
-
{connectedRobots.length === 0 ? (
|
222 |
-
<div className="text-center py-8 text-gray-500">
|
223 |
-
<div className="text-2xl mb-2">🤖</div>
|
224 |
-
<p>No robots found</p>
|
225 |
-
<p className="text-xs">
|
226 |
-
Click "Find & Connect Robots" to discover devices
|
227 |
-
</p>
|
228 |
-
</div>
|
229 |
-
) : (
|
230 |
-
<div className="space-y-4">
|
231 |
-
{connectedRobots.map((robot, index) => (
|
232 |
-
<RobotCard
|
233 |
-
key={robot.serialNumber || index}
|
234 |
-
robot={robot}
|
235 |
-
onDisconnect={() => handleDisconnect(index)}
|
236 |
-
onCalibrate={() => handleCalibrate(robot)}
|
237 |
-
onTeleoperate={() => handleTeleoperate(robot)}
|
238 |
-
/>
|
239 |
-
))}
|
240 |
-
</div>
|
241 |
-
)}
|
242 |
-
</div>
|
243 |
-
</div>
|
244 |
-
</CardContent>
|
245 |
-
</Card>
|
246 |
-
);
|
247 |
-
}
|
248 |
-
|
249 |
-
interface RobotCardProps {
|
250 |
-
robot: RobotConnection;
|
251 |
-
onDisconnect: () => void;
|
252 |
-
onCalibrate: () => void;
|
253 |
-
onTeleoperate: () => void;
|
254 |
-
}
|
255 |
-
|
256 |
-
function RobotCard({
|
257 |
-
robot,
|
258 |
-
onDisconnect,
|
259 |
-
onCalibrate,
|
260 |
-
onTeleoperate,
|
261 |
-
}: RobotCardProps) {
|
262 |
-
const [calibrationStatus, setCalibrationStatus] = useState<{
|
263 |
-
timestamp: string;
|
264 |
-
readCount: number;
|
265 |
-
} | null>(null);
|
266 |
-
const [isEditing, setIsEditing] = useState(false);
|
267 |
-
const [editRobotType, setEditRobotType] = useState<
|
268 |
-
"so100_follower" | "so100_leader"
|
269 |
-
>(robot.robotType || "so100_follower");
|
270 |
-
const [editRobotId, setEditRobotId] = useState(robot.robotId || "");
|
271 |
-
|
272 |
-
const isConfigured = robot.robotType && robot.robotId;
|
273 |
-
|
274 |
-
// Check calibration status using unified storage
|
275 |
-
useEffect(() => {
|
276 |
-
const checkCalibrationStatus = async () => {
|
277 |
-
if (!robot.serialNumber) return;
|
278 |
-
|
279 |
-
try {
|
280 |
-
const { getCalibrationStatus } = await import("../lib/unified-storage");
|
281 |
-
const status = getCalibrationStatus(robot.serialNumber);
|
282 |
-
setCalibrationStatus(status);
|
283 |
-
} catch (error) {
|
284 |
-
console.warn("Failed to check calibration status:", error);
|
285 |
-
}
|
286 |
-
};
|
287 |
-
|
288 |
-
checkCalibrationStatus();
|
289 |
-
}, [robot.serialNumber]);
|
290 |
-
|
291 |
-
const handleSaveConfig = async () => {
|
292 |
-
if (!editRobotId.trim() || !robot.serialNumber) return;
|
293 |
-
|
294 |
-
try {
|
295 |
-
const { saveRobotConfig } = await import("../lib/unified-storage");
|
296 |
-
saveRobotConfig(
|
297 |
-
robot.serialNumber,
|
298 |
-
editRobotType,
|
299 |
-
editRobotId.trim(),
|
300 |
-
robot.usbMetadata
|
301 |
-
);
|
302 |
-
|
303 |
-
// Update the robot object (this should trigger a re-render)
|
304 |
-
robot.robotType = editRobotType;
|
305 |
-
robot.robotId = editRobotId.trim();
|
306 |
-
|
307 |
-
setIsEditing(false);
|
308 |
-
console.log("✅ Robot configuration saved");
|
309 |
-
} catch (error) {
|
310 |
-
console.error("Failed to save robot configuration:", error);
|
311 |
-
}
|
312 |
-
};
|
313 |
-
|
314 |
-
const handleCancelEdit = () => {
|
315 |
-
setEditRobotType(robot.robotType || "so100_follower");
|
316 |
-
setEditRobotId(robot.robotId || "");
|
317 |
-
setIsEditing(false);
|
318 |
-
};
|
319 |
-
|
320 |
-
return (
|
321 |
-
<div className="border rounded-lg p-4 space-y-3">
|
322 |
-
{/* Header */}
|
323 |
-
<div className="flex items-center justify-between">
|
324 |
-
<div className="flex items-center space-x-2">
|
325 |
-
<div className="flex flex-col">
|
326 |
-
<span className="font-medium">
|
327 |
-
{robot.robotId || robot.name || "Unnamed Robot"}
|
328 |
-
</span>
|
329 |
-
<span className="text-xs text-gray-500">
|
330 |
-
{robot.robotType?.replace("_", " ") || "Not configured"}
|
331 |
-
</span>
|
332 |
-
{robot.serialNumber && (
|
333 |
-
<span className="text-xs text-gray-400 font-mono">
|
334 |
-
{robot.serialNumber.length > 20
|
335 |
-
? robot.serialNumber.substring(0, 20) + "..."
|
336 |
-
: robot.serialNumber}
|
337 |
-
</span>
|
338 |
-
)}
|
339 |
-
</div>
|
340 |
-
<div className="flex flex-col gap-1">
|
341 |
-
<Badge variant={robot.isConnected ? "default" : "outline"}>
|
342 |
-
{robot.isConnected ? "Connected" : "Available"}
|
343 |
-
</Badge>
|
344 |
-
{calibrationStatus && (
|
345 |
-
<Badge variant="default" className="bg-green-100 text-green-800">
|
346 |
-
✅ Calibrated
|
347 |
-
</Badge>
|
348 |
-
)}
|
349 |
-
</div>
|
350 |
-
</div>
|
351 |
-
<Button variant="destructive" size="sm" onClick={onDisconnect}>
|
352 |
-
Remove
|
353 |
-
</Button>
|
354 |
-
</div>
|
355 |
-
|
356 |
-
{/* Robot Configuration Display (when not editing) */}
|
357 |
-
{!isEditing && isConfigured && (
|
358 |
-
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
359 |
-
<div className="flex items-center space-x-3">
|
360 |
-
<div>
|
361 |
-
<div className="font-medium text-sm">{robot.robotId}</div>
|
362 |
-
<div className="text-xs text-gray-600">
|
363 |
-
{robot.robotType?.replace("_", " ")}
|
364 |
-
</div>
|
365 |
-
</div>
|
366 |
-
</div>
|
367 |
-
<Button
|
368 |
-
variant="outline"
|
369 |
-
size="sm"
|
370 |
-
onClick={() => setIsEditing(true)}
|
371 |
-
>
|
372 |
-
Edit
|
373 |
-
</Button>
|
374 |
-
</div>
|
375 |
-
)}
|
376 |
-
|
377 |
-
{/* Configuration Prompt for unconfigured robots */}
|
378 |
-
{!isEditing && !isConfigured && (
|
379 |
-
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
|
380 |
-
<div className="text-sm text-blue-800">
|
381 |
-
Robot needs configuration before use
|
382 |
-
</div>
|
383 |
-
<Button
|
384 |
-
variant="outline"
|
385 |
-
size="sm"
|
386 |
-
onClick={() => setIsEditing(true)}
|
387 |
-
>
|
388 |
-
Configure
|
389 |
-
</Button>
|
390 |
-
</div>
|
391 |
-
)}
|
392 |
-
|
393 |
-
{/* Robot Configuration Form (when editing) */}
|
394 |
-
{isEditing && (
|
395 |
-
<div className="space-y-3 p-3 bg-gray-50 rounded-lg">
|
396 |
-
<div className="grid grid-cols-2 gap-3">
|
397 |
-
<div>
|
398 |
-
<label className="text-sm font-medium block mb-1">
|
399 |
-
Robot Type
|
400 |
-
</label>
|
401 |
-
<select
|
402 |
-
value={editRobotType}
|
403 |
-
onChange={(e) =>
|
404 |
-
setEditRobotType(
|
405 |
-
e.target.value as "so100_follower" | "so100_leader"
|
406 |
-
)
|
407 |
-
}
|
408 |
-
className="w-full px-2 py-1 border rounded text-sm"
|
409 |
-
>
|
410 |
-
<option value="so100_follower">SO-100 Follower</option>
|
411 |
-
<option value="so100_leader">SO-100 Leader</option>
|
412 |
-
</select>
|
413 |
-
</div>
|
414 |
-
<div>
|
415 |
-
<label className="text-sm font-medium block mb-1">Robot ID</label>
|
416 |
-
<input
|
417 |
-
type="text"
|
418 |
-
value={editRobotId}
|
419 |
-
onChange={(e) => setEditRobotId(e.target.value)}
|
420 |
-
placeholder="e.g., my_robot"
|
421 |
-
className="w-full px-2 py-1 border rounded text-sm"
|
422 |
-
/>
|
423 |
-
</div>
|
424 |
-
</div>
|
425 |
-
|
426 |
-
<div className="flex gap-2">
|
427 |
-
<Button
|
428 |
-
size="sm"
|
429 |
-
onClick={handleSaveConfig}
|
430 |
-
disabled={!editRobotId.trim()}
|
431 |
-
>
|
432 |
-
Save
|
433 |
-
</Button>
|
434 |
-
<Button size="sm" variant="outline" onClick={handleCancelEdit}>
|
435 |
-
Cancel
|
436 |
-
</Button>
|
437 |
-
</div>
|
438 |
-
</div>
|
439 |
-
)}
|
440 |
-
|
441 |
-
{/* Calibration Status */}
|
442 |
-
{isConfigured && !isEditing && (
|
443 |
-
<div className="text-sm text-gray-600">
|
444 |
-
{calibrationStatus ? (
|
445 |
-
<span>
|
446 |
-
Last calibrated:{" "}
|
447 |
-
{new Date(calibrationStatus.timestamp).toLocaleDateString()}
|
448 |
-
<span className="text-xs ml-1">
|
449 |
-
({calibrationStatus.readCount} readings)
|
450 |
-
</span>
|
451 |
-
</span>
|
452 |
-
) : (
|
453 |
-
<span>Not calibrated yet</span>
|
454 |
-
)}
|
455 |
-
</div>
|
456 |
-
)}
|
457 |
-
|
458 |
-
{/* Actions */}
|
459 |
-
{isConfigured && !isEditing && (
|
460 |
-
<div className="flex gap-2">
|
461 |
-
<Button
|
462 |
-
size="sm"
|
463 |
-
variant={calibrationStatus ? "outline" : "default"}
|
464 |
-
onClick={onCalibrate}
|
465 |
-
>
|
466 |
-
{calibrationStatus ? "📏 Re-calibrate" : "📏 Calibrate"}
|
467 |
-
</Button>
|
468 |
-
<Button
|
469 |
-
size="sm"
|
470 |
-
variant="outline"
|
471 |
-
onClick={onTeleoperate}
|
472 |
-
disabled={!robot.isConnected}
|
473 |
-
title={
|
474 |
-
!robot.isConnected
|
475 |
-
? "Use 'Find & Connect Robots' first"
|
476 |
-
: undefined
|
477 |
-
}
|
478 |
-
>
|
479 |
-
🎮 Teleoperate
|
480 |
-
</Button>
|
481 |
-
</div>
|
482 |
-
)}
|
483 |
-
</div>
|
484 |
-
);
|
485 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/components/TeleoperationPanel.tsx
DELETED
@@ -1,587 +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 |
-
type TeleoperateConfig,
|
11 |
-
} from "@lerobot/web";
|
12 |
-
import { getUnifiedRobotData } from "../lib/unified-storage";
|
13 |
-
import type { RobotConnection } from "@lerobot/web";
|
14 |
-
import { SO100_KEYBOARD_CONTROLS } from "@lerobot/web";
|
15 |
-
|
16 |
-
interface TeleoperationPanelProps {
|
17 |
-
robot: RobotConnection;
|
18 |
-
onClose: () => void;
|
19 |
-
}
|
20 |
-
|
21 |
-
export function TeleoperationPanel({
|
22 |
-
robot,
|
23 |
-
onClose,
|
24 |
-
}: TeleoperationPanelProps) {
|
25 |
-
const [teleoperationState, setTeleoperationState] =
|
26 |
-
useState<TeleoperationState>({
|
27 |
-
isActive: false,
|
28 |
-
motorConfigs: [],
|
29 |
-
lastUpdate: 0,
|
30 |
-
keyStates: {},
|
31 |
-
});
|
32 |
-
const [error, setError] = useState<string | null>(null);
|
33 |
-
const [, setIsInitialized] = useState(false);
|
34 |
-
|
35 |
-
// Separate refs for keyboard and direct teleoperators
|
36 |
-
const keyboardProcessRef = useRef<TeleoperationProcess | null>(null);
|
37 |
-
const directProcessRef = useRef<TeleoperationProcess | null>(null);
|
38 |
-
|
39 |
-
// Initialize both teleoperation processes
|
40 |
-
useEffect(() => {
|
41 |
-
const initializeTeleoperation = async () => {
|
42 |
-
if (!robot || !robot.robotType) {
|
43 |
-
setError("No robot configuration available");
|
44 |
-
return;
|
45 |
-
}
|
46 |
-
|
47 |
-
try {
|
48 |
-
// Load calibration data from demo storage (app concern)
|
49 |
-
let calibrationData;
|
50 |
-
if (robot.serialNumber) {
|
51 |
-
const data = getUnifiedRobotData(robot.serialNumber);
|
52 |
-
calibrationData = data?.calibration;
|
53 |
-
if (calibrationData) {
|
54 |
-
console.log("✅ Loaded calibration data for", robot.serialNumber);
|
55 |
-
}
|
56 |
-
}
|
57 |
-
|
58 |
-
// Create keyboard teleoperation process
|
59 |
-
const keyboardConfig: TeleoperateConfig = {
|
60 |
-
robot: robot,
|
61 |
-
teleop: {
|
62 |
-
type: "keyboard",
|
63 |
-
},
|
64 |
-
calibrationData,
|
65 |
-
onStateUpdate: (state: TeleoperationState) => {
|
66 |
-
setTeleoperationState(state);
|
67 |
-
},
|
68 |
-
};
|
69 |
-
const keyboardProcess = await teleoperate(keyboardConfig);
|
70 |
-
|
71 |
-
// Create direct teleoperation process
|
72 |
-
const directConfig: TeleoperateConfig = {
|
73 |
-
robot: robot,
|
74 |
-
teleop: {
|
75 |
-
type: "direct",
|
76 |
-
},
|
77 |
-
calibrationData,
|
78 |
-
};
|
79 |
-
const directProcess = await teleoperate(directConfig);
|
80 |
-
|
81 |
-
keyboardProcessRef.current = keyboardProcess;
|
82 |
-
directProcessRef.current = directProcess;
|
83 |
-
setTeleoperationState(keyboardProcess.getState());
|
84 |
-
setIsInitialized(true);
|
85 |
-
setError(null);
|
86 |
-
|
87 |
-
console.log("✅ Initialized both keyboard and direct teleoperators");
|
88 |
-
} catch (error) {
|
89 |
-
const errorMessage =
|
90 |
-
error instanceof Error
|
91 |
-
? error.message
|
92 |
-
: "Failed to initialize teleoperation";
|
93 |
-
setError(errorMessage);
|
94 |
-
console.error("❌ Failed to initialize teleoperation:", error);
|
95 |
-
}
|
96 |
-
};
|
97 |
-
|
98 |
-
initializeTeleoperation();
|
99 |
-
|
100 |
-
return () => {
|
101 |
-
// Cleanup on unmount
|
102 |
-
const cleanup = async () => {
|
103 |
-
try {
|
104 |
-
if (keyboardProcessRef.current) {
|
105 |
-
await keyboardProcessRef.current.disconnect();
|
106 |
-
keyboardProcessRef.current = null;
|
107 |
-
}
|
108 |
-
if (directProcessRef.current) {
|
109 |
-
await directProcessRef.current.disconnect();
|
110 |
-
directProcessRef.current = null;
|
111 |
-
}
|
112 |
-
console.log("🧹 Teleoperation cleanup completed");
|
113 |
-
} catch (error) {
|
114 |
-
console.warn("Error during teleoperation cleanup:", error);
|
115 |
-
}
|
116 |
-
};
|
117 |
-
cleanup();
|
118 |
-
};
|
119 |
-
}, [robot]);
|
120 |
-
|
121 |
-
// Keyboard event handlers
|
122 |
-
const handleKeyDown = useCallback(
|
123 |
-
(event: KeyboardEvent) => {
|
124 |
-
if (!teleoperationState.isActive || !keyboardProcessRef.current) return;
|
125 |
-
|
126 |
-
const key = event.key;
|
127 |
-
event.preventDefault();
|
128 |
-
keyboardProcessRef.current.updateKeyState(key, true);
|
129 |
-
},
|
130 |
-
[teleoperationState.isActive]
|
131 |
-
);
|
132 |
-
|
133 |
-
const handleKeyUp = useCallback(
|
134 |
-
(event: KeyboardEvent) => {
|
135 |
-
if (!teleoperationState.isActive || !keyboardProcessRef.current) return;
|
136 |
-
|
137 |
-
const key = event.key;
|
138 |
-
event.preventDefault();
|
139 |
-
keyboardProcessRef.current.updateKeyState(key, false);
|
140 |
-
},
|
141 |
-
[teleoperationState.isActive]
|
142 |
-
);
|
143 |
-
|
144 |
-
// Register keyboard events
|
145 |
-
useEffect(() => {
|
146 |
-
if (teleoperationState.isActive) {
|
147 |
-
window.addEventListener("keydown", handleKeyDown);
|
148 |
-
window.addEventListener("keyup", handleKeyUp);
|
149 |
-
|
150 |
-
return () => {
|
151 |
-
window.removeEventListener("keydown", handleKeyDown);
|
152 |
-
window.removeEventListener("keyup", handleKeyUp);
|
153 |
-
};
|
154 |
-
}
|
155 |
-
}, [teleoperationState.isActive, handleKeyDown, handleKeyUp]);
|
156 |
-
|
157 |
-
const handleStart = () => {
|
158 |
-
if (!keyboardProcessRef.current || !directProcessRef.current) {
|
159 |
-
setError("Teleoperation not initialized");
|
160 |
-
return;
|
161 |
-
}
|
162 |
-
|
163 |
-
try {
|
164 |
-
keyboardProcessRef.current.start();
|
165 |
-
directProcessRef.current.start();
|
166 |
-
console.log("🎮 Both keyboard and direct teleoperation started");
|
167 |
-
} catch (error) {
|
168 |
-
const errorMessage =
|
169 |
-
error instanceof Error
|
170 |
-
? error.message
|
171 |
-
: "Failed to start teleoperation";
|
172 |
-
setError(errorMessage);
|
173 |
-
}
|
174 |
-
};
|
175 |
-
|
176 |
-
const handleStop = async () => {
|
177 |
-
try {
|
178 |
-
if (keyboardProcessRef.current) {
|
179 |
-
keyboardProcessRef.current.stop();
|
180 |
-
}
|
181 |
-
if (directProcessRef.current) {
|
182 |
-
directProcessRef.current.stop();
|
183 |
-
}
|
184 |
-
console.log("🛑 Both keyboard and direct teleoperation stopped");
|
185 |
-
} catch (error) {
|
186 |
-
console.warn("Error during teleoperation stop:", error);
|
187 |
-
}
|
188 |
-
};
|
189 |
-
|
190 |
-
const handleClose = async () => {
|
191 |
-
try {
|
192 |
-
if (keyboardProcessRef.current) {
|
193 |
-
keyboardProcessRef.current.stop();
|
194 |
-
await keyboardProcessRef.current.disconnect();
|
195 |
-
}
|
196 |
-
if (directProcessRef.current) {
|
197 |
-
directProcessRef.current.stop();
|
198 |
-
await directProcessRef.current.disconnect();
|
199 |
-
}
|
200 |
-
console.log("🔌 Properly disconnected from robot");
|
201 |
-
} catch (error) {
|
202 |
-
console.warn("Error during teleoperation cleanup:", error);
|
203 |
-
}
|
204 |
-
onClose();
|
205 |
-
};
|
206 |
-
|
207 |
-
const simulateKeyPress = (key: string) => {
|
208 |
-
if (!keyboardProcessRef.current) return;
|
209 |
-
keyboardProcessRef.current.updateKeyState(key, true);
|
210 |
-
};
|
211 |
-
|
212 |
-
const simulateKeyRelease = (key: string) => {
|
213 |
-
if (!keyboardProcessRef.current) return;
|
214 |
-
keyboardProcessRef.current.updateKeyState(key, false);
|
215 |
-
};
|
216 |
-
|
217 |
-
// Unified motor control: Both sliders AND keyboard use the same teleoperator
|
218 |
-
// This ensures the UI always shows the correct motor positions
|
219 |
-
const moveMotorToPosition = async (motorIndex: number, position: number) => {
|
220 |
-
if (!keyboardProcessRef.current) return;
|
221 |
-
|
222 |
-
try {
|
223 |
-
const motorName = teleoperationState.motorConfigs[motorIndex]?.name;
|
224 |
-
if (motorName) {
|
225 |
-
const keyboardTeleoperator = keyboardProcessRef.current
|
226 |
-
.teleoperator as any;
|
227 |
-
await keyboardTeleoperator.moveMotor(motorName, position);
|
228 |
-
}
|
229 |
-
} catch (error) {
|
230 |
-
console.warn(
|
231 |
-
`Failed to move motor ${motorIndex + 1} to position ${position}:`,
|
232 |
-
error
|
233 |
-
);
|
234 |
-
}
|
235 |
-
};
|
236 |
-
|
237 |
-
const isConnected = robot?.isConnected || false;
|
238 |
-
const isActive = teleoperationState.isActive;
|
239 |
-
const motorConfigs = teleoperationState.motorConfigs;
|
240 |
-
const keyStates = teleoperationState.keyStates;
|
241 |
-
|
242 |
-
// Virtual keyboard component
|
243 |
-
const VirtualKeyboard = () => {
|
244 |
-
const isKeyPressed = (key: string) => {
|
245 |
-
return keyStates?.[key]?.pressed || false;
|
246 |
-
};
|
247 |
-
|
248 |
-
const KeyButton = ({
|
249 |
-
keyCode,
|
250 |
-
children,
|
251 |
-
className = "",
|
252 |
-
size = "default" as "default" | "sm" | "lg" | "icon",
|
253 |
-
}: {
|
254 |
-
keyCode: string;
|
255 |
-
children: React.ReactNode;
|
256 |
-
className?: string;
|
257 |
-
size?: "default" | "sm" | "lg" | "icon";
|
258 |
-
}) => {
|
259 |
-
const control =
|
260 |
-
SO100_KEYBOARD_CONTROLS[
|
261 |
-
keyCode as keyof typeof SO100_KEYBOARD_CONTROLS
|
262 |
-
];
|
263 |
-
const pressed = isKeyPressed(keyCode);
|
264 |
-
|
265 |
-
const handleMouseDown = (e: React.MouseEvent) => {
|
266 |
-
e.preventDefault();
|
267 |
-
if (!isActive) return;
|
268 |
-
simulateKeyPress(keyCode);
|
269 |
-
};
|
270 |
-
|
271 |
-
const handleMouseUp = (e: React.MouseEvent) => {
|
272 |
-
e.preventDefault();
|
273 |
-
if (!isActive) return;
|
274 |
-
simulateKeyRelease(keyCode);
|
275 |
-
};
|
276 |
-
|
277 |
-
return (
|
278 |
-
<Button
|
279 |
-
variant={pressed ? "default" : "outline"}
|
280 |
-
size={size}
|
281 |
-
className={`
|
282 |
-
${className}
|
283 |
-
${
|
284 |
-
pressed
|
285 |
-
? "bg-blue-600 text-white shadow-inner"
|
286 |
-
: "hover:bg-gray-100"
|
287 |
-
}
|
288 |
-
transition-all duration-75 font-mono text-xs
|
289 |
-
${!isActive ? "opacity-50 cursor-not-allowed" : ""}
|
290 |
-
`}
|
291 |
-
disabled={!isActive}
|
292 |
-
onMouseDown={handleMouseDown}
|
293 |
-
onMouseUp={handleMouseUp}
|
294 |
-
onMouseLeave={handleMouseUp}
|
295 |
-
title={control?.description || keyCode}
|
296 |
-
>
|
297 |
-
{children}
|
298 |
-
</Button>
|
299 |
-
);
|
300 |
-
};
|
301 |
-
|
302 |
-
return (
|
303 |
-
<div className="space-y-4">
|
304 |
-
{/* Arrow Keys */}
|
305 |
-
<div className="text-center">
|
306 |
-
<h4 className="text-xs font-semibold mb-2 text-gray-600">Shoulder</h4>
|
307 |
-
<div className="flex flex-col items-center gap-1">
|
308 |
-
<KeyButton keyCode="ArrowUp" size="sm">
|
309 |
-
↑
|
310 |
-
</KeyButton>
|
311 |
-
<div className="flex gap-1">
|
312 |
-
<KeyButton keyCode="ArrowLeft" size="sm">
|
313 |
-
←
|
314 |
-
</KeyButton>
|
315 |
-
<KeyButton keyCode="ArrowDown" size="sm">
|
316 |
-
↓
|
317 |
-
</KeyButton>
|
318 |
-
<KeyButton keyCode="ArrowRight" size="sm">
|
319 |
-
→
|
320 |
-
</KeyButton>
|
321 |
-
</div>
|
322 |
-
</div>
|
323 |
-
</div>
|
324 |
-
|
325 |
-
{/* WASD Keys */}
|
326 |
-
<div className="text-center">
|
327 |
-
<h4 className="text-xs font-semibold mb-2 text-gray-600">
|
328 |
-
Elbow/Wrist
|
329 |
-
</h4>
|
330 |
-
<div className="flex flex-col items-center gap-1">
|
331 |
-
<KeyButton keyCode="w" size="sm">
|
332 |
-
W
|
333 |
-
</KeyButton>
|
334 |
-
<div className="flex gap-1">
|
335 |
-
<KeyButton keyCode="a" size="sm">
|
336 |
-
A
|
337 |
-
</KeyButton>
|
338 |
-
<KeyButton keyCode="s" size="sm">
|
339 |
-
S
|
340 |
-
</KeyButton>
|
341 |
-
<KeyButton keyCode="d" size="sm">
|
342 |
-
D
|
343 |
-
</KeyButton>
|
344 |
-
</div>
|
345 |
-
</div>
|
346 |
-
</div>
|
347 |
-
|
348 |
-
{/* Q/E and Gripper */}
|
349 |
-
<div className="flex justify-center gap-2">
|
350 |
-
<div className="text-center">
|
351 |
-
<h4 className="text-xs font-semibold mb-2 text-gray-600">Roll</h4>
|
352 |
-
<div className="flex gap-1">
|
353 |
-
<KeyButton keyCode="q" size="sm">
|
354 |
-
Q
|
355 |
-
</KeyButton>
|
356 |
-
<KeyButton keyCode="e" size="sm">
|
357 |
-
E
|
358 |
-
</KeyButton>
|
359 |
-
</div>
|
360 |
-
</div>
|
361 |
-
<div className="text-center">
|
362 |
-
<h4 className="text-xs font-semibold mb-2 text-gray-600">
|
363 |
-
Gripper
|
364 |
-
</h4>
|
365 |
-
<div className="flex gap-1">
|
366 |
-
<KeyButton keyCode="o" size="sm">
|
367 |
-
O
|
368 |
-
</KeyButton>
|
369 |
-
<KeyButton keyCode="c" size="sm">
|
370 |
-
C
|
371 |
-
</KeyButton>
|
372 |
-
</div>
|
373 |
-
</div>
|
374 |
-
</div>
|
375 |
-
|
376 |
-
{/* Emergency Stop */}
|
377 |
-
<div className="text-center border-t pt-2">
|
378 |
-
<KeyButton
|
379 |
-
keyCode="Escape"
|
380 |
-
className="bg-red-100 border-red-300 hover:bg-red-200 text-red-800 text-xs"
|
381 |
-
>
|
382 |
-
ESC
|
383 |
-
</KeyButton>
|
384 |
-
</div>
|
385 |
-
</div>
|
386 |
-
);
|
387 |
-
};
|
388 |
-
|
389 |
-
return (
|
390 |
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
391 |
-
<div className="container mx-auto px-6 py-8">
|
392 |
-
{/* Header */}
|
393 |
-
<div className="flex justify-between items-center mb-6">
|
394 |
-
<div>
|
395 |
-
<h1 className="text-3xl font-bold text-gray-900">
|
396 |
-
🎮 Robot Teleoperation
|
397 |
-
</h1>
|
398 |
-
<p className="text-gray-600">
|
399 |
-
{robot.robotId || robot.name} - {robot.serialNumber}
|
400 |
-
</p>
|
401 |
-
</div>
|
402 |
-
<Button variant="outline" onClick={handleClose}>
|
403 |
-
← Back to Dashboard
|
404 |
-
</Button>
|
405 |
-
</div>
|
406 |
-
|
407 |
-
{/* Error Alert */}
|
408 |
-
{error && (
|
409 |
-
<Alert variant="destructive" className="mb-6">
|
410 |
-
<AlertDescription>{error}</AlertDescription>
|
411 |
-
</Alert>
|
412 |
-
)}
|
413 |
-
|
414 |
-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
415 |
-
{/* Status Panel */}
|
416 |
-
<Card>
|
417 |
-
<CardHeader>
|
418 |
-
<CardTitle className="flex items-center gap-2">
|
419 |
-
Status
|
420 |
-
<Badge variant={isConnected ? "default" : "destructive"}>
|
421 |
-
{isConnected ? "Connected" : "Disconnected"}
|
422 |
-
</Badge>
|
423 |
-
</CardTitle>
|
424 |
-
</CardHeader>
|
425 |
-
<CardContent className="space-y-4">
|
426 |
-
<div className="flex items-center justify-between">
|
427 |
-
<span className="text-sm text-gray-600">Teleoperation</span>
|
428 |
-
<Badge variant={isActive ? "default" : "secondary"}>
|
429 |
-
{isActive ? "Active" : "Stopped"}
|
430 |
-
</Badge>
|
431 |
-
</div>
|
432 |
-
|
433 |
-
<div className="flex items-center justify-between">
|
434 |
-
<span className="text-sm text-gray-600">Active Keys</span>
|
435 |
-
<Badge variant="outline">
|
436 |
-
{
|
437 |
-
Object.values(keyStates || {}).filter(
|
438 |
-
(state) => state.pressed
|
439 |
-
).length
|
440 |
-
}
|
441 |
-
</Badge>
|
442 |
-
</div>
|
443 |
-
|
444 |
-
<div className="space-y-2">
|
445 |
-
{isActive ? (
|
446 |
-
<Button
|
447 |
-
onClick={handleStop}
|
448 |
-
variant="destructive"
|
449 |
-
className="w-full"
|
450 |
-
>
|
451 |
-
⏹️ Stop Teleoperation
|
452 |
-
</Button>
|
453 |
-
) : (
|
454 |
-
<Button
|
455 |
-
onClick={handleStart}
|
456 |
-
disabled={!isConnected}
|
457 |
-
className="w-full"
|
458 |
-
>
|
459 |
-
▶️ Start Teleoperation
|
460 |
-
</Button>
|
461 |
-
)}
|
462 |
-
</div>
|
463 |
-
</CardContent>
|
464 |
-
</Card>
|
465 |
-
|
466 |
-
{/* Virtual Keyboard */}
|
467 |
-
<Card>
|
468 |
-
<CardHeader>
|
469 |
-
<CardTitle>Virtual Keyboard</CardTitle>
|
470 |
-
</CardHeader>
|
471 |
-
<CardContent>
|
472 |
-
<VirtualKeyboard />
|
473 |
-
</CardContent>
|
474 |
-
</Card>
|
475 |
-
|
476 |
-
{/* Motor Status */}
|
477 |
-
<Card>
|
478 |
-
<CardHeader>
|
479 |
-
<CardTitle>Motor Positions</CardTitle>
|
480 |
-
</CardHeader>
|
481 |
-
<CardContent className="space-y-3">
|
482 |
-
{motorConfigs.map((motor, index) => {
|
483 |
-
return (
|
484 |
-
<div key={motor.name} className="space-y-1">
|
485 |
-
<div className="flex justify-between items-center">
|
486 |
-
<span className="text-sm font-medium">
|
487 |
-
{motor.name.replace("_", " ")}
|
488 |
-
</span>
|
489 |
-
<span className="text-xs text-gray-500">
|
490 |
-
{motor.currentPosition}
|
491 |
-
</span>
|
492 |
-
</div>
|
493 |
-
<input
|
494 |
-
type="range"
|
495 |
-
min={motor.minPosition}
|
496 |
-
max={motor.maxPosition}
|
497 |
-
value={motor.currentPosition}
|
498 |
-
disabled={!isActive}
|
499 |
-
className={`w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200 slider-thumb ${
|
500 |
-
!isActive ? "opacity-50 cursor-not-allowed" : ""
|
501 |
-
}`}
|
502 |
-
style={{
|
503 |
-
background: isActive
|
504 |
-
? `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${
|
505 |
-
((motor.currentPosition - motor.minPosition) /
|
506 |
-
(motor.maxPosition - motor.minPosition)) *
|
507 |
-
100
|
508 |
-
}%, #e5e7eb ${
|
509 |
-
((motor.currentPosition - motor.minPosition) /
|
510 |
-
(motor.maxPosition - motor.minPosition)) *
|
511 |
-
100
|
512 |
-
}%, #e5e7eb 100%)`
|
513 |
-
: "#e5e7eb",
|
514 |
-
}}
|
515 |
-
onChange={async (e) => {
|
516 |
-
if (!isActive) return;
|
517 |
-
const newPosition = parseInt(e.target.value);
|
518 |
-
try {
|
519 |
-
await moveMotorToPosition(index, newPosition);
|
520 |
-
} catch (error) {
|
521 |
-
console.warn(
|
522 |
-
"Failed to move motor via slider:",
|
523 |
-
error
|
524 |
-
);
|
525 |
-
}
|
526 |
-
}}
|
527 |
-
/>
|
528 |
-
<div className="flex justify-between text-xs text-gray-400">
|
529 |
-
<span>{motor.minPosition}</span>
|
530 |
-
<span>{motor.maxPosition}</span>
|
531 |
-
</div>
|
532 |
-
</div>
|
533 |
-
);
|
534 |
-
})}
|
535 |
-
</CardContent>
|
536 |
-
</Card>
|
537 |
-
</div>
|
538 |
-
|
539 |
-
{/* Help Card */}
|
540 |
-
<Card className="mt-6">
|
541 |
-
<CardHeader>
|
542 |
-
<CardTitle>Control Instructions</CardTitle>
|
543 |
-
</CardHeader>
|
544 |
-
<CardContent>
|
545 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
546 |
-
<div>
|
547 |
-
<h4 className="font-semibold mb-2">Arrow Keys</h4>
|
548 |
-
<ul className="space-y-1 text-gray-600">
|
549 |
-
<li>↑ ↓ Shoulder lift</li>
|
550 |
-
<li>← → Shoulder pan</li>
|
551 |
-
</ul>
|
552 |
-
</div>
|
553 |
-
<div>
|
554 |
-
<h4 className="font-semibold mb-2">WASD Keys</h4>
|
555 |
-
<ul className="space-y-1 text-gray-600">
|
556 |
-
<li>W S Elbow flex</li>
|
557 |
-
<li>A D Wrist flex</li>
|
558 |
-
</ul>
|
559 |
-
</div>
|
560 |
-
<div>
|
561 |
-
<h4 className="font-semibold mb-2">Other Keys</h4>
|
562 |
-
<ul className="space-y-1 text-gray-600">
|
563 |
-
<li>Q E Wrist roll</li>
|
564 |
-
<li>O Open gripper</li>
|
565 |
-
<li>C Close gripper</li>
|
566 |
-
</ul>
|
567 |
-
</div>
|
568 |
-
<div>
|
569 |
-
<h4 className="font-semibold mb-2 text-red-700">Emergency</h4>
|
570 |
-
<ul className="space-y-1 text-red-600">
|
571 |
-
<li>ESC Emergency stop</li>
|
572 |
-
</ul>
|
573 |
-
</div>
|
574 |
-
</div>
|
575 |
-
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
576 |
-
<p className="text-sm text-blue-800">
|
577 |
-
💡 <strong>Pro tip:</strong> Use your physical keyboard for
|
578 |
-
faster control, or click the virtual keys below. Hold keys down
|
579 |
-
for continuous movement.
|
580 |
-
</p>
|
581 |
-
</div>
|
582 |
-
</CardContent>
|
583 |
-
</Card>
|
584 |
-
</div>
|
585 |
-
</div>
|
586 |
-
);
|
587 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 />);
|
|
|
|
|
|
|
|
|
|
|
|
examples/robot-control-web/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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
index.html
DELETED
@@ -1,81 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="en">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
-
<title>🤖 lerobot.js - React Demo</title>
|
7 |
-
<meta
|
8 |
-
name="description"
|
9 |
-
content="State-of-the-art AI for real-world robotics in JavaScript/TypeScript - Interactive React Demo"
|
10 |
-
/>
|
11 |
-
<style>
|
12 |
-
:root {
|
13 |
-
--background: 0 0% 100%;
|
14 |
-
--foreground: 240 10% 3.9%;
|
15 |
-
--card: 0 0% 100%;
|
16 |
-
--card-foreground: 240 10% 3.9%;
|
17 |
-
--popover: 0 0% 100%;
|
18 |
-
--popover-foreground: 240 10% 3.9%;
|
19 |
-
--primary: 240 9% 17%;
|
20 |
-
--primary-foreground: 0 0% 98%;
|
21 |
-
--secondary: 240 4.8% 95.9%;
|
22 |
-
--secondary-foreground: 240 5.9% 10%;
|
23 |
-
--muted: 240 4.8% 95.9%;
|
24 |
-
--muted-foreground: 240 3.8% 46.1%;
|
25 |
-
--accent: 240 4.8% 95.9%;
|
26 |
-
--accent-foreground: 240 5.9% 10%;
|
27 |
-
--destructive: 0 84.2% 60.2%;
|
28 |
-
--destructive-foreground: 0 0% 98%;
|
29 |
-
--border: 240 5.9% 90%;
|
30 |
-
--input: 240 5.9% 90%;
|
31 |
-
--ring: 240 10% 3.9%;
|
32 |
-
--chart-1: 12 76% 61%;
|
33 |
-
--chart-2: 173 58% 39%;
|
34 |
-
--chart-3: 197 37% 24%;
|
35 |
-
--chart-4: 43 74% 66%;
|
36 |
-
--chart-5: 27 87% 67%;
|
37 |
-
--radius: 0.5rem;
|
38 |
-
}
|
39 |
-
|
40 |
-
.dark {
|
41 |
-
--background: 240 10% 3.9%;
|
42 |
-
--foreground: 0 0% 98%;
|
43 |
-
--card: 240 10% 3.9%;
|
44 |
-
--card-foreground: 0 0% 98%;
|
45 |
-
--popover: 240 10% 3.9%;
|
46 |
-
--popover-foreground: 0 0% 98%;
|
47 |
-
--primary: 0 0% 98%;
|
48 |
-
--primary-foreground: 240 5.9% 10%;
|
49 |
-
--secondary: 240 3.7% 15.9%;
|
50 |
-
--secondary-foreground: 0 0% 98%;
|
51 |
-
--muted: 240 3.7% 15.9%;
|
52 |
-
--muted-foreground: 240 5% 64.9%;
|
53 |
-
--accent: 240 3.7% 15.9%;
|
54 |
-
--accent-foreground: 0 0% 98%;
|
55 |
-
--destructive: 0 62.8% 30.6%;
|
56 |
-
--destructive-foreground: 0 0% 98%;
|
57 |
-
--border: 240 3.7% 15.9%;
|
58 |
-
--input: 240 3.7% 15.9%;
|
59 |
-
--ring: 240 4.9% 83.9%;
|
60 |
-
--chart-1: 220 70% 50%;
|
61 |
-
--chart-2: 160 60% 45%;
|
62 |
-
--chart-3: 30 80% 55%;
|
63 |
-
--chart-4: 280 65% 60%;
|
64 |
-
--chart-5: 340 75% 55%;
|
65 |
-
}
|
66 |
-
|
67 |
-
* {
|
68 |
-
border-color: hsl(var(--border));
|
69 |
-
}
|
70 |
-
|
71 |
-
body {
|
72 |
-
background-color: hsl(var(--background));
|
73 |
-
color: hsl(var(--foreground));
|
74 |
-
}
|
75 |
-
</style>
|
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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
postcss.config.mjs
DELETED
@@ -1,6 +0,0 @@
|
|
1 |
-
import tailwindcss from "tailwindcss";
|
2 |
-
import autoprefixer from "autoprefixer";
|
3 |
-
|
4 |
-
export default {
|
5 |
-
plugins: [tailwindcss, autoprefixer],
|
6 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/web_interface.css
DELETED
@@ -1,238 +0,0 @@
|
|
1 |
-
/* lerobot.js Web Interface Styles */
|
2 |
-
|
3 |
-
* {
|
4 |
-
margin: 0;
|
5 |
-
padding: 0;
|
6 |
-
box-sizing: border-box;
|
7 |
-
}
|
8 |
-
|
9 |
-
body {
|
10 |
-
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
11 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
12 |
-
min-height: 100vh;
|
13 |
-
display: flex;
|
14 |
-
align-items: center;
|
15 |
-
justify-content: center;
|
16 |
-
color: #333;
|
17 |
-
}
|
18 |
-
|
19 |
-
.lerobot-app {
|
20 |
-
background: white;
|
21 |
-
border-radius: 12px;
|
22 |
-
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
23 |
-
max-width: 800px;
|
24 |
-
width: 90%;
|
25 |
-
padding: 2rem;
|
26 |
-
margin: 2rem;
|
27 |
-
}
|
28 |
-
|
29 |
-
.lerobot-header {
|
30 |
-
text-align: center;
|
31 |
-
margin-bottom: 2rem;
|
32 |
-
padding-bottom: 1rem;
|
33 |
-
border-bottom: 1px solid #eee;
|
34 |
-
}
|
35 |
-
|
36 |
-
.lerobot-header h1 {
|
37 |
-
font-size: 2.5rem;
|
38 |
-
color: #2c3e50;
|
39 |
-
margin-bottom: 0.5rem;
|
40 |
-
}
|
41 |
-
|
42 |
-
.lerobot-header p {
|
43 |
-
color: #7f8c8d;
|
44 |
-
font-size: 1.1rem;
|
45 |
-
}
|
46 |
-
|
47 |
-
.lerobot-main {
|
48 |
-
display: grid;
|
49 |
-
gap: 2rem;
|
50 |
-
}
|
51 |
-
|
52 |
-
.tool-section {
|
53 |
-
background: #f8f9fa;
|
54 |
-
padding: 1.5rem;
|
55 |
-
border-radius: 8px;
|
56 |
-
border-left: 4px solid #3498db;
|
57 |
-
}
|
58 |
-
|
59 |
-
.tool-section h2 {
|
60 |
-
color: #2c3e50;
|
61 |
-
margin-bottom: 0.5rem;
|
62 |
-
font-size: 1.4rem;
|
63 |
-
}
|
64 |
-
|
65 |
-
.tool-section p {
|
66 |
-
color: #7f8c8d;
|
67 |
-
margin-bottom: 1rem;
|
68 |
-
}
|
69 |
-
|
70 |
-
.button-group {
|
71 |
-
display: flex;
|
72 |
-
gap: 0.5rem;
|
73 |
-
margin-bottom: 1rem;
|
74 |
-
flex-wrap: wrap;
|
75 |
-
}
|
76 |
-
|
77 |
-
.info-box {
|
78 |
-
background: #e8f4fd;
|
79 |
-
border: 1px solid #3498db;
|
80 |
-
border-radius: 6px;
|
81 |
-
padding: 12px 16px;
|
82 |
-
margin-bottom: 1rem;
|
83 |
-
display: flex;
|
84 |
-
align-items: center;
|
85 |
-
gap: 8px;
|
86 |
-
color: #2c3e50;
|
87 |
-
}
|
88 |
-
|
89 |
-
.info-icon {
|
90 |
-
font-size: 1.1rem;
|
91 |
-
flex-shrink: 0;
|
92 |
-
}
|
93 |
-
|
94 |
-
.info-text {
|
95 |
-
font-size: 0.95rem;
|
96 |
-
line-height: 1.4;
|
97 |
-
}
|
98 |
-
|
99 |
-
.primary-btn,
|
100 |
-
.secondary-btn {
|
101 |
-
border: none;
|
102 |
-
padding: 12px 24px;
|
103 |
-
border-radius: 6px;
|
104 |
-
font-size: 1rem;
|
105 |
-
font-weight: 600;
|
106 |
-
cursor: pointer;
|
107 |
-
transition: all 0.3s ease;
|
108 |
-
}
|
109 |
-
|
110 |
-
.primary-btn {
|
111 |
-
background: linear-gradient(135deg, #3498db, #2980b9);
|
112 |
-
color: white;
|
113 |
-
}
|
114 |
-
|
115 |
-
.secondary-btn {
|
116 |
-
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
|
117 |
-
color: white;
|
118 |
-
}
|
119 |
-
|
120 |
-
.primary-btn:hover:not(:disabled) {
|
121 |
-
transform: translateY(-2px);
|
122 |
-
box-shadow: 0 6px 12px rgba(52, 152, 219, 0.3);
|
123 |
-
}
|
124 |
-
|
125 |
-
.secondary-btn:hover:not(:disabled) {
|
126 |
-
transform: translateY(-2px);
|
127 |
-
box-shadow: 0 6px 12px rgba(149, 165, 166, 0.3);
|
128 |
-
}
|
129 |
-
|
130 |
-
.primary-btn:disabled,
|
131 |
-
.secondary-btn:disabled {
|
132 |
-
opacity: 0.6;
|
133 |
-
cursor: not-allowed;
|
134 |
-
transform: none;
|
135 |
-
}
|
136 |
-
|
137 |
-
.results-area {
|
138 |
-
background: #2c3e50;
|
139 |
-
color: #ecf0f1;
|
140 |
-
padding: 1rem;
|
141 |
-
border-radius: 6px;
|
142 |
-
font-family: "Fira Code", "Courier New", monospace;
|
143 |
-
font-size: 0.9rem;
|
144 |
-
line-height: 1.4;
|
145 |
-
max-height: 300px;
|
146 |
-
overflow-y: auto;
|
147 |
-
min-height: 100px;
|
148 |
-
}
|
149 |
-
|
150 |
-
.results-area:empty::before {
|
151 |
-
content: "Results will appear here...";
|
152 |
-
color: #7f8c8d;
|
153 |
-
font-style: italic;
|
154 |
-
}
|
155 |
-
|
156 |
-
.results-area .log {
|
157 |
-
margin: 0.25rem 0;
|
158 |
-
color: #bdc3c7;
|
159 |
-
}
|
160 |
-
|
161 |
-
.results-area .status {
|
162 |
-
margin: 0.25rem 0;
|
163 |
-
color: #3498db;
|
164 |
-
font-weight: 600;
|
165 |
-
}
|
166 |
-
|
167 |
-
.results-area .error {
|
168 |
-
margin: 0.25rem 0;
|
169 |
-
color: #e74c3c;
|
170 |
-
font-weight: 600;
|
171 |
-
}
|
172 |
-
|
173 |
-
.results-area .success {
|
174 |
-
margin: 0.25rem 0;
|
175 |
-
color: #27ae60;
|
176 |
-
font-weight: 600;
|
177 |
-
}
|
178 |
-
|
179 |
-
.info-section {
|
180 |
-
background: #fff3cd;
|
181 |
-
padding: 1.5rem;
|
182 |
-
border-radius: 8px;
|
183 |
-
border-left: 4px solid #ffc107;
|
184 |
-
}
|
185 |
-
|
186 |
-
.info-section h3 {
|
187 |
-
color: #856404;
|
188 |
-
margin-bottom: 0.5rem;
|
189 |
-
font-size: 1.2rem;
|
190 |
-
}
|
191 |
-
|
192 |
-
.info-section p {
|
193 |
-
color: #856404;
|
194 |
-
margin-bottom: 0.5rem;
|
195 |
-
}
|
196 |
-
|
197 |
-
.info-section ul {
|
198 |
-
color: #856404;
|
199 |
-
margin-left: 1.5rem;
|
200 |
-
}
|
201 |
-
|
202 |
-
.info-section li {
|
203 |
-
margin-bottom: 0.25rem;
|
204 |
-
}
|
205 |
-
|
206 |
-
.info-section a {
|
207 |
-
color: #0056b3;
|
208 |
-
text-decoration: none;
|
209 |
-
}
|
210 |
-
|
211 |
-
.info-section a:hover {
|
212 |
-
text-decoration: underline;
|
213 |
-
}
|
214 |
-
|
215 |
-
.info-section code {
|
216 |
-
background: #f5f5f5;
|
217 |
-
padding: 2px 6px;
|
218 |
-
border-radius: 3px;
|
219 |
-
font-family: "Fira Code", "Courier New", monospace;
|
220 |
-
font-size: 0.9em;
|
221 |
-
color: #333;
|
222 |
-
}
|
223 |
-
|
224 |
-
/* Responsive design */
|
225 |
-
@media (max-width: 600px) {
|
226 |
-
.lerobot-app {
|
227 |
-
margin: 1rem;
|
228 |
-
padding: 1.5rem;
|
229 |
-
}
|
230 |
-
|
231 |
-
.lerobot-header h1 {
|
232 |
-
font-size: 2rem;
|
233 |
-
}
|
234 |
-
|
235 |
-
.lerobot-header p {
|
236 |
-
font-size: 1rem;
|
237 |
-
}
|
238 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tailwind.config.js
DELETED
@@ -1,59 +0,0 @@
|
|
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: {
|
10 |
-
lg: "var(--radius)",
|
11 |
-
md: "calc(var(--radius) - 2px)",
|
12 |
-
sm: "calc(var(--radius) - 4px)",
|
13 |
-
},
|
14 |
-
colors: {
|
15 |
-
background: "hsl(var(--background))",
|
16 |
-
foreground: "hsl(var(--foreground))",
|
17 |
-
card: {
|
18 |
-
DEFAULT: "hsl(var(--card))",
|
19 |
-
foreground: "hsl(var(--foreground))",
|
20 |
-
},
|
21 |
-
popover: {
|
22 |
-
DEFAULT: "hsl(var(--popover))",
|
23 |
-
foreground: "hsl(var(--popover-foreground))",
|
24 |
-
},
|
25 |
-
primary: {
|
26 |
-
DEFAULT: "hsl(var(--primary))",
|
27 |
-
foreground: "hsl(var(--primary-foreground))",
|
28 |
-
},
|
29 |
-
secondary: {
|
30 |
-
DEFAULT: "hsl(var(--secondary))",
|
31 |
-
foreground: "hsl(var(--secondary-foreground))",
|
32 |
-
},
|
33 |
-
muted: {
|
34 |
-
DEFAULT: "hsl(var(--muted))",
|
35 |
-
foreground: "hsl(var(--muted-foreground))",
|
36 |
-
},
|
37 |
-
accent: {
|
38 |
-
DEFAULT: "hsl(var(--accent))",
|
39 |
-
foreground: "hsl(var(--accent-foreground))",
|
40 |
-
},
|
41 |
-
destructive: {
|
42 |
-
DEFAULT: "hsl(var(--destructive))",
|
43 |
-
foreground: "hsl(var(--destructive-foreground))",
|
44 |
-
},
|
45 |
-
border: "hsl(var(--border))",
|
46 |
-
input: "hsl(var(--input))",
|
47 |
-
ring: "hsl(var(--ring))",
|
48 |
-
chart: {
|
49 |
-
1: "hsl(var(--chart-1))",
|
50 |
-
2: "hsl(var(--chart-2))",
|
51 |
-
3: "hsl(var(--chart-3))",
|
52 |
-
4: "hsl(var(--chart-4))",
|
53 |
-
5: "hsl(var(--chart-5))",
|
54 |
-
},
|
55 |
-
},
|
56 |
-
},
|
57 |
-
},
|
58 |
-
plugins: [],
|
59 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tsconfig.json
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"compilerOptions": {
|
3 |
-
"target": "ES2020",
|
4 |
-
"useDefineForClassFields": true,
|
5 |
-
"module": "ESNext",
|
6 |
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
7 |
-
"skipLibCheck": true,
|
8 |
-
|
9 |
-
/* React */
|
10 |
-
"jsx": "react-jsx",
|
11 |
-
|
12 |
-
/* Bundler mode */
|
13 |
-
"moduleResolution": "bundler",
|
14 |
-
"allowImportingTsExtensions": true,
|
15 |
-
"verbatimModuleSyntax": true,
|
16 |
-
"moduleDetection": "force",
|
17 |
-
"noEmit": true,
|
18 |
-
|
19 |
-
/* Linting */
|
20 |
-
"strict": true,
|
21 |
-
"noUnusedLocals": true,
|
22 |
-
"noUnusedParameters": true,
|
23 |
-
"erasableSyntaxOnly": true,
|
24 |
-
"noFallthroughCasesInSwitch": true,
|
25 |
-
"noUncheckedSideEffectImports": true
|
26 |
-
},
|
27 |
-
"include": ["src", "examples/robot-control-web"]
|
28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vite.config.ts
DELETED
@@ -1,83 +0,0 @@
|
|
1 |
-
import { defineConfig } from "vite";
|
2 |
-
import react from "@vitejs/plugin-react";
|
3 |
-
import { resolve } from "path";
|
4 |
-
import { existsSync } from "fs";
|
5 |
-
|
6 |
-
export default defineConfig(({ mode }) => {
|
7 |
-
// Check if we're in a workspace environment (has packages/web/src)
|
8 |
-
const isWorkspace = existsSync(resolve(__dirname, "./packages/web/src"));
|
9 |
-
|
10 |
-
const baseConfig = {
|
11 |
-
plugins: [],
|
12 |
-
resolve: {
|
13 |
-
alias: {
|
14 |
-
"@": resolve(__dirname, "./src"),
|
15 |
-
// Only add workspace alias if in workspace environment
|
16 |
-
...(isWorkspace && {
|
17 |
-
"@lerobot/web": resolve(__dirname, "./packages/web/src"),
|
18 |
-
}),
|
19 |
-
},
|
20 |
-
},
|
21 |
-
};
|
22 |
-
|
23 |
-
if (mode === "demo") {
|
24 |
-
// React demo mode - includes React, Tailwind, shadcn/ui
|
25 |
-
return {
|
26 |
-
...baseConfig,
|
27 |
-
plugins: [react()],
|
28 |
-
css: {
|
29 |
-
postcss: "./postcss.config.mjs",
|
30 |
-
},
|
31 |
-
build: {
|
32 |
-
outDir: "dist/demo",
|
33 |
-
rollupOptions: {
|
34 |
-
input: {
|
35 |
-
main: resolve(__dirname, "index.html"),
|
36 |
-
},
|
37 |
-
},
|
38 |
-
},
|
39 |
-
};
|
40 |
-
}
|
41 |
-
|
42 |
-
if (mode === "vanilla") {
|
43 |
-
// Vanilla mode - current implementation without React
|
44 |
-
return {
|
45 |
-
...baseConfig,
|
46 |
-
build: {
|
47 |
-
outDir: "dist/vanilla",
|
48 |
-
rollupOptions: {
|
49 |
-
input: {
|
50 |
-
main: resolve(__dirname, "vanilla.html"),
|
51 |
-
},
|
52 |
-
},
|
53 |
-
},
|
54 |
-
};
|
55 |
-
}
|
56 |
-
|
57 |
-
if (mode === "lib") {
|
58 |
-
// Library mode - core library without any demo UI
|
59 |
-
return {
|
60 |
-
...baseConfig,
|
61 |
-
build: {
|
62 |
-
lib: {
|
63 |
-
entry: resolve(__dirname, "src/main.ts"),
|
64 |
-
name: "LeRobot",
|
65 |
-
fileName: "lerobot",
|
66 |
-
},
|
67 |
-
rollupOptions: {
|
68 |
-
external: ["serialport", "react", "react-dom"],
|
69 |
-
output: {
|
70 |
-
globals: {
|
71 |
-
serialport: "SerialPort",
|
72 |
-
react: "React",
|
73 |
-
"react-dom": "ReactDOM",
|
74 |
-
},
|
75 |
-
},
|
76 |
-
},
|
77 |
-
},
|
78 |
-
};
|
79 |
-
}
|
80 |
-
|
81 |
-
// Default mode (fallback to demo)
|
82 |
-
return defineConfig({ mode: "demo" });
|
83 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|