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.** | |