import { useEffect, useRef, useCallback } from "react"; import { URDFViewerElement } from "@/lib/urdfViewerHelpers"; import { useApi } from "@/contexts/ApiContext"; interface JointData { type: "joint_update"; joints: Record; timestamp: number; } interface UseRealTimeJointsProps { viewerRef: React.RefObject; enabled?: boolean; websocketUrl?: string; } export const useRealTimeJoints = ({ viewerRef, enabled = true, websocketUrl, }: UseRealTimeJointsProps) => { const { baseUrl, wsBaseUrl, fetchWithHeaders } = useApi(); const defaultWebSocketUrl = `${wsBaseUrl}/ws/joint-data`; const finalWebSocketUrl = websocketUrl || defaultWebSocketUrl; const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const isConnectedRef = useRef(false); const updateJointValues = useCallback( (joints: Record) => { const viewer = viewerRef.current; if (!viewer || typeof viewer.setJointValue !== "function") { return; } // Update each joint value in the URDF viewer Object.entries(joints).forEach(([jointName, value]) => { try { viewer.setJointValue(jointName, value); } catch (error) { console.warn(`Failed to set joint ${jointName}:`, error); } }); }, [viewerRef] ); const connectWebSocket = useCallback(() => { if (!enabled) return; // First, test if the server is running const testServerConnection = async () => { try { const response = await fetchWithHeaders(`${baseUrl}/health`); if (!response.ok) { console.error("❌ Server health check failed:", response.status); return false; } const data = await response.json(); console.log("✅ Server is running:", data); return true; } catch (error) { console.error("❌ Server is not reachable:", error); return false; } }; // Test server connection first testServerConnection().then((serverAvailable) => { if (!serverAvailable) { console.error("❌ Cannot connect to WebSocket: Server is not running"); console.log( "💡 Make sure to start the FastAPI server with: python -m uvicorn lerobot.livelab.app.main:app --reload" ); return; } try { console.log("🔗 Connecting to WebSocket:", finalWebSocketUrl); const ws = new WebSocket(finalWebSocketUrl); wsRef.current = ws; ws.onopen = () => { console.log("✅ WebSocket connected for real-time joints"); isConnectedRef.current = true; // Clear any existing reconnect timeout if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } }; ws.onmessage = (event) => { try { const data: JointData = JSON.parse(event.data); if (data.type === "joint_update" && data.joints) { updateJointValues(data.joints); } } catch (error) { console.error("❌ Error parsing WebSocket message:", error); } }; ws.onclose = (event) => { console.log( "🔌 WebSocket connection closed:", event.code, event.reason ); isConnectedRef.current = false; wsRef.current = null; // Provide more specific error information if (event.code === 1006) { console.error( "❌ WebSocket connection failed - server may not be running or endpoint not found" ); } else if (event.code === 1000) { console.log("✅ WebSocket closed normally"); } // Attempt to reconnect after a delay if enabled if (enabled && !reconnectTimeoutRef.current && event.code !== 1000) { reconnectTimeoutRef.current = setTimeout(() => { console.log("🔄 Attempting to reconnect WebSocket..."); connectWebSocket(); }, 3000); // Reconnect after 3 seconds } }; ws.onerror = (error) => { console.error("❌ WebSocket error:", error); console.log("💡 Troubleshooting tips:"); console.log( " 1. Make sure FastAPI server is running on localhost:8000" ); console.log(" 2. Check if the /ws/joint-data endpoint exists"); console.log( " 3. Restart the server if you just added WebSocket support" ); isConnectedRef.current = false; }; } catch (error) { console.error("❌ Failed to create WebSocket connection:", error); } }); }, [enabled, websocketUrl, updateJointValues]); const disconnect = useCallback(() => { // Clear reconnect timeout if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } // Close WebSocket connection if (wsRef.current) { wsRef.current.close(); wsRef.current = null; } isConnectedRef.current = false; }, []); // Effect to manage WebSocket connection useEffect(() => { if (enabled) { connectWebSocket(); } else { disconnect(); } // Cleanup on unmount return () => { disconnect(); }; }, [enabled, connectWebSocket, disconnect]); // Return connection status and control functions return { isConnected: isConnectedRef.current, disconnect, reconnect: connectWebSocket, }; };