Nicolas Rabault commited on
Commit
b9bad26
·
unverified ·
2 Parent(s): 916b4fe a6ba3b9

Merge pull request #4 from jurmy24/calib

Browse files
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="ghost"
25
- size="icon"
26
  onClick={onShowInstructions}
27
- className="mx-auto"
28
  >
29
- <Info className="h-6 w-6 text-gray-400 hover:text-white" />
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
+ 👉&nbsp;&nbsp;&nbsp;&nbsp;Getting started&nbsp;&nbsp;&nbsp;&nbsp;👈
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
- Running LeLab Locally
29
  </DialogTitle>
30
  <DialogDescription>
31
- Instructions for setting up and running the project on your machine.
32
  </DialogDescription>
33
  </DialogHeader>
34
- <div className="space-y-8 text-sm py-4">
35
  <div className="space-y-4">
36
- <h4 className="font-semibold text-gray-100 text-lg mb-2 border-b border-gray-700 pb-2">
37
  1. Installation
38
  </h4>
39
- <p>
40
- Clone the repository from GitHub:{" "}
41
- <a
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-3 rounded-md text-xs overflow-x-auto text-left">
51
- <code>
52
- git clone https://github.com/nicolas-rabault/leLab
53
- <br />
54
- cd leLab
55
  </code>
56
  </pre>
57
- <p className="mt-2 font-medium text-gray-200">
58
- Install dependencies (virtual environment recommended):
59
- </p>
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-2 border-b border-gray-700 pb-2">
77
- 2. Running the Application
78
  </h4>
79
- <p>After installation, use one of the command-line tools:</p>
80
- <ul className="space-y-4 text-xs text-left">
81
- <li>
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 FastAPI backend (port 8000) and this Vite frontend
104
- (port 8080).
105
  </p>
106
- </li>
107
- <li>
108
- <code className="bg-gray-800 p-1 rounded font-mono text-sm">
109
  lelab-frontend
110
  </code>
111
- <p className="text-gray-400 mt-1">
112
- Starts only the frontend development server.
113
  </p>
114
- </li>
115
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", "calibrating", "completed", "error", "stopping"
40
  device_type: string | null;
41
  error: string | null;
42
  message: string;
43
- console_output: string;
 
 
 
 
 
 
44
  }
45
 
46
  interface CalibrationRequest {
@@ -91,7 +96,10 @@ const Calibration = () => {
91
  device_type: null,
92
  error: null,
93
  message: "",
94
- console_output: "",
 
 
 
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
- // Stop polling if calibration is completed or error
110
  if (
 
 
111
  !status.calibration_active &&
112
- (status.status === "completed" || status.status === "error")
113
  ) {
 
 
 
 
 
 
 
 
 
 
 
 
114
  setIsPolling(false);
115
  }
116
  }
@@ -181,7 +213,11 @@ const Calibration = () => {
181
  title: "Calibration Stopped",
182
  description: "Calibration has been stopped",
183
  });
184
- setIsPolling(false);
 
 
 
 
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
- // Send Enter to calibration process
285
- const handleSendEnter = async () => {
286
  if (!calibrationStatus.calibration_active) return;
287
 
288
- console.log("🔵 Enter button clicked - sending input...");
289
-
290
  try {
291
- const response = await fetchWithHeaders(`${baseUrl}/calibration-input`, {
292
- method: "POST",
293
- body: JSON.stringify({ input: "\n" }), // Send actual newline character
294
- });
 
 
295
 
296
  const data = await response.json();
297
- console.log("🔵 Server response:", data);
298
 
299
  if (data.success) {
300
  toast({
301
- title: "Enter Sent",
302
- description: "Enter key sent to calibration process",
303
  });
304
  } else {
305
  toast({
306
- title: "Input Failed",
307
- description: data.message || "Could not send Enter",
308
  variant: "destructive",
309
  });
310
  }
311
  } catch (error) {
312
- console.error("🔴 Error sending Enter:", error);
313
  toast({
314
  title: "Error",
315
- description: "Could not send Enter to calibration",
316
  variant: "destructive",
317
  });
318
  }
@@ -325,17 +343,16 @@ const Calibration = () => {
325
  let interval: NodeJS.Timeout;
326
 
327
  if (isPolling) {
328
- // Use ultra-fast polling during active calibration for real-time updates
329
- const pollInterval =
330
- calibrationStatus.status === "calibrating" ? 25 : 100;
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.status]);
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 "calibrating":
416
  return {
417
  color: "bg-blue-500",
418
  icon: <Activity className="w-4 h-4" />,
419
- text: "Calibrating",
 
 
 
 
 
 
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
- {calibrationStatus.device_type && (
671
- <div className="flex items-center justify-between p-3 bg-slate-900/50 rounded-md">
672
- <span className="text-slate-300">Device:</span>
673
- <span className="text-white capitalize">
674
- {calibrationStatus.device_type}
675
- </span>
676
- </div>
677
- )}
678
-
679
- {/* Calibration Console */}
680
- {calibrationStatus.calibration_active && (
681
- <div className="space-y-3">
682
- <div className="flex items-center gap-2">
683
- <Settings className="w-4 h-4 text-slate-400" />
684
- <span className="text-sm font-medium text-slate-300">
685
- Calibration Console
686
- </span>
687
- </div>
688
-
689
- {/* Console Output */}
690
- <div className="bg-black rounded-lg p-4 font-mono text-sm border border-slate-700">
691
- <div
692
- ref={consoleRef}
693
- className="text-green-400 h-80 overflow-y-auto whitespace-pre-wrap"
694
- >
695
- {memoizedConsoleOutput}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 === "calibrating" && (
728
- <Alert className="bg-blue-900/50 border-blue-700 text-blue-200">
729
- <Activity className="h-4 w-4" />
730
- <AlertDescription>
731
- Calibration in progress. Follow device instructions.
732
- </AlertDescription>
733
- </Alert>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
  )}
735
 
736
  {calibrationStatus.status === "completed" && (
@@ -752,19 +802,36 @@ const Calibration = () => {
752
  </Alert>
753
  )}
754
 
755
- {/* Instructions */}
756
  <div className="bg-slate-900/50 p-4 rounded-lg border border-slate-700">
757
- <h4 className="font-semibold mb-2 text-slate-200">
758
- Instructions:
759
  </h4>
760
- <ol className="text-sm text-slate-300 space-y-1 list-decimal list-inside">
761
- <li>Select device type.</li>
762
- <li>Enter the correct port.</li>
763
- <li>Provide a name for the new calibration config.</li>
764
- <li>Move the robot to a middle position.</li>
765
- <li>Click "Start Calibration" & follow device prompts.</li>
766
- <li>Move each motor to its limits on both sides.</li>
767
- </ol>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: "Direct Follower Control",
375
- description: "Control robot arm with mouse movements.",
376
- handler: handleDirectFollowerClick,
377
- color: "bg-blue-500 hover:bg-blue-600",
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: "Replay Dataset",
396
- description: "Replay and analyze recorded datasets.",
397
- handler: handleReplayDatasetClick,
398
- color: "bg-purple-500 hover:bg-purple-600",
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
  ];