LeRobot.js / packages /web /src /find_port.ts
NERDDISCO's picture
feat: move "src/lerobot/web" to "packages/web/src"
b664dbe
raw
history blame
7.34 kB
/**
* 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");
},
};
}