/** * Base Teleoperator class for Node.js platform * Uses serialport package for serial communication * Mirrors Python lerobot/common/teleoperators/teleoperator.py */ import { SerialPort } from "serialport"; import { mkdir, writeFile } from "fs/promises"; import { join } from "path"; import type { TeleoperatorConfig } from "../types/teleoperator-config.js"; import { getCalibrationDir, TELEOPERATORS } from "../utils/constants.js"; export abstract class Teleoperator { protected port: SerialPort | null = null; protected config: TeleoperatorConfig; protected calibrationDir: string; protected calibrationPath: string; protected name: string; constructor(config: TeleoperatorConfig) { this.config = config; this.name = config.type; // Determine calibration directory // Mirrors Python: config.calibration_dir if config.calibration_dir else HF_LEROBOT_CALIBRATION / TELEOPERATORS / self.name this.calibrationDir = config.calibration_dir || join(getCalibrationDir(), TELEOPERATORS, this.name); // Use teleoperator ID or type as filename const teleopId = config.id || this.name; this.calibrationPath = join(this.calibrationDir, `${teleopId}.json`); } /** * Connect to the teleoperator * Mirrors Python teleoperator.connect() */ async connect(_calibrate: boolean = false): Promise { try { this.port = new SerialPort({ path: this.config.port, baudRate: 1000000, // Correct baud rate for Feetech motors (SO-100) - matches Python lerobot dataBits: 8, // 8 data bits - matches Python serial.EIGHTBITS stopBits: 1, // 1 stop bit - matches Python default parity: "none", // No parity - matches Python default autoOpen: false, }); // Open the port await new Promise((resolve, reject) => { this.port!.open((error) => { if (error) { reject( new Error( `Failed to open port ${this.config.port}: ${error.message}` ) ); } else { resolve(); } }); }); } catch (error) { throw new Error( `Could not connect to teleoperator on port ${this.config.port}` ); } } /** * Calibrate the teleoperator * Must be implemented by subclasses */ abstract calibrate(): Promise; /** * Disconnect from the teleoperator * Mirrors Python teleoperator.disconnect() */ async disconnect(): Promise { if (this.port && this.port.isOpen) { await new Promise((resolve) => { this.port!.close(() => { resolve(); }); }); this.port = null; } } /** * Save calibration data to JSON file * Mirrors Python's configuration saving */ protected async saveCalibration(calibrationData: any): Promise { // Ensure calibration directory exists await mkdir(this.calibrationDir, { recursive: true }); // Save calibration data as JSON await writeFile( this.calibrationPath, JSON.stringify(calibrationData, null, 2) ); console.log(`Configuration saved to: ${this.calibrationPath}`); } /** * Send command to teleoperator via serial port */ protected async sendCommand(command: string): Promise { if (!this.port || !this.port.isOpen) { throw new Error("Teleoperator not connected"); } return new Promise((resolve, reject) => { this.port!.write(command, (error) => { if (error) { reject(new Error(`Failed to send command: ${error.message}`)); } else { resolve(); } }); }); } /** * Read data from teleoperator */ protected async readData(timeout: number = 5000): Promise { if (!this.port || !this.port.isOpen) { throw new Error("Teleoperator not connected"); } return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error("Read timeout")); }, timeout); this.port!.once("data", (data: Buffer) => { clearTimeout(timer); resolve(data); }); }); } }