LeRobot.js / packages /web /src /teleoperators /direct-teleoperator.ts
NERDDISCO's picture
feat: move teleoperators out of teleoperate into their own classes (keyboard / direct)
130bae4
/**
* Direct teleoperator for Web platform
* Handles programmatic motor control (sliders, API calls, etc.)
*/
import {
BaseWebTeleoperator,
type TeleoperatorSpecificState,
} from "./base-teleoperator.js";
import type {
DirectTeleoperatorConfig,
MotorConfig,
TeleoperationState,
} from "../types/teleoperation.js";
import type { MotorCommunicationPort } from "../utils/motor-communication.js";
import {
readMotorPosition,
writeMotorPosition,
} from "../utils/motor-communication.js";
export class DirectTeleoperator extends BaseWebTeleoperator {
private onStateUpdate?: (state: TeleoperationState) => void;
constructor(
config: DirectTeleoperatorConfig,
port: MotorCommunicationPort,
motorConfigs: MotorConfig[],
onStateUpdate?: (state: TeleoperationState) => void
) {
super(port, motorConfigs);
this.onStateUpdate = onStateUpdate;
}
async initialize(): Promise<void> {
// Read current motor positions
for (const config of this.motorConfigs) {
const position = await readMotorPosition(this.port, config.id);
if (position !== null) {
config.currentPosition = position;
}
}
}
start(): void {
this.isActive = true;
}
stop(): void {
this.isActive = false;
// Notify UI of state change
if (this.onStateUpdate) {
this.onStateUpdate(this.buildTeleoperationState());
}
}
getState(): TeleoperatorSpecificState {
return {};
}
/**
* Move motor to exact position
*/
async moveMotor(motorName: string, targetPosition: number): Promise<boolean> {
const motorConfig = this.motorConfigs.find((m) => m.name === motorName);
if (!motorConfig) return false;
const clampedPosition = Math.max(
motorConfig.minPosition,
Math.min(motorConfig.maxPosition, targetPosition)
);
try {
await writeMotorPosition(
this.port,
motorConfig.id,
Math.round(clampedPosition)
);
motorConfig.currentPosition = clampedPosition;
// Notify UI of position change
if (this.onStateUpdate) {
this.onStateUpdate(this.buildTeleoperationState());
}
return true;
} catch (error) {
console.warn(`Failed to move motor ${motorName}:`, error);
return false;
}
}
/**
* Set multiple motor positions at once
*/
async setMotorPositions(positions: {
[motorName: string]: number;
}): Promise<boolean> {
const results = await Promise.all(
Object.entries(positions).map(([motorName, position]) =>
this.moveMotor(motorName, position)
)
);
return results.every((result) => result);
}
private buildTeleoperationState(): TeleoperationState {
return {
isActive: this.isActive,
motorConfigs: [...this.motorConfigs],
lastUpdate: Date.now(),
};
}
}