NERDDISCO commited on
Commit
1a7b22d
·
1 Parent(s): ba80dbe

feat: improved things around calibrate, findPort (added web usb back)

Browse files
examples/robot-control-web/components/CalibrationPanel.tsx CHANGED
@@ -10,13 +10,13 @@ import {
10
  import { Badge } from "./ui/badge.js";
11
  import {
12
  calibrate,
 
 
 
13
  type WebCalibrationResults,
14
  type LiveCalibrationData,
15
  type CalibrationProcess,
16
  } from "@lerobot/web";
17
- import { releaseMotors } from "@lerobot/web";
18
- import { WebSerialPortWrapper } from "@lerobot/web";
19
- import { createSO100Config } from "@lerobot/web";
20
  import { CalibrationModal } from "./CalibrationModal.js";
21
  import type { RobotConnection } from "@lerobot/web";
22
 
@@ -64,22 +64,21 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
64
  setMotorData(initialData);
65
  }, [motorNames]);
66
 
67
- // Release motor torque for better UX - allows immediate joint movement
68
  const releaseMotorTorque = useCallback(async () => {
69
- if (!robot.port || !robot.robotType) {
70
- return;
71
- }
72
-
73
  try {
74
  setIsPreparing(true);
75
  setStatus("🔓 Releasing motor torque - joints can now be moved freely");
76
 
77
- // Create port wrapper and config to get motor IDs
 
 
 
 
78
  const port = new WebSerialPortWrapper(robot.port);
79
  await port.initialize();
80
- const config = createSO100Config(robot.robotType);
81
 
82
- // Release motors so they can be moved freely by hand
83
  await releaseMotors(port, config.motorIds);
84
 
85
  setStatus("✅ Joints are now free to move - set your homing position");
@@ -265,7 +264,7 @@ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
265
  {!isCalibrating && !calibrationResult && (
266
  <Button
267
  onClick={async () => {
268
- // Release motor torque FIRST - so user can move joints immediately
269
  await releaseMotorTorque();
270
  // THEN open modal - user can now follow instructions right away
271
  setModalOpen(true);
 
10
  import { Badge } from "./ui/badge.js";
11
  import {
12
  calibrate,
13
+ releaseMotors,
14
+ WebSerialPortWrapper,
15
+ createSO100Config,
16
  type WebCalibrationResults,
17
  type LiveCalibrationData,
18
  type CalibrationProcess,
19
  } from "@lerobot/web";
 
 
 
20
  import { CalibrationModal } from "./CalibrationModal.js";
21
  import type { RobotConnection } from "@lerobot/web";
22
 
 
64
  setMotorData(initialData);
65
  }, [motorNames]);
66
 
67
+ // Release motor torque
68
  const releaseMotorTorque = useCallback(async () => {
 
 
 
 
69
  try {
70
  setIsPreparing(true);
71
  setStatus("🔓 Releasing motor torque - joints can now be moved freely");
72
 
73
+ if (!robot.robotType) {
74
+ throw new Error("Robot type not configured");
75
+ }
76
+
77
+ // Create port and get motor config
78
  const port = new WebSerialPortWrapper(robot.port);
79
  await port.initialize();
 
80
 
81
+ const config = createSO100Config(robot.robotType);
82
  await releaseMotors(port, config.motorIds);
83
 
84
  setStatus("✅ Joints are now free to move - set your homing position");
 
264
  {!isCalibrating && !calibrationResult && (
265
  <Button
266
  onClick={async () => {
267
+ // Release motor torque FIRST - so user can move joints immediately
268
  await releaseMotorTorque();
269
  // THEN open modal - user can now follow instructions right away
270
  setModalOpen(true);
examples/robot-control-web/components/PortManager.tsx CHANGED
@@ -9,39 +9,13 @@ import {
9
  } from "./ui/card";
10
  import { Alert, AlertDescription } from "./ui/alert";
11
  import { Badge } from "./ui/badge";
12
- import {
13
- Dialog,
14
- DialogContent,
15
- DialogDescription,
16
- DialogFooter,
17
- DialogHeader,
18
- DialogTitle,
19
- } from "./ui/dialog";
20
- import { isWebSerialSupported } from "@lerobot/web";
21
  import type { RobotConnection } from "@lerobot/web";
22
 
23
- /**
24
- * Type definitions for WebSerial API (missing from TypeScript)
25
- */
26
- interface SerialPortInfo {
27
- usbVendorId?: number;
28
- usbProductId?: number;
29
- }
30
-
31
- declare global {
32
- interface SerialPort {
33
- getInfo(): SerialPortInfo;
34
- }
35
- }
36
-
37
  interface PortManagerProps {
38
  connectedRobots: RobotConnection[];
39
  onConnectedRobotsChange: (robots: RobotConnection[]) => void;
40
- onCalibrate?: (
41
- port: SerialPort,
42
- robotType: "so100_follower" | "so100_leader",
43
- robotId: string
44
- ) => void;
45
  onTeleoperate?: (robot: RobotConnection) => void;
46
  }
47
 
@@ -51,407 +25,74 @@ export function PortManager({
51
  onCalibrate,
52
  onTeleoperate,
53
  }: PortManagerProps) {
54
- const [isConnecting, setIsConnecting] = useState(false);
55
  const [isFindingPorts, setIsFindingPorts] = useState(false);
56
  const [findPortsLog, setFindPortsLog] = useState<string[]>([]);
57
  const [error, setError] = useState<string | null>(null);
58
- const [confirmDeleteDialog, setConfirmDeleteDialog] = useState<{
59
- open: boolean;
60
- robotIndex: number;
61
- robotName: string;
62
- serialNumber: string;
63
- }>({
64
- open: false,
65
- robotIndex: -1,
66
- robotName: "",
67
- serialNumber: "",
68
- });
69
- // Load saved port data from localStorage on mount
70
  useEffect(() => {
71
- loadSavedPorts();
72
  }, []);
73
 
74
- // Note: Robot data is now automatically saved to unified storage when robot config is updated
75
-
76
- const loadSavedPorts = async () => {
77
  try {
78
- const existingPorts = await navigator.serial.getPorts();
79
- const restoredPorts: RobotConnection[] = [];
80
-
81
- for (const port of existingPorts) {
82
- // Get USB device metadata to determine serial number
83
- let serialNumber = null;
84
- let usbMetadata = null;
85
-
86
- try {
87
- // Get all USB devices and try to match with this serial port
88
- const usbDevices = await navigator.usb.getDevices();
89
- const portInfo = port.getInfo();
90
 
91
- // Try to find matching USB device by vendor/product ID
92
- const matchingDevice = usbDevices.find(
93
- (device) =>
94
- device.vendorId === portInfo.usbVendorId &&
95
- device.productId === portInfo.usbProductId
96
- );
97
 
98
- if (matchingDevice) {
99
- serialNumber =
100
- matchingDevice.serialNumber ||
101
- `${matchingDevice.vendorId}-${
102
- matchingDevice.productId
103
- }-${Date.now()}`;
104
- usbMetadata = {
105
- vendorId: `0x${matchingDevice.vendorId
106
- .toString(16)
107
- .padStart(4, "0")}`,
108
- productId: `0x${matchingDevice.productId
109
- .toString(16)
110
- .padStart(4, "0")}`,
111
- serialNumber: matchingDevice.serialNumber || "Generated ID",
112
- manufacturerName: matchingDevice.manufacturerName || "Unknown",
113
- productName: matchingDevice.productName || "Unknown",
114
- usbVersionMajor: matchingDevice.usbVersionMajor,
115
- usbVersionMinor: matchingDevice.usbVersionMinor,
116
- deviceClass: matchingDevice.deviceClass,
117
- deviceSubclass: matchingDevice.deviceSubclass,
118
- deviceProtocol: matchingDevice.deviceProtocol,
119
- };
120
- console.log("✅ Restored USB metadata for port:", serialNumber);
121
- }
122
- } catch (usbError) {
123
- console.log("⚠️ Could not restore USB metadata:", usbError);
124
- // Generate fallback if no USB metadata available
125
- serialNumber = `fallback-${Date.now()}-${Math.random()
126
- .toString(36)
127
- .substr(2, 9)}`;
128
- }
129
-
130
- // Load robot configuration from unified storage
131
- let robotType: "so100_follower" | "so100_leader" | undefined;
132
- let robotId: string | undefined;
133
- let shouldAutoConnect = false;
134
-
135
- if (serialNumber) {
136
- try {
137
- const { getUnifiedRobotData } = await import(
138
- "../lib/unified-storage"
139
- );
140
- const unifiedData = getUnifiedRobotData(serialNumber);
141
- if (unifiedData?.device_info) {
142
- robotType = unifiedData.device_info.robotType;
143
- robotId = unifiedData.device_info.robotId;
144
- shouldAutoConnect = true;
145
- console.log(
146
- `📋 Loaded robot config from unified storage: ${robotType} (${robotId})`
147
- );
148
- }
149
- } catch (error) {
150
- console.warn("Failed to load unified robot data:", error);
151
- }
152
- }
153
 
154
- // Auto-connect to configured robots
155
- let isConnected = false;
156
- try {
157
- // Check if already open
158
- if (port.readable !== null && port.writable !== null) {
159
- isConnected = true;
160
- console.log("Port already open, reusing connection");
161
- } else if (shouldAutoConnect && robotType && robotId) {
162
- // Auto-open robots that have saved configuration
163
  console.log(
164
- `Auto-connecting to saved robot: ${robotType} (${robotId})`
165
  );
166
- await port.open({ baudRate: 1000000 });
167
- isConnected = true;
168
- } else {
169
- console.log(
170
- "Port found but no saved robot configuration, skipping auto-connect"
171
- );
172
- isConnected = false;
173
  }
174
- } catch (error) {
175
- console.log("Could not auto-connect to robot:", error);
176
- isConnected = false;
177
  }
178
-
179
- restoredPorts.push({
180
- port,
181
- name: getPortDisplayName(port),
182
- isConnected,
183
- robotType,
184
- robotId,
185
- serialNumber: serialNumber!,
186
- usbMetadata: usbMetadata || undefined,
187
- });
188
  }
189
 
190
- onConnectedRobotsChange(restoredPorts);
191
- } catch (error) {
192
- console.error("Failed to load saved ports:", error);
193
- }
194
- };
195
-
196
- const getPortDisplayName = (port: SerialPort): string => {
197
- try {
198
- const info = port.getInfo();
199
- if (info.usbVendorId && info.usbProductId) {
200
- return `USB Port (${info.usbVendorId}:${info.usbProductId})`;
201
- }
202
- if (info.usbVendorId) {
203
- return `Serial Port (VID:${info.usbVendorId
204
- .toString(16)
205
- .toUpperCase()})`;
206
- }
207
- } catch (error) {
208
- // getInfo() might not be available
209
- }
210
- return `Serial Port ${Date.now()}`;
211
- };
212
-
213
- const handleConnect = async () => {
214
- if (!isWebSerialSupported()) {
215
- setError("Web Serial API is not supported in this browser");
216
- return;
217
- }
218
-
219
- try {
220
- setIsConnecting(true);
221
- setError(null);
222
-
223
- // Step 1: Request Web Serial port
224
- console.log("Step 1: Requesting Web Serial port...");
225
- const port = await navigator.serial.requestPort();
226
- await port.open({ baudRate: 1000000 });
227
-
228
- // Step 2: Request WebUSB device for metadata
229
- console.log(
230
- "Step 2: Requesting WebUSB device for unique identification..."
231
- );
232
- let serialNumber = null;
233
- let usbMetadata = null;
234
 
235
- try {
236
- // Request USB device access for metadata
237
- const usbDevice = await navigator.usb.requestDevice({
238
- filters: [
239
- { vendorId: 0x0403 }, // FTDI
240
- { vendorId: 0x067b }, // Prolific
241
- { vendorId: 0x10c4 }, // Silicon Labs
242
- { vendorId: 0x1a86 }, // QinHeng Electronics (CH340)
243
- { vendorId: 0x239a }, // Adafruit
244
- { vendorId: 0x2341 }, // Arduino
245
- { vendorId: 0x2e8a }, // Raspberry Pi Foundation
246
- { vendorId: 0x1b4f }, // SparkFun
247
- ],
248
  });
249
 
250
- if (usbDevice) {
251
- serialNumber =
252
- usbDevice.serialNumber ||
253
- `${usbDevice.vendorId}-${usbDevice.productId}-${Date.now()}`;
254
- usbMetadata = {
255
- vendorId: `0x${usbDevice.vendorId.toString(16).padStart(4, "0")}`,
256
- productId: `0x${usbDevice.productId.toString(16).padStart(4, "0")}`,
257
- serialNumber: usbDevice.serialNumber || "Generated ID",
258
- manufacturerName: usbDevice.manufacturerName || "Unknown",
259
- productName: usbDevice.productName || "Unknown",
260
- usbVersionMajor: usbDevice.usbVersionMajor,
261
- usbVersionMinor: usbDevice.usbVersionMinor,
262
- deviceClass: usbDevice.deviceClass,
263
- deviceSubclass: usbDevice.deviceSubclass,
264
- deviceProtocol: usbDevice.deviceProtocol,
265
- };
266
- console.log("✅ USB device metadata acquired:", usbMetadata);
267
- }
268
- } catch (usbError) {
269
  console.log(
270
- "⚠️ WebUSB request failed, generating fallback ID:",
271
- usbError
 
272
  );
273
- // Generate a fallback unique ID if WebUSB fails
274
- serialNumber = `fallback-${Date.now()}-${Math.random()
275
- .toString(36)
276
- .substr(2, 9)}`;
277
- usbMetadata = {
278
- vendorId: "Unknown",
279
- productId: "Unknown",
280
- serialNumber: serialNumber,
281
- manufacturerName: "USB Metadata Not Available",
282
- productName: "Check browser WebUSB support",
283
- };
284
- }
285
 
286
- const portName = getPortDisplayName(port);
287
-
288
- // Step 3: Check if this robot (by serial number) is already connected
289
- const existingIndex = connectedRobots.findIndex(
290
- (robot) => robot.serialNumber === serialNumber
291
- );
292
-
293
- if (existingIndex === -1) {
294
- // New robot - add to list
295
- const newRobot: RobotConnection = {
296
- port,
297
- name: portName,
298
- isConnected: true,
299
- serialNumber: serialNumber!,
300
- usbMetadata: usbMetadata || undefined,
301
- };
302
-
303
- // Try to load saved robot info by serial number using unified storage
304
- if (serialNumber) {
305
- try {
306
- const { getRobotConfig } = await import("../lib/unified-storage");
307
- const savedConfig = getRobotConfig(serialNumber);
308
- if (savedConfig) {
309
- newRobot.robotType = savedConfig.robotType as
310
- | "so100_follower"
311
- | "so100_leader";
312
- newRobot.robotId = savedConfig.robotId;
313
- console.log("📋 Loaded saved robot configuration:", savedConfig);
314
- }
315
- } catch (error) {
316
- console.warn("Failed to load saved robot data:", error);
317
- }
318
- }
319
-
320
- onConnectedRobotsChange([...connectedRobots, newRobot]);
321
- console.log("🤖 New robot connected with ID:", serialNumber);
322
  } else {
323
- // Existing robot - update port and connection status
324
- const updatedRobots = connectedRobots.map((robot, index) =>
325
- index === existingIndex
326
- ? { ...robot, port, isConnected: true, name: portName }
327
- : robot
328
- );
329
- onConnectedRobotsChange(updatedRobots);
330
- console.log("🔄 Existing robot reconnected:", serialNumber);
331
  }
332
  } catch (error) {
333
- if (
334
- error instanceof Error &&
335
- (error.message.includes("cancelled") ||
336
- error.message.includes("No port selected by the user") ||
337
- error.name === "NotAllowedError")
338
- ) {
339
- // User cancelled - no error message needed, just log to console
340
- console.log("Connection cancelled by user");
341
- return;
342
- }
343
- setError(
344
- error instanceof Error ? error.message : "Failed to connect to robot"
345
- );
346
- } finally {
347
- setIsConnecting(false);
348
- }
349
- };
350
-
351
- const handleDisconnect = async (index: number) => {
352
- const portInfo = connectedRobots[index];
353
- const robotName = portInfo.robotId || portInfo.name;
354
- const serialNumber = portInfo.serialNumber || "unknown";
355
-
356
- // Show confirmation dialog
357
- setConfirmDeleteDialog({
358
- open: true,
359
- robotIndex: index,
360
- robotName,
361
- serialNumber,
362
- });
363
- };
364
-
365
- const confirmDelete = async () => {
366
- const { robotIndex } = confirmDeleteDialog;
367
- const portInfo = connectedRobots[robotIndex];
368
-
369
- setConfirmDeleteDialog({
370
- open: false,
371
- robotIndex: -1,
372
- robotName: "",
373
- serialNumber: "",
374
- });
375
-
376
- try {
377
- // Close the serial port connection
378
- if (portInfo.isConnected) {
379
- await portInfo.port.close();
380
- }
381
-
382
- // Delete from unified storage if serial number is available
383
- if (portInfo.serialNumber) {
384
- try {
385
- const { getUnifiedKey } = await import("../lib/unified-storage");
386
- const unifiedKey = getUnifiedKey(portInfo.serialNumber);
387
-
388
- // Remove unified storage data
389
- localStorage.removeItem(unifiedKey);
390
- console.log(`🗑️ Deleted unified robot data: ${unifiedKey}`);
391
- } catch (error) {
392
- console.warn("Failed to delete unified storage data:", error);
393
- }
394
- }
395
-
396
- // Remove from UI
397
- const updatedRobots = connectedRobots.filter((_, i) => i !== robotIndex);
398
- onConnectedRobotsChange(updatedRobots);
399
-
400
- console.log(
401
- `✅ Robot "${confirmDeleteDialog.robotName}" permanently removed from system`
402
- );
403
- } catch (error) {
404
- setError(
405
- error instanceof Error ? error.message : "Failed to remove robot"
406
- );
407
  }
408
  };
409
 
410
- const cancelDelete = () => {
411
- setConfirmDeleteDialog({
412
- open: false,
413
- robotIndex: -1,
414
- robotName: "",
415
- serialNumber: "",
416
- });
417
- };
418
-
419
- const handleUpdatePortInfo = (
420
- index: number,
421
- robotType: "so100_follower" | "so100_leader",
422
- robotId: string
423
- ) => {
424
- const updatedRobots = connectedRobots.map((robot, i) => {
425
- if (i === index) {
426
- const updatedRobot = { ...robot, robotType, robotId };
427
-
428
- // Save robot configuration using unified storage
429
- if (updatedRobot.serialNumber) {
430
- import("../lib/unified-storage")
431
- .then(({ saveRobotConfig }) => {
432
- saveRobotConfig(
433
- updatedRobot.serialNumber!,
434
- robotType,
435
- robotId,
436
- updatedRobot.usbMetadata
437
- );
438
- console.log(
439
- "💾 Saved robot configuration for:",
440
- updatedRobot.serialNumber
441
- );
442
- })
443
- .catch((error) => {
444
- console.warn("Failed to save robot configuration:", error);
445
- });
446
- }
447
-
448
- return updatedRobot;
449
- }
450
- return robot;
451
- });
452
- onConnectedRobotsChange(updatedRobots);
453
- };
454
-
455
  const handleFindPorts = async () => {
456
  if (!isWebSerialSupported()) {
457
  setError("Web Serial API is not supported in this browser");
@@ -463,44 +104,35 @@ export function PortManager({
463
  setFindPortsLog([]);
464
  setError(null);
465
 
466
- // Use the new findPort API from standard library
467
- const { findPort } = await import("@lerobot/web");
468
-
469
  const findPortProcess = await findPort({
470
  onMessage: (message) => {
471
  setFindPortsLog((prev) => [...prev, message]);
472
  },
473
  });
474
 
475
- const robotConnections = (await findPortProcess.result) as any; // RobotConnection[] from findPort
476
- const robotConnection = robotConnections[0]; // Get first robot from array
477
 
478
- const portName = getPortDisplayName(robotConnection.port);
479
- setFindPortsLog((prev) => [...prev, `✅ Port ready: ${portName}`]);
480
-
481
- // Add to connected ports if not already there
482
- const existingIndex = connectedRobots.findIndex(
483
- (p) => p.name === portName
484
  );
485
- if (existingIndex === -1) {
486
- const newPort: RobotConnection = {
487
- port: robotConnection.port,
488
- name: portName,
489
- isConnected: true,
490
- robotType: robotConnection.robotType,
491
- robotId: robotConnection.robotId,
492
- serialNumber: robotConnection.serialNumber,
493
- };
494
- onConnectedRobotsChange([...connectedRobots, newPort]);
495
- }
496
  } catch (error) {
497
  if (
498
  error instanceof Error &&
499
  (error.message.includes("cancelled") ||
500
  error.name === "NotAllowedError")
501
  ) {
502
- // User cancelled - no message needed, just log to console
503
- console.log("Port identification cancelled by user");
504
  return;
505
  }
506
  setError(error instanceof Error ? error.message : "Failed to find ports");
@@ -509,54 +141,37 @@ export function PortManager({
509
  }
510
  };
511
 
512
- const ensurePortIsOpen = async (robotIndex: number) => {
513
- const robot = connectedRobots[robotIndex];
514
- if (!robot) return false;
515
-
516
- try {
517
- // If port is already open, we're good
518
- if (robot.port.readable !== null && robot.port.writable !== null) {
519
- return true;
520
- }
521
-
522
- // Try to open the port
523
- await robot.port.open({ baudRate: 1000000 });
524
-
525
- // Update the robot's connection status
526
- const updatedRobots = connectedRobots.map((r, i) =>
527
- i === robotIndex ? { ...r, isConnected: true } : r
528
- );
529
- onConnectedRobotsChange(updatedRobots);
530
-
531
- return true;
532
- } catch (error) {
533
- console.error("Failed to open port for calibration:", error);
534
- setError(error instanceof Error ? error.message : "Failed to open port");
535
- return false;
536
- }
537
  };
538
 
539
- const handleCalibrate = async (port: RobotConnection) => {
540
- if (!port.robotType || !port.robotId) {
541
- setError("Please set robot type and ID before calibrating");
542
  return;
543
  }
 
 
 
 
544
 
545
- // Find the robot index
546
- const robotIndex = connectedRobots.findIndex((r) => r.port === port.port);
547
- if (robotIndex === -1) {
548
- setError("Robot not found in connected robots list");
549
  return;
550
  }
551
 
552
- // Ensure port is open before calibrating
553
- const isOpen = await ensurePortIsOpen(robotIndex);
554
- if (!isOpen) {
555
- return; // Error already set in ensurePortIsOpen
 
556
  }
557
 
558
- if (onCalibrate) {
559
- onCalibrate(port.port, port.robotType, port.robotId);
 
560
  }
561
  };
562
 
@@ -565,7 +180,7 @@ export function PortManager({
565
  <CardHeader>
566
  <CardTitle>🔌 Robot Connection Manager</CardTitle>
567
  <CardDescription>
568
- Connect, identify, and manage your robot arms
569
  </CardDescription>
570
  </CardHeader>
571
  <CardContent>
@@ -577,28 +192,18 @@ export function PortManager({
577
  </Alert>
578
  )}
579
 
580
- {/* Connection Controls */}
581
- <div className="flex gap-2">
582
- <Button
583
- onClick={handleConnect}
584
- disabled={isConnecting || !isWebSerialSupported()}
585
- className="flex-1"
586
- >
587
- {isConnecting ? "Connecting..." : "Connect Robot"}
588
- </Button>
589
- <Button
590
- variant="outline"
591
- onClick={handleFindPorts}
592
- disabled={isFindingPorts || !isWebSerialSupported()}
593
- className="flex-1"
594
- >
595
- {isFindingPorts ? "Finding..." : "Find Port"}
596
- </Button>
597
- </div>
598
 
599
  {/* Find Ports Log */}
600
  {findPortsLog.length > 0 && (
601
- <div className="bg-gray-50 p-3 rounded-md text-sm space-y-1">
602
  {findPortsLog.map((log, index) => (
603
  <div key={index} className="text-gray-700">
604
  {log}
@@ -607,7 +212,7 @@ export function PortManager({
607
  </div>
608
  )}
609
 
610
- {/* Connected Ports */}
611
  <div>
612
  <h4 className="font-semibold mb-3">
613
  Connected Robots ({connectedRobots.length})
@@ -616,23 +221,20 @@ export function PortManager({
616
  {connectedRobots.length === 0 ? (
617
  <div className="text-center py-8 text-gray-500">
618
  <div className="text-2xl mb-2">🤖</div>
619
- <p>No robots connected</p>
620
  <p className="text-xs">
621
- Use "Connect Robot" or "Find Port" to add robots
622
  </p>
623
  </div>
624
  ) : (
625
  <div className="space-y-4">
626
- {connectedRobots.map((portInfo, index) => (
627
- <PortCard
628
- key={index}
629
- portInfo={portInfo}
630
  onDisconnect={() => handleDisconnect(index)}
631
- onUpdateInfo={(robotType, robotId) =>
632
- handleUpdatePortInfo(index, robotType, robotId)
633
- }
634
- onCalibrate={() => handleCalibrate(portInfo)}
635
- onTeleoperate={() => onTeleoperate?.(portInfo)}
636
  />
637
  ))}
638
  </div>
@@ -640,397 +242,127 @@ export function PortManager({
640
  </div>
641
  </div>
642
  </CardContent>
643
-
644
- {/* Confirmation Dialog */}
645
- <Dialog open={confirmDeleteDialog.open} onOpenChange={cancelDelete}>
646
- <DialogContent>
647
- <DialogHeader>
648
- <DialogTitle>🗑️ Permanently Delete Robot Data?</DialogTitle>
649
- <DialogDescription>
650
- This action cannot be undone. All robot data will be permanently
651
- deleted.
652
- </DialogDescription>
653
- </DialogHeader>
654
-
655
- <div className="space-y-3">
656
- <div className="p-4 bg-red-50 rounded-lg border border-red-200">
657
- <div className="font-medium text-red-900 mb-2">
658
- Robot Information:
659
- </div>
660
- <div className="text-sm text-red-800 space-y-1">
661
- <div>
662
- • Name:{" "}
663
- <span className="font-mono">
664
- {confirmDeleteDialog.robotName}
665
- </span>
666
- </div>
667
- <div>
668
- • Serial:{" "}
669
- <span className="font-mono">
670
- {confirmDeleteDialog.serialNumber}
671
- </span>
672
- </div>
673
- </div>
674
- </div>
675
-
676
- <div className="p-4 bg-red-50 rounded-lg border border-red-200">
677
- <div className="font-medium text-red-900 mb-2">
678
- This will permanently delete:
679
- </div>
680
- <div className="text-sm text-red-800 space-y-1">
681
- <div>• Robot configuration</div>
682
- <div>• Calibration data</div>
683
- <div>• All saved settings</div>
684
- </div>
685
- </div>
686
- </div>
687
-
688
- <DialogFooter>
689
- <Button variant="outline" onClick={cancelDelete}>
690
- Cancel
691
- </Button>
692
- <Button variant="destructive" onClick={confirmDelete}>
693
- Delete Forever
694
- </Button>
695
- </DialogFooter>
696
- </DialogContent>
697
- </Dialog>
698
  </Card>
699
  );
700
  }
701
 
702
- interface PortCardProps {
703
- portInfo: RobotConnection;
704
  onDisconnect: () => void;
705
- onUpdateInfo: (
706
- robotType: "so100_follower" | "so100_leader",
707
- robotId: string
708
- ) => void;
709
  onCalibrate: () => void;
710
  onTeleoperate: () => void;
711
  }
712
 
713
- function PortCard({
714
- portInfo,
715
  onDisconnect,
716
- onUpdateInfo,
717
  onCalibrate,
718
  onTeleoperate,
719
- }: PortCardProps) {
720
- const [robotType, setRobotType] = useState<"so100_follower" | "so100_leader">(
721
- portInfo.robotType || "so100_follower"
722
- );
723
- const [robotId, setRobotId] = useState(portInfo.robotId || "");
724
  const [isEditing, setIsEditing] = useState(false);
725
- const [isScanning, setIsScanning] = useState(false);
726
- const [motorIDs, setMotorIDs] = useState<number[]>([]);
727
- const [portMetadata, setPortMetadata] = useState<any>(null);
728
- const [showDeviceInfo, setShowDeviceInfo] = useState(false);
729
-
730
- // Check for calibration using unified storage
731
- const getCalibrationStatus = () => {
732
- // Use the same serial number logic as calibration: prefer main serialNumber, fallback to USB metadata, then "unknown"
733
- const serialNumber =
734
- portInfo.serialNumber || portInfo.usbMetadata?.serialNumber || "unknown";
735
-
736
- try {
737
- // Use unified storage system with automatic migration
738
- import("../lib/unified-storage")
739
- .then(({ getCalibrationStatus }) => {
740
- const status = getCalibrationStatus(serialNumber);
741
- return status;
742
- })
743
- .catch((error) => {
744
- console.warn("Failed to load unified calibration data:", error);
745
- return null;
746
- });
747
-
748
- // For immediate synchronous return, try to get existing unified data first
749
- const unifiedKey = `lerobotjs-${serialNumber}`;
750
- const existing = localStorage.getItem(unifiedKey);
751
- if (existing) {
752
- const data = JSON.parse(existing);
753
- if (data.calibration?.metadata) {
754
- return {
755
- timestamp: data.calibration.metadata.timestamp,
756
- readCount: data.calibration.metadata.readCount,
757
- };
758
- }
759
- }
760
- } catch (error) {
761
- console.warn("Failed to read calibration from unified storage:", error);
762
- }
763
- return null;
764
- };
765
-
766
- const calibrationStatus = getCalibrationStatus();
767
-
768
- const handleSave = () => {
769
- if (robotId.trim()) {
770
- onUpdateInfo(robotType, robotId.trim());
771
- setIsEditing(false);
772
- }
773
- };
774
-
775
- // Use current values (either from props or local state)
776
- const currentRobotType = portInfo.robotType || robotType;
777
- const currentRobotId = portInfo.robotId || robotId;
778
-
779
- const handleCancel = () => {
780
- setRobotType(portInfo.robotType || "so100_follower");
781
- setRobotId(portInfo.robotId || "");
782
- setIsEditing(false);
783
- };
784
-
785
- // Scan for motor IDs and gather USB device metadata
786
- const scanDeviceInfo = async () => {
787
- if (!portInfo.port || !portInfo.isConnected) {
788
- console.warn("Port not connected");
789
- return;
790
- }
791
 
792
- setIsScanning(true);
793
- setMotorIDs([]);
794
- setPortMetadata(null);
795
- const foundIDs: number[] = [];
796
 
797
- try {
798
- // Try to get USB device info using WebUSB for better metadata
799
- let usbDeviceInfo = null;
 
800
 
801
  try {
802
- // First, check if we already have USB device permissions
803
- let usbDevices = await navigator.usb.getDevices();
804
- console.log("Already permitted USB devices:", usbDevices);
805
-
806
- // If no devices found, request permission for USB-to-serial devices
807
- if (usbDevices.length === 0) {
808
- console.log(
809
- "No USB permissions yet, requesting access to USB-to-serial devices..."
810
- );
811
-
812
- // Request access to common USB-to-serial chips
813
- try {
814
- const device = await navigator.usb.requestDevice({
815
- filters: [
816
- { vendorId: 0x0403 }, // FTDI
817
- { vendorId: 0x067b }, // Prolific
818
- { vendorId: 0x10c4 }, // Silicon Labs
819
- { vendorId: 0x1a86 }, // QinHeng Electronics (CH340)
820
- { vendorId: 0x239a }, // Adafruit
821
- { vendorId: 0x2341 }, // Arduino
822
- { vendorId: 0x2e8a }, // Raspberry Pi Foundation
823
- { vendorId: 0x1b4f }, // SparkFun
824
- ],
825
- });
826
-
827
- if (device) {
828
- usbDevices = [device];
829
- console.log("USB device access granted:", device);
830
- }
831
- } catch (requestError) {
832
- console.log(
833
- "User cancelled USB device selection or no devices found"
834
- );
835
- // Try requesting any device as fallback
836
- try {
837
- const anyDevice = await navigator.usb.requestDevice({
838
- filters: [], // Allow any USB device
839
- });
840
- if (anyDevice) {
841
- usbDevices = [anyDevice];
842
- console.log("Fallback USB device selected:", anyDevice);
843
- }
844
- } catch (fallbackError) {
845
- console.log("No USB device selected");
846
- }
847
- }
848
- }
849
-
850
- // Try to match with Web Serial port (this is tricky, so we'll take the first available)
851
- if (usbDevices.length > 0) {
852
- // Look for common USB-to-serial chip vendor IDs
853
- const serialChipVendors = [
854
- 0x0403, // FTDI
855
- 0x067b, // Prolific
856
- 0x10c4, // Silicon Labs
857
- 0x1a86, // QinHeng Electronics (CH340)
858
- 0x239a, // Adafruit
859
- 0x2341, // Arduino
860
- 0x2e8a, // Raspberry Pi Foundation
861
- 0x1b4f, // SparkFun
862
- ];
863
-
864
- const serialDevice =
865
- usbDevices.find((device) =>
866
- serialChipVendors.includes(device.vendorId)
867
- ) || usbDevices[0]; // Fallback to first device
868
-
869
- if (serialDevice) {
870
- usbDeviceInfo = {
871
- vendorId: `0x${serialDevice.vendorId
872
- .toString(16)
873
- .padStart(4, "0")}`,
874
- productId: `0x${serialDevice.productId
875
- .toString(16)
876
- .padStart(4, "0")}`,
877
- serialNumber: serialDevice.serialNumber || "Not available",
878
- manufacturerName: serialDevice.manufacturerName || "Unknown",
879
- productName: serialDevice.productName || "Unknown",
880
- usbVersionMajor: serialDevice.usbVersionMajor,
881
- usbVersionMinor: serialDevice.usbVersionMinor,
882
- deviceClass: serialDevice.deviceClass,
883
- deviceSubclass: serialDevice.deviceSubclass,
884
- deviceProtocol: serialDevice.deviceProtocol,
885
- };
886
- console.log("USB device info:", usbDeviceInfo);
887
- }
888
- }
889
- } catch (usbError) {
890
- console.log("WebUSB not available or no permissions:", usbError);
891
- // Fallback to Web Serial API info
892
- const portInfo_metadata = portInfo.port.getInfo();
893
- console.log("Serial port metadata fallback:", portInfo_metadata);
894
- if (Object.keys(portInfo_metadata).length > 0) {
895
- usbDeviceInfo = {
896
- vendorId: portInfo_metadata.usbVendorId
897
- ? `0x${portInfo_metadata.usbVendorId
898
- .toString(16)
899
- .padStart(4, "0")}`
900
- : "Not available",
901
- productId: portInfo_metadata.usbProductId
902
- ? `0x${portInfo_metadata.usbProductId
903
- .toString(16)
904
- .padStart(4, "0")}`
905
- : "Not available",
906
- serialNumber: "Not available via Web Serial",
907
- manufacturerName: "Not available via Web Serial",
908
- productName: "Not available via Web Serial",
909
- };
910
- }
911
- }
912
-
913
- setPortMetadata(usbDeviceInfo);
914
-
915
- // Get reader/writer for the port
916
- const reader = portInfo.port.readable?.getReader();
917
- const writer = portInfo.port.writable?.getWriter();
918
-
919
- if (!reader || !writer) {
920
- console.warn("Cannot access port reader/writer");
921
- setShowDeviceInfo(true);
922
- return;
923
  }
 
924
 
925
- // Test motor IDs 1-10 (common range for servos)
926
- for (let motorId = 1; motorId <= 10; motorId++) {
927
- try {
928
- // Create STS3215 ping packet
929
- const packet = new Uint8Array([
930
- 0xff,
931
- 0xff,
932
- motorId,
933
- 0x02,
934
- 0x01,
935
- 0x00,
936
- ]);
937
- const checksum = ~(motorId + 0x02 + 0x01) & 0xff;
938
- packet[5] = checksum;
939
-
940
- // Send ping
941
- await writer.write(packet);
942
 
943
- // Wait a bit for response
944
- await new Promise((resolve) => setTimeout(resolve, 20));
945
 
946
- // Try to read response with timeout
947
- const timeoutPromise = new Promise((_, reject) =>
948
- setTimeout(() => reject(new Error("Timeout")), 50)
949
- );
950
-
951
- try {
952
- const result = (await Promise.race([
953
- reader.read(),
954
- timeoutPromise,
955
- ])) as ReadableStreamReadResult<Uint8Array>;
956
-
957
- if (
958
- result &&
959
- !result.done &&
960
- result.value &&
961
- result.value.length >= 6
962
- ) {
963
- const response = result.value;
964
- const responseId = response[2];
965
-
966
- // If we got a response with matching ID, motor exists
967
- if (responseId === motorId) {
968
- foundIDs.push(motorId);
969
- }
970
- }
971
- } catch (readError) {
972
- // No response from this motor ID - that's normal
973
- }
974
- } catch (error) {
975
- console.warn(`Error testing motor ID ${motorId}:`, error);
976
- }
977
-
978
- // Small delay between tests
979
- await new Promise((resolve) => setTimeout(resolve, 10));
980
- }
981
 
982
- reader.releaseLock();
983
- writer.releaseLock();
 
984
 
985
- setMotorIDs(foundIDs);
986
- setShowDeviceInfo(true);
987
  } catch (error) {
988
- console.error("Device info scan failed:", error);
989
- } finally {
990
- setIsScanning(false);
991
  }
992
  };
993
 
 
 
 
 
 
 
994
  return (
995
  <div className="border rounded-lg p-4 space-y-3">
996
- {/* Header with port name and status */}
997
  <div className="flex items-center justify-between">
998
  <div className="flex items-center space-x-2">
999
  <div className="flex flex-col">
1000
- <span className="font-medium">{portInfo.name}</span>
1001
- {portInfo.serialNumber && (
1002
- <span className="text-xs text-gray-500 font-mono">
1003
- ID:{" "}
1004
- {portInfo.serialNumber.length > 20
1005
- ? portInfo.serialNumber.substring(0, 20) + "..."
1006
- : portInfo.serialNumber}
 
 
 
 
1007
  </span>
1008
  )}
1009
  </div>
1010
- <Badge variant={portInfo.isConnected ? "default" : "outline"}>
1011
- {portInfo.isConnected ? "Connected" : "Available"}
1012
- </Badge>
 
 
 
 
 
 
 
1013
  </div>
1014
  <Button variant="destructive" size="sm" onClick={onDisconnect}>
1015
  Remove
1016
  </Button>
1017
  </div>
1018
 
1019
- {/* Robot Info Display (when not editing) */}
1020
- {!isEditing && currentRobotType && currentRobotId && (
1021
  <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
1022
  <div className="flex items-center space-x-3">
1023
  <div>
1024
- <div className="font-medium text-sm">{currentRobotId}</div>
1025
  <div className="text-xs text-gray-600">
1026
- {currentRobotType.replace("_", " ")}
1027
  </div>
1028
  </div>
1029
- {calibrationStatus && (
1030
- <Badge variant="default" className="bg-green-100 text-green-800">
1031
- ✅ Calibrated
1032
- </Badge>
1033
- )}
1034
  </div>
1035
  <Button
1036
  variant="outline"
@@ -1042,8 +374,8 @@ function PortCard({
1042
  </div>
1043
  )}
1044
 
1045
- {/* Setup prompt for unconfigured robots */}
1046
- {!isEditing && (!currentRobotType || !currentRobotId) && (
1047
  <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
1048
  <div className="text-sm text-blue-800">
1049
  Robot needs configuration before use
@@ -1067,9 +399,9 @@ function PortCard({
1067
  Robot Type
1068
  </label>
1069
  <select
1070
- value={robotType}
1071
  onChange={(e) =>
1072
- setRobotType(
1073
  e.target.value as "so100_follower" | "so100_leader"
1074
  )
1075
  }
@@ -1083,8 +415,8 @@ function PortCard({
1083
  <label className="text-sm font-medium block mb-1">Robot ID</label>
1084
  <input
1085
  type="text"
1086
- value={robotId}
1087
- onChange={(e) => setRobotId(e.target.value)}
1088
  placeholder="e.g., my_robot"
1089
  className="w-full px-2 py-1 border rounded text-sm"
1090
  />
@@ -1092,160 +424,62 @@ function PortCard({
1092
  </div>
1093
 
1094
  <div className="flex gap-2">
1095
- <Button size="sm" onClick={handleSave} disabled={!robotId.trim()}>
 
 
 
 
1096
  Save
1097
  </Button>
1098
- <Button size="sm" variant="outline" onClick={handleCancel}>
1099
  Cancel
1100
  </Button>
1101
  </div>
1102
  </div>
1103
  )}
1104
 
1105
- {/* Calibration Status and Action */}
1106
- {currentRobotType && currentRobotId && (
1107
- <div className="space-y-3">
1108
- <div className="flex items-center justify-between">
1109
- <div className="text-sm text-gray-600">
1110
- {calibrationStatus ? (
1111
- <span>
1112
- Last calibrated:{" "}
1113
- {new Date(calibrationStatus.timestamp).toLocaleDateString()}
1114
- <span className="text-xs ml-1">
1115
- ({calibrationStatus.readCount} readings)
1116
- </span>
1117
- </span>
1118
- ) : (
1119
- <span>Not calibrated yet</span>
1120
- )}
1121
- </div>
1122
- <div className="flex gap-2">
1123
- <Button
1124
- size="sm"
1125
- variant={calibrationStatus ? "outline" : "default"}
1126
- onClick={onCalibrate}
1127
- disabled={!currentRobotType || !currentRobotId}
1128
- >
1129
- {calibrationStatus ? "Re-calibrate" : "Calibrate"}
1130
- </Button>
1131
- <Button
1132
- size="sm"
1133
- variant="outline"
1134
- onClick={onTeleoperate}
1135
- disabled={
1136
- !currentRobotType || !currentRobotId || !portInfo.isConnected
1137
- }
1138
- >
1139
- 🎮 Teleoperate
1140
- </Button>
1141
- </div>
1142
- </div>
1143
-
1144
- {/* Device Info Scanner */}
1145
- <div className="flex items-center justify-between">
1146
- <div className="text-sm text-gray-600">
1147
- Scan device info and motor IDs
1148
- </div>
1149
- <Button
1150
- size="sm"
1151
- variant="outline"
1152
- onClick={scanDeviceInfo}
1153
- disabled={!portInfo.isConnected || isScanning}
1154
- >
1155
- {isScanning ? "Scanning..." : "Show Device Info"}
1156
- </Button>
1157
- </div>
1158
-
1159
- {/* Device Info Results */}
1160
- {showDeviceInfo && (
1161
- <div className="p-3 bg-gray-50 rounded-lg space-y-3">
1162
- {/* USB Device Information */}
1163
- {portMetadata && (
1164
- <div>
1165
- <div className="text-sm font-medium mb-2">
1166
- 📱 USB Device Info:
1167
- </div>
1168
- <div className="space-y-1 text-xs">
1169
- <div className="flex justify-between">
1170
- <span className="text-gray-600">Vendor ID:</span>
1171
- <span className="font-mono">{portMetadata.vendorId}</span>
1172
- </div>
1173
- <div className="flex justify-between">
1174
- <span className="text-gray-600">Product ID:</span>
1175
- <span className="font-mono">
1176
- {portMetadata.productId}
1177
- </span>
1178
- </div>
1179
- <div className="flex justify-between">
1180
- <span className="text-gray-600">Serial Number:</span>
1181
- <span className="font-mono text-green-600 font-semibold">
1182
- {portMetadata.serialNumber}
1183
- </span>
1184
- </div>
1185
- <div className="flex justify-between">
1186
- <span className="text-gray-600">Manufacturer:</span>
1187
- <span>{portMetadata.manufacturerName}</span>
1188
- </div>
1189
- <div className="flex justify-between">
1190
- <span className="text-gray-600">Product:</span>
1191
- <span>{portMetadata.productName}</span>
1192
- </div>
1193
- {portMetadata.usbVersionMajor && (
1194
- <div className="flex justify-between">
1195
- <span className="text-gray-600">USB Version:</span>
1196
- <span>
1197
- {portMetadata.usbVersionMajor}.
1198
- {portMetadata.usbVersionMinor}
1199
- </span>
1200
- </div>
1201
- )}
1202
- {portMetadata.deviceClass !== undefined && (
1203
- <div className="flex justify-between">
1204
- <span className="text-gray-600">Device Class:</span>
1205
- <span>
1206
- 0x
1207
- {portMetadata.deviceClass
1208
- .toString(16)
1209
- .padStart(2, "0")}
1210
- </span>
1211
- </div>
1212
- )}
1213
- </div>
1214
- </div>
1215
- )}
1216
-
1217
- {/* Motor IDs */}
1218
- <div>
1219
- <div className="text-sm font-medium mb-2">
1220
- 🤖 Found Motor IDs:
1221
- </div>
1222
- {motorIDs.length > 0 ? (
1223
- <div className="flex flex-wrap gap-2">
1224
- {motorIDs.map((id) => (
1225
- <Badge key={id} variant="outline" className="text-xs">
1226
- Motor {id}
1227
- </Badge>
1228
- ))}
1229
- </div>
1230
- ) : (
1231
- <div className="text-sm text-gray-500">
1232
- No motor IDs found. Check connection and power.
1233
- </div>
1234
- )}
1235
- </div>
1236
-
1237
- <Button
1238
- size="sm"
1239
- variant="outline"
1240
- onClick={() => setShowDeviceInfo(false)}
1241
- className="mt-2 text-xs"
1242
- >
1243
- Hide
1244
- </Button>
1245
- </div>
1246
  )}
1247
  </div>
1248
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1249
  </div>
1250
  );
1251
  }
 
9
  } from "./ui/card";
10
  import { Alert, AlertDescription } from "./ui/alert";
11
  import { Badge } from "./ui/badge";
12
+ import { findPort, isWebSerialSupported } from "@lerobot/web";
 
 
 
 
 
 
 
 
13
  import type { RobotConnection } from "@lerobot/web";
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  interface PortManagerProps {
16
  connectedRobots: RobotConnection[];
17
  onConnectedRobotsChange: (robots: RobotConnection[]) => void;
18
+ onCalibrate?: (port: any) => void; // Let library handle port type
 
 
 
 
19
  onTeleoperate?: (robot: RobotConnection) => void;
20
  }
21
 
 
25
  onCalibrate,
26
  onTeleoperate,
27
  }: PortManagerProps) {
 
28
  const [isFindingPorts, setIsFindingPorts] = useState(false);
29
  const [findPortsLog, setFindPortsLog] = useState<string[]>([]);
30
  const [error, setError] = useState<string | null>(null);
31
+
32
+ // Load saved robots on mount by calling findPort with saved data
 
 
 
 
 
 
 
 
 
 
33
  useEffect(() => {
34
+ loadSavedRobots();
35
  }, []);
36
 
37
+ const loadSavedRobots = async () => {
 
 
38
  try {
39
+ console.log("🔄 Loading saved robots from localStorage...");
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ // Load saved robot configs for auto-connect mode
42
+ const robotConfigs: any[] = [];
43
+ const { getUnifiedRobotData } = await import("../lib/unified-storage");
 
 
 
44
 
45
+ // Check localStorage for saved robot data
46
+ for (let i = 0; i < localStorage.length; i++) {
47
+ const key = localStorage.key(i);
48
+ if (key && key.startsWith("lerobotjs-")) {
49
+ const serialNumber = key.replace("lerobotjs-", "");
50
+ const robotData = getUnifiedRobotData(serialNumber);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ if (robotData) {
 
 
 
 
 
 
 
 
53
  console.log(
54
+ `✅ Found saved robot: ${robotData.device_info.robotId}`
55
  );
56
+
57
+ // Create robot config for auto-connect mode
58
+ robotConfigs.push({
59
+ robotType: robotData.device_info.robotType,
60
+ robotId: robotData.device_info.robotId,
61
+ serialNumber: serialNumber,
62
+ });
63
  }
 
 
 
64
  }
 
 
 
 
 
 
 
 
 
 
65
  }
66
 
67
+ if (robotConfigs.length > 0) {
68
+ console.log(
69
+ `🔄 Auto-connecting to ${robotConfigs.length} saved robots...`
70
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ // Use auto-connect mode - NO DIALOG will be shown!
73
+ const findPortProcess = await findPort({
74
+ robotConfigs,
75
+ onMessage: (message) => {
76
+ console.log(`Auto-connect: ${message}`);
77
+ },
 
 
 
 
 
 
 
78
  });
79
 
80
+ const reconnectedRobots = await findPortProcess.result;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  console.log(
82
+ `✅ Auto-connected to ${
83
+ reconnectedRobots.filter((r) => r.isConnected).length
84
+ }/${robotConfigs.length} saved robots`
85
  );
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ onConnectedRobotsChange(reconnectedRobots);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  } else {
89
+ console.log("No saved robots found in localStorage");
 
 
 
 
 
 
 
90
  }
91
  } catch (error) {
92
+ console.error("Failed to load saved robots:", error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
94
  };
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  const handleFindPorts = async () => {
97
  if (!isWebSerialSupported()) {
98
  setError("Web Serial API is not supported in this browser");
 
104
  setFindPortsLog([]);
105
  setError(null);
106
 
107
+ // Use clean library API - library handles everything!
 
 
108
  const findPortProcess = await findPort({
109
  onMessage: (message) => {
110
  setFindPortsLog((prev) => [...prev, message]);
111
  },
112
  });
113
 
114
+ const robotConnections = await findPortProcess.result;
 
115
 
116
+ // Add new robots to the list (avoid duplicates)
117
+ const newRobots = robotConnections.filter(
118
+ (newRobot) =>
119
+ !connectedRobots.some(
120
+ (existing) => existing.serialNumber === newRobot.serialNumber
121
+ )
122
  );
123
+
124
+ onConnectedRobotsChange([...connectedRobots, ...newRobots]);
125
+ setFindPortsLog((prev) => [
126
+ ...prev,
127
+ `✅ Found ${newRobots.length} new robots`,
128
+ ]);
 
 
 
 
 
129
  } catch (error) {
130
  if (
131
  error instanceof Error &&
132
  (error.message.includes("cancelled") ||
133
  error.name === "NotAllowedError")
134
  ) {
135
+ console.log("Port discovery cancelled by user");
 
136
  return;
137
  }
138
  setError(error instanceof Error ? error.message : "Failed to find ports");
 
141
  }
142
  };
143
 
144
+ const handleDisconnect = (index: number) => {
145
+ const updatedRobots = connectedRobots.filter((_, i) => i !== index);
146
+ onConnectedRobotsChange(updatedRobots);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  };
148
 
149
+ const handleCalibrate = (robot: RobotConnection) => {
150
+ if (!robot.robotType || !robot.robotId) {
151
+ setError("Please configure robot type and ID first");
152
  return;
153
  }
154
+ if (onCalibrate) {
155
+ onCalibrate(robot.port);
156
+ }
157
+ };
158
 
159
+ const handleTeleoperate = (robot: RobotConnection) => {
160
+ if (!robot.robotType || !robot.robotId) {
161
+ setError("Please configure robot type and ID first");
 
162
  return;
163
  }
164
 
165
+ if (!robot.isConnected || !robot.port) {
166
+ setError(
167
+ "Robot is not connected. Please use 'Find & Connect Robots' first."
168
+ );
169
+ return;
170
  }
171
 
172
+ // Robot is connected, proceed with teleoperation
173
+ if (onTeleoperate) {
174
+ onTeleoperate(robot);
175
  }
176
  };
177
 
 
180
  <CardHeader>
181
  <CardTitle>🔌 Robot Connection Manager</CardTitle>
182
  <CardDescription>
183
+ Find and connect to your robot devices
184
  </CardDescription>
185
  </CardHeader>
186
  <CardContent>
 
192
  </Alert>
193
  )}
194
 
195
+ {/* Find Ports Button */}
196
+ <Button
197
+ onClick={handleFindPorts}
198
+ disabled={isFindingPorts || !isWebSerialSupported()}
199
+ className="w-full"
200
+ >
201
+ {isFindingPorts ? "Finding Robots..." : "🔍 Find & Connect Robots"}
202
+ </Button>
 
 
 
 
 
 
 
 
 
 
203
 
204
  {/* Find Ports Log */}
205
  {findPortsLog.length > 0 && (
206
+ <div className="bg-gray-50 p-3 rounded-md text-sm space-y-1 max-h-32 overflow-y-auto">
207
  {findPortsLog.map((log, index) => (
208
  <div key={index} className="text-gray-700">
209
  {log}
 
212
  </div>
213
  )}
214
 
215
+ {/* Connected Robots */}
216
  <div>
217
  <h4 className="font-semibold mb-3">
218
  Connected Robots ({connectedRobots.length})
 
221
  {connectedRobots.length === 0 ? (
222
  <div className="text-center py-8 text-gray-500">
223
  <div className="text-2xl mb-2">🤖</div>
224
+ <p>No robots found</p>
225
  <p className="text-xs">
226
+ Click "Find & Connect Robots" to discover devices
227
  </p>
228
  </div>
229
  ) : (
230
  <div className="space-y-4">
231
+ {connectedRobots.map((robot, index) => (
232
+ <RobotCard
233
+ key={robot.serialNumber || index}
234
+ robot={robot}
235
  onDisconnect={() => handleDisconnect(index)}
236
+ onCalibrate={() => handleCalibrate(robot)}
237
+ onTeleoperate={() => handleTeleoperate(robot)}
 
 
 
238
  />
239
  ))}
240
  </div>
 
242
  </div>
243
  </div>
244
  </CardContent>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  </Card>
246
  );
247
  }
248
 
249
+ interface RobotCardProps {
250
+ robot: RobotConnection;
251
  onDisconnect: () => void;
 
 
 
 
252
  onCalibrate: () => void;
253
  onTeleoperate: () => void;
254
  }
255
 
256
+ function RobotCard({
257
+ robot,
258
  onDisconnect,
 
259
  onCalibrate,
260
  onTeleoperate,
261
+ }: RobotCardProps) {
262
+ const [calibrationStatus, setCalibrationStatus] = useState<{
263
+ timestamp: string;
264
+ readCount: number;
265
+ } | null>(null);
266
  const [isEditing, setIsEditing] = useState(false);
267
+ const [editRobotType, setEditRobotType] = useState<
268
+ "so100_follower" | "so100_leader"
269
+ >(robot.robotType || "so100_follower");
270
+ const [editRobotId, setEditRobotId] = useState(robot.robotId || "");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ const isConfigured = robot.robotType && robot.robotId;
 
 
 
273
 
274
+ // Check calibration status using unified storage
275
+ useEffect(() => {
276
+ const checkCalibrationStatus = async () => {
277
+ if (!robot.serialNumber) return;
278
 
279
  try {
280
+ const { getCalibrationStatus } = await import("../lib/unified-storage");
281
+ const status = getCalibrationStatus(robot.serialNumber);
282
+ setCalibrationStatus(status);
283
+ } catch (error) {
284
+ console.warn("Failed to check calibration status:", error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  }
286
+ };
287
 
288
+ checkCalibrationStatus();
289
+ }, [robot.serialNumber]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ const handleSaveConfig = async () => {
292
+ if (!editRobotId.trim() || !robot.serialNumber) return;
293
 
294
+ try {
295
+ const { saveRobotConfig } = await import("../lib/unified-storage");
296
+ saveRobotConfig(
297
+ robot.serialNumber,
298
+ editRobotType,
299
+ editRobotId.trim(),
300
+ robot.usbMetadata
301
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
+ // Update the robot object (this should trigger a re-render)
304
+ robot.robotType = editRobotType;
305
+ robot.robotId = editRobotId.trim();
306
 
307
+ setIsEditing(false);
308
+ console.log("✅ Robot configuration saved");
309
  } catch (error) {
310
+ console.error("Failed to save robot configuration:", error);
 
 
311
  }
312
  };
313
 
314
+ const handleCancelEdit = () => {
315
+ setEditRobotType(robot.robotType || "so100_follower");
316
+ setEditRobotId(robot.robotId || "");
317
+ setIsEditing(false);
318
+ };
319
+
320
  return (
321
  <div className="border rounded-lg p-4 space-y-3">
322
+ {/* Header */}
323
  <div className="flex items-center justify-between">
324
  <div className="flex items-center space-x-2">
325
  <div className="flex flex-col">
326
+ <span className="font-medium">
327
+ {robot.robotId || robot.name || "Unnamed Robot"}
328
+ </span>
329
+ <span className="text-xs text-gray-500">
330
+ {robot.robotType?.replace("_", " ") || "Not configured"}
331
+ </span>
332
+ {robot.serialNumber && (
333
+ <span className="text-xs text-gray-400 font-mono">
334
+ {robot.serialNumber.length > 20
335
+ ? robot.serialNumber.substring(0, 20) + "..."
336
+ : robot.serialNumber}
337
  </span>
338
  )}
339
  </div>
340
+ <div className="flex flex-col gap-1">
341
+ <Badge variant={robot.isConnected ? "default" : "outline"}>
342
+ {robot.isConnected ? "Connected" : "Available"}
343
+ </Badge>
344
+ {calibrationStatus && (
345
+ <Badge variant="default" className="bg-green-100 text-green-800">
346
+ ✅ Calibrated
347
+ </Badge>
348
+ )}
349
+ </div>
350
  </div>
351
  <Button variant="destructive" size="sm" onClick={onDisconnect}>
352
  Remove
353
  </Button>
354
  </div>
355
 
356
+ {/* Robot Configuration Display (when not editing) */}
357
+ {!isEditing && isConfigured && (
358
  <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
359
  <div className="flex items-center space-x-3">
360
  <div>
361
+ <div className="font-medium text-sm">{robot.robotId}</div>
362
  <div className="text-xs text-gray-600">
363
+ {robot.robotType?.replace("_", " ")}
364
  </div>
365
  </div>
 
 
 
 
 
366
  </div>
367
  <Button
368
  variant="outline"
 
374
  </div>
375
  )}
376
 
377
+ {/* Configuration Prompt for unconfigured robots */}
378
+ {!isEditing && !isConfigured && (
379
  <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
380
  <div className="text-sm text-blue-800">
381
  Robot needs configuration before use
 
399
  Robot Type
400
  </label>
401
  <select
402
+ value={editRobotType}
403
  onChange={(e) =>
404
+ setEditRobotType(
405
  e.target.value as "so100_follower" | "so100_leader"
406
  )
407
  }
 
415
  <label className="text-sm font-medium block mb-1">Robot ID</label>
416
  <input
417
  type="text"
418
+ value={editRobotId}
419
+ onChange={(e) => setEditRobotId(e.target.value)}
420
  placeholder="e.g., my_robot"
421
  className="w-full px-2 py-1 border rounded text-sm"
422
  />
 
424
  </div>
425
 
426
  <div className="flex gap-2">
427
+ <Button
428
+ size="sm"
429
+ onClick={handleSaveConfig}
430
+ disabled={!editRobotId.trim()}
431
+ >
432
  Save
433
  </Button>
434
+ <Button size="sm" variant="outline" onClick={handleCancelEdit}>
435
  Cancel
436
  </Button>
437
  </div>
438
  </div>
439
  )}
440
 
441
+ {/* Calibration Status */}
442
+ {isConfigured && !isEditing && (
443
+ <div className="text-sm text-gray-600">
444
+ {calibrationStatus ? (
445
+ <span>
446
+ Last calibrated:{" "}
447
+ {new Date(calibrationStatus.timestamp).toLocaleDateString()}
448
+ <span className="text-xs ml-1">
449
+ ({calibrationStatus.readCount} readings)
450
+ </span>
451
+ </span>
452
+ ) : (
453
+ <span>Not calibrated yet</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  )}
455
  </div>
456
  )}
457
+
458
+ {/* Actions */}
459
+ {isConfigured && !isEditing && (
460
+ <div className="flex gap-2">
461
+ <Button
462
+ size="sm"
463
+ variant={calibrationStatus ? "outline" : "default"}
464
+ onClick={onCalibrate}
465
+ >
466
+ {calibrationStatus ? "📏 Re-calibrate" : "📏 Calibrate"}
467
+ </Button>
468
+ <Button
469
+ size="sm"
470
+ variant="outline"
471
+ onClick={onTeleoperate}
472
+ disabled={!robot.isConnected}
473
+ title={
474
+ !robot.isConnected
475
+ ? "Use 'Find & Connect Robots' first"
476
+ : undefined
477
+ }
478
+ >
479
+ 🎮 Teleoperate
480
+ </Button>
481
+ </div>
482
+ )}
483
  </div>
484
  );
485
  }
examples/robot-control-web/components/TeleoperationPanel.tsx CHANGED
@@ -208,6 +208,41 @@ export function TeleoperationPanel({
208
  keyCode as keyof typeof SO100_KEYBOARD_CONTROLS
209
  ];
210
  const pressed = isKeyPressed(keyCode);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
  return (
213
  <Button
@@ -226,15 +261,15 @@ export function TeleoperationPanel({
226
  disabled={!isActive}
227
  onMouseDown={(e) => {
228
  e.preventDefault();
229
- if (isActive) simulateKeyPress(keyCode);
230
  }}
231
  onMouseUp={(e) => {
232
  e.preventDefault();
233
- if (isActive) simulateKeyRelease(keyCode);
234
  }}
235
  onMouseLeave={(e) => {
236
  e.preventDefault();
237
- if (isActive) simulateKeyRelease(keyCode);
238
  }}
239
  title={control?.description || keyCode}
240
  >
 
208
  keyCode as keyof typeof SO100_KEYBOARD_CONTROLS
209
  ];
210
  const pressed = isKeyPressed(keyCode);
211
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
212
+
213
+ const startContinuousPress = () => {
214
+ if (!isActive || !teleoperationProcessRef.current) return;
215
+
216
+ // Initial press
217
+ simulateKeyPress(keyCode);
218
+
219
+ // Set up continuous updates to maintain key state
220
+ // Update every 50ms to stay well within the 10 second timeout
221
+ intervalRef.current = setInterval(() => {
222
+ if (teleoperationProcessRef.current) {
223
+ simulateKeyPress(keyCode);
224
+ }
225
+ }, 50);
226
+ };
227
+
228
+ const stopContinuousPress = () => {
229
+ if (intervalRef.current) {
230
+ clearInterval(intervalRef.current);
231
+ intervalRef.current = null;
232
+ }
233
+ if (isActive) {
234
+ simulateKeyRelease(keyCode);
235
+ }
236
+ };
237
+
238
+ // Cleanup interval on unmount
239
+ useEffect(() => {
240
+ return () => {
241
+ if (intervalRef.current) {
242
+ clearInterval(intervalRef.current);
243
+ }
244
+ };
245
+ }, []);
246
 
247
  return (
248
  <Button
 
261
  disabled={!isActive}
262
  onMouseDown={(e) => {
263
  e.preventDefault();
264
+ startContinuousPress();
265
  }}
266
  onMouseUp={(e) => {
267
  e.preventDefault();
268
+ stopContinuousPress();
269
  }}
270
  onMouseLeave={(e) => {
271
  e.preventDefault();
272
+ stopContinuousPress();
273
  }}
274
  title={control?.description || keyCode}
275
  >
examples/test-sequential-operations.html ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Sequential Operations Test - LeRobot.js</title>
7
+ <style>
8
+ body {
9
+ font-family: system-ui, sans-serif;
10
+ max-width: 800px;
11
+ margin: 2rem auto;
12
+ padding: 2rem;
13
+ background: #f5f5f5;
14
+ }
15
+
16
+ .container {
17
+ background: white;
18
+ padding: 2rem;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
21
+ }
22
+
23
+ button {
24
+ background: #2563eb;
25
+ color: white;
26
+ border: none;
27
+ padding: 12px 24px;
28
+ border-radius: 6px;
29
+ font-size: 16px;
30
+ cursor: pointer;
31
+ margin: 8px;
32
+ }
33
+
34
+ button:hover {
35
+ background: #1d4ed8;
36
+ }
37
+
38
+ button:disabled {
39
+ background: #9ca3af;
40
+ cursor: not-allowed;
41
+ }
42
+
43
+ .log {
44
+ background: #1f2937;
45
+ color: #f9fafb;
46
+ padding: 1rem;
47
+ border-radius: 6px;
48
+ font-family: "Courier New", monospace;
49
+ font-size: 14px;
50
+ max-height: 400px;
51
+ overflow-y: auto;
52
+ white-space: pre-wrap;
53
+ margin-top: 1rem;
54
+ }
55
+
56
+ .warning {
57
+ background: #fef3c7;
58
+ border: 1px solid #f59e0b;
59
+ color: #92400e;
60
+ padding: 1rem;
61
+ border-radius: 6px;
62
+ margin: 1rem 0;
63
+ }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <div class="container">
68
+ <h1>🧪 Sequential Operations Test</h1>
69
+ <p>
70
+ This test checks if calling <code>findPort</code> →
71
+ <code>calibrate</code> → <code>releaseMotors</code> →
72
+ <code>teleoperate</code> works without connection conflicts.
73
+ </p>
74
+
75
+ <div class="warning">
76
+ <strong>⚠️ Note:</strong> This test requires a robot to be connected.
77
+ Make sure your SO-100 robot is plugged in via USB.
78
+ </div>
79
+
80
+ <button id="runTest" onclick="runSequentialTest()">
81
+ 🚀 Run Sequential Operations Test
82
+ </button>
83
+
84
+ <button id="clearLog" onclick="clearLog()">🗑️ Clear Log</button>
85
+
86
+ <div id="log" class="log"></div>
87
+ </div>
88
+
89
+ <script type="module" src="./test-sequential-operations.ts"></script>
90
+ </body>
91
+ </html>
examples/test-sequential-operations.ts ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Sequential Operations Test Logic
3
+ * Tests: findPort → calibrate → releaseMotors → teleoperate
4
+ */
5
+
6
+ import {
7
+ findPort,
8
+ calibrate,
9
+ releaseMotors,
10
+ teleoperate,
11
+ WebSerialPortWrapper,
12
+ createSO100Config,
13
+ } from "@lerobot/web";
14
+
15
+ let isRunning = false;
16
+
17
+ function log(message: string) {
18
+ const logElement = document.getElementById("log");
19
+ if (logElement) {
20
+ const timestamp = new Date().toLocaleTimeString();
21
+ logElement.textContent += `[${timestamp}] ${message}\n`;
22
+ logElement.scrollTop = logElement.scrollHeight;
23
+ }
24
+ }
25
+
26
+ function setButtonState(running: boolean) {
27
+ isRunning = running;
28
+ const button = document.getElementById("runTest") as HTMLButtonElement;
29
+ if (button) {
30
+ button.disabled = running;
31
+ button.textContent = running
32
+ ? "⏳ Running Test..."
33
+ : "🚀 Run Sequential Operations Test";
34
+ }
35
+ }
36
+
37
+ declare global {
38
+ interface Window {
39
+ clearLog: () => void;
40
+ runSequentialTest: () => Promise<void>;
41
+ }
42
+ }
43
+
44
+ window.clearLog = function () {
45
+ const logElement = document.getElementById("log");
46
+ if (logElement) {
47
+ logElement.textContent = "Log cleared.\n";
48
+ }
49
+ };
50
+
51
+ window.runSequentialTest = async function () {
52
+ if (isRunning) return;
53
+
54
+ setButtonState(true);
55
+ log("🚀 Starting sequential operations test...");
56
+
57
+ try {
58
+ // Step 1: Find port
59
+ log("\n1️⃣ Finding robot port...");
60
+ const findProcess = await findPort();
61
+ const robots = await findProcess.result;
62
+ const robot = robots[0]; // Get first robot
63
+
64
+ if (!robot || !robot.isConnected) {
65
+ throw new Error("No robot found or robot not connected");
66
+ }
67
+
68
+ log(`✅ Found robot: ${robot.name} (${robot.robotType})`);
69
+ log(` Serial: ${robot.serialNumber}`);
70
+
71
+ // Step 2: Release motors first, then calibrate
72
+ log("\n2️⃣ Releasing motors for calibration setup...");
73
+
74
+ if (!robot.robotType) {
75
+ throw new Error("Robot type not configured");
76
+ }
77
+
78
+ // Release motors so you can move the arm during calibration
79
+ log("🔧 Creating port and config for motor release...");
80
+ const setupPort = new WebSerialPortWrapper(robot.port);
81
+ await setupPort.initialize();
82
+ const setupConfig = createSO100Config(robot.robotType);
83
+
84
+ log(`🔓 Releasing ${setupConfig.motorIds.length} motors...`);
85
+ await releaseMotors(setupPort, setupConfig.motorIds);
86
+ log("✅ Motors released - you can now move the arm freely!");
87
+
88
+ // Now start calibration
89
+ log("\n📏 Starting calibration with live position updates...");
90
+ log("💡 Move the arm through its range of motion to see live updates!");
91
+
92
+ const useSimulatedCalibration = false; // Real calibration to see live updates
93
+ let calibrationResult: any;
94
+
95
+ if (useSimulatedCalibration) {
96
+ // Simulated calibration data (for testing without robot movement)
97
+ log("📊 Using simulated calibration data for testing...");
98
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulate calibration time
99
+
100
+ calibrationResult = {
101
+ shoulder_pan: {
102
+ id: 1,
103
+ drive_mode: 0,
104
+ homing_offset: 34,
105
+ range_min: 994,
106
+ range_max: 3100,
107
+ },
108
+ shoulder_lift: {
109
+ id: 2,
110
+ drive_mode: 0,
111
+ homing_offset: 991,
112
+ range_min: 960,
113
+ range_max: 3233,
114
+ },
115
+ elbow_flex: {
116
+ id: 3,
117
+ drive_mode: 0,
118
+ homing_offset: -881,
119
+ range_min: 1029,
120
+ range_max: 3065,
121
+ },
122
+ wrist_flex: {
123
+ id: 4,
124
+ drive_mode: 0,
125
+ homing_offset: 128,
126
+ range_min: 710,
127
+ range_max: 3135,
128
+ },
129
+ wrist_roll: {
130
+ id: 5,
131
+ drive_mode: 0,
132
+ homing_offset: -15,
133
+ range_min: 0,
134
+ range_max: 4095,
135
+ },
136
+ gripper: {
137
+ id: 6,
138
+ drive_mode: 0,
139
+ homing_offset: -1151,
140
+ range_min: 2008,
141
+ range_max: 3606,
142
+ },
143
+ };
144
+
145
+ log("✅ Calibration completed (simulated)");
146
+ } else {
147
+ // Real calibration
148
+ const calibrationProcess = await calibrate(robot, {
149
+ onProgress: (message) => log(`📊 ${message}`),
150
+ onLiveUpdate: (data) => {
151
+ const motors = Object.keys(data);
152
+ if (motors.length > 0) {
153
+ const ranges = motors.map((m) => data[m].range).join(", ");
154
+ log(`📏 Motor ranges: [${ranges}]`);
155
+ }
156
+ },
157
+ });
158
+
159
+ // Auto-stop calibration after 8 seconds for testing
160
+ setTimeout(() => {
161
+ log("⏱️ Auto-stopping calibration for test...");
162
+ calibrationProcess.stop();
163
+ }, 8000);
164
+
165
+ await calibrationProcess.result;
166
+ log("✅ Calibration completed (real)");
167
+ }
168
+
169
+ // Step 3: Use your provided calibration config for teleoperation
170
+ log("\n3️⃣ Setting up teleoperation with your calibration config...");
171
+
172
+ // Use your provided calibration config instead of real calibration result
173
+ calibrationResult = {
174
+ shoulder_pan: {
175
+ id: 1,
176
+ drive_mode: 0,
177
+ homing_offset: 34,
178
+ range_min: 994,
179
+ range_max: 3100,
180
+ },
181
+ shoulder_lift: {
182
+ id: 2,
183
+ drive_mode: 0,
184
+ homing_offset: 991,
185
+ range_min: 960,
186
+ range_max: 3233,
187
+ },
188
+ elbow_flex: {
189
+ id: 3,
190
+ drive_mode: 0,
191
+ homing_offset: -881,
192
+ range_min: 1029,
193
+ range_max: 3065,
194
+ },
195
+ wrist_flex: {
196
+ id: 4,
197
+ drive_mode: 0,
198
+ homing_offset: 128,
199
+ range_min: 710,
200
+ range_max: 3135,
201
+ },
202
+ wrist_roll: {
203
+ id: 5,
204
+ drive_mode: 0,
205
+ homing_offset: -15,
206
+ range_min: 0,
207
+ range_max: 4095,
208
+ },
209
+ gripper: {
210
+ id: 6,
211
+ drive_mode: 0,
212
+ homing_offset: -1151,
213
+ range_min: 2008,
214
+ range_max: 3606,
215
+ },
216
+ };
217
+
218
+ log(
219
+ `✅ Using your calibration config: ${Object.keys(calibrationResult).join(
220
+ ", "
221
+ )}`
222
+ );
223
+
224
+ // Step 4: Teleoperate with auto key simulation
225
+ log("\n4️⃣ Starting teleoperation...");
226
+ const teleoperationProcess = await teleoperate(robot, {
227
+ calibrationData: calibrationResult,
228
+ onStateUpdate: (state) => {
229
+ if (state.isActive && Object.keys(state.keyStates).length > 0) {
230
+ const activeKeys = Object.keys(state.keyStates).filter(
231
+ (k) => state.keyStates[k].pressed
232
+ );
233
+ log(`🎮 Auto-simulated keys: ${activeKeys.join(", ")}`);
234
+ }
235
+ },
236
+ });
237
+
238
+ teleoperationProcess.start();
239
+ log("✅ Teleoperation started");
240
+ log("🤖 Auto-simulating arrow key presses to move the arm...");
241
+
242
+ // Auto-simulate key presses (left/right arrows to move shoulder pan)
243
+ let keySimulationActive = true;
244
+ let currentDirection = "ArrowLeft";
245
+
246
+ const simulateKeys = () => {
247
+ if (!keySimulationActive) return;
248
+
249
+ // Press current key
250
+ teleoperationProcess.updateKeyState(currentDirection, true);
251
+ log(`🔄 Pressing ${currentDirection} (shoulder pan movement)`);
252
+
253
+ // Hold for 1 second, then release and switch direction
254
+ setTimeout(() => {
255
+ if (!keySimulationActive) return;
256
+ teleoperationProcess.updateKeyState(currentDirection, false);
257
+
258
+ // Switch direction
259
+ currentDirection =
260
+ currentDirection === "ArrowLeft" ? "ArrowRight" : "ArrowLeft";
261
+
262
+ // Wait 500ms then start next movement
263
+ setTimeout(() => {
264
+ if (keySimulationActive) simulateKeys();
265
+ }, 500);
266
+ }, 1000);
267
+ };
268
+
269
+ // Start key simulation after a brief delay
270
+ setTimeout(simulateKeys, 1000);
271
+
272
+ // Auto-stop teleoperation after 8 seconds for testing
273
+ setTimeout(() => {
274
+ keySimulationActive = false;
275
+ log("⏱️ Auto-stopping teleoperation for test...");
276
+ teleoperationProcess.stop();
277
+ log("\n🎉 All sequential operations completed successfully!");
278
+ log(
279
+ "\n📝 RESULT: The current approach works but is too complex for users!"
280
+ );
281
+ log(
282
+ "📝 Users shouldn't need WebSerialPortWrapper and createSO100Config!"
283
+ );
284
+ setButtonState(false);
285
+ }, 8000);
286
+ } catch (error: any) {
287
+ log(`❌ Sequential operations failed: ${error.message}`);
288
+
289
+ // Check if it's a connection conflict
290
+ if (
291
+ error.message.includes("port") ||
292
+ error.message.includes("serial") ||
293
+ error.message.includes("connection")
294
+ ) {
295
+ log("🔍 This might be a port connection conflict!");
296
+ log("💡 Multiple WebSerialPortWrapper instances may be interfering");
297
+ }
298
+
299
+ setButtonState(false);
300
+ }
301
+ };
302
+
303
+ // Initialize on DOM load
304
+ document.addEventListener("DOMContentLoaded", () => {
305
+ // Check Web Serial support
306
+ if (!("serial" in navigator)) {
307
+ log("❌ Web Serial API not supported in this browser");
308
+ log("💡 Try Chrome/Edge with --enable-web-serial flag");
309
+ const button = document.getElementById("runTest") as HTMLButtonElement;
310
+ if (button) button.disabled = true;
311
+ } else {
312
+ log("✅ Web Serial API supported");
313
+ log("Ready to test. Click 'Run Sequential Operations Test' to begin...");
314
+ }
315
+ });
package.json CHANGED
@@ -25,10 +25,12 @@
25
  "dev": "vite --mode demo",
26
  "dev:vanilla": "vite --mode vanilla",
27
  "dev:lib": "vite --mode lib",
 
28
  "build": "pnpm run build:cli",
29
  "build:cli": "tsc --project tsconfig.cli.json",
30
  "build:web": "tsc && vite build --mode lib",
31
  "build:demo": "tsc && vite build --mode demo",
 
32
  "preview": "vite preview",
33
  "cli:find-port": "tsx src/cli/index.ts find-port",
34
  "cli:calibrate": "tsx src/cli/index.ts calibrate",
 
25
  "dev": "vite --mode demo",
26
  "dev:vanilla": "vite --mode vanilla",
27
  "dev:lib": "vite --mode lib",
28
+ "test-dev": "vite --mode test",
29
  "build": "pnpm run build:cli",
30
  "build:cli": "tsc --project tsconfig.cli.json",
31
  "build:web": "tsc && vite build --mode lib",
32
  "build:demo": "tsc && vite build --mode demo",
33
+ "build:test": "tsc && vite build --mode test",
34
  "preview": "vite preview",
35
  "cli:find-port": "tsx src/cli/index.ts find-port",
36
  "cli:calibrate": "tsx src/cli/index.ts calibrate",
packages/web/README.md CHANGED
@@ -1,8 +1,16 @@
1
  # @lerobot/web
2
 
3
- Control robotics hardware directly from the browser using WebSerial API.
4
 
5
- ## Install
 
 
 
 
 
 
 
 
6
 
7
  ```bash
8
  npm install @lerobot/web
@@ -13,42 +21,442 @@ npm install @lerobot/web
13
  ```typescript
14
  import { findPort, calibrate, teleoperate } from "@lerobot/web";
15
 
16
- // 1. Find hardware
17
- const devices = await findPort();
18
- const robot = devices[0];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- // 2. Calibrate
21
- const calibration = await calibrate(robot);
22
- await calibration.result;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- // 3. Control
25
- const controller = await teleoperate(robot);
26
- controller.start();
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  ```
28
 
29
- ## API
 
 
30
 
31
- ### `findPort(options?)`
32
 
33
- Detects connected hardware. Returns `RobotConnection[]`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- ### `calibrate(robot, options?)`
 
 
 
36
 
37
- Calibrates motors and records ranges. Returns `CalibrationProcess`.
38
 
39
- ### `teleoperate(robot, options?)`
 
 
 
 
 
 
40
 
41
- Enables real-time control. Returns `TeleoperationProcess`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- ## Browser Support
44
 
45
- - Chrome/Edge 89+
46
- - Requires HTTPS or localhost
47
- - [WebSerial API](https://caniuse.com/web-serial) support
48
 
49
- ## Hardware
50
 
51
- Currently supports SO-100 follower/leader arms. More devices coming.
52
 
53
  ## License
54
 
 
1
  # @lerobot/web
2
 
3
+ Browser-native robotics control using WebSerial API. No Python dependencies required.
4
 
5
+ ## Features
6
+
7
+ - **Direct Hardware Control**: STS3215 motor communication via WebSerial API
8
+ - **Real-time Teleoperation**: Keyboard control with live motor feedback
9
+ - **Motor Calibration**: Automated homing offset and range recording
10
+ - **Cross-browser Support**: Chrome/Edge 89+ with HTTPS or localhost
11
+ - **TypeScript Native**: Full type safety and IntelliSense support
12
+
13
+ ## Installation
14
 
15
  ```bash
16
  npm install @lerobot/web
 
21
  ```typescript
22
  import { findPort, calibrate, teleoperate } from "@lerobot/web";
23
 
24
+ // 1. Find and connect to hardware
25
+ const findProcess = await findPort();
26
+ const robots = await findProcess.result;
27
+ const robot = robots[0];
28
+
29
+ // 2. Calibrate motors
30
+ const calibrationProcess = await calibrate(robot, {
31
+ onProgress: (message) => console.log(message),
32
+ onLiveUpdate: (data) => console.log("Live positions:", data),
33
+ });
34
+ const calibrationData = await calibrationProcess.result;
35
+
36
+ // 3. Start teleoperation
37
+ const teleop = await teleoperate(robot, { calibrationData });
38
+ teleop.start();
39
+ ```
40
+
41
+ ## Core API
42
+
43
+ ### `findPort(options?): Promise<FindPortProcess>`
44
+
45
+ Discovers and connects to robotics hardware using WebSerial API. Two modes: interactive (shows port dialog) and auto-connect (reconnects to known robots).
46
+
47
+ #### Interactive Mode (Default)
48
+
49
+ First-time usage or discovering new robots. Shows native browser port selection dialog.
50
+
51
+ ```typescript
52
+ // User selects robot via browser dialog
53
+ const findProcess = await findPort();
54
+ const robots = await findProcess.result; // RobotConnection[]
55
+ const robot = robots[0]; // User-selected robot
56
+
57
+ // Configure and save robot for future auto-connect
58
+ robot.robotType = "so100_follower";
59
+ robot.robotId = "my_robot_arm";
60
+
61
+ // Save to localStorage (or your storage system)
62
+ localStorage.setItem(
63
+ `robot-${robot.serialNumber}`,
64
+ JSON.stringify({
65
+ robotType: robot.robotType,
66
+ robotId: robot.robotId,
67
+ serialNumber: robot.serialNumber,
68
+ })
69
+ );
70
+ ```
71
+
72
+ #### Auto-Connect Mode
73
+
74
+ Automatically reconnects to previously configured robots without showing dialogs.
75
+
76
+ ```typescript
77
+ // Build robotConfigs from saved data
78
+ const robotConfigs = [];
79
+
80
+ // Option 1: Load from localStorage (typical web app pattern)
81
+ for (let i = 0; i < localStorage.length; i++) {
82
+ const key = localStorage.key(i);
83
+ if (key?.startsWith("robot-")) {
84
+ const saved = JSON.parse(localStorage.getItem(key)!);
85
+ robotConfigs.push({
86
+ robotType: saved.robotType,
87
+ robotId: saved.robotId,
88
+ serialNumber: saved.serialNumber,
89
+ });
90
+ }
91
+ }
92
+
93
+ // Option 2: Create manually if you know your robots
94
+ const robotConfigs = [
95
+ { robotType: "so100_follower", robotId: "left_arm", serialNumber: "USB123" },
96
+ { robotType: "so100_leader", robotId: "right_arm", serialNumber: "USB456" },
97
+ ];
98
+
99
+ // Auto-connect to all known robots
100
+ const findProcess = await findPort({
101
+ robotConfigs,
102
+ onMessage: (msg) => console.log(msg),
103
+ });
104
+
105
+ const robots = await findProcess.result;
106
+ const connectedRobots = robots.filter((r) => r.isConnected);
107
+ console.log(
108
+ `Connected to ${connectedRobots.length}/${robotConfigs.length} robots`
109
+ );
110
+ ```
111
+
112
+ #### Complete Workflow Example
113
+
114
+ ```typescript
115
+ // First run: Interactive discovery
116
+ async function discoverNewRobots() {
117
+ const findProcess = await findPort();
118
+ const robots = await findProcess.result;
119
+
120
+ for (const robot of robots) {
121
+ // Configure each robot
122
+ robot.robotType = "so100_follower"; // User choice
123
+ robot.robotId = `robot_${Date.now()}`; // User input
124
+
125
+ // Save for auto-connect
126
+ localStorage.setItem(
127
+ `robot-${robot.serialNumber}`,
128
+ JSON.stringify({
129
+ robotType: robot.robotType,
130
+ robotId: robot.robotId,
131
+ serialNumber: robot.serialNumber,
132
+ })
133
+ );
134
+ }
135
+
136
+ return robots;
137
+ }
138
+
139
+ // Subsequent runs: Auto-connect
140
+ async function reconnectSavedRobots() {
141
+ // Load saved robot configs
142
+ const robotConfigs = [];
143
+ for (let i = 0; i < localStorage.length; i++) {
144
+ const key = localStorage.key(i);
145
+ if (key?.startsWith("robot-")) {
146
+ const saved = JSON.parse(localStorage.getItem(key)!);
147
+ robotConfigs.push(saved);
148
+ }
149
+ }
150
+
151
+ if (robotConfigs.length === 0) {
152
+ return discoverNewRobots(); // No saved robots, go interactive
153
+ }
154
 
155
+ // Auto-connect to saved robots
156
+ const findProcess = await findPort({ robotConfigs });
157
+ return await findProcess.result;
158
+ }
159
+ ```
160
+
161
+ #### RobotConfig Structure
162
+
163
+ ```typescript
164
+ interface RobotConfig {
165
+ robotType: "so100_follower" | "so100_leader";
166
+ robotId: string; // Your custom identifier (e.g., "left_arm")
167
+ serialNumber: string; // Device serial number (from previous findPort)
168
+ }
169
+ ```
170
+
171
+ #### Options
172
+
173
+ - `robotConfigs?: RobotConfig[]` - Auto-connect to these known robots
174
+ - `onMessage?: (message: string) => void` - Progress messages callback
175
+
176
+ #### Returns: `FindPortProcess`
177
+
178
+ - `result: Promise<RobotConnection[]>` - Array of robot connections
179
+ - `stop(): void` - Cancel discovery process
180
+
181
+ ---
182
+
183
+ ### `calibrate(robot, options?): Promise<CalibrationProcess>`
184
+
185
+ Calibrates motor homing offsets and records range of motion.
186
+
187
+ ```typescript
188
+ const calibrationProcess = await calibrate(robot, {
189
+ onProgress: (message) => {
190
+ console.log(message); // "⚙️ Setting motor homing offsets"
191
+ },
192
+ onLiveUpdate: (data) => {
193
+ // Real-time motor positions during range recording
194
+ Object.entries(data).forEach(([motor, info]) => {
195
+ console.log(`${motor}: ${info.current} (range: ${info.range})`);
196
+ });
197
+ },
198
+ });
199
+
200
+ // Move robot through full range of motion...
201
+ // Press any key when done
202
+
203
+ const calibrationData = await calibrationProcess.result;
204
+ // Save calibration data to localStorage or file
205
+ ```
206
+
207
+ #### Parameters
208
+
209
+ - `robot: RobotConnection` - Connected robot from `findPort()`
210
+ - `options?: CalibrationOptions`
211
+ - `onProgress?: (message: string) => void` - Progress messages
212
+ - `onLiveUpdate?: (data: LiveCalibrationData) => void` - Real-time position updates
213
+
214
+ #### Returns: `CalibrationProcess`
215
 
216
+ - `result: Promise<WebCalibrationResults>` - Calibration data (Python-compatible format)
217
+ - `stop(): void` - Stop calibration process
218
+
219
+ #### Calibration Data Format
220
+
221
+ ```typescript
222
+ {
223
+ "shoulder_pan": {
224
+ "id": 1,
225
+ "drive_mode": 0,
226
+ "homing_offset": 47,
227
+ "range_min": 985,
228
+ "range_max": 3085
229
+ },
230
+ // ... other motors
231
+ }
232
  ```
233
 
234
+ ---
235
+
236
+ ### `teleoperate(robot, options?): Promise<TeleoperationProcess>`
237
 
238
+ Enables real-time robot control with keyboard input and programmatic movement.
239
 
240
+ ```typescript
241
+ const teleop = await teleoperate(robot, {
242
+ calibrationData: savedCalibrationData, // From calibrate()
243
+ onStateUpdate: (state) => {
244
+ console.log(`Active: ${state.isActive}`);
245
+ console.log(`Motors:`, state.motorConfigs);
246
+ },
247
+ });
248
+
249
+ // Start keyboard control
250
+ teleop.start();
251
+
252
+ // Programmatic control
253
+ await teleop.moveMotor("shoulder_pan", 2048);
254
+ await teleop.setMotorPositions({
255
+ shoulder_pan: 2048,
256
+ elbow_flex: 1500,
257
+ });
258
+
259
+ // Stop when done
260
+ teleop.stop();
261
+ ```
262
+
263
+ #### Parameters
264
 
265
+ - `robot: RobotConnection` - Connected robot from `findPort()`
266
+ - `options?: TeleoperationOptions`
267
+ - `calibrationData?: { [motorName: string]: any }` - Calibration data from `calibrate()`
268
+ - `onStateUpdate?: (state: TeleoperationState) => void` - State change callback
269
 
270
+ #### Returns: `TeleoperationProcess`
271
 
272
+ - `start(): void` - Begin keyboard teleoperation
273
+ - `stop(): void` - Stop teleoperation and clear key states
274
+ - `updateKeyState(key: string, pressed: boolean): void` - Manual key state control
275
+ - `getState(): TeleoperationState` - Current state and motor positions
276
+ - `moveMotor(motorName: string, position: number): Promise<boolean>` - Move single motor
277
+ - `setMotorPositions(positions: { [motorName: string]: number }): Promise<boolean>` - Move multiple motors
278
+ - `disconnect(): Promise<void>` - Stop and disconnect
279
 
280
+ #### Keyboard Controls (SO-100)
281
+
282
+ ```
283
+ Arrow Keys: Shoulder pan/lift
284
+ WASD: Elbow flex, wrist flex
285
+ Q/E: Wrist roll
286
+ O/C: Gripper open/close
287
+ Escape: Emergency stop
288
+ ```
289
+
290
+ ---
291
+
292
+ ### `isWebSerialSupported(): boolean`
293
+
294
+ Checks if WebSerial API is available in the current browser.
295
+
296
+ ```typescript
297
+ if (!isWebSerialSupported()) {
298
+ console.log("Please use Chrome/Edge 89+ with HTTPS or localhost");
299
+ return;
300
+ }
301
+ ```
302
+
303
+ ---
304
+
305
+ ### `releaseMotors(robot, motorIds?): Promise<void>`
306
+
307
+ Releases motor torque so robot can be moved freely by hand.
308
+
309
+ ```typescript
310
+ // Release all motors for calibration
311
+ await releaseMotors(robot);
312
+
313
+ // Release specific motors only
314
+ await releaseMotors(robot, [1, 2, 3]); // First 3 motors
315
+ ```
316
+
317
+ #### Parameters
318
+
319
+ - `robot: RobotConnection` - Connected robot
320
+ - `motorIds?: number[]` - Specific motor IDs (default: all motors for robot type)
321
+
322
+ ## Types
323
+
324
+ ### `RobotConnection`
325
+
326
+ ```typescript
327
+ interface RobotConnection {
328
+ port: SerialPort; // WebSerial port object
329
+ name: string; // Display name
330
+ isConnected: boolean; // Connection status
331
+ robotType?: "so100_follower" | "so100_leader";
332
+ robotId?: string; // User-defined ID
333
+ serialNumber: string; // Device serial number
334
+ error?: string; // Error message if failed
335
+ }
336
+ ```
337
+
338
+ ### `WebCalibrationResults`
339
+
340
+ ```typescript
341
+ interface WebCalibrationResults {
342
+ [motorName: string]: {
343
+ id: number; // Motor ID (1-6)
344
+ drive_mode: number; // Drive mode (0 for SO-100)
345
+ homing_offset: number; // Calculated offset
346
+ range_min: number; // Minimum position
347
+ range_max: number; // Maximum position
348
+ };
349
+ }
350
+ ```
351
+
352
+ ### `TeleoperationState`
353
+
354
+ ```typescript
355
+ interface TeleoperationState {
356
+ isActive: boolean; // Whether teleoperation is running
357
+ motorConfigs: MotorConfig[]; // Current motor positions and limits
358
+ lastUpdate: number; // Timestamp of last update
359
+ keyStates: { [key: string]: { pressed: boolean; timestamp: number } };
360
+ }
361
+ ```
362
+
363
+ ### `MotorConfig`
364
+
365
+ ```typescript
366
+ interface MotorConfig {
367
+ id: number; // Motor ID
368
+ name: string; // Motor name (e.g., "shoulder_pan")
369
+ currentPosition: number; // Current position
370
+ minPosition: number; // Position limit minimum
371
+ maxPosition: number; // Position limit maximum
372
+ }
373
+ ```
374
+
375
+ ### `RobotConfig`
376
+
377
+ ```typescript
378
+ interface RobotConfig {
379
+ robotType: "so100_follower" | "so100_leader";
380
+ robotId: string; // Your custom identifier (e.g., "left_arm")
381
+ serialNumber: string; // Device serial number (from previous findPort)
382
+ }
383
+ ```
384
+
385
+ ## Advanced Usage
386
+
387
+ ### Custom Motor Communication
388
+
389
+ ```typescript
390
+ import {
391
+ WebSerialPortWrapper,
392
+ readMotorPosition,
393
+ writeMotorPosition,
394
+ } from "@lerobot/web";
395
+
396
+ const port = new WebSerialPortWrapper(robotConnection.port);
397
+ await port.initialize();
398
+
399
+ // Read motor position directly
400
+ const position = await readMotorPosition(port, 1);
401
+
402
+ // Write motor position directly
403
+ await writeMotorPosition(port, 1, 2048);
404
+ ```
405
+
406
+ ### Robot Configuration
407
+
408
+ ```typescript
409
+ import { createSO100Config, SO100_KEYBOARD_CONTROLS } from "@lerobot/web";
410
+
411
+ const config = createSO100Config("so100_follower");
412
+ console.log("Motor names:", config.motorNames);
413
+ console.log("Motor IDs:", config.motorIds);
414
+ console.log("Keyboard controls:", SO100_KEYBOARD_CONTROLS);
415
+ ```
416
+
417
+ ### Low-Level Motor Control
418
+
419
+ ```typescript
420
+ import { releaseMotorsLowLevel } from "@lerobot/web";
421
+
422
+ const port = new WebSerialPortWrapper(robotConnection.port);
423
+ await port.initialize();
424
+
425
+ // Release specific motors at low level
426
+ await releaseMotorsLowLevel(port, [1, 2, 3]);
427
+ ```
428
+
429
+ ## Error Handling
430
+
431
+ ```typescript
432
+ try {
433
+ const findProcess = await findPort();
434
+ const robots = await findProcess.result;
435
+
436
+ if (robots.length === 0) {
437
+ throw new Error("No robots found");
438
+ }
439
+
440
+ const robot = robots[0];
441
+ if (!robot.isConnected) {
442
+ throw new Error(`Failed to connect: ${robot.error}`);
443
+ }
444
+
445
+ // Proceed with calibration/teleoperation
446
+ } catch (error) {
447
+ console.error("Robot connection failed:", error.message);
448
+ }
449
+ ```
450
 
451
+ ## Browser Requirements
452
 
453
+ - **Chrome/Edge 89+** with WebSerial API support
454
+ - **HTTPS or localhost** (required for WebSerial API)
455
+ - **User gesture** required for initial port selection
456
 
457
+ ## Hardware Support
458
 
459
+ Currently supports SO-100 follower and leader arms with STS3215 motors. More devices coming soon.
460
 
461
  ## License
462
 
packages/web/src/calibrate.ts CHANGED
@@ -218,10 +218,3 @@ export async function calibrate(
218
  result: resultPromise,
219
  };
220
  }
221
-
222
- /**
223
- * Check if Web Serial API is supported
224
- */
225
- export function isWebSerialSupported(): boolean {
226
- return "serial" in navigator;
227
- }
 
218
  result: resultPromise,
219
  };
220
  }
 
 
 
 
 
 
 
packages/web/src/find_port.ts CHANGED
@@ -1,6 +1,7 @@
1
  /**
2
- * Browser implementation of find_port using WebSerial API
3
- * Clean API with native dialogs and auto-connect modes
 
4
  *
5
  * Usage Examples:
6
  *
@@ -30,6 +31,10 @@
30
 
31
  import { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js";
32
  import { readMotorPosition } from "./utils/motor-communication.js";
 
 
 
 
33
  import type {
34
  RobotConnection,
35
  RobotConfig,
@@ -39,34 +44,131 @@ import type {
39
  Serial,
40
  FindPortOptions,
41
  FindPortProcess,
 
42
  } from "./types/port-discovery.js";
43
 
44
- declare global {
45
- interface Navigator {
46
- serial: Serial;
 
 
 
 
47
  }
 
48
  }
49
 
50
  /**
51
- * Check if WebSerial API is available
52
  */
53
- function isWebSerialSupported(): boolean {
54
- return "serial" in navigator;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
  /**
58
- * Get display name for a port
59
  */
60
- function getPortDisplayName(port: SerialPort): string {
61
- const info = port.getInfo();
62
- if (info.usbVendorId && info.usbProductId) {
63
- return `USB Device (${info.usbVendorId}:${info.usbProductId})`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
- return "Serial Device";
66
  }
67
 
68
  /**
69
- * Interactive mode: Show native dialog for port selection
70
  */
71
  async function findPortInteractive(
72
  options: FindPortOptions
@@ -76,17 +178,37 @@ async function findPortInteractive(
76
  onMessage?.("Opening port selection dialog...");
77
 
78
  try {
79
- // Use native browser dialog - much better UX than port diffing!
80
  const port = await navigator.serial.requestPort();
81
-
82
- // Open the port
83
  await port.open({ baudRate: 1000000 });
84
 
85
  const portName = getPortDisplayName(port);
86
  onMessage?.(`✅ Connected to ${portName}`);
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  // Return unified RobotConnection object in array (consistent API)
89
- // In interactive mode, user will need to specify robot details separately
90
  return [
91
  {
92
  port,
@@ -94,7 +216,8 @@ async function findPortInteractive(
94
  isConnected: true,
95
  robotType: "so100_follower", // Default, user can change
96
  robotId: "interactive_robot",
97
- serialNumber: `interactive_${Date.now()}`,
 
98
  },
99
  ];
100
  } catch (error) {
@@ -127,19 +250,26 @@ async function findPortAutoConnect(
127
  const availablePorts = await navigator.serial.getPorts();
128
  onMessage?.(`Found ${availablePorts.length} available port(s)`);
129
 
130
- for (const config of robotConfigs) {
131
- onMessage?.(`Connecting to ${config.robotId} (${config.serialNumber})...`);
 
 
 
 
 
132
 
133
- let connected = false;
134
- let matchedPort: SerialPort | null = null;
135
- let error: string | undefined;
 
 
 
 
 
 
136
 
137
- try {
138
- // For now, we'll try each available port and see if we can connect
139
- // In a future enhancement, we could match by actual serial number reading
140
- for (const port of availablePorts) {
141
  try {
142
- // Try to open and use this port
143
  const wasOpen = port.readable !== null;
144
  if (!wasOpen) {
145
  await port.open({ baudRate: 1000000 });
@@ -154,42 +284,72 @@ async function findPortAutoConnect(
154
 
155
  // If we can read a position, this is likely a working robot port
156
  if (testPosition !== null) {
157
- matchedPort = port;
158
- connected = true;
159
- onMessage?.(`✅ Connected to ${config.robotId}`);
160
- break;
 
 
 
 
 
 
 
161
  } else {
162
  throw new Error("No motor response - not a robot port");
163
  }
164
- } catch (portError) {
165
- // This port didn't work, try next one
166
- console.log(
167
- `Port ${getPortDisplayName(port)} didn't match ${config.robotId}:`,
168
- portError
 
 
169
  );
170
- continue;
171
- }
172
- }
173
 
174
- if (!connected) {
175
- error = `No matching port found for ${config.robotId} (${config.serialNumber})`;
176
- onMessage?.(`❌ ${error}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
- } catch (err) {
179
- error = err instanceof Error ? err.message : "Unknown error";
180
- onMessage?.(`❌ Failed to connect to ${config.robotId}: ${error}`);
 
181
  }
 
182
 
183
- // Add result (successful or failed)
184
- results.push({
185
- port: matchedPort!,
186
- name: matchedPort ? getPortDisplayName(matchedPort) : "Unknown Port",
187
- isConnected: connected,
188
- robotType: config.robotType,
189
- robotId: config.robotId,
190
- serialNumber: config.serialNumber,
191
- error,
192
- });
 
 
 
 
 
 
 
193
  }
194
 
195
  const successCount = results.filter((r) => r.isConnected).length;
 
1
  /**
2
+ * Browser implementation of find_port using WebSerial + WebUSB APIs
3
+ * WebSerial: Communication with device
4
+ * WebUSB: Device identification and serial numbers
5
  *
6
  * Usage Examples:
7
  *
 
31
 
32
  import { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js";
33
  import { readMotorPosition } from "./utils/motor-communication.js";
34
+ import {
35
+ isWebSerialSupported,
36
+ isWebUSBSupported,
37
+ } from "./utils/browser-support.js";
38
  import type {
39
  RobotConnection,
40
  RobotConfig,
 
44
  Serial,
45
  FindPortOptions,
46
  FindPortProcess,
47
+ USBDevice,
48
  } from "./types/port-discovery.js";
49
 
50
+ /**
51
+ * Get display name for a port
52
+ */
53
+ function getPortDisplayName(port: SerialPort): string {
54
+ const info = port.getInfo();
55
+ if (info.usbVendorId && info.usbProductId) {
56
+ return `USB Device (${info.usbVendorId}:${info.usbProductId})`;
57
  }
58
+ return "Serial Device";
59
  }
60
 
61
  /**
62
+ * Request USB device for metadata and serial number extraction
63
  */
64
+ async function requestUSBDeviceMetadata(): Promise<{
65
+ serialNumber: string;
66
+ usbMetadata: RobotConnection["usbMetadata"];
67
+ }> {
68
+ try {
69
+ // Request USB device access for metadata (no filters - accept any device)
70
+ const usbDevice = await navigator.usb.requestDevice({
71
+ filters: [], // No filtering - let user choose any device
72
+ });
73
+
74
+ const serialNumber =
75
+ usbDevice.serialNumber ||
76
+ `${usbDevice.vendorId}-${usbDevice.productId}-${Date.now()}`;
77
+
78
+ const usbMetadata = {
79
+ vendorId: `0x${usbDevice.vendorId.toString(16).padStart(4, "0")}`,
80
+ productId: `0x${usbDevice.productId.toString(16).padStart(4, "0")}`,
81
+ serialNumber: usbDevice.serialNumber || "Generated ID",
82
+ manufacturerName: usbDevice.manufacturerName || "Unknown",
83
+ productName: usbDevice.productName || "Unknown",
84
+ usbVersionMajor: usbDevice.usbVersionMajor,
85
+ usbVersionMinor: usbDevice.usbVersionMinor,
86
+ deviceClass: usbDevice.deviceClass,
87
+ deviceSubclass: usbDevice.deviceSubclass,
88
+ deviceProtocol: usbDevice.deviceProtocol,
89
+ };
90
+
91
+ return { serialNumber, usbMetadata };
92
+ } catch (usbError) {
93
+ console.log("⚠️ WebUSB request failed, generating fallback ID:", usbError);
94
+ // Generate a fallback unique ID if WebUSB fails
95
+ const serialNumber = `fallback-${Date.now()}-${Math.random()
96
+ .toString(36)
97
+ .substr(2, 9)}`;
98
+
99
+ const usbMetadata = {
100
+ vendorId: "Unknown",
101
+ productId: "Unknown",
102
+ serialNumber: serialNumber,
103
+ manufacturerName: "WebUSB Not Available",
104
+ productName: "Check browser WebUSB support",
105
+ };
106
+
107
+ return { serialNumber, usbMetadata };
108
+ }
109
  }
110
 
111
  /**
112
+ * Get USB device metadata for already permitted devices
113
  */
114
+ async function getStoredUSBDeviceMetadata(port: SerialPort): Promise<{
115
+ serialNumber: string;
116
+ usbMetadata?: RobotConnection["usbMetadata"];
117
+ }> {
118
+ try {
119
+ if (!isWebUSBSupported()) {
120
+ throw new Error("WebUSB not supported");
121
+ }
122
+
123
+ // Get already permitted USB devices
124
+ const usbDevices = await navigator.usb.getDevices();
125
+ const portInfo = port.getInfo();
126
+
127
+ // Try to find matching USB device by vendor/product ID
128
+ const matchingDevice = usbDevices.find(
129
+ (device) =>
130
+ device.vendorId === portInfo.usbVendorId &&
131
+ device.productId === portInfo.usbProductId
132
+ );
133
+
134
+ if (matchingDevice) {
135
+ const serialNumber =
136
+ matchingDevice.serialNumber ||
137
+ `${matchingDevice.vendorId}-${matchingDevice.productId}-${Date.now()}`;
138
+
139
+ const usbMetadata = {
140
+ vendorId: `0x${matchingDevice.vendorId.toString(16).padStart(4, "0")}`,
141
+ productId: `0x${matchingDevice.productId
142
+ .toString(16)
143
+ .padStart(4, "0")}`,
144
+ serialNumber: matchingDevice.serialNumber || "Generated ID",
145
+ manufacturerName: matchingDevice.manufacturerName || "Unknown",
146
+ productName: matchingDevice.productName || "Unknown",
147
+ usbVersionMajor: matchingDevice.usbVersionMajor,
148
+ usbVersionMinor: matchingDevice.usbVersionMinor,
149
+ deviceClass: matchingDevice.deviceClass,
150
+ deviceSubclass: matchingDevice.deviceSubclass,
151
+ deviceProtocol: matchingDevice.deviceProtocol,
152
+ };
153
+
154
+ console.log("✅ Restored USB metadata for port:", serialNumber);
155
+ return { serialNumber, usbMetadata };
156
+ }
157
+
158
+ throw new Error("No matching USB device found");
159
+ } catch (usbError) {
160
+ console.log("⚠️ Could not restore USB metadata:", usbError);
161
+ // Generate fallback if no USB metadata available
162
+ const serialNumber = `fallback-${Date.now()}-${Math.random()
163
+ .toString(36)
164
+ .substr(2, 9)}`;
165
+
166
+ return { serialNumber };
167
  }
 
168
  }
169
 
170
  /**
171
+ * Interactive mode: Show native dialogs for port + device selection
172
  */
173
  async function findPortInteractive(
174
  options: FindPortOptions
 
178
  onMessage?.("Opening port selection dialog...");
179
 
180
  try {
181
+ // Step 1: Request Web Serial port
182
  const port = await navigator.serial.requestPort();
 
 
183
  await port.open({ baudRate: 1000000 });
184
 
185
  const portName = getPortDisplayName(port);
186
  onMessage?.(`✅ Connected to ${portName}`);
187
 
188
+ // Step 2: Request WebUSB device for metadata (if supported)
189
+ let serialNumber: string;
190
+ let usbMetadata: RobotConnection["usbMetadata"];
191
+
192
+ if (isWebUSBSupported()) {
193
+ onMessage?.("📱 Requesting device identification...");
194
+ const usbData = await requestUSBDeviceMetadata();
195
+ serialNumber = usbData.serialNumber;
196
+ usbMetadata = usbData.usbMetadata;
197
+ onMessage?.(`🆔 Device ID: ${serialNumber}`);
198
+ } else {
199
+ onMessage?.("⚠️ WebUSB not supported, using fallback ID");
200
+ const fallbackId = `no-usb-${Date.now()}`;
201
+ serialNumber = fallbackId;
202
+ usbMetadata = {
203
+ vendorId: "Unknown",
204
+ productId: "Unknown",
205
+ serialNumber: fallbackId,
206
+ manufacturerName: "WebUSB Not Supported",
207
+ productName: "Browser limitation",
208
+ };
209
+ }
210
+
211
  // Return unified RobotConnection object in array (consistent API)
 
212
  return [
213
  {
214
  port,
 
216
  isConnected: true,
217
  robotType: "so100_follower", // Default, user can change
218
  robotId: "interactive_robot",
219
+ serialNumber,
220
+ usbMetadata,
221
  },
222
  ];
223
  } catch (error) {
 
250
  const availablePorts = await navigator.serial.getPorts();
251
  onMessage?.(`Found ${availablePorts.length} available port(s)`);
252
 
253
+ // For each available port, try to restore USB metadata and match with configs
254
+ for (const port of availablePorts) {
255
+ try {
256
+ // Get USB device metadata for this port
257
+ const { serialNumber, usbMetadata } = await getStoredUSBDeviceMetadata(
258
+ port
259
+ );
260
 
261
+ // Find matching robot config by serial number
262
+ const matchingConfig = robotConfigs.find(
263
+ (config) => config.serialNumber === serialNumber
264
+ );
265
+
266
+ if (matchingConfig) {
267
+ onMessage?.(
268
+ `Connecting to ${matchingConfig.robotId} (${serialNumber})...`
269
+ );
270
 
 
 
 
 
271
  try {
272
+ // Try to open the port
273
  const wasOpen = port.readable !== null;
274
  if (!wasOpen) {
275
  await port.open({ baudRate: 1000000 });
 
284
 
285
  // If we can read a position, this is likely a working robot port
286
  if (testPosition !== null) {
287
+ onMessage?.(`✅ Connected to ${matchingConfig.robotId}`);
288
+
289
+ results.push({
290
+ port,
291
+ name: getPortDisplayName(port),
292
+ isConnected: true,
293
+ robotType: matchingConfig.robotType,
294
+ robotId: matchingConfig.robotId,
295
+ serialNumber,
296
+ usbMetadata,
297
+ });
298
  } else {
299
  throw new Error("No motor response - not a robot port");
300
  }
301
+ } catch (connectionError) {
302
+ onMessage?.(
303
+ `❌ Failed to connect to ${matchingConfig.robotId}: ${
304
+ connectionError instanceof Error
305
+ ? connectionError.message
306
+ : connectionError
307
+ }`
308
  );
 
 
 
309
 
310
+ results.push({
311
+ port,
312
+ name: getPortDisplayName(port),
313
+ isConnected: false,
314
+ robotType: matchingConfig.robotType,
315
+ robotId: matchingConfig.robotId,
316
+ serialNumber,
317
+ usbMetadata,
318
+ error:
319
+ connectionError instanceof Error
320
+ ? connectionError.message
321
+ : "Unknown error",
322
+ });
323
+ }
324
+ } else {
325
+ console.log(
326
+ `Port with serial ${serialNumber} not in requested configs, skipping`
327
+ );
328
  }
329
+ } catch (metadataError) {
330
+ console.log(`Failed to get metadata for port:`, metadataError);
331
+ // Skip this port if we can't get metadata
332
+ continue;
333
  }
334
+ }
335
 
336
+ // Handle robots that weren't found
337
+ for (const config of robotConfigs) {
338
+ const found = results.some((r) => r.serialNumber === config.serialNumber);
339
+ if (!found) {
340
+ onMessage?.(
341
+ `❌ Robot ${config.robotId} (${config.serialNumber}) not found`
342
+ );
343
+ results.push({
344
+ port: null as any, // Will not be used since isConnected = false
345
+ name: "Not Found",
346
+ isConnected: false,
347
+ robotType: config.robotType,
348
+ robotId: config.robotId,
349
+ serialNumber: config.serialNumber,
350
+ error: `Device with serial number ${config.serialNumber} not found`,
351
+ });
352
+ }
353
  }
354
 
355
  const successCount = results.filter((r) => r.isConnected).length;
packages/web/src/index.ts CHANGED
@@ -6,11 +6,15 @@
6
  */
7
 
8
  // Core functions
9
- export { calibrate, isWebSerialSupported } from "./calibrate.js";
10
  export { teleoperate } from "./teleoperate.js";
11
  export { findPort } from "./find_port.js";
12
 
13
- console.log("asdfasdfasdfasdf");
 
 
 
 
14
 
15
  // Types
16
  export type {
 
6
  */
7
 
8
  // Core functions
9
+ export { calibrate } from "./calibrate.js";
10
  export { teleoperate } from "./teleoperate.js";
11
  export { findPort } from "./find_port.js";
12
 
13
+ // Browser support utilities
14
+ export {
15
+ isWebSerialSupported,
16
+ isWebUSBSupported,
17
+ } from "./utils/browser-support.js";
18
 
19
  // Types
20
  export type {
packages/web/src/teleoperate.ts CHANGED
@@ -92,7 +92,7 @@ export class WebTeleoperationController {
92
  // Movement parameters
93
  private readonly STEP_SIZE = 8;
94
  private readonly UPDATE_RATE = 60; // 60 FPS
95
- private readonly KEY_TIMEOUT = 600; // ms - longer than browser keyboard repeat delay (~500ms)
96
 
97
  constructor(
98
  port: MotorCommunicationPort,
 
92
  // Movement parameters
93
  private readonly STEP_SIZE = 8;
94
  private readonly UPDATE_RATE = 60; // 60 FPS
95
+ private readonly KEY_TIMEOUT = 10000; // ms - very long timeout (10 seconds) for virtual buttons
96
 
97
  constructor(
98
  port: MotorCommunicationPort,
packages/web/src/types/port-discovery.ts CHANGED
@@ -26,6 +26,36 @@ export interface SerialPortFilter {
26
  usbProductId?: number;
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  /**
30
  * Options for findPort function
31
  */
 
26
  usbProductId?: number;
27
  }
28
 
29
+ /**
30
+ * WebUSB API type definitions
31
+ */
32
+ export interface USBDevice {
33
+ vendorId: number;
34
+ productId: number;
35
+ serialNumber?: string;
36
+ manufacturerName?: string;
37
+ productName?: string;
38
+ usbVersionMajor: number;
39
+ usbVersionMinor: number;
40
+ deviceClass: number;
41
+ deviceSubclass: number;
42
+ deviceProtocol: number;
43
+ }
44
+
45
+ export interface USBDeviceFilter {
46
+ vendorId?: number;
47
+ productId?: number;
48
+ }
49
+
50
+ export interface USBDeviceRequestOptions {
51
+ filters: USBDeviceFilter[];
52
+ }
53
+
54
+ export interface USB {
55
+ getDevices(): Promise<USBDevice[]>;
56
+ requestDevice(options: USBDeviceRequestOptions): Promise<USBDevice>;
57
+ }
58
+
59
  /**
60
  * Options for findPort function
61
  */
packages/web/src/types/robot-connection.ts CHANGED
@@ -3,6 +3,8 @@
3
  * These types are shared between findPort, calibrate, teleoperate, and other modules
4
  */
5
 
 
 
6
  /**
7
  * Type definitions for WebSerial API (not yet in all TypeScript libs)
8
  */
@@ -39,6 +41,7 @@ export interface RobotConnection {
39
  robotId?: string; // Optional until user configures
40
  serialNumber: string; // Always required for identification
41
  error?: string; // Error message if connection failed
 
42
  usbMetadata?: {
43
  // USB device information
44
  vendorId: string;
 
3
  * These types are shared between findPort, calibrate, teleoperate, and other modules
4
  */
5
 
6
+ import type { RobotHardwareConfig } from "./robot-config.js";
7
+
8
  /**
9
  * Type definitions for WebSerial API (not yet in all TypeScript libs)
10
  */
 
41
  robotId?: string; // Optional until user configures
42
  serialNumber: string; // Always required for identification
43
  error?: string; // Error message if connection failed
44
+ config?: RobotHardwareConfig; // Robot configuration (motorIds, controls, etc.) - set when robotType is configured
45
  usbMetadata?: {
46
  // USB device information
47
  vendorId: string;
packages/web/src/utils/browser-support.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Browser API support detection utilities
3
+ * Centralized support checking to avoid duplication across modules
4
+ */
5
+
6
+ import type { Serial, USB } from "../types/port-discovery.js";
7
+
8
+ declare global {
9
+ interface Navigator {
10
+ serial: Serial;
11
+ usb: USB;
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Check if Web Serial API is available in the current browser
17
+ * @returns true if Web Serial is supported, false otherwise
18
+ */
19
+ export function isWebSerialSupported(): boolean {
20
+ return "serial" in navigator && typeof navigator.serial !== "undefined";
21
+ }
22
+
23
+ /**
24
+ * Check if WebUSB API is available in the current browser
25
+ * @returns true if WebUSB is supported, false otherwise
26
+ */
27
+ export function isWebUSBSupported(): boolean {
28
+ return "usb" in navigator && typeof navigator.usb !== "undefined";
29
+ }
packages/web/src/utils/motor-communication.ts CHANGED
@@ -247,14 +247,14 @@ export async function releaseMotor(
247
  }
248
 
249
  /**
250
- * Release motors (motors can be moved freely - perfect for calibration)
251
  */
252
- export async function releaseMotors(
253
  port: MotorCommunicationPort,
254
  motorIds: number[]
255
  ): Promise<void> {
256
  for (const motorId of motorIds) {
257
- await releaseMotor(port, motorId);
258
  // Small delay between motors
259
  await new Promise((resolve) =>
260
  setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY)
@@ -263,14 +263,14 @@ export async function releaseMotors(
263
  }
264
 
265
  /**
266
- * Lock motors (motors will hold their positions - perfect after calibration)
267
  */
268
- export async function lockMotors(
269
  port: MotorCommunicationPort,
270
  motorIds: number[]
271
  ): Promise<void> {
272
  for (const motorId of motorIds) {
273
- await lockMotor(port, motorId);
274
  // Small delay between motors
275
  await new Promise((resolve) =>
276
  setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY)
 
247
  }
248
 
249
  /**
250
+ * Lock motors (motors will hold their positions - perfect after calibration)
251
  */
252
+ export async function lockMotors(
253
  port: MotorCommunicationPort,
254
  motorIds: number[]
255
  ): Promise<void> {
256
  for (const motorId of motorIds) {
257
+ await lockMotor(port, motorId);
258
  // Small delay between motors
259
  await new Promise((resolve) =>
260
  setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY)
 
263
  }
264
 
265
  /**
266
+ * Release motors (motors can be moved freely - perfect for calibration)
267
  */
268
+ export async function releaseMotors(
269
  port: MotorCommunicationPort,
270
  motorIds: number[]
271
  ): Promise<void> {
272
  for (const motorId of motorIds) {
273
+ await releaseMotor(port, motorId);
274
  // Small delay between motors
275
  await new Promise((resolve) =>
276
  setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY)
vite.config.ts CHANGED
@@ -54,6 +54,27 @@ export default defineConfig(({ mode }) => {
54
  };
55
  }
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  if (mode === "lib") {
58
  // Library mode - core library without any demo UI
59
  return {
 
54
  };
55
  }
56
 
57
+ if (mode === "test") {
58
+ // Test mode - sequential operations test
59
+ return {
60
+ ...baseConfig,
61
+ server: {
62
+ open: "/examples/test-sequential-operations.html",
63
+ },
64
+ build: {
65
+ outDir: "dist/test",
66
+ rollupOptions: {
67
+ input: {
68
+ main: resolve(
69
+ __dirname,
70
+ "examples/test-sequential-operations.html"
71
+ ),
72
+ },
73
+ },
74
+ },
75
+ };
76
+ }
77
+
78
  if (mode === "lib") {
79
  // Library mode - core library without any demo UI
80
  return {