fix(trial-execution): handle SSH actions without requiring ROS connection

- Add startTime parameter to executeRobotActionInternal for proper duration tracking
- SSH actions (animations, posture commands) now work without ROS bridge connection
- Refactor executeAction to handle SSH and ROS paths separately
This commit is contained in:
2026-04-01 19:44:08 -04:00
parent 27f633fb4b
commit 3e2aa894a0
+85 -66
View File
@@ -181,41 +181,71 @@ export class RobotCommunicationService extends EventEmitter {
* Execute a robot action * Execute a robot action
*/ */
async executeAction(action: RobotAction): Promise<RobotActionResult> { async executeAction(action: RobotAction): Promise<RobotActionResult> {
const actionId = `action_${this.messageId++}`;
const startTime = Date.now();
// Check if this is an SSH-only action (animations, posture, arbitrary SSH commands)
const { implementation, actionId: actionType } = action;
const baseActionId = actionType.includes(".")
? actionType.split(".").pop()
: actionType;
const isAnimationAction = baseActionId?.startsWith("play_animation_");
const sshCommand = implementation.payloadMapping?.sshCommand
|| implementation.ros2?.payloadMapping?.sshCommand;
// SSH actions don't require ROS connection
if (isAnimationAction || sshCommand) {
const timeout = setTimeout(() => {
throw new Error(`SSH action timeout: ${action.actionId}`);
}, 30000);
try {
console.log(`[RobotComm] Executing SSH action: ${action.actionId}`);
const result = await this.executeRobotActionInternal(action, actionId, startTime);
clearTimeout(timeout);
return result;
} catch (error) {
clearTimeout(timeout);
throw error;
}
}
// Non-SSH actions require ROS connection
if (!this.isConnected) { if (!this.isConnected) {
throw new Error("Not connected to ROS bridge"); throw new Error("Not connected to ROS bridge");
} }
const startTime = Date.now(); // Store pending action
const actionId = `action_${this.messageId++}`; const pending = {
resolve: (() => {}) as (result: RobotActionResult) => void,
return new Promise((resolve, reject) => { reject: (() => {}) as (error: Error) => void,
// Set up timeout timeout: setTimeout(() => {
const timeout = setTimeout(() => {
this.pendingActions.delete(actionId); this.pendingActions.delete(actionId);
reject(new Error(`Action timeout: ${action.actionId}`)); pending.reject(new Error(`Action timeout: ${action.actionId}`));
}, 30000); // 30 second timeout }, 30000),
startTime,
};
// Store pending action this.pendingActions.set(actionId, pending);
this.pendingActions.set(actionId, {
resolve,
reject,
timeout,
startTime,
});
try { // Wrap the pending resolve/reject in a way that works with async method
// Log the action we're about to execute return new Promise<RobotActionResult>((resolve, reject) => {
console.log(`[RobotComm] Executing robot action: ${action.actionId}`); pending.resolve = resolve;
console.log(`[RobotComm] Topic: ${action.implementation.topic}`); pending.reject = reject;
console.log(`[RobotComm] Parameters:`, action.parameters);
// Execute action based on type and platform // Execute action
this.executeRobotActionInternal(action, actionId); this.executeRobotActionInternal(action, actionId, startTime)
} catch (error) { .then((result) => {
clearTimeout(timeout); clearTimeout(pending.timeout);
this.pendingActions.delete(actionId); this.pendingActions.delete(actionId);
reject(error); resolve(result);
} })
.catch((error) => {
clearTimeout(pending.timeout);
this.pendingActions.delete(actionId);
reject(error);
});
}); });
} }
@@ -228,10 +258,11 @@ export class RobotCommunicationService extends EventEmitter {
// Private methods // Private methods
private executeRobotActionInternal( private async executeRobotActionInternal(
action: RobotAction, action: RobotAction,
actionId: string, actionId: string,
): void { startTime: number,
): Promise<RobotActionResult> {
const { implementation, parameters, actionId: actionType } = action; const { implementation, parameters, actionId: actionType } = action;
// Use SSH for play_animation actions (check both namespaced and non-namespaced) // Use SSH for play_animation actions (check both namespaced and non-namespaced)
@@ -240,18 +271,12 @@ export class RobotCommunicationService extends EventEmitter {
: actionType; : actionType;
if (baseActionId?.startsWith("play_animation_")) { if (baseActionId?.startsWith("play_animation_")) {
this.executeAnimationViaSSH(baseActionId).then(() => { await this.executeAnimationViaSSH(baseActionId);
this.completeAction(actionId, { return {
success: true, success: true,
duration: duration: Date.now() - startTime,
Date.now() - data: { method: "ssh", action: baseActionId },
(this.pendingActions.get(actionId)?.startTime || Date.now()), };
data: { method: "ssh", action: baseActionId },
});
}).catch((error) => {
this.pendingActions.get(actionId)?.reject(error);
});
return;
} }
// Check for SSH command type // Check for SSH command type
@@ -259,18 +284,12 @@ export class RobotCommunicationService extends EventEmitter {
|| implementation.ros2?.payloadMapping?.sshCommand; || implementation.ros2?.payloadMapping?.sshCommand;
if (sshCommand) { if (sshCommand) {
this.executeSSHCommand(sshCommand).then(() => { await this.executeSSHCommand(sshCommand);
this.completeAction(actionId, { return {
success: true, success: true,
duration: duration: Date.now() - startTime,
Date.now() - data: { method: "ssh", command: sshCommand },
(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 // Apply transform if specified
@@ -297,19 +316,19 @@ export class RobotCommunicationService extends EventEmitter {
// For actions that complete immediately (like movement commands), // For actions that complete immediately (like movement commands),
// we simulate completion after a short delay // we simulate completion after a short delay
setTimeout(() => { return new Promise((resolve) => {
this.completeAction(actionId, { setTimeout(() => {
success: true, resolve({
duration: success: true,
Date.now() - duration: Date.now() - startTime,
(this.pendingActions.get(actionId)?.startTime || Date.now()), data: {
data: { topic: implementation.topic,
topic: implementation.topic, messageType: implementation.messageType,
messageType: implementation.messageType, message,
message, },
}, });
}); }, 100);
}, 100); });
} }
private async executeSSHCommand(command: string): Promise<void> { private async executeSSHCommand(command: string): Promise<void> {