blanchon's picture
Mostly UI Update
18b0fa5
import type {
SlaveDriver,
DriverJointState,
ConnectionStatus,
RobotCommand,
USBSlaveConfig,
StateUpdateCallback,
StatusChangeCallback,
UnsubscribeFn
} from "$lib/types/robotDriver";
// import { scsServoSDK } from '../../../../feetech.js/index.mjs';
import { scsServoSDK } from "feetech.js";
import { servoPositionToAngle, degreesToServoPosition } from "$lib/utils";
/**
* USB Slave Driver
* Controls physical robots via feetech.js USB connection
*/
export class USBSlave implements SlaveDriver {
readonly type = "slave" as const;
readonly id: string;
readonly name: string;
private _status: ConnectionStatus = { isConnected: false };
private config: USBSlaveConfig;
// Joint states
private jointStates: DriverJointState[] = [];
private initialPositions: number[] = [];
// Event callbacks
private stateCallbacks: StateUpdateCallback[] = [];
private statusCallbacks: StatusChangeCallback[] = [];
constructor(config: USBSlaveConfig, initialJointStates: DriverJointState[]) {
this.config = config;
this.id = `usb-slave-${Date.now()}`;
this.name = `USB Slave Robot`;
// Initialize joint states
this.jointStates = initialJointStates.map((state) => ({ ...state }));
// Validate that all active joints have servo IDs
const missingServos = this.jointStates.filter((j) => !j.servoId);
if (missingServos.length > 0) {
console.warn(
`USB slave: Some joints missing servo IDs: ${missingServos.map((j) => j.name).join(", ")}`
);
}
console.log(`Created USBSlave with ${this.jointStates.length} joints`);
}
get status(): ConnectionStatus {
return this._status;
}
async connect(): Promise<void> {
console.log(`Connecting ${this.name}...`);
try {
this._status = { isConnected: false };
this.notifyStatusChange();
await scsServoSDK.connect();
// Initialize each servo
const initialPositions: number[] = [];
for (const joint of this.jointStates) {
if (!joint.servoId) {
initialPositions.push(0);
continue;
}
try {
if (joint.type === "continuous") {
// Configure for speed control
await scsServoSDK.setWheelMode(joint.servoId);
await scsServoSDK.writeWheelSpeed(joint.servoId, 0);
initialPositions.push(0);
// Update joint state
joint.realValue = 0;
} else {
// Configure for position control
await scsServoSDK.setPositionMode(joint.servoId);
// Read initial position
const servoPosition = await scsServoSDK.readPosition(joint.servoId);
const positionInDegrees = servoPositionToAngle(servoPosition);
initialPositions.push(positionInDegrees);
// Enable torque
await scsServoSDK.writeTorqueEnable(joint.servoId, true);
// Update joint state
joint.realValue = positionInDegrees;
}
} catch (error) {
console.error(`Failed to initialize servo ${joint.servoId} (${joint.name}):`, error);
initialPositions.push(0);
joint.realValue = undefined;
}
}
this.initialPositions = initialPositions;
this._status = {
isConnected: true,
lastConnected: new Date(),
error: undefined
};
this.notifyStatusChange();
console.log(`${this.name} connected successfully`);
} catch (error) {
this._status = {
isConnected: false,
error: `Connection failed: ${error}`
};
this.notifyStatusChange();
throw error;
}
}
async disconnect(): Promise<void> {
console.log(`Disconnecting ${this.name}...`);
if (!this._status.isConnected) return;
try {
// Stop all joints safely
for (const joint of this.jointStates) {
if (!joint.servoId) continue;
try {
if (joint.type === "continuous") {
await scsServoSDK.writeWheelSpeed(joint.servoId, 0);
}
await scsServoSDK.writeTorqueEnable(joint.servoId, false);
} catch (error) {
console.error(`Failed to stop servo ${joint.servoId}:`, error);
}
}
await scsServoSDK.disconnect();
// Clear real values
this.jointStates.forEach((joint) => {
joint.realValue = undefined;
});
this._status = { isConnected: false };
this.notifyStatusChange();
console.log(`${this.name} disconnected`);
} catch (error) {
this._status = {
isConnected: false,
error: `Disconnect failed: ${error}`
};
this.notifyStatusChange();
throw error;
}
}
async executeCommand(command: RobotCommand): Promise<void> {
if (!this._status.isConnected) {
throw new Error("Cannot execute command: USB slave not connected");
}
console.log(`USBSlave executing command with ${command.joints.length} joint updates`);
for (const jointUpdate of command.joints) {
const joint = this.jointStates.find((j) => j.name === jointUpdate.name);
if (!joint || !joint.servoId) continue;
try {
if (joint.type === "continuous") {
await scsServoSDK.writeWheelSpeed(joint.servoId, jointUpdate.value);
joint.realValue = jointUpdate.value;
} else {
// Use relative positioning for revolute joints
const jointIndex = this.jointStates.indexOf(joint);
const relativeValue = (this.initialPositions[jointIndex] || 0) + jointUpdate.value;
if (relativeValue >= 0 && relativeValue <= 360) {
const servoPosition = degreesToServoPosition(relativeValue);
await scsServoSDK.writePosition(joint.servoId, Math.round(servoPosition));
joint.realValue = relativeValue;
} else {
throw new Error(`Position ${relativeValue}° out of range (0-360°)`);
}
}
// Update virtual value to match what we sent
joint.virtualValue = jointUpdate.value;
} catch (error) {
console.error(`Failed to execute command for joint ${joint.name}:`, error);
joint.realValue = undefined;
throw error;
}
}
// Notify state update
this.notifyStateUpdate();
}
async executeCommands(commands: RobotCommand[]): Promise<void> {
console.log(`USBSlave executing batch of ${commands.length} commands`);
for (const command of commands) {
await this.executeCommand(command);
// Small delay between commands to avoid overwhelming servos
if (commands.length > 1) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
}
async readJointStates(): Promise<DriverJointState[]> {
if (!this._status.isConnected) {
throw new Error("Cannot read states: USB slave not connected");
}
const states: DriverJointState[] = [];
for (let i = 0; i < this.jointStates.length; i++) {
const joint = this.jointStates[i];
if (!joint.servoId) {
states.push({ ...joint });
continue;
}
try {
if (joint.type === "revolute") {
const servoPosition = await scsServoSDK.readPosition(joint.servoId);
const positionInDegrees = servoPositionToAngle(servoPosition);
states.push({
...joint,
realValue: positionInDegrees
});
} else {
// For continuous joints, we don't read speed (not available in feetech)
// Keep the last known value
states.push({ ...joint });
}
} catch (error) {
console.error(`Failed to read servo ${joint.servoId}:`, error);
states.push({
...joint,
realValue: undefined
});
}
}
return states;
}
async writeJointState(jointName: string, value: number): Promise<void> {
const command: RobotCommand = {
timestamp: Date.now(),
joints: [{ name: jointName, value }]
};
await this.executeCommand(command);
}
async writeJointStates(updates: { jointName: string; value: number }[]): Promise<void> {
const command: RobotCommand = {
timestamp: Date.now(),
joints: updates.map((update) => ({ name: update.jointName, value: update.value }))
};
await this.executeCommand(command);
}
// Event subscription methods
onStateUpdate(callback: StateUpdateCallback): UnsubscribeFn {
this.stateCallbacks.push(callback);
return () => {
const index = this.stateCallbacks.indexOf(callback);
if (index >= 0) {
this.stateCallbacks.splice(index, 1);
}
};
}
onStatusChange(callback: StatusChangeCallback): UnsubscribeFn {
this.statusCallbacks.push(callback);
return () => {
const index = this.statusCallbacks.indexOf(callback);
if (index >= 0) {
this.statusCallbacks.splice(index, 1);
}
};
}
// Private methods
private notifyStateUpdate(): void {
this.stateCallbacks.forEach((callback) => {
try {
callback([...this.jointStates]);
} catch (error) {
console.error("Error in state update callback:", error);
}
});
}
private notifyStatusChange(): void {
this.statusCallbacks.forEach((callback) => {
try {
callback(this._status);
} catch (error) {
console.error("Error in status change callback:", error);
}
});
}
}