mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-05-15 00:29:50 -04:00
86c1f35537
- robot-communication.ts: add sshCommand to payloadMapping type - trial-execution.ts: fix executeRobotActionWithComm to use ros2 key as implementation fallback and skip ROS connection for SSH actions - route.ts: move studyId membership check inside initialize/executeSystemAction cases so executeSSH works without studyId; fix command param location - experiments.ts: build tempId→dbUUID map on step insert and replace branch nextStepId references after all steps are saved - WizardInterface.tsx: stop filtering branch actions from step action list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
209 lines
8.4 KiB
TypeScript
209 lines
8.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { auth } from "~/lib/auth";
|
|
import { db } from "~/server/db";
|
|
import { studyMembers } from "~/server/db/schema";
|
|
import { and, eq } from "drizzle-orm";
|
|
import { exec } from "child_process";
|
|
import { promisify } from "util";
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const session = await auth.api.getSession({
|
|
headers: request.headers,
|
|
});
|
|
|
|
if (!session?.user) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
const body = await request.json();
|
|
const { action, studyId, robotId, parameters } = body;
|
|
|
|
const robotIp =
|
|
process.env.NAO_ROBOT_IP || process.env.NAO_IP || "134.82.159.168";
|
|
const password = process.env.NAO_PASSWORD || "robolab";
|
|
|
|
switch (action) {
|
|
case "initialize": {
|
|
// Requires study membership
|
|
const membership = await db.query.studyMembers.findFirst({
|
|
where: and(
|
|
eq(studyMembers.studyId, studyId),
|
|
eq(studyMembers.userId, session.user.id),
|
|
),
|
|
});
|
|
|
|
if (!membership || !["owner", "researcher"].includes(membership.role)) {
|
|
return NextResponse.json(
|
|
{ error: "Insufficient permissions" },
|
|
{ status: 403 },
|
|
);
|
|
}
|
|
|
|
console.log(`[Robots API] Initializing robot at ${robotIp}`);
|
|
|
|
const disableAlCmd = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; al = naoqi.ALProxy('ALAutonomousLife', '127.0.0.1', 9559); al.setState('disabled')\\""`;
|
|
|
|
const wakeUpCmd = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.wakeUp()\\""`;
|
|
|
|
await execAsync(disableAlCmd).catch((e) =>
|
|
console.warn("AL disable failed (non-critical/already disabled):", e),
|
|
);
|
|
|
|
await execAsync(wakeUpCmd);
|
|
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
|
|
case "executeSystemAction": {
|
|
// Requires study membership
|
|
const membership = await db.query.studyMembers.findFirst({
|
|
where: and(
|
|
eq(studyMembers.studyId, studyId),
|
|
eq(studyMembers.userId, session.user.id),
|
|
),
|
|
});
|
|
|
|
if (!membership || !["owner", "researcher"].includes(membership.role)) {
|
|
return NextResponse.json(
|
|
{ error: "Insufficient permissions" },
|
|
{ status: 403 },
|
|
);
|
|
}
|
|
|
|
const { id, parameters: actionParams } = parameters ?? {};
|
|
console.log(`[Robots API] Executing system action ${id}`);
|
|
|
|
let command = "";
|
|
|
|
switch (id) {
|
|
case "say_with_emotion":
|
|
case "say_text_with_emotion": {
|
|
const text = String(actionParams?.text || "Hello");
|
|
const emotion = String(actionParams?.emotion || "happy");
|
|
const tag =
|
|
emotion === "happy"
|
|
? "^joyful"
|
|
: emotion === "sad"
|
|
? "^sad"
|
|
: emotion === "thinking"
|
|
? "^thoughtful"
|
|
: "^joyful";
|
|
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; s = naoqi.ALProxy('ALAnimatedSpeech', '127.0.0.1', 9559); s.say('${tag} ${text.replace(/'/g, "\\'")}')\\""`;
|
|
break;
|
|
}
|
|
|
|
case "wake_up":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.wakeUp()\\""`;
|
|
break;
|
|
|
|
case "rest":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.rest()\\""`;
|
|
break;
|
|
|
|
case "play_animation_bow":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/BowShort_1'"`;
|
|
break;
|
|
|
|
case "play_animation_hey":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/Hey_1'"`;
|
|
break;
|
|
|
|
case "play_animation_show_floor":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/ShowFloor_1'"`;
|
|
break;
|
|
|
|
case "play_animation_enthusiastic":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/Enthusiastic_4'"`;
|
|
break;
|
|
|
|
case "play_animation_yes":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/Yes_1'"`;
|
|
break;
|
|
|
|
case "play_animation_no":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/No_3'"`;
|
|
break;
|
|
|
|
case "play_animation_idontknow":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/IDontKnow_1'"`;
|
|
break;
|
|
|
|
case "stand":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALRobotPosture.goToPosture Stand 0.5"`;
|
|
break;
|
|
|
|
case "stand_init":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALRobotPosture.goToPosture StandInit 0.5"`;
|
|
break;
|
|
|
|
case "sit":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALRobotPosture.goToPosture Sit 0.5"`;
|
|
break;
|
|
|
|
case "crouch":
|
|
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALRobotPosture.goToPosture Crouch 0.5"`;
|
|
break;
|
|
|
|
default:
|
|
return NextResponse.json(
|
|
{ error: `System action ${id} not implemented` },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
await execAsync(command);
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
|
|
case "executeSSH": {
|
|
// Session auth is sufficient — no studyId needed
|
|
// command may be top-level in body or nested under parameters
|
|
const { command } = parameters ?? body;
|
|
if (!command) {
|
|
return NextResponse.json(
|
|
{ error: "Missing command parameter" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
console.log(`[Robots API] Executing SSH command: ${command}`);
|
|
|
|
const sshCmd = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "nao@${robotIp}" "${command}"`;
|
|
|
|
try {
|
|
const { stdout, stderr } = await execAsync(sshCmd);
|
|
if (stderr && !stderr.includes("null") && stderr.trim()) {
|
|
console.warn(`[Robots API] SSH stderr: ${stderr}`);
|
|
}
|
|
console.log(`[Robots API] SSH result: ${stdout}`);
|
|
return NextResponse.json({ success: true, stdout, stderr });
|
|
} catch (error) {
|
|
console.error(`[Robots API] SSH command failed:`, error);
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : "SSH command failed" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
default:
|
|
return NextResponse.json(
|
|
{ error: `Unknown action: ${action}` },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("[Robots API] Error:", error);
|
|
return NextResponse.json(
|
|
{
|
|
error: error instanceof Error ? error.message : "Internal server error",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|