nao6 ros2 integration updated

This commit is contained in:
2025-11-13 10:58:45 -05:00
parent 70882b9dbb
commit 86b5ed80c4
276 changed files with 4288 additions and 1552 deletions
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
Regular → Executable
View File
View File
View File
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
+42 -22
View File
@@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Play, CheckCircle, X, Clock, AlertCircle } from "lucide-react";
import { Badge } from "~/components/ui/badge";
import { Progress } from "~/components/ui/progress";
@@ -55,7 +55,7 @@ interface StepData {
order: number;
}
export function WizardInterface({
export const WizardInterface = React.memo(function WizardInterface({
trial: initialTrial,
userRole: _userRole,
}: WizardInterfaceProps) {
@@ -120,34 +120,54 @@ export function WizardInterface({
const { data: pollingData } = api.trials.get.useQuery(
{ id: trial.id },
{
refetchInterval: 2000, // Poll every 2 seconds
refetchInterval: trial.status === "in_progress" ? 10000 : 30000, // Poll less frequently
staleTime: 5000, // Consider data fresh for 5 seconds
refetchOnWindowFocus: false, // Don't refetch on window focus
},
);
// Mock trial events for now (can be populated from database later)
const trialEvents: Array<{
type: string;
timestamp: Date;
data?: unknown;
message?: string;
}> = [];
// Memoized trial events to prevent re-creation on every render
const trialEvents = useMemo<
Array<{
type: string;
timestamp: Date;
data?: unknown;
message?: string;
}>
>(() => [], []);
// Update trial data from polling
React.useEffect(() => {
if (pollingData) {
setTrial({
...pollingData,
metadata: pollingData.metadata as Record<string, unknown> | null,
// Update trial data from polling (optimized to prevent unnecessary re-renders)
const updateTrial = useCallback((newTrialData: typeof pollingData) => {
if (!newTrialData) return;
setTrial((prevTrial) => {
// Only update if data actually changed
if (
prevTrial.id === newTrialData.id &&
prevTrial.status === newTrialData.status &&
prevTrial.startedAt === newTrialData.startedAt &&
prevTrial.completedAt === newTrialData.completedAt
) {
return prevTrial; // No changes, keep existing state
}
return {
...newTrialData,
metadata: newTrialData.metadata as Record<string, unknown> | null,
participant: {
...pollingData.participant,
demographics: pollingData.participant.demographics as Record<
...newTrialData.participant,
demographics: newTrialData.participant.demographics as Record<
string,
unknown
> | null,
},
});
}
}, [pollingData]);
};
});
}, []);
useEffect(() => {
updateTrial(pollingData);
}, [pollingData, updateTrial]);
// Transform experiment steps to component format
const steps: StepData[] =
@@ -438,6 +458,6 @@ export function WizardInterface({
</div>
</div>
);
}
});
export default WizardInterface;
View File
View File
View File
View File
View File
+559 -59
View File
@@ -1,6 +1,6 @@
"use client";
import React from "react";
import React, { useState, useRef } from "react";
import {
Bot,
User,
@@ -21,7 +21,6 @@ import { Alert, AlertDescription } from "~/components/ui/alert";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
import { Progress } from "~/components/ui/progress";
import { Button } from "~/components/ui/button";
// import { useRosBridge } from "~/hooks/useRosBridge"; // Removed ROS dependency
interface TrialData {
id: string;
@@ -64,7 +63,7 @@ interface WizardMonitoringPanelProps {
onTabChange: (tab: "status" | "robot" | "events") => void;
}
export function WizardMonitoringPanel({
const WizardMonitoringPanel = React.memo(function WizardMonitoringPanel({
trial,
trialEvents,
isConnected,
@@ -72,32 +71,340 @@ export function WizardMonitoringPanel({
activeTab,
onTabChange,
}: WizardMonitoringPanelProps) {
// Mock robot status for development (ROS bridge removed for now)
const mockRobotStatus = {
// ROS Bridge connection state
const [rosConnected, setRosConnected] = useState(false);
const [rosConnecting, setRosConnecting] = useState(false);
const [rosError, setRosError] = useState<string | null>(null);
const [rosSocket, setRosSocket] = useState<WebSocket | null>(null);
const [robotStatus, setRobotStatus] = useState({
connected: false,
battery: 85,
battery: 0,
position: { x: 0, y: 0, theta: 0 },
joints: {},
sensors: {},
lastUpdate: new Date(),
});
const ROS_BRIDGE_URL = "ws://134.82.159.25:9090";
// Use refs to persist connection state across re-renders
const connectionAttemptRef = useRef(false);
const socketRef = useRef<WebSocket | null>(null);
const connectRos = () => {
// Prevent multiple connection attempts
if (connectionAttemptRef.current) {
console.log("Connection already in progress, skipping");
return;
}
if (
rosSocket?.readyState === WebSocket.OPEN ||
socketRef.current?.readyState === WebSocket.OPEN
) {
console.log("Already connected, skipping");
return;
}
// Prevent rapid reconnection attempts
if (rosConnecting) {
console.log("Connection in progress, please wait");
return;
}
connectionAttemptRef.current = true;
setRosConnecting(true);
setRosError(null);
console.log("🔌 Connecting to ROS Bridge:", ROS_BRIDGE_URL);
const socket = new WebSocket(ROS_BRIDGE_URL);
socketRef.current = socket;
// Add connection timeout
const connectionTimeout = setTimeout(() => {
if (socket.readyState === WebSocket.CONNECTING) {
socket.close();
connectionAttemptRef.current = false;
setRosConnecting(false);
setRosError("Connection timeout (10s) - ROS Bridge not responding");
}
}, 10000);
socket.onopen = () => {
clearTimeout(connectionTimeout);
connectionAttemptRef.current = false;
console.log("Connected to ROS Bridge successfully");
setRosConnected(true);
setRosConnecting(false);
setRosSocket(socket);
setRosError(null);
// Just log connection success - no auto actions
console.log("WebSocket connected successfully to ROS Bridge");
setRobotStatus((prev) => ({
...prev,
connected: true,
lastUpdate: new Date(),
}));
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data as string) as {
topic?: string;
msg?: Record<string, unknown>;
op?: string;
level?: string;
};
// Handle status messages
if (data.op === "status") {
console.log("ROS Bridge status:", data.msg, "Level:", data.level);
return;
}
// Handle topic messages
if (data.topic === "/joint_states" && data.msg) {
setRobotStatus((prev) => ({
...prev,
joints: data.msg ?? {},
lastUpdate: new Date(),
}));
} else if (data.topic === "/naoqi_driver/battery" && data.msg) {
const batteryPercent = (data.msg.percentage as number) || 0;
setRobotStatus((prev) => ({
...prev,
battery: Math.round(batteryPercent),
lastUpdate: new Date(),
}));
} else if (data.topic === "/diagnostics" && data.msg) {
// Handle diagnostic messages for battery
console.log("Diagnostics received:", data.msg);
}
} catch (error) {
console.error("Error parsing ROS message:", error);
}
};
socket.onclose = (event) => {
clearTimeout(connectionTimeout);
connectionAttemptRef.current = false;
setRosConnected(false);
setRosConnecting(false);
setRosSocket(null);
socketRef.current = null;
setRobotStatus((prev) => ({
...prev,
connected: false,
battery: 0,
joints: {},
sensors: {},
}));
// Only show error if it wasn't a normal closure (code 1000)
if (event.code !== 1000) {
let errorMsg = "Connection lost";
if (event.code === 1006) {
errorMsg =
"ROS Bridge not responding - check if rosbridge_server is running";
} else if (event.code === 1011) {
errorMsg = "Server error in ROS Bridge";
} else if (event.code === 1002) {
errorMsg = "Protocol error - check ROS Bridge version";
} else if (event.code === 1001) {
errorMsg = "Server going away - ROS Bridge may have restarted";
}
console.log(
`🔌 Connection closed - Code: ${event.code}, Reason: ${event.reason}`,
);
setRosError(`${errorMsg} (${event.code})`);
}
};
socket.onerror = (error) => {
clearTimeout(connectionTimeout);
connectionAttemptRef.current = false;
console.error("ROS Bridge WebSocket error:", error);
setRosConnected(false);
setRosConnecting(false);
setRosError(
"Failed to connect to ROS bridge - check if rosbridge_server is running",
);
setRobotStatus((prev) => ({ ...prev, connected: false }));
};
};
const disconnectRos = () => {
console.log("Manually disconnecting from ROS Bridge");
connectionAttemptRef.current = false;
if (rosSocket) {
// Close with normal closure code to avoid error messages
rosSocket.close(1000, "User disconnected");
}
if (socketRef.current) {
socketRef.current.close(1000, "User disconnected");
}
// Clear all state
setRosSocket(null);
socketRef.current = null;
setRosConnected(false);
setRosConnecting(false);
setRosError(null);
setRobotStatus({
connected: false,
battery: 0,
position: { x: 0, y: 0, theta: 0 },
joints: {},
sensors: {},
lastUpdate: new Date(),
});
};
const rosConnected = false;
const rosConnecting = false;
const rosError = null;
const robotStatus = mockRobotStatus;
// const connectRos = () => console.log("ROS connection not implemented yet");
const disconnectRos = () =>
console.log("ROS disconnection not implemented yet");
const executeRobotAction = (
action: string,
parameters?: Record<string, unknown>,
) => console.log("Robot action:", action, parameters);
) => {
if (!rosSocket || !rosConnected) {
setRosError("Robot not connected");
return;
}
const formatTimestamp = (timestamp: Date) => {
return new Date(timestamp).toLocaleTimeString();
let message: {
op: string;
topic: string;
type: string;
msg: Record<string, unknown>;
};
switch (action) {
case "say_text":
const speechText = parameters?.text ?? "Hello from wizard interface!";
console.log("🔊 Preparing speech command:", speechText);
message = {
op: "publish",
topic: "/speech",
type: "std_msgs/String",
msg: { data: speechText },
};
console.log(
"📤 Speech message constructed:",
JSON.stringify(message, null, 2),
);
break;
case "move_forward":
case "move_backward":
case "turn_left":
case "turn_right":
const speed = (parameters?.speed as number) || 0.1;
const linear = action.includes("forward")
? speed
: action.includes("backward")
? -speed
: 0;
const angular = action.includes("left")
? speed
: action.includes("right")
? -speed
: 0;
message = {
op: "publish",
topic: "/cmd_vel",
type: "geometry_msgs/Twist",
msg: {
linear: { x: linear, y: 0, z: 0 },
angular: { x: 0, y: 0, z: angular },
},
};
break;
case "stop_movement":
message = {
op: "publish",
topic: "/cmd_vel",
type: "geometry_msgs/Twist",
msg: {
linear: { x: 0, y: 0, z: 0 },
angular: { x: 0, y: 0, z: 0 },
},
};
break;
case "head_movement":
case "turn_head":
const yaw = (parameters?.yaw as number) || 0;
const pitch = (parameters?.pitch as number) || 0;
const headSpeed = (parameters?.speed as number) || 0.3;
message = {
op: "publish",
topic: "/joint_angles",
type: "naoqi_bridge_msgs/JointAnglesWithSpeed",
msg: {
joint_names: ["HeadYaw", "HeadPitch"],
joint_angles: [yaw, pitch],
speed: headSpeed,
},
};
break;
case "play_animation":
const animation = (parameters?.animation as string) ?? "Hello";
message = {
op: "publish",
topic: "/naoqi_driver/animation",
type: "std_msgs/String",
msg: { data: animation },
};
break;
default:
setRosError(`Unknown action: ${String(action)}`);
return;
}
try {
const messageStr = JSON.stringify(message);
console.log("📡 Sending to ROS Bridge:", messageStr);
rosSocket.send(messageStr);
console.log(`✅ Sent robot action: ${action}`, parameters);
} catch (error) {
console.error("❌ Failed to send command:", error);
setRosError(`Failed to send command: ${String(error)}`);
}
};
const subscribeToTopic = (topic: string, messageType: string) => {
if (!rosSocket || !rosConnected) {
setRosError("Cannot subscribe - not connected");
return;
}
try {
const subscribeMsg = {
op: "subscribe",
topic: topic,
type: messageType,
};
rosSocket.send(JSON.stringify(subscribeMsg));
console.log(`Manually subscribed to ${topic}`);
} catch (error) {
setRosError(`Failed to subscribe to ${topic}: ${String(error)}`);
}
};
// Don't close connection on unmount to prevent disconnection issues
// Connection will persist across component re-renders
// Removed auto-reconnect to prevent interference with manual connections
const formatTimestamp = React.useCallback((timestamp: Date) => {
return new Date(timestamp).toLocaleTimeString();
}, []);
const getEventIcon = (eventType: string) => {
switch (eventType.toLowerCase()) {
case "trial_started":
@@ -370,16 +677,36 @@ export function WizardMonitoringPanel({
<span className="text-muted-foreground text-xs">
ROS Bridge
</span>
<Badge
variant={rosConnected ? "default" : "outline"}
className="text-xs"
>
{rosConnecting
? "Connecting..."
: rosConnected
? "Connected"
: "Offline"}
</Badge>
<div className="flex items-center gap-1">
<Badge
variant={
rosConnected
? "default"
: rosError
? "destructive"
: "outline"
}
className="text-xs"
>
{rosConnecting
? "Connecting..."
: rosConnected
? "Ready"
: rosError
? "Failed"
: "Offline"}
</Badge>
{rosConnected && (
<span className="animate-pulse text-xs text-green-600">
</span>
)}
{rosConnecting && (
<span className="animate-spin text-xs text-blue-600">
</span>
)}
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs">
@@ -387,12 +714,18 @@ export function WizardMonitoringPanel({
</span>
<div className="flex items-center gap-1">
<span className="text-xs">
{robotStatus
? `${Math.round(robotStatus.battery * 100)}%`
: "--"}
{robotStatus && robotStatus.battery > 0
? `${Math.round(robotStatus.battery)}%`
: rosConnected
? "Reading..."
: "No data"}
</span>
<Progress
value={robotStatus ? robotStatus.battery * 100 : 0}
value={
robotStatus && robotStatus.battery > 0
? robotStatus.battery
: 0
}
className="h-1 w-8"
/>
</div>
@@ -426,13 +759,15 @@ export function WizardMonitoringPanel({
size="sm"
variant="outline"
className="w-full text-xs"
onClick={() =>
console.log("Connect robot (not implemented)")
}
disabled={true}
onClick={connectRos}
disabled={rosConnecting || rosConnected}
>
<Bot className="mr-1 h-3 w-3" />
Connect Robot (Coming Soon)
{rosConnecting
? "Connecting..."
: rosConnected
? "Connected ✓"
: "Connect to NAO6"}
</Button>
) : (
<Button
@@ -442,7 +777,7 @@ export function WizardMonitoringPanel({
onClick={disconnectRos}
>
<PowerOff className="mr-1 h-3 w-3" />
Disconnect Robot
Disconnect
</Button>
)}
</div>
@@ -451,7 +786,29 @@ export function WizardMonitoringPanel({
<Alert variant="destructive" className="mt-2">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-xs">
ROS Error: {rosError}
{rosError}
</AlertDescription>
</Alert>
)}
{/* Connection Help */}
{!rosConnected && !rosConnecting && (
<Alert className="mt-2">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-xs">
<div className="space-y-1">
<div className="font-medium">Troubleshooting:</div>
<div>
1. Check ROS Bridge:{" "}
<code className="bg-muted rounded px-1 text-xs">
telnet 134.82.159.25 9090
</code>
</div>
<div>2. NAO6 must be awake and connected</div>
<div>
3. Try: Click Connect Wait 2s Test Speech
</div>
</div>
</AlertDescription>
</Alert>
)}
@@ -538,48 +895,119 @@ export function WizardMonitoringPanel({
</div>
</div>
{/* Quick Robot Actions */}
{/* Manual Subscription Controls */}
{rosConnected && (
<div className="space-y-2">
<div className="text-sm font-medium">Quick Actions</div>
<div className="grid grid-cols-2 gap-1">
<div className="text-sm font-medium">Manual Controls</div>
{/* Connection Test */}
<div className="grid grid-cols-1 gap-1">
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() =>
executeRobotAction("say_text", {
text: "Hello from wizard!",
text: "Connection test - can you hear me?",
})
}
disabled={!rosConnected}
>
Say Hello
{rosConnected ? "🔊 Test Speech" : "🔊 Not Ready"}
</Button>
</div>
{/* Topic Subscriptions */}
<div className="space-y-1">
<div className="text-muted-foreground text-xs font-medium">
Subscribe to Topics:
</div>
<div className="grid grid-cols-1 gap-1">
<Button
size="sm"
variant="ghost"
className="justify-start text-xs"
onClick={() =>
subscribeToTopic(
"/naoqi_driver/battery",
"naoqi_bridge_msgs/Battery",
)
}
>
🔋 Battery Status
</Button>
<Button
size="sm"
variant="ghost"
className="justify-start text-xs"
onClick={() =>
subscribeToTopic(
"/naoqi_driver/joint_states",
"sensor_msgs/JointState",
)
}
>
🤖 Joint States
</Button>
<Button
size="sm"
variant="ghost"
className="justify-start text-xs"
onClick={() =>
subscribeToTopic(
"/naoqi_driver/bumper",
"naoqi_bridge_msgs/Bumper",
)
}
>
👟 Bumper Sensors
</Button>
</div>
</div>
</div>
)}
{/* Quick Robot Actions */}
{rosConnected && (
<div className="space-y-2">
<div className="text-sm font-medium">Robot Actions</div>
{/* Movement Controls */}
<div className="grid grid-cols-3 gap-1">
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() =>
executeRobotAction("move_forward", { speed: 0.05 })
}
>
Forward
</Button>
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() =>
executeRobotAction("play_animation", {
animation: "Hello",
})
executeRobotAction("turn_left", { speed: 0.3 })
}
>
Wave
Turn Left
</Button>
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() =>
executeRobotAction("set_led_color", {
color: "blue",
intensity: 1.0,
})
executeRobotAction("turn_right", { speed: 0.3 })
}
>
Blue LEDs
Turn Right
</Button>
</div>
{/* Head Controls */}
<div className="grid grid-cols-3 gap-1">
<Button
size="sm"
variant="outline"
@@ -594,18 +1022,88 @@ export function WizardMonitoringPanel({
>
Center Head
</Button>
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() =>
executeRobotAction("turn_head", {
yaw: 0.5,
pitch: 0,
speed: 0.3,
})
}
>
Look Left
</Button>
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() =>
executeRobotAction("turn_head", {
yaw: -0.5,
pitch: 0,
speed: 0.3,
})
}
>
Look Right
</Button>
</div>
{/* Animation & LED Controls */}
<div className="grid grid-cols-2 gap-1">
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() =>
executeRobotAction("play_animation", {
animation: "Hello",
})
}
>
Wave Hello
</Button>
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() =>
executeRobotAction("say_text", {
text: "Experiment ready!",
})
}
>
Say Ready
</Button>
</div>
{/* Emergency Controls */}
<div className="grid grid-cols-1 gap-1">
<Button
size="sm"
variant="destructive"
className="text-xs"
onClick={() => executeRobotAction("stop_movement", {})}
>
🛑 Emergency Stop
</Button>
</div>
</div>
)}
{!rosConnected && !rosConnecting && (
<Alert className="mt-4">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-xs">
Connect to ROS bridge for live robot monitoring and
control
</AlertDescription>
</Alert>
<div className="mt-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-xs">
Connect to ROS bridge for live robot monitoring and
control
</AlertDescription>
</Alert>
</div>
)}
</div>
</ScrollArea>
@@ -669,4 +1167,6 @@ export function WizardMonitoringPanel({
</Tabs>
</div>
);
}
});
export { WizardMonitoringPanel };
Regular → Executable
View File
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
Regular → Executable
View File
View File
View File
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
Regular → Executable
View File
Regular → Executable
View File
View File

Some files were not shown because too many files have changed in this diff Show More