From de1b125b132a96cdf579ac0c131838c5555691d8 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Wed, 1 Apr 2026 18:51:40 -0400 Subject: [PATCH] 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. --- src/app/api/robots/command/route.ts | 36 ++++++++++++++ src/server/services/robot-communication.ts | 56 +++++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/app/api/robots/command/route.ts b/src/app/api/robots/command/route.ts index d88c757..c570991 100644 --- a/src/app/api/robots/command/route.ts +++ b/src/app/api/robots/command/route.ts @@ -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()\\""`; 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: return NextResponse.json( { error: `System action ${id} not implemented` }, diff --git a/src/server/services/robot-communication.ts b/src/server/services/robot-communication.ts index 1746b22..69fed09 100755 --- a/src/server/services/robot-communication.ts +++ b/src/server/services/robot-communication.ts @@ -6,6 +6,10 @@ import WebSocket from "ws"; import { EventEmitter } from "events"; +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); export interface RobotCommunicationConfig { rosBridgeUrl: string; @@ -212,7 +216,23 @@ export class RobotCommunicationService extends EventEmitter { action: RobotAction, actionId: string, ): 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 const message = this.buildRosMessage( @@ -244,6 +264,40 @@ export class RobotCommunicationService extends EventEmitter { }, 100); } + private async executeAnimationViaSSH(actionType: string): Promise { + const animationMap: Record = { + "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( template: Record, parameters: Record,