import { useState, useEffect, useRef, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { ArrowLeft, Settings, Activity, CheckCircle, XCircle, AlertCircle, Loader2, Play, Square, Trash2, List, } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import Logo from "@/components/Logo"; import PortDetectionButton from "@/components/ui/PortDetectionButton"; import PortDetectionModal from "@/components/ui/PortDetectionModal"; import { useApi } from "@/contexts/ApiContext"; interface CalibrationStatus { calibration_active: boolean; status: string; // "idle", "connecting", "homing", "recording", "completed", "error", "stopping" device_type: string | null; error: string | null; message: string; step: number; // Current calibration step total_steps: number; // Total number of calibration steps current_positions: Record | null; recorded_ranges: Record< string, { min: number; max: number; current: number } > | null; } interface CalibrationRequest { device_type: string; // "robot" or "teleop" port: string; config_file: string; } interface CalibrationConfig { name: string; filename: string; size: number; modified: number; } // ConfigsResponse interface removed since we're using text input const Calibration = () => { const navigate = useNavigate(); const { toast } = useToast(); const { baseUrl, fetchWithHeaders } = useApi(); // Ref for auto-scrolling console const consoleRef = useRef(null); // Form state const [deviceType, setDeviceType] = useState("robot"); const [port, setPort] = useState(""); const [configFile, setConfigFile] = useState(""); // Config loading and management const [isLoadingConfigs, setIsLoadingConfigs] = useState(false); const [availableConfigs, setAvailableConfigs] = useState( [] ); // Port detection state const [showPortDetection, setShowPortDetection] = useState(false); const [detectionRobotType, setDetectionRobotType] = useState< "leader" | "follower" >("leader"); // Calibration state const [calibrationStatus, setCalibrationStatus] = useState( { calibration_active: false, status: "idle", device_type: null, error: null, message: "", step: 0, total_steps: 2, current_positions: null, recorded_ranges: null, } ); const [isPolling, setIsPolling] = useState(false); // Config loading removed since we're using text input now // Poll calibration status const pollStatus = async () => { try { const response = await fetchWithHeaders(`${baseUrl}/calibration-status`); if (response.ok) { const status = await response.json(); const previousStatus = calibrationStatus.status; // Debug logging console.log("Status update:", { previousStatus, newStatus: status.status, calibrationActive: status.calibration_active, polling: isPolling, }); setCalibrationStatus(status); // If calibration just completed successfully, refresh the configs list if ( previousStatus !== "completed" && status.status === "completed" && !status.calibration_active && deviceType ) { console.log("Calibration completed - refreshing available configs"); loadAvailableConfigs(deviceType); } // Stop polling if calibration is completed, error, or stopped (idle) if ( !status.calibration_active && (status.status === "completed" || status.status === "error" || status.status === "idle") ) { console.log("Stopping polling due to status:", status.status); setIsPolling(false); } } } catch (error) { console.error("Error polling status:", error); } }; // Start calibration const handleStartCalibration = async () => { if (!deviceType || !port || !configFile) { toast({ title: "Missing Information", description: "Please fill in all required fields", variant: "destructive", }); return; } const request: CalibrationRequest = { device_type: deviceType, port: port, config_file: configFile, }; try { const response = await fetchWithHeaders(`${baseUrl}/start-calibration`, { method: "POST", body: JSON.stringify(request), }); const result = await response.json(); if (result.success) { toast({ title: "Calibration Started", description: `Calibration started for ${deviceType}`, }); setIsPolling(true); } else { toast({ title: "Calibration Failed", description: result.message || "Failed to start calibration", variant: "destructive", }); } } catch (error) { console.error("Error starting calibration:", error); toast({ title: "Error", description: "Failed to start calibration", variant: "destructive", }); } }; // Stop calibration const handleStopCalibration = async () => { try { const response = await fetchWithHeaders(`${baseUrl}/stop-calibration`, { method: "POST", }); const result = await response.json(); if (result.success) { toast({ title: "Calibration Stopped", description: "Calibration has been stopped", }); // Force a status check after stopping setTimeout(() => { pollStatus(); }, 500); } else { toast({ title: "Error", description: result.message || "Failed to stop calibration", variant: "destructive", }); } } catch (error) { console.error("Error stopping calibration:", error); toast({ title: "Error", description: "Failed to stop calibration", variant: "destructive", }); } }; // Load available configs for the selected device type const loadAvailableConfigs = async (deviceType: string) => { if (!deviceType) return; setIsLoadingConfigs(true); try { const response = await fetchWithHeaders( `${baseUrl}/calibration-configs/${deviceType}` ); const data = await response.json(); if (data.success) { setAvailableConfigs(data.configs || []); } else { toast({ title: "Error Loading Configs", description: data.message || "Could not load calibration configs", variant: "destructive", }); } } catch (error) { toast({ title: "Error Loading Configs", description: "Could not connect to the backend server", variant: "destructive", }); } finally { setIsLoadingConfigs(false); } }; // Delete a config file const handleDeleteConfig = async (configName: string) => { if (!deviceType) return; try { const response = await fetchWithHeaders( `${baseUrl}/calibration-configs/${deviceType}/${configName}`, { method: "DELETE" } ); const data = await response.json(); if (data.success) { toast({ title: "Config Deleted", description: data.message, }); // Reload the configs list loadAvailableConfigs(deviceType); } else { toast({ title: "Delete Failed", description: data.message || "Could not delete the configuration", variant: "destructive", }); } } catch (error) { toast({ title: "Error", description: "Could not delete the configuration", variant: "destructive", }); } }; // Complete current calibration step const handleCompleteStep = async () => { if (!calibrationStatus.calibration_active) return; try { const response = await fetchWithHeaders( `${baseUrl}/complete-calibration-step`, { method: "POST", } ); const data = await response.json(); if (data.success) { toast({ title: "Step Completed", description: data.message, }); } else { toast({ title: "Step Failed", description: data.message || "Could not complete step", variant: "destructive", }); } } catch (error) { console.error("Error completing step:", error); toast({ title: "Error", description: "Could not complete calibration step", variant: "destructive", }); } }; // Config loading removed - using text input instead // Set up polling useEffect(() => { let interval: NodeJS.Timeout; if (isPolling) { // Use fast polling during active calibration for real-time updates const pollInterval = calibrationStatus.calibration_active ? 100 : 200; interval = setInterval(pollStatus, pollInterval); pollStatus(); // Initial poll } return () => { if (interval) clearInterval(interval); }; }, [isPolling, calibrationStatus.calibration_active]); // Load configs when device type changes useEffect(() => { if (deviceType) { loadAvailableConfigs(deviceType); } else { setAvailableConfigs([]); } }, [deviceType]); // Load default port when device type changes useEffect(() => { const loadDefaultPort = async () => { if (!deviceType) return; try { const robotType = deviceType === "robot" ? "follower" : "leader"; const response = await fetchWithHeaders( `${baseUrl}/robot-port/${robotType}` ); const data = await response.json(); if (data.status === "success") { // Use saved port if available, otherwise use default port const portToUse = data.saved_port || data.default_port; if (portToUse) { setPort(portToUse); } } } catch (error) { console.error("Error loading default port:", error); } }; loadDefaultPort(); }, [deviceType]); // Handle port detection const handlePortDetection = () => { const robotType = deviceType === "robot" ? "follower" : "leader"; setDetectionRobotType(robotType); setShowPortDetection(true); }; const handlePortDetected = (detectedPort: string) => { setPort(detectedPort); }; // Get status color and icon const getStatusDisplay = () => { switch (calibrationStatus.status) { case "idle": return { color: "bg-slate-500", icon: , text: "Idle", }; case "connecting": return { color: "bg-yellow-500", icon: , text: "Connecting", }; case "homing": return { color: "bg-blue-500", icon: , text: "Setting Home Position", }; case "recording": return { color: "bg-purple-500", icon: , text: "Recording Ranges", }; case "completed": return { color: "bg-green-500", icon: , text: "Completed", }; case "error": return { color: "bg-red-500", icon: , text: "Error", }; case "stopping": return { color: "bg-orange-500", icon: , text: "Stopping", }; default: return { color: "bg-slate-500", icon: , text: "Unknown", }; } }; const statusDisplay = getStatusDisplay(); return (
{/* Header */}

Device Calibration

{/* Configuration Panel */} Configuration {/* Device Type Selection */}
{/* Port Configuration */}
setPort(e.target.value)} placeholder="/dev/tty.usbmodem..." className="bg-slate-700 border-slate-600 text-white rounded-md flex-1" />
{/* Config File Name */}
setConfigFile(e.target.value)} placeholder="config_name (e.g., my_robot_v1)" className="bg-slate-700 border-slate-600 text-white rounded-md" />
{/* Available Configurations List */} {deviceType && (
{isLoadingConfigs && ( )}
{availableConfigs.length === 0 ? (
{isLoadingConfigs ? "Loading..." : "No configurations found"}
) : (
{availableConfigs.map((config) => (
{new Date( config.modified * 1000 ).toLocaleDateString()} {" • "} {(config.size / 1024).toFixed(1)} KB
))}
)}
)} {/* Action Buttons */}
{!calibrationStatus.calibration_active ? ( ) : ( )}
{/* Status Panel */} Status {/* Current Status */}
Status: {statusDisplay.icon} {statusDisplay.text}
{/* Live Position Data (during recording) */} {calibrationStatus.status === "recording" && calibrationStatus.recorded_ranges && (
Live Position Data
{Object.entries(calibrationStatus.recorded_ranges).map( ([motor, range]) => { // Calculate progress percentage (current position relative to min/max range) const totalRange = range.max - range.min; const currentOffset = range.current - range.min; const progressPercent = totalRange > 0 ? (currentOffset / totalRange) * 100 : 50; return (
{motor} {range.current}
{/* Progress bar background */}
{/* Min/Max range bar */}
{/* Current position indicator */}
{/* Min/Max labels */}
{range.min} {range.max}
); } )}
)} {/* Status Messages */} {calibrationStatus.status === "connecting" && ( Connecting to the device. Please ensure it's connected. )} {calibrationStatus.status === "homing" && (
Move the device to the middle position of its range, then click "Ready".
)} {calibrationStatus.status === "recording" && (
Important: Move EACH joint from its minimum to maximum position to record full range. Watch the min/max values change in the live data above. Ensure all joints have significant range before finishing.
)} {calibrationStatus.status === "completed" && ( Calibration completed successfully! )} {calibrationStatus.status === "error" && calibrationStatus.error && ( Error: {calibrationStatus.error} )} {/* Calibration Video */}

Calibration Demo:

); }; export default Calibration;