LeRobot.js / packages /web /src /calibrate.ts
NERDDISCO's picture
feat: everything is using just one config
dc82a28
/**
* Web calibration functionality using Web Serial API
* Simple library API - pass in robotConnection, get calibration results
*
* Handles different robot types internally - users don't need to know about configs
*/
import { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js";
import {
readAllMotorPositions,
type MotorCommunicationPort,
} from "./utils/motor-communication.js";
import {
setHomingOffsets,
writeHardwarePositionLimits,
} from "./utils/motor-calibration.js";
import { createSO100Config } from "./robots/so100_config.js";
import type { RobotConnection } from "./types/robot-connection.js";
import type { RobotHardwareConfig } from "./types/robot-config.js";
import type {
CalibrateConfig,
WebCalibrationResults,
LiveCalibrationData,
CalibrationProcess,
} from "./types/calibration.js";
// Re-export types for external use
export type {
WebCalibrationResults,
LiveCalibrationData,
CalibrationProcess,
} from "./types/calibration.js";
/**
* Record ranges of motion with live updates
*/
async function recordRangesOfMotion(
port: MotorCommunicationPort,
motorIds: number[],
motorNames: string[],
shouldStop: () => boolean,
onLiveUpdate?: (data: LiveCalibrationData) => void
): Promise<{
rangeMins: { [motor: string]: number };
rangeMaxes: { [motor: string]: number };
}> {
const rangeMins: { [motor: string]: number } = {};
const rangeMaxes: { [motor: string]: number } = {};
// Read actual current positions
const startPositions = await readAllMotorPositions(port, motorIds);
for (let i = 0; i < motorNames.length; i++) {
const motorName = motorNames[i];
const startPosition = startPositions[i];
rangeMins[motorName] = startPosition;
rangeMaxes[motorName] = startPosition;
}
// Recording loop
while (!shouldStop()) {
try {
const positions = await readAllMotorPositions(port, motorIds);
for (let i = 0; i < motorNames.length; i++) {
const motorName = motorNames[i];
const position = positions[i];
if (position < rangeMins[motorName]) {
rangeMins[motorName] = position;
}
if (position > rangeMaxes[motorName]) {
rangeMaxes[motorName] = position;
}
}
// Call live update callback if provided
if (onLiveUpdate) {
const liveData: LiveCalibrationData = {};
for (let i = 0; i < motorNames.length; i++) {
const motorName = motorNames[i];
liveData[motorName] = {
current: positions[i],
min: rangeMins[motorName],
max: rangeMaxes[motorName],
range: rangeMaxes[motorName] - rangeMins[motorName],
};
}
onLiveUpdate(liveData);
}
} catch (error) {
// Continue recording despite errors
}
// 20fps reading rate for stable Web Serial communication
await new Promise((resolve) => setTimeout(resolve, 50));
}
return { rangeMins, rangeMaxes };
}
/**
* Apply robot-specific range adjustments
* Different robot types may have special cases (like continuous rotation motors)
*/
function applyRobotSpecificRangeAdjustments(
robotType: string,
protocol: { resolution: number },
rangeMins: { [motor: string]: number },
rangeMaxes: { [motor: string]: number }
): void {
// SO-100 specific: wrist_roll is a continuous rotation motor
if (robotType.startsWith("so100") && rangeMins["wrist_roll"] !== undefined) {
// The wrist_roll is a continuous rotation motor that should use the full
// 0-4095 range regardless of what the user recorded during calibration.
rangeMins["wrist_roll"] = 0;
rangeMaxes["wrist_roll"] = protocol.resolution - 1;
}
// Future robot types can add their own specific adjustments here
// if (robotType.startsWith('newrobot') && rangeMins["special_joint"] !== undefined) {
// rangeMins["special_joint"] = 0;
// rangeMaxes["special_joint"] = 2048;
// }
}
/**
* Main calibrate function - simple API, handles robot types internally
*/
export async function calibrate(
config: CalibrateConfig
): Promise<CalibrationProcess> {
const { robot, onLiveUpdate, onProgress } = config;
// Validate required fields
if (!robot.robotType) {
throw new Error(
"Robot type is required for calibration. Please configure the robot first."
);
}
// Create web serial port wrapper
const port = new WebSerialPortWrapper(robot.port);
await port.initialize();
// Get robot-specific configuration
let robotConfig: RobotHardwareConfig;
if (robot.robotType.startsWith("so100")) {
robotConfig = createSO100Config(robot.robotType);
} else {
throw new Error(`Unsupported robot type: ${robot.robotType}`);
}
let shouldStop = false;
const stopFunction = () => shouldStop;
// Start calibration process
const resultPromise = (async (): Promise<WebCalibrationResults> => {
// Step 1: Set homing offsets (automatic)
onProgress?.("⚙️ Setting motor homing offsets");
const homingOffsets = await setHomingOffsets(
port,
robotConfig.motorIds,
robotConfig.motorNames
);
// Step 2: Record ranges of motion with live updates
const { rangeMins, rangeMaxes } = await recordRangesOfMotion(
port,
robotConfig.motorIds,
robotConfig.motorNames,
stopFunction,
onLiveUpdate
);
// Step 3: Apply robot-specific range adjustments
applyRobotSpecificRangeAdjustments(
robot.robotType!,
robotConfig.protocol,
rangeMins,
rangeMaxes
);
// Step 4: Write hardware position limits to motors
await writeHardwarePositionLimits(
port,
robotConfig.motorIds,
robotConfig.motorNames,
rangeMins,
rangeMaxes
);
// Step 5: Compile results
const results: WebCalibrationResults = {};
for (let i = 0; i < robotConfig.motorNames.length; i++) {
const motorName = robotConfig.motorNames[i];
const motorId = robotConfig.motorIds[i];
results[motorName] = {
id: motorId,
drive_mode: robotConfig.driveModes[i],
homing_offset: homingOffsets[motorName],
range_min: rangeMins[motorName],
range_max: rangeMaxes[motorName],
};
}
return results;
})();
// Return control object
return {
stop: () => {
shouldStop = true;
},
result: resultPromise,
};
}