mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
fix: migrate wizard from polling to WebSocket and fix duplicate ROS connections
- Removed non-functional trial WebSocket (no server exists) - Kept ROS WebSocket for robot control via useWizardRos - Fixed duplicate ROS connections by passing connection as props - WizardMonitoringPanel now receives ROS connection from parent - Trial status uses reliable tRPC polling (5-15s intervals) - Updated connection badges to show 'ROS Connected/Offline' - Added loading overlay with fade-in to designer - Fixed hash computation to include parameter values - Fixed incremental hash caching for parameter changes Fixes: - WebSocket connection errors eliminated - Connect button now works properly - No more conflicting duplicate connections - Accurate connection status display
This commit is contained in:
296
src/hooks/useWizardRos.ts
Normal file
296
src/hooks/useWizardRos.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
"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;
|
||||
}
|
||||
|
||||
export interface UseWizardRosReturn {
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
connectionError: string | null;
|
||||
robotStatus: RobotStatus;
|
||||
activeActions: RobotActionExecution[];
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => void;
|
||||
executeRobotAction: (
|
||||
pluginName: string,
|
||||
actionId: string,
|
||||
parameters: Record<string, unknown>,
|
||||
actionConfig?: {
|
||||
topic: string;
|
||||
messageType: string;
|
||||
payloadMapping: {
|
||||
type: string;
|
||||
payload?: Record<string, unknown>;
|
||||
transformFn?: string;
|
||||
};
|
||||
},
|
||||
) => Promise<RobotActionExecution>;
|
||||
}
|
||||
|
||||
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<string | null>(null);
|
||||
const [robotStatus, setRobotStatus] = useState<RobotStatus>({
|
||||
connected: false,
|
||||
battery: 0,
|
||||
position: { x: 0, y: 0, theta: 0 },
|
||||
joints: {},
|
||||
sensors: {},
|
||||
lastUpdate: new Date(),
|
||||
});
|
||||
const [activeActions, setActiveActions] = useState<RobotActionExecution[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
// Prevent multiple connections
|
||||
const isInitializedRef = useRef(false);
|
||||
const connectAttemptRef = useRef(false);
|
||||
|
||||
const serviceRef = useRef<WizardRosService | null>(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 = () => {
|
||||
if (!mountedRef.current) return;
|
||||
console.log("[useWizardRos] Connected to ROS bridge");
|
||||
setIsConnected(true);
|
||||
setIsConnecting(false);
|
||||
setConnectionError(null);
|
||||
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<void> => {
|
||||
const service = serviceRef.current;
|
||||
if (!service || isConnected || isConnecting || connectAttemptRef.current)
|
||||
return;
|
||||
|
||||
connectAttemptRef.current = true;
|
||||
setIsConnecting(true);
|
||||
setConnectionError(null);
|
||||
|
||||
try {
|
||||
await service.connect();
|
||||
} 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.error("[useWizardRos] Auto-connect failed:", error);
|
||||
});
|
||||
}, 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<string, unknown>,
|
||||
actionConfig?: {
|
||||
topic: string;
|
||||
messageType: string;
|
||||
payloadMapping: {
|
||||
type: string;
|
||||
payload?: Record<string, unknown>;
|
||||
transformFn?: string;
|
||||
};
|
||||
},
|
||||
): Promise<RobotActionExecution> => {
|
||||
const service = serviceRef.current;
|
||||
if (!service) {
|
||||
throw new Error("ROS service not initialized");
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
throw new Error("Not connected to ROS bridge");
|
||||
}
|
||||
|
||||
return service.executeRobotAction(
|
||||
pluginName,
|
||||
actionId,
|
||||
parameters,
|
||||
actionConfig,
|
||||
);
|
||||
},
|
||||
[isConnected],
|
||||
);
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
connectionError,
|
||||
robotStatus,
|
||||
activeActions,
|
||||
connect,
|
||||
disconnect,
|
||||
executeRobotAction,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user