LeRobot.js / src /demo /lib /unified-storage.ts
NERDDISCO's picture
feat: calibration in web
737c5f6
raw
history blame
9.06 kB
// Unified storage system for robot data
// Consolidates robot config, calibration data, and metadata under one key per device
export interface UnifiedRobotData {
device_info: {
serialNumber: string;
robotType: "so100_follower" | "so100_leader";
robotId: string;
usbMetadata?: any;
lastUpdated: string;
};
calibration?: {
// Motor calibration data (from lerobot_calibration_* keys)
shoulder_pan?: {
id: number;
drive_mode: number;
homing_offset: number;
range_min: number;
range_max: number;
};
shoulder_lift?: {
id: number;
drive_mode: number;
homing_offset: number;
range_min: number;
range_max: number;
};
elbow_flex?: {
id: number;
drive_mode: number;
homing_offset: number;
range_min: number;
range_max: number;
};
wrist_flex?: {
id: number;
drive_mode: number;
homing_offset: number;
range_min: number;
range_max: number;
};
wrist_roll?: {
id: number;
drive_mode: number;
homing_offset: number;
range_min: number;
range_max: number;
};
gripper?: {
id: number;
drive_mode: number;
homing_offset: number;
range_min: number;
range_max: number;
};
// Calibration metadata (from lerobot-calibration-* keys)
metadata: {
timestamp: string;
readCount: number;
platform: string;
api: string;
device_type: string;
device_id: string;
calibrated_at: string;
};
};
}
/**
* Get unified storage key for a robot by serial number
*/
export function getUnifiedKey(serialNumber: string): string {
return `lerobotjs-${serialNumber}`;
}
/**
* Migrate data from old storage keys to unified format
* Safely combines data from three sources:
* 1. lerobot-robot-{serialNumber} - robot config
* 2. lerobot-calibration-{serialNumber} - calibration metadata
* 3. lerobot_calibration_{robotType}_{robotId} - actual calibration data
*/
export function migrateToUnifiedStorage(
serialNumber: string
): UnifiedRobotData | null {
try {
const unifiedKey = getUnifiedKey(serialNumber);
// Check if already migrated
const existing = localStorage.getItem(unifiedKey);
if (existing) {
console.log(`βœ… Data already unified for ${serialNumber}`);
return JSON.parse(existing);
}
console.log(`πŸ”„ Migrating data for serial number: ${serialNumber}`);
// 1. Get robot configuration
const robotConfigKey = `lerobot-robot-${serialNumber}`;
const robotConfigRaw = localStorage.getItem(robotConfigKey);
if (!robotConfigRaw) {
return null;
}
const robotConfig = JSON.parse(robotConfigRaw);
console.log(`πŸ“‹ Found robot config:`, robotConfig);
// 2. Get calibration metadata
const calibrationMetaKey = `lerobot-calibration-${serialNumber}`;
const calibrationMetaRaw = localStorage.getItem(calibrationMetaKey);
const calibrationMeta = calibrationMetaRaw
? JSON.parse(calibrationMetaRaw)
: null;
console.log(`πŸ“Š Found calibration metadata:`, calibrationMeta);
// 3. Get actual calibration data (using robotType and robotId from config)
const calibrationDataKey = `lerobot_calibration_${robotConfig.robotType}_${robotConfig.robotId}`;
const calibrationDataRaw = localStorage.getItem(calibrationDataKey);
const calibrationData = calibrationDataRaw
? JSON.parse(calibrationDataRaw)
: null;
console.log(`πŸ”§ Found calibration data:`, calibrationData);
// 4. Build unified structure
const unifiedData: UnifiedRobotData = {
device_info: {
serialNumber: robotConfig.serialNumber || serialNumber,
robotType: robotConfig.robotType,
robotId: robotConfig.robotId,
lastUpdated: robotConfig.lastUpdated || new Date().toISOString(),
},
};
// Add calibration if available
if (calibrationData && calibrationMeta) {
const motors: any = {};
// Copy motor data (excluding metadata fields)
Object.keys(calibrationData).forEach((key) => {
if (
![
"device_type",
"device_id",
"calibrated_at",
"platform",
"api",
].includes(key)
) {
motors[key] = calibrationData[key];
}
});
unifiedData.calibration = {
...motors,
metadata: {
timestamp: calibrationMeta.timestamp || calibrationData.calibrated_at,
readCount: calibrationMeta.readCount || 0,
platform: calibrationData.platform || "web",
api: calibrationData.api || "Web Serial API",
device_type: calibrationData.device_type || robotConfig.robotType,
device_id: calibrationData.device_id || robotConfig.robotId,
calibrated_at:
calibrationData.calibrated_at || calibrationMeta.timestamp,
},
};
}
// 5. Save unified data
localStorage.setItem(unifiedKey, JSON.stringify(unifiedData));
console.log(`βœ… Successfully unified data for ${serialNumber}`);
console.log(`πŸ“¦ Unified data:`, unifiedData);
// 6. Clean up old keys (optional - keep for now for safety)
// localStorage.removeItem(robotConfigKey);
// localStorage.removeItem(calibrationMetaKey);
// localStorage.removeItem(calibrationDataKey);
return unifiedData;
} catch (error) {
console.error(`❌ Failed to migrate data for ${serialNumber}:`, error);
return null;
}
}
/**
* Get unified robot data
*/
export function getUnifiedRobotData(
serialNumber: string
): UnifiedRobotData | null {
const unifiedKey = getUnifiedKey(serialNumber);
// Try to get existing unified data
const existing = localStorage.getItem(unifiedKey);
if (existing) {
try {
return JSON.parse(existing);
} catch (error) {
console.warn(`Failed to parse unified data for ${serialNumber}:`, error);
}
}
return null;
}
/**
* Save robot configuration to unified storage
*/
export function saveRobotConfig(
serialNumber: string,
robotType: "so100_follower" | "so100_leader",
robotId: string,
usbMetadata?: any
): void {
const unifiedKey = getUnifiedKey(serialNumber);
const existing =
getUnifiedRobotData(serialNumber) || ({} as UnifiedRobotData);
existing.device_info = {
serialNumber,
robotType,
robotId,
usbMetadata,
lastUpdated: new Date().toISOString(),
};
localStorage.setItem(unifiedKey, JSON.stringify(existing));
console.log(`πŸ’Ύ Saved robot config for ${serialNumber}`);
}
/**
* Save calibration data to unified storage
*/
export function saveCalibrationData(
serialNumber: string,
calibrationData: any,
metadata: { timestamp: string; readCount: number }
): void {
const unifiedKey = getUnifiedKey(serialNumber);
const existing =
getUnifiedRobotData(serialNumber) || ({} as UnifiedRobotData);
// Ensure device_info exists
if (!existing.device_info) {
console.warn(
`No device info found for ${serialNumber}, cannot save calibration`
);
return;
}
// Extract motor data (exclude metadata fields)
const motors: any = {};
Object.keys(calibrationData).forEach((key) => {
if (
![
"device_type",
"device_id",
"calibrated_at",
"platform",
"api",
].includes(key)
) {
motors[key] = calibrationData[key];
}
});
existing.calibration = {
...motors,
metadata: {
timestamp: metadata.timestamp,
readCount: metadata.readCount,
platform: calibrationData.platform || "web",
api: calibrationData.api || "Web Serial API",
device_type:
calibrationData.device_type || existing.device_info.robotType,
device_id: calibrationData.device_id || existing.device_info.robotId,
calibrated_at: calibrationData.calibrated_at || metadata.timestamp,
},
};
localStorage.setItem(unifiedKey, JSON.stringify(existing));
console.log(`πŸ”§ Saved calibration data for ${serialNumber}`);
}
/**
* Check if robot is calibrated
*/
export function isRobotCalibrated(serialNumber: string): boolean {
const data = getUnifiedRobotData(serialNumber);
return !!data?.calibration?.metadata?.timestamp;
}
/**
* Get calibration status for dashboard
*/
export function getCalibrationStatus(
serialNumber: string
): { timestamp: string; readCount: number } | null {
const data = getUnifiedRobotData(serialNumber);
if (data?.calibration?.metadata) {
return {
timestamp: data.calibration.metadata.timestamp,
readCount: data.calibration.metadata.readCount,
};
}
return null;
}
/**
* Get robot configuration
*/
export function getRobotConfig(
serialNumber: string
): { robotType: string; robotId: string } | null {
const data = getUnifiedRobotData(serialNumber);
if (data?.device_info) {
return {
robotType: data.device_info.robotType,
robotId: data.device_info.robotId,
};
}
return null;
}