LeRobot-Arena / ROBOT_ARCHITECTURE.md
blanchon's picture
Mostly UI Update
18b0fa5
# 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.**