"use client"; import { useState, useEffect, useRef } from "react"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Textarea } from "~/components/ui/textarea"; import { Badge } from "~/components/ui/badge"; import { Separator } from "~/components/ui/separator"; import { Slider } from "~/components/ui/slider"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { PageHeader } from "~/components/ui/page-header"; import { PageLayout } from "~/components/ui/page-layout"; import { Play, Square, Volume2, Camera, Zap, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, RotateCcw, RotateCw, Wifi, WifiOff, AlertTriangle, CheckCircle, Activity, Battery, Eye, Hand, Footprints, } from "lucide-react"; interface RosMessage { topic: string; msg: any; type: string; } export default function NaoTestPage() { const [connectionStatus, setConnectionStatus] = useState< "disconnected" | "connecting" | "connected" | "error" >("disconnected"); const [rosSocket, setRosSocket] = useState(null); const [robotStatus, setRobotStatus] = useState(null); const [jointStates, setJointStates] = useState(null); const [speechText, setSpeechText] = useState(""); const [walkSpeed, setWalkSpeed] = useState([0.1]); const [turnSpeed, setTurnSpeed] = useState([0.3]); const [headYaw, setHeadYaw] = useState([0]); const [headPitch, setHeadPitch] = useState([0]); const [logs, setLogs] = useState([]); const [sensorData, setSensorData] = useState({}); const logsEndRef = useRef(null); const ROS_BRIDGE_URL = process.env.NEXT_PUBLIC_ROS_BRIDGE_URL || "ws://localhost:9090"; const addLog = (message: string) => { const timestamp = new Date().toLocaleTimeString(); setLogs((prev) => [...prev.slice(-49), `[${timestamp}] ${message}`]); }; useEffect(() => { logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [logs]); const connectToRos = () => { if (rosSocket?.readyState === WebSocket.OPEN) return; setConnectionStatus("connecting"); addLog("Connecting to ROS bridge..."); const socket = new WebSocket(ROS_BRIDGE_URL); socket.onopen = () => { setConnectionStatus("connected"); setRosSocket(socket); addLog("Connected to ROS bridge successfully"); // Subscribe to robot topics subscribeToTopics(socket); }; socket.onmessage = (event) => { try { const data = JSON.parse(event.data); handleRosMessage(data); } catch (error) { console.error("Error parsing ROS message:", error); } }; socket.onclose = () => { setConnectionStatus("disconnected"); setRosSocket(null); addLog("Disconnected from ROS bridge"); }; socket.onerror = () => { setConnectionStatus("error"); addLog("Error connecting to ROS bridge"); }; }; const disconnectFromRos = () => { if (rosSocket) { rosSocket.close(); setRosSocket(null); setConnectionStatus("disconnected"); addLog("Manually disconnected from ROS bridge"); } }; const subscribeToTopics = (socket: WebSocket) => { const topics = [ { topic: "/naoqi_driver/joint_states", type: "sensor_msgs/JointState" }, { topic: "/naoqi_driver/info", type: "naoqi_bridge_msgs/StringStamped" }, { topic: "/naoqi_driver/bumper", type: "naoqi_bridge_msgs/Bumper" }, { topic: "/naoqi_driver/hand_touch", type: "naoqi_bridge_msgs/HandTouch", }, { topic: "/naoqi_driver/head_touch", type: "naoqi_bridge_msgs/HeadTouch", }, { topic: "/naoqi_driver/sonar/left", type: "sensor_msgs/Range" }, { topic: "/naoqi_driver/sonar/right", type: "sensor_msgs/Range" }, ]; topics.forEach(({ topic, type }) => { const subscribeMsg = { op: "subscribe", topic, type, }; socket.send(JSON.stringify(subscribeMsg)); addLog(`Subscribed to ${topic}`); }); }; const handleRosMessage = (data: any) => { if (data.topic === "/naoqi_driver/joint_states") { setJointStates(data.msg); } else if (data.topic === "/naoqi_driver/info") { setRobotStatus(data.msg); } else if ( data.topic?.includes("bumper") || data.topic?.includes("touch") || data.topic?.includes("sonar") ) { setSensorData((prev) => ({ ...prev, [data.topic]: data.msg, })); } }; const publishMessage = (topic: string, type: string, msg: any) => { if (!rosSocket || rosSocket.readyState !== WebSocket.OPEN) { addLog("Error: Not connected to ROS bridge"); return; } const rosMsg = { op: "publish", topic, type, msg, }; rosSocket.send(JSON.stringify(rosMsg)); addLog(`Published to ${topic}: ${JSON.stringify(msg)}`); }; const sayText = () => { if (!speechText.trim()) return; publishMessage("/speech", "std_msgs/String", { data: speechText, }); setSpeechText(""); }; const walkForward = () => { publishMessage("/cmd_vel", "geometry_msgs/Twist", { linear: { x: walkSpeed[0], y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 }, }); }; const walkBackward = () => { publishMessage("/cmd_vel", "geometry_msgs/Twist", { linear: { x: -walkSpeed[0], y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 }, }); }; const turnLeft = () => { publishMessage("/cmd_vel", "geometry_msgs/Twist", { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: turnSpeed[0] }, }); }; const turnRight = () => { publishMessage("/cmd_vel", "geometry_msgs/Twist", { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: -turnSpeed[0] }, }); }; const stopMovement = () => { publishMessage("/cmd_vel", "geometry_msgs/Twist", { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 }, }); }; const moveHead = () => { publishMessage("/joint_angles", "naoqi_bridge_msgs/JointAnglesWithSpeed", { joint_names: ["HeadYaw", "HeadPitch"], joint_angles: [headYaw[0], headPitch[0]], speed: 0.3, }); }; const getConnectionStatusIcon = () => { switch (connectionStatus) { case "connected": return ; case "connecting": return ; case "error": return ; default: return ; } }; const getConnectionStatusBadge = () => { const variants = { connected: "default", connecting: "secondary", error: "destructive", disconnected: "outline", } as const; return ( {getConnectionStatusIcon()} {connectionStatus.charAt(0).toUpperCase() + connectionStatus.slice(1)} ); }; return (
{/* Connection Status */} ROS Bridge Connection {getConnectionStatusBadge()} Connect to ROS bridge at {ROS_BRIDGE_URL}
{connectionStatus === "connected" ? ( ) : ( )}
{connectionStatus === "connected" && ( Robot Control Sensor Data Robot Status Logs
{/* Speech Control */} Speech