import type { ConnectionStatus, USBDriverConfig } from '../models.js'; import { CalibrationState } from '../calibration/CalibrationState.svelte.js'; import { ScsServoSDK } from 'feetech.js'; import { ROBOT_CONFIG } from '../config.js'; export abstract class USBServoDriver { readonly id: string; readonly name: string; readonly config: USBDriverConfig; protected _status: ConnectionStatus = { isConnected: false }; protected statusCallbacks: ((status: ConnectionStatus) => void)[] = []; protected scsServoSDK: ScsServoSDK | null = null; // Calibration state - directly embedded readonly calibrationState: CalibrationState; // Calibration polling private calibrationPollingInterval: ReturnType | null = null; // Joint to servo ID mapping for SO-100 arm protected readonly jointToServoMap = { "Rotation": 1, "Pitch": 2, "Elbow": 3, "Wrist_Pitch": 4, "Wrist_Roll": 5, "Jaw": 6 }; constructor(config: USBDriverConfig, driverType: string) { this.config = config; this.id = `usb-${driverType}-${Date.now()}`; this.name = `USB ${driverType}`; this.calibrationState = new CalibrationState(); } get status(): ConnectionStatus { return this._status; } get needsCalibration(): boolean { return this.calibrationState.needsCalibration; } get isCalibrating(): boolean { return this.calibrationState.isCalibrating; } get isCalibrated(): boolean { return this.calibrationState.isCalibrated; } // Type guard to check if a driver is a USB driver static isUSBDriver(driver: any): driver is USBServoDriver { return driver && typeof driver.calibrationState === 'object' && typeof driver.needsCalibration === 'boolean' && typeof driver.isCalibrated === 'boolean' && typeof driver.startCalibration === 'function'; } // Type-safe method to get calibration interface getCalibrationInterface(): { needsCalibration: boolean; isCalibrating: boolean; isCalibrated: boolean; startCalibration: () => Promise; completeCalibration: () => Promise>; skipCalibration: () => void; cancelCalibration: () => void; onCalibrationCompleteWithPositions: (callback: (positions: Record) => void) => () => void; } { return { needsCalibration: this.needsCalibration, isCalibrating: this.isCalibrating, isCalibrated: this.isCalibrated, startCalibration: () => this.startCalibration(), completeCalibration: () => this.completeCalibration(), skipCalibration: () => this.skipCalibration(), cancelCalibration: () => this.cancelCalibration(), onCalibrationCompleteWithPositions: (callback) => this.onCalibrationCompleteWithPositions(callback) }; } // Abstract methods that subclasses must implement abstract connect(): Promise; abstract disconnect(): Promise; // Common connection logic protected async connectToUSB(): Promise { if (this._status.isConnected) { console.log(`[${this.name}] Already connected`); return; } try { console.log(`[${this.name}] Connecting...`); // Create a new SDK instance for this driver instead of using the singleton // This allows multiple drivers to connect to different ports simultaneously this.scsServoSDK = new ScsServoSDK(); await this.scsServoSDK.connect({ baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate, protocolEnd: 0 // STS/SMS protocol }); this._status = { isConnected: true, lastConnected: new Date() }; this.notifyStatusChange(); console.log(`[${this.name}] Connected successfully`); // Debug: Log SDK instance methods to identify the issue console.log(`[${this.name}] SDK instance methods:`, Object.getOwnPropertyNames(this.scsServoSDK)); console.log(`[${this.name}] SDK prototype methods:`, Object.getOwnPropertyNames(Object.getPrototypeOf(this.scsServoSDK))); console.log(`[${this.name}] writeTorqueEnable available:`, typeof this.scsServoSDK.writeTorqueEnable); console.log(`[${this.name}] syncReadPositions available:`, typeof this.scsServoSDK.syncReadPositions); } catch (error) { console.error(`[${this.name}] Connection failed:`, error); this._status = { isConnected: false, error: `Connection failed: ${error}` }; this.notifyStatusChange(); throw error; } } protected async disconnectFromUSB(): Promise { if (this.scsServoSDK) { try { await this.unlockAllServos(); await this.scsServoSDK.disconnect(); } catch (error) { console.warn(`[${this.name}] Error during disconnect:`, error); } this.scsServoSDK = null; } this._status = { isConnected: false }; this.notifyStatusChange(); console.log(`[${this.name}] Disconnected`); } // Calibration methods async startCalibration(): Promise { if (!this._status.isConnected) { await this.connectToUSB(); } if (!this._status.isConnected) { throw new Error('Cannot start calibration: not connected'); } console.log(`[${this.name}] Starting calibration...`); this.calibrationState.startCalibration(); // Unlock servos for manual movement during calibration await this.unlockAllServos(); // Start polling positions during calibration await this.startCalibrationPolling(); } async completeCalibration(): Promise> { if (!this.isCalibrating) { throw new Error('Not currently calibrating'); } // Stop polling this.stopCalibrationPolling(); // Read final positions const finalPositions = await this.readCurrentPositions(); // Complete calibration state this.calibrationState.completeCalibration(); console.log(`[${this.name}] Calibration completed`); return finalPositions; } skipCalibration(): void { // Stop polling if active this.stopCalibrationPolling(); this.calibrationState.skipCalibration(); } async setPredefinedCalibration(): Promise { // Stop polling if active this.stopCalibrationPolling(); this.skipCalibration(); } // Cancel calibration cancelCalibration(): void { // Stop polling if active this.stopCalibrationPolling(); this.calibrationState.cancelCalibration(); } // Start polling servo positions during calibration private async startCalibrationPolling(): Promise { if (this.calibrationPollingInterval !== null) { return; // Already polling } console.log(`[${this.name}] Starting calibration position polling...`); // Poll positions every 100ms during calibration this.calibrationPollingInterval = setInterval(async () => { if (!this.isCalibrating || !this._status.isConnected || !this.scsServoSDK) { this.stopCalibrationPolling(); return; } try { // Read positions for all servos const servoIds = Object.values(this.jointToServoMap); const positions = await this.scsServoSDK.syncReadPositions(servoIds); // Update calibration state with current positions Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { const position = positions.get(servoId); if (position !== undefined) { this.calibrationState.updateCurrentValue(jointName, position); console.debug(`[${this.name}] ${jointName} (servo ${servoId}): ${position}`); } }); } catch (error) { console.warn(`[${this.name}] Calibration polling error:`, error); // Continue polling despite errors - user might be moving servos rapidly } }, 100); // Poll every 100ms } // Stop polling servo positions private stopCalibrationPolling(): void { if (this.calibrationPollingInterval !== null) { clearInterval(this.calibrationPollingInterval); this.calibrationPollingInterval = null; console.log(`[${this.name}] Stopped calibration position polling`); } } // Servo position reading (for calibration) async readCurrentPositions(): Promise> { if (!this.scsServoSDK || !this._status.isConnected) { throw new Error('Cannot read positions: not connected'); } const positions: Record = {}; try { const servoIds = Object.values(this.jointToServoMap); const servoPositions = await this.scsServoSDK.syncReadPositions(servoIds); Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { const position = servoPositions.get(servoId); if (position !== undefined) { positions[jointName] = position; // Update calibration state with current position this.calibrationState.updateCurrentValue(jointName, position); } }); } catch (error) { console.error(`[${this.name}] Error reading positions:`, error); throw error; } return positions; } // Value conversion methods normalizeValue(rawValue: number, jointName: string): number { if (!this.isCalibrated) { throw new Error('Cannot normalize value: not calibrated'); } return this.calibrationState.normalizeValue(rawValue, jointName); } denormalizeValue(normalizedValue: number, jointName: string): number { if (!this.isCalibrated) { throw new Error('Cannot denormalize value: not calibrated'); } return this.calibrationState.denormalizeValue(normalizedValue, jointName); } // Servo control methods protected async lockAllServos(): Promise { if (!this.scsServoSDK) return; try { console.log(`[${this.name}] Locking all servos...`); const servoIds = Object.values(this.jointToServoMap); for (const servoId of servoIds) { try { // Check if writeTorqueEnable method exists before calling if (typeof this.scsServoSDK.writeTorqueEnable !== 'function') { console.warn(`[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}`); continue; } await this.scsServoSDK.writeTorqueEnable(servoId, true); await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay)); } catch (error) { console.warn(`[${this.name}] Failed to lock servo ${servoId}:`, error); } } console.log(`[${this.name}] All servos locked`); } catch (error) { console.error(`[${this.name}] Error locking servos:`, error); } } protected async unlockAllServos(): Promise { if (!this.scsServoSDK) return; try { console.log(`[${this.name}] Unlocking all servos...`); const servoIds = Object.values(this.jointToServoMap); for (const servoId of servoIds) { try { // Check if writeTorqueEnable method exists before calling if (typeof this.scsServoSDK.writeTorqueEnable !== 'function') { console.warn(`[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}`); continue; } await this.scsServoSDK.writeTorqueEnable(servoId, false); await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay)); } catch (error) { console.warn(`[${this.name}] Failed to unlock servo ${servoId}:`, error); } } console.log(`[${this.name}] All servos unlocked`); } catch (error) { console.error(`[${this.name}] Error unlocking servos:`, error); } } // Event handlers onStatusChange(callback: (status: ConnectionStatus) => void): () => void { this.statusCallbacks.push(callback); return () => { const index = this.statusCallbacks.indexOf(callback); if (index >= 0) { this.statusCallbacks.splice(index, 1); } }; } protected notifyStatusChange(): void { this.statusCallbacks.forEach(callback => { try { callback(this._status); } catch (error) { console.error(`[${this.name}] Error in status callback:`, error); } }); } // Register callback for calibration completion with positions onCalibrationCompleteWithPositions(callback: (positions: Record) => void): () => void { return this.calibrationState.onCalibrationCompleteWithPositions(callback); } // Sync robot joint positions using normalized values from calibration syncRobotPositions(finalPositions: Record, updateRobotCallback?: (jointName: string, normalizedValue: number) => void): void { if (!updateRobotCallback) return; console.log(`[${this.name}] 🔄 Syncing robot to final calibration positions...`); Object.entries(finalPositions).forEach(([jointName, rawPosition]) => { try { // Convert raw servo position to normalized value using calibration const normalizedValue = this.normalizeValue(rawPosition, jointName); // Clamp to appropriate normalized range based on joint type let clampedValue: number; if (jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper') { clampedValue = Math.max(0, Math.min(100, normalizedValue)); } else { clampedValue = Math.max(-100, Math.min(100, normalizedValue)); } console.log(`[${this.name}] ${jointName}: ${rawPosition} (raw) -> ${normalizedValue.toFixed(1)} -> ${clampedValue.toFixed(1)} (normalized)`); // Update robot joint through callback updateRobotCallback(jointName, clampedValue); } catch (error) { console.warn(`[${this.name}] Failed to sync position for joint ${jointName}:`, error); } }); console.log(`[${this.name}] ✅ Robot synced to calibration positions`); } // Cleanup async destroy(): Promise { this.stopCalibrationPolling(); await this.disconnect(); this.calibrationState.destroy(); } }