Spaces:
Running
Running
// 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; | |
} | |