"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { WizardRosService, type RobotStatus, type RobotActionExecution, getWizardRosService, } from "~/lib/ros/wizard-ros-service"; export interface UseWizardRosOptions { autoConnect?: boolean; onConnected?: () => void; onDisconnected?: () => void; onError?: (error: unknown) => void; onActionCompleted?: (execution: RobotActionExecution) => void; onActionFailed?: (execution: RobotActionExecution) => void; onSystemAction?: ( actionId: string, parameters: Record, ) => Promise; } export interface UseWizardRosReturn { isConnected: boolean; isConnecting: boolean; connectionError: string | null; robotStatus: RobotStatus; activeActions: RobotActionExecution[]; connect: () => Promise; disconnect: () => void; executeRobotAction: ( pluginName: string, actionId: string, parameters: Record, actionConfig?: { topic: string; messageType: string; payloadMapping: { type: string; payload?: Record; transformFn?: string; }; }, ) => Promise; callService: ( service: string, args?: Record, ) => Promise; setAutonomousLife: (enabled: boolean) => Promise; } export function useWizardRos( options: UseWizardRosOptions = {}, ): UseWizardRosReturn { const { autoConnect = true, onConnected, onDisconnected, onError, onActionCompleted, onActionFailed, } = options; const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [connectionError, setConnectionError] = useState(null); const [robotStatus, setRobotStatus] = useState({ connected: false, battery: 0, position: { x: 0, y: 0, theta: 0 }, joints: {}, sensors: {}, lastUpdate: new Date(), }); const [activeActions, setActiveActions] = useState( [], ); // Prevent multiple connections const isInitializedRef = useRef(false); const connectAttemptRef = useRef(false); const serviceRef = useRef(null); const mountedRef = useRef(true); // Use refs for callbacks to prevent infinite re-renders const onConnectedRef = useRef(onConnected); const onDisconnectedRef = useRef(onDisconnected); const onErrorRef = useRef(onError); const onActionCompletedRef = useRef(onActionCompleted); const onActionFailedRef = useRef(onActionFailed); // Update refs when callbacks change onConnectedRef.current = onConnected; onDisconnectedRef.current = onDisconnected; onErrorRef.current = onError; onActionCompletedRef.current = onActionCompleted; onActionFailedRef.current = onActionFailed; // Initialize service (only once) useEffect(() => { if (!isInitializedRef.current) { serviceRef.current = getWizardRosService(); isInitializedRef.current = true; } return () => { mountedRef.current = false; }; }, []); // Set up event listeners with stable callbacks useEffect(() => { const service = serviceRef.current; if (!service) return; const handleConnected = () => { console.log( "[useWizardRos] handleConnected called, mountedRef:", mountedRef.current, ); // Set state immediately, before checking mounted status setIsConnected(true); setIsConnecting(false); setConnectionError(null); if (mountedRef.current) { onConnectedRef.current?.(); } }; const handleDisconnected = () => { if (!mountedRef.current) return; console.log("[useWizardRos] Disconnected from ROS bridge"); setIsConnected(false); setIsConnecting(false); onDisconnectedRef.current?.(); }; const handleError = (error: unknown) => { if (!mountedRef.current) return; console.error("[useWizardRos] ROS connection error:", error); setConnectionError( error instanceof Error ? error.message : "Connection error", ); setIsConnecting(false); onErrorRef.current?.(error); }; const handleRobotStatusUpdated = (status: RobotStatus) => { if (!mountedRef.current) return; setRobotStatus(status); }; const handleActionStarted = (execution: RobotActionExecution) => { if (!mountedRef.current) return; setActiveActions((prev) => { const filtered = prev.filter((action) => action.id !== execution.id); return [...filtered, execution]; }); }; const handleActionCompleted = (execution: RobotActionExecution) => { if (!mountedRef.current) return; setActiveActions((prev) => prev.map((action) => (action.id === execution.id ? execution : action)), ); onActionCompletedRef.current?.(execution); }; const handleActionFailed = (execution: RobotActionExecution) => { if (!mountedRef.current) return; setActiveActions((prev) => prev.map((action) => (action.id === execution.id ? execution : action)), ); onActionFailedRef.current?.(execution); }; const handleMaxReconnectsReached = () => { if (!mountedRef.current) return; setConnectionError("Maximum reconnection attempts reached"); setIsConnecting(false); }; // Add event listeners service.on("connected", handleConnected); service.on("disconnected", handleDisconnected); service.on("error", handleError); service.on("robot_status_updated", handleRobotStatusUpdated); service.on("action_started", handleActionStarted); service.on("action_completed", handleActionCompleted); service.on("action_failed", handleActionFailed); service.on("max_reconnects_reached", handleMaxReconnectsReached); // Initialize connection status setIsConnected(service.getConnectionStatus()); setRobotStatus(service.getRobotStatus()); setActiveActions(service.getActiveActions()); return () => { service.off("connected", handleConnected); service.off("disconnected", handleDisconnected); service.off("error", handleError); service.off("robot_status_updated", handleRobotStatusUpdated); service.off("action_started", handleActionStarted); service.off("action_completed", handleActionCompleted); service.off("action_failed", handleActionFailed); service.off("max_reconnects_reached", handleMaxReconnectsReached); }; }, []); // Empty deps since we use refs const connect = useCallback(async (): Promise => { const service = serviceRef.current; if (!service || isConnected || isConnecting || connectAttemptRef.current) return; connectAttemptRef.current = true; setIsConnecting(true); setConnectionError(null); try { await service.connect(); // Connection successful - state will be updated by event handler } catch (error) { if (mountedRef.current) { setIsConnecting(false); setConnectionError( error instanceof Error ? error.message : "Connection failed", ); } throw error; } finally { connectAttemptRef.current = false; } }, [isConnected, isConnecting]); // Auto-connect if enabled (only once per hook instance) useEffect(() => { if ( autoConnect && serviceRef.current && !isConnected && !isConnecting && !connectAttemptRef.current ) { const timeoutId = setTimeout(() => { connect().catch((error) => { console.warn( "[useWizardRos] Auto-connect failed (retrying manually):", error instanceof Error ? error.message : error, ); // Don't retry automatically - let user manually connect }); }, 100); // Small delay to prevent immediate connection attempts return () => clearTimeout(timeoutId); } }, [autoConnect, isConnected, isConnecting, connect]); const disconnect = useCallback((): void => { const service = serviceRef.current; if (!service) return; connectAttemptRef.current = false; service.disconnect(); setIsConnected(false); setIsConnecting(false); setConnectionError(null); }, []); const executeRobotAction = useCallback( async ( pluginName: string, actionId: string, parameters: Record, actionConfig?: { topic: string; messageType: string; payloadMapping: { type: string; payload?: Record; transformFn?: string; }; }, ): Promise => { const service = serviceRef.current; if (!service) { throw new Error("ROS service not initialized"); } if (!isConnected) { throw new Error("Not connected to ROS bridge"); } // Handle system actions that bypass ROS 2 bridge (e.g. emotional speech via SSH) if ( actionId === "say_with_emotion" || actionId === "say_text_with_emotion" || actionId === "wake_up" || actionId === "rest" ) { console.log(`[useWizardRos] Intercepting system action: ${actionId}`); const executionId = `sys_${Date.now()}`; // Create a synthetic execution record const execution: RobotActionExecution = { id: executionId, pluginName, actionId, parameters, status: "executing", startTime: new Date(), }; // Trigger started event service.emit("action_started", execution); try { if (options.onSystemAction) { await options.onSystemAction(actionId, parameters); } else { console.warn( "[useWizardRos] No onSystemAction handler provided for system action", ); // Fallback to builtin ROS action if no system handler return service.executeRobotAction( pluginName, actionId, parameters, actionConfig, ); } const completedExecution: RobotActionExecution = { ...execution, status: "completed", endTime: new Date(), }; service.emit("action_completed", completedExecution); return completedExecution; } catch (error) { const failedExecution: RobotActionExecution = { ...execution, status: "failed", endTime: new Date(), error: error instanceof Error ? error.message : "System action failed", }; service.emit("action_failed", failedExecution); throw error; } } return service.executeRobotAction( pluginName, actionId, parameters, actionConfig, ); }, [isConnected], ); const callService = useCallback( async (service: string, args?: Record): Promise => { const srv = serviceRef.current; if (!srv || !isConnected) throw new Error("Not connected"); return srv.callService(service, args); }, [isConnected], ); const setAutonomousLife = useCallback( async (enabled: boolean): Promise => { const srv = serviceRef.current; if (!srv || !isConnected) throw new Error("Not connected"); return srv.setAutonomousLife(enabled); }, [isConnected], ); return { isConnected, isConnecting, connectionError, robotStatus, activeActions, connect, disconnect, executeRobotAction, callService, setAutonomousLife, }; }