File size: 5,999 Bytes
bdc1ac8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Node.js teleoperation functionality using serialport API
 * Provides both Python lerobot compatible CLI behavior and programmatic usage
 * Uses proven teleoperator classes with web-compatible API
 */

import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js";
import { createSO100Config } from "./robots/so100_config.js";
import { readAllMotorPositions } from "./utils/motor-communication.js";
import {
  KeyboardTeleoperator,
  DirectTeleoperator,
} from "./teleoperators/index.js";
import { readFile } from "fs/promises";
import { join } from "path";
import { homedir } from "os";
import { existsSync } from "fs";
import type {
  TeleoperateConfig,
  TeleoperationProcess,
  MotorConfig,
  TeleoperationState,
} from "./types/teleoperation.js";
import type { RobotConnection } from "./types/robot-connection.js";
import type { CalibrationResults } from "./types/calibration.js";

/**
 * Get calibration file path (matches Python lerobot location)
 */
function getCalibrationFilePath(robotType: string, robotId: string): string {
  const HF_HOME =
    process.env.HF_HOME || join(homedir(), ".cache", "huggingface");
  const calibrationDir = join(
    HF_HOME,
    "lerobot",
    "calibration",
    "robots",
    robotType
  );
  return join(calibrationDir, `${robotId}.json`);
}

/**
 * Load calibration data from file system
 */
async function loadCalibrationData(
  robotType: string,
  robotId: string
): Promise<CalibrationResults | null> {
  const calibrationPath = getCalibrationFilePath(robotType, robotId);

  if (!existsSync(calibrationPath)) {
    return null;
  }

  try {
    const calibrationJson = await readFile(calibrationPath, "utf-8");
    return JSON.parse(calibrationJson) as CalibrationResults;
  } catch (error) {
    console.warn(
      `Failed to load calibration data from ${calibrationPath}:`,
      error
    );
    return null;
  }
}

/**
 * Build motor configurations from robot config and calibration data
 */
function buildMotorConfigs(
  robotConfig: any,
  calibrationData?: CalibrationResults | null
): MotorConfig[] {
  const motorConfigs: MotorConfig[] = [];

  for (let i = 0; i < robotConfig.motorNames.length; i++) {
    const motorName = robotConfig.motorNames[i];
    const motorId = robotConfig.motorIds[i];

    let minPosition = 0;
    let maxPosition = robotConfig.protocol.resolution - 1; // Default full range

    // Use calibration data if available
    if (calibrationData && calibrationData[motorName]) {
      minPosition = calibrationData[motorName].range_min;
      maxPosition = calibrationData[motorName].range_max;
    }

    motorConfigs.push({
      id: motorId,
      name: motorName,
      currentPosition: Math.floor((minPosition + maxPosition) / 2), // Start at center
      minPosition,
      maxPosition,
    });
  }

  return motorConfigs;
}

/**
 * Main teleoperate function with web-compatible API
 */
export async function teleoperate(
  config: TeleoperateConfig
): Promise<TeleoperationProcess> {
  const { robot, teleop, calibrationData, onStateUpdate } = config;

  // Validate robot configuration
  if (!robot.robotType) {
    throw new Error(
      "Robot type is required for teleoperation. Please configure the robot first."
    );
  }

  if (!robot.isConnected || !robot.port) {
    throw new Error(
      "Robot is not connected. Please use findPort() to connect first."
    );
  }

  // Use the EXISTING port connection (don't create new one!)
  const port = robot.port;

  // Get robot-specific configuration
  let robotConfig;
  if (robot.robotType.startsWith("so100")) {
    robotConfig = createSO100Config(robot.robotType);
  } else {
    throw new Error(`Unsupported robot type: ${robot.robotType}`);
  }

  // Load or use provided calibration data
  let effectiveCalibrationData = calibrationData;
  if (!effectiveCalibrationData && robot.robotId) {
    effectiveCalibrationData = await loadCalibrationData(
      robot.robotType,
      robot.robotId
    );
  }

  if (!effectiveCalibrationData) {
    console.warn(
      "No calibration data found. Using default motor ranges. Consider running calibration first."
    );
  }

  // Build motor configurations
  const motorConfigs = buildMotorConfigs(robotConfig, effectiveCalibrationData);

  // Read current motor positions
  try {
    const currentPositions = await readAllMotorPositions(
      port,
      robotConfig.motorIds
    );
    for (let i = 0; i < motorConfigs.length; i++) {
      motorConfigs[i].currentPosition = currentPositions[i];
    }
  } catch (error) {
    console.warn("Failed to read initial motor positions:", error);
  }

  // Create appropriate teleoperator based on configuration
  let teleoperator;
  switch (teleop.type) {
    case "keyboard":
      teleoperator = new KeyboardTeleoperator(
        teleop,
        port,
        motorConfigs,
        robotConfig.keyboardControls,
        onStateUpdate
      );
      break;

    case "direct":
      teleoperator = new DirectTeleoperator(teleop, port, motorConfigs);
      break;

    default:
      throw new Error(`Unsupported teleoperator type: ${(teleop as any).type}`);
  }

  // Initialize teleoperator
  await teleoperator.initialize();

  // Build process object
  const process: TeleoperationProcess = {
    start(): void {
      teleoperator.start();
    },

    stop(): void {
      teleoperator.stop();
    },

    updateKeyState(key: string, pressed: boolean): void {
      if ("updateKeyState" in teleoperator) {
        (teleoperator as any).updateKeyState(key, pressed);
      }
    },

    getState(): TeleoperationState {
      const teleoperatorSpecificState = teleoperator.getState();
      return {
        isActive: teleoperator.isActiveTeleoperator,
        motorConfigs: [...teleoperator.motorConfigs],
        lastUpdate: Date.now(),
        ...teleoperatorSpecificState,
      };
    },

    teleoperator: teleoperator,

    async disconnect(): Promise<void> {
      await teleoperator.disconnect();
    },
  };

  return process;
}