import { Robot } from "./Robot.svelte.js"; import type { JointState, USBDriverConfig, RemoteDriverConfig } from "./models.js"; import type { Position3D } from "$lib/types/positionable.js"; import { createUrdfRobot } from "@/elements/robot/createRobot.svelte.js"; import type { RobotUrdfConfig } from "$lib/types/urdf.js"; import { generateName } from "$lib/utils/generateName.js"; import { positionManager } from "$lib/utils/positionManager.js"; import { settings } from "$lib/runes/settings.svelte"; import { robotics } from "@robothub/transport-server-client"; import type { robotics as roboticsTypes } from "@robothub/transport-server-client"; import { robotUrdfConfigMap } from "$lib/configs/robotUrdfConfig"; export class RobotManager { private _robots = $state([]); // Room management state - using transport server for communication rooms = $state([]); roomsLoading = $state(false); // Reactive getters get robots(): Robot[] { return this._robots; } get robotCount(): number { return this._robots.length; } /** * Room Management Methods */ async listRooms(workspaceId: string): Promise { try { const client = new robotics.RoboticsClientCore(settings.transportServerUrl); const rooms = await client.listRooms(workspaceId); this.rooms = rooms; return rooms; } catch (error) { console.error("Failed to list robotics rooms:", error); return []; } } async refreshRooms(workspaceId: string): Promise { this.roomsLoading = true; try { await this.listRooms(workspaceId); } finally { this.roomsLoading = false; } } async createRoboticsRoom( workspaceId: string, roomId?: string ): Promise<{ success: boolean; roomId?: string; error?: string }> { try { const client = new robotics.RoboticsClientCore(settings.transportServerUrl); const result = await client.createRoom(workspaceId, roomId); // Refresh rooms list to include the new room await this.refreshRooms(workspaceId); return { success: true, roomId: result.roomId }; } catch (error) { console.error("Failed to create robotics room:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error" }; } } generateRoomId(robotId: string): string { return `${robotId}-${generateName()}`; } /** * Connect consumer to an existing robotics room as consumer * This will receive commands from producers in that room */ async connectConsumerToRoom(workspaceId: string, robotId: string, roomId: string): Promise { const robot = this.getRobot(robotId); if (!robot) { throw new Error(`Robot ${robotId} not found`); } const config: RemoteDriverConfig = { type: "remote", url: settings.transportServerUrl.replace("http://", "ws://").replace("https://", "wss://"), robotId: roomId, workspaceId: workspaceId }; // Use joinAsConsumer to join existing room await robot.joinAsConsumer(config); } /** * Connect producer to an existing robotics room as producer * This will send commands to consumers in that room */ async connectProducerToRoom(workspaceId: string, robotId: string, roomId: string): Promise { const robot = this.getRobot(robotId); if (!robot) { throw new Error(`Robot ${robotId} not found`); } const config: RemoteDriverConfig = { type: "remote", url: settings.transportServerUrl.replace("http://", "ws://").replace("https://", "wss://"), robotId: roomId, workspaceId: workspaceId }; // Use joinAsProducer to join existing room await robot.joinAsProducer(config); } /** * Create and connect producer as producer to a new room */ async connectProducerAsProducer( workspaceId: string, robotId: string, roomId?: string ): Promise<{ success: boolean; roomId?: string; error?: string }> { try { // Create room first if roomId provided, otherwise generate one const finalRoomId = roomId || this.generateRoomId(robotId); const createResult = await this.createRoboticsRoom(workspaceId, finalRoomId); if (!createResult.success) { return createResult; } // Connect producer to the new room await this.connectProducerToRoom(workspaceId, robotId, createResult.roomId!); return { success: true, roomId: createResult.roomId }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error" }; } } /** * Create a robot with the default SO-100 arm configuration */ async createSO100Robot(id?: string, position?: Position3D): Promise { const robotId = id || `so100-${Date.now()}`; const urdfConfig = robotUrdfConfigMap["so-arm100"]; return this.createRobotFromUrdf(robotId, urdfConfig, position); } /** * Create a new robot directly from URDF configuration - automatically extracts joint limits */ async createRobotFromUrdf( id: string, urdfConfig: RobotUrdfConfig, position?: Position3D ): Promise { // Check if robot already exists if (this._robots.find((r) => r.id === id)) { throw new Error(`Robot with ID ${id} already exists`); } try { // Load and parse URDF const robotState = await createUrdfRobot(urdfConfig); // Extract joint information from URDF const joints: JointState[] = []; let servoId = 1; // Auto-assign servo IDs in order for (const urdfJoint of robotState.urdfRobot.joints) { // Only include revolute joints (movable joints) if (urdfJoint.type === "revolute" && urdfJoint.name) { const jointState: JointState = { name: urdfJoint.name, value: 0, // Start at center (0%) servoId: servoId++ }; // Extract limits from URDF if available if (urdfJoint.limit) { jointState.limits = { lower: urdfJoint.limit.lower, upper: urdfJoint.limit.upper }; } joints.push(jointState); } } console.log( `Extracted ${joints.length} joints from URDF:`, joints.map( (j) => `${j.name} [${j.limits?.lower?.toFixed(2)}:${j.limits?.upper?.toFixed(2)}]` ) ); // Create robot with extracted joints AND URDF robot state const robot = new Robot(id, joints, robotState.urdfRobot); // Set position (from position manager if not provided) robot.position = position || positionManager.getNextPosition(); // Add to reactive array this._robots.push(robot); console.log(`Created robot ${id} from URDF. Total robots: ${this._robots.length}`); return robot; } catch (error) { console.error(`Failed to create robot ${id} from URDF:`, error); throw error; } } /** * Create a new robot with joints defined at initialization (for backwards compatibility) */ createRobot(id: string, joints: JointState[], position?: Position3D): Robot { // Check if robot already exists if (this._robots.find((r) => r.id === id)) { throw new Error(`Robot with ID ${id} already exists`); } // Create robot const robot = new Robot(id, joints); // Set position (from position manager if not provided) robot.position = position || positionManager.getNextPosition(); // Add to reactive array this._robots.push(robot); console.log(`Created robot ${id}. Total robots: ${this._robots.length}`); return robot; } /** * Remove a robot */ async removeRobot(id: string): Promise { const robotIndex = this._robots.findIndex((r) => r.id === id); if (robotIndex === -1) return; const robot = this._robots[robotIndex]; // Clean up robot resources await robot.destroy(); // Remove from reactive array this._robots.splice(robotIndex, 1); console.log(`Removed robot ${id}. Remaining robots: ${this._robots.length}`); } /** * Get robot by ID */ getRobot(id: string): Robot | undefined { return this._robots.find((r) => r.id === id); } /** * Clean up all robots */ async destroy(): Promise { const cleanupPromises = this._robots.map((robot) => robot.destroy()); await Promise.allSettled(cleanupPromises); this._robots.length = 0; } } // Global robot manager instance export const robotManager = new RobotManager();