Nicolas Rabault commited on
Commit
137f4c1
·
1 Parent(s): 916b4fe

Fix and improve Calibration

Browse files
Files changed (1) hide show
  1. src/pages/Calibration.tsx +206 -139
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>