RobotHub-Frontend / src /lib /elements /robot /calibration /CalibrationState.svelte.ts
blanchon's picture
Update
67a499d
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 = [];
}
}