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; | |
| } | |