blanchon's picture
Update
6ce4ca6
raw
history blame
18.5 kB
import type { JointCalibration, CalibrationState } from '../models.js';
import { scsServoSDK } from "feetech.js";
import { ROBOT_CONFIG } from '../config.js';
export class USBCalibrationManager {
// Joint configuration
private readonly jointIds = [1, 2, 3, 4, 5, 6];
private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"];
// Calibration state
private jointCalibrations: Record<string, JointCalibration> = {};
private _calibrationState: CalibrationState = {
isCalibrating: false,
progress: 0
};
// Connection state for calibration
private isConnectedForCalibration = false;
private baudRate: number = 1000000;
// Calibration polling
private calibrationPollingAbortController: AbortController | null = null;
private lastPositions: Record<string, number> = {};
private calibrationCallbacks: (() => void)[] = [];
// Calibration completion callback with final positions
private calibrationCompleteCallback: ((finalPositions: Record<string, number>) => void) | null = null;
// Servo reading queue for calibration
private isReadingServos = false;
private readingQueue: Array<{
servoId: number;
resolve: (value: number) => void;
reject: (error: Error) => void;
}> = [];
constructor(baudRate: number = ROBOT_CONFIG.usb.baudRate) {
this.baudRate = baudRate;
// Initialize joint calibrations
this.jointNames.forEach(name => {
this.jointCalibrations[name] = { isCalibrated: false };
});
}
// Getters
get isCalibrated(): boolean {
return Object.values(this.jointCalibrations).every(cal => cal.isCalibrated);
}
get needsCalibration(): boolean {
return !this.isCalibrated;
}
get calibrationState(): CalibrationState {
return this._calibrationState;
}
get jointNames_(): string[] {
return [...this.jointNames];
}
// Connection management for calibration
async ensureConnectedForCalibration(): Promise<void> {
if (this.isConnectedForCalibration) {
console.log('[USBCalibrationManager] Already connected for calibration');
return;
}
try {
console.log('[USBCalibrationManager] Connecting SDK for calibration...');
await scsServoSDK.connect({ baudRate: this.baudRate });
this.isConnectedForCalibration = true;
console.log('[USBCalibrationManager] Connected successfully for calibration');
} catch (error) {
console.error('[USBCalibrationManager] Failed to connect SDK for calibration:', error);
throw error;
}
}
async disconnectFromCalibration(): Promise<void> {
if (!this.isConnectedForCalibration) return;
try {
await scsServoSDK.disconnect();
this.isConnectedForCalibration = false;
console.log('[USBCalibrationManager] Disconnected from calibration');
} catch (error) {
console.warn('[USBCalibrationManager] Failed to disconnect from calibration:', error);
}
}
// Check if the SDK is currently connected (for external use)
get isSDKConnected(): boolean {
return this.isConnectedForCalibration && scsServoSDK.isConnected();
}
// Calibration methods
async startCalibration(): Promise<void> {
if (this._calibrationState.isCalibrating) {
console.warn('[USBCalibrationManager] Calibration already in progress');
return;
}
console.log('[USBCalibrationManager] Starting calibration process');
// Ensure connection for calibration
await this.ensureConnectedForCalibration();
// Unlock all servos for calibration (allow manual movement)
console.log('[USBCalibrationManager] 🔓 Unlocking all servos for calibration...');
try {
await scsServoSDK.unlockServos(this.jointIds);
console.log('[USBCalibrationManager] ✅ All servos unlocked for manual movement during calibration');
} catch (error) {
console.warn('[USBCalibrationManager] Warning: Failed to unlock some servos for calibration:', error);
}
this._calibrationState = {
isCalibrating: true,
progress: 0
};
// Initialize calibrations with current values
this.jointCalibrations = {};
this.jointNames.forEach(name => {
const currentValue = this.lastPositions[name] || 2048;
this.jointCalibrations[name] = {
isCalibrated: false,
minServoValue: currentValue,
maxServoValue: currentValue
};
});
this.startCalibrationPolling();
this.notifyCalibrationChange();
}
async stopCalibration(): Promise<void> {
console.log('[USBCalibrationManager] Stopping calibration');
this._calibrationState = {
isCalibrating: false,
progress: 100
};
// Mark all joints as calibrated
this.jointNames.forEach(name => {
if (this.jointCalibrations[name]) {
this.jointCalibrations[name].isCalibrated = true;
}
});
this.stopCalibrationPolling();
// NEW: Read final positions and sync to virtual robot before locking
console.log('[USBCalibrationManager] 📍 Reading final servo positions for virtual robot sync...');
try {
const finalPositions = await this.readFinalPositionsAndSync();
console.log('[USBCalibrationManager] ✅ Final positions read and synced to virtual robot');
// Notify robot of calibration completion with final positions
if (this.calibrationCompleteCallback) {
this.calibrationCompleteCallback(finalPositions);
}
} catch (error) {
console.error('[USBCalibrationManager] Failed to read final positions:', error);
}
this.notifyCalibrationChange();
// Keep connection open - don't disconnect automatically
// The connection will be reused by USB drivers
console.log('[USBCalibrationManager] Calibration complete, keeping connection for drivers');
}
skipCalibration(): void {
console.log('[USBCalibrationManager] Skipping calibration, using full range');
// Set full range for all joints
this.jointNames.forEach(name => {
this.jointCalibrations[name] = {
isCalibrated: true,
minServoValue: 0,
maxServoValue: 4095
};
});
this._calibrationState = {
isCalibrating: false,
progress: 100
};
this.notifyCalibrationChange();
}
// NEW: Set predefined calibration values
async setPredefinedCalibration(): Promise<void> {
console.log('[USBCalibrationManager] Setting predefined calibration values');
// Ensure SDK connection for hardware access
await this.ensureConnectedForCalibration();
// Predefined calibration values based on known good robot configuration
const predefinedValues: Record<string, { min: number; max: number; current: number }> = {
"Rotation": { current: 2180, min: 764, max: 3388 },
"Pitch": { current: 1159, min: 1138, max: 3501 },
"Elbow": { current: 2874, min: 660, max: 2876 },
"Wrist_Pitch": { current: 2138, min: 762, max: 3075 },
"Wrist_Roll": { current: 2081, min: 154, max: 3995 },
"Jaw": { current: 2061, min: 2013, max: 3555 }
};
// Set calibration values for all joints
this.jointNames.forEach(name => {
const values = predefinedValues[name];
if (values) {
this.jointCalibrations[name] = {
isCalibrated: true,
minServoValue: values.min,
maxServoValue: values.max
};
// Set current position for reference
this.lastPositions[name] = values.current;
}
});
this._calibrationState = {
isCalibrating: false,
progress: 100
};
this.notifyCalibrationChange();
console.log('[USBCalibrationManager] Predefined calibration values applied successfully');
}
// NEW: Read final positions and prepare for sync
private async readFinalPositionsAndSync(): Promise<Record<string, number>> {
const finalPositions: Record<string, number> = {};
console.log('[USBCalibrationManager] Reading final positions from all servos...');
// Read all servo positions sequentially
for (let i = 0; i < this.jointIds.length; i++) {
const servoId = this.jointIds[i];
const jointName = this.jointNames[i];
try {
const position = await this.readServoPosition(servoId);
finalPositions[jointName] = position;
this.lastPositions[jointName] = position;
console.log(`[USBCalibrationManager] ${jointName} (servo ${servoId}): ${position} (raw) -> ${this.normalizeValue(position, jointName).toFixed(1)}% (normalized)`);
} catch (error) {
console.warn(`[USBCalibrationManager] Failed to read final position for ${jointName} (servo ${servoId}):`, error);
// Use last known position as fallback
finalPositions[jointName] = this.lastPositions[jointName] || 2048;
}
}
return finalPositions;
}
// NEW: Set callback for calibration completion with final positions
onCalibrationCompleteWithPositions(callback: (finalPositions: Record<string, number>) => void): () => void {
this.calibrationCompleteCallback = callback;
return () => {
this.calibrationCompleteCallback = null;
};
}
// Post-calibration servo locking methods
async lockServosForProduction(): Promise<void> {
if (!this.isSDKConnected) {
throw new Error('SDK not connected - cannot lock servos');
}
console.log('[USBCalibrationManager] 🔒 Locking all servos for production use (robot control)...');
try {
// Use the new lockServosForProduction function that both locks and enables torque
await scsServoSDK.lockServosForProduction(this.jointIds);
console.log('[USBCalibrationManager] ✅ All servos locked for production - robot is now controlled and cannot be moved manually');
} catch (error) {
console.error('[USBCalibrationManager] Failed to lock servos for production:', error);
throw error;
}
}
async keepServosUnlockedForConsumer(): Promise<void> {
if (!this.isSDKConnected) {
console.log('[USBCalibrationManager] SDK not connected - servos remain in current state');
return;
}
console.log('[USBCalibrationManager] 🔓 Keeping servos unlocked for consumer use (reading positions)...');
try {
// Ensure servos are unlocked for reading
await scsServoSDK.unlockServos(this.jointIds);
console.log('[USBCalibrationManager] ✅ All servos remain unlocked for consumer - can be moved manually and positions read');
} catch (error) {
console.warn('[USBCalibrationManager] Warning: Failed to ensure servos are unlocked for consumer:', error);
}
}
// Data access methods
getCurrentRawValue(jointName: string): number | undefined {
return this.lastPositions[jointName];
}
getJointCalibration(jointName: string): JointCalibration | undefined {
return this.jointCalibrations[jointName];
}
getJointRange(jointName: string): number {
const calibration = this.jointCalibrations[jointName];
if (!calibration?.minServoValue || !calibration?.maxServoValue) return 0;
return Math.abs(calibration.maxServoValue - calibration.minServoValue);
}
getAllCalibrations(): Record<string, JointCalibration> {
return { ...this.jointCalibrations };
}
// Value conversion methods
normalizeValue(servoValue: number, jointName: string): number {
const calibration = this.jointCalibrations[jointName];
const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper';
if (!calibration?.isCalibrated || !calibration.minServoValue || !calibration.maxServoValue) {
if (isGripper) {
return (servoValue / 4095) * 100;
} else {
return ((servoValue - 2048) / 2048) * 100;
}
}
const { minServoValue, maxServoValue } = calibration;
if (maxServoValue === minServoValue) return 0;
const bounded = Math.max(minServoValue, Math.min(maxServoValue, servoValue));
if (isGripper) {
return ((bounded - minServoValue) / (maxServoValue - minServoValue)) * 100;
} else {
return (((bounded - minServoValue) / (maxServoValue - minServoValue)) * 200) - 100;
}
}
denormalizeValue(normalizedValue: number, jointName: string): number {
const calibration = this.jointCalibrations[jointName];
const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper';
if (!calibration?.isCalibrated || !calibration.minServoValue || !calibration.maxServoValue) {
// No calibration, use appropriate default conversion
if (isGripper) {
return Math.round((normalizedValue / 100) * 4095);
} else {
return Math.round(2048 + (normalizedValue / 100) * 2048);
}
}
const { minServoValue, maxServoValue } = calibration;
let ratio: number;
if (isGripper) {
ratio = normalizedValue / 100;
} else {
ratio = (normalizedValue + 100) / 200;
}
const servoValue = minServoValue + ratio * (maxServoValue - minServoValue);
return Math.round(Math.max(minServoValue, Math.min(maxServoValue, servoValue)));
}
// Event handling
onCalibrationChange(callback: () => void): () => void {
this.calibrationCallbacks.push(callback);
return () => {
const index = this.calibrationCallbacks.indexOf(callback);
if (index >= 0) {
this.calibrationCallbacks.splice(index, 1);
}
};
}
// Format servo value for display
formatServoValue(value: number | undefined): string {
return value !== undefined ? value.toString() : '---';
}
// Cleanup
async destroy(): Promise<void> {
console.log('[USBCalibrationManager] 🧹 Destroying calibration manager...');
this.stopCalibrationPolling();
// Safely unlock all servos before disconnecting (best practice)
if (this.isSDKConnected) {
try {
console.log('[USBCalibrationManager] 🔓 Safely unlocking all servos before cleanup...');
await scsServoSDK.unlockServosForManualMovement(this.jointIds);
console.log('[USBCalibrationManager] ✅ All servos safely unlocked for manual movement');
} catch (error) {
console.warn('[USBCalibrationManager] Warning: Failed to safely unlock servos during cleanup:', error);
}
}
await this.disconnectFromCalibration();
this.calibrationCallbacks = [];
this.calibrationCompleteCallback = null;
console.log('[USBCalibrationManager] ✅ Calibration manager destroyed');
}
// Private methods
private async readServoPosition(servoId: number): Promise<number> {
return new Promise((resolve, reject) => {
this.readingQueue.push({ servoId, resolve, reject });
this.processReadingQueue();
});
}
private async processReadingQueue(): Promise<void> {
if (this.isReadingServos || this.readingQueue.length === 0) {
return;
}
this.isReadingServos = true;
try {
const batch = [...this.readingQueue];
this.readingQueue = [];
for (const { servoId, resolve, reject } of batch) {
try {
const position = await scsServoSDK.readPosition(servoId);
resolve(position);
} catch (error) {
reject(error instanceof Error ? error : new Error(`Failed to read servo ${servoId}`));
}
await new Promise(resolve => setTimeout(resolve, 5));
}
} finally {
this.isReadingServos = false;
if (this.readingQueue.length > 0) {
setTimeout(() => this.processReadingQueue(), 50);
}
}
}
private async startCalibrationPolling(): Promise<void> {
this.stopCalibrationPolling();
this.calibrationPollingAbortController = new AbortController();
const signal = this.calibrationPollingAbortController.signal;
console.log('[USBCalibrationManager] Starting calibration polling');
try {
while (!signal.aborted && this._calibrationState.isCalibrating) {
const readPromises = this.jointIds.map(async (servoId, i) => {
if (signal.aborted) return null;
const jointName = this.jointNames[i];
try {
const currentValue = await this.readServoPosition(servoId);
return { jointName, currentValue };
} catch (error) {
return null;
}
});
const results = await Promise.all(readPromises);
let hasUpdates = false;
results.forEach(result => {
if (!result) return;
const { jointName, currentValue } = result;
this.lastPositions[jointName] = currentValue;
const calibration = this.jointCalibrations[jointName];
if (calibration) {
if (currentValue < calibration.minServoValue!) {
calibration.minServoValue = currentValue;
hasUpdates = true;
}
if (currentValue > calibration.maxServoValue!) {
calibration.maxServoValue = currentValue;
hasUpdates = true;
}
}
});
if (hasUpdates) {
this.notifyCalibrationChange();
}
// Calculate progress
const totalRangeNeeded = 500;
let totalRangeDiscovered = 0;
this.jointNames.forEach(name => {
const calibration = this.jointCalibrations[name];
if (calibration?.minServoValue !== undefined && calibration?.maxServoValue !== undefined) {
totalRangeDiscovered += Math.abs(calibration.maxServoValue - calibration.minServoValue);
}
});
const newProgress = Math.min(100, (totalRangeDiscovered / (totalRangeNeeded * this.jointNames.length)) * 100);
if (Math.abs(newProgress - this._calibrationState.progress) > 1) {
this._calibrationState.progress = newProgress;
this.notifyCalibrationChange();
}
await new Promise(resolve => setTimeout(resolve, 10));
}
} catch (error) {
if (!signal.aborted) {
console.error('[USBCalibrationManager] Calibration polling error:', error);
}
}
}
private stopCalibrationPolling(): void {
if (this.calibrationPollingAbortController) {
this.calibrationPollingAbortController.abort();
this.calibrationPollingAbortController = null;
}
}
private notifyCalibrationChange(): void {
this.calibrationCallbacks.forEach(callback => {
try {
callback();
} catch (error) {
console.error('[USBCalibrationManager] Error in calibration callback:', error);
}
});
}
}