feat: Introduce dedicated participant, experiment, and trial detail/edit pages, enable MinIO, and refactor dashboard navigation.

This commit is contained in:
2025-12-11 20:04:52 -05:00
parent 5be4ff0372
commit d83c02759a
45 changed files with 4123 additions and 1455 deletions

View File

@@ -14,6 +14,17 @@ export interface RosMessage {
values?: Record<string, unknown>;
}
export interface ServiceRequest {
service: string;
args?: Record<string, unknown>;
}
export interface ServiceResponse {
result: boolean;
values?: Record<string, unknown>;
error?: string;
}
export interface RobotStatus {
connected: boolean;
battery: number;
@@ -405,7 +416,8 @@ export class WizardRosService extends EventEmitter {
let msg: Record<string, unknown>;
if (
config.payloadMapping.type === "template" &&
(config.payloadMapping.type === "template" ||
config.payloadMapping.type === "static") &&
config.payloadMapping.payload
) {
// Template-based payload construction
@@ -451,10 +463,15 @@ export class WizardRosService extends EventEmitter {
this.executeMovementAction(actionId, parameters);
break;
case "move_head":
case "turn_head":
this.executeTurnHead(parameters);
break;
case "move_arm":
this.executeMoveArm(parameters);
break;
case "emergency_stop":
this.publish("/cmd_vel", "geometry_msgs/Twist", {
linear: { x: 0, y: 0, z: 0 },
@@ -497,7 +514,7 @@ export class WizardRosService extends EventEmitter {
break;
}
this.publish("/cmd_vel", "geometry_msgs/Twist", { linear, angular });
this.publish("/naoqi_driver/cmd_vel", "geometry_msgs/Twist", { linear, angular });
}
/**
@@ -515,6 +532,139 @@ export class WizardRosService extends EventEmitter {
});
}
/**
* Execute arm movement
*/
private executeMoveArm(parameters: Record<string, unknown>): void {
const arm = String(parameters.arm || "Right");
const roll = Number(parameters.roll) || 0;
const pitch = Number(parameters.pitch) || 0;
const speed = Number(parameters.speed) || 0.2;
const prefix = arm === "Left" ? "L" : "R";
const jointNames = [`${prefix}ShoulderPitch`, `${prefix}ShoulderRoll`];
const jointAngles = [pitch, roll];
this.publish("/naoqi_driver/joint_angles", "naoqi_bridge_msgs/JointAnglesWithSpeed", {
joint_names: jointNames,
joint_angles: jointAngles,
speed: speed,
});
}
/**
* Call a ROS service
*/
async callService(
service: string,
args: Record<string, unknown> = {},
): Promise<ServiceResponse> {
if (!this.isConnected) {
throw new Error("Not connected to ROS bridge");
}
const id = `call_${this.messageId++}`;
return new Promise((resolve, reject) => {
const handleResponse = (message: RosMessage) => {
if (message.op === "service_response" && message.id === id) {
this.off("message", handleResponse);
if (message.result === false) {
resolve({ result: false, error: String(message.values || "Service call failed") });
} else {
resolve({ result: true, values: message.values });
}
}
};
this.on("message", handleResponse);
this.send({
op: "call_service",
service,
args,
id,
});
setTimeout(() => {
this.off("message", handleResponse);
reject(new Error("Service call timed out"));
}, 5000);
});
}
/**
* Set Autonomous Life state with fallbacks
*/
async setAutonomousLife(enabled: boolean): Promise<boolean> {
const desiredState = enabled ? "interactive" : "disabled";
// List of services to try in order
const attempts = [
// Standard NaoQi Bridge pattern
{
service: "/naoqi_driver/ALAutonomousLife/setState",
args: { state: desiredState }
},
{
service: "/naoqi_driver/ALAutonomousLife/set_state",
args: { state: desiredState }
},
// Direct module mapping
{
service: "/ALAutonomousLife/setState",
args: { state: desiredState }
},
// Shortcuts/Aliases
{
service: "/naoqi_driver/set_autonomous_life",
args: { state: desiredState }
},
{
service: "/autonomous_life/set_state",
args: { state: desiredState }
},
// Fallback: Enable/Disable topics/services
{
service: enabled ? "/life/enable" : "/life/disable",
args: {}
},
// Last resort: Generic proxy call (if available)
{
service: "/naoqi_driver/function_call",
args: {
service: "ALAutonomousLife",
function: "setState",
args: [desiredState]
}
}
];
console.log(`[WizardROS] Setting Autonomous Life to: ${desiredState}`);
for (const attempt of attempts) {
try {
console.log(`[WizardROS] Trying service: ${attempt.service}`);
const response = await this.callService(attempt.service, attempt.args);
// If the service call didn't timeout (it resolved), check result
if (response.result) {
console.log(`[WizardROS] Success via ${attempt.service}`);
return true;
} else {
// Resolved but failed? (e.g. internal error)
console.warn(`[WizardROS] Service ${attempt.service} returned false result:`, response.error);
}
} catch (error) {
// Service call failed or timed out
console.warn(`[WizardROS] Service ${attempt.service} failed/timeout:`, error);
}
}
console.error("[WizardROS] All Autonomous Life service attempts failed.");
return false;
}
/**
* Build template-based payload
*/
@@ -574,11 +724,13 @@ export class WizardRosService extends EventEmitter {
};
case "naoSpeechTransform":
case "transformToStringMessage":
return {
data: String(parameters.text || "Hello"),
};
case "naoHeadTransform":
case "transformToHeadMovement":
return {
joint_names: ["HeadYaw", "HeadPitch"],
joint_angles: [
@@ -588,6 +740,13 @@ export class WizardRosService extends EventEmitter {
speed: Number(parameters.speed) || 0.3,
};
case "transformToJointAngles":
return {
joint_names: [String(parameters.joint_name || "HeadYaw")],
joint_angles: [Number(parameters.angle) || 0],
speed: Number(parameters.speed) || 0.2,
};
default:
console.warn(`Unknown transform function: ${transformFn}`);
return parameters;