Spaces:
Running
Running
| 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<Robot[]>([]); | |
| // Room management state - using transport server for communication | |
| rooms = $state<roboticsTypes.RoomInfo[]>([]); | |
| 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<roboticsTypes.RoomInfo[]> { | |
| 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<void> { | |
| 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<void> { | |
| 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<void> { | |
| 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<Robot> { | |
| 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<Robot> { | |
| // 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<void> { | |
| 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<void> { | |
| 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(); | |