File size: 13,975 Bytes
c8b4583
1a7b22d
 
 
c8b4583
5eb1bc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8b4583
 
6fa48c4
 
1a7b22d
 
 
 
24f0634
 
 
 
 
ebd0121
 
dc82a28
ebd0121
1a7b22d
ebd0121
c8b4583
1a7b22d
 
 
 
 
 
 
c8b4583
1a7b22d
c8b4583
 
 
1a7b22d
c8b4583
1a7b22d
 
 
 
 
 
982ff9b
1a7b22d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8b4583
 
 
1a7b22d
c8b4583
1a7b22d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5eb1bc0
 
c8b4583
 
1a7b22d
c8b4583
5eb1bc0
dc82a28
5eb1bc0
 
c8b4583
66cb510
c8b4583
 
982ff9b
 
66cb510
982ff9b
66cb510
982ff9b
 
 
5eb1bc0
 
982ff9b
5eb1bc0
982ff9b
 
 
66cb510
982ff9b
66cb510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a7b22d
 
 
 
 
 
 
 
 
 
 
 
 
5eb1bc0
 
 
 
982ff9b
5eb1bc0
 
 
1a7b22d
 
5eb1bc0
 
c8b4583
5eb1bc0
 
 
 
 
 
c8b4583
5eb1bc0
c8b4583
 
5eb1bc0
c8b4583
5eb1bc0
 
 
 
 
 
dc82a28
5eb1bc0
 
 
 
 
 
 
 
 
 
1a7b22d
 
 
 
 
 
 
5eb1bc0
1a7b22d
 
 
 
 
 
 
 
 
5eb1bc0
 
1a7b22d
5eb1bc0
 
 
dc82a28
 
5eb1bc0
 
6fa48c4
 
 
 
 
dc82a28
 
 
 
 
 
 
 
 
 
 
 
6fa48c4
 
 
1a7b22d
 
 
 
 
 
 
 
 
 
 
6fa48c4
 
 
1a7b22d
 
 
 
 
 
 
5eb1bc0
 
1a7b22d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5eb1bc0
1a7b22d
 
 
 
c8b4583
1a7b22d
5eb1bc0
1a7b22d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8b4583
 
5eb1bc0
 
 
c8b4583
 
5eb1bc0
 
c8b4583
5eb1bc0
 
 
 
 
 
 
dc82a28
5eb1bc0
 
 
 
 
 
 
 
dc82a28
5eb1bc0
 
 
 
c8b4583
5eb1bc0
 
 
 
c8b4583
 
5eb1bc0
 
 
 
c8b4583
 
5eb1bc0
dc82a28
5eb1bc0
dc82a28
5eb1bc0
 
 
 
 
 
 
 
 
 
 
c8b4583
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
/**
 * Browser implementation of find_port using WebSerial + WebUSB APIs
 * WebSerial: Communication with device
 * WebUSB: Device identification and serial numbers
 *
 * Usage Examples:
 *
 * // Interactive mode - always returns array
 * const findProcess = await findPort();
 * const robotConnections = await findProcess.result;
 * const robot = robotConnections[0]; // First (and only) robot
 * await calibrate(robot, options);
 *
 * // Auto-connect mode - returns array of all attempted connections
 * const findProcess = await findPort({
 *   robotConfigs: [
 *     { robotType: "so100_follower", robotId: "arm1", serialNumber: "ABC123" },
 *     { robotType: "so100_leader", robotId: "arm2", serialNumber: "DEF456" }
 *   ]
 * });
 * const robotConnections = await findProcess.result;
 * for (const robot of robotConnections.filter(r => r.isConnected)) {
 *   await calibrate(robot, options);
 * }
 *
 * // Store/load from localStorage
 * localStorage.setItem('myRobots', JSON.stringify(robotConnections));
 * const storedRobots = JSON.parse(localStorage.getItem('myRobots'));
 * await calibrate(storedRobots[0], options);
 */

import { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js";
import { readMotorPosition } from "./utils/motor-communication.js";
import {
  isWebSerialSupported,
  isWebUSBSupported,
} from "./utils/browser-support.js";
import type {
  RobotConnection,
  RobotConfig,
  SerialPort,
} from "./types/robot-connection.js";
import type {
  Serial,
  FindPortConfig,
  FindPortProcess,
  USBDevice,
} from "./types/port-discovery.js";

/**
 * Get display name for a port
 */
function getPortDisplayName(port: SerialPort): string {
  const info = port.getInfo();
  if (info.usbVendorId && info.usbProductId) {
    return `USB Device (${info.usbVendorId}:${info.usbProductId})`;
  }
  return "Serial Device";
}

/**
 * Request USB device for metadata and serial number extraction
 */
async function requestUSBDeviceMetadata(): Promise<{
  serialNumber: string;
  usbMetadata: RobotConnection["usbMetadata"];
}> {
  try {
    // Request USB device access for metadata (no filters - accept any device)
    const usbDevice = await navigator.usb.requestDevice({ filters: [] });

    const serialNumber =
      usbDevice.serialNumber ||
      `${usbDevice.vendorId}-${usbDevice.productId}-${Date.now()}`;

    const usbMetadata = {
      vendorId: `0x${usbDevice.vendorId.toString(16).padStart(4, "0")}`,
      productId: `0x${usbDevice.productId.toString(16).padStart(4, "0")}`,
      serialNumber: usbDevice.serialNumber || "Generated ID",
      manufacturerName: usbDevice.manufacturerName || "Unknown",
      productName: usbDevice.productName || "Unknown",
      usbVersionMajor: usbDevice.usbVersionMajor,
      usbVersionMinor: usbDevice.usbVersionMinor,
      deviceClass: usbDevice.deviceClass,
      deviceSubclass: usbDevice.deviceSubclass,
      deviceProtocol: usbDevice.deviceProtocol,
    };

    return { serialNumber, usbMetadata };
  } catch (usbError) {
    console.log("⚠️ WebUSB request failed, generating fallback ID:", usbError);
    // Generate a fallback unique ID if WebUSB fails
    const serialNumber = `fallback-${Date.now()}-${Math.random()
      .toString(36)
      .substr(2, 9)}`;

    const usbMetadata = {
      vendorId: "Unknown",
      productId: "Unknown",
      serialNumber: serialNumber,
      manufacturerName: "WebUSB Not Available",
      productName: "Check browser WebUSB support",
    };

    return { serialNumber, usbMetadata };
  }
}

/**
 * Get USB device metadata for already permitted devices
 */
async function getStoredUSBDeviceMetadata(port: SerialPort): Promise<{
  serialNumber: string;
  usbMetadata?: RobotConnection["usbMetadata"];
}> {
  try {
    if (!isWebUSBSupported()) {
      throw new Error("WebUSB not supported");
    }

    // Get already permitted USB devices
    const usbDevices = await navigator.usb.getDevices();
    const portInfo = port.getInfo();

    // Try to find matching USB device by vendor/product ID
    const matchingDevice = usbDevices.find(
      (device) =>
        device.vendorId === portInfo.usbVendorId &&
        device.productId === portInfo.usbProductId
    );

    if (matchingDevice) {
      const serialNumber =
        matchingDevice.serialNumber ||
        `${matchingDevice.vendorId}-${matchingDevice.productId}-${Date.now()}`;

      const usbMetadata = {
        vendorId: `0x${matchingDevice.vendorId.toString(16).padStart(4, "0")}`,
        productId: `0x${matchingDevice.productId
          .toString(16)
          .padStart(4, "0")}`,
        serialNumber: matchingDevice.serialNumber || "Generated ID",
        manufacturerName: matchingDevice.manufacturerName || "Unknown",
        productName: matchingDevice.productName || "Unknown",
        usbVersionMajor: matchingDevice.usbVersionMajor,
        usbVersionMinor: matchingDevice.usbVersionMinor,
        deviceClass: matchingDevice.deviceClass,
        deviceSubclass: matchingDevice.deviceSubclass,
        deviceProtocol: matchingDevice.deviceProtocol,
      };

      console.log("βœ… Restored USB metadata for port:", serialNumber);
      return { serialNumber, usbMetadata };
    }

    throw new Error("No matching USB device found");
  } catch (usbError) {
    console.log("⚠️ Could not restore USB metadata:", usbError);
    // Generate fallback if no USB metadata available
    const serialNumber = `fallback-${Date.now()}-${Math.random()
      .toString(36)
      .substr(2, 9)}`;

    return { serialNumber };
  }
}

/**
 * Interactive mode: Show native dialogs for port + device selection
 */
async function findPortInteractive(
  options: FindPortConfig
): Promise<RobotConnection[]> {
  const { onMessage } = options;

  onMessage?.("Opening device selection dialogs...");

  try {
    let serialNumber: string;
    let usbMetadata: RobotConnection["usbMetadata"];

    onMessage?.("πŸ“± Requesting device access permissions...");

    // Step 1: Request serial port
    onMessage?.("πŸ“‘ Step 1: Select serial port...");
    const port = await navigator.serial.requestPort();
    await port.open({ baudRate: 1000000 });

    onMessage?.(`βœ… Connected to ${getPortDisplayName(port)}`);

    // Step 2: Request USB device for identification
    if (isWebUSBSupported()) {
      onMessage?.("πŸ†” Step 2: Select device for identification...");
      try {
        const usbData = await requestUSBDeviceMetadata();
        serialNumber = usbData.serialNumber;
        usbMetadata = usbData.usbMetadata;
        onMessage?.(`πŸ†” Device ID: ${serialNumber}`);
      } catch (usbError) {
        onMessage?.("⚠️ Device identification failed, using fallback ID");
        const fallbackId = `fallback-${Date.now()}`;
        serialNumber = fallbackId;
        usbMetadata = {
          vendorId: "Unknown",
          productId: "Unknown",
          serialNumber: fallbackId,
          manufacturerName: "USB Dialog Cancelled",
          productName: "User cancelled device selection",
        };
      }
    } else {
      onMessage?.("⚠️ WebUSB not supported, using fallback ID");
      const fallbackId = `no-usb-${Date.now()}`;
      serialNumber = fallbackId;
      usbMetadata = {
        vendorId: "Unknown",
        productId: "Unknown",
        serialNumber: fallbackId,
        manufacturerName: "WebUSB Not Supported",
        productName: "Browser limitation",
      };
    }

    // Return unified RobotConnection object in array (consistent API)
    return [
      {
        port,
        name: getPortDisplayName(port),
        isConnected: true,
        robotType: "so100_follower", // Default, user can change
        robotId: "interactive_robot",
        serialNumber,
        usbMetadata,
      },
    ];
  } catch (error) {
    if (
      error instanceof Error &&
      (error.message.includes("cancelled") || error.name === "NotAllowedError")
    ) {
      throw new Error("Port selection cancelled by user");
    }
    throw new Error(
      `Failed to select port: ${error instanceof Error ? error.message : error}`
    );
  }
}

/**
 * Auto-connect mode: Connect to robots by serial number
 * Returns all successfully connected robots
 */
async function findPortAutoConnect(
  robotConfigs: RobotConfig[],
  options: FindPortConfig
): Promise<RobotConnection[]> {
  const { onMessage } = options;
  const results: RobotConnection[] = [];

  onMessage?.(`πŸ” Auto-connecting to ${robotConfigs.length} robot(s)...`);

  // Get all available ports
  const availablePorts = await navigator.serial.getPorts();
  onMessage?.(`Found ${availablePorts.length} available port(s)`);

  // For each available port, try to restore USB metadata and match with configs
  for (const port of availablePorts) {
    try {
      // Get USB device metadata for this port
      const { serialNumber, usbMetadata } = await getStoredUSBDeviceMetadata(
        port
      );

      // Find matching robot config by serial number
      const matchingConfig = robotConfigs.find(
        (config) => config.serialNumber === serialNumber
      );

      if (matchingConfig) {
        onMessage?.(
          `Connecting to ${matchingConfig.robotId} (${serialNumber})...`
        );

        try {
          // Try to open the port
          const wasOpen = port.readable !== null;
          if (!wasOpen) {
            await port.open({ baudRate: 1000000 });
            // Small delay to allow port to stabilize after opening
            await new Promise((resolve) => setTimeout(resolve, 100));
          }

          // Test connection by trying basic motor communication
          const portWrapper = new WebSerialPortWrapper(port);
          await portWrapper.initialize();

          // Try to read from motor ID 1 (most robots have at least one motor)
          // Retry mechanism for more robust connection testing
          let testPosition: number | null = null;
          for (let attempt = 0; attempt < 3; attempt++) {
            try {
              testPosition = await readMotorPosition(portWrapper, 1);
              if (testPosition !== null) break;
              await new Promise((resolve) => setTimeout(resolve, 50));
            } catch (retryError) {
              if (attempt === 2) throw retryError;
              await new Promise((resolve) => setTimeout(resolve, 100));
            }
          }

          // If we can read a position, this is likely a working robot port
          if (testPosition !== null) {
            onMessage?.(`βœ… Connected to ${matchingConfig.robotId}`);

            results.push({
              port,
              name: getPortDisplayName(port),
              isConnected: true,
              robotType: matchingConfig.robotType,
              robotId: matchingConfig.robotId,
              serialNumber,
              usbMetadata,
            });
          } else {
            throw new Error("No motor response - not a robot port");
          }
        } catch (connectionError) {
          onMessage?.(
            `❌ Failed to connect to ${matchingConfig.robotId}: ${
              connectionError instanceof Error
                ? connectionError.message
                : connectionError
            }`
          );

          results.push({
            port,
            name: getPortDisplayName(port),
            isConnected: false,
            robotType: matchingConfig.robotType,
            robotId: matchingConfig.robotId,
            serialNumber,
            usbMetadata,
            error:
              connectionError instanceof Error
                ? connectionError.message
                : "Unknown error",
          });
        }
      } else {
        console.log(
          `Port with serial ${serialNumber} not in requested configs, skipping`
        );
      }
    } catch (metadataError) {
      console.log(`Failed to get metadata for port:`, metadataError);
      // Skip this port if we can't get metadata
      continue;
    }
  }

  // Handle robots that weren't found
  for (const config of robotConfigs) {
    const found = results.some((r) => r.serialNumber === config.serialNumber);
    if (!found) {
      onMessage?.(
        `❌ Robot ${config.robotId} (${config.serialNumber}) not found`
      );
      results.push({
        port: null as any, // Will not be used since isConnected = false
        name: "Not Found",
        isConnected: false,
        robotType: config.robotType,
        robotId: config.robotId,
        serialNumber: config.serialNumber,
        error: `Device with serial number ${config.serialNumber} not found`,
      });
    }
  }

  const successCount = results.filter((r) => r.isConnected).length;
  onMessage?.(
    `🎯 Connected to ${successCount}/${robotConfigs.length} robot(s)`
  );

  return results;
}

/**
 * Main findPort function - clean API with Two modes:
 *
 * Mode 1: Interactive - Returns single RobotConnection
 * Mode 2: Auto-connect - Returns RobotConnection[]
 */
export async function findPort(
  config: FindPortConfig = {}
): Promise<FindPortProcess> {
  // Check WebSerial support
  if (!isWebSerialSupported()) {
    throw new Error(
      "WebSerial API not supported. Please use Chrome/Edge 89+ with HTTPS or localhost."
    );
  }

  const { robotConfigs, onMessage } = config;
  let stopped = false;

  // Determine mode
  const isAutoConnectMode = robotConfigs && robotConfigs.length > 0;

  onMessage?.(
    `πŸ€– ${
      isAutoConnectMode ? "Auto-connect" : "Interactive"
    } port discovery started`
  );

  // Create result promise
  const resultPromise = (async () => {
    if (stopped) {
      throw new Error("Port discovery was stopped");
    }

    if (isAutoConnectMode) {
      return await findPortAutoConnect(robotConfigs!, config);
    } else {
      return await findPortInteractive(config);
    }
  })();

  // Return process object
  return {
    result: resultPromise,
    stop: () => {
      stopped = true;
      onMessage?.("πŸ›‘ Port discovery stopped");
    },
  };
}