Spaces:
Running
Running
| # LeRobot Arena - Robot Control Architecture v2.0 | |
| > **Master-Slave Pattern for Scalable Robot Control** | |
| > A revolutionary architecture that separates command generation (Masters) from execution (Slaves), enabling sophisticated robot control scenarios from simple manual operation to complex multi-robot coordination. | |
| ## ๐๏ธ Architecture Overview | |
| The architecture follows a **Master-Slave Pattern** with complete separation of concerns: | |
| ``` | |
| โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ | |
| โ Web Frontend โ โ RobotManager โ โ Masters โ | |
| โ โโโโโบโ โโโโโบโ โ | |
| โ โข 3D Visualization โ โข Robot Creation โ โ โข USB Master โ | |
| โ โข Manual Controlโ โ โข Master/Slave โ โ โข Remote Server โ | |
| โ โข Monitoring โ โ Orchestration โ โ โข Mock Sequence โ | |
| โ (disabled when โ โ โข State Sync โ โ (1 per robot) โ | |
| โ master active) โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ | |
| โโโโโโโโโโโโโโโโโโโ โ | |
| โผ | |
| โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ | |
| โ Robot Class โโโโโบโ Slaves โ | |
| โ โ โ โ | |
| โ โข Joint States โ โ โข USB Robot โ | |
| โ โข URDF Model โ โ โข Remote Robot โ | |
| โ โข Command Queue โ โ โข WebSocket โ | |
| โ โข Calibration โ โ (N per robot) โ | |
| โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ | |
| โ | |
| โผ | |
| โโโโโโโโโโโโโโโโโโโโ | |
| โ Python Backend โ | |
| โ โ | |
| โ โข WebSocket API โ | |
| โ โข Connection Mgr โ | |
| โ โข Robot Manager โ | |
| โโโโโโโโโโโโโโโโโโโโ | |
| ``` | |
| ### Control Flow States | |
| ``` | |
| โโโโโโโโโโโโโโโโโโโ Master Connected โโโโโโโโโโโโโโโโโโโ | |
| โ Manual Mode โ โโโโโโโโโโโโโโโโโโโโโโโบ โ Master Mode โ | |
| โ โ โ โ | |
| โ โ Panel Active โ โ โ Panel Locked โ | |
| โ โ Direct Controlโ โ โ Master Commandsโ | |
| โ โ No Master โ โ โ All Slaves Execโ | |
| โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ | |
| Master Disconnected | |
| ``` | |
| ## ๐ฏ Core Concepts | |
| ### Masters (Command Sources) | |
| **Purpose**: Generate and provide robot control commands | |
| | Type | Description | Connection | Use Case | | |
| |------|-------------|------------|----------| | |
| | **USB Master** | Physical robot as command source | USB/Serial | Teleoperation, motion teaching | | |
| | **Remote Server** | WebSocket/HTTP command reception | Network | External control systems | | |
| | **Mock Sequence** | Predefined movement patterns | Internal | Testing, demonstrations | | |
| **Key Rules**: | |
| - ๐ **Exclusive Control**: Only 1 master per robot | |
| - ๐ซ **Panel Lock**: Manual control disabled when master active | |
| - ๐ **Seamless Switch**: Masters can be swapped dynamically | |
| ### Slaves (Execution Targets) | |
| **Purpose**: Execute commands on physical or virtual robots | |
| | Type | Description | Connection | Use Case | | |
| |------|-------------|------------|----------| | |
| | **USB Slave** | Physical robot control | USB/Serial | Hardware execution | | |
| | **Remote Server Slave** | Network robot control | WebSocket | Distributed robots | | |
| | **WebSocket Slave** | Real-time WebSocket execution | WebSocket | Cloud robots | | |
| **Key Rules**: | |
| - ๐ข **Multiple Allowed**: N slaves per robot | |
| - ๐ฏ **Parallel Execution**: All slaves execute same commands | |
| - ๐ **Independent Operation**: Slaves can fail independently | |
| ### Architecture Comparison | |
| | Aspect | v1.0 (Single Driver) | v2.0 (Master-Slave) | | |
| |--------|---------------------|---------------------| | |
| | **Connection Model** | 1 Driver โ 1 Robot | 1 Master + N Slaves โ 1 Robot | | |
| | **Command Source** | Always UI Panel | Master OR UI Panel | | |
| | **Execution Targets** | Single Connection | Multiple Parallel | | |
| | **Control Hierarchy** | Flat | Hierarchical | | |
| | **Scalability** | Limited | Unlimited | | |
| ## ๐ Project Structure | |
| ### Frontend Architecture (TypeScript + Svelte) | |
| ``` | |
| src/lib/robot/ | |
| โโโ Robot.svelte.ts # Individual robot master-slave coordination | |
| โโโ RobotManager.svelte.ts # Global robot orchestration | |
| โโโ drivers/ | |
| โโโ USBMaster.ts # Physical robot as command source | |
| โโโ RemoteServerMaster.ts # WebSocket command reception | |
| โโโ USBSlave.ts # Physical robot execution | |
| โโโ RemoteServerSlave.ts # Network robot execution | |
| โโโ WebSocketSlave.ts # Real-time WebSocket execution | |
| src/lib/types/ | |
| โโโ robotDriver.ts # Master/Slave interfaces | |
| โโโ robot.ts # Robot state management | |
| ``` | |
| ### Backend Architecture (Python + FastAPI) | |
| ``` | |
| src-python/src/ | |
| โโโ main.py # FastAPI server + WebSocket endpoints | |
| โโโ robot_manager.py # Server-side robot lifecycle | |
| โโโ connection_manager.py # WebSocket connection handling | |
| โโโ models.py # Pydantic data models | |
| ``` | |
| ## ๐ฎ Usage Examples | |
| ### Basic Robot Setup | |
| ```typescript | |
| import { robotManager } from "$lib/robot/RobotManager.svelte"; | |
| // Create robot from URDF | |
| const robot = await robotManager.createRobot("demo-arm", { | |
| urdfPath: "/robots/so-arm100/robot.urdf", | |
| jointNameIdMap: { "Rotation": 1, "Pitch": 2, "Elbow": 3 }, | |
| restPosition: { "Rotation": 0, "Pitch": 0, "Elbow": 0 } | |
| }); | |
| // Add execution targets (slaves) | |
| await robotManager.connectUSBSlave("demo-arm"); // Real hardware | |
| await robotManager.connectRemoteServerSlave("demo-arm"); // Network robot | |
| // Connect command source (master) - panel becomes locked | |
| await robotManager.connectUSBMaster("demo-arm"); | |
| // Result: USB master controls both USB and Remote slaves | |
| ``` | |
| ### Master Switching Workflow | |
| ```typescript | |
| const robot = robotManager.getRobot("my-robot"); | |
| // Start with manual control | |
| console.log(robot.manualControlEnabled); // โ true | |
| // Switch to USB master (robot becomes command source) | |
| await robotManager.connectUSBMaster("my-robot"); | |
| console.log(robot.manualControlEnabled); // โ false (panel locked) | |
| // Switch to remote control | |
| await robotManager.disconnectMaster("my-robot"); | |
| await robotManager.connectMaster("my-robot", { | |
| type: "remote-server", | |
| url: "ws://robot-controller:8080/ws" | |
| }); | |
| // Restore manual control | |
| await robotManager.disconnectMaster("my-robot"); | |
| console.log(robot.manualControlEnabled); // โ true (panel restored) | |
| ``` | |
| ## ๐ Driver Implementations | |
| ### USB Master Driver | |
| **Physical robot as command source for teleoperation** | |
| ```typescript | |
| // USBMaster.ts - Core implementation | |
| export class USBMaster implements MasterDriver { | |
| readonly type = "master" as const; | |
| private feetechDriver: FeetechSerialDriver; | |
| private pollIntervalId?: number; | |
| async connect(): Promise<void> { | |
| // Initialize feetech.js serial connection | |
| this.feetechDriver = new FeetechSerialDriver({ | |
| port: this.config.port || await this.detectPort(), | |
| baudRate: this.config.baudRate || 115200 | |
| }); | |
| await this.feetechDriver.connect(); | |
| this.startPolling(); | |
| } | |
| private startPolling(): void { | |
| this.pollIntervalId = setInterval(async () => { | |
| try { | |
| // Read current joint positions from hardware | |
| const jointStates = await this.readAllJoints(); | |
| // Convert to robot commands | |
| const commands = this.convertToCommands(jointStates); | |
| // Emit commands to slaves | |
| this.notifyCommand(commands); | |
| } catch (error) { | |
| console.error('USB Master polling error:', error); | |
| } | |
| }, this.config.pollInterval || 100); | |
| } | |
| private async readAllJoints(): Promise<DriverJointState[]> { | |
| const states: DriverJointState[] = []; | |
| for (const [jointName, servoId] of Object.entries(this.jointMap)) { | |
| const position = await this.feetechDriver.readPosition(servoId); | |
| states.push({ | |
| name: jointName, | |
| servoId, | |
| type: "revolute", | |
| virtualValue: position, | |
| realValue: position | |
| }); | |
| } | |
| return states; | |
| } | |
| } | |
| ``` | |
| **Usage Pattern:** | |
| - Connect USB robot as master | |
| - Physical robot becomes the command source | |
| - Move robot manually โ slaves follow the movement | |
| - Ideal for: Teleoperation, motion teaching, demonstration recording | |
| ### USB Slave Driver | |
| **Physical robot as execution target** | |
| ```typescript | |
| // USBSlave.ts - Core implementation | |
| export class USBSlave implements SlaveDriver { | |
| readonly type = "slave" as const; | |
| private feetechDriver: FeetechSerialDriver; | |
| private calibrationOffsets: Map<string, number> = new Map(); | |
| async executeCommand(command: RobotCommand): Promise<void> { | |
| for (const joint of command.joints) { | |
| const servoId = this.getServoId(joint.name); | |
| if (!servoId) continue; | |
| // Apply calibration offset | |
| const offset = this.calibrationOffsets.get(joint.name) || 0; | |
| const adjustedValue = joint.value + offset; | |
| // Send to hardware via feetech.js | |
| await this.feetechDriver.writePosition(servoId, adjustedValue, { | |
| speed: joint.speed || 100, | |
| acceleration: 50 | |
| }); | |
| } | |
| } | |
| async readJointStates(): Promise<DriverJointState[]> { | |
| const states: DriverJointState[] = []; | |
| for (const joint of this.jointStates) { | |
| const position = await this.feetechDriver.readPosition(joint.servoId); | |
| const offset = this.calibrationOffsets.get(joint.name) || 0; | |
| states.push({ | |
| ...joint, | |
| realValue: position - offset // Remove offset for accurate state | |
| }); | |
| } | |
| return states; | |
| } | |
| async calibrate(): Promise<void> { | |
| console.log('Calibrating USB robot...'); | |
| for (const joint of this.jointStates) { | |
| // Read current hardware position | |
| const currentPos = await this.feetechDriver.readPosition(joint.servoId); | |
| // Calculate offset: desired_rest - actual_position | |
| const offset = joint.restPosition - currentPos; | |
| this.calibrationOffsets.set(joint.name, offset); | |
| console.log(`Joint ${joint.name}: offset=${offset.toFixed(1)}ยฐ`); | |
| } | |
| } | |
| } | |
| ``` | |
| **Features:** | |
| - Direct hardware control via feetech.js | |
| - Real position feedback | |
| - Calibration offset support | |
| - Smooth motion interpolation | |
| ### Remote Server Master | |
| **Network command reception via WebSocket** | |
| ```typescript | |
| // RemoteServerMaster.ts - Core implementation | |
| export class RemoteServerMaster implements MasterDriver { | |
| readonly type = "master" as const; | |
| private websocket?: WebSocket; | |
| private reconnectAttempts = 0; | |
| async connect(): Promise<void> { | |
| const wsUrl = `${this.config.url}/ws/master/${this.robotId}`; | |
| this.websocket = new WebSocket(wsUrl); | |
| this.websocket.onopen = () => { | |
| console.log(`Remote master connected: ${wsUrl}`); | |
| this.reconnectAttempts = 0; | |
| this.updateStatus({ isConnected: true }); | |
| }; | |
| this.websocket.onmessage = (event) => { | |
| try { | |
| const message = JSON.parse(event.data); | |
| this.handleServerMessage(message); | |
| } catch (error) { | |
| console.error('Failed to parse server message:', error); | |
| } | |
| }; | |
| this.websocket.onclose = () => { | |
| this.updateStatus({ isConnected: false }); | |
| this.attemptReconnect(); | |
| }; | |
| } | |
| private handleServerMessage(message: any): void { | |
| switch (message.type) { | |
| case 'command': | |
| // Convert server message to robot command | |
| const command: RobotCommand = { | |
| timestamp: Date.now(), | |
| joints: message.data.joints.map((j: any) => ({ | |
| name: j.name, | |
| value: j.value, | |
| speed: j.speed | |
| })) | |
| }; | |
| this.notifyCommand([command]); | |
| break; | |
| case 'sequence': | |
| // Handle command sequence | |
| const sequence: CommandSequence = message.data; | |
| this.notifySequence(sequence); | |
| break; | |
| } | |
| } | |
| async sendSlaveStatus(slaveStates: DriverJointState[]): Promise<void> { | |
| if (!this.websocket) return; | |
| const statusMessage = { | |
| type: 'slave_status', | |
| timestamp: new Date().toISOString(), | |
| robot_id: this.robotId, | |
| data: { | |
| joints: slaveStates.map(state => ({ | |
| name: state.name, | |
| virtual_value: state.virtualValue, | |
| real_value: state.realValue | |
| })) | |
| } | |
| }; | |
| this.websocket.send(JSON.stringify(statusMessage)); | |
| } | |
| } | |
| ``` | |
| **Protocol:** | |
| ```json | |
| // Command from server to robot | |
| { | |
| "type": "command", | |
| "timestamp": "2024-01-15T10:30:00Z", | |
| "data": { | |
| "joints": [ | |
| { "name": "Rotation", "value": 45, "speed": 100 }, | |
| { "name": "Elbow", "value": -30, "speed": 80 } | |
| ] | |
| } | |
| } | |
| // Status from robot to server | |
| { | |
| "type": "slave_status", | |
| "timestamp": "2024-01-15T10:30:01Z", | |
| "robot_id": "robot-1", | |
| "data": { | |
| "joints": [ | |
| { "name": "Rotation", "virtual_value": 45, "real_value": 44.8 }, | |
| { "name": "Elbow", "virtual_value": -30, "real_value": -29.9 } | |
| ] | |
| } | |
| } | |
| ``` | |
| ### Remote Server Slave | |
| **Network robot execution via WebSocket** | |
| ```typescript | |
| // RemoteServerSlave.ts - Core implementation | |
| export class RemoteServerSlave implements SlaveDriver { | |
| readonly type = "slave" as const; | |
| private websocket?: WebSocket; | |
| async executeCommand(command: RobotCommand): Promise<void> { | |
| if (!this.websocket) throw new Error('Not connected'); | |
| const message = { | |
| type: 'command', | |
| timestamp: new Date().toISOString(), | |
| robot_id: this.config.robotId, | |
| data: { | |
| joints: command.joints.map(j => ({ | |
| name: j.name, | |
| value: j.value, | |
| speed: j.speed | |
| })) | |
| } | |
| }; | |
| this.websocket.send(JSON.stringify(message)); | |
| // Wait for acknowledgment | |
| return new Promise((resolve, reject) => { | |
| const timeout = setTimeout(() => reject(new Error('Command timeout')), 5000); | |
| const messageHandler = (event: MessageEvent) => { | |
| const response = JSON.parse(event.data); | |
| if (response.type === 'command_ack') { | |
| clearTimeout(timeout); | |
| this.websocket?.removeEventListener('message', messageHandler); | |
| resolve(); | |
| } | |
| }; | |
| this.websocket.addEventListener('message', messageHandler); | |
| }); | |
| } | |
| async readJointStates(): Promise<DriverJointState[]> { | |
| if (!this.websocket) throw new Error('Not connected'); | |
| const message = { | |
| type: 'status_request', | |
| timestamp: new Date().toISOString(), | |
| robot_id: this.config.robotId | |
| }; | |
| this.websocket.send(JSON.stringify(message)); | |
| return new Promise((resolve, reject) => { | |
| const timeout = setTimeout(() => reject(new Error('Status timeout')), 3000); | |
| const messageHandler = (event: MessageEvent) => { | |
| const response = JSON.parse(event.data); | |
| if (response.type === 'joint_states') { | |
| clearTimeout(timeout); | |
| this.websocket?.removeEventListener('message', messageHandler); | |
| const states = response.data.joints.map((j: any) => ({ | |
| name: j.name, | |
| servoId: j.servo_id, | |
| type: j.type, | |
| virtualValue: j.virtual_value, | |
| realValue: j.real_value | |
| })); | |
| resolve(states); | |
| } | |
| }; | |
| this.websocket.addEventListener('message', messageHandler); | |
| }); | |
| } | |
| } | |
| ``` | |
| ## ๐ Command Flow Architecture | |
| ### Command Structure | |
| ```typescript | |
| interface RobotCommand { | |
| timestamp: number; | |
| joints: { | |
| name: string; | |
| value: number; // degrees for revolute, speed for continuous | |
| speed?: number; // optional movement speed | |
| }[]; | |
| duration?: number; // optional execution time | |
| metadata?: Record<string, unknown>; | |
| } | |
| ``` | |
| ### Control Flow | |
| 1. **Master Generation**: Masters generate commands from various sources | |
| 2. **Robot Routing**: Robot class routes commands to all connected slaves | |
| 3. **Parallel Execution**: All slaves execute commands simultaneously | |
| 4. **State Feedback**: Slaves report back real joint positions | |
| 5. **Synchronization**: Robot maintains synchronized state across all slaves | |
| ### State Management | |
| ```typescript | |
| // Robot.svelte.ts - Core state management | |
| export interface ManagedJointState { | |
| name: string; | |
| urdfJoint: IUrdfJoint; | |
| servoId?: number; | |
| // State values | |
| virtualValue: number; // What the UI shows | |
| realValue?: number; // What hardware reports | |
| commandedValue: number; // Last commanded value | |
| // Calibration | |
| calibrationOffset: number; // Hardware compensation | |
| restPosition: number; // Safe default position | |
| // Synchronization | |
| lastVirtualUpdate: Date; | |
| lastRealUpdate?: Date; | |
| lastCommandUpdate?: Date; | |
| } | |
| ``` | |
| ## ๐ Benefits Summary | |
| | Benefit | Description | Impact | | |
| |---------|-------------|---------| | |
| | **๐ Clear Control Hierarchy** | Masters provide commands exclusively, slaves execute in parallel | No command conflicts, predictable behavior | | |
| | **๐ Flexible Command Sources** | Easy switching between manual, automated, and remote control | Supports development, testing, and production | | |
| | **๐ก Multiple Execution Targets** | Same commands executed on multiple robots simultaneously | Real hardware + simulation testing | | |
| | **๐๏ธ Automatic Panel Management** | UI automatically adapts to master presence | Intuitive user experience | | |
| | **๐ Development Workflow** | Clear separation enables independent development | Faster iteration cycles | | |
| --- | |
| **This architecture provides unprecedented flexibility for robot control, from simple manual operation to sophisticated multi-robot coordination, all with a clean, extensible, and production-ready design.** | |