diff --git a/src/app/api/robots/command/route.ts b/src/app/api/robots/command/route.ts index 8e21e71..b781ed6 100644 --- a/src/app/api/robots/command/route.ts +++ b/src/app/api/robots/command/route.ts @@ -128,6 +128,35 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true }); } + case "executeSSH": { + const { command } = parameters ?? {}; + 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}` }, diff --git a/src/lib/ros/wizard-ros-service.ts b/src/lib/ros/wizard-ros-service.ts index 937051e..94d1323 100644 --- a/src/lib/ros/wizard-ros-service.ts +++ b/src/lib/ros/wizard-ros-service.ts @@ -718,11 +718,18 @@ export class WizardRosService extends EventEmitter { transformFn?: string; service?: string; args?: Record; + sshCommand?: string; }; }, parameters: Record, actionId?: string, ): Promise { + // SSH command actions + if (config.payloadMapping.type === "ssh" && config.payloadMapping.sshCommand) { + await this.executeSSHCommand(config.payloadMapping.sshCommand); + return; + } + // Service-call actions — no topic publish involved if (config.payloadMapping.type === "service") { const service = config.payloadMapping.service ?? config.topic; @@ -1068,6 +1075,29 @@ export class WizardRosService extends EventEmitter { console.log(`[WizardROS] Animation completed: ${animation}`); } + /** + * Execute an arbitrary SSH command via the API + */ + private async executeSSHCommand(command: string): Promise { + console.log(`[WizardROS] Executing SSH command: ${command}`); + + const response = await fetch("/api/robots/command", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "executeSSH", + command, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`SSH command failed: ${error}`); + } + + console.log(`[WizardROS] SSH command completed: ${command}`); + } + /** * Set Autonomous Life state with fallbacks */ diff --git a/src/server/services/robot-communication.ts b/src/server/services/robot-communication.ts index 9617c87..3e8409e 100755 --- a/src/server/services/robot-communication.ts +++ b/src/server/services/robot-communication.ts @@ -26,6 +26,22 @@ export interface RobotAction { topic: string; messageType: string; messageTemplate: Record; + payloadMapping?: { + type?: string; + transformFn?: string; + payload?: Record; + }; + ros2?: { + topic?: string; + messageType?: string; + service?: string; + action?: string; + payloadMapping?: { + type?: string; + transformFn?: string; + payload?: Record; + }; + }; }; } @@ -238,11 +254,39 @@ export class RobotCommunicationService extends EventEmitter { return; } - // Build ROS message from template - const message = this.buildRosMessage( - implementation.messageTemplate, - parameters, - ); + // Check for SSH command type + const sshCommand = implementation.payloadMapping?.sshCommand + || implementation.ros2?.payloadMapping?.sshCommand; + + if (sshCommand) { + this.executeSSHCommand(sshCommand).then(() => { + this.completeAction(actionId, { + success: true, + duration: + Date.now() - + (this.pendingActions.get(actionId)?.startTime || Date.now()), + data: { method: "ssh", command: sshCommand }, + }); + }).catch((error) => { + this.pendingActions.get(actionId)?.reject(error); + }); + return; + } + + // Apply transform if specified + let message: Record; + const transformFn = implementation.payloadMapping?.transformFn + || implementation.ros2?.payloadMapping?.transformFn; + + if (transformFn) { + message = this.applyTransform(transformFn, parameters); + } else { + // Build ROS message from template + message = this.buildRosMessage( + implementation.messageTemplate, + parameters, + ); + } // Publish to ROS topic this.publishToTopic( @@ -268,6 +312,23 @@ export class RobotCommunicationService extends EventEmitter { }, 100); } + private async executeSSHCommand(command: string): Promise { + const robotIp = process.env.NAO_IP || "134.82.159.168"; + const password = process.env.NAO_PASSWORD || "robolab"; + + console.log(`[RobotComm] Executing SSH command: ${command}`); + + const sshCommand = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "nao@${robotIp}" "${command}"`; + + const { stdout, stderr } = await execAsync(sshCommand); + + if (stderr && !stderr.includes("null") && stderr.trim()) { + console.warn(`[RobotComm] SSH stderr: ${stderr}`); + } + + console.log(`[RobotComm] SSH result: ${stdout}`); + } + private async executeAnimationViaSSH(actionType: string): Promise { const animationMap: Record = { "play_animation_bow": "animations/Stand/Gestures/BowShort_1", @@ -300,6 +361,32 @@ export class RobotCommunicationService extends EventEmitter { console.log(`[RobotComm] Animation result: ${stdout}`); } + private transformToEmotionalSpeech(parameters: Record): { data: string } { + const text = String(parameters.text || "Hello"); + const emotion = String(parameters.emotion || "neutral"); + + switch (emotion) { + case "happy": + return { data: `\\rspd=120\\ ${text}` }; + case "sad": + return { data: `\\rspd=80\\ ${text}` }; + case "neutral": + default: + return { data: text }; + } + } + + private applyTransform(transformFn: string, parameters: Record): Record { + switch (transformFn) { + case "transformToEmotionalSpeech": + case "transformToEmotionSpeech": + return this.transformToEmotionalSpeech(parameters); + default: + console.warn(`[RobotComm] Unknown transform: ${transformFn}`); + return parameters; + } + } + private buildRosMessage( template: Record, parameters: Record,