import type { JointCalibration, CalibrationState } from '../models.js'; import { scsServoSDK } from "feetech.js"; import { ROBOT_CONFIG } from '../config.js'; export class USBCalibrationManager { // Joint configuration private readonly jointIds = [1, 2, 3, 4, 5, 6]; private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"]; // Calibration state private jointCalibrations: Record = {}; private _calibrationState: CalibrationState = { isCalibrating: false, progress: 0 }; // Connection state for calibration private isConnectedForCalibration = false; private baudRate: number = 1000000; // Calibration polling private calibrationPollingAbortController: AbortController | null = null; private lastPositions: Record = {}; private calibrationCallbacks: (() => void)[] = []; // Calibration completion callback with final positions private calibrationCompleteCallback: ((finalPositions: Record) => void) | null = null; // Servo reading queue for calibration private isReadingServos = false; private readingQueue: Array<{ servoId: number; resolve: (value: number) => void; reject: (error: Error) => void; }> = []; constructor(baudRate: number = ROBOT_CONFIG.usb.baudRate) { this.baudRate = baudRate; // Initialize joint calibrations this.jointNames.forEach(name => { this.jointCalibrations[name] = { isCalibrated: false }; }); } // Getters get isCalibrated(): boolean { return Object.values(this.jointCalibrations).every(cal => cal.isCalibrated); } get needsCalibration(): boolean { return !this.isCalibrated; } get calibrationState(): CalibrationState { return this._calibrationState; } get jointNames_(): string[] { return [...this.jointNames]; } // Connection management for calibration async ensureConnectedForCalibration(): Promise { if (this.isConnectedForCalibration) { console.log('[USBCalibrationManager] Already connected for calibration'); return; } try { console.log('[USBCalibrationManager] Connecting SDK for calibration...'); await scsServoSDK.connect({ baudRate: this.baudRate }); this.isConnectedForCalibration = true; console.log('[USBCalibrationManager] Connected successfully for calibration'); } catch (error) { console.error('[USBCalibrationManager] Failed to connect SDK for calibration:', error); throw error; } } async disconnectFromCalibration(): Promise { if (!this.isConnectedForCalibration) return; try { await scsServoSDK.disconnect(); this.isConnectedForCalibration = false; console.log('[USBCalibrationManager] Disconnected from calibration'); } catch (error) { console.warn('[USBCalibrationManager] Failed to disconnect from calibration:', error); } } // Check if the SDK is currently connected (for external use) get isSDKConnected(): boolean { return this.isConnectedForCalibration && scsServoSDK.isConnected(); } // Calibration methods async startCalibration(): Promise { if (this._calibrationState.isCalibrating) { console.warn('[USBCalibrationManager] Calibration already in progress'); return; } console.log('[USBCalibrationManager] Starting calibration process'); // Ensure connection for calibration await this.ensureConnectedForCalibration(); // Unlock all servos for calibration (allow manual movement) console.log('[USBCalibrationManager] ๐Ÿ”“ Unlocking all servos for calibration...'); try { await scsServoSDK.unlockServos(this.jointIds); console.log('[USBCalibrationManager] โœ… All servos unlocked for manual movement during calibration'); } catch (error) { console.warn('[USBCalibrationManager] Warning: Failed to unlock some servos for calibration:', error); } this._calibrationState = { isCalibrating: true, progress: 0 }; // Initialize calibrations with current values this.jointCalibrations = {}; this.jointNames.forEach(name => { const currentValue = this.lastPositions[name] || 2048; this.jointCalibrations[name] = { isCalibrated: false, minServoValue: currentValue, maxServoValue: currentValue }; }); this.startCalibrationPolling(); this.notifyCalibrationChange(); } async stopCalibration(): Promise { console.log('[USBCalibrationManager] Stopping calibration'); this._calibrationState = { isCalibrating: false, progress: 100 }; // Mark all joints as calibrated this.jointNames.forEach(name => { if (this.jointCalibrations[name]) { this.jointCalibrations[name].isCalibrated = true; } }); this.stopCalibrationPolling(); // NEW: Read final positions and sync to virtual robot before locking console.log('[USBCalibrationManager] ๐Ÿ“ Reading final servo positions for virtual robot sync...'); try { const finalPositions = await this.readFinalPositionsAndSync(); console.log('[USBCalibrationManager] โœ… Final positions read and synced to virtual robot'); // Notify robot of calibration completion with final positions if (this.calibrationCompleteCallback) { this.calibrationCompleteCallback(finalPositions); } } catch (error) { console.error('[USBCalibrationManager] Failed to read final positions:', error); } this.notifyCalibrationChange(); // Keep connection open - don't disconnect automatically // The connection will be reused by USB drivers console.log('[USBCalibrationManager] Calibration complete, keeping connection for drivers'); } skipCalibration(): void { console.log('[USBCalibrationManager] Skipping calibration, using full range'); // Set full range for all joints this.jointNames.forEach(name => { this.jointCalibrations[name] = { isCalibrated: true, minServoValue: 0, maxServoValue: 4095 }; }); this._calibrationState = { isCalibrating: false, progress: 100 }; this.notifyCalibrationChange(); } // NEW: Set predefined calibration values async setPredefinedCalibration(): Promise { console.log('[USBCalibrationManager] Setting predefined calibration values'); // Ensure SDK connection for hardware access await this.ensureConnectedForCalibration(); // Predefined calibration values based on known good robot configuration const predefinedValues: Record = { "Rotation": { current: 2180, min: 764, max: 3388 }, "Pitch": { current: 1159, min: 1138, max: 3501 }, "Elbow": { current: 2874, min: 660, max: 2876 }, "Wrist_Pitch": { current: 2138, min: 762, max: 3075 }, "Wrist_Roll": { current: 2081, min: 154, max: 3995 }, "Jaw": { current: 2061, min: 2013, max: 3555 } }; // Set calibration values for all joints this.jointNames.forEach(name => { const values = predefinedValues[name]; if (values) { this.jointCalibrations[name] = { isCalibrated: true, minServoValue: values.min, maxServoValue: values.max }; // Set current position for reference this.lastPositions[name] = values.current; } }); this._calibrationState = { isCalibrating: false, progress: 100 }; this.notifyCalibrationChange(); console.log('[USBCalibrationManager] Predefined calibration values applied successfully'); } // NEW: Read final positions and prepare for sync private async readFinalPositionsAndSync(): Promise> { const finalPositions: Record = {}; console.log('[USBCalibrationManager] Reading final positions from all servos...'); // Read all servo positions sequentially for (let i = 0; i < this.jointIds.length; i++) { const servoId = this.jointIds[i]; const jointName = this.jointNames[i]; try { const position = await this.readServoPosition(servoId); finalPositions[jointName] = position; this.lastPositions[jointName] = position; console.log(`[USBCalibrationManager] ${jointName} (servo ${servoId}): ${position} (raw) -> ${this.normalizeValue(position, jointName).toFixed(1)}% (normalized)`); } catch (error) { console.warn(`[USBCalibrationManager] Failed to read final position for ${jointName} (servo ${servoId}):`, error); // Use last known position as fallback finalPositions[jointName] = this.lastPositions[jointName] || 2048; } } return finalPositions; } // NEW: Set callback for calibration completion with final positions onCalibrationCompleteWithPositions(callback: (finalPositions: Record) => void): () => void { this.calibrationCompleteCallback = callback; return () => { this.calibrationCompleteCallback = null; }; } // Post-calibration servo locking methods async lockServosForProduction(): Promise { if (!this.isSDKConnected) { throw new Error('SDK not connected - cannot lock servos'); } console.log('[USBCalibrationManager] ๐Ÿ”’ Locking all servos for production use (robot control)...'); try { // Use the new lockServosForProduction function that both locks and enables torque await scsServoSDK.lockServosForProduction(this.jointIds); console.log('[USBCalibrationManager] โœ… All servos locked for production - robot is now controlled and cannot be moved manually'); } catch (error) { console.error('[USBCalibrationManager] Failed to lock servos for production:', error); throw error; } } async keepServosUnlockedForConsumer(): Promise { if (!this.isSDKConnected) { console.log('[USBCalibrationManager] SDK not connected - servos remain in current state'); return; } console.log('[USBCalibrationManager] ๐Ÿ”“ Keeping servos unlocked for consumer use (reading positions)...'); try { // Ensure servos are unlocked for reading await scsServoSDK.unlockServos(this.jointIds); console.log('[USBCalibrationManager] โœ… All servos remain unlocked for consumer - can be moved manually and positions read'); } catch (error) { console.warn('[USBCalibrationManager] Warning: Failed to ensure servos are unlocked for consumer:', error); } } // Data access methods getCurrentRawValue(jointName: string): number | undefined { return this.lastPositions[jointName]; } getJointCalibration(jointName: string): JointCalibration | undefined { return this.jointCalibrations[jointName]; } getJointRange(jointName: string): number { const calibration = this.jointCalibrations[jointName]; if (!calibration?.minServoValue || !calibration?.maxServoValue) return 0; return Math.abs(calibration.maxServoValue - calibration.minServoValue); } getAllCalibrations(): Record { return { ...this.jointCalibrations }; } // Value conversion methods normalizeValue(servoValue: number, jointName: string): number { const calibration = this.jointCalibrations[jointName]; const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper'; if (!calibration?.isCalibrated || !calibration.minServoValue || !calibration.maxServoValue) { if (isGripper) { return (servoValue / 4095) * 100; } else { return ((servoValue - 2048) / 2048) * 100; } } const { minServoValue, maxServoValue } = calibration; if (maxServoValue === minServoValue) return 0; const bounded = Math.max(minServoValue, Math.min(maxServoValue, servoValue)); if (isGripper) { return ((bounded - minServoValue) / (maxServoValue - minServoValue)) * 100; } else { return (((bounded - minServoValue) / (maxServoValue - minServoValue)) * 200) - 100; } } denormalizeValue(normalizedValue: number, jointName: string): number { const calibration = this.jointCalibrations[jointName]; const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper'; if (!calibration?.isCalibrated || !calibration.minServoValue || !calibration.maxServoValue) { // No calibration, use appropriate default conversion if (isGripper) { return Math.round((normalizedValue / 100) * 4095); } else { return Math.round(2048 + (normalizedValue / 100) * 2048); } } const { minServoValue, maxServoValue } = calibration; let ratio: number; if (isGripper) { ratio = normalizedValue / 100; } else { ratio = (normalizedValue + 100) / 200; } const servoValue = minServoValue + ratio * (maxServoValue - minServoValue); return Math.round(Math.max(minServoValue, Math.min(maxServoValue, servoValue))); } // Event handling onCalibrationChange(callback: () => void): () => void { this.calibrationCallbacks.push(callback); return () => { const index = this.calibrationCallbacks.indexOf(callback); if (index >= 0) { this.calibrationCallbacks.splice(index, 1); } }; } // Format servo value for display formatServoValue(value: number | undefined): string { return value !== undefined ? value.toString() : '---'; } // Cleanup async destroy(): Promise { console.log('[USBCalibrationManager] ๐Ÿงน Destroying calibration manager...'); this.stopCalibrationPolling(); // Safely unlock all servos before disconnecting (best practice) if (this.isSDKConnected) { try { console.log('[USBCalibrationManager] ๐Ÿ”“ Safely unlocking all servos before cleanup...'); await scsServoSDK.unlockServosForManualMovement(this.jointIds); console.log('[USBCalibrationManager] โœ… All servos safely unlocked for manual movement'); } catch (error) { console.warn('[USBCalibrationManager] Warning: Failed to safely unlock servos during cleanup:', error); } } await this.disconnectFromCalibration(); this.calibrationCallbacks = []; this.calibrationCompleteCallback = null; console.log('[USBCalibrationManager] โœ… Calibration manager destroyed'); } // Private methods private async readServoPosition(servoId: number): Promise { return new Promise((resolve, reject) => { this.readingQueue.push({ servoId, resolve, reject }); this.processReadingQueue(); }); } private async processReadingQueue(): Promise { if (this.isReadingServos || this.readingQueue.length === 0) { return; } this.isReadingServos = true; try { const batch = [...this.readingQueue]; this.readingQueue = []; for (const { servoId, resolve, reject } of batch) { try { const position = await scsServoSDK.readPosition(servoId); resolve(position); } catch (error) { reject(error instanceof Error ? error : new Error(`Failed to read servo ${servoId}`)); } await new Promise(resolve => setTimeout(resolve, 5)); } } finally { this.isReadingServos = false; if (this.readingQueue.length > 0) { setTimeout(() => this.processReadingQueue(), 50); } } } private async startCalibrationPolling(): Promise { this.stopCalibrationPolling(); this.calibrationPollingAbortController = new AbortController(); const signal = this.calibrationPollingAbortController.signal; console.log('[USBCalibrationManager] Starting calibration polling'); try { while (!signal.aborted && this._calibrationState.isCalibrating) { const readPromises = this.jointIds.map(async (servoId, i) => { if (signal.aborted) return null; const jointName = this.jointNames[i]; try { const currentValue = await this.readServoPosition(servoId); return { jointName, currentValue }; } catch (error) { return null; } }); const results = await Promise.all(readPromises); let hasUpdates = false; results.forEach(result => { if (!result) return; const { jointName, currentValue } = result; this.lastPositions[jointName] = currentValue; const calibration = this.jointCalibrations[jointName]; if (calibration) { if (currentValue < calibration.minServoValue!) { calibration.minServoValue = currentValue; hasUpdates = true; } if (currentValue > calibration.maxServoValue!) { calibration.maxServoValue = currentValue; hasUpdates = true; } } }); if (hasUpdates) { this.notifyCalibrationChange(); } // Calculate progress const totalRangeNeeded = 500; let totalRangeDiscovered = 0; this.jointNames.forEach(name => { const calibration = this.jointCalibrations[name]; if (calibration?.minServoValue !== undefined && calibration?.maxServoValue !== undefined) { totalRangeDiscovered += Math.abs(calibration.maxServoValue - calibration.minServoValue); } }); const newProgress = Math.min(100, (totalRangeDiscovered / (totalRangeNeeded * this.jointNames.length)) * 100); if (Math.abs(newProgress - this._calibrationState.progress) > 1) { this._calibrationState.progress = newProgress; this.notifyCalibrationChange(); } await new Promise(resolve => setTimeout(resolve, 10)); } } catch (error) { if (!signal.aborted) { console.error('[USBCalibrationManager] Calibration polling error:', error); } } } private stopCalibrationPolling(): void { if (this.calibrationPollingAbortController) { this.calibrationPollingAbortController.abort(); this.calibrationPollingAbortController = null; } } private notifyCalibrationChange(): void { this.calibrationCallbacks.forEach(callback => { try { callback(); } catch (error) { console.error('[USBCalibrationManager] Error in calibration callback:', error); } }); } }