feat(nao6): add SSH-based animation execution for NAO6 robot

- Add play_animation actions to robots/command API using qicli SSH
- Add SSH-based animation execution to robot-communication service
- Animations: bow, hey, show_floor, show_sole, enthusiastic, think, yes, no, idontknow

This bypasses ROS2 cross-container issues by using direct SSH connection.
This commit is contained in:
2026-04-01 18:51:40 -04:00
parent 143cf2ce50
commit de1b125b13
2 changed files with 91 additions and 1 deletions
+36
View File
@@ -89,6 +89,42 @@ export async function POST(request: NextRequest) {
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()\\""`; 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; 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_show_sole":
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/ShowSole_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_think":
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "qicli call ALAnimationPlayer.run 'animations/Stand/Gestures/Think_1'"`;
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;
default: default:
return NextResponse.json( return NextResponse.json(
{ error: `System action ${id} not implemented` }, { error: `System action ${id} not implemented` },
+55 -1
View File
@@ -6,6 +6,10 @@
import WebSocket from "ws"; import WebSocket from "ws";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
export interface RobotCommunicationConfig { export interface RobotCommunicationConfig {
rosBridgeUrl: string; rosBridgeUrl: string;
@@ -212,7 +216,23 @@ export class RobotCommunicationService extends EventEmitter {
action: RobotAction, action: RobotAction,
actionId: string, actionId: string,
): void { ): void {
const { implementation, parameters } = action; const { implementation, parameters, actionId: actionType } = action;
// Use SSH for play_animation actions
if (actionType.startsWith("play_animation_")) {
this.executeAnimationViaSSH(actionType).then(() => {
this.completeAction(actionId, {
success: true,
duration:
Date.now() -
(this.pendingActions.get(actionId)?.startTime || Date.now()),
data: { method: "ssh", action: actionType },
});
}).catch((error) => {
this.pendingActions.get(actionId)?.reject(error);
});
return;
}
// Build ROS message from template // Build ROS message from template
const message = this.buildRosMessage( const message = this.buildRosMessage(
@@ -244,6 +264,40 @@ export class RobotCommunicationService extends EventEmitter {
}, 100); }, 100);
} }
private async executeAnimationViaSSH(actionType: string): Promise<void> {
const animationMap: Record<string, string> = {
"play_animation_bow": "animations/Stand/Gestures/BowShort_1",
"play_animation_hey": "animations/Stand/Gestures/Hey_1",
"play_animation_show_floor": "animations/Stand/Gestures/ShowFloor_1",
"play_animation_show_sole": "animations/Stand/Gestures/ShowSole_1",
"play_animation_enthusiastic": "animations/Stand/Gestures/Enthusiastic_4",
"play_animation_think": "animations/Stand/Gestures/Think_1",
"play_animation_yes": "animations/Stand/Gestures/Yes_1",
"play_animation_no": "animations/Stand/Gestures/No_3",
"play_animation_idontknow": "animations/Stand/Gestures/IDontKnow_1",
};
const animation = animationMap[actionType];
if (!animation) {
throw new Error(`Unknown animation: ${actionType}`);
}
const robotIp = process.env.NAO_IP || "134.82.159.168";
const password = process.env.NAO_PASSWORD || "robolab";
console.log(`[RobotComm] Executing animation via SSH: ${animation}`);
const command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "nao@${robotIp}" "qicli call ALAnimationPlayer.run '${animation}'"`;
const { stdout, stderr } = await execAsync(command);
if (stderr && !stderr.includes("null")) {
console.warn(`[RobotComm] SSH stderr: ${stderr}`);
}
console.log(`[RobotComm] Animation result: ${stdout}`);
}
private buildRosMessage( private buildRosMessage(
template: Record<string, unknown>, template: Record<string, unknown>,
parameters: Record<string, unknown>, parameters: Record<string, unknown>,