Spaces:
Build error
Build error
import type { Producer, ConnectionStatus, RobotCommand, USBDriverConfig } from '../models.js'; | |
import { USBCalibrationManager } from '../calibration/USBCalibrationManager.js'; | |
import { scsServoSDK } from "feetech.js"; | |
import { ROBOT_CONFIG } from '../config.js'; | |
export class USBProducer implements Producer { | |
readonly id: string; | |
readonly name = 'USB Producer'; | |
readonly config: USBDriverConfig; | |
private _status: ConnectionStatus = { isConnected: false }; | |
private statusCallbacks: ((status: ConnectionStatus) => void)[] = []; | |
// Joint configuration | |
private readonly jointIds = [1, 2, 3, 4, 5, 6]; | |
private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"]; | |
// Shared calibration manager | |
private calibrationManager: USBCalibrationManager; | |
// Serial command processing to prevent "Port is busy" errors | |
private commandQueue: Array<{ joints: Array<{ name: string; value: number }>, resolve: () => void, reject: (error: Error) => void }> = []; | |
private isProcessingCommands = false; | |
constructor(config: USBDriverConfig, calibrationManager: USBCalibrationManager) { | |
this.config = config; | |
this.calibrationManager = calibrationManager; | |
this.id = `usb-producer-${Date.now()}`; | |
} | |
get status(): ConnectionStatus { | |
return this._status; | |
} | |
async connect(): Promise<void> { | |
if (this._status.isConnected) { | |
console.debug('[USBProducer] Already connected'); | |
return; | |
} | |
try { | |
console.debug('[USBProducer] Connecting...'); | |
// Check if calibration is needed | |
if (this.calibrationManager.needsCalibration) { | |
throw new Error('USB Producer requires calibration. Please complete calibration first.'); | |
} | |
// Ensure the SDK is connected (reuse calibration connection if available) | |
if (!this.calibrationManager.isSDKConnected) { | |
console.debug('[USBProducer] Establishing new SDK connection'); | |
await scsServoSDK.connect({ | |
baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate | |
}); | |
} else { | |
console.debug('[USBProducer] Reusing existing SDK connection from calibration'); | |
} | |
// Lock servos for production use (robot control) | |
console.debug('[USBProducer] π Locking servos for production use...'); | |
await this.calibrationManager.lockServosForProduction(); | |
this._status = { | |
isConnected: true, | |
lastConnected: new Date() | |
}; | |
this.notifyStatusChange(); | |
console.debug('[USBProducer] β Connected successfully - servos locked for robot control'); | |
} catch (error) { | |
console.error('[USBProducer] Connection failed:', error); | |
this._status = { | |
isConnected: false, | |
error: error instanceof Error ? error.message : 'Connection failed' | |
}; | |
this.notifyStatusChange(); | |
throw error; | |
} | |
} | |
async disconnect(): Promise<void> { | |
if (this._status.isConnected) { | |
console.debug('[USBProducer] π Disconnecting and unlocking servos...'); | |
try { | |
// Safely unlock servos when disconnecting (best practice) | |
if (this.calibrationManager.isSDKConnected) { | |
console.debug('[USBProducer] π Safely unlocking servos for manual movement...'); | |
await scsServoSDK.unlockServosForManualMovement(this.jointIds); | |
console.debug('[USBProducer] β Servos safely unlocked - can now be moved manually'); | |
} | |
} catch (error) { | |
console.warn('[USBProducer] Warning: Failed to unlock servos during disconnect:', error); | |
} | |
// Don't disconnect the SDK here - let calibration manager handle it | |
// This allows multiple USB drivers to share the same connection | |
} | |
this._status = { isConnected: false }; | |
this.notifyStatusChange(); | |
console.debug('[USBProducer] β Disconnected'); | |
} | |
async sendCommand(command: RobotCommand): Promise<void> { | |
if (!this._status.isConnected) { | |
throw new Error('Cannot send command: USB Producer not connected'); | |
} | |
console.debug(`[USBProducer] Queuing command:`, command); | |
// Queue command for serial processing | |
return new Promise((resolve, reject) => { | |
this.commandQueue.push({ | |
joints: command.joints, | |
resolve, | |
reject | |
}); | |
// Start processing if not already running | |
this.processCommandQueue(); | |
}); | |
} | |
// 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); | |
} | |
}; | |
} | |
// Private methods | |
private async processCommandQueue(): Promise<void> { | |
if (this.isProcessingCommands || this.commandQueue.length === 0) { | |
return; | |
} | |
this.isProcessingCommands = true; | |
try { | |
while (this.commandQueue.length > 0) { | |
const { joints, resolve, reject } = this.commandQueue.shift()!; | |
try { | |
// Process servos sequentially to prevent "Port is busy" errors | |
for (const jointCmd of joints) { | |
const jointIndex = this.jointNames.indexOf(jointCmd.name); | |
if (jointIndex >= 0) { | |
const servoId = this.jointIds[jointIndex]; | |
const servoPosition = this.calibrationManager.denormalizeValue(jointCmd.value, jointCmd.name); | |
await this.writeServoWithRetry(servoId, servoPosition, jointCmd.name); | |
// Small delay between servo writes to prevent port conflicts | |
await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay)); | |
} | |
} | |
resolve(); | |
} catch (error) { | |
reject(error as Error); | |
} | |
} | |
} finally { | |
this.isProcessingCommands = false; | |
} | |
} | |
private async writeServoWithRetry(servoId: number, position: number, jointName: string): Promise<void> { | |
let lastError: Error | null = null; | |
for (let attempt = 1; attempt <= ROBOT_CONFIG.usb.maxRetries; attempt++) { | |
try { | |
await scsServoSDK.writePositionUnlocked(servoId, position); | |
console.debug(`[USBProducer] β ${jointName} (servo ${servoId}) -> ${position}`); | |
return; // Success! | |
} catch (error) { | |
lastError = error as Error; | |
console.warn(`[USBProducer] Attempt ${attempt}/${ROBOT_CONFIG.usb.maxRetries} failed for servo ${servoId}:`, error); | |
if (attempt < ROBOT_CONFIG.usb.maxRetries) { | |
await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.retryDelay)); | |
} | |
} | |
} | |
// All retries failed | |
throw new Error(`Failed to write servo ${servoId} after ${ROBOT_CONFIG.usb.maxRetries} attempts: ${lastError?.message}`); | |
} | |
private notifyStatusChange(): void { | |
this.statusCallbacks.forEach(callback => { | |
try { | |
callback(this._status); | |
} catch (error) { | |
console.error('[USBProducer] Error in status callback:', error); | |
} | |
}); | |
} | |
} |