Spaces:
Running
Running
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<Robot[]>([]); | |
// 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<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 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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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(); | |