mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-02-04 23:46:32 -05:00
nao6 ros2 integration updated
This commit is contained in:
0
src/components/trials/wizard/ActionControls.tsx
Normal file → Executable file
0
src/components/trials/wizard/ActionControls.tsx
Normal file → Executable file
0
src/components/trials/wizard/EventsLogSidebar.tsx
Normal file → Executable file
0
src/components/trials/wizard/EventsLogSidebar.tsx
Normal file → Executable file
0
src/components/trials/wizard/ExecutionStepDisplay.tsx
Normal file → Executable file
0
src/components/trials/wizard/ExecutionStepDisplay.tsx
Normal file → Executable file
0
src/components/trials/wizard/ParticipantInfo.tsx
Normal file → Executable file
0
src/components/trials/wizard/ParticipantInfo.tsx
Normal file → Executable file
0
src/components/trials/wizard/RobotActionsPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/RobotActionsPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/RobotStatus.tsx
Normal file → Executable file
0
src/components/trials/wizard/RobotStatus.tsx
Normal file → Executable file
0
src/components/trials/wizard/StepDisplay.tsx
Normal file → Executable file
0
src/components/trials/wizard/StepDisplay.tsx
Normal file → Executable file
0
src/components/trials/wizard/TrialProgress.tsx
Normal file → Executable file
0
src/components/trials/wizard/TrialProgress.tsx
Normal file → Executable file
64
src/components/trials/wizard/WizardInterface.tsx
Normal file → Executable file
64
src/components/trials/wizard/WizardInterface.tsx
Normal file → Executable 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;
|
||||
|
||||
0
src/components/trials/wizard/panels/ExecutionPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/ExecutionPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/MonitoringPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/MonitoringPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/TrialControlPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/TrialControlPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/WizardControlPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/WizardControlPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/WizardExecutionPanel.tsx
Normal file → Executable file
0
src/components/trials/wizard/panels/WizardExecutionPanel.tsx
Normal file → Executable file
618
src/components/trials/wizard/panels/WizardMonitoringPanel.tsx
Normal file → Executable file
618
src/components/trials/wizard/panels/WizardMonitoringPanel.tsx
Normal file → Executable 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 };
|
||||
|
||||
Reference in New Issue
Block a user