Spaces:
Running
Running
File size: 14,284 Bytes
ca45f6b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 |
# User Story 004: Keyboard Teleoperation
## Story
**As a** robotics developer using SO-100 robot arms for testing and demonstrations
**I want** to control my robot arm using keyboard keys in real-time
**So that** I can manually operate the robot, test its movements, and demonstrate its capabilities without needing a second robot arm
## Background
Keyboard teleoperation provides an immediate way to control robot arms for testing, demonstration, and manual operation. While the Python lerobot focuses primarily on leader-follower teleoperation, keyboard control is an essential feature for:
- **Quick Testing**: Verify robot functionality and range of motion
- **Demonstrations**: Show robot capabilities without complex setup
- **Development**: Test robot behavior during development
- **Troubleshooting**: Manually position robot for debugging
- **Accessibility**: Control robots without specialized hardware
The Python lerobot includes keyboard teleoperation capabilities within its teleoperator framework. We need to implement this as a standalone feature in lerobot.js, reusing our existing robot connection and calibration infrastructure from User Story 002.
This will be a "quick win" implementation that:
1. Reuses existing SO-100 robot connection logic
2. Adds keyboard input handling (Node.js terminal + Web browser)
3. Provides real-time motor control within calibrated ranges
4. Shows live position feedback and performance metrics
## Acceptance Criteria
### Core Functionality
- [ ] **Single Robot Control**: Connect to one SO-100 follower robot
- [ ] **Keyboard Input**: Arrow keys, WASD, and other keys control robot motors
- [ ] **Real-time Control**: Immediate response to keyboard input
- [ ] **Position Limits**: Respect calibrated min/max ranges from calibration data
- [ ] **Live Feedback**: Display current motor positions in real-time
- [ ] **Graceful Shutdown**: Clean disconnection on ESC or Ctrl+C
- [ ] **Cross-Platform**: Work on Windows, macOS, and Linux
- [ ] **CLI Interface**: Provide `npx lerobot teleoperate` command
### User Experience
- [ ] **Clear Controls**: Display control instructions (which keys do what)
- [ ] **Live Position Display**: Real-time motor position values
- [ ] **Performance Feedback**: Show control loop timing and responsiveness
- [ ] **Error Handling**: Handle connection failures and invalid movements gracefully
- [ ] **Emergency Stop**: ESC key immediately stops all movement
- [ ] **Smooth Control**: Responsive and intuitive robot movement
### Technical Requirements
- [ ] **Dual Platform**: Support both Node.js (CLI) and Web (browser) platforms
- [ ] **Existing Robot Reuse**: Use existing SO-100 robot connection logic from calibration
- [ ] **TypeScript**: Fully typed implementation following project conventions
- [ ] **Configuration Integration**: Load and use calibration data for position limits
- [ ] **Platform-Appropriate Input**: Terminal keyboard (Node.js) vs browser keyboard (Web)
## Expected User Flow
### Node.js CLI Keyboard Teleoperation
```bash
# Simple keyboard control
$ npx lerobot teleoperate \
--robot.type=so100_follower \
--robot.port=COM4 \
--robot.id=my_follower_arm \
--teleop.type=keyboard
Connecting to robot: so100_follower on COM4
Robot connected successfully.
Loading calibration: my_follower_arm
Starting keyboard teleoperation...
Controls:
ββ Arrow Keys: Shoulder Lift
ββ Arrow Keys: Shoulder Pan
W/S: Elbow Flex
A/D: Wrist Flex
Q/E: Wrist Roll
Space: Gripper Toggle
ESC: Emergency Stop
Ctrl+C: Exit
Press any control key to begin...
Current Positions:
shoulder_pan: 2047 (range: 985-3085)
shoulder_lift: 2047 (range: 1200-2800)
elbow_flex: 2047 (range: 1000-3000)
wrist_flex: 2047 (range: 1100-2900)
wrist_roll: 2047 (range: 0-4095)
gripper: 2047 (range: 1800-2300)
Loop: 16.67ms (60 Hz) | Status: Connected
```
### Web Browser Keyboard Teleoperation
```typescript
// In a web application
import { teleoperate } from "lerobot/web/teleoperate";
// Must be triggered by user interaction
await teleoperate({
robot: {
type: "so100_follower",
id: "my_follower_arm",
// port selected via browser dialog
},
teleop: {
type: "keyboard",
},
});
// Browser shows modern teleoperation interface with:
// - Live robot arm position visualization
// - On-screen keyboard control instructions
// - Real-time position values and ranges
// - Emergency stop button
// - Performance metrics
```
### Advanced Usage
```bash
# With custom control settings
$ npx lerobot teleoperate \
--robot.type=so100_follower \
--robot.port=COM4 \
--robot.id=my_follower_arm \
--teleop.type=keyboard \
--step_size=50 \
--fps=30
# Different step sizes for finer/coarser control
# Custom frame rates for different performance needs
```
## Implementation Details
### File Structure
```
src/lerobot/
βββ node/
β βββ teleoperate.ts # Node.js keyboard teleoperation
β βββ keyboard_teleop.ts # Node.js keyboard input handling
β βββ robots/
β βββ so100_follower.ts # Extend existing robot for teleoperation
βββ web/
βββ teleoperate.ts # Web keyboard teleoperation
βββ keyboard_teleop.ts # Web keyboard input handling
βββ robots/
βββ so100_follower.ts # Extend existing robot for teleoperation
src/demo/
βββ components/
β βββ KeyboardTeleopInterface.tsx # Keyboard teleoperation interface
β βββ RobotPositionDisplay.tsx # Live position visualization
β βββ ControlInstructions.tsx # Keyboard control help
β βββ PerformanceMonitor.tsx # Loop timing and metrics
βββ pages/
βββ KeyboardTeleop.tsx # Keyboard teleoperation demo page
src/cli/
βββ index.ts # CLI entry point (Node.js only)
```
### Key Dependencies
#### Node.js Platform
- **keypress**: For raw keyboard input in terminal
- **chalk**: For colored terminal output and status display
- **Existing robot classes**: Reuse SO-100 connection logic from calibration
#### Web Platform
- **KeyboardEvent API**: Built-in browser keyboard handling
- **Existing robot classes**: Reuse SO-100 connection logic from calibration
- **React**: For demo interface components
### Core Functions to Implement
#### Simplified Interface
```typescript
// teleoperate.ts (simplified for keyboard-only)
interface TeleoperateConfig {
robot: RobotConfig; // Reuse from calibration work
teleop: TeleoperatorConfig; // Teleoperator configuration
step_size?: number; // Default: 25 (motor position units)
fps?: number; // Default: 60
duration_s?: number | null; // Default: null (infinite)
}
interface TeleoperatorConfig {
type: "keyboard"; // Only keyboard for now, expandable later
}
async function teleoperate(config: TeleoperateConfig): Promise<void>;
// Keyboard control mappings
interface KeyboardControls {
shoulder_pan: { decrease: string; increase: string }; // left/right arrows
shoulder_lift: { decrease: string; increase: string }; // down/up arrows
elbow_flex: { decrease: string; increase: string }; // s/w
wrist_flex: { decrease: string; increase: string }; // a/d
wrist_roll: { decrease: string; increase: string }; // q/e
gripper: { toggle: string }; // space
emergency_stop: string; // esc
}
```
#### Platform-Specific Keyboard Handling
```typescript
// Node.js keyboard input
class NodeKeyboardController {
private currentPositions: Record<string, number> = {};
private robot: NodeSO100Follower;
private stepSize: number;
constructor(robot: NodeSO100Follower, stepSize: number = 25) {
this.robot = robot;
this.stepSize = stepSize;
}
async start(): Promise<void> {
process.stdin.setRawMode(true);
process.stdin.on("keypress", this.handleKeypress.bind(this));
// Initialize current positions from robot
this.currentPositions = await this.robot.getPositions();
}
private async handleKeypress(chunk: any, key: any): Promise<void> {
let positionChanged = false;
switch (key.name) {
case "up":
this.currentPositions.shoulder_lift += this.stepSize;
positionChanged = true;
break;
case "down":
this.currentPositions.shoulder_lift -= this.stepSize;
positionChanged = true;
break;
case "left":
this.currentPositions.shoulder_pan -= this.stepSize;
positionChanged = true;
break;
case "right":
this.currentPositions.shoulder_pan += this.stepSize;
positionChanged = true;
break;
// ... other key mappings
case "escape":
await this.emergencyStop();
return;
}
if (positionChanged) {
// Apply calibration limits
this.enforcePositionLimits();
// Send to robot
await this.robot.setPositions(this.currentPositions);
}
}
}
```
```typescript
// Web keyboard input
class WebKeyboardController {
private currentPositions: Record<string, number> = {};
private robot: WebSO100Follower;
private stepSize: number;
private keysPressed: Set<string> = new Set();
constructor(robot: WebSO100Follower, stepSize: number = 25) {
this.robot = robot;
this.stepSize = stepSize;
}
async start(): Promise<void> {
document.addEventListener("keydown", this.handleKeyDown.bind(this));
document.addEventListener("keyup", this.handleKeyUp.bind(this));
// Initialize current positions from robot
this.currentPositions = await this.robot.getPositions();
// Start control loop for smooth movement
this.startControlLoop();
}
private handleKeyDown(event: KeyboardEvent): void {
event.preventDefault();
this.keysPressed.add(event.code);
if (event.code === "Escape") {
this.emergencyStop();
}
}
private async startControlLoop(): Promise<void> {
setInterval(async () => {
let positionChanged = false;
// Check all pressed keys and update positions
if (this.keysPressed.has("ArrowUp")) {
this.currentPositions.shoulder_lift += this.stepSize;
positionChanged = true;
}
if (this.keysPressed.has("ArrowDown")) {
this.currentPositions.shoulder_lift -= this.stepSize;
positionChanged = true;
}
// ... other key checks
if (positionChanged) {
this.enforcePositionLimits();
await this.robot.setPositions(this.currentPositions);
}
}, 1000 / 60); // 60 FPS control loop
}
}
```
### Technical Considerations
#### Reusing Existing Robot Infrastructure
```typescript
// Extend existing robot classes instead of reimplementing
class TeleopSO100Follower extends SO100Follower {
private calibrationData: CalibrationData;
constructor(config: RobotConfig) {
super(config);
// Load calibration data from existing calibration system
this.calibrationData = loadCalibrationData(config.id);
}
async getPositions(): Promise<Record<string, number>> {
// Reuse existing position reading logic
return await this.readCurrentPositions();
}
async setPositions(positions: Record<string, number>): Promise<void> {
// Reuse existing position writing logic with validation
await this.writePositions(positions);
}
enforcePositionLimits(
positions: Record<string, number>
): Record<string, number> {
// Use calibration data to enforce limits
for (const [motor, position] of Object.entries(positions)) {
const limits = this.calibrationData[motor];
positions[motor] = Math.max(
limits.range_min,
Math.min(limits.range_max, position)
);
}
return positions;
}
}
```
#### Control Loop and Performance
```typescript
// Simple control loop focused on keyboard input
async function keyboardControlLoop(
keyboardController: KeyboardController,
robot: TeleopSO100Follower,
fps: number = 60
): Promise<void> {
while (true) {
const loopStart = performance.now();
// Keyboard controller handles input and robot updates internally
// Just need to display current status
const positions = await robot.getPositions();
displayPositions(positions);
// Frame rate control
const loopTime = performance.now() - loopStart;
const targetLoopTime = 1000 / fps;
const sleepTime = targetLoopTime - loopTime;
if (sleepTime > 0) {
await sleep(sleepTime);
}
displayPerformanceMetrics(loopTime, fps);
}
}
```
#### CLI Arguments (Simplified)
```typescript
// CLI interface matching Python structure
interface TeleoperateConfig {
robot: {
type: string; // "so100_follower"
port: string; // COM port
id?: string; // robot identifier
};
teleop: {
type: string; // "keyboard"
};
step_size?: number; // position step size per keypress
fps?: number; // control loop frame rate
}
// CLI parsing
program
.option("--robot.type <type>", "Robot type (so100_follower)")
.option("--robot.port <port>", "Robot serial port")
.option("--robot.id <id>", "Robot identifier")
.option("--teleop.type <type>", "Teleoperator type (keyboard)")
.option("--step_size <size>", "Position step size per keypress", "25")
.option("--fps <fps>", "Control loop frame rate", "60");
```
## Definition of Done
- [ ] **Functional**: Successfully controls SO-100 robot arm via keyboard input
- [ ] **CLI Ready**: `npx lerobot teleoperate` provides keyboard control
- [ ] **Intuitive Controls**: Arrow keys, WASD provide natural robot movement
- [ ] **Web Compatible**: Browser-based keyboard teleoperation with modern interface
- [ ] **Cross-Platform**: Node.js works on Windows, macOS, and Linux; Web works in Chromium browsers
- [ ] **Safety Features**: Position limits, emergency stop, connection monitoring
- [ ] **Real-time Feedback**: Live position display and performance metrics
- [ ] **Integration**: Uses existing robot connection and calibration infrastructure
- [ ] **Error Handling**: Graceful handling of connection failures and invalid movements
- [ ] **Type Safe**: Full TypeScript coverage with strict mode for both implementations
- [ ] **Quick Win**: Demonstrable keyboard robot control within minimal development time
|