mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-05-08 13:58:55 -04:00
feat(nao6): add SSH-based posture actions (wake_up, rest, stand, sit, crouch)
- Update plugin with sshCommand payloadMapping type - Add server-side SSH command execution in robot-communication.ts - Add client-side SSH command execution in wizard-ros-service.ts - Update API route to handle executeSSH action
This commit is contained in:
@@ -128,6 +128,35 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ success: true });
|
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:
|
default:
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: `Unknown action: ${action}` },
|
{ error: `Unknown action: ${action}` },
|
||||||
|
|||||||
@@ -718,11 +718,18 @@ export class WizardRosService extends EventEmitter {
|
|||||||
transformFn?: string;
|
transformFn?: string;
|
||||||
service?: string;
|
service?: string;
|
||||||
args?: Record<string, unknown>;
|
args?: Record<string, unknown>;
|
||||||
|
sshCommand?: string;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
parameters: Record<string, unknown>,
|
parameters: Record<string, unknown>,
|
||||||
actionId?: string,
|
actionId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// 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
|
// Service-call actions — no topic publish involved
|
||||||
if (config.payloadMapping.type === "service") {
|
if (config.payloadMapping.type === "service") {
|
||||||
const service = config.payloadMapping.service ?? config.topic;
|
const service = config.payloadMapping.service ?? config.topic;
|
||||||
@@ -1068,6 +1075,29 @@ export class WizardRosService extends EventEmitter {
|
|||||||
console.log(`[WizardROS] Animation completed: ${animation}`);
|
console.log(`[WizardROS] Animation completed: ${animation}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an arbitrary SSH command via the API
|
||||||
|
*/
|
||||||
|
private async executeSSHCommand(command: string): Promise<void> {
|
||||||
|
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
|
* Set Autonomous Life state with fallbacks
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,6 +26,22 @@ export interface RobotAction {
|
|||||||
topic: string;
|
topic: string;
|
||||||
messageType: string;
|
messageType: string;
|
||||||
messageTemplate: Record<string, unknown>;
|
messageTemplate: Record<string, unknown>;
|
||||||
|
payloadMapping?: {
|
||||||
|
type?: string;
|
||||||
|
transformFn?: string;
|
||||||
|
payload?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
ros2?: {
|
||||||
|
topic?: string;
|
||||||
|
messageType?: string;
|
||||||
|
service?: string;
|
||||||
|
action?: string;
|
||||||
|
payloadMapping?: {
|
||||||
|
type?: string;
|
||||||
|
transformFn?: string;
|
||||||
|
payload?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,11 +254,39 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build ROS message from template
|
// Check for SSH command type
|
||||||
const message = this.buildRosMessage(
|
const sshCommand = implementation.payloadMapping?.sshCommand
|
||||||
implementation.messageTemplate,
|
|| implementation.ros2?.payloadMapping?.sshCommand;
|
||||||
parameters,
|
|
||||||
);
|
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<string, unknown>;
|
||||||
|
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
|
// Publish to ROS topic
|
||||||
this.publishToTopic(
|
this.publishToTopic(
|
||||||
@@ -268,6 +312,23 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async executeSSHCommand(command: string): Promise<void> {
|
||||||
|
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<void> {
|
private async executeAnimationViaSSH(actionType: string): Promise<void> {
|
||||||
const animationMap: Record<string, string> = {
|
const animationMap: Record<string, string> = {
|
||||||
"play_animation_bow": "animations/Stand/Gestures/BowShort_1",
|
"play_animation_bow": "animations/Stand/Gestures/BowShort_1",
|
||||||
@@ -300,6 +361,32 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
console.log(`[RobotComm] Animation result: ${stdout}`);
|
console.log(`[RobotComm] Animation result: ${stdout}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private transformToEmotionalSpeech(parameters: Record<string, unknown>): { 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<string, unknown>): Record<string, unknown> {
|
||||||
|
switch (transformFn) {
|
||||||
|
case "transformToEmotionalSpeech":
|
||||||
|
case "transformToEmotionSpeech":
|
||||||
|
return this.transformToEmotionalSpeech(parameters);
|
||||||
|
default:
|
||||||
|
console.warn(`[RobotComm] Unknown transform: ${transformFn}`);
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private buildRosMessage(
|
private buildRosMessage(
|
||||||
template: Record<string, unknown>,
|
template: Record<string, unknown>,
|
||||||
parameters: Record<string, unknown>,
|
parameters: Record<string, unknown>,
|
||||||
|
|||||||
Reference in New Issue
Block a user