Spaces:
Running
Running
import type { JointCalibration } from '../models.js'; | |
import { ROBOT_CONFIG } from '../config.js'; | |
export class CalibrationState { | |
// Reactive calibration state | |
isCalibrating = $state(false); | |
progress = $state(0); | |
// Joint calibration data | |
private jointCalibrations = $state<Record<string, JointCalibration>>({}); | |
private currentValues = $state<Record<string, number>>({}); | |
// Callbacks for completion with final positions | |
private completionCallbacks: Array<(positions: Record<string, number>) => void> = []; | |
constructor() { | |
// Initialize calibration data for expected joints | |
const jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"]; | |
jointNames.forEach(name => { | |
this.jointCalibrations[name] = { | |
isCalibrated: false, | |
minServoValue: undefined, | |
maxServoValue: undefined | |
}; | |
this.currentValues[name] = 0; | |
}); | |
} | |
// Computed properties | |
get needsCalibration(): boolean { | |
return Object.values(this.jointCalibrations).some(cal => !cal.isCalibrated); | |
} | |
get isCalibrated(): boolean { | |
return Object.values(this.jointCalibrations).every(cal => cal.isCalibrated); | |
} | |
// Update current servo value during calibration | |
updateCurrentValue(jointName: string, servoValue: number): void { | |
this.currentValues[jointName] = servoValue; | |
// Update calibration range if calibrating | |
if (this.isCalibrating) { | |
const calibration = this.jointCalibrations[jointName]; | |
if (calibration) { | |
// Update min/max values | |
if (calibration.minServoValue === undefined || servoValue < calibration.minServoValue) { | |
calibration.minServoValue = servoValue; | |
} | |
if (calibration.maxServoValue === undefined || servoValue > calibration.maxServoValue) { | |
calibration.maxServoValue = servoValue; | |
} | |
// Update progress based on range coverage | |
this.updateProgress(); | |
} | |
} | |
} | |
// Get current value for a joint | |
getCurrentValue(jointName: string): number | undefined { | |
return this.currentValues[jointName]; | |
} | |
// Get calibration data for a joint | |
getJointCalibration(jointName: string): JointCalibration | undefined { | |
return this.jointCalibrations[jointName]; | |
} | |
// Get formatted range string for display | |
getJointRange(jointName: string): string { | |
const calibration = this.jointCalibrations[jointName]; | |
if (!calibration || calibration.minServoValue === undefined || calibration.maxServoValue === undefined) { | |
return "Not set"; | |
} | |
return `${calibration.minServoValue}-${calibration.maxServoValue}`; | |
} | |
// Format servo value for display | |
formatServoValue(value: number | undefined): string { | |
return value !== undefined ? value.toString() : "---"; | |
} | |
// Start calibration process | |
startCalibration(): void { | |
console.log("[CalibrationState] Starting calibration..."); | |
this.isCalibrating = true; | |
this.progress = 0; | |
// Reset calibration data | |
Object.keys(this.jointCalibrations).forEach(jointName => { | |
this.jointCalibrations[jointName] = { | |
isCalibrated: false, | |
minServoValue: undefined, | |
maxServoValue: undefined | |
}; | |
}); | |
} | |
// Complete calibration and mark joints as calibrated | |
completeCalibration(): Record<string, number> { | |
console.log("[CalibrationState] Completing calibration..."); | |
const finalPositions: Record<string, number> = {}; | |
// Mark all joints with sufficient range as calibrated | |
Object.keys(this.jointCalibrations).forEach(jointName => { | |
const calibration = this.jointCalibrations[jointName]; | |
if (calibration.minServoValue !== undefined && calibration.maxServoValue !== undefined) { | |
const range = calibration.maxServoValue - calibration.minServoValue; | |
if (range >= ROBOT_CONFIG.calibration.minRangeThreshold) { | |
calibration.isCalibrated = true; | |
finalPositions[jointName] = this.currentValues[jointName] || 0; | |
console.log(`[CalibrationState] Joint ${jointName} calibrated: ${this.getJointRange(jointName)} (range: ${range})`); | |
} else { | |
console.warn(`[CalibrationState] Joint ${jointName} range too small: ${range} < ${ROBOT_CONFIG.calibration.minRangeThreshold}`); | |
} | |
} | |
}); | |
this.isCalibrating = false; | |
this.progress = 100; | |
// Notify completion callbacks | |
this.completionCallbacks.forEach(callback => { | |
try { | |
callback(finalPositions); | |
} catch (error) { | |
console.error("[CalibrationState] Error in completion callback:", error); | |
} | |
}); | |
return finalPositions; | |
} | |
// Cancel calibration | |
cancelCalibration(): void { | |
console.log("[CalibrationState] Calibration cancelled"); | |
this.isCalibrating = false; | |
this.progress = 0; | |
// Reset calibration data | |
Object.keys(this.jointCalibrations).forEach(jointName => { | |
this.jointCalibrations[jointName] = { | |
isCalibrated: false, | |
minServoValue: undefined, | |
maxServoValue: undefined | |
}; | |
}); | |
} | |
// Skip calibration (use predefined values) | |
skipCalibration(): void { | |
console.log("[CalibrationState] Skipping calibration with predefined values"); | |
// Set predefined calibration values for SO-100 arm | |
const predefinedCalibrations = { | |
"Rotation": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true }, | |
"Pitch": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true }, | |
"Elbow": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true }, | |
"Wrist_Pitch": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true }, | |
"Wrist_Roll": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true }, | |
"Jaw": { minServoValue: 1000, maxServoValue: 3000, isCalibrated: true } | |
}; | |
Object.entries(predefinedCalibrations).forEach(([jointName, calibration]) => { | |
this.jointCalibrations[jointName] = calibration; | |
}); | |
this.isCalibrating = false; | |
this.progress = 100; | |
} | |
// Convert raw servo value to normalized percentage (for USB INPUT - reading from servo) | |
normalizeValue(rawValue: number, jointName: string): number { | |
const calibration = this.jointCalibrations[jointName]; | |
if (!calibration || !calibration.isCalibrated || | |
calibration.minServoValue === undefined || calibration.maxServoValue === undefined) { | |
// No calibration, use appropriate default conversion | |
const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper'; | |
if (isGripper) { | |
return Math.max(0, Math.min(100, (rawValue / 4095) * 100)); | |
} else { | |
return Math.max(-100, Math.min(100, ((rawValue - 2048) / 2048) * 100)); | |
} | |
} | |
const { minServoValue, maxServoValue } = calibration; | |
if (maxServoValue === minServoValue) return 0; | |
// Bound the input servo value to calibrated range | |
const bounded = Math.max(minServoValue, Math.min(maxServoValue, rawValue)); | |
const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper'; | |
if (isGripper) { | |
// Gripper: 0-100% | |
return ((bounded - minServoValue) / (maxServoValue - minServoValue)) * 100; | |
} else { | |
// Regular joint: -100 to +100% | |
return (((bounded - minServoValue) / (maxServoValue - minServoValue)) * 200) - 100; | |
} | |
} | |
// Convert normalized percentage to raw servo value (for USB OUTPUT - writing to servo) | |
denormalizeValue(normalizedValue: number, jointName: string): number { | |
const calibration = this.jointCalibrations[jointName]; | |
if (!calibration || !calibration.isCalibrated || | |
calibration.minServoValue === undefined || calibration.maxServoValue === undefined) { | |
// No calibration, use appropriate default conversion | |
const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper'; | |
if (isGripper) { | |
return Math.round((normalizedValue / 100) * 4095); | |
} else { | |
return Math.round(2048 + (normalizedValue / 100) * 2048); | |
} | |
} | |
const { minServoValue, maxServoValue } = calibration; | |
const range = maxServoValue - minServoValue; | |
let normalizedRatio: number; | |
const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper'; | |
if (isGripper) { | |
// Gripper: 0-100% -> 0-1 | |
normalizedRatio = Math.max(0, Math.min(1, normalizedValue / 100)); | |
} else { | |
// Regular joint: -100 to +100% -> 0-1 | |
normalizedRatio = Math.max(0, Math.min(1, (normalizedValue + 100) / 200)); | |
} | |
return Math.round(minServoValue + normalizedRatio * range); | |
} | |
// Register callback for calibration completion with final positions | |
onCalibrationCompleteWithPositions(callback: (positions: Record<string, number>) => void): () => void { | |
this.completionCallbacks.push(callback); | |
// Return unsubscribe function | |
return () => { | |
const index = this.completionCallbacks.indexOf(callback); | |
if (index >= 0) { | |
this.completionCallbacks.splice(index, 1); | |
} | |
}; | |
} | |
// Update progress based on calibration coverage | |
private updateProgress(): void { | |
if (!this.isCalibrating) return; | |
let totalProgress = 0; | |
let jointCount = 0; | |
Object.values(this.jointCalibrations).forEach(calibration => { | |
jointCount++; | |
if (calibration.minServoValue !== undefined && calibration.maxServoValue !== undefined) { | |
const range = calibration.maxServoValue - calibration.minServoValue; | |
// Progress is based on range size (more range = more progress) | |
const jointProgress = Math.min(100, (range / ROBOT_CONFIG.calibration.minRangeThreshold) * 100); | |
totalProgress += jointProgress; | |
} | |
}); | |
this.progress = jointCount > 0 ? totalProgress / jointCount : 0; | |
} | |
// Cleanup | |
destroy(): void { | |
this.completionCallbacks = []; | |
} | |
} |