blanchon's picture
Update
d4d25f7
import type { ConnectionStatus, USBDriverConfig } from '../models.js';
import { CalibrationState } from '../calibration/CalibrationState.svelte.js';
import { ScsServoSDK } from 'feetech.js';
import { ROBOT_CONFIG } from '../config.js';
export abstract class USBServoDriver {
readonly id: string;
readonly name: string;
readonly config: USBDriverConfig;
protected _status: ConnectionStatus = { isConnected: false };
protected statusCallbacks: ((status: ConnectionStatus) => void)[] = [];
protected scsServoSDK: ScsServoSDK | null = null;
// Calibration state - directly embedded
readonly calibrationState: CalibrationState;
// Calibration polling
private calibrationPollingInterval: ReturnType<typeof setInterval> | null = null;
// Joint to servo ID mapping for SO-100 arm
protected readonly jointToServoMap = {
"Rotation": 1,
"Pitch": 2,
"Elbow": 3,
"Wrist_Pitch": 4,
"Wrist_Roll": 5,
"Jaw": 6
};
constructor(config: USBDriverConfig, driverType: string) {
this.config = config;
this.id = `usb-${driverType}-${Date.now()}`;
this.name = `USB ${driverType}`;
this.calibrationState = new CalibrationState();
}
get status(): ConnectionStatus {
return this._status;
}
get needsCalibration(): boolean {
return this.calibrationState.needsCalibration;
}
get isCalibrating(): boolean {
return this.calibrationState.isCalibrating;
}
get isCalibrated(): boolean {
return this.calibrationState.isCalibrated;
}
// Type guard to check if a driver is a USB driver
static isUSBDriver(driver: any): driver is USBServoDriver {
return driver && typeof driver.calibrationState === 'object' &&
typeof driver.needsCalibration === 'boolean' &&
typeof driver.isCalibrated === 'boolean' &&
typeof driver.startCalibration === 'function';
}
// Type-safe method to get calibration interface
getCalibrationInterface(): {
needsCalibration: boolean;
isCalibrating: boolean;
isCalibrated: boolean;
startCalibration: () => Promise<void>;
completeCalibration: () => Promise<Record<string, number>>;
skipCalibration: () => void;
cancelCalibration: () => void;
onCalibrationCompleteWithPositions: (callback: (positions: Record<string, number>) => void) => () => void;
} {
return {
needsCalibration: this.needsCalibration,
isCalibrating: this.isCalibrating,
isCalibrated: this.isCalibrated,
startCalibration: () => this.startCalibration(),
completeCalibration: () => this.completeCalibration(),
skipCalibration: () => this.skipCalibration(),
cancelCalibration: () => this.cancelCalibration(),
onCalibrationCompleteWithPositions: (callback) => this.onCalibrationCompleteWithPositions(callback)
};
}
// Abstract methods that subclasses must implement
abstract connect(): Promise<void>;
abstract disconnect(): Promise<void>;
// Common connection logic
protected async connectToUSB(): Promise<void> {
if (this._status.isConnected) {
console.log(`[${this.name}] Already connected`);
return;
}
try {
console.log(`[${this.name}] Connecting...`);
// Create a new SDK instance for this driver instead of using the singleton
// This allows multiple drivers to connect to different ports simultaneously
this.scsServoSDK = new ScsServoSDK();
await this.scsServoSDK.connect({
baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate,
protocolEnd: 0 // STS/SMS protocol
});
this._status = { isConnected: true, lastConnected: new Date() };
this.notifyStatusChange();
console.log(`[${this.name}] Connected successfully`);
// Debug: Log SDK instance methods to identify the issue
console.log(`[${this.name}] SDK instance methods:`, Object.getOwnPropertyNames(this.scsServoSDK));
console.log(`[${this.name}] SDK prototype methods:`, Object.getOwnPropertyNames(Object.getPrototypeOf(this.scsServoSDK)));
console.log(`[${this.name}] writeTorqueEnable available:`, typeof this.scsServoSDK.writeTorqueEnable);
console.log(`[${this.name}] syncReadPositions available:`, typeof this.scsServoSDK.syncReadPositions);
} catch (error) {
console.error(`[${this.name}] Connection failed:`, error);
this._status = { isConnected: false, error: `Connection failed: ${error}` };
this.notifyStatusChange();
throw error;
}
}
protected async disconnectFromUSB(): Promise<void> {
if (this.scsServoSDK) {
try {
await this.unlockAllServos();
await this.scsServoSDK.disconnect();
} catch (error) {
console.warn(`[${this.name}] Error during disconnect:`, error);
}
this.scsServoSDK = null;
}
this._status = { isConnected: false };
this.notifyStatusChange();
console.log(`[${this.name}] Disconnected`);
}
// Calibration methods
async startCalibration(): Promise<void> {
if (!this._status.isConnected) {
await this.connectToUSB();
}
if (!this._status.isConnected) {
throw new Error('Cannot start calibration: not connected');
}
console.log(`[${this.name}] Starting calibration...`);
this.calibrationState.startCalibration();
// Unlock servos for manual movement during calibration
await this.unlockAllServos();
// Start polling positions during calibration
await this.startCalibrationPolling();
}
async completeCalibration(): Promise<Record<string, number>> {
if (!this.isCalibrating) {
throw new Error('Not currently calibrating');
}
// Stop polling
this.stopCalibrationPolling();
// Read final positions
const finalPositions = await this.readCurrentPositions();
// Complete calibration state
this.calibrationState.completeCalibration();
console.log(`[${this.name}] Calibration completed`);
return finalPositions;
}
skipCalibration(): void {
// Stop polling if active
this.stopCalibrationPolling();
this.calibrationState.skipCalibration();
}
async setPredefinedCalibration(): Promise<void> {
// Stop polling if active
this.stopCalibrationPolling();
this.skipCalibration();
}
// Cancel calibration
cancelCalibration(): void {
// Stop polling if active
this.stopCalibrationPolling();
this.calibrationState.cancelCalibration();
}
// Start polling servo positions during calibration
private async startCalibrationPolling(): Promise<void> {
if (this.calibrationPollingInterval !== null) {
return; // Already polling
}
console.log(`[${this.name}] Starting calibration position polling...`);
// Poll positions every 100ms during calibration
this.calibrationPollingInterval = setInterval(async () => {
if (!this.isCalibrating || !this._status.isConnected || !this.scsServoSDK) {
this.stopCalibrationPolling();
return;
}
try {
// Read positions for all servos
const servoIds = Object.values(this.jointToServoMap);
const positions = await this.scsServoSDK.syncReadPositions(servoIds);
// Update calibration state with current positions
Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => {
const position = positions.get(servoId);
if (position !== undefined) {
this.calibrationState.updateCurrentValue(jointName, position);
console.debug(`[${this.name}] ${jointName} (servo ${servoId}): ${position}`);
}
});
} catch (error) {
console.warn(`[${this.name}] Calibration polling error:`, error);
// Continue polling despite errors - user might be moving servos rapidly
}
}, 100); // Poll every 100ms
}
// Stop polling servo positions
private stopCalibrationPolling(): void {
if (this.calibrationPollingInterval !== null) {
clearInterval(this.calibrationPollingInterval);
this.calibrationPollingInterval = null;
console.log(`[${this.name}] Stopped calibration position polling`);
}
}
// Servo position reading (for calibration)
async readCurrentPositions(): Promise<Record<string, number>> {
if (!this.scsServoSDK || !this._status.isConnected) {
throw new Error('Cannot read positions: not connected');
}
const positions: Record<string, number> = {};
try {
const servoIds = Object.values(this.jointToServoMap);
const servoPositions = await this.scsServoSDK.syncReadPositions(servoIds);
Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => {
const position = servoPositions.get(servoId);
if (position !== undefined) {
positions[jointName] = position;
// Update calibration state with current position
this.calibrationState.updateCurrentValue(jointName, position);
}
});
} catch (error) {
console.error(`[${this.name}] Error reading positions:`, error);
throw error;
}
return positions;
}
// Value conversion methods
normalizeValue(rawValue: number, jointName: string): number {
if (!this.isCalibrated) {
throw new Error('Cannot normalize value: not calibrated');
}
return this.calibrationState.normalizeValue(rawValue, jointName);
}
denormalizeValue(normalizedValue: number, jointName: string): number {
if (!this.isCalibrated) {
throw new Error('Cannot denormalize value: not calibrated');
}
return this.calibrationState.denormalizeValue(normalizedValue, jointName);
}
// Servo control methods
protected async lockAllServos(): Promise<void> {
if (!this.scsServoSDK) return;
try {
console.log(`[${this.name}] Locking all servos...`);
const servoIds = Object.values(this.jointToServoMap);
for (const servoId of servoIds) {
try {
// Check if writeTorqueEnable method exists before calling
if (typeof this.scsServoSDK.writeTorqueEnable !== 'function') {
console.warn(`[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}`);
continue;
}
await this.scsServoSDK.writeTorqueEnable(servoId, true);
await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay));
} catch (error) {
console.warn(`[${this.name}] Failed to lock servo ${servoId}:`, error);
}
}
console.log(`[${this.name}] All servos locked`);
} catch (error) {
console.error(`[${this.name}] Error locking servos:`, error);
}
}
protected async unlockAllServos(): Promise<void> {
if (!this.scsServoSDK) return;
try {
console.log(`[${this.name}] Unlocking all servos...`);
const servoIds = Object.values(this.jointToServoMap);
for (const servoId of servoIds) {
try {
// Check if writeTorqueEnable method exists before calling
if (typeof this.scsServoSDK.writeTorqueEnable !== 'function') {
console.warn(`[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}`);
continue;
}
await this.scsServoSDK.writeTorqueEnable(servoId, false);
await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay));
} catch (error) {
console.warn(`[${this.name}] Failed to unlock servo ${servoId}:`, error);
}
}
console.log(`[${this.name}] All servos unlocked`);
} catch (error) {
console.error(`[${this.name}] Error unlocking servos:`, error);
}
}
// Event handlers
onStatusChange(callback: (status: ConnectionStatus) => void): () => void {
this.statusCallbacks.push(callback);
return () => {
const index = this.statusCallbacks.indexOf(callback);
if (index >= 0) {
this.statusCallbacks.splice(index, 1);
}
};
}
protected notifyStatusChange(): void {
this.statusCallbacks.forEach(callback => {
try {
callback(this._status);
} catch (error) {
console.error(`[${this.name}] Error in status callback:`, error);
}
});
}
// Register callback for calibration completion with positions
onCalibrationCompleteWithPositions(callback: (positions: Record<string, number>) => void): () => void {
return this.calibrationState.onCalibrationCompleteWithPositions(callback);
}
// Sync robot joint positions using normalized values from calibration
syncRobotPositions(finalPositions: Record<string, number>, updateRobotCallback?: (jointName: string, normalizedValue: number) => void): void {
if (!updateRobotCallback) return;
console.log(`[${this.name}] 🔄 Syncing robot to final calibration positions...`);
Object.entries(finalPositions).forEach(([jointName, rawPosition]) => {
try {
// Convert raw servo position to normalized value using calibration
const normalizedValue = this.normalizeValue(rawPosition, jointName);
// Clamp to appropriate normalized range based on joint type
let clampedValue: number;
if (jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper') {
clampedValue = Math.max(0, Math.min(100, normalizedValue));
} else {
clampedValue = Math.max(-100, Math.min(100, normalizedValue));
}
console.log(`[${this.name}] ${jointName}: ${rawPosition} (raw) -> ${normalizedValue.toFixed(1)} -> ${clampedValue.toFixed(1)} (normalized)`);
// Update robot joint through callback
updateRobotCallback(jointName, clampedValue);
} catch (error) {
console.warn(`[${this.name}] Failed to sync position for joint ${jointName}:`, error);
}
});
console.log(`[${this.name}] ✅ Robot synced to calibration positions`);
}
// Cleanup
async destroy(): Promise<void> {
this.stopCalibrationPolling();
await this.disconnect();
this.calibrationState.destroy();
}
}