/** * Web teleoperation functionality using Web Serial API */ import { createSO100Config } from "./robots/so100_config.js"; import type { RobotHardwareConfig } from "./types/robot-config.js"; import type { RobotConnection } from "./types/robot-connection.js"; import { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js"; import { readMotorPosition, writeMotorPosition, type MotorCommunicationPort, } from "./utils/motor-communication.js"; import type { MotorConfig, TeleoperationState, TeleoperationProcess, TeleoperateConfig, TeleoperatorConfig, DirectTeleoperatorConfig, } from "./types/teleoperation.js"; import { KeyboardTeleoperator, DirectTeleoperator, type WebTeleoperator, } from "./teleoperators/index.js"; // Re-export types for external use export type { MotorConfig, TeleoperationState, TeleoperationProcess, TeleoperateConfig, TeleoperatorConfig, DirectTeleoperatorConfig, } from "./types/teleoperation.js"; /** * Create motor configurations from robot hardware config * Pure function - converts robot specs to motor configs */ function createMotorConfigsFromRobotConfig( robotConfig: RobotHardwareConfig ): MotorConfig[] { return robotConfig.motorNames.map((name: string, i: number) => ({ id: robotConfig.motorIds[i], name, currentPosition: 2048, minPosition: 1024, maxPosition: 3072, })); } /** * Apply calibration data to motor configurations * Pure function - takes calibration data as parameter */ export function applyCalibrationToMotorConfigs( defaultConfigs: MotorConfig[], calibrationData: { [motorName: string]: any } ): MotorConfig[] { return defaultConfigs.map((defaultConfig) => { const calibData = calibrationData[defaultConfig.name]; if ( calibData && typeof calibData === "object" && "id" in calibData && "range_min" in calibData && "range_max" in calibData ) { // Use calibrated values but keep current position as default return { ...defaultConfig, id: calibData.id, minPosition: calibData.range_min, maxPosition: calibData.range_max, }; } return defaultConfig; }); } /** * Create appropriate teleoperator based on configuration */ async function createTeleoperator( config: TeleoperateConfig, port: MotorCommunicationPort, motorConfigs: MotorConfig[], robotHardwareConfig: RobotHardwareConfig ): Promise { switch (config.teleop.type) { case "keyboard": return new KeyboardTeleoperator( config.teleop, port, motorConfigs, robotHardwareConfig.keyboardControls, config.onStateUpdate ); case "direct": return new DirectTeleoperator( config.teleop, port, motorConfigs, config.onStateUpdate ); case "so100_leader": throw new Error("Leader arm teleoperator not yet implemented"); case "gamepad": throw new Error("Gamepad teleoperator not yet implemented"); default: throw new Error( `Unsupported teleoperator type: ${(config.teleop as any).type}` ); } } /** * Build TeleoperationState from teleoperator and motor configs */ function buildTeleoperationStateFromTeleoperator( teleoperator: WebTeleoperator ): TeleoperationState { const teleoperatorState = teleoperator.getState(); const isActive = (teleoperator as any).isActive; return { isActive: isActive || false, motorConfigs: [...teleoperator.motorConfigs], // Get fresh motor configs from teleoperator lastUpdate: Date.now(), ...teleoperatorState, }; } /** * Main teleoperate function */ export async function teleoperate( config: TeleoperateConfig ): Promise { const teleoperator = await createTeleoperatorProcess(config); const motorConfigs = teleoperator.motorConfigs; return { start: () => { teleoperator.start(); // CRITICAL: State update loop for UI synchronization // This ensures sliders and UI reflect actual motor positions when moved via keyboard if (config.onStateUpdate) { const updateLoop = () => { const state = buildTeleoperationStateFromTeleoperator(teleoperator); if (state.isActive) { config.onStateUpdate!(state); setTimeout(updateLoop, 100); // 10fps state updates - keeps sliders in sync } }; updateLoop(); } }, stop: () => teleoperator.stop(), updateKeyState: (key: string, pressed: boolean) => { // Delegate to teleoperator if it supports keyboard input if (teleoperator instanceof KeyboardTeleoperator) { teleoperator.updateKeyState(key, pressed); } }, getState: () => buildTeleoperationStateFromTeleoperator(teleoperator), teleoperator, disconnect: () => teleoperator.disconnect(), }; } /** * Create teleoperator instance (shared logic) */ async function createTeleoperatorProcess( config: TeleoperateConfig ): Promise { // Validate required fields if (!config.robot.robotType) { throw new Error( "Robot type is required for teleoperation. Please configure the robot first." ); } // Create web serial port wrapper const port = new WebSerialPortWrapper(config.robot.port); await port.initialize(); // Get robot-specific configuration let robotHardwareConfig: RobotHardwareConfig; if (config.robot.robotType.startsWith("so100")) { robotHardwareConfig = createSO100Config(config.robot.robotType); } else { throw new Error(`Unsupported robot type: ${config.robot.robotType}`); } // Create motor configs from robot hardware specs const defaultMotorConfigs = createMotorConfigsFromRobotConfig(robotHardwareConfig); // Apply calibration data if provided const motorConfigs = config.calibrationData ? applyCalibrationToMotorConfigs( defaultMotorConfigs, config.calibrationData ) : defaultMotorConfigs; // Create teleoperator const teleoperator = await createTeleoperator( config, port, motorConfigs, robotHardwareConfig ); await teleoperator.initialize(); return teleoperator; }