Spaces:
Running
Running
| /** | |
| * Keyboard teleoperation controller for Node.js terminal | |
| * Handles raw keyboard input and robot position control using the keypress package. | |
| */ | |
| import * as readline from "readline"; | |
| import { SO100Follower } from "../robots/so100_follower.js"; | |
| /** | |
| * Keyboard controller for robot teleoperation | |
| * Handles terminal keyboard input and robot position updates | |
| */ | |
| export class KeyboardController { | |
| private robot: SO100Follower; | |
| private stepSize: number; | |
| private currentPositions: Record<string, number> = {}; | |
| private motorNames = [ | |
| "shoulder_pan", | |
| "shoulder_lift", | |
| "elbow_flex", | |
| "wrist_flex", | |
| "wrist_roll", | |
| "gripper", | |
| ]; | |
| private running = false; | |
| private gripperState = false; // Toggle state for gripper | |
| constructor(robot: SO100Follower, stepSize: number = 25) { | |
| this.robot = robot; | |
| this.stepSize = stepSize; | |
| } | |
| /** | |
| * Start keyboard teleoperation | |
| * Sets up raw keyboard input and initializes robot positions | |
| */ | |
| async start(): Promise<void> { | |
| console.log("Initializing keyboard controller..."); | |
| // Initialize current positions from robot | |
| try { | |
| this.currentPositions = await this.readRobotPositions(); | |
| } catch (error) { | |
| console.warn( | |
| "Could not read initial robot positions, using calibrated centers" | |
| ); | |
| // Initialize with calibrated center positions if available, otherwise use middle positions | |
| const calibratedLimits = this.robot.getCalibrationLimits(); | |
| this.motorNames.forEach((motor) => { | |
| const limits = calibratedLimits[motor]; | |
| const centerPosition = limits | |
| ? Math.floor((limits.min + limits.max) / 2) | |
| : 2047; | |
| this.currentPositions[motor] = centerPosition; | |
| }); | |
| } | |
| // Set up raw keyboard input | |
| this.setupKeyboardInput(); | |
| this.running = true; | |
| console.log("Keyboard controller ready. Use controls to move robot."); | |
| } | |
| /** | |
| * Stop keyboard teleoperation | |
| * Cleans up keyboard input handling | |
| */ | |
| async stop(): Promise<void> { | |
| this.running = false; | |
| // Reset terminal to normal mode | |
| if (process.stdin.setRawMode) { | |
| process.stdin.setRawMode(false); | |
| } | |
| process.stdin.removeAllListeners("keypress"); | |
| console.log("Keyboard controller stopped."); | |
| } | |
| /** | |
| * Get current robot positions | |
| */ | |
| async getCurrentPositions(): Promise<Record<string, number>> { | |
| return { ...this.currentPositions }; | |
| } | |
| /** | |
| * Set up keyboard input handling | |
| * Uses readline for cross-platform keyboard input | |
| */ | |
| private setupKeyboardInput(): void { | |
| // Set up raw mode for immediate key response | |
| if (process.stdin.setRawMode) { | |
| process.stdin.setRawMode(true); | |
| } | |
| process.stdin.resume(); | |
| process.stdin.setEncoding("utf8"); | |
| // Handle keyboard input | |
| process.stdin.on("data", (key: string) => { | |
| if (!this.running) return; | |
| this.handleKeyPress(key); | |
| }); | |
| } | |
| /** | |
| * Handle individual key presses | |
| * Maps keys to robot motor movements | |
| */ | |
| private async handleKeyPress(key: string): Promise<void> { | |
| let positionChanged = false; | |
| const newPositions = { ...this.currentPositions }; | |
| // Handle arrow keys first (they start with ESC but are multi-byte sequences) | |
| if (key.startsWith("\u001b[")) { | |
| const arrowKey = key.slice(2); | |
| switch (arrowKey) { | |
| case "A": // Up arrow | |
| newPositions.shoulder_lift += this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case "B": // Down arrow | |
| newPositions.shoulder_lift -= this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case "C": // Right arrow | |
| newPositions.shoulder_pan += this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case "D": // Left arrow | |
| newPositions.shoulder_pan -= this.stepSize; | |
| positionChanged = true; | |
| break; | |
| } | |
| } else { | |
| // Handle single character keys | |
| const keyCode = key.charCodeAt(0); | |
| switch (keyCode) { | |
| // Standalone ESC key (emergency stop) | |
| case 27: | |
| if (key.length === 1) { | |
| console.log("\n🛑 EMERGENCY STOP!"); | |
| await this.emergencyStop(); | |
| return; | |
| } | |
| break; | |
| // Regular character keys | |
| case 119: // 'w' | |
| newPositions.elbow_flex += this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case 115: // 's' | |
| newPositions.elbow_flex -= this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case 97: // 'a' | |
| newPositions.wrist_flex -= this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case 100: // 'd' | |
| newPositions.wrist_flex += this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case 113: // 'q' | |
| newPositions.wrist_roll -= this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case 101: // 'e' | |
| newPositions.wrist_roll += this.stepSize; | |
| positionChanged = true; | |
| break; | |
| case 32: // Space | |
| // Toggle gripper | |
| this.gripperState = !this.gripperState; | |
| newPositions.gripper = this.gripperState ? 2300 : 1800; | |
| positionChanged = true; | |
| break; | |
| // Ctrl+C | |
| case 3: | |
| console.log("\nExiting..."); | |
| process.exit(0); | |
| } | |
| } | |
| if (positionChanged) { | |
| // Apply position limits using calibration | |
| this.enforcePositionLimits(newPositions); | |
| // Update robot positions - only send changed motors for better performance | |
| try { | |
| await this.writeRobotPositions(newPositions); | |
| this.currentPositions = newPositions; | |
| } catch (error) { | |
| console.warn( | |
| `Failed to update robot positions: ${ | |
| error instanceof Error ? error.message : error | |
| }` | |
| ); | |
| } | |
| } | |
| } | |
| /** | |
| * Read current positions from robot | |
| * Uses SO100Follower position reading methods | |
| */ | |
| private async readRobotPositions(): Promise<Record<string, number>> { | |
| try { | |
| return await this.robot.getMotorPositions(); | |
| } catch (error) { | |
| console.warn( | |
| `Failed to read robot positions: ${ | |
| error instanceof Error ? error.message : error | |
| }` | |
| ); | |
| // Return default positions as fallback | |
| const positions: Record<string, number> = {}; | |
| this.motorNames.forEach((motor, index) => { | |
| positions[motor] = 2047; // STS3215 middle position | |
| }); | |
| return positions; | |
| } | |
| } | |
| /** | |
| * Write positions to robot - optimized to only send changed motors | |
| * This was the key to the smooth performance in the working version | |
| */ | |
| private async writeRobotPositions( | |
| newPositions: Record<string, number> | |
| ): Promise<void> { | |
| // Only send commands for motors that actually changed | |
| const changedPositions: Record<string, number> = {}; | |
| let hasChanges = false; | |
| for (const [motor, newPosition] of Object.entries(newPositions)) { | |
| if (Math.abs(this.currentPositions[motor] - newPosition) > 0.5) { | |
| changedPositions[motor] = newPosition; | |
| hasChanges = true; | |
| } | |
| } | |
| if (hasChanges) { | |
| await this.robot.setMotorPositions(changedPositions); | |
| } | |
| } | |
| /** | |
| * Enforce position limits based on calibration data | |
| * Uses actual calibrated limits instead of hardcoded defaults | |
| */ | |
| private enforcePositionLimits(positions: Record<string, number>): void { | |
| // Get calibrated limits from robot | |
| const calibratedLimits = this.robot.getCalibrationLimits(); | |
| for (const [motor, position] of Object.entries(positions)) { | |
| const limits = calibratedLimits[motor]; | |
| if (limits) { | |
| positions[motor] = Math.max(limits.min, Math.min(limits.max, position)); | |
| } | |
| } | |
| } | |
| /** | |
| * Emergency stop - halt all robot movement | |
| */ | |
| private async emergencyStop(): Promise<void> { | |
| try { | |
| // Stop all robot movement | |
| // TODO: Implement emergency stop in SO100Follower | |
| console.log("Emergency stop executed."); | |
| await this.stop(); | |
| process.exit(0); | |
| } catch (error) { | |
| console.error("Emergency stop failed:", error); | |
| process.exit(1); | |
| } | |
| } | |
| } | |