Spaces:
Running
Running
| import { useEffect, useRef, useCallback } from "react"; | |
| import { URDFViewerElement } from "@/lib/urdfViewerHelpers"; | |
| import { useApi } from "@/contexts/ApiContext"; | |
| interface JointData { | |
| type: "joint_update"; | |
| joints: Record<string, number>; | |
| timestamp: number; | |
| } | |
| interface UseRealTimeJointsProps { | |
| viewerRef: React.RefObject<URDFViewerElement>; | |
| 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<WebSocket | null>(null); | |
| const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null); | |
| const isConnectedRef = useRef<boolean>(false); | |
| const updateJointValues = useCallback( | |
| (joints: Record<string, number>) => { | |
| 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, | |
| }; | |
| }; | |