import type { MasterDriver, ConnectionStatus, RobotCommand, CommandSequence, MockSequenceMasterConfig, CommandCallback, SequenceCallback, StatusChangeCallback, UnsubscribeFn } from "$lib/types/robotDriver"; import { getRobotPollingConfig } from "$lib/configs/performanceConfig"; /** * Mock Sequence Master Driver * Provides predefined movement sequences for testing master-slave architecture */ export class MockSequenceMaster implements MasterDriver { readonly type = "master" as const; readonly id: string; readonly name: string; private _status: ConnectionStatus = { isConnected: false }; private config: MockSequenceMasterConfig; // Event callbacks private commandCallbacks: CommandCallback[] = []; private sequenceCallbacks: SequenceCallback[] = []; private statusCallbacks: StatusChangeCallback[] = []; // Playback control private isPlaying = false; private isPaused = false; private currentSequenceIndex = 0; private currentCommandIndex = 0; private playbackIntervalId?: number; constructor(config: MockSequenceMasterConfig) { this.config = config; this.id = `mock-sequence-${Date.now()}`; this.name = `Mock Sequence Master`; console.log(`Created MockSequenceMaster with ${config.sequences.length} sequences`); } get status(): ConnectionStatus { return this._status; } async connect(): Promise { console.log(`Connecting ${this.name}...`); this._status = { isConnected: true, lastConnected: new Date() }; this.notifyStatusChange(); // Auto-start if configured if (this.config.autoStart) { await this.start(); } console.log(`${this.name} connected`); } async disconnect(): Promise { console.log(`Disconnecting ${this.name}...`); await this.stop(); this._status = { isConnected: false }; this.notifyStatusChange(); console.log(`${this.name} disconnected`); } async start(): Promise { if (!this._status.isConnected) { throw new Error("Cannot start: master not connected"); } if (this.isPlaying && !this.isPaused) { console.log("Sequence already playing"); return; } console.log(`Starting sequence playback...`); this.isPlaying = true; this.isPaused = false; // Reset to beginning if not paused if (this.currentSequenceIndex >= this.config.sequences.length) { this.currentSequenceIndex = 0; this.currentCommandIndex = 0; } this.startPlaybackLoop(); } async stop(): Promise { console.log("Stopping sequence playback"); this.isPlaying = false; this.isPaused = false; this.currentSequenceIndex = 0; this.currentCommandIndex = 0; if (this.playbackIntervalId) { clearInterval(this.playbackIntervalId); this.playbackIntervalId = undefined; } } async pause(): Promise { console.log("Pausing sequence playback"); this.isPaused = true; if (this.playbackIntervalId) { clearInterval(this.playbackIntervalId); this.playbackIntervalId = undefined; } } async resume(): Promise { if (!this.isPlaying || !this.isPaused) { return; } console.log("Resuming sequence playback"); this.isPaused = false; this.startPlaybackLoop(); } // Event subscription methods onCommand(callback: CommandCallback): UnsubscribeFn { this.commandCallbacks.push(callback); return () => { const index = this.commandCallbacks.indexOf(callback); if (index >= 0) { this.commandCallbacks.splice(index, 1); } }; } onSequence(callback: SequenceCallback): UnsubscribeFn { this.sequenceCallbacks.push(callback); return () => { const index = this.sequenceCallbacks.indexOf(callback); if (index >= 0) { this.sequenceCallbacks.splice(index, 1); } }; } onStatusChange(callback: StatusChangeCallback): UnsubscribeFn { this.statusCallbacks.push(callback); return () => { const index = this.statusCallbacks.indexOf(callback); if (index >= 0) { this.statusCallbacks.splice(index, 1); } }; } // Private methods private startPlaybackLoop(): void { if (this.playbackIntervalId) { clearInterval(this.playbackIntervalId); } this.playbackIntervalId = setInterval(() => { this.executeNextCommand(); }, getRobotPollingConfig().SEQUENCE_PLAYBACK_INTERVAL_MS); } private executeNextCommand(): void { if (!this.isPlaying || this.isPaused || this.config.sequences.length === 0) { return; } const currentSequence = this.config.sequences[this.currentSequenceIndex]; if (!currentSequence) { this.handleSequenceEnd(); return; } const currentCommand = currentSequence.commands[this.currentCommandIndex]; if (!currentCommand) { this.handleCommandEnd(); return; } console.log( `Executing command ${this.currentCommandIndex + 1}/${currentSequence.commands.length} from sequence "${currentSequence.name}"` ); // Send the command this.notifyCommand([currentCommand]); // Move to next command this.currentCommandIndex++; } private handleCommandEnd(): void { const currentSequence = this.config.sequences[this.currentSequenceIndex]; // Sequence completed, notify this.notifySequence(currentSequence); // Move to next sequence or loop this.currentCommandIndex = 0; this.currentSequenceIndex++; if (this.currentSequenceIndex >= this.config.sequences.length) { if (this.config.loopMode) { console.log("Looping back to first sequence"); this.currentSequenceIndex = 0; } else { console.log("All sequences completed"); this.stop(); } } } private handleSequenceEnd(): void { if (this.config.loopMode) { this.currentSequenceIndex = 0; this.currentCommandIndex = 0; } else { this.stop(); } } private notifyCommand(commands: RobotCommand[]): void { this.commandCallbacks.forEach((callback) => { try { callback(commands); } catch (error) { console.error("Error in command callback:", error); } }); } private notifySequence(sequence: CommandSequence): void { this.sequenceCallbacks.forEach((callback) => { try { callback(sequence); } catch (error) { console.error("Error in sequence callback:", error); } }); } private notifyStatusChange(): void { this.statusCallbacks.forEach((callback) => { try { callback(this._status); } catch (error) { console.error("Error in status callback:", error); } }); } } // Predefined demo sequences export const DEMO_SEQUENCES: CommandSequence[] = [ { id: "gentle-wave", name: "Gentle Wave Pattern", totalDuration: 6000, commands: [ { timestamp: 0, joints: [ { name: "Rotation", value: -10 }, { name: "Pitch", value: 8 }, { name: "Elbow", value: -12 } ], duration: 2000 }, { timestamp: 2000, joints: [{ name: "Wrist_Roll", value: 10 }], duration: 1000 }, { timestamp: 3000, joints: [{ name: "Wrist_Roll", value: -10 }], duration: 1000 }, { timestamp: 4000, joints: [ { name: "Wrist_Roll", value: 0 }, { name: "Rotation", value: 0 }, { name: "Pitch", value: 0 }, { name: "Elbow", value: 0 } ], duration: 2000 } ] }, { id: "small-scan", name: "Small Scanning Pattern", totalDuration: 8000, commands: [ { timestamp: 0, joints: [ { name: "Rotation", value: -15 }, { name: "Pitch", value: 10 } ], duration: 2000 }, { timestamp: 2000, joints: [{ name: "Rotation", value: 15 }], duration: 3000 }, { timestamp: 5000, joints: [ { name: "Rotation", value: 0 }, { name: "Pitch", value: 0 } ], duration: 3000 } ] }, { id: "tiny-flex", name: "Tiny Flex Pattern", totalDuration: 8000, commands: [ { timestamp: 0, joints: [ { name: "Elbow", value: -15 }, { name: "Wrist_Pitch", value: 8 } ], duration: 2000 }, { timestamp: 2000, joints: [{ name: "Jaw", value: 8 }], duration: 1000 }, { timestamp: 3000, joints: [{ name: "Elbow", value: -25 }], duration: 2000 }, { timestamp: 5000, joints: [ { name: "Jaw", value: 0 }, { name: "Elbow", value: 0 }, { name: "Wrist_Pitch", value: 0 } ], duration: 3000 } ] } ];