import { Robot, type ManagedJointState } from "./Robot.svelte"; import type { MasterDriver, SlaveDriver, DriverJointState, MasterDriverConfig, SlaveDriverConfig } from "$lib/types/robotDriver"; import { MockSequenceMaster, DEMO_SEQUENCES } from "./drivers/MockSequenceMaster"; import { MockSlave } from "./drivers/MockSlave"; import { USBSlave } from "./drivers/USBSlave"; import { RemoteServerMaster } from "./drivers/RemoteServerMaster"; import { RemoteServerSlave } from "./drivers/RemoteServerSlave"; import { USBMaster } from "./drivers/USBMaster"; import { createRobot } from "@/components/3d/robot/URDF/createRobot.svelte"; import type { RobotUrdfConfig } from "$lib/types/urdf"; import { getCommunicationConfig, getRobotPollingConfig, getDataProcessingConfig } from "$lib/configs/performanceConfig"; /** * Central manager for all robots with master-slave architecture * * Masters: Command sources (remote servers, scripts, manual control) * Slaves: Execution targets (physical robots, simulators) */ export class RobotManager { private _robots = $state([]); // Reactive getters get robots(): Robot[] { return this._robots; } get robotCount(): number { return this._robots.length; } get robotsWithMaster(): Robot[] { return this._robots.filter((robot) => robot.master !== undefined); } get robotsWithSlaves(): Robot[] { return this._robots.filter((robot) => robot.slaves.length > 0); } /** * Create a new robot from URDF configuration */ async createRobot(id: string, urdfConfig: RobotUrdfConfig): Promise { // Check if robot already exists if (this._robots.find((r) => r.id === id)) { throw new Error(`Robot with ID ${id} already exists`); } // Create robot state from URDF const robotState = await createRobot(urdfConfig); // Create managed robot const robot = new Robot(id, robotState); // Add to reactive array this._robots.push(robot); console.log(`Created robot ${id}. Total robots: ${this._robots.length}`); return robot; } /** * Get robot by ID */ getRobot(id: string): Robot | undefined { return this._robots.find((r) => r.id === id); } /** * 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]; // Move to rest position before removal if has connected slaves if (robot.connectedSlaves.length > 0) { try { console.log(`Removing robot ${id}: moving to rest position first`); await robot.moveToRestPosition(3000); await new Promise((resolve) => setTimeout(resolve, 500)); } catch (error) { console.warn(`Failed to move robot ${id} to rest position before removal:`, error); } } // 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}`); } // ============= MASTER MANAGEMENT ============= /** * Connect a master driver to a robot */ async connectMaster(robotId: string, masterConfig: MasterDriverConfig): Promise { const robot = this._robots.find((r) => r.id === robotId); if (!robot) { throw new Error(`Robot ${robotId} not found`); } // Create master driver instance const master = this.createMaster(masterConfig, robotId); // Connect the master await master.connect(); // Attach to robot await robot.setMaster(master); console.log(`Master ${master.name} connected to robot ${robotId}`); } /** * Disconnect a robot's master */ async disconnectMaster(robotId: string): Promise { const robot = this._robots.find((r) => r.id === robotId); if (!robot) { throw new Error(`Robot ${robotId} not found`); } await robot.removeMaster(); console.log(`Master disconnected from robot ${robotId}. Manual control restored.`); } // ============= SLAVE MANAGEMENT ============= /** * Connect a slave driver to a robot */ async connectSlave(robotId: string, slaveConfig: SlaveDriverConfig): Promise { const robot = this._robots.find((r) => r.id === robotId); if (!robot) { throw new Error(`Robot ${robotId} not found`); } // Create slave driver instance const slave = this.createSlave(slaveConfig, robot); // Add to robot (this handles connection and initialization) await robot.addSlave(slave); console.log(`Slave ${slave.name} connected to robot ${robotId}`); } /** * Disconnect a slave driver from a robot */ async disconnectSlave(robotId: string, slaveId: string): Promise { const robot = this._robots.find((r) => r.id === robotId); if (!robot) { throw new Error(`Robot ${robotId} not found`); } await robot.removeSlave(slaveId); console.log(`Slave ${slaveId} disconnected from robot ${robotId}`); } // ============= CONVENIENCE METHODS ============= /** * Connect demo sequence master to a robot */ async connectDemoSequences(robotId: string, loopMode: boolean = true): Promise { const config: MasterDriverConfig = { type: "mock-sequence", sequences: DEMO_SEQUENCES, autoStart: true, loopMode }; await this.connectMaster(robotId, config); } /** * Connect mock slave to a robot */ async connectMockSlave(robotId: string, simulateLatency: number = 50): Promise { const config: SlaveDriverConfig = { type: "mock-slave", simulateLatency, simulateErrors: false, responseDelay: 20 }; await this.connectSlave(robotId, config); } /** * Connect USB slave to a robot (when implemented) */ async connectUSBSlave(robotId: string, port?: string): Promise { const config: SlaveDriverConfig = { type: "usb-slave", port, baudRate: 115200 }; await this.connectSlave(robotId, config); } /** * Connect remote server slave to a robot */ async connectRemoteServerSlave( robotId: string, url: string = "ws://localhost:8080", apiKey?: string, targetRobotId?: string ): Promise { const config: SlaveDriverConfig = { type: "remote-server-slave", url, apiKey, robotId: targetRobotId || robotId // Use targetRobotId if provided, otherwise use local robotId }; await this.connectSlave(robotId, config); } /** * Connect USB master to a robot */ async connectUSBMaster( robotId: string, options: { port?: string; baudRate?: number; pollInterval?: number; smoothing?: boolean } = {} ): Promise { const config: MasterDriverConfig = { type: "usb-master", port: options.port, baudRate: options.baudRate || getCommunicationConfig().USB_BAUD_RATE, pollInterval: options.pollInterval || getRobotPollingConfig().USB_MASTER_POLL_INTERVAL_MS, smoothing: options.smoothing ?? getDataProcessingConfig().ENABLE_SMOOTHING }; await this.connectMaster(robotId, config); } /** * Get detailed robot status */ getRobotStatus(robotId: string): | { id: string; hasActiveMaster: boolean; masterName?: string; manualControlEnabled: boolean; connectedSlaves: number; totalSlaves: number; lastCommandSource: string; } | undefined { const robot = this._robots.find((r) => r.id === robotId); if (!robot) return undefined; return { id: robot.id, hasActiveMaster: robot.controlState.hasActiveMaster, masterName: robot.controlState.masterName, manualControlEnabled: robot.manualControlEnabled, connectedSlaves: robot.connectedSlaves.length, totalSlaves: robot.slaves.length, lastCommandSource: robot.controlState.lastCommandSource }; } /** * Get joint states from all robots */ getAllJointStates(): { robotId: string; joints: ManagedJointState[] }[] { return this._robots.map((robot) => ({ robotId: robot.id, joints: robot.joints })); } /** * Clean up all robots */ async destroy(): Promise { const cleanupPromises = this._robots.map((robot) => robot.destroy()); await Promise.allSettled(cleanupPromises); this._robots.length = 0; } // ============= DRIVER FACTORIES ============= /** * Create a master driver instance */ private createMaster(config: MasterDriverConfig, robotId: string): MasterDriver { switch (config.type) { case "mock-sequence": return new MockSequenceMaster(config); case "remote-server": return new RemoteServerMaster(config, robotId); case "script-player": // TODO: Implement ScriptPlayerMaster throw new Error("Script player master not implemented yet"); case "usb-master": return new USBMaster(config); default: { // TypeScript exhaustiveness check const _exhaustive: never = config; throw new Error( `Unknown master driver type: ${(_exhaustive as unknown as { type: string }).type}` ); } } } /** * Create a slave driver instance */ private createSlave(config: SlaveDriverConfig, robot: Robot): SlaveDriver { // Convert robot joints to driver joint states const driverJointStates: DriverJointState[] = robot.joints.map((joint) => ({ name: joint.name, servoId: joint.servoId || 0, type: joint.urdfJoint.type as "revolute" | "continuous", virtualValue: joint.virtualValue, realValue: joint.realValue, limits: joint.urdfJoint.limit ? { lower: joint.urdfJoint.limit.lower, upper: joint.urdfJoint.limit.upper, velocity: joint.urdfJoint.limit.velocity, effort: joint.urdfJoint.limit.effort } : undefined })); switch (config.type) { case "mock-slave": return new MockSlave(config, driverJointStates); case "usb-slave": return new USBSlave(config, driverJointStates); case "simulation-slave": // TODO: Implement SimulationSlave throw new Error("Simulation slave driver not implemented yet"); case "remote-server-slave": return new RemoteServerSlave(config, driverJointStates); default: { // TypeScript exhaustiveness check const _exhaustive: never = config; throw new Error( `Unknown slave driver type: ${(_exhaustive as unknown as { type: string }).type}` ); } } } } // Global robot manager instance export const robotManager = new RobotManager();