import type { RobotState } from "$lib/types/robot"; import type { MasterDriver, SlaveDriver, DriverJointState, ConnectionStatus, RobotCommand, CommandSequence, UnsubscribeFn } from "$lib/types/robotDriver"; import type IUrdfJoint from "@/components/3d/robot/URDF/interfaces/IUrdfJoint"; import { getRobotPollingConfig } from "$lib/configs/performanceConfig"; /** * Enhanced joint state that combines URDF data with driver state */ export interface ManagedJointState { // URDF properties name: string; urdfJoint: IUrdfJoint; servoId?: number; // Current state virtualValue: number; // What the UI shows/controls realValue?: number; // What the physical robot reports commandedValue: number; // What was last commanded by master/manual isActive: boolean; // Whether this joint is being controlled // Calibration state calibrationOffset: number; // Offset to apply: realValue = rawValue + offset restPosition: number; // Expected position for this joint in rest mode // Sync state lastVirtualUpdate: Date; lastRealUpdate?: Date; lastCommandUpdate?: Date; syncError?: string; } /** * Calibration state for robot */ export interface CalibrationState { isCalibrated: boolean; calibrationTime?: Date; offsets: { [jointName: string]: number }; } /** * Control source information */ export interface ControlState { hasActiveMaster: boolean; masterName?: string; manualControlEnabled: boolean; lastCommandSource: "master" | "manual" | "none"; commandQueue: RobotCommand[]; } /** * Robot instance that manages URDF visualization and master-slave connections */ export class Robot { readonly id: string; readonly robotState: RobotState; // Joint management private _joints = $state([]); // Master driver (command source) private _master = $state(undefined); private _masterStatus = $state({ isConnected: false }); // Slave drivers (execution targets) private _slaves = $state([]); private _slaveStatuses = $state>(new Map()); // Control state private _controlState = $state({ hasActiveMaster: false, manualControlEnabled: true, lastCommandSource: "none", commandQueue: [] }); // Calibration management private _calibrationState = $state({ isCalibrated: false, offsets: {} }); // Sync control - use performance config private syncIntervalId?: number; private readonly syncInterval = getRobotPollingConfig().ROBOT_SYNC_INTERVAL_MS; // Event subscriptions private unsubscribeCallbacks: UnsubscribeFn[] = []; // Reactive getters get joints() { return this._joints; } get master() { return this._master; } get masterStatus() { return this._masterStatus; } get slaves() { return this._slaves; } get slaveStatuses() { return this._slaveStatuses; } get controlState() { return this._controlState; } get calibrationState() { return this._calibrationState; } get isCalibrated() { return this._calibrationState.isCalibrated; } get activeJoints() { return this._joints.filter((joint) => joint.isActive); } get connectedSlaves() { return this._slaves.filter((slave) => this._slaveStatuses.get(slave.id)?.isConnected); } get manualControlEnabled() { return this._controlState.manualControlEnabled && !this._controlState.hasActiveMaster; } constructor(id: string, robotState: RobotState) { this.id = id; this.robotState = robotState; this.initializeJoints(); } /** * Initialize joint states from URDF */ private initializeJoints(): void { const servoMap = this.robotState.urdfConfig.jointNameIdMap || {}; const restPositions = this.robotState.urdfConfig.restPosition || {}; this._joints = this.robotState.robot.joints .filter((urdfJoint) => urdfJoint.type === "revolute" || urdfJoint.type === "continuous") .map((urdfJoint) => { const jointName = urdfJoint.name || ""; const restPosition = restPositions[jointName] || 0; // Initialize URDF joint rotation to rest position if (urdfJoint.type === "revolute") { const radians = (restPosition * Math.PI) / 180; const axis = urdfJoint.axis_xyz || [0, 0, 1]; urdfJoint.rotation = [radians * axis[0], radians * axis[1], radians * axis[2]]; } return { name: jointName, urdfJoint, servoId: servoMap[jointName], virtualValue: restPosition, commandedValue: restPosition, realValue: undefined, isActive: !!servoMap[jointName], lastVirtualUpdate: new Date(), lastRealUpdate: undefined, lastCommandUpdate: undefined, syncError: undefined, calibrationOffset: 0, restPosition }; }); console.log(`Initialized ${this._joints.length} joints for robot ${this.id}`); } // ============= MASTER MANAGEMENT ============= /** * Connect a master driver (command source) */ async setMaster(master: MasterDriver): Promise { // Disconnect existing master if (this._master) { await this.removeMaster(); } this._master = master; this._masterStatus = master.status; // Subscribe to master events const statusUnsub = master.onStatusChange((status) => { this._masterStatus = status; this._controlState.hasActiveMaster = status.isConnected; this._controlState.manualControlEnabled = !status.isConnected; }); const commandUnsub = master.onCommand((commands) => { this.handleMasterCommands(commands); }); const sequenceUnsub = master.onSequence((sequence) => { this.handleMasterSequence(sequence); }); this.unsubscribeCallbacks.push(statusUnsub, commandUnsub, sequenceUnsub); // Update control state this._controlState.hasActiveMaster = master.status.isConnected; this._controlState.manualControlEnabled = !master.status.isConnected; this._controlState.masterName = master.name; // Start the master (for masters that need to poll/generate commands) if (master.status.isConnected) { await master.start(); } console.log(`Master ${master.name} connected to robot ${this.id}`); } /** * Remove the current master */ async removeMaster(): Promise { if (!this._master) return; if (this._master.status.isConnected) { // Stop the master first, then disconnect try { await this._master.stop(); } catch (error) { console.warn(`Error stopping master ${this._master.name}:`, error); } await this._master.disconnect(); } this._master = undefined; this._masterStatus = { isConnected: false }; this._controlState.hasActiveMaster = false; this._controlState.manualControlEnabled = true; this._controlState.masterName = undefined; console.log(`Master removed from robot ${this.id}. Manual control restored.`); } // ============= SLAVE MANAGEMENT ============= /** * Add a slave driver (execution target) */ async addSlave(slave: SlaveDriver): Promise { // Check if slave already exists if (this._slaves.find((s) => s.id === slave.id)) { throw new Error(`Slave ${slave.id} already connected to robot ${this.id}`); } // Connect the slave await slave.connect(); this._slaves.push(slave); this._slaveStatuses.set(slave.id, slave.status); // Subscribe to slave events const statusUnsub = slave.onStatusChange((status) => { this._slaveStatuses.set(slave.id, status); }); const stateUnsub = slave.onStateUpdate((driverStates) => { this.updateFromSlave(slave.id, driverStates); }); this.unsubscribeCallbacks.push(statusUnsub, stateUnsub); // Move slave to rest position for safety await this.moveSlaveToRestPosition(slave); console.log(`Slave ${slave.name} (${slave.id}) connected to robot ${this.id}`); } /** * Remove a slave driver */ async removeSlave(slaveId: string): Promise { const slaveIndex = this._slaves.findIndex((s) => s.id === slaveId); if (slaveIndex === -1) return; const slave = this._slaves[slaveIndex]; // Move to rest position before disconnection if (this._slaveStatuses.get(slaveId)?.isConnected) { try { await this.moveSlaveToRestPosition(slave); await new Promise((resolve) => setTimeout(resolve, 500)); } catch (error) { console.warn(`Failed to move slave ${slaveId} to rest position:`, error); } } // Disconnect slave if (slave.status.isConnected) { await slave.disconnect(); } // Remove from arrays this._slaves.splice(slaveIndex, 1); this._slaveStatuses.delete(slaveId); console.log(`Slave ${slaveId} removed from robot ${this.id}`); } // ============= COMMAND HANDLING ============= /** * Handle commands from master */ private async handleMasterCommands(commands: RobotCommand[]): Promise { console.log(`Received ${commands.length} commands from master for robot ${this.id}`); // Update control state this._controlState.lastCommandSource = "master"; this._controlState.commandQueue = [...commands]; // Execute on all connected slaves const executePromises = this.connectedSlaves.map((slave) => slave .executeCommands(commands) .catch((error) => console.error(`Slave ${slave.id} failed to execute commands:`, error)) ); await Promise.allSettled(executePromises); // Update virtual state from commands this.updateVirtualFromCommands(commands); } /** * Handle command sequence from master */ private async handleMasterSequence(sequence: CommandSequence): Promise { console.log(`Received sequence "${sequence.name}" from master for robot ${this.id}`); // Process sequence commands one by one for (const command of sequence.commands) { await this.handleMasterCommands([command]); // Wait for command duration if specified if (command.duration) { await new Promise((resolve) => setTimeout(resolve, command.duration)); } } } /** * Manual control - update joint value (when no master active) */ async updateJointValue(jointName: string, value: number): Promise { if (!this.manualControlEnabled) { console.warn(`Manual control disabled - master is active for robot ${this.id}`); return; } const joint = this._joints.find((j) => j.name === jointName); if (!joint) { console.warn(`Joint ${jointName} not found in robot ${this.id}`); return; } // Apply limits const clampedValue = this.clampJointValue(joint, value); // Update virtual state joint.virtualValue = clampedValue; joint.commandedValue = clampedValue; joint.lastVirtualUpdate = new Date(); joint.lastCommandUpdate = new Date(); // Update URDF visualization this.updateUrdfJointRotation(joint, clampedValue); // Create command for slaves const command: RobotCommand = { timestamp: Date.now(), joints: [{ name: jointName, value: clampedValue }] }; // Send to all connected slaves const executePromises = this.connectedSlaves.map((slave) => slave .executeCommand(command) .catch((error) => console.error(`Slave ${slave.id} failed to execute manual command:`, error) ) ); await Promise.allSettled(executePromises); this._controlState.lastCommandSource = "manual"; } /** * Manual control - update multiple joints */ async updateJointValues(updates: { jointName: string; value: number }[]): Promise { if (!this.manualControlEnabled) { console.warn(`Manual control disabled - master is active for robot ${this.id}`); return; } // Update virtual states const commandJoints: { name: string; value: number }[] = []; for (const { jointName, value } of updates) { const joint = this._joints.find((j) => j.name === jointName); if (!joint) continue; const clampedValue = this.clampJointValue(joint, value); joint.virtualValue = clampedValue; joint.commandedValue = clampedValue; joint.lastVirtualUpdate = new Date(); joint.lastCommandUpdate = new Date(); // Update URDF visualization this.updateUrdfJointRotation(joint, clampedValue); commandJoints.push({ name: jointName, value: clampedValue }); } if (commandJoints.length === 0) return; // Create batch command for slaves const command: RobotCommand = { timestamp: Date.now(), joints: commandJoints }; // Send to all connected slaves const executePromises = this.connectedSlaves.map((slave) => slave .executeCommand(command) .catch((error) => console.error(`Slave ${slave.id} failed to execute manual batch command:`, error) ) ); await Promise.allSettled(executePromises); this._controlState.lastCommandSource = "manual"; } // ============= STATE MANAGEMENT ============= /** * Update virtual state from master commands */ private updateVirtualFromCommands(commands: RobotCommand[]): void { for (const command of commands) { for (const jointCommand of command.joints) { const joint = this._joints.find((j) => j.name === jointCommand.name); if (joint) { joint.virtualValue = jointCommand.value; joint.commandedValue = jointCommand.value; joint.lastCommandUpdate = new Date(); // Update URDF visualization this.updateUrdfJointRotation(joint, jointCommand.value); } } } } /** * Update joint states from slave feedback */ private updateFromSlave(slaveId: string, driverStates: DriverJointState[]): void { for (const driverState of driverStates) { const joint = this._joints.find((j) => j.name === driverState.name); if (joint) { // Apply calibration offset to raw real value if (driverState.realValue !== undefined) { joint.realValue = driverState.realValue + joint.calibrationOffset; } else { joint.realValue = undefined; } joint.lastRealUpdate = new Date(); } } } /** * Update URDF joint rotation for 3D visualization */ private updateUrdfJointRotation(joint: ManagedJointState, degrees: number): void { const urdfJoint = joint.urdfJoint; if (!urdfJoint) return; if (urdfJoint.type === "revolute") { const radians = (degrees * Math.PI) / 180; const axis = urdfJoint.axis_xyz || [0, 0, 1]; urdfJoint.rotation = [radians * axis[0], radians * axis[1], radians * axis[2]]; } } /** * Apply joint limits to a value */ private clampJointValue(joint: ManagedJointState, value: number): number { const limit = joint.urdfJoint.limit; if (!limit) return value; if (joint.urdfJoint.type === "revolute") { if (limit.lower !== undefined && limit.upper !== undefined) { const lowerDeg = (limit.lower * 180) / Math.PI; const upperDeg = (limit.upper * 180) / Math.PI; return Math.max(lowerDeg, Math.min(upperDeg, value)); } } else if (joint.urdfJoint.type === "continuous") { if (limit.velocity !== undefined) { return Math.max(-limit.velocity, Math.min(limit.velocity, value)); } } return value; } // ============= MOVEMENT FUNCTIONS ============= /** * Move robot to rest position smoothly */ async moveToRestPosition(durationMs: number = 3000): Promise { console.log(`Moving robot ${this.id} to rest position over ${durationMs}ms`); const movements: Array<{ joint: ManagedJointState; startValue: number; targetValue: number; totalDelta: number; }> = []; for (const joint of this._joints) { if (joint.isActive) { const startValue = joint.virtualValue; const targetValue = joint.restPosition; const totalDelta = targetValue - startValue; movements.push({ joint, startValue, targetValue, totalDelta }); } } if (movements.length === 0) { console.log("No active joints to move"); return; } const startTime = Date.now(); const updateInterval = 50; return new Promise((resolve) => { const animationStep = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / durationMs, 1.0); const easedProgress = progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2; const updates: Array<{ jointName: string; value: number }> = []; for (const movement of movements) { const currentValue = movement.startValue + movement.totalDelta * easedProgress; updates.push({ jointName: movement.joint.name, value: currentValue }); } this.updateJointValues(updates); if (progress < 1.0) { setTimeout(animationStep, updateInterval); } else { console.log(`Robot ${this.id} reached rest position`); resolve(); } }; animationStep(); }); } /** * Move a specific slave to rest position */ private async moveSlaveToRestPosition(slave: SlaveDriver): Promise { const restCommand: RobotCommand = { timestamp: Date.now(), joints: this._joints.map((joint) => ({ name: joint.name, value: joint.restPosition })) }; await slave.executeCommand(restCommand); } // ============= CALIBRATION ============= /** * Calibrate robot using first connected slave */ async calibrateRobot(): Promise { const connectedSlaves = this.connectedSlaves; if (connectedSlaves.length === 0) { throw new Error("Cannot calibrate: no slaves connected"); } const primarySlave = connectedSlaves[0]; console.log(`Calibrating robot ${this.id} using slave ${primarySlave.id}...`); try { const driverStates = await primarySlave.readJointStates(); const offsets: { [jointName: string]: number } = {}; for (const driverState of driverStates) { const joint = this._joints.find((j) => j.name === driverState.name); if (joint && driverState.realValue !== undefined) { const offset = joint.restPosition - driverState.realValue; offsets[joint.name] = offset; joint.calibrationOffset = offset; console.log( `Joint ${joint.name}: rest=${joint.restPosition}° real=${driverState.realValue}° offset=${offset.toFixed(1)}°` ); } } this._calibrationState = { isCalibrated: true, calibrationTime: new Date(), offsets }; console.log(`Robot ${this.id} calibrated successfully`); } catch (error) { console.error(`Calibration failed for robot ${this.id}:`, error); throw error; } } /** * Clear calibration */ clearCalibration(): void { this._calibrationState = { isCalibrated: false, offsets: {} }; this._joints.forEach((joint) => { joint.calibrationOffset = 0; }); console.log(`Cleared calibration for robot ${this.id}`); } // ============= LIFECYCLE ============= /** * Get joint by name */ getJoint(name: string): ManagedJointState | undefined { return this._joints.find((j) => j.name === name); } /** * Clean up resources */ async destroy(): Promise { // Clean up subscriptions this.unsubscribeCallbacks.forEach((unsub) => unsub()); this.unsubscribeCallbacks = []; // Remove master await this.removeMaster(); // Remove all slaves const slaveIds = this._slaves.map((s) => s.id); for (const slaveId of slaveIds) { await this.removeSlave(slaveId); } console.log(`Robot ${this.id} destroyed`); } }