Spaces:
Running
Running
File size: 4,806 Bytes
ec936d5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
/**
* Base Robot class for Web platform
* Uses Web Serial API for serial communication
* Mirrors Python lerobot/common/robots/robot.py but adapted for browser environment
*/
import type { RobotConfig } from "../../node/robots/config.js";
// Web Serial API type declarations (minimal for our needs)
declare global {
interface SerialPort {
open(options: { baudRate: number }): Promise<void>;
close(): Promise<void>;
readable: ReadableStream<Uint8Array> | null;
writable: WritableStream<Uint8Array> | null;
}
}
export abstract class Robot {
protected port: SerialPort | null = null;
protected config: RobotConfig;
protected name: string;
protected reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
protected writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
constructor(config: RobotConfig) {
this.config = config;
this.name = config.type;
}
/**
* Connect to the robot using Web Serial API
* Requires user interaction to select port
*/
async connect(_calibrate: boolean = false): Promise<void> {
try {
// Request port from user (requires user interaction)
this.port = await navigator.serial.requestPort();
// Open the port with correct SO-100 baudRate
await this.port.open({ baudRate: 1000000 }); // Correct baudRate for Feetech motors (SO-100)
// Set up readable and writable streams
if (this.port.readable) {
this.reader = this.port.readable.getReader();
}
if (this.port.writable) {
this.writer = this.port.writable.getWriter();
}
} catch (error) {
throw new Error(
`Could not connect to robot: ${
error instanceof Error ? error.message : error
}`
);
}
}
/**
* Calibrate the robot
* Must be implemented by subclasses
*/
abstract calibrate(): Promise<void>;
/**
* Disconnect from the robot
*/
async disconnect(): Promise<void> {
if (this.reader) {
await this.reader.cancel();
this.reader.releaseLock();
this.reader = null;
}
if (this.writer) {
await this.writer.close();
this.writer = null;
}
if (this.port) {
await this.port.close();
this.port = null;
}
}
/**
* Save calibration data to browser storage
* Uses localStorage as fallback, IndexedDB preferred for larger data
*/
protected async saveCalibration(calibrationData: any): Promise<void> {
const robotId = this.config.id || this.name;
const key = `lerobot_calibration_${this.name}_${robotId}`;
try {
// Save to localStorage for now (could be enhanced to use File System Access API)
localStorage.setItem(key, JSON.stringify(calibrationData));
// Optionally trigger download
this.downloadCalibration(calibrationData, robotId);
console.log(`Configuration saved to browser storage and downloaded.`);
} catch (error) {
this.downloadCalibration(calibrationData, robotId);
console.log(`Configuration downloaded as file.`);
}
}
/**
* Download calibration data as JSON file
*/
private downloadCalibration(calibrationData: any, robotId: string): void {
const dataStr = JSON.stringify(calibrationData, null, 2);
const dataBlob = new Blob([dataStr], { type: "application/json" });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.href = url;
link.download = `${robotId}_calibration.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Send command to robot via Web Serial API
*/
protected async sendCommand(command: string): Promise<void> {
if (!this.writer) {
throw new Error("Robot not connected");
}
const encoder = new TextEncoder();
const data = encoder.encode(command);
await this.writer.write(data);
}
/**
* Read data from robot with timeout
*/
protected async readData(timeout: number = 5000): Promise<Uint8Array> {
if (!this.reader) {
throw new Error("Robot not connected");
}
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Read timeout")), timeout);
});
const readPromise = this.reader.read().then((result) => {
if (result.done) {
throw new Error("Stream closed");
}
return result.value;
});
return Promise.race([readPromise, timeoutPromise]);
}
/**
* Disable torque on disconnect (SO-100 specific)
*/
protected async disableTorque(): Promise<void> {
try {
await this.sendCommand("TORQUE_DISABLE\r\n");
} catch (error) {
console.warn("Warning: Could not disable torque on disconnect");
}
}
}
|