Spaces:
Running
Running
/** | |
* Advanced Robot Optimization Utilities | |
* Inspired by lerobot techniques for high-performance robot control | |
*/ | |
import { getSafetyConfig, getTimingConfig, getLoggingConfig } from "$lib/configs/performanceConfig"; | |
import type { RobotCommand } from "$lib/types/robotDriver"; | |
/** | |
* Performance timing utilities (inspired by lerobot's perf_counter usage) | |
*/ | |
export class PerformanceTimer { | |
private startTime: number; | |
private logs: Map<string, number> = new Map(); | |
constructor() { | |
this.startTime = performance.now(); | |
} | |
/** | |
* Mark a timing checkpoint | |
*/ | |
mark(label: string): void { | |
const now = performance.now(); | |
this.logs.set(label, now - this.startTime); | |
this.startTime = now; | |
} | |
/** | |
* Get timing for a specific label | |
*/ | |
getTime(label: string): number | undefined { | |
return this.logs.get(label); | |
} | |
/** | |
* Get all timing data | |
*/ | |
getAllTimings(): Map<string, number> { | |
return new Map(this.logs); | |
} | |
/** | |
* Log performance data in lerobot style | |
*/ | |
logPerformance(prefix: string = "robot"): void { | |
if (!getLoggingConfig().ENABLE_TIMING_MEASUREMENTS) return; | |
const items: string[] = []; | |
for (const [label, timeMs] of this.logs) { | |
const hz = 1000 / timeMs; | |
items.push(`${label}:${timeMs.toFixed(2)}ms (${hz.toFixed(1)}Hz)`); | |
} | |
if (items.length > 0) { | |
console.log(`${prefix} ${items.join(" ")}`); | |
} | |
} | |
/** | |
* Reset timing data | |
*/ | |
reset(): void { | |
this.logs.clear(); | |
this.startTime = performance.now(); | |
} | |
} | |
/** | |
* Safety position clamping (inspired by lerobot's ensure_safe_goal_position) | |
* Prevents sudden large movements that could damage the robot | |
*/ | |
export function ensureSafeGoalPosition( | |
goalPosition: number, | |
currentPosition: number, | |
maxRelativeTarget?: number | |
): number { | |
const safetyConfig = getSafetyConfig(); | |
if (!safetyConfig.ENABLE_POSITION_CLAMPING) { | |
return goalPosition; | |
} | |
const maxTarget = maxRelativeTarget || safetyConfig.MAX_RELATIVE_TARGET_DEG; | |
const deltaPosition = goalPosition - currentPosition; | |
// Check for emergency stop condition | |
if (Math.abs(deltaPosition) > safetyConfig.EMERGENCY_STOP_THRESHOLD_DEG) { | |
console.warn( | |
`⚠️ Emergency stop: movement too large (${deltaPosition.toFixed(1)}° > ${safetyConfig.EMERGENCY_STOP_THRESHOLD_DEG}°)` | |
); | |
return currentPosition; // Don't move at all | |
} | |
// Clamp to maximum relative movement | |
if (Math.abs(deltaPosition) > maxTarget) { | |
const clampedPosition = currentPosition + Math.sign(deltaPosition) * maxTarget; | |
console.log( | |
`🛡️ Safety clamp: ${goalPosition.toFixed(1)}° → ${clampedPosition.toFixed(1)}° (max: ±${maxTarget}°)` | |
); | |
return clampedPosition; | |
} | |
return goalPosition; | |
} | |
/** | |
* Velocity limiting (inspired by lerobot's joint velocity constraints) | |
*/ | |
export function limitJointVelocity( | |
currentPosition: number, | |
goalPosition: number, | |
deltaTimeMs: number, | |
maxVelocityDegS?: number | |
): number { | |
const safetyConfig = getSafetyConfig(); | |
const maxVel = maxVelocityDegS || safetyConfig.MAX_JOINT_VELOCITY_DEG_S; | |
const deltaPosition = goalPosition - currentPosition; | |
const deltaTimeS = deltaTimeMs / 1000; | |
const requiredVelocity = Math.abs(deltaPosition) / deltaTimeS; | |
if (requiredVelocity > maxVel) { | |
const maxMovement = maxVel * deltaTimeS; | |
const limitedPosition = currentPosition + Math.sign(deltaPosition) * maxMovement; | |
console.log( | |
`🐌 Velocity limit: ${requiredVelocity.toFixed(1)}°/s → ${maxVel}°/s (pos: ${limitedPosition.toFixed(1)}°)` | |
); | |
return limitedPosition; | |
} | |
return goalPosition; | |
} | |
/** | |
* Busy wait for precise timing (inspired by lerobot's busy_wait function) | |
* More accurate than setTimeout for high-frequency control loops | |
*/ | |
export async function busyWait(durationMs: number): Promise<void> { | |
const timingConfig = getTimingConfig(); | |
if (!timingConfig.USE_BUSY_WAIT || durationMs <= 0) { | |
return; | |
} | |
const startTime = performance.now(); | |
const targetTime = startTime + durationMs; | |
// Use a combination of setTimeout and busy waiting for efficiency | |
if (durationMs > 5) { | |
// For longer waits, use setTimeout for most of the duration | |
await new Promise((resolve) => setTimeout(resolve, durationMs - 2)); | |
} | |
// Busy wait for the remaining time for high precision | |
while (performance.now() < targetTime) { | |
// Busy loop - more accurate than setTimeout for short durations | |
} | |
} | |
/** | |
* Frame rate controller with precise timing | |
*/ | |
export class FrameRateController { | |
private lastFrameTime: number; | |
private targetFrameTimeMs: number; | |
private frameCount: number = 0; | |
private performanceTimer: PerformanceTimer; | |
constructor(targetFps: number) { | |
this.targetFrameTimeMs = 1000 / targetFps; | |
this.lastFrameTime = performance.now(); | |
this.performanceTimer = new PerformanceTimer(); | |
} | |
/** | |
* Wait until the next frame should start | |
*/ | |
async waitForNextFrame(): Promise<void> { | |
const now = performance.now(); | |
const elapsed = now - this.lastFrameTime; | |
const remaining = this.targetFrameTimeMs - elapsed; | |
if (remaining > 0) { | |
await busyWait(remaining); | |
} | |
this.lastFrameTime = performance.now(); | |
this.frameCount++; | |
// Log performance periodically | |
if (this.frameCount % 60 === 0) { | |
const actualFrameTime = now - this.lastFrameTime + remaining; | |
const actualFps = 1000 / actualFrameTime; | |
this.performanceTimer.logPerformance(`frame_rate: ${actualFps.toFixed(1)}fps`); | |
} | |
} | |
/** | |
* Get current frame timing info | |
*/ | |
getFrameInfo(): { frameCount: number; actualFps: number; targetFps: number } { | |
const now = performance.now(); | |
const actualFrameTime = now - this.lastFrameTime; | |
const actualFps = 1000 / actualFrameTime; | |
const targetFps = 1000 / this.targetFrameTimeMs; | |
return { | |
frameCount: this.frameCount, | |
actualFps, | |
targetFps | |
}; | |
} | |
} | |
/** | |
* Batch command processor for efficient joint updates | |
*/ | |
export class BatchCommandProcessor { | |
private commandQueue: RobotCommand[] = []; | |
private batchSize: number; | |
private processingInterval?: number; | |
constructor(batchSize: number = 10, processingIntervalMs: number = 16) { | |
this.batchSize = batchSize; | |
this.startProcessing(processingIntervalMs); | |
} | |
/** | |
* Add a command to the batch queue | |
*/ | |
queueCommand(command: RobotCommand): void { | |
this.commandQueue.push(command); | |
// Process immediately if batch is full | |
if (this.commandQueue.length >= this.batchSize) { | |
this.processBatch(); | |
} | |
} | |
/** | |
* Process a batch of commands | |
*/ | |
private processBatch(): RobotCommand[] { | |
if (this.commandQueue.length === 0) return []; | |
const batch = this.commandQueue.splice(0, this.batchSize); | |
// Merge commands for the same joints (latest wins) | |
const mergedJoints = new Map<string, { name: string; value: number }>(); | |
for (const command of batch) { | |
for (const joint of command.joints) { | |
mergedJoints.set(joint.name, joint); | |
} | |
} | |
const mergedCommand: RobotCommand = { | |
timestamp: Date.now(), | |
joints: Array.from(mergedJoints.values()), | |
metadata: { source: "batch_processor", batchSize: batch.length } | |
}; | |
return [mergedCommand]; | |
} | |
/** | |
* Start automatic batch processing | |
*/ | |
private startProcessing(intervalMs: number): void { | |
this.processingInterval = setInterval(() => { | |
if (this.commandQueue.length > 0) { | |
this.processBatch(); | |
} | |
}, intervalMs); | |
} | |
/** | |
* Stop batch processing | |
*/ | |
stop(): void { | |
if (this.processingInterval) { | |
clearInterval(this.processingInterval); | |
this.processingInterval = undefined; | |
} | |
} | |
/** | |
* Get queue status | |
*/ | |
getQueueStatus(): { queueLength: number; batchSize: number } { | |
return { | |
queueLength: this.commandQueue.length, | |
batchSize: this.batchSize | |
}; | |
} | |
} | |
/** | |
* Connection health monitor | |
*/ | |
export class ConnectionHealthMonitor { | |
private lastSuccessTime: number; | |
private errorCount: number = 0; | |
private healthCheckInterval?: number; | |
constructor(healthCheckIntervalMs: number = 1000) { | |
this.lastSuccessTime = Date.now(); | |
this.startHealthCheck(healthCheckIntervalMs); | |
} | |
/** | |
* Report a successful operation | |
*/ | |
reportSuccess(): void { | |
this.lastSuccessTime = Date.now(); | |
this.errorCount = 0; | |
} | |
/** | |
* Report an error | |
*/ | |
reportError(): void { | |
this.errorCount++; | |
} | |
/** | |
* Check if connection is healthy | |
*/ | |
isHealthy(maxErrorCount: number = 5, maxSilenceMs: number = 5000): boolean { | |
const timeSinceLastSuccess = Date.now() - this.lastSuccessTime; | |
return this.errorCount < maxErrorCount && timeSinceLastSuccess < maxSilenceMs; | |
} | |
/** | |
* Get health metrics | |
*/ | |
getHealthMetrics(): { errorCount: number; timeSinceLastSuccessMs: number; isHealthy: boolean } { | |
return { | |
errorCount: this.errorCount, | |
timeSinceLastSuccessMs: Date.now() - this.lastSuccessTime, | |
isHealthy: this.isHealthy() | |
}; | |
} | |
/** | |
* Start health monitoring | |
*/ | |
private startHealthCheck(intervalMs: number): void { | |
this.healthCheckInterval = setInterval(() => { | |
const metrics = this.getHealthMetrics(); | |
if (!metrics.isHealthy) { | |
console.warn( | |
`⚠️ Connection health warning: ${metrics.errorCount} errors, ${metrics.timeSinceLastSuccessMs}ms since last success` | |
); | |
} | |
}, intervalMs); | |
} | |
/** | |
* Stop health monitoring | |
*/ | |
stop(): void { | |
if (this.healthCheckInterval) { | |
clearInterval(this.healthCheckInterval); | |
this.healthCheckInterval = undefined; | |
} | |
} | |
} | |
/** | |
* Async operation timeout utility | |
*/ | |
export function withTimeout<T>( | |
promise: Promise<T>, | |
timeoutMs: number, | |
operation: string = "operation" | |
): Promise<T> { | |
return Promise.race([ | |
promise, | |
new Promise<never>((_, reject) => | |
setTimeout(() => reject(new Error(`${operation} timed out after ${timeoutMs}ms`)), timeoutMs) | |
) | |
]); | |
} | |