Spaces:
Running
Running
| /** | |
| * Browser implementation of find_port using WebSerial API | |
| * Clean API with native dialogs and auto-connect modes | |
| * | |
| * 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 type { | |
| RobotConnection, | |
| RobotConfig, | |
| SerialPort, | |
| } from "./types/robot-connection.js"; | |
| import type { | |
| Serial, | |
| FindPortOptions, | |
| FindPortProcess, | |
| } from "./types/port-discovery.js"; | |
| declare global { | |
| interface Navigator { | |
| serial: Serial; | |
| } | |
| } | |
| /** | |
| * Check if WebSerial API is available | |
| */ | |
| function isWebSerialSupported(): boolean { | |
| return "serial" in navigator; | |
| } | |
| /** | |
| * 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"; | |
| } | |
| /** | |
| * Interactive mode: Show native dialog for port selection | |
| */ | |
| async function findPortInteractive( | |
| options: FindPortOptions | |
| ): Promise<RobotConnection[]> { | |
| const { onMessage } = options; | |
| onMessage?.("Opening port selection dialog..."); | |
| try { | |
| // Use native browser dialog - much better UX than port diffing! | |
| const port = await navigator.serial.requestPort(); | |
| // Open the port | |
| await port.open({ baudRate: 1000000 }); | |
| const portName = getPortDisplayName(port); | |
| onMessage?.(`β Connected to ${portName}`); | |
| // Return unified RobotConnection object in array (consistent API) | |
| // In interactive mode, user will need to specify robot details separately | |
| return [ | |
| { | |
| port, | |
| name: portName, | |
| isConnected: true, | |
| robotType: "so100_follower", // Default, user can change | |
| robotId: "interactive_robot", | |
| serialNumber: `interactive_${Date.now()}`, | |
| }, | |
| ]; | |
| } 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: FindPortOptions | |
| ): 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 (const config of robotConfigs) { | |
| onMessage?.(`Connecting to ${config.robotId} (${config.serialNumber})...`); | |
| let connected = false; | |
| let matchedPort: SerialPort | null = null; | |
| let error: string | undefined; | |
| try { | |
| // For now, we'll try each available port and see if we can connect | |
| // In a future enhancement, we could match by actual serial number reading | |
| for (const port of availablePorts) { | |
| try { | |
| // Try to open and use this port | |
| const wasOpen = port.readable !== null; | |
| if (!wasOpen) { | |
| await port.open({ baudRate: 1000000 }); | |
| } | |
| // 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) | |
| const testPosition = await readMotorPosition(portWrapper, 1); | |
| // If we can read a position, this is likely a working robot port | |
| if (testPosition !== null) { | |
| matchedPort = port; | |
| connected = true; | |
| onMessage?.(`β Connected to ${config.robotId}`); | |
| break; | |
| } else { | |
| throw new Error("No motor response - not a robot port"); | |
| } | |
| } catch (portError) { | |
| // This port didn't work, try next one | |
| console.log( | |
| `Port ${getPortDisplayName(port)} didn't match ${config.robotId}:`, | |
| portError | |
| ); | |
| continue; | |
| } | |
| } | |
| if (!connected) { | |
| error = `No matching port found for ${config.robotId} (${config.serialNumber})`; | |
| onMessage?.(`β ${error}`); | |
| } | |
| } catch (err) { | |
| error = err instanceof Error ? err.message : "Unknown error"; | |
| onMessage?.(`β Failed to connect to ${config.robotId}: ${error}`); | |
| } | |
| // Add result (successful or failed) | |
| results.push({ | |
| port: matchedPort!, | |
| name: matchedPort ? getPortDisplayName(matchedPort) : "Unknown Port", | |
| isConnected: connected, | |
| robotType: config.robotType, | |
| robotId: config.robotId, | |
| serialNumber: config.serialNumber, | |
| error, | |
| }); | |
| } | |
| 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( | |
| options: FindPortOptions = {} | |
| ): 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 } = options; | |
| 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!, options); | |
| } else { | |
| return await findPortInteractive(options); | |
| } | |
| })(); | |
| // Return process object | |
| return { | |
| result: resultPromise, | |
| stop: () => { | |
| stopped = true; | |
| onMessage?.("π Port discovery stopped"); | |
| }, | |
| }; | |
| } | |