Spaces:
Running
Running
Merge pull request #4 from jurmy24/calib
Browse files- src/components/landing/LandingHeader.tsx +3 -4
- src/components/landing/UsageInstructionsModal.tsx +65 -66
- src/pages/Calibration.tsx +206 -139
- src/pages/Landing.tsx +15 -15
src/components/landing/LandingHeader.tsx
CHANGED
@@ -21,12 +21,11 @@ const LandingHeader: React.FC<LandingHeaderProps> = ({
|
|
21 |
<h1 className="text-5xl font-bold tracking-tight">LeLab</h1>
|
22 |
<p className="text-xl text-gray-400">LeRobot but on HFSpace.</p>
|
23 |
<Button
|
24 |
-
variant="
|
25 |
-
size="icon"
|
26 |
onClick={onShowInstructions}
|
27 |
-
className="mx-auto"
|
28 |
>
|
29 |
-
|
30 |
</Button>
|
31 |
</div>
|
32 |
</div>
|
|
|
21 |
<h1 className="text-5xl font-bold tracking-tight">LeLab</h1>
|
22 |
<p className="text-xl text-gray-400">LeRobot but on HFSpace.</p>
|
23 |
<Button
|
24 |
+
variant="outline"
|
|
|
25 |
onClick={onShowInstructions}
|
26 |
+
className="mx-auto flex items-center gap-2 border-blue-500 text-blue-400 hover:text-white hover:bg-blue-600 hover:border-blue-600 transition-colors duration-200 px-6 py-2"
|
27 |
>
|
28 |
+
👉 Getting started 👈
|
29 |
</Button>
|
30 |
</div>
|
31 |
</div>
|
src/components/landing/UsageInstructionsModal.tsx
CHANGED
@@ -25,94 +25,93 @@ const UsageInstructionsModal: React.FC<UsageInstructionsModalProps> = ({
|
|
25 |
<DialogHeader className="text-center sm:text-center">
|
26 |
<DialogTitle className="text-white flex items-center justify-center gap-2 text-xl">
|
27 |
<Terminal className="w-6 h-6" />
|
28 |
-
|
29 |
</DialogTitle>
|
30 |
<DialogDescription>
|
31 |
-
|
32 |
</DialogDescription>
|
33 |
</DialogHeader>
|
34 |
-
<div className="space-y-
|
35 |
<div className="space-y-4">
|
36 |
-
<h4 className="font-semibold text-gray-100 text-lg mb-
|
37 |
1. Installation
|
38 |
</h4>
|
39 |
-
<p>
|
40 |
-
|
41 |
-
|
42 |
-
href="https://github.com/nicolas-rabault/leLab"
|
43 |
-
target="_blank"
|
44 |
-
rel="noopener noreferrer"
|
45 |
-
className="text-blue-400 hover:underline"
|
46 |
-
>
|
47 |
-
nicolas-rabault/leLab
|
48 |
-
</a>
|
49 |
</p>
|
50 |
-
<pre className="bg-gray-800 p-
|
51 |
-
<code>
|
52 |
-
|
53 |
-
<br />
|
54 |
-
cd leLab
|
55 |
</code>
|
56 |
</pre>
|
57 |
-
<p className="
|
58 |
-
|
59 |
-
|
60 |
-
<pre className="bg-gray-800 p-3 rounded-md text-xs overflow-x-auto text-left">
|
61 |
-
<code>
|
62 |
-
# Create and activate virtual environment
|
63 |
-
<br />
|
64 |
python -m venv .venv
|
65 |
-
<br />
|
66 |
-
source .venv/bin/activate
|
67 |
-
<br />
|
68 |
-
<br />
|
69 |
-
# Install in editable mode
|
70 |
-
<br />
|
71 |
-
pip install -e .
|
72 |
</code>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
</pre>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
</div>
|
|
|
75 |
<div className="space-y-4">
|
76 |
-
<h4 className="font-semibold text-gray-100 text-lg mb-
|
77 |
-
|
78 |
</h4>
|
79 |
-
<
|
80 |
-
|
81 |
-
|
82 |
-
<code className="bg-gray-800 p-1 rounded font-mono text-sm">
|
83 |
-
lelab
|
84 |
-
</code>
|
85 |
-
<p className="text-gray-400 mt-1">
|
86 |
-
Starts only the FastAPI backend server on{" "}
|
87 |
-
<a
|
88 |
-
href={baseUrl}
|
89 |
-
target="_blank"
|
90 |
-
rel="noopener noreferrer"
|
91 |
-
className="text-blue-400 hover:underline"
|
92 |
-
>
|
93 |
-
{baseUrl}
|
94 |
-
</a>
|
95 |
-
.
|
96 |
-
</p>
|
97 |
-
</li>
|
98 |
-
<li>
|
99 |
-
<code className="bg-gray-800 p-1 rounded font-mono text-sm">
|
100 |
lelab-fullstack
|
101 |
</code>
|
102 |
-
<p className="text-gray-400 mt-1">
|
103 |
-
Starts both
|
104 |
-
(port 8080).
|
105 |
</p>
|
106 |
-
</
|
107 |
-
<
|
108 |
-
<code className="bg-gray-800
|
109 |
lelab-frontend
|
110 |
</code>
|
111 |
-
<p className="text-gray-400 mt-1">
|
112 |
-
Starts only the frontend development server
|
113 |
</p>
|
114 |
-
</
|
115 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
</div>
|
117 |
</div>
|
118 |
</DialogContent>
|
|
|
25 |
<DialogHeader className="text-center sm:text-center">
|
26 |
<DialogTitle className="text-white flex items-center justify-center gap-2 text-xl">
|
27 |
<Terminal className="w-6 h-6" />
|
28 |
+
Getting Started with LeLab
|
29 |
</DialogTitle>
|
30 |
<DialogDescription>
|
31 |
+
Quick setup guide to get LeLab running on your machine.
|
32 |
</DialogDescription>
|
33 |
</DialogHeader>
|
34 |
+
<div className="space-y-6 text-sm py-4">
|
35 |
<div className="space-y-4">
|
36 |
+
<h4 className="font-semibold text-gray-100 text-lg mb-3 border-b border-gray-700 pb-2">
|
37 |
1. Installation
|
38 |
</h4>
|
39 |
+
<p className="text-gray-300 leading-relaxed">
|
40 |
+
Install LeLab directly from GitHub (virtual environment
|
41 |
+
recommended):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
</p>
|
43 |
+
<pre className="bg-gray-800 p-4 rounded-lg text-xs overflow-x-auto text-left border border-gray-700">
|
44 |
+
<code className="text-green-400">
|
45 |
+
pip install git+https://github.com/nicolas-rabault/leLab
|
|
|
|
|
46 |
</code>
|
47 |
</pre>
|
48 |
+
<p className="text-gray-400 text-xs mt-2">
|
49 |
+
💡 <strong>Tip:</strong> Create a virtual environment first with{" "}
|
50 |
+
<code className="bg-gray-800 px-1 rounded text-xs">
|
|
|
|
|
|
|
|
|
51 |
python -m venv .venv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
</code>
|
53 |
+
</p>
|
54 |
+
</div>
|
55 |
+
|
56 |
+
<div className="space-y-4">
|
57 |
+
<h4 className="font-semibold text-gray-100 text-lg mb-3 border-b border-gray-700 pb-2">
|
58 |
+
2. Running LeLab
|
59 |
+
</h4>
|
60 |
+
<p className="text-gray-300 leading-relaxed">
|
61 |
+
After installation, start LeLab with:
|
62 |
+
</p>
|
63 |
+
<pre className="bg-gray-800 p-4 rounded-lg text-xs overflow-x-auto text-left border border-gray-700">
|
64 |
+
<code className="text-blue-400">lelab</code>
|
65 |
</pre>
|
66 |
+
<p className="text-gray-300 leading-relaxed">
|
67 |
+
This will start the FastAPI backend server on{" "}
|
68 |
+
<a
|
69 |
+
href={baseUrl}
|
70 |
+
target="_blank"
|
71 |
+
rel="noopener noreferrer"
|
72 |
+
className="text-blue-400 hover:underline font-medium"
|
73 |
+
>
|
74 |
+
{baseUrl}
|
75 |
+
</a>
|
76 |
+
</p>
|
77 |
</div>
|
78 |
+
|
79 |
<div className="space-y-4">
|
80 |
+
<h4 className="font-semibold text-gray-100 text-lg mb-3 border-b border-gray-700 pb-2">
|
81 |
+
3. Additional Commands
|
82 |
</h4>
|
83 |
+
<div className="space-y-3">
|
84 |
+
<div>
|
85 |
+
<code className="bg-gray-800 px-2 py-1 rounded font-mono text-sm text-yellow-400">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
lelab-fullstack
|
87 |
</code>
|
88 |
+
<p className="text-gray-400 text-xs mt-1 ml-2">
|
89 |
+
Starts both backend and frontend development servers
|
|
|
90 |
</p>
|
91 |
+
</div>
|
92 |
+
<div>
|
93 |
+
<code className="bg-gray-800 px-2 py-1 rounded font-mono text-sm text-purple-400">
|
94 |
lelab-frontend
|
95 |
</code>
|
96 |
+
<p className="text-gray-400 text-xs mt-1 ml-2">
|
97 |
+
Starts only the frontend development server
|
98 |
</p>
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
|
103 |
+
<div className="pt-4 border-t border-gray-700">
|
104 |
+
<p className="text-gray-400 text-xs text-center">
|
105 |
+
For detailed documentation, visit the{" "}
|
106 |
+
<a
|
107 |
+
href="https://github.com/nicolas-rabault/leLab"
|
108 |
+
target="_blank"
|
109 |
+
rel="noopener noreferrer"
|
110 |
+
className="text-blue-400 hover:underline font-medium"
|
111 |
+
>
|
112 |
+
LeLab GitHub repository
|
113 |
+
</a>
|
114 |
+
</p>
|
115 |
</div>
|
116 |
</div>
|
117 |
</DialogContent>
|
src/pages/Calibration.tsx
CHANGED
@@ -24,7 +24,6 @@ import {
|
|
24 |
Loader2,
|
25 |
Play,
|
26 |
Square,
|
27 |
-
RefreshCw,
|
28 |
Trash2,
|
29 |
List,
|
30 |
} from "lucide-react";
|
@@ -36,11 +35,17 @@ import { useApi } from "@/contexts/ApiContext";
|
|
36 |
|
37 |
interface CalibrationStatus {
|
38 |
calibration_active: boolean;
|
39 |
-
status: string; // "idle", "connecting", "
|
40 |
device_type: string | null;
|
41 |
error: string | null;
|
42 |
message: string;
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
}
|
45 |
|
46 |
interface CalibrationRequest {
|
@@ -91,7 +96,10 @@ const Calibration = () => {
|
|
91 |
device_type: null,
|
92 |
error: null,
|
93 |
message: "",
|
94 |
-
|
|
|
|
|
|
|
95 |
}
|
96 |
);
|
97 |
const [isPolling, setIsPolling] = useState(false);
|
@@ -104,13 +112,37 @@ const Calibration = () => {
|
|
104 |
const response = await fetchWithHeaders(`${baseUrl}/calibration-status`);
|
105 |
if (response.ok) {
|
106 |
const status = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
setCalibrationStatus(status);
|
108 |
|
109 |
-
//
|
110 |
if (
|
|
|
|
|
111 |
!status.calibration_active &&
|
112 |
-
|
113 |
) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
setIsPolling(false);
|
115 |
}
|
116 |
}
|
@@ -181,7 +213,11 @@ const Calibration = () => {
|
|
181 |
title: "Calibration Stopped",
|
182 |
description: "Calibration has been stopped",
|
183 |
});
|
184 |
-
|
|
|
|
|
|
|
|
|
185 |
} else {
|
186 |
toast({
|
187 |
title: "Error",
|
@@ -199,23 +235,6 @@ const Calibration = () => {
|
|
199 |
}
|
200 |
};
|
201 |
|
202 |
-
// Reset form
|
203 |
-
const handleReset = () => {
|
204 |
-
setDeviceType("robot");
|
205 |
-
setPort("");
|
206 |
-
setConfigFile("");
|
207 |
-
setAvailableConfigs([]);
|
208 |
-
setCalibrationStatus({
|
209 |
-
calibration_active: false,
|
210 |
-
status: "idle",
|
211 |
-
device_type: null,
|
212 |
-
error: null,
|
213 |
-
message: "",
|
214 |
-
console_output: "",
|
215 |
-
});
|
216 |
-
setIsPolling(false);
|
217 |
-
};
|
218 |
-
|
219 |
// Load available configs for the selected device type
|
220 |
const loadAvailableConfigs = async (deviceType: string) => {
|
221 |
if (!deviceType) return;
|
@@ -281,38 +300,37 @@ const Calibration = () => {
|
|
281 |
}
|
282 |
};
|
283 |
|
284 |
-
//
|
285 |
-
const
|
286 |
if (!calibrationStatus.calibration_active) return;
|
287 |
|
288 |
-
console.log("🔵 Enter button clicked - sending input...");
|
289 |
-
|
290 |
try {
|
291 |
-
const response = await fetchWithHeaders(
|
292 |
-
|
293 |
-
|
294 |
-
|
|
|
|
|
295 |
|
296 |
const data = await response.json();
|
297 |
-
console.log("🔵 Server response:", data);
|
298 |
|
299 |
if (data.success) {
|
300 |
toast({
|
301 |
-
title: "
|
302 |
-
description:
|
303 |
});
|
304 |
} else {
|
305 |
toast({
|
306 |
-
title: "
|
307 |
-
description: data.message || "Could not
|
308 |
variant: "destructive",
|
309 |
});
|
310 |
}
|
311 |
} catch (error) {
|
312 |
-
console.error("
|
313 |
toast({
|
314 |
title: "Error",
|
315 |
-
description: "Could not
|
316 |
variant: "destructive",
|
317 |
});
|
318 |
}
|
@@ -325,17 +343,16 @@ const Calibration = () => {
|
|
325 |
let interval: NodeJS.Timeout;
|
326 |
|
327 |
if (isPolling) {
|
328 |
-
// Use
|
329 |
-
const pollInterval =
|
330 |
-
|
331 |
-
interval = setInterval(pollStatus, pollInterval); // 25ms during calibration, 100ms otherwise
|
332 |
pollStatus(); // Initial poll
|
333 |
}
|
334 |
|
335 |
return () => {
|
336 |
if (interval) clearInterval(interval);
|
337 |
};
|
338 |
-
}, [isPolling, calibrationStatus.
|
339 |
|
340 |
// Load configs when device type changes
|
341 |
useEffect(() => {
|
@@ -346,20 +363,6 @@ const Calibration = () => {
|
|
346 |
}
|
347 |
}, [deviceType]);
|
348 |
|
349 |
-
// Auto-scroll console to bottom when output changes (with debounce)
|
350 |
-
useEffect(() => {
|
351 |
-
if (consoleRef.current && calibrationStatus.console_output) {
|
352 |
-
// Small delay to ensure DOM is updated before scrolling
|
353 |
-
const timeoutId = setTimeout(() => {
|
354 |
-
if (consoleRef.current) {
|
355 |
-
consoleRef.current.scrollTop = consoleRef.current.scrollHeight;
|
356 |
-
}
|
357 |
-
}, 10);
|
358 |
-
|
359 |
-
return () => clearTimeout(timeoutId);
|
360 |
-
}
|
361 |
-
}, [calibrationStatus.console_output]);
|
362 |
-
|
363 |
// Load default port when device type changes
|
364 |
useEffect(() => {
|
365 |
const loadDefaultPort = async () => {
|
@@ -412,11 +415,17 @@ const Calibration = () => {
|
|
412 |
icon: <Loader2 className="w-4 h-4 animate-spin" />,
|
413 |
text: "Connecting",
|
414 |
};
|
415 |
-
case "
|
416 |
return {
|
417 |
color: "bg-blue-500",
|
418 |
icon: <Activity className="w-4 h-4" />,
|
419 |
-
text: "
|
|
|
|
|
|
|
|
|
|
|
|
|
420 |
};
|
421 |
case "completed":
|
422 |
return {
|
@@ -447,13 +456,6 @@ const Calibration = () => {
|
|
447 |
|
448 |
const statusDisplay = getStatusDisplay();
|
449 |
|
450 |
-
// Memoize console output to prevent unnecessary re-renders
|
451 |
-
const memoizedConsoleOutput = useMemo(() => {
|
452 |
-
return (
|
453 |
-
calibrationStatus.console_output || "Waiting for calibration output..."
|
454 |
-
);
|
455 |
-
}, [calibrationStatus.console_output]);
|
456 |
-
|
457 |
return (
|
458 |
<div className="min-h-screen bg-slate-900 text-white p-4">
|
459 |
<div className="max-w-4xl mx-auto">
|
@@ -633,16 +635,6 @@ const Calibration = () => {
|
|
633 |
Stop Calibration
|
634 |
</Button>
|
635 |
)}
|
636 |
-
|
637 |
-
<Button
|
638 |
-
onClick={handleReset}
|
639 |
-
variant="outline"
|
640 |
-
className="w-full border-slate-600 hover:bg-slate-700 rounded-full py-6 text-lg"
|
641 |
-
disabled={calibrationStatus.calibration_active}
|
642 |
-
>
|
643 |
-
<RefreshCw className="w-5 h-5 mr-2" />
|
644 |
-
Reset
|
645 |
-
</Button>
|
646 |
</div>
|
647 |
</CardContent>
|
648 |
</Card>
|
@@ -667,52 +659,73 @@ const Calibration = () => {
|
|
667 |
</Badge>
|
668 |
</div>
|
669 |
|
670 |
-
{
|
671 |
-
|
672 |
-
|
673 |
-
<
|
674 |
-
|
675 |
-
|
676 |
-
|
677 |
-
|
678 |
-
|
679 |
-
|
680 |
-
|
681 |
-
|
682 |
-
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
|
687 |
-
|
688 |
-
|
689 |
-
|
690 |
-
|
691 |
-
|
692 |
-
|
693 |
-
|
694 |
-
|
695 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
696 |
</div>
|
697 |
</div>
|
698 |
-
|
699 |
-
{/* Enter Button */}
|
700 |
-
<div className="flex justify-center">
|
701 |
-
<Button
|
702 |
-
onClick={handleSendEnter}
|
703 |
-
disabled={!calibrationStatus.calibration_active}
|
704 |
-
className="bg-blue-600 hover:bg-blue-700 px-8 py-2 rounded-full"
|
705 |
-
>
|
706 |
-
Press Enter
|
707 |
-
</Button>
|
708 |
-
</div>
|
709 |
-
|
710 |
-
<div className="text-xs text-slate-400 text-center">
|
711 |
-
Click the button above to send Enter to the calibration
|
712 |
-
process
|
713 |
-
</div>
|
714 |
-
</div>
|
715 |
-
)}
|
716 |
|
717 |
{/* Status Messages */}
|
718 |
{calibrationStatus.status === "connecting" && (
|
@@ -724,13 +737,50 @@ const Calibration = () => {
|
|
724 |
</Alert>
|
725 |
)}
|
726 |
|
727 |
-
{calibrationStatus.status === "
|
728 |
-
<
|
729 |
-
<
|
730 |
-
|
731 |
-
|
732 |
-
|
733 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
734 |
)}
|
735 |
|
736 |
{calibrationStatus.status === "completed" && (
|
@@ -752,19 +802,36 @@ const Calibration = () => {
|
|
752 |
</Alert>
|
753 |
)}
|
754 |
|
755 |
-
{/*
|
756 |
<div className="bg-slate-900/50 p-4 rounded-lg border border-slate-700">
|
757 |
-
<h4 className="font-semibold mb-
|
758 |
-
|
759 |
</h4>
|
760 |
-
<
|
761 |
-
<
|
762 |
-
|
763 |
-
|
764 |
-
|
765 |
-
|
766 |
-
|
767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
768 |
</div>
|
769 |
</CardContent>
|
770 |
</Card>
|
|
|
24 |
Loader2,
|
25 |
Play,
|
26 |
Square,
|
|
|
27 |
Trash2,
|
28 |
List,
|
29 |
} from "lucide-react";
|
|
|
35 |
|
36 |
interface CalibrationStatus {
|
37 |
calibration_active: boolean;
|
38 |
+
status: string; // "idle", "connecting", "homing", "recording", "completed", "error", "stopping"
|
39 |
device_type: string | null;
|
40 |
error: string | null;
|
41 |
message: string;
|
42 |
+
step: number; // Current calibration step
|
43 |
+
total_steps: number; // Total number of calibration steps
|
44 |
+
current_positions: Record<string, number> | null;
|
45 |
+
recorded_ranges: Record<
|
46 |
+
string,
|
47 |
+
{ min: number; max: number; current: number }
|
48 |
+
> | null;
|
49 |
}
|
50 |
|
51 |
interface CalibrationRequest {
|
|
|
96 |
device_type: null,
|
97 |
error: null,
|
98 |
message: "",
|
99 |
+
step: 0,
|
100 |
+
total_steps: 2,
|
101 |
+
current_positions: null,
|
102 |
+
recorded_ranges: null,
|
103 |
}
|
104 |
);
|
105 |
const [isPolling, setIsPolling] = useState(false);
|
|
|
112 |
const response = await fetchWithHeaders(`${baseUrl}/calibration-status`);
|
113 |
if (response.ok) {
|
114 |
const status = await response.json();
|
115 |
+
const previousStatus = calibrationStatus.status;
|
116 |
+
|
117 |
+
// Debug logging
|
118 |
+
console.log("Status update:", {
|
119 |
+
previousStatus,
|
120 |
+
newStatus: status.status,
|
121 |
+
calibrationActive: status.calibration_active,
|
122 |
+
polling: isPolling,
|
123 |
+
});
|
124 |
+
|
125 |
setCalibrationStatus(status);
|
126 |
|
127 |
+
// If calibration just completed successfully, refresh the configs list
|
128 |
if (
|
129 |
+
previousStatus !== "completed" &&
|
130 |
+
status.status === "completed" &&
|
131 |
!status.calibration_active &&
|
132 |
+
deviceType
|
133 |
) {
|
134 |
+
console.log("Calibration completed - refreshing available configs");
|
135 |
+
loadAvailableConfigs(deviceType);
|
136 |
+
}
|
137 |
+
|
138 |
+
// Stop polling if calibration is completed, error, or stopped (idle)
|
139 |
+
if (
|
140 |
+
!status.calibration_active &&
|
141 |
+
(status.status === "completed" ||
|
142 |
+
status.status === "error" ||
|
143 |
+
status.status === "idle")
|
144 |
+
) {
|
145 |
+
console.log("Stopping polling due to status:", status.status);
|
146 |
setIsPolling(false);
|
147 |
}
|
148 |
}
|
|
|
213 |
title: "Calibration Stopped",
|
214 |
description: "Calibration has been stopped",
|
215 |
});
|
216 |
+
|
217 |
+
// Force a status check after stopping
|
218 |
+
setTimeout(() => {
|
219 |
+
pollStatus();
|
220 |
+
}, 500);
|
221 |
} else {
|
222 |
toast({
|
223 |
title: "Error",
|
|
|
235 |
}
|
236 |
};
|
237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
238 |
// Load available configs for the selected device type
|
239 |
const loadAvailableConfigs = async (deviceType: string) => {
|
240 |
if (!deviceType) return;
|
|
|
300 |
}
|
301 |
};
|
302 |
|
303 |
+
// Complete current calibration step
|
304 |
+
const handleCompleteStep = async () => {
|
305 |
if (!calibrationStatus.calibration_active) return;
|
306 |
|
|
|
|
|
307 |
try {
|
308 |
+
const response = await fetchWithHeaders(
|
309 |
+
`${baseUrl}/complete-calibration-step`,
|
310 |
+
{
|
311 |
+
method: "POST",
|
312 |
+
}
|
313 |
+
);
|
314 |
|
315 |
const data = await response.json();
|
|
|
316 |
|
317 |
if (data.success) {
|
318 |
toast({
|
319 |
+
title: "Step Completed",
|
320 |
+
description: data.message,
|
321 |
});
|
322 |
} else {
|
323 |
toast({
|
324 |
+
title: "Step Failed",
|
325 |
+
description: data.message || "Could not complete step",
|
326 |
variant: "destructive",
|
327 |
});
|
328 |
}
|
329 |
} catch (error) {
|
330 |
+
console.error("Error completing step:", error);
|
331 |
toast({
|
332 |
title: "Error",
|
333 |
+
description: "Could not complete calibration step",
|
334 |
variant: "destructive",
|
335 |
});
|
336 |
}
|
|
|
343 |
let interval: NodeJS.Timeout;
|
344 |
|
345 |
if (isPolling) {
|
346 |
+
// Use fast polling during active calibration for real-time updates
|
347 |
+
const pollInterval = calibrationStatus.calibration_active ? 100 : 200;
|
348 |
+
interval = setInterval(pollStatus, pollInterval);
|
|
|
349 |
pollStatus(); // Initial poll
|
350 |
}
|
351 |
|
352 |
return () => {
|
353 |
if (interval) clearInterval(interval);
|
354 |
};
|
355 |
+
}, [isPolling, calibrationStatus.calibration_active]);
|
356 |
|
357 |
// Load configs when device type changes
|
358 |
useEffect(() => {
|
|
|
363 |
}
|
364 |
}, [deviceType]);
|
365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
366 |
// Load default port when device type changes
|
367 |
useEffect(() => {
|
368 |
const loadDefaultPort = async () => {
|
|
|
415 |
icon: <Loader2 className="w-4 h-4 animate-spin" />,
|
416 |
text: "Connecting",
|
417 |
};
|
418 |
+
case "homing":
|
419 |
return {
|
420 |
color: "bg-blue-500",
|
421 |
icon: <Activity className="w-4 h-4" />,
|
422 |
+
text: "Setting Home Position",
|
423 |
+
};
|
424 |
+
case "recording":
|
425 |
+
return {
|
426 |
+
color: "bg-purple-500",
|
427 |
+
icon: <Activity className="w-4 h-4" />,
|
428 |
+
text: "Recording Ranges",
|
429 |
};
|
430 |
case "completed":
|
431 |
return {
|
|
|
456 |
|
457 |
const statusDisplay = getStatusDisplay();
|
458 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
459 |
return (
|
460 |
<div className="min-h-screen bg-slate-900 text-white p-4">
|
461 |
<div className="max-w-4xl mx-auto">
|
|
|
635 |
Stop Calibration
|
636 |
</Button>
|
637 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
638 |
</div>
|
639 |
</CardContent>
|
640 |
</Card>
|
|
|
659 |
</Badge>
|
660 |
</div>
|
661 |
|
662 |
+
{/* Live Position Data (during recording) */}
|
663 |
+
{calibrationStatus.status === "recording" &&
|
664 |
+
calibrationStatus.recorded_ranges && (
|
665 |
+
<div className="space-y-3">
|
666 |
+
<div className="flex items-center gap-2">
|
667 |
+
<Activity className="w-4 h-4 text-purple-400" />
|
668 |
+
<span className="text-sm font-medium text-slate-300">
|
669 |
+
Live Position Data
|
670 |
+
</span>
|
671 |
+
</div>
|
672 |
+
<div className="bg-slate-800 rounded-lg p-4 border border-slate-700">
|
673 |
+
<div className="space-y-3">
|
674 |
+
{Object.entries(calibrationStatus.recorded_ranges).map(
|
675 |
+
([motor, range]) => {
|
676 |
+
// Calculate progress percentage (current position relative to min/max range)
|
677 |
+
const totalRange = range.max - range.min;
|
678 |
+
const currentOffset = range.current - range.min;
|
679 |
+
const progressPercent =
|
680 |
+
totalRange > 0
|
681 |
+
? (currentOffset / totalRange) * 100
|
682 |
+
: 50;
|
683 |
+
|
684 |
+
return (
|
685 |
+
<div key={motor} className="space-y-2">
|
686 |
+
<div className="flex items-center justify-between">
|
687 |
+
<span className="text-white font-semibold text-sm">
|
688 |
+
{motor}
|
689 |
+
</span>
|
690 |
+
<span className="text-slate-300 text-xs font-mono">
|
691 |
+
{range.current}
|
692 |
+
</span>
|
693 |
+
</div>
|
694 |
+
<div className="relative">
|
695 |
+
{/* Progress bar background */}
|
696 |
+
<div className="w-full bg-slate-700 rounded-full h-3">
|
697 |
+
{/* Min/Max range bar */}
|
698 |
+
<div
|
699 |
+
className="bg-slate-600 h-3 rounded-full relative"
|
700 |
+
style={{ width: "100%" }}
|
701 |
+
>
|
702 |
+
{/* Current position indicator */}
|
703 |
+
<div
|
704 |
+
className="absolute top-0 w-1 h-3 bg-yellow-400 rounded-full transition-all duration-100"
|
705 |
+
style={{
|
706 |
+
left: `${Math.max(
|
707 |
+
0,
|
708 |
+
Math.min(100, progressPercent)
|
709 |
+
)}%`,
|
710 |
+
transform: "translateX(-50%)",
|
711 |
+
}}
|
712 |
+
/>
|
713 |
+
</div>
|
714 |
+
</div>
|
715 |
+
{/* Min/Max labels */}
|
716 |
+
<div className="flex justify-between text-xs text-slate-400 mt-1">
|
717 |
+
<span>{range.min}</span>
|
718 |
+
<span>{range.max}</span>
|
719 |
+
</div>
|
720 |
+
</div>
|
721 |
+
</div>
|
722 |
+
);
|
723 |
+
}
|
724 |
+
)}
|
725 |
+
</div>
|
726 |
</div>
|
727 |
</div>
|
728 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
729 |
|
730 |
{/* Status Messages */}
|
731 |
{calibrationStatus.status === "connecting" && (
|
|
|
737 |
</Alert>
|
738 |
)}
|
739 |
|
740 |
+
{calibrationStatus.status === "homing" && (
|
741 |
+
<div className="space-y-3">
|
742 |
+
<Alert className="bg-blue-900/50 border-blue-700 text-blue-200">
|
743 |
+
<Activity className="h-4 w-4" />
|
744 |
+
<AlertDescription>
|
745 |
+
Move the device to the middle position of its range, then
|
746 |
+
click "Ready".
|
747 |
+
</AlertDescription>
|
748 |
+
</Alert>
|
749 |
+
<div className="flex justify-center">
|
750 |
+
<Button
|
751 |
+
onClick={handleCompleteStep}
|
752 |
+
disabled={!calibrationStatus.calibration_active}
|
753 |
+
className="bg-green-600 hover:bg-green-700 px-8 py-3 rounded-full"
|
754 |
+
>
|
755 |
+
<CheckCircle className="w-4 h-4 mr-2" />
|
756 |
+
Ready
|
757 |
+
</Button>
|
758 |
+
</div>
|
759 |
+
</div>
|
760 |
+
)}
|
761 |
+
|
762 |
+
{calibrationStatus.status === "recording" && (
|
763 |
+
<div className="space-y-3">
|
764 |
+
<Alert className="bg-purple-900/50 border-purple-700 text-purple-200">
|
765 |
+
<Activity className="h-4 w-4" />
|
766 |
+
<AlertDescription>
|
767 |
+
<strong>Important:</strong> Move EACH joint from its
|
768 |
+
minimum to maximum position to record full range. Watch
|
769 |
+
the min/max values change in the live data above. Ensure
|
770 |
+
all joints have significant range before finishing.
|
771 |
+
</AlertDescription>
|
772 |
+
</Alert>
|
773 |
+
<div className="flex justify-center">
|
774 |
+
<Button
|
775 |
+
onClick={handleCompleteStep}
|
776 |
+
disabled={!calibrationStatus.calibration_active}
|
777 |
+
className="bg-green-600 hover:bg-green-700 px-8 py-3 rounded-full"
|
778 |
+
>
|
779 |
+
<CheckCircle className="w-4 h-4 mr-2" />
|
780 |
+
Calibration End
|
781 |
+
</Button>
|
782 |
+
</div>
|
783 |
+
</div>
|
784 |
)}
|
785 |
|
786 |
{calibrationStatus.status === "completed" && (
|
|
|
802 |
</Alert>
|
803 |
)}
|
804 |
|
805 |
+
{/* Calibration Video */}
|
806 |
<div className="bg-slate-900/50 p-4 rounded-lg border border-slate-700">
|
807 |
+
<h4 className="font-semibold mb-3 text-slate-200">
|
808 |
+
Calibration Demo:
|
809 |
</h4>
|
810 |
+
<div className="relative rounded-lg overflow-hidden bg-slate-800">
|
811 |
+
<video
|
812 |
+
className="w-full h-auto rounded-md"
|
813 |
+
controls
|
814 |
+
preload="auto"
|
815 |
+
muted
|
816 |
+
>
|
817 |
+
<source
|
818 |
+
src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lerobot/calibrate_so101_2.mp4"
|
819 |
+
type="video/mp4"
|
820 |
+
/>
|
821 |
+
<p className="text-slate-400 text-sm text-center py-4">
|
822 |
+
Your browser does not support the video tag.
|
823 |
+
<br />
|
824 |
+
<a
|
825 |
+
href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lerobot/calibrate_so101_2.mp4"
|
826 |
+
className="text-blue-400 hover:text-blue-300 underline"
|
827 |
+
target="_blank"
|
828 |
+
rel="noopener noreferrer"
|
829 |
+
>
|
830 |
+
Click here to view the calibration video
|
831 |
+
</a>
|
832 |
+
</p>
|
833 |
+
</video>
|
834 |
+
</div>
|
835 |
</div>
|
836 |
</CardContent>
|
837 |
</Card>
|
src/pages/Landing.tsx
CHANGED
@@ -358,6 +358,13 @@ const Landing = () => {
|
|
358 |
};
|
359 |
|
360 |
const actions: Action[] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
361 |
{
|
362 |
title: "Teleoperation",
|
363 |
description: "Control the robot arm in real-time.",
|
@@ -371,17 +378,10 @@ const Landing = () => {
|
|
371 |
color: "bg-red-500 hover:bg-red-600",
|
372 |
},
|
373 |
{
|
374 |
-
title: "
|
375 |
-
description: "
|
376 |
-
handler:
|
377 |
-
color: "bg-
|
378 |
-
isWorkInProgress: true,
|
379 |
-
},
|
380 |
-
{
|
381 |
-
title: "Calibration",
|
382 |
-
description: "Calibrate robot arm positions.",
|
383 |
-
handler: handleCalibrationClick,
|
384 |
-
color: "bg-indigo-500 hover:bg-indigo-600",
|
385 |
isWorkInProgress: true,
|
386 |
},
|
387 |
{
|
@@ -392,10 +392,10 @@ const Landing = () => {
|
|
392 |
isWorkInProgress: true,
|
393 |
},
|
394 |
{
|
395 |
-
title: "
|
396 |
-
description: "
|
397 |
-
handler:
|
398 |
-
color: "bg-
|
399 |
isWorkInProgress: true,
|
400 |
},
|
401 |
];
|
|
|
358 |
};
|
359 |
|
360 |
const actions: Action[] = [
|
361 |
+
{
|
362 |
+
title: "Calibration",
|
363 |
+
description: "Calibrate robot arm positions.",
|
364 |
+
handler: handleCalibrationClick,
|
365 |
+
color: "bg-indigo-500 hover:bg-indigo-600",
|
366 |
+
isWorkInProgress: false,
|
367 |
+
},
|
368 |
{
|
369 |
title: "Teleoperation",
|
370 |
description: "Control the robot arm in real-time.",
|
|
|
378 |
color: "bg-red-500 hover:bg-red-600",
|
379 |
},
|
380 |
{
|
381 |
+
title: "Replay Dataset",
|
382 |
+
description: "Replay and analyze recorded datasets.",
|
383 |
+
handler: handleReplayDatasetClick,
|
384 |
+
color: "bg-purple-500 hover:bg-purple-600",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
385 |
isWorkInProgress: true,
|
386 |
},
|
387 |
{
|
|
|
392 |
isWorkInProgress: true,
|
393 |
},
|
394 |
{
|
395 |
+
title: "Direct Follower Control",
|
396 |
+
description: "Control robot arm with mouse movements.",
|
397 |
+
handler: handleDirectFollowerClick,
|
398 |
+
color: "bg-blue-500 hover:bg-blue-600",
|
399 |
isWorkInProgress: true,
|
400 |
},
|
401 |
];
|