import type { Consumer, RobotCommand } from "../models.js"; import { USBServoDriver } from "./USBServoDriver.js"; import { ROBOT_CONFIG } from "../config.js"; export class USBConsumer extends USBServoDriver implements Consumer { private commandCallbacks: ((command: RobotCommand) => void)[] = []; private pollingInterval: ReturnType | null = null; private lastPositions: Record = {}; private errorCount = 0; constructor(config: any) { super(config, "Consumer"); } async connect(): Promise { // Connect to USB first (this triggers browser's device selection dialog) await this.connectToUSB(); // Unlock servos for manual movement (consumer mode) await this.unlockAllServos(); // Note: Calibration is checked when operations are actually needed } async disconnect(): Promise { await this.stopListening(); await this.disconnectFromUSB(); } async startListening(): Promise { if (!this._status.isConnected || this.pollingInterval !== null) { return; } if (!this.isCalibrated) { throw new Error("Cannot start listening: not calibrated"); } console.log(`[${this.name}] Starting position listening...`); this.errorCount = 0; this.pollingInterval = setInterval(async () => { try { await this.pollAndBroadcastPositions(); this.errorCount = 0; } catch (error) { this.errorCount++; console.warn(`[${this.name}] Polling error (${this.errorCount}):`, error); if (this.errorCount >= ROBOT_CONFIG.polling.maxPollingErrors) { console.warn(`[${this.name}] Too many polling errors, slowing down...`); await this.stopListening(); setTimeout(() => this.startListening(), ROBOT_CONFIG.polling.errorBackoffRate); } } }, ROBOT_CONFIG.polling.consumerPollingRate); } async stopListening(): Promise { if (this.pollingInterval !== null) { clearInterval(this.pollingInterval); this.pollingInterval = null; console.log(`[${this.name}] Stopped position listening`); } } // Event handlers already in base class onCommand(callback: (command: RobotCommand) => void): () => void { this.commandCallbacks.push(callback); return () => { const index = this.commandCallbacks.indexOf(callback); if (index >= 0) { this.commandCallbacks.splice(index, 1); } }; } // Private methods private async pollAndBroadcastPositions(): Promise { if (!this.scsServoSDK || !this._status.isConnected) { return; } try { // Read positions for all servos const servoIds = Object.values(this.jointToServoMap); const positions = await this.scsServoSDK.syncReadPositions(servoIds); const jointsWithChanges: { name: string; value: number }[] = []; // Check for position changes and convert to normalized values Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { const currentPosition = positions.get(servoId); const lastPosition = this.lastPositions[servoId]; if ( currentPosition !== undefined && (lastPosition === undefined || Math.abs(currentPosition - lastPosition) > ROBOT_CONFIG.performance.jointUpdateThreshold) ) { this.lastPositions[servoId] = currentPosition; // Convert to normalized value using calibration (required) const normalizedValue = this.normalizeValue(currentPosition, jointName); jointsWithChanges.push({ name: jointName, value: normalizedValue }); } }); // Broadcast changes if any if (jointsWithChanges.length > 0) { const command: RobotCommand = { timestamp: Date.now(), joints: jointsWithChanges }; this.notifyCommand(command); } } catch (error) { throw error; // Re-throw for error handling in polling loop } } private notifyCommand(command: RobotCommand): void { this.commandCallbacks.forEach((callback) => { try { callback(command); } catch (error) { console.error(`[${this.name}] Error in command callback:`, error); } }); } }