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");
    }
  }
}